fix(frontend): agent favorites layout (#11733)

## Changes 🏗️

<img width="800" height="744" alt="Screenshot 2026-01-09 at 16 07 08"
src="https://github.com/user-attachments/assets/034c97e2-18f3-441c-a13d-71f668ad672f"
/>

- Remove feature flag for agent favourites ( _keep it always visible_ )
- Fix the layout on the card so the ❤️ icon appears next to the `...`
menu
- Remove icons on toasts

## Checklist 📋

### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run the app locally and check the above


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Favorites now respond to the current search term and are available to
all users (no feature-flag).

* **UI/UX Improvements**
* Redesigned Favorites section with simplified header, inline agent
counts, updated spacing/dividers, and removal of skeleton placeholders.
  * Favorite button repositioned and visually simplified on agent cards.
* Toast visuals simplified by removing per-type icons and adjusting
close-button positioning.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Ubbe
2026-01-09 18:52:07 +07:00
committed by GitHub
parent 36fb1ea004
commit ec00aa951a
8 changed files with 77 additions and 87 deletions

View File

@@ -1,15 +1,17 @@
"use client";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import { Text } from "@/components/atoms/Text/Text";
import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { HeartIcon } from "@phosphor-icons/react";
import { useFavoriteAgents } from "../../hooks/useFavoriteAgents";
import { LibraryAgentCard } from "../LibraryAgentCard/LibraryAgentCard";
export function FavoritesSection() {
const isAgentFavoritingEnabled = useGetFlag(Flag.AGENT_FAVORITING);
interface Props {
searchTerm: string;
}
export function FavoritesSection({ searchTerm }: Props) {
const {
allAgents: favoriteAgents,
agentLoading: isLoading,
@@ -17,60 +19,50 @@ export function FavoritesSection() {
hasNextPage,
fetchNextPage,
isFetchingNextPage,
} = useFavoriteAgents();
} = useFavoriteAgents({ searchTerm });
// Only show this section if the feature flag is enabled
if (!isAgentFavoritingEnabled) {
return null;
}
// Don't show the section if there are no favorites
if (!isLoading && favoriteAgents.length === 0) {
if (isLoading || favoriteAgents.length === 0) {
return null;
}
return (
<div className="mb-8">
<div className="flex items-center gap-[10px] p-2 pb-[10px]">
<HeartIcon className="h-5 w-5 fill-red-500 text-red-500" />
<span className="font-poppin text-[18px] font-semibold leading-[28px] text-neutral-800">
Favorites
</span>
{!isLoading && (
<span className="font-sans text-[14px] font-normal leading-6">
{agentCount} {agentCount === 1 ? "agent" : "agents"}
</span>
)}
<div className="!mb-8">
<div className="mb-3 flex items-center gap-2 p-2">
<HeartIcon className="h-5 w-5" weight="fill" />
<div className="flex items-baseline gap-2">
<Text variant="h4">Favorites</Text>
{!isLoading && (
<Text
variant="body"
data-testid="agents-count"
className="relative bottom-px text-zinc-500"
>
{agentCount}
</Text>
)}
</div>
</div>
<div className="relative">
{isLoading ? (
<InfiniteScroll
isFetchingNextPage={isFetchingNextPage}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
loader={
<div className="flex h-8 w-full items-center justify-center">
<div className="h-6 w-6 animate-spin rounded-full border-b-2 border-t-2 border-neutral-800" />
</div>
}
>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{[...Array(4)].map((_, i) => (
<Skeleton key={i} className="h-48 w-full rounded-lg" />
{favoriteAgents.map((agent: LibraryAgent) => (
<LibraryAgentCard key={agent.id} agent={agent} />
))}
</div>
) : (
<InfiniteScroll
isFetchingNextPage={isFetchingNextPage}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
loader={
<div className="flex h-8 w-full items-center justify-center">
<div className="h-6 w-6 animate-spin rounded-full border-b-2 border-t-2 border-neutral-800" />
</div>
}
>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{favoriteAgents.map((agent: LibraryAgent) => (
<LibraryAgentCard key={agent.id} agent={agent} />
))}
</div>
</InfiniteScroll>
)}
</InfiniteScroll>
</div>
{favoriteAgents.length > 0 && <div className="mt-6 border-t pt-6" />}
{favoriteAgents.length > 0 && <div className="!mt-10 border-t" />}
</div>
);
}

View File

@@ -24,7 +24,6 @@ export function LibraryAgentCard({ agent }: Props) {
const {
isFromMarketplace,
isAgentFavoritingEnabled,
isFavorite,
profile,
creator_image_url,
@@ -37,9 +36,8 @@ export function LibraryAgentCard({ agent }: Props) {
data-agent-id={id}
className="group relative inline-flex h-[10.625rem] w-full max-w-[25rem] flex-col items-start justify-start gap-2.5 rounded-medium border border-zinc-100 bg-white transition-all duration-300 hover:shadow-md"
>
<AgentCardMenu agent={agent} />
<NextLink href={`/library/agents/${id}`} className="w-full flex-shrink-0">
<div className="flex items-center gap-2 px-4 pt-3">
<NextLink href={`/library/agents/${id}`} className="flex-shrink-0">
<div className="relative flex items-center gap-2 px-4 pt-3">
<Avatar className="h-4 w-4 rounded-full">
<AvatarImage
src={
@@ -58,13 +56,13 @@ export function LibraryAgentCard({ agent }: Props) {
{isFromMarketplace ? "FROM MARKETPLACE" : "Built by you"}
</Text>
</div>
{isAgentFavoritingEnabled && (
<FavoriteButton
isFavorite={isFavorite}
onClick={handleToggleFavorite}
/>
)}
</NextLink>
<FavoriteButton
isFavorite={isFavorite}
onClick={handleToggleFavorite}
className="absolute right-10 top-0"
/>
<AgentCardMenu agent={agent} />
<div className="flex w-full flex-1 flex-col px-4 pb-2">
<Link

View File

@@ -2,21 +2,27 @@
import { cn } from "@/lib/utils";
import { HeartIcon } from "@phosphor-icons/react";
import type { MouseEvent } from "react";
interface FavoriteButtonProps {
isFavorite: boolean;
onClick: (e: React.MouseEvent) => void;
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
className?: string;
}
export function FavoriteButton({ isFavorite, onClick }: FavoriteButtonProps) {
export function FavoriteButton({
isFavorite,
onClick,
className,
}: FavoriteButtonProps) {
return (
<button
onClick={onClick}
className={cn(
"rounded-full bg-white/90 p-2 backdrop-blur-sm transition-all duration-200",
"hover:scale-110 hover:bg-white",
"focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2",
"rounded-full p-2 transition-all duration-200",
"hover:scale-110",
!isFavorite && "opacity-0 group-hover:opacity-100",
className,
)}
aria-label={isFavorite ? "Remove from favorites" : "Add to favorites"}
>

View File

@@ -8,7 +8,6 @@ import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { okData } from "@/app/api/helpers";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { updateFavoriteInQueries } from "./helpers";
interface Props {
@@ -20,7 +19,6 @@ export function useLibraryAgentCard({ agent }: Props) {
agent;
const isFromMarketplace = Boolean(marketplace_listing);
const isAgentFavoritingEnabled = useGetFlag(Flag.AGENT_FAVORITING);
const [isFavorite, setIsFavorite] = useState(is_favorite);
const { toast } = useToast();
const queryClient = getQueryClient();
@@ -49,8 +47,6 @@ export function useLibraryAgentCard({ agent }: Props) {
e.preventDefault();
e.stopPropagation();
if (!isAgentFavoritingEnabled) return;
const newIsFavorite = !isFavorite;
setIsFavorite(newIsFavorite);
@@ -80,7 +76,6 @@ export function useLibraryAgentCard({ agent }: Props) {
return {
isFromMarketplace,
isAgentFavoritingEnabled,
isFavorite,
profile,
creator_image_url,

View File

@@ -1,13 +1,15 @@
"use client";
import {
getPaginatedTotalCount,
getPaginationNextPageNumber,
unpaginate,
} from "@/app/api/helpers";
import { useGetV2ListFavoriteLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library";
import { getPaginationNextPageNumber, unpaginate } from "@/app/api/helpers";
import { useMemo } from "react";
import { filterAgents } from "../components/LibraryAgentList/helpers";
export function useFavoriteAgents() {
interface Props {
searchTerm: string;
}
export function useFavoriteAgents({ searchTerm }: Props) {
const {
data: agentsQueryData,
fetchNextPage,
@@ -27,10 +29,16 @@ export function useFavoriteAgents() {
const allAgents = agentsQueryData
? unpaginate(agentsQueryData, "agents")
: [];
const agentCount = getPaginatedTotalCount(agentsQueryData);
const filteredAgents = useMemo(
() => filterAgents(allAgents, searchTerm),
[allAgents, searchTerm],
);
const agentCount = filteredAgents.length;
return {
allAgents,
allAgents: filteredAgents,
agentLoading,
hasNextPage,
agentCount,

View File

@@ -17,7 +17,7 @@ export default function LibraryPage() {
return (
<main className="pt-160 container min-h-screen space-y-4 pb-20 pt-16 sm:px-8 md:px-12">
<LibraryActionHeader setSearchTerm={setSearchTerm} />
<FavoritesSection />
<FavoritesSection searchTerm={searchTerm} />
<LibraryAgentList
searchTerm={searchTerm}
librarySort={librarySort}

View File

@@ -37,11 +37,3 @@ html body .toastDescription {
font-size: 0.75rem !important;
line-height: 1.25rem !important;
}
/* Position close button on the right */
/* stylelint-disable-next-line selector-pseudo-class-no-unknown */
#root [data-sonner-toast] [data-close-button="true"] {
left: unset !important;
right: -18px !important;
top: -3px !important;
}

View File

@@ -1,6 +1,5 @@
"use client";
import { CheckCircle, Info, Warning, XCircle } from "@phosphor-icons/react";
import { Toaster as SonnerToaster } from "sonner";
import styles from "./styles.module.css";
@@ -23,10 +22,10 @@ export function Toaster() {
}}
className="custom__toast"
icons={{
success: <CheckCircle className="h-4 w-4" color="#fff" weight="fill" />,
error: <XCircle className="h-4 w-4" color="#fff" weight="fill" />,
warning: <Warning className="h-4 w-4" color="#fff" weight="fill" />,
info: <Info className="h-4 w-4" color="#fff" weight="fill" />,
success: null,
error: null,
warning: null,
info: null,
}}
/>
);