mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-13 08:14:58 -05:00
feat(library): integrate emoji picker and enhance folder management UI
- Added `@ferrucc-io/emoji-picker` dependency for improved emoji selection in folder creation. - Updated `LibraryFolderCreationDialog` to utilize the new emoji picker, enhancing user experience. - Enhanced `AgentCardMenu` with options to move agents to folders and remove them from folders, improving agent organization. - Refactored `LibraryAgentList` to support folder editing and deletion, streamlining folder management. - Improved API integration for folder operations, ensuring seamless updates to the UI. These changes significantly enhance the library's usability and organization features, making it easier for users to manage their agents and folders.
This commit is contained in:
@@ -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",
|
||||
|
||||
135
autogpt_platform/frontend/pnpm-lock.yaml
generated
135
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowMoveDialog(true);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
Move to folder
|
||||
</DropdownMenuItem>
|
||||
{agent.folder_id && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemoveFromFolder();
|
||||
}}
|
||||
disabled={isRemovingFromFolder}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
Remove from folder
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -183,6 +258,14 @@ export function AgentCardMenu({ agent }: AgentCardMenuProps) {
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
|
||||
<MoveToFolderDialog
|
||||
agentId={agent.id}
|
||||
agentName={agent.name}
|
||||
currentFolderId={agent.folder_id}
|
||||
isOpen={showMoveDialog}
|
||||
setIsOpen={setShowMoveDialog}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<LibraryActionSubHeader
|
||||
@@ -157,6 +124,8 @@ export function LibraryAgentList({
|
||||
icon={folder.icon ?? "📁"}
|
||||
onAgentDrop={handleAgentDrop}
|
||||
onClick={() => onFolderSelect(folder.id)}
|
||||
onEdit={() => setEditingFolder(folder)}
|
||||
onDelete={() => setDeletingFolder(folder)}
|
||||
/>
|
||||
))}
|
||||
{agents.map((agent) => (
|
||||
@@ -167,6 +136,27 @@ export function LibraryAgentList({
|
||||
</InfiniteScroll>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editingFolder && (
|
||||
<LibraryFolderEditDialog
|
||||
folder={editingFolder}
|
||||
isOpen={!!editingFolder}
|
||||
setIsOpen={(open) => {
|
||||
if (!open) setEditingFolder(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deletingFolder && (
|
||||
<LibraryFolderDeleteDialog
|
||||
folder={deletingFolder}
|
||||
isOpen={!!deletingFolder}
|
||||
setIsOpen={(open) => {
|
||||
if (!open) setDeletingFolder(null);
|
||||
}}
|
||||
onDeleted={handleFolderDeleted}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<LibraryAgentSort | null>(null);
|
||||
|
||||
const [editingFolder, setEditingFolder] = useState<LibraryFolder | null>(
|
||||
null,
|
||||
);
|
||||
const [deletingFolder, setDeletingFolder] = useState<LibraryFolder | null>(
|
||||
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<getV2ListLibraryFoldersResponseSuccess>(
|
||||
{ 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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<Button
|
||||
variant="icon"
|
||||
size="icon"
|
||||
aria-label="Favorite agent"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFavorite?.();
|
||||
}}
|
||||
className="h-8 w-8 p-2"
|
||||
>
|
||||
<HeartIcon
|
||||
className="h-4 w-4"
|
||||
weight={isFavorite ? "fill" : "regular"}
|
||||
color={isFavorite ? "#facc15" : "currentColor"}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="icon"
|
||||
size="icon"
|
||||
|
||||
@@ -13,27 +13,17 @@ import {
|
||||
} from "@/components/molecules/Form/Form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { FolderSimpleIcon } from "@phosphor-icons/react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { EmojiStyle } from "emoji-picker-react";
|
||||
import { usePostV2CreateFolder } from "@/app/api/__generated__/endpoints/folders/folders";
|
||||
import { EmojiPicker } from "@ferrucc-io/emoji-picker";
|
||||
import {
|
||||
usePostV2CreateFolder,
|
||||
getGetV2ListLibraryFoldersQueryKey,
|
||||
} from "@/app/api/__generated__/endpoints/folders/folders";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
// Dynamically import EmojiPicker to avoid SSR issues
|
||||
const EmojiPicker = dynamic(() => import("emoji-picker-react"), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="flex h-[350px] items-center justify-center">
|
||||
<Text variant="small" className="text-zinc-400">
|
||||
Loading emoji picker...
|
||||
</Text>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
const FOLDER_COLORS = [
|
||||
{ value: "#3B82F6", label: "Blue" },
|
||||
{ value: "#A855F7", label: "Purple" },
|
||||
@@ -56,7 +46,7 @@ export default function LibraryFolderCreationDialog() {
|
||||
const { mutate: createFolder, isPending } = usePostV2CreateFolder({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["getV2ListLibraryFolders"] });
|
||||
queryClient.invalidateQueries({ queryKey: getGetV2ListLibraryFoldersQueryKey() });
|
||||
setIsOpen(false);
|
||||
form.reset();
|
||||
toast({
|
||||
@@ -133,7 +123,7 @@ export default function LibraryFolderCreationDialog() {
|
||||
id={field.name}
|
||||
label="Folder name"
|
||||
placeholder="Enter folder name"
|
||||
className="w-full rounded-[10px]"
|
||||
className="w-full"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -196,17 +186,20 @@ export default function LibraryFolderCreationDialog() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<EmojiPicker
|
||||
onEmojiClick={(emojiData) => {
|
||||
field.onChange(emojiData.emoji);
|
||||
}}
|
||||
width="100%"
|
||||
height="295px"
|
||||
style={{ borderRadius: "20px" }}
|
||||
emojiStyle={EmojiStyle.APPLE}
|
||||
searchPlaceHolder="Search emoji..."
|
||||
previewConfig={{ showPreview: false }}
|
||||
/>
|
||||
<div className="h-[295px] w-full overflow-hidden">
|
||||
|
||||
<EmojiPicker
|
||||
onEmojiSelect={(emoji) => {
|
||||
field.onChange(emoji);
|
||||
}}
|
||||
emojiSize={32}
|
||||
className="w-full rounded-2xl px-2"
|
||||
>
|
||||
<EmojiPicker.Group>
|
||||
<EmojiPicker.List hideStickyHeader containerHeight={295} />
|
||||
</EmojiPicker.Group>
|
||||
</EmojiPicker>
|
||||
</div>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import {
|
||||
useDeleteV2DeleteFolder,
|
||||
getGetV2ListLibraryFoldersQueryKey,
|
||||
} from "@/app/api/__generated__/endpoints/folders/folders";
|
||||
import { getGetV2ListLibraryAgentsQueryKey } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { LibraryFolder } from "@/app/api/__generated__/models/libraryFolder";
|
||||
|
||||
interface Props {
|
||||
folder: LibraryFolder;
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
onDeleted?: () => void;
|
||||
}
|
||||
|
||||
export function LibraryFolderDeleteDialog({
|
||||
folder,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
onDeleted,
|
||||
}: Props) {
|
||||
const queryClient = useQueryClient();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutate: deleteFolder, isPending } = useDeleteV2DeleteFolder({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListLibraryFoldersQueryKey(),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListLibraryAgentsQueryKey(),
|
||||
});
|
||||
toast({
|
||||
title: "Folder deleted",
|
||||
description: `"${folder.name}" has been deleted.`,
|
||||
});
|
||||
setIsOpen(false);
|
||||
onDeleted?.();
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to delete folder. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function handleDelete() {
|
||||
deleteFolder({ folderId: folder.id });
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
controlled={{
|
||||
isOpen,
|
||||
set: setIsOpen,
|
||||
}}
|
||||
styling={{ maxWidth: "32rem" }}
|
||||
title="Delete folder"
|
||||
>
|
||||
<Dialog.Content>
|
||||
<div>
|
||||
<Text variant="large">
|
||||
Are you sure you want to delete “{folder.name}”? Agents
|
||||
inside this folder will be moved back to your library.
|
||||
</Text>
|
||||
<Dialog.Footer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={isPending}
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
loading={isPending}
|
||||
>
|
||||
Delete Folder
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -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<z.infer<typeof editFolderSchema>>({
|
||||
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<getV2ListLibraryFoldersResponseSuccess>(
|
||||
{ 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<typeof editFolderSchema>) {
|
||||
updateFolder({
|
||||
folderId: folder.id,
|
||||
data: {
|
||||
name: values.folderName,
|
||||
color: values.folderColor,
|
||||
icon: values.folderIcon,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Edit Folder"
|
||||
styling={{ maxWidth: "30rem" }}
|
||||
controlled={{
|
||||
isOpen,
|
||||
set: setIsOpen,
|
||||
}}
|
||||
>
|
||||
<Dialog.Content>
|
||||
<Form
|
||||
form={form}
|
||||
onSubmit={(values) => onSubmit(values)}
|
||||
className="flex flex-col justify-center gap-4 px-1"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="folderName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
id={field.name}
|
||||
label="Folder name"
|
||||
placeholder="Enter folder name"
|
||||
className="w-full rounded-[10px]"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="folderColor"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select
|
||||
id="folderColor"
|
||||
label="Folder color"
|
||||
placeholder="Select a color"
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
options={FOLDER_COLORS.map((color) => ({
|
||||
value: color.value,
|
||||
label: color.label,
|
||||
icon: (
|
||||
<div
|
||||
className="h-4 w-4 rounded-full"
|
||||
style={{ backgroundColor: color.value }}
|
||||
/>
|
||||
),
|
||||
}))}
|
||||
renderItem={(option) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{option.icon}
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="folderIcon"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Text variant="large-medium" as="span" className="text-black">
|
||||
Folder icon
|
||||
</Text>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Text variant="small" className="text-zinc-500">
|
||||
Selected:
|
||||
</Text>
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg border border-zinc-200 bg-zinc-50 text-2xl">
|
||||
{form.watch("folderIcon") || (
|
||||
<span className="text-sm text-zinc-400">—</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[295px] w-full overflow-hidden">
|
||||
<EmojiPicker
|
||||
onEmojiSelect={(emoji) => {
|
||||
field.onChange(emoji);
|
||||
}}
|
||||
emojiSize={32}
|
||||
className="w-full"
|
||||
>
|
||||
<EmojiPicker.Group>
|
||||
<EmojiPicker.List hideStickyHeader containerHeight={295} />
|
||||
</EmojiPicker.Group>
|
||||
</EmojiPicker>
|
||||
</div>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="mt-2 min-w-[18rem]"
|
||||
disabled={!form.formState.isValid || isPending}
|
||||
loading={isPending}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</Form>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import {
|
||||
useGetV2ListLibraryFolders,
|
||||
usePostV2BulkMoveAgents,
|
||||
getGetV2ListLibraryFoldersQueryKey,
|
||||
} from "@/app/api/__generated__/endpoints/folders/folders";
|
||||
import { getGetV2ListLibraryAgentsQueryKey } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
interface Props {
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
currentFolderId?: string | null;
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function MoveToFolderDialog({
|
||||
agentId,
|
||||
agentName,
|
||||
currentFolderId,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}: Props) {
|
||||
const queryClient = useQueryClient();
|
||||
const { toast } = useToast();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const { data: foldersData } = useGetV2ListLibraryFolders(undefined, {
|
||||
query: { select: okData },
|
||||
});
|
||||
|
||||
const { mutate: moveAgent, isPending } = usePostV2BulkMoveAgents({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListLibraryAgentsQueryKey(),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListLibraryFoldersQueryKey(),
|
||||
});
|
||||
setIsOpen(false);
|
||||
setSearch("");
|
||||
toast({
|
||||
title: "Agent moved",
|
||||
description: `"${agentName}" has been moved.`,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to move agent. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const folders = (foldersData?.folders ?? []).filter(
|
||||
(f) =>
|
||||
f.id !== currentFolderId &&
|
||||
f.name.toLowerCase().includes(search.toLowerCase()),
|
||||
);
|
||||
|
||||
function handleMoveToFolder(folderId: string) {
|
||||
moveAgent({
|
||||
data: {
|
||||
agent_ids: [agentId],
|
||||
folder_id: folderId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
controlled={{ isOpen, set: setIsOpen }}
|
||||
styling={{ maxWidth: "28rem" }}
|
||||
title="Move to folder"
|
||||
onClose={() => {
|
||||
setSearch("");
|
||||
}}
|
||||
>
|
||||
<Dialog.Content>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Input
|
||||
id="search-folders"
|
||||
label="Search folders"
|
||||
placeholder="Search folders..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="max-h-[280px] overflow-y-auto">
|
||||
{folders.length === 0 ? (
|
||||
<div className="flex h-20 items-center justify-center">
|
||||
<Text variant="small" className="text-zinc-400">
|
||||
No folders found
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
{folders.map((folder) => (
|
||||
<Button
|
||||
key={folder.id}
|
||||
variant="ghost"
|
||||
className="w-full justify-start gap-3 px-3 py-2.5"
|
||||
disabled={isPending}
|
||||
onClick={() => handleMoveToFolder(folder.id)}
|
||||
>
|
||||
<span className="text-lg">{folder.icon ?? "📁"}</span>
|
||||
<div className="flex flex-col items-start">
|
||||
<Text variant="small-medium">{folder.name}</Text>
|
||||
<Text variant="small" className="text-zinc-400">
|
||||
{folder.agent_count ?? 0}{" "}
|
||||
{(folder.agent_count ?? 0) === 1 ? "agent" : "agents"}
|
||||
</Text>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user