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 */}