From 4337b67149d571ea2b77a17a674f781d8c2b1d5b Mon Sep 17 00:00:00 2001 From: abhi1992002 Date: Sat, 24 Jan 2026 13:03:06 +0530 Subject: [PATCH] feat(library): enhance library management with folder support and UI improvements - Added functionality for managing library folders, allowing users to organize agents more effectively. - Implemented new API endpoints for creating, updating, and deleting folders, as well as moving agents between them. - Updated the UI to include folder selection and display, enhancing user experience with a tabbed interface for favorites and all agents. - Integrated drag-and-drop functionality for moving agents into folders. - Improved loading states and added animations for favorite actions. This update significantly enhances the organization and usability of the library feature, making it easier for users to manage their agents. --- autogpt_platform/frontend/package.json | 1 + autogpt_platform/frontend/pnpm-lock.yaml | 19 + .../FavoritesSection/FavoritesSection.tsx | 56 +- .../components/FlyingHeart/FlyingHeart.tsx | 66 +++ .../LibraryActionSubHeader.tsx | 2 +- .../LibraryAgentCard/LibraryAgentCard.tsx | 38 +- .../components/FavoriteButton.tsx | 45 +- .../LibraryAgentCard/useLibraryAgentCard.ts | 23 +- .../LibraryAgentList/LibraryAgentList.tsx | 133 ++++- .../LibraryAgentList/useLibraryAgentList.ts | 9 +- .../components/LibraryFolder/FolderIcon.tsx | 220 ++++++-- .../LibraryFolder/LibraryFolder.tsx | 51 +- .../LibraryFolderCreationDialog.tsx | 230 +++++++++ .../LibrarySubSection/LibrarySubSection.tsx | 17 + .../components/LibraryTabs/LibraryTabs.tsx | 134 +++++ .../context/FavoriteAnimationContext.tsx | 77 +++ .../src/app/(platform)/library/page.tsx | 47 +- .../frontend/src/app/api/openapi.json | 479 ++++++++++++++++++ 18 files changed, 1525 insertions(+), 122 deletions(-) create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/components/FlyingHeart/FlyingHeart.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolderCreationDialog/LibraryFolderCreationDialog.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySubSection/LibrarySubSection.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/components/LibraryTabs/LibraryTabs.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/library/context/FavoriteAnimationContext.tsx diff --git a/autogpt_platform/frontend/package.json b/autogpt_platform/frontend/package.json index 5988e59c90..848f3c5dd3 100644 --- a/autogpt_platform/frontend/package.json +++ b/autogpt_platform/frontend/package.json @@ -84,6 +84,7 @@ "dotenv": "17.2.3", "elliptic": "6.6.1", "embla-carousel-react": "8.6.0", + "emoji-picker-react": "4.17.1", "flatbush": "4.5.0", "framer-motion": "12.23.24", "geist": "1.5.1", diff --git a/autogpt_platform/frontend/pnpm-lock.yaml b/autogpt_platform/frontend/pnpm-lock.yaml index 468e2f312d..53e6a5382a 100644 --- a/autogpt_platform/frontend/pnpm-lock.yaml +++ b/autogpt_platform/frontend/pnpm-lock.yaml @@ -174,6 +174,9 @@ importers: embla-carousel-react: specifier: 8.6.0 version: 8.6.0(react@18.3.1) + emoji-picker-react: + specifier: 4.17.1 + version: 4.17.1(react@18.3.1) flatbush: specifier: 4.5.0 version: 4.5.0 @@ -4989,6 +4992,12 @@ packages: embla-carousel@8.6.0: resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + emoji-picker-react@4.17.1: + resolution: {integrity: sha512-DxGGPxHRcH/PnGFZEVkWSNZoFg8UO2/kikZrp/OZ8CBz5F/mKJbKLcd1anxeV8Hu1ZzY8MBuNnFG2wSJYkf1Ug==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -5366,6 +5375,9 @@ packages: resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} engines: {node: '>=18'} + flairup@1.0.0: + resolution: {integrity: sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==} + flat-cache@3.2.0: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} @@ -13743,6 +13755,11 @@ snapshots: embla-carousel@8.6.0: {} + emoji-picker-react@4.17.1(react@18.3.1): + dependencies: + flairup: 1.0.0 + react: 18.3.1 + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -14313,6 +14330,8 @@ snapshots: path-exists: 5.0.0 unicorn-magic: 0.1.0 + flairup@1.0.0: {} + flat-cache@3.2.0: dependencies: flatted: 3.3.3 diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/FavoritesSection/FavoritesSection.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/FavoritesSection/FavoritesSection.tsx index d1167cc747..a9ed3bdc14 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/FavoritesSection/FavoritesSection.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/FavoritesSection/FavoritesSection.tsx @@ -1,17 +1,25 @@ "use client"; import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; +import { LibraryAgentSort } from "@/app/api/__generated__/models/libraryAgentSort"; import { Text } from "@/components/atoms/Text/Text"; +import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll"; import { HeartIcon } from "@phosphor-icons/react"; import { useFavoriteAgents } from "../../hooks/useFavoriteAgents"; import { LibraryAgentCard } from "../LibraryAgentCard/LibraryAgentCard"; +import { LibraryTabs, Tab } from "../LibraryTabs/LibraryTabs"; +import { LibraryActionSubHeader } from "../LibraryActionSubHeader/LibraryActionSubHeader"; interface Props { searchTerm: string; + tabs: Tab[]; + activeTab: string; + onTabChange: (tabId: string) => void; + setLibrarySort: (value: LibraryAgentSort) => void; } -export function FavoritesSection({ searchTerm }: Props) { +export function FavoritesSection({ searchTerm, tabs, activeTab, onTabChange, setLibrarySort }: Props) { const { allAgents: favoriteAgents, agentLoading: isLoading, @@ -21,38 +29,26 @@ export function FavoritesSection({ searchTerm }: Props) { isFetchingNextPage, } = useFavoriteAgents({ searchTerm }); - if (isLoading || favoriteAgents.length === 0) { - return null; - } - return ( -
-
- -
- Favorites - {!isLoading && ( - - {agentCount} - - )} -
-
+ <> + + -
+ {isLoading ? ( +
+ +
+ ) : favoriteAgents.length === 0 ? ( +
+ + No favorite agents yet +
+ ) : ( -
-
- } + loader={} >
{favoriteAgents.map((agent: LibraryAgent) => ( @@ -60,9 +56,7 @@ export function FavoritesSection({ searchTerm }: Props) { ))}
-
- - {favoriteAgents.length > 0 &&
} -
+ )} + ); } diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/FlyingHeart/FlyingHeart.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/FlyingHeart/FlyingHeart.tsx new file mode 100644 index 0000000000..500b93d02d --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/FlyingHeart/FlyingHeart.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { motion, AnimatePresence } from "framer-motion"; +import { HeartIcon } from "@phosphor-icons/react"; +import { useEffect, useState } from "react"; + +interface FlyingHeartProps { + startPosition: { x: number; y: number } | null; + targetPosition: { x: number; y: number } | null; + onAnimationComplete: () => void; +} + +export function FlyingHeart({ + startPosition, + targetPosition, + onAnimationComplete, +}: FlyingHeartProps) { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (startPosition && targetPosition) { + setIsVisible(true); + } + }, [startPosition, targetPosition]); + + if (!startPosition || !targetPosition) return null; + + return ( + + {isVisible && ( + { + setIsVisible(false); + onAnimationComplete(); + }} + > + + + )} + + ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryActionSubHeader/LibraryActionSubHeader.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryActionSubHeader/LibraryActionSubHeader.tsx index edefa52911..86edb3debe 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryActionSubHeader/LibraryActionSubHeader.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryActionSubHeader/LibraryActionSubHeader.tsx @@ -13,7 +13,7 @@ export function LibraryActionSubHeader({ agentCount, setLibrarySort }: Props) { return (
- My agents + My agents ) { + e.dataTransfer.setData("application/agent-id", id); + e.dataTransfer.effectAllowed = "move"; + } const { isFromMarketplace, @@ -28,14 +40,29 @@ export function LibraryAgentCard({ agent }: Props) { profile, creator_image_url, handleToggleFavorite, - } = useLibraryAgentCard({ agent }); + } = useLibraryAgentCard({ + agent, + onFavoriteAdd: triggerFavoriteAnimation, + }); return (
+
@@ -125,6 +152,7 @@ export function LibraryAgentCard({ agent }: Props) { )}
+
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/components/FavoriteButton.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/components/FavoriteButton.tsx index df11553ec1..a003d64f59 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/components/FavoriteButton.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/components/FavoriteButton.tsx @@ -3,10 +3,12 @@ import { cn } from "@/lib/utils"; import { HeartIcon } from "@phosphor-icons/react"; import type { MouseEvent } from "react"; +import { useRef } from "react"; +import { motion, AnimatePresence } from "framer-motion"; interface FavoriteButtonProps { isFavorite: boolean; - onClick: (e: MouseEvent) => void; + onClick: (e: MouseEvent, position: { x: number; y: number }) => void; className?: string; } @@ -15,25 +17,46 @@ export function FavoriteButton({ onClick, className, }: FavoriteButtonProps) { + const buttonRef = useRef(null); + + function handleClick(e: MouseEvent) { + const rect = buttonRef.current?.getBoundingClientRect(); + const position = rect + ? { x: rect.left + rect.width / 2 - 12, y: rect.top + rect.height / 2 - 12 } + : { x: 0, y: 0 }; + onClick(e, position); + } + return ( ); } diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/useLibraryAgentCard.ts b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/useLibraryAgentCard.ts index 87e9e9e9bc..1b1feccdd4 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/useLibraryAgentCard.ts +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/useLibraryAgentCard.ts @@ -14,11 +14,11 @@ import { updateFavoriteInQueries } from "./helpers"; interface Props { agent: LibraryAgent; + onFavoriteAdd?: (position: { x: number; y: number }) => void; } -export function useLibraryAgentCard({ agent }: Props) { - const { id, name, is_favorite, creator_image_url, marketplace_listing } = - agent; +export function useLibraryAgentCard({ agent, onFavoriteAdd }: Props) { + const { id, is_favorite, creator_image_url, marketplace_listing } = agent; const isFromMarketplace = Boolean(marketplace_listing); const [isFavorite, setIsFavorite] = useState(is_favorite); @@ -49,26 +49,31 @@ export function useLibraryAgentCard({ agent }: Props) { }); } - async function handleToggleFavorite(e: React.MouseEvent) { + async function handleToggleFavorite( + e: React.MouseEvent, + position: { x: number; y: number } + ) { e.preventDefault(); e.stopPropagation(); const newIsFavorite = !isFavorite; + // Optimistic update - update UI immediately setIsFavorite(newIsFavorite); updateQueryData(newIsFavorite); + // Trigger animation immediately for adding to favorites + if (newIsFavorite && onFavoriteAdd) { + onFavoriteAdd(position); + } + try { await updateLibraryAgent({ libraryAgentId: id, data: { is_favorite: newIsFavorite }, }); - - toast({ - title: newIsFavorite ? "Added to favorites" : "Removed from favorites", - description: `${name} has been ${newIsFavorite ? "added to" : "removed from"} your favorites.`, - }); } catch { + // Revert on failure setIsFavorite(!newIsFavorite); updateQueryData(!newIsFavorite); diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/LibraryAgentList.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/LibraryAgentList.tsx index d1fa365e87..4ed1ef45bb 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/LibraryAgentList.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/LibraryAgentList.tsx @@ -6,18 +6,53 @@ import { LibraryActionSubHeader } from "../LibraryActionSubHeader/LibraryActionS import { LibraryAgentCard } from "../LibraryAgentCard/LibraryAgentCard"; import { useLibraryAgentList } from "./useLibraryAgentList"; import { LibraryFolder } from "../LibraryFolder/LibraryFolder"; +import { + useGetV2ListLibraryFolders, + usePostV2BulkMoveAgents, + getGetV2ListLibraryFoldersQueryKey, +} from "@/app/api/__generated__/endpoints/folders/folders"; +import { okData } from "@/app/api/helpers"; +import { LibrarySubSection } from "../LibrarySubSection/LibrarySubSection"; +import { useQueryClient } from "@tanstack/react-query"; +import { getGetV2ListLibraryAgentsQueryKey } from "@/app/api/__generated__/endpoints/library/library"; +import { Button } from "@/components/atoms/Button/Button"; +import { ArrowLeftIcon, HeartIcon } from "@phosphor-icons/react"; +import { Text } from "@/components/atoms/Text/Text"; +import { Tab } from "../LibraryTabs/LibraryTabs"; +import { useFavoriteAgents } from "../../hooks/useFavoriteAgents"; +import { LayoutGroup } from "framer-motion"; interface Props { searchTerm: string; librarySort: LibraryAgentSort; setLibrarySort: (value: LibraryAgentSort) => void; + selectedFolderId: string | null; + onFolderSelect: (folderId: string | null) => void; + tabs: Tab[]; + activeTab: string; + onTabChange: (tabId: string) => void; } export function LibraryAgentList({ searchTerm, librarySort, setLibrarySort, + selectedFolderId, + onFolderSelect, + tabs, + activeTab, + onTabChange, }: Props) { + const isFavoritesTab = activeTab === "favorites"; + + const allAgentsData = useLibraryAgentList({ + searchTerm, + librarySort, + folderId: selectedFolderId, + }); + + const favoriteAgentsData = useFavoriteAgents({ searchTerm }); + const { agentLoading, agentCount, @@ -25,7 +60,40 @@ export function LibraryAgentList({ hasNextPage, isFetchingNextPage, fetchNextPage, - } = useLibraryAgentList({ searchTerm, librarySort }); + } = isFavoritesTab ? favoriteAgentsData : allAgentsData; + + const { data: foldersData } = useGetV2ListLibraryFolders(undefined, { + query: { select: okData }, + }); + + const queryClient = useQueryClient(); + const { mutate: moveAgentToFolder } = usePostV2BulkMoveAgents({ + mutation: { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: getGetV2ListLibraryFoldersQueryKey(), + }); + queryClient.invalidateQueries({ + queryKey: getGetV2ListLibraryAgentsQueryKey(), + }); + }, + }, + }); + + function handleAgentDrop(agentId: string, folderId: string) { + moveAgentToFolder({ + data: { + agent_ids: [agentId], + folder_id: folderId, + }, + }); + } + + const currentFolder = selectedFolderId + ? foldersData?.folders.find((f) => f.id === selectedFolderId) + : null; + + const showFolders = !isFavoritesTab && !selectedFolderId; return ( <> @@ -33,11 +101,42 @@ export function LibraryAgentList({ agentCount={agentCount} setLibrarySort={setLibrarySort} /> -
+ {!selectedFolderId && ( + + )} + +
+ {selectedFolderId && ( +
+ + {currentFolder && ( + + {currentFolder.icon} {currentFolder.name} + + )} +
+ )} {agentLoading ? (
+ ) : isFavoritesTab && agents.length === 0 ? ( +
+ + No favorite agents yet +
) : ( } > -
- - - - - - {agents.map((agent) => ( - - ))} -
+ +
+ {showFolders && + foldersData?.folders.map((folder) => ( + onFolderSelect(folder.id)} + /> + ))} + {agents.map((agent) => ( + + ))} +
+
)}
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts index 0b102ff545..fe1cc78d62 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts @@ -13,9 +13,14 @@ import { useEffect, useRef } from "react"; interface Props { searchTerm: string; librarySort: LibraryAgentSort; + folderId?: string | null; } -export function useLibraryAgentList({ searchTerm, librarySort }: Props) { +export function useLibraryAgentList({ + searchTerm, + librarySort, + folderId, +}: Props) { const queryClient = getQueryClient(); const prevSortRef = useRef(null); @@ -31,6 +36,8 @@ export function useLibraryAgentList({ searchTerm, librarySort }: Props) { page_size: 20, search_term: searchTerm || undefined, sort_by: librarySort, + folder_id: folderId ?? undefined, + include_root_only: folderId === null ? true : undefined, }, { query: { diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolder/FolderIcon.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolder/FolderIcon.tsx index 3a0dbab363..11ab2226b3 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolder/FolderIcon.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolder/FolderIcon.tsx @@ -3,7 +3,7 @@ import { motion } from "framer-motion"; import { Text } from "@/components/atoms/Text/Text"; type FolderSize = "xs" | "sm" | "md" | "lg" | "xl"; -export type FolderColor = +export type FolderColorName = | "neutral" | "slate" | "zinc" @@ -26,6 +26,28 @@ export type FolderColor = | "pink" | "rose"; +export type FolderColor = FolderColorName | (string & {}); + +const hexToColorName: Record = { + "#3B82F6": "blue", + "#3b82f6": "blue", + "#A855F7": "purple", + "#a855f7": "purple", + "#10B981": "emerald", + "#10b981": "emerald", + "#F97316": "orange", + "#f97316": "orange", + "#EC4899": "pink", + "#ec4899": "pink", +}; + +function resolveColor(color: FolderColor | undefined): FolderColorName { + if (!color) return "blue"; + if (color in hexToColorName) return hexToColorName[color]; + if (color in colorMap) return color as FolderColorName; + return "blue"; +} + interface Props { className?: string; size?: FolderSize | number; @@ -35,7 +57,7 @@ interface Props { } const sizeMap: Record = { - xs: 0.5, + xs: 0.4, sm: 0.75, md: 1, lg: 1.25, @@ -43,30 +65,162 @@ const sizeMap: Record = { }; const colorMap: Record< - FolderColor, - { bg: string; border: string; borderLight: string; fill: string; stroke: string } + FolderColorName, + { + bg: string; + border: string; + borderLight: string; + fill: string; + stroke: string; + } > = { - neutral: { bg: "bg-neutral-300", border: "border-neutral-300", borderLight: "border-neutral-200", fill: "fill-neutral-300", stroke: "stroke-neutral-400" }, - slate: { bg: "bg-slate-300", border: "border-slate-300", borderLight: "border-slate-200", fill: "fill-slate-300", stroke: "stroke-slate-400" }, - zinc: { bg: "bg-zinc-300", border: "border-zinc-300", borderLight: "border-zinc-200", fill: "fill-zinc-300", stroke: "stroke-zinc-400" }, - stone: { bg: "bg-stone-300", border: "border-stone-300", borderLight: "border-stone-200", fill: "fill-stone-300", stroke: "stroke-stone-400" }, - red: { bg: "bg-red-300", border: "border-red-300", borderLight: "border-red-200", fill: "fill-red-300", stroke: "stroke-red-400" }, - orange: { bg: "bg-orange-200", border: "border-orange-200", borderLight: "border-orange-200", fill: "fill-orange-200", stroke: "stroke-orange-400" }, - amber: { bg: "bg-amber-200", border: "border-amber-200", borderLight: "border-amber-200", fill: "fill-amber-200", stroke: "stroke-amber-400" }, - yellow: { bg: "bg-yellow-200", border: "border-yellow-200", borderLight: "border-yellow-200", fill: "fill-yellow-200", stroke: "stroke-yellow-400" }, - lime: { bg: "bg-lime-300", border: "border-lime-300", borderLight: "border-lime-200", fill: "fill-lime-300", stroke: "stroke-lime-400" }, - green: { bg: "bg-green-200", border: "border-green-200", borderLight: "border-green-200", fill: "fill-green-200", stroke: "stroke-green-400" }, - emerald: { bg: "bg-emerald-300", border: "border-emerald-300", borderLight: "border-emerald-200", fill: "fill-emerald-300", stroke: "stroke-emerald-400" }, - teal: { bg: "bg-teal-300", border: "border-teal-300", borderLight: "border-teal-200", fill: "fill-teal-300", stroke: "stroke-teal-400" }, - cyan: { bg: "bg-cyan-300", border: "border-cyan-300", borderLight: "border-cyan-200", fill: "fill-cyan-300", stroke: "stroke-cyan-400" }, - sky: { bg: "bg-sky-300", border: "border-sky-300", borderLight: "border-sky-200", fill: "fill-sky-300", stroke: "stroke-sky-400" }, - blue: { bg: "bg-blue-300", border: "border-blue-300", borderLight: "border-blue-200", fill: "fill-blue-300", stroke: "stroke-blue-400" }, - indigo: { bg: "bg-indigo-300", border: "border-indigo-300", borderLight: "border-indigo-200", fill: "fill-indigo-300", stroke: "stroke-indigo-400" }, - violet: { bg: "bg-violet-300", border: "border-violet-300", borderLight: "border-violet-200", fill: "fill-violet-300", stroke: "stroke-violet-400" }, - purple: { bg: "bg-purple-200", border: "border-purple-200", borderLight: "border-purple-200", fill: "fill-purple-200", stroke: "stroke-purple-400" }, - fuchsia: { bg: "bg-fuchsia-300", border: "border-fuchsia-300", borderLight: "border-fuchsia-200", fill: "fill-fuchsia-300", stroke: "stroke-fuchsia-400" }, - pink: { bg: "bg-pink-300", border: "border-pink-300", borderLight: "border-pink-200", fill: "fill-pink-300", stroke: "stroke-pink-400" }, - rose: { bg: "bg-rose-300", border: "border-rose-300", borderLight: "border-rose-200", fill: "fill-rose-300", stroke: "stroke-rose-400" }, + neutral: { + bg: "bg-neutral-300", + border: "border-neutral-300", + borderLight: "border-neutral-200", + fill: "fill-neutral-300", + stroke: "stroke-neutral-400", + }, + slate: { + bg: "bg-slate-300", + border: "border-slate-300", + borderLight: "border-slate-200", + fill: "fill-slate-300", + stroke: "stroke-slate-400", + }, + zinc: { + bg: "bg-zinc-300", + border: "border-zinc-300", + borderLight: "border-zinc-200", + fill: "fill-zinc-300", + stroke: "stroke-zinc-400", + }, + stone: { + bg: "bg-stone-300", + border: "border-stone-300", + borderLight: "border-stone-200", + fill: "fill-stone-300", + stroke: "stroke-stone-400", + }, + red: { + bg: "bg-red-300", + border: "border-red-300", + borderLight: "border-red-200", + fill: "fill-red-300", + stroke: "stroke-red-400", + }, + orange: { + bg: "bg-orange-200", + border: "border-orange-200", + borderLight: "border-orange-200", + fill: "fill-orange-200", + stroke: "stroke-orange-400", + }, + amber: { + bg: "bg-amber-200", + border: "border-amber-200", + borderLight: "border-amber-200", + fill: "fill-amber-200", + stroke: "stroke-amber-400", + }, + yellow: { + bg: "bg-yellow-200", + border: "border-yellow-200", + borderLight: "border-yellow-200", + fill: "fill-yellow-200", + stroke: "stroke-yellow-400", + }, + lime: { + bg: "bg-lime-300", + border: "border-lime-300", + borderLight: "border-lime-200", + fill: "fill-lime-300", + stroke: "stroke-lime-400", + }, + green: { + bg: "bg-green-200", + border: "border-green-200", + borderLight: "border-green-200", + fill: "fill-green-200", + stroke: "stroke-green-400", + }, + emerald: { + bg: "bg-emerald-300", + border: "border-emerald-300", + borderLight: "border-emerald-200", + fill: "fill-emerald-300", + stroke: "stroke-emerald-400", + }, + teal: { + bg: "bg-teal-300", + border: "border-teal-300", + borderLight: "border-teal-200", + fill: "fill-teal-300", + stroke: "stroke-teal-400", + }, + cyan: { + bg: "bg-cyan-300", + border: "border-cyan-300", + borderLight: "border-cyan-200", + fill: "fill-cyan-300", + stroke: "stroke-cyan-400", + }, + sky: { + bg: "bg-sky-300", + border: "border-sky-300", + borderLight: "border-sky-200", + fill: "fill-sky-300", + stroke: "stroke-sky-400", + }, + blue: { + bg: "bg-blue-300", + border: "border-blue-300", + borderLight: "border-blue-200", + fill: "fill-blue-300", + stroke: "stroke-blue-400", + }, + indigo: { + bg: "bg-indigo-300", + border: "border-indigo-300", + borderLight: "border-indigo-200", + fill: "fill-indigo-300", + stroke: "stroke-indigo-400", + }, + violet: { + bg: "bg-violet-300", + border: "border-violet-300", + borderLight: "border-violet-200", + fill: "fill-violet-300", + stroke: "stroke-violet-400", + }, + purple: { + bg: "bg-purple-200", + border: "border-purple-200", + borderLight: "border-purple-200", + fill: "fill-purple-200", + stroke: "stroke-purple-400", + }, + fuchsia: { + bg: "bg-fuchsia-300", + border: "border-fuchsia-300", + borderLight: "border-fuchsia-200", + fill: "fill-fuchsia-300", + stroke: "stroke-fuchsia-400", + }, + pink: { + bg: "bg-pink-300", + border: "border-pink-300", + borderLight: "border-pink-200", + fill: "fill-pink-300", + stroke: "stroke-pink-400", + }, + rose: { + bg: "bg-rose-300", + border: "border-rose-300", + borderLight: "border-rose-200", + fill: "fill-rose-300", + stroke: "stroke-rose-400", + }, }; export function FolderIcon({ @@ -77,7 +231,8 @@ export function FolderIcon({ isOpen = false, }: Props) { const scale = typeof size === "number" ? size : sizeMap[size]; - const colors = colorMap[color]; + const resolvedColor = resolveColor(color); + const colors = colorMap[resolvedColor]; return (
- + ))}
@@ -152,7 +307,7 @@ export function FolderIcon({ style={{ transformStyle: "preserve-3d" }} > -
- {icon} + {icon}
@@ -174,7 +328,7 @@ export function FolderIcon({ } interface PageProps { - color: FolderColor; + color: FolderColorName; } function Page({ color = "blue" }: PageProps) { @@ -184,7 +338,9 @@ function Page({ color = "blue" }: PageProps) { className={`h-full w-full rounded-xl border bg-white p-4 ${colors.borderLight}`} >
- agent.json + + agent.json + {Array.from({ length: 8 }).map((_, i) => (
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolder/LibraryFolder.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolder/LibraryFolder.tsx index f321c0bd98..ef6fdbe8d1 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolder/LibraryFolder.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolder/LibraryFolder.tsx @@ -4,24 +4,24 @@ import { Text } from "@/components/atoms/Text/Text"; import { Button } from "@/components/atoms/Button/Button"; import { FolderIcon, FolderColor } from "./FolderIcon"; import { useState } from "react"; -import { - PencilSimpleIcon, - TrashIcon, - StarIcon, -} from "@phosphor-icons/react"; +import { PencilSimpleIcon, TrashIcon, HeartIcon } from "@phosphor-icons/react"; interface Props { + id: string; name: string; agentCount: number; - color: FolderColor; + color?: FolderColor; icon: string; onEdit?: () => void; onDelete?: () => void; onFavorite?: () => void; + onAgentDrop?: (agentId: string, folderId: string) => void; + onClick?: () => void; isFavorite?: boolean; } export function LibraryFolder({ + id, name, agentCount, color, @@ -29,16 +29,49 @@ export function LibraryFolder({ onEdit, onDelete, onFavorite, + onAgentDrop, + onClick, isFavorite = false, }: Props) { const [isHovered, setIsHovered] = useState(false); + const [isDragOver, setIsDragOver] = useState(false); + + function handleDragOver(e: React.DragEvent) { + if (e.dataTransfer.types.includes("application/agent-id")) { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + setIsDragOver(true); + } + } + + function handleDragLeave() { + setIsDragOver(false); + } + + function handleDrop(e: React.DragEvent) { + e.preventDefault(); + setIsDragOver(false); + const agentId = e.dataTransfer.getData("application/agent-id"); + if (agentId && onAgentDrop) { + onAgentDrop(agentId, id); + } + } return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + onClick={onClick} >
{/* Left side - Folder name and agent count */} @@ -67,7 +100,7 @@ export function LibraryFolder({ {/* Action buttons - visible on hover */}
+ + +
onSubmit(values)} + className="flex flex-col justify-center gap-4 px-1" + > + ( + + + + + + + )} + /> + + ( + + +