diff --git a/autogpt_platform/frontend/package.json b/autogpt_platform/frontend/package.json index 848f3c5dd3..4e330a5ff4 100644 --- a/autogpt_platform/frontend/package.json +++ b/autogpt_platform/frontend/package.json @@ -32,6 +32,7 @@ "dependencies": { "@ai-sdk/react": "3.0.61", "@faker-js/faker": "10.0.0", + "@ferrucc-io/emoji-picker": "0.0.48", "@hookform/resolvers": "5.2.2", "@next/third-parties": "15.4.6", "@phosphor-icons/react": "2.1.10", @@ -84,7 +85,6 @@ "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 53e6a5382a..dd84582410 100644 --- a/autogpt_platform/frontend/pnpm-lock.yaml +++ b/autogpt_platform/frontend/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: '@faker-js/faker': specifier: 10.0.0 version: 10.0.0 + '@ferrucc-io/emoji-picker': + specifier: 0.0.48 + version: 0.0.48(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.17) '@hookform/resolvers': specifier: 5.2.2 version: 5.2.2(react-hook-form@7.66.0(react@18.3.1)) @@ -174,9 +177,6 @@ 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 @@ -1510,6 +1510,14 @@ packages: resolution: {integrity: sha512-UollFEUkVXutsaP+Vndjxar40Gs5JL2HeLcl8xO1QAjJgOdhc3OmBFWyEylS+RddWaaBiAzH+5/17PLQJwDiLw==} engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} + '@ferrucc-io/emoji-picker@0.0.48': + resolution: {integrity: sha512-DJ5u+6VLF9OK7x+S/luwrVb5CHC6W16jL5b8vBUYNpxKWSuFgyliDHVtw1SGe6+dr5RUbf8WQwPJdKZmU3Ittg==} + engines: {node: '>=18'} + peerDependencies: + react: ^18.2.0 || ^19.0.0 + react-dom: ^18.2.0 || ^19.0.0 + tailwindcss: '>=3.0.0' + '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -3117,6 +3125,10 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -3384,10 +3396,19 @@ packages: react: '>=16.8' react-dom: '>=16.8' + '@tanstack/react-virtual@3.13.18': + resolution: {integrity: sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/table-core@8.21.3': resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} + '@tanstack/virtual-core@3.13.18': + resolution: {integrity: sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==} + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -4381,6 +4402,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -4992,18 +5017,15 @@ 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==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + emojis-list@3.0.0: resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} engines: {node: '>= 4'} @@ -5375,9 +5397,6 @@ 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} @@ -5987,6 +6006,24 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jotai@2.17.1: + resolution: {integrity: sha512-TFNZZDa/0ewCLQyRC/Sq9crtixNj/Xdf/wmj9631xxMuKToVJZDbqcHIYN0OboH+7kh6P6tpIK7uKWClj86PKw==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@babel/core': '>=7.0.0' + '@babel/template': '>=7.0.0' + '@types/react': '>=17.0.0' + react: '>=17.0.0' + peerDependenciesMeta: + '@babel/core': + optional: true + '@babel/template': + optional: true + '@types/react': + optional: true + react: + optional: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -6605,6 +6642,10 @@ packages: node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-emoji@2.2.0: + resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} + engines: {node: '>=18'} + node-fetch-h2@2.3.0: resolution: {integrity: sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==} engines: {node: 4.x || >=6.0.0} @@ -7703,6 +7744,10 @@ packages: resolution: {integrity: sha512-LH7FpTAkeD+y5xQC4fzS+tFtaNlvt3Ib1zKzvhjv/Y+cioV4zIuw4IZr2yhRLu67CWL7FR9/6KXKnjRoZTvGGQ==} engines: {node: '>=12'} + skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -8180,6 +8225,13 @@ packages: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} + unicode-emoji-json@0.8.0: + resolution: {integrity: sha512-3wDXXvp6YGoKGhS2O2H7+V+bYduOBydN1lnI0uVfr1cIdY02uFFiEH1i3kE5CCE4l6UqbLKVmEFW9USxTAMD1g==} + + unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} + unicode-match-property-ecmascript@2.0.0: resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} engines: {node: '>=4'} @@ -9789,6 +9841,22 @@ snapshots: '@faker-js/faker@10.0.0': {} + '@ferrucc-io/emoji-picker@0.0.48(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.17)': + dependencies: + '@tanstack/react-virtual': 3.13.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + clsx: 2.1.1 + jotai: 2.17.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@18.3.17)(react@18.3.1) + node-emoji: 2.2.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tailwind-merge: 2.6.0 + tailwindcss: 3.4.17 + unicode-emoji-json: 0.8.0 + transitivePeerDependencies: + - '@babel/core' + - '@babel/template' + - '@types/react' + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 @@ -11550,6 +11618,8 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/is@4.6.0': {} + '@standard-schema/spec@1.0.0': {} '@standard-schema/spec@1.1.0': {} @@ -12023,8 +12093,16 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@tanstack/react-virtual@3.13.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/virtual-core': 3.13.18 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@tanstack/table-core@8.21.3': {} + '@tanstack/virtual-core@3.13.18': {} + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.27.1 @@ -13116,6 +13194,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + char-regex@1.0.2: {} + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -13755,15 +13835,12 @@ 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: {} + emojilib@2.4.0: {} + emojis-list@3.0.0: {} endent@2.1.0: @@ -14330,8 +14407,6 @@ 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 @@ -15047,6 +15122,13 @@ snapshots: jiti@2.6.1: {} + jotai@2.17.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@18.3.17)(react@18.3.1): + optionalDependencies: + '@babel/core': 7.28.5 + '@babel/template': 7.27.2 + '@types/react': 18.3.17 + react: 18.3.1 + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -15915,6 +15997,13 @@ snapshots: node-abort-controller@3.1.1: {} + node-emoji@2.2.0: + dependencies: + '@sindresorhus/is': 4.6.0 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 + node-fetch-h2@2.3.0: dependencies: http2-client: 1.3.5 @@ -17215,6 +17304,10 @@ snapshots: dependencies: jsep: 1.4.0 + skin-tone@2.0.0: + dependencies: + unicode-emoji-modifier-base: 1.0.0 + slash@3.0.0: {} sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -17730,6 +17823,10 @@ snapshots: unicode-canonical-property-names-ecmascript@2.0.1: {} + unicode-emoji-json@0.8.0: {} + + unicode-emoji-modifier-base@1.0.0: {} + unicode-match-property-ecmascript@2.0.0: dependencies: unicode-canonical-property-names-ecmascript: 2.0.1 diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/components/AgentCardMenu.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/components/AgentCardMenu.tsx index 933cdd34fa..d0a2de4e0a 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/components/AgentCardMenu.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/components/AgentCardMenu.tsx @@ -5,6 +5,10 @@ import { useDeleteV2DeleteLibraryAgent, usePostV2ForkLibraryAgent, } from "@/app/api/__generated__/endpoints/library/library"; +import { + usePostV2BulkMoveAgents, + getGetV2ListLibraryFoldersQueryKey, +} from "@/app/api/__generated__/endpoints/folders/folders"; import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import { Button } from "@/components/atoms/Button/Button"; import { Text } from "@/components/atoms/Text/Text"; @@ -22,6 +26,7 @@ import { useQueryClient } from "@tanstack/react-query"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState } from "react"; +import { MoveToFolderDialog } from "../../MoveToFolderDialog/MoveToFolderDialog"; interface AgentCardMenuProps { agent: LibraryAgent; @@ -32,11 +37,25 @@ export function AgentCardMenu({ agent }: AgentCardMenuProps) { const queryClient = useQueryClient(); const router = useRouter(); const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [showMoveDialog, setShowMoveDialog] = useState(false); const [isDeletingAgent, setIsDeletingAgent] = useState(false); const [isDuplicatingAgent, setIsDuplicatingAgent] = useState(false); + const [isRemovingFromFolder, setIsRemovingFromFolder] = useState(false); const { mutateAsync: deleteAgent } = useDeleteV2DeleteLibraryAgent(); const { mutateAsync: forkAgent } = usePostV2ForkLibraryAgent(); + const { mutateAsync: bulkMoveAgents } = usePostV2BulkMoveAgents({ + mutation: { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: getGetV2ListLibraryAgentsQueryKey(), + }); + queryClient.invalidateQueries({ + queryKey: getGetV2ListLibraryFoldersQueryKey(), + }); + }, + }, + }); async function handleDuplicateAgent() { if (!agent.id) return; @@ -70,6 +89,37 @@ export function AgentCardMenu({ agent }: AgentCardMenuProps) { } } + async function handleRemoveFromFolder() { + if (!agent.id) return; + + setIsRemovingFromFolder(true); + + try { + await bulkMoveAgents({ + data: { + agent_ids: [agent.id], + folder_id: undefined, + }, + }); + + toast({ + title: "Removed from folder", + description: "Agent has been moved back to your library.", + }); + } catch (error: unknown) { + toast({ + title: "Failed to remove from folder", + description: + error instanceof Error + ? error.message + : "An unexpected error occurred.", + variant: "destructive", + }); + } finally { + setIsRemovingFromFolder(false); + } + } + async function handleDeleteAgent() { if (!agent.id) return; @@ -138,6 +188,31 @@ export function AgentCardMenu({ agent }: AgentCardMenuProps) { Duplicate agent + { + e.stopPropagation(); + setShowMoveDialog(true); + }} + className="flex items-center gap-2" + > + Move to folder + + {agent.folder_id && ( + <> + + { + e.stopPropagation(); + handleRemoveFromFolder(); + }} + disabled={isRemovingFromFolder} + className="flex items-center gap-2" + > + Remove from folder + + + )} + { e.stopPropagation(); @@ -183,6 +258,14 @@ export function AgentCardMenu({ agent }: AgentCardMenuProps) { + + ); } 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 4ed1ef45bb..3e0dcfe525 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 @@ -1,26 +1,20 @@ "use client"; + import { LibraryAgentSort } from "@/app/api/__generated__/models/libraryAgentSort"; import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll"; import { LibraryActionSubHeader } from "../LibraryActionSubHeader/LibraryActionSubHeader"; 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"; +import { LibraryFolderEditDialog } from "../LibraryFolderEditDialog/LibraryFolderEditDialog"; +import { LibraryFolderDeleteDialog } from "../LibraryFolderDeleteDialog/LibraryFolderDeleteDialog"; +import { useLibraryAgentList } from "./useLibraryAgentList"; interface Props { searchTerm: string; @@ -43,58 +37,31 @@ export function LibraryAgentList({ activeTab, onTabChange, }: Props) { - const isFavoritesTab = activeTab === "favorites"; - - const allAgentsData = useLibraryAgentList({ - searchTerm, - librarySort, - folderId: selectedFolderId, - }); - - const favoriteAgentsData = useFavoriteAgents({ searchTerm }); - const { + isFavoritesTab, agentLoading, agentCount, - allAgents: agents, + agents, hasNextPage, isFetchingNextPage, fetchNextPage, - } = isFavoritesTab ? favoriteAgentsData : allAgentsData; - - const { data: foldersData } = useGetV2ListLibraryFolders(undefined, { - query: { select: okData }, + foldersData, + currentFolder, + showFolders, + editingFolder, + setEditingFolder, + deletingFolder, + setDeletingFolder, + handleAgentDrop, + handleFolderDeleted, + } = useLibraryAgentList({ + searchTerm, + librarySort, + selectedFolderId, + onFolderSelect, + activeTab, }); - 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 ( <> onFolderSelect(folder.id)} + onEdit={() => setEditingFolder(folder)} + onDelete={() => setDeletingFolder(folder)} /> ))} {agents.map((agent) => ( @@ -167,6 +136,27 @@ export function LibraryAgentList({ )} + + {editingFolder && ( + { + if (!open) setEditingFolder(null); + }} + /> + )} + + {deletingFolder && ( + { + if (!open) setDeletingFolder(null); + }} + onDeleted={handleFolderDeleted} + /> + )} ); } 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 fe1cc78d62..b9ef9ae594 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 @@ -1,43 +1,71 @@ "use client"; import { useGetV2ListLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library"; +import { getGetV2ListLibraryAgentsQueryKey } from "@/app/api/__generated__/endpoints/library/library"; +import { + useGetV2ListLibraryFolders, + usePostV2BulkMoveAgents, + getGetV2ListLibraryFoldersQueryKey, +} from "@/app/api/__generated__/endpoints/folders/folders"; +import type { getV2ListLibraryFoldersResponseSuccess } from "@/app/api/__generated__/endpoints/folders/folders"; +import type { LibraryFolder } from "@/app/api/__generated__/models/libraryFolder"; import { LibraryAgentSort } from "@/app/api/__generated__/models/libraryAgentSort"; import { + okData, getPaginatedTotalCount, getPaginationNextPageNumber, unpaginate, } from "@/app/api/helpers"; +import { useToast } from "@/components/molecules/Toast/use-toast"; +import { useFavoriteAgents } from "../../hooks/useFavoriteAgents"; import { getQueryClient } from "@/lib/react-query/queryClient"; -import { useEffect, useRef } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useEffect, useRef, useState } from "react"; interface Props { searchTerm: string; librarySort: LibraryAgentSort; - folderId?: string | null; + selectedFolderId: string | null; + onFolderSelect: (folderId: string | null) => void; + activeTab: string; } export function useLibraryAgentList({ searchTerm, librarySort, - folderId, + selectedFolderId, + onFolderSelect, + activeTab, }: Props) { - const queryClient = getQueryClient(); + const isFavoritesTab = activeTab === "favorites"; + const { toast } = useToast(); + const stableQueryClient = getQueryClient(); + const queryClient = useQueryClient(); const prevSortRef = useRef(null); + const [editingFolder, setEditingFolder] = useState( + null, + ); + const [deletingFolder, setDeletingFolder] = useState( + null, + ); + + // --- Agent list fetching --- + const { data: agentsQueryData, fetchNextPage, hasNextPage, isFetchingNextPage, - isLoading: agentLoading, + isLoading: allAgentsLoading, } = useGetV2ListLibraryAgentsInfinite( { page: 1, page_size: 20, search_term: searchTerm || undefined, sort_by: librarySort, - folder_id: folderId ?? undefined, - include_root_only: folderId === null ? true : undefined, + folder_id: selectedFolderId ?? undefined, + include_root_only: selectedFolderId === null ? true : undefined, }, { query: { @@ -46,28 +74,148 @@ export function useLibraryAgentList({ }, ); - // Reset queries when sort changes to ensure fresh data with correct sorting useEffect(() => { if (prevSortRef.current !== null && prevSortRef.current !== librarySort) { - // Reset all library agent queries to ensure fresh fetch with new sort - queryClient.resetQueries({ + stableQueryClient.resetQueries({ queryKey: ["/api/library/agents"], }); } prevSortRef.current = librarySort; - }, [librarySort, queryClient]); + }, [librarySort, stableQueryClient]); - const allAgents = agentsQueryData + const allAgentsList = agentsQueryData ? unpaginate(agentsQueryData, "agents") : []; - const agentCount = getPaginatedTotalCount(agentsQueryData); + const allAgentsCount = getPaginatedTotalCount(agentsQueryData); + + // --- Favorites --- + + const favoriteAgentsData = useFavoriteAgents({ searchTerm }); + + const { + agentLoading, + agentCount, + allAgents: agents, + hasNextPage: agentsHasNextPage, + isFetchingNextPage: agentsIsFetchingNextPage, + fetchNextPage: agentsFetchNextPage, + } = isFavoritesTab + ? favoriteAgentsData + : { + agentLoading: allAgentsLoading, + agentCount: allAgentsCount, + allAgents: allAgentsList, + hasNextPage: hasNextPage, + isFetchingNextPage: isFetchingNextPage, + fetchNextPage: fetchNextPage, + }; + + // --- Folders --- + + const { data: foldersData } = useGetV2ListLibraryFolders(undefined, { + query: { select: okData }, + }); + + const { mutate: moveAgentToFolder } = usePostV2BulkMoveAgents({ + mutation: { + onMutate: async ({ data }) => { + await queryClient.cancelQueries({ + queryKey: getGetV2ListLibraryFoldersQueryKey(), + }); + await queryClient.cancelQueries({ + queryKey: getGetV2ListLibraryAgentsQueryKey(), + }); + + const previousFolders = queryClient.getQueriesData< + getV2ListLibraryFoldersResponseSuccess + >({ queryKey: getGetV2ListLibraryFoldersQueryKey() }); + + if (data.folder_id) { + queryClient.setQueriesData( + { queryKey: getGetV2ListLibraryFoldersQueryKey() }, + (old) => { + if (!old?.data?.folders) return old; + return { + ...old, + data: { + ...old.data, + folders: old.data.folders.map((f) => + f.id === data.folder_id + ? { + ...f, + agent_count: + (f.agent_count ?? 0) + data.agent_ids.length, + } + : f, + ), + }, + }; + }, + ); + } + + return { previousFolders }; + }, + onError: (_error, _variables, context) => { + if (context?.previousFolders) { + for (const [queryKey, data] of context.previousFolders) { + queryClient.setQueryData(queryKey, data); + } + } + toast({ + title: "Error", + description: "Failed to move agent. Please try again.", + variant: "destructive", + }); + }, + onSettled: () => { + 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; + + function handleFolderDeleted() { + if (selectedFolderId === deletingFolder?.id) { + onFolderSelect(null); + } + } return { - allAgents, + isFavoritesTab, agentLoading, - hasNextPage, agentCount, - isFetchingNextPage, - fetchNextPage, + agents, + hasNextPage: agentsHasNextPage, + isFetchingNextPage: agentsIsFetchingNextPage, + fetchNextPage: agentsFetchNextPage, + foldersData, + currentFolder, + showFolders, + editingFolder, + setEditingFolder, + deletingFolder, + setDeletingFolder, + handleAgentDrop, + handleFolderDeleted, }; } 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 ef6fdbe8d1..7af688f230 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,7 +4,7 @@ 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, HeartIcon } from "@phosphor-icons/react"; +import { PencilSimpleIcon, TrashIcon } from "@phosphor-icons/react"; interface Props { id: string; @@ -14,10 +14,8 @@ interface Props { icon: string; onEdit?: () => void; onDelete?: () => void; - onFavorite?: () => void; onAgentDrop?: (agentId: string, folderId: string) => void; onClick?: () => void; - isFavorite?: boolean; } export function LibraryFolder({ @@ -28,10 +26,8 @@ export function LibraryFolder({ icon, onEdit, onDelete, - onFavorite, onAgentDrop, onClick, - isFavorite = false, }: Props) { const [isHovered, setIsHovered] = useState(false); const [isDragOver, setIsDragOver] = useState(false); @@ -103,22 +99,6 @@ export function LibraryFolder({ className="flex items-center justify-end gap-2" data-testid="library-folder-actions" > - + + + + + + ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolderEditDialog/LibraryFolderEditDialog.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolderEditDialog/LibraryFolderEditDialog.tsx new file mode 100644 index 0000000000..c846d94962 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryFolderEditDialog/LibraryFolderEditDialog.tsx @@ -0,0 +1,270 @@ +"use client"; + +import { Button } from "@/components/atoms/Button/Button"; +import { Input } from "@/components/atoms/Input/Input"; +import { Select } from "@/components/atoms/Select/Select"; +import { Text } from "@/components/atoms/Text/Text"; +import { Dialog } from "@/components/molecules/Dialog/Dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/components/molecules/Form/Form"; +import { useToast } from "@/components/molecules/Toast/use-toast"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { EmojiPicker } from "@ferrucc-io/emoji-picker"; +import { + usePatchV2UpdateFolder, + getGetV2ListLibraryFoldersQueryKey, +} from "@/app/api/__generated__/endpoints/folders/folders"; +import { useQueryClient } from "@tanstack/react-query"; +import type { LibraryFolder } from "@/app/api/__generated__/models/libraryFolder"; +import type { getV2ListLibraryFoldersResponseSuccess } from "@/app/api/__generated__/endpoints/folders/folders"; + +const FOLDER_COLORS = [ + { value: "#3B82F6", label: "Blue" }, + { value: "#A855F7", label: "Purple" }, + { value: "#10B981", label: "Green" }, + { value: "#F97316", label: "Orange" }, + { value: "#EC4899", label: "Pink" }, +]; + +const editFolderSchema = z.object({ + folderName: z.string().min(1, "Folder name is required"), + folderColor: z.string().min(1, "Folder color is required"), + folderIcon: z.string().min(1, "Folder icon is required"), +}); + +interface Props { + folder: LibraryFolder; + isOpen: boolean; + setIsOpen: (open: boolean) => void; +} + +export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) { + const queryClient = useQueryClient(); + const { toast } = useToast(); + + const form = useForm>({ + resolver: zodResolver(editFolderSchema), + defaultValues: { + folderName: folder.name, + folderColor: folder.color ?? "", + folderIcon: folder.icon ?? "", + }, + }); + + useEffect(() => { + if (isOpen) { + form.reset({ + folderName: folder.name, + folderColor: folder.color ?? "", + folderIcon: folder.icon ?? "", + }); + } + }, [isOpen, folder, form]); + + const { mutate: updateFolder, isPending } = usePatchV2UpdateFolder({ + mutation: { + onMutate: async ({ folderId, data }) => { + await queryClient.cancelQueries({ + queryKey: getGetV2ListLibraryFoldersQueryKey(), + }); + + const previousData = queryClient.getQueriesData< + getV2ListLibraryFoldersResponseSuccess + >({ + queryKey: getGetV2ListLibraryFoldersQueryKey(), + }); + + queryClient.setQueriesData( + { queryKey: getGetV2ListLibraryFoldersQueryKey() }, + (old) => { + if (!old?.data?.folders) return old; + return { + ...old, + data: { + ...old.data, + folders: old.data.folders.map((f) => + f.id === folderId + ? { + ...f, + name: data.name ?? f.name, + color: data.color ?? f.color, + icon: data.icon ?? f.icon, + } + : f, + ), + }, + }; + }, + ); + + return { previousData }; + }, + onError: (_error, _variables, context) => { + if (context?.previousData) { + for (const [queryKey, data] of context.previousData) { + queryClient.setQueryData(queryKey, data); + } + } + toast({ + title: "Error", + description: "Failed to update folder. Please try again.", + variant: "destructive", + }); + }, + onSuccess: () => { + setIsOpen(false); + toast({ + title: "Folder updated", + description: "Your folder has been updated successfully.", + }); + }, + onSettled: () => { + queryClient.invalidateQueries({ + queryKey: getGetV2ListLibraryFoldersQueryKey(), + }); + }, + }, + }); + + function onSubmit(values: z.infer) { + updateFolder({ + folderId: folder.id, + data: { + name: values.folderName, + color: values.folderColor, + icon: values.folderIcon, + }, + }); + } + + return ( + + +
onSubmit(values)} + className="flex flex-col justify-center gap-4 px-1" + > + ( + + + + + + + )} + /> + + ( + + + setSearch(e.target.value)} + className="w-full" + /> +
+ {folders.length === 0 ? ( +
+ + No folders found + +
+ ) : ( +
+ {folders.map((folder) => ( + + ))} +
+ )} +
+ + +
+ ); +} diff --git a/autogpt_platform/frontend/src/app/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json index a518cb21bf..1f975ff575 100644 --- a/autogpt_platform/frontend/src/app/api/openapi.json +++ b/autogpt_platform/frontend/src/app/api/openapi.json @@ -8394,6 +8394,96 @@ "title": "ExecutionStartedResponse", "description": "Response for run/schedule actions." }, + "FolderCreateRequest": { + "properties": { + "name": { + "type": "string", + "maxLength": 100, + "minLength": 1, + "title": "Name" + }, + "icon": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Icon" + }, + "color": { + "anyOf": [ + { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$" }, + { "type": "null" } + ], + "title": "Color", + "description": "Hex color code (#RRGGBB)" + }, + "parent_id": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Parent Id" + } + }, + "type": "object", + "required": ["name"], + "title": "FolderCreateRequest", + "description": "Request model for creating a folder." + }, + "FolderListResponse": { + "properties": { + "folders": { + "items": { "$ref": "#/components/schemas/LibraryFolder" }, + "type": "array", + "title": "Folders" + }, + "pagination": { "$ref": "#/components/schemas/Pagination" } + }, + "type": "object", + "required": ["folders", "pagination"], + "title": "FolderListResponse", + "description": "Response schema for a list of folders." + }, + "FolderMoveRequest": { + "properties": { + "target_parent_id": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Target Parent Id" + } + }, + "type": "object", + "title": "FolderMoveRequest", + "description": "Request model for moving a folder to a new parent." + }, + "FolderTreeResponse": { + "properties": { + "tree": { + "items": { "$ref": "#/components/schemas/LibraryFolderTree" }, + "type": "array", + "title": "Tree" + } + }, + "type": "object", + "required": ["tree"], + "title": "FolderTreeResponse", + "description": "Response schema for folder tree structure." + }, + "FolderUpdateRequest": { + "properties": { + "name": { + "anyOf": [ + { "type": "string", "maxLength": 100, "minLength": 1 }, + { "type": "null" } + ], + "title": "Name" + }, + "icon": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Icon" + }, + "color": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Color" + } + }, + "type": "object", + "title": "FolderUpdateRequest", + "description": "Request model for updating a folder." + }, "Graph": { "properties": { "id": { "type": "string", "title": "Id" },