From b3e87f6cd41bc66b804bf45c35a96f9972d66acb Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 16 Feb 2026 18:17:59 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20replace=20per-?= =?UTF-8?q?item=20Editing=20components=20with=20singleton=20EditingPopover?= =?UTF-8?q?=20(#12327)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ refactor: replace per-item Editing components with singleton EditingPopover Eliminate 3 duplicate Editing components (AgentItem, AgentGroupItem, Group) in favor of a single imperative EditingPopover using @lobehub/ui Popover atoms. Anchor elements are passed via React state (useState + callback ref) instead of DOM queries. Removes agentRenamingId/groupRenamingId from homeStore. * fix: edit group agent avaar Signed-off-by: Innei * ✅ test(e2e): update rename popover selectors and allow console in tests Support both antd Popover and @lobehub/ui Popover atoms selectors. Use save button click instead of click-outside for non-Enter rename flow. Disable no-console rule for e2e and test files. * ✅ test(e2e): fix rename popover input detection with data-testid Add data-testid="editing-popover" to PopoverPopup. Simplify inputNewName to use single combined selector instead of sequential try-catch loop that caused 8s+ timeout. Support both @lobehub/ui and antd Popover. --------- Signed-off-by: Innei --- e2e/src/steps/home/sidebarAgent.steps.ts | 80 +-- eslint-suppressions.json | 466 ------------------ eslint.config.mjs | 7 + .../Agent/List/AgentGroupItem/Editing.tsx | 152 ------ .../Body/Agent/List/AgentGroupItem/index.tsx | 66 +-- .../List/AgentGroupItem/useDropdownMenu.tsx | 20 +- .../Body/Agent/List/AgentItem/Editing.tsx | 134 ----- .../Body/Agent/List/AgentItem/index.tsx | 62 +-- .../Agent/List/AgentItem/useDropdownMenu.tsx | 17 +- .../_layout/Body/Agent/List/Group/Editing.tsx | 69 --- .../_layout/Body/Agent/List/Group/Item.tsx | 26 +- .../Body/Agent/List/Group/useDropdownMenu.tsx | 11 +- .../home/_layout/Body/Agent/ModalProvider.tsx | 3 + .../hooks/useSessionGroupMenuItems.tsx | 7 +- src/features/EditingPopover/AgentContent.tsx | 92 ++++ src/features/EditingPopover/GroupContent.tsx | 146 ++++++ src/features/EditingPopover/index.tsx | 47 ++ src/features/EditingPopover/store.ts | 27 + .../home/slices/sidebarUI/action.test.ts | 52 -- src/store/home/slices/sidebarUI/action.ts | 8 - .../home/slices/sidebarUI/initialState.ts | 10 - 21 files changed, 441 insertions(+), 1061 deletions(-) delete mode 100644 src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/Editing.tsx delete mode 100644 src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/Editing.tsx delete mode 100644 src/app/[variants]/(main)/home/_layout/Body/Agent/List/Group/Editing.tsx create mode 100644 src/features/EditingPopover/AgentContent.tsx create mode 100644 src/features/EditingPopover/GroupContent.tsx create mode 100644 src/features/EditingPopover/index.tsx create mode 100644 src/features/EditingPopover/store.ts diff --git a/e2e/src/steps/home/sidebarAgent.steps.ts b/e2e/src/steps/home/sidebarAgent.steps.ts index 1969e5eaa5..ecc18b18e1 100644 --- a/e2e/src/steps/home/sidebarAgent.steps.ts +++ b/e2e/src/steps/home/sidebarAgent.steps.ts @@ -10,7 +10,7 @@ import { Given, Then, When } from '@cucumber/cucumber'; import { expect } from '@playwright/test'; import { TEST_USER } from '../../support/seedTestUser'; -import { CustomWorld, WAIT_TIMEOUT } from '../../support/world'; +import { type CustomWorld, WAIT_TIMEOUT } from '../../support/world'; // ============================================ // Helper Functions @@ -23,67 +23,29 @@ async function inputNewName( ): Promise { await this.page.waitForTimeout(300); - // Try to find the popover input - const popoverInputSelectors = [ - '.ant-popover-inner input', - '.ant-popover-content input', - '.ant-popover input', - ]; + // Primary: find input inside EditingPopover (data-testid) or antd Popover + const renameInput = this.page + .locator('[data-testid="editing-popover"] input, .ant-popover input') + .first(); - let renameInput = null; + await renameInput.waitFor({ state: 'visible', timeout: 5000 }); + await renameInput.click(); + await renameInput.clear(); + await renameInput.fill(newName); - for (const selector of popoverInputSelectors) { - try { - const locator = this.page.locator(selector).first(); - await locator.waitFor({ state: 'visible', timeout: 2000 }); - renameInput = locator; - break; - } catch { - // Try next selector - } - } - - if (!renameInput) { - // Fallback: find any visible input - const allInputs = this.page.locator('input:visible'); - const count = await allInputs.count(); - - for (let i = 0; i < count; i++) { - const input = allInputs.nth(i); - const placeholder = (await input.getAttribute('placeholder').catch(() => '')) || ''; - if (placeholder.includes('Search') || placeholder.includes('搜索')) continue; - - const isInPopover = await input.evaluate((el) => { - return el.closest('.ant-popover') !== null || el.closest('[class*="popover"]') !== null; - }); - - if (isInPopover || count <= 2) { - renameInput = input; - break; - } - } - } - - if (renameInput) { - await renameInput.click(); - await renameInput.clear(); - await renameInput.fill(newName); - - if (pressEnter) { - await renameInput.press('Enter'); - } else { - await this.page.click('body', { position: { x: 10, y: 10 } }); - } + if (pressEnter) { + await renameInput.press('Enter'); } else { - // Keyboard fallback - await this.page.keyboard.press('Meta+A'); - await this.page.waitForTimeout(50); - await this.page.keyboard.type(newName, { delay: 20 }); - - if (pressEnter) { - await this.page.keyboard.press('Enter'); - } else { - await this.page.click('body', { position: { x: 10, y: 10 } }); + // Click the save button (ActionIcon with Check icon) next to the input + const saveButton = this.page + .locator('[data-testid="editing-popover"] svg.lucide-check, .ant-popover svg.lucide-check') + .first(); + try { + await saveButton.waitFor({ state: 'visible', timeout: 2000 }); + await saveButton.click(); + } catch { + // Fallback: press Enter to save + await renameInput.press('Enter'); } } diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 49607007cf..df24daa121 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1,14 +1,4 @@ { - "src/app/(backend)/api/agent/route.ts": { - "object-shorthand": { - "count": 1 - } - }, - "src/app/(backend)/api/agent/run/route.ts": { - "object-shorthand": { - "count": 1 - } - }, "src/app/(backend)/api/dev/memory-user-memory/benchmark-locomo/route.ts": { "no-console": { "count": 6 @@ -54,124 +44,31 @@ "count": 1 } }, - "src/app/[variants]/(main)/agent/profile/features/Header/AgentPublishButton/useMarketPublish.ts": { - "object-shorthand": { - "count": 3 - } - }, "src/app/[variants]/(main)/community/(detail)/agent/features/AgentForkTag.tsx": { "no-console": { "count": 1 } }, - "src/app/[variants]/(main)/community/(detail)/agent/features/Details/Capabilities/PluginItem.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/app/[variants]/(main)/community/(detail)/agent/features/Header.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/app/[variants]/(main)/community/(detail)/agent/features/Sidebar/ActionButton/index.tsx": { - "object-shorthand": { - "count": 2 - } - }, - "src/app/[variants]/(main)/community/(detail)/group_agent/features/Sidebar/ActionButton/index.tsx": { - "object-shorthand": { - "count": 2 - } - }, - "src/app/[variants]/(main)/community/(detail)/user/features/UserAgentCard.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, "src/app/[variants]/(main)/community/(detail)/user/features/UserAgentList.tsx": { "no-console": { "count": 1 } }, - "src/app/[variants]/(main)/community/(detail)/user/features/UserFavoriteAgents.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/app/[variants]/(main)/community/(detail)/user/features/UserFavoritePlugins.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/app/[variants]/(main)/community/(detail)/user/features/UserGroupCard.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/app/[variants]/(main)/community/(list)/(home)/loading.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/app/[variants]/(main)/community/(list)/mcp/features/List/Item.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/app/[variants]/(main)/community/(list)/provider/features/List/Item.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, "src/app/[variants]/(main)/community/components/VirtuosoGridList/index.tsx": { "@eslint-react/no-nested-component-definitions": { "count": 2 } }, - "src/app/[variants]/(main)/group/profile/features/Header/AgentPublishButton/useMarketPublish.ts": { - "object-shorthand": { - "count": 4 - } - }, - "src/app/[variants]/(main)/home/_layout/Body/Agent/Modals/ConfigGroupModal/index.tsx": { - "@typescript-eslint/consistent-type-imports": { - "count": 1 - }, - "simple-import-sort/imports": { - "count": 1 - } - }, "src/app/[variants]/(main)/home/features/RecentPage/Item.tsx": { "regexp/no-super-linear-backtracking": { "count": 1 } }, - "src/app/[variants]/(main)/home/features/components/GroupSkeleton.tsx": { - "object-shorthand": { - "count": 1 - } - }, "src/app/[variants]/(main)/home/features/index.tsx": { "@eslint-react/no-nested-component-definitions": { "count": 1 } }, - "src/app/[variants]/(main)/image/_layout/ConfigPanel/utils/__tests__/imageValidation.test.ts": { - "object-shorthand": { - "count": 1 - } - }, - "src/app/[variants]/(main)/image/_layout/TopicSidebar.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx": { - "object-shorthand": { - "count": 1 - } - }, "src/app/[variants]/(main)/memory/features/GridView/index.tsx": { "@eslint-react/no-nested-component-definitions": { "count": 1 @@ -187,16 +84,6 @@ "count": 1 } }, - "src/app/[variants]/(main)/resource/library/_layout/Header/LibraryHead.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/app/[variants]/(main)/settings/provider/ProviderMenu/List.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, "src/app/[variants]/(main)/settings/provider/detail/ollama/CheckError.tsx": { "regexp/no-dupe-characters-character-class": { "count": 1 @@ -205,11 +92,6 @@ "count": 1 } }, - "src/app/[variants]/(main)/settings/provider/features/ProviderConfig/index.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, "src/app/[variants]/(main)/settings/skill/features/KlavisSkillItem.tsx": { "no-console": { "count": 2 @@ -220,21 +102,6 @@ "count": 1 } }, - "src/app/[variants]/(main)/settings/stats/features/usage/UsageCards/ActiveModels/ModelTable.tsx": { - "object-shorthand": { - "count": 1 - } - }, - "src/app/[variants]/(main)/video/_layout/TopicSidebar.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/app/[variants]/(main)/video/features/GenerationFeed/index.tsx": { - "object-shorthand": { - "count": 1 - } - }, "src/app/[variants]/onboarding/components/KlavisServerList/hooks/useKlavisOAuth.ts": { "no-console": { "count": 1 @@ -255,11 +122,6 @@ "count": 3 } }, - "src/components/ChatGroupWizard/ChatGroupWizard.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, "src/components/ChatGroupWizard/index.ts": { "sort-keys-fix/sort-keys-fix": { "count": 1 @@ -283,26 +145,11 @@ "count": 1 } }, - "src/components/MCPStdioCommandInput/index.tsx": { - "object-shorthand": { - "count": 1 - } - }, - "src/components/MaxTokenSlider.tsx": { - "object-shorthand": { - "count": 1 - } - }, "src/components/ModelSelect/index.tsx": { "no-shadow-restricted-names": { "count": 1 } }, - "src/components/mdx/index.tsx": { - "object-shorthand": { - "count": 1 - } - }, "src/config/featureFlags/schema.ts": { "sort-keys-fix/sort-keys-fix": { "count": 1 @@ -319,9 +166,6 @@ } }, "src/envs/auth.ts": { - "perfectionist/sort-interfaces": { - "count": 19 - }, "sort-keys-fix/sort-keys-fix": { "count": 1 }, @@ -355,11 +199,6 @@ "count": 1 } }, - "src/features/ChatInput/ActionBar/Model/ReasoningTokenSlider.tsx": { - "object-shorthand": { - "count": 1 - } - }, "src/features/ChatInput/ActionBar/Tools/KlavisServerItem.tsx": { "no-console": { "count": 2 @@ -383,36 +222,6 @@ "count": 1 } }, - "src/features/Conversation/Messages/Assistant/Actions/index.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/features/Conversation/Messages/AssistantGroup/Actions/index.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/features/Conversation/Messages/Supervisor/Actions/index.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/features/Conversation/Messages/Supervisor/components/ContentBlock.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/features/Conversation/components/Reaction/ReactionPicker.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/features/Conversation/store/slices/message/action/index.ts": { - "object-shorthand": { - "count": 1 - } - }, "src/features/Conversation/store/slices/message/action/sendMessage.ts": { "no-console": { "count": 1 @@ -433,41 +242,6 @@ "count": 1 } }, - "src/features/GenerationTopicPanel/index.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/features/LibraryModal/CreateNew/CreateForm.tsx": { - "unused-imports/no-unused-imports": { - "count": 1 - } - }, - "src/features/LibraryModal/CreateNew/index.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/features/MCP/Scores.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/features/MCPPluginDetail/Header.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/features/PluginDevModal/MCPManifestForm/QuickImportSection.tsx": { - "object-shorthand": { - "count": 2 - } - }, - "src/features/PluginDevModal/MCPManifestForm/utils.ts": { - "object-shorthand": { - "count": 1 - } - }, "src/features/PluginsUI/Render/utils/iframeOnReady.test.ts": { "unicorn/no-invalid-remove-event-listener": { "count": 1 @@ -484,44 +258,18 @@ } }, "src/features/ProtocolUrlHandler/InstallPlugin/CustomPluginInstallModal.tsx": { - "object-shorthand": { - "count": 1 - }, "prefer-const": { "count": 1 } }, - "src/features/ResourceManager/components/Explorer/Header/SearchInput.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, "src/features/ResourceManager/components/Explorer/ListView/index.tsx": { "@eslint-react/no-nested-component-definitions": { "count": 1 } }, - "src/features/ResourceManager/components/Explorer/MasonryView/MasonryItem/DefaultFileItem.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/features/ResourceManager/components/Explorer/MasonryView/MasonryItem/ImageFileItem.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/features/ResourceManager/components/Explorer/MasonryView/MasonryItem/MarkdownFileItem.tsx": { - "simple-import-sort/imports": { - "count": 1 - } - }, "src/features/ResourceManager/components/Explorer/MasonryView/MasonryItem/NoteFileItem.tsx": { "regexp/no-super-linear-backtracking": { "count": 1 - }, - "simple-import-sort/imports": { - "count": 1 } }, "src/features/ShareModal/ShareJSON/generateFullExport.ts": { @@ -534,21 +282,11 @@ "count": 1 } }, - "src/helpers/toolEngineering/index.ts": { - "object-shorthand": { - "count": 2 - } - }, "src/hooks/useAgentOwnershipCheck.ts": { "no-console": { "count": 6 } }, - "src/hooks/useFetchAiVideoConfig.ts": { - "simple-import-sort/imports": { - "count": 1 - } - }, "src/hooks/useHotkeys/useHotkeyById.ts": { "no-console": { "count": 1 @@ -570,17 +308,9 @@ "count": 1 } }, - "src/libs/better-auth/sso/index.ts": { - "object-shorthand": { - "count": 1 - } - }, "src/libs/next/proxy/define-config.ts": { "no-console": { "count": 1 - }, - "object-shorthand": { - "count": 2 } }, "src/libs/observability/traceparent.test.ts": { @@ -594,14 +324,6 @@ }, "@typescript-eslint/no-unsafe-function-type": { "count": 1 - }, - "object-shorthand": { - "count": 1 - } - }, - "src/libs/oidc-provider/jwt.ts": { - "object-shorthand": { - "count": 1 } }, "src/libs/swr/localStorageProvider.ts": { @@ -627,21 +349,6 @@ "count": 1 } }, - "src/server/manifest.ts": { - "object-shorthand": { - "count": 3 - } - }, - "src/server/modules/AgentRuntime/RuntimeExecutors.ts": { - "object-shorthand": { - "count": 4 - } - }, - "src/server/modules/KeyVaultsEncrypt/index.ts": { - "object-shorthand": { - "count": 2 - } - }, "src/server/modules/Mecha/ContextEngineering/index.ts": { "sort-keys-fix/sort-keys-fix": { "count": 1 @@ -652,19 +359,6 @@ "count": 1 } }, - "src/server/modules/ModelRuntime/trace.ts": { - "object-shorthand": { - "count": 1 - } - }, - "src/server/modules/S3/index.ts": { - "object-shorthand": { - "count": 3 - }, - "simple-import-sort/imports": { - "count": 1 - } - }, "src/server/routers/lambda/__tests__/integration/aiAgent.createClientGroupAgentTaskThread.integration.test.ts": { "@typescript-eslint/no-non-null-asserted-optional-chain": { "count": 2 @@ -675,11 +369,6 @@ "count": 2 } }, - "src/server/routers/lambda/__tests__/integration/message.integration.test.ts": { - "object-shorthand": { - "count": 1 - } - }, "src/server/routers/lambda/agent.ts": { "no-console": { "count": 1 @@ -725,21 +414,11 @@ "count": 2 } }, - "src/server/routers/tools/_helpers/scheduleToolCallReport.test.ts": { - "simple-import-sort/imports": { - "count": 1 - } - }, "src/server/routers/tools/mcp.ts": { "sort-keys-fix/sort-keys-fix": { "count": 1 } }, - "src/server/services/chunk/index.ts": { - "object-shorthand": { - "count": 1 - } - }, "src/server/services/comfyui/config/fluxModelRegistry.ts": { "sort-keys-fix/sort-keys-fix": { "count": 2 @@ -833,86 +512,6 @@ "count": 2 } }, - "src/server/services/mcp/index.ts": { - "object-shorthand": { - "count": 1 - } - }, - "src/server/services/memory/userMemory/extract.ts": { - "object-shorthand": { - "count": 2 - } - }, - "src/server/services/message/__tests__/index.integration.test.ts": { - "object-shorthand": { - "count": 1 - } - }, - "src/server/services/oidc/index.ts": { - "object-shorthand": { - "count": 2 - } - }, - "src/server/services/search/impls/anspire/index.ts": { - "object-shorthand": { - "count": 1 - } - }, - "src/server/services/search/impls/bocha/index.ts": { - "object-shorthand": { - "count": 1 - } - }, - "src/server/services/search/impls/brave/index.ts": { - "object-shorthand": { - "count": 1 - } - }, - "src/server/services/search/impls/exa/index.ts": { - "object-shorthand": { - "count": 1 - } - }, - "src/server/services/search/impls/firecrawl/index.ts": { - "object-shorthand": { - "count": 1 - } - }, - "src/server/services/search/impls/google/index.ts": { - "object-shorthand": { - "count": 1 - } - }, - "src/server/services/search/impls/jina/index.ts": { - "object-shorthand": { - "count": 1 - } - }, - "src/server/services/search/impls/kagi/index.ts": { - "object-shorthand": { - "count": 1 - } - }, - "src/server/services/search/impls/search1api/index.ts": { - "object-shorthand": { - "count": 1 - } - }, - "src/server/services/search/impls/tavily/index.ts": { - "object-shorthand": { - "count": 1 - } - }, - "src/server/services/search/index.ts": { - "object-shorthand": { - "count": 5 - } - }, - "src/server/services/usage/index.test.ts": { - "object-shorthand": { - "count": 11 - } - }, "src/server/services/webhookUser/index.test.ts": { "unicorn/no-thenable": { "count": 7 @@ -928,11 +527,6 @@ "count": 1 } }, - "src/services/chat/index.ts": { - "object-shorthand": { - "count": 1 - } - }, "src/services/models.ts": { "no-console": { "count": 1 @@ -963,15 +557,7 @@ "count": 1 } }, - "src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts": { - "object-shorthand": { - "count": 1 - } - }, "src/store/chat/agents/createAgentExecutors.ts": { - "object-shorthand": { - "count": 1 - }, "prefer-const": { "count": 1 }, @@ -995,16 +581,6 @@ "count": 1 } }, - "src/store/chat/slices/aiChat/actions/__tests__/StreamingHandler.test.ts": { - "unused-imports/no-unused-imports": { - "count": 1 - } - }, - "src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts": { - "object-shorthand": { - "count": 8 - } - }, "src/store/chat/slices/aiChat/actions/conversationControl.ts": { "no-unused-private-class-members": { "count": 1 @@ -1063,11 +639,6 @@ "count": 1 } }, - "src/store/chat/slices/plugin/action.test.ts": { - "object-shorthand": { - "count": 3 - } - }, "src/store/chat/slices/plugin/actions/internals.ts": { "no-unused-private-class-members": { "count": 2 @@ -1212,11 +783,6 @@ "count": 1 } }, - "src/store/file/slices/document/action.ts": { - "object-shorthand": { - "count": 1 - } - }, "src/store/file/slices/tts/action.ts": { "no-unused-private-class-members": { "count": 1 @@ -1225,9 +791,6 @@ "src/store/file/slices/upload/action.ts": { "no-unused-private-class-members": { "count": 2 - }, - "object-shorthand": { - "count": 1 } }, "src/store/global/actions/workspacePane.ts": { @@ -1250,14 +813,6 @@ "count": 1 } }, - "src/store/image/slices/generationConfig/action.ts": { - "no-useless-rename": { - "count": 4 - }, - "object-shorthand": { - "count": 1 - } - }, "src/store/image/slices/generationConfig/initialState.ts": { "sort-keys-fix/sort-keys-fix": { "count": 1 @@ -1276,11 +831,6 @@ "count": 1 } }, - "src/store/page/slices/crud/action.ts": { - "object-shorthand": { - "count": 1 - } - }, "src/store/serverConfig/action.ts": { "no-unused-private-class-members": { "count": 1 @@ -1292,9 +842,6 @@ } }, "src/store/session/slices/session/action.ts": { - "object-shorthand": { - "count": 1 - }, "typescript-sort-keys/interface": { "count": 1 } @@ -1315,9 +862,6 @@ "src/store/tool/slices/mcpStore/action.ts": { "no-console": { "count": 1 - }, - "object-shorthand": { - "count": 3 } }, "src/store/tool/slices/oldStore/initialState.ts": { @@ -1330,16 +874,6 @@ "count": 1 } }, - "src/store/video/initialState.ts": { - "simple-import-sort/imports": { - "count": 1 - } - }, - "src/store/video/store.ts": { - "simple-import-sort/imports": { - "count": 1 - } - }, "src/styles/antdOverride.ts": { "unicorn/no-anonymous-default-export": { "count": 1 diff --git a/eslint.config.mjs b/eslint.config.mjs index 6397a257ef..5d31b65e94 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -92,4 +92,11 @@ export default eslint( 'unicorn/prefer-top-level-await': 0, }, }, + // E2E and test files - allow console.log for debugging + { + files: ['e2e/**/*', '**/*.test.ts', '**/*.test.tsx'], + rules: { + 'no-console': 0, + }, + }, ); diff --git a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/Editing.tsx b/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/Editing.tsx deleted file mode 100644 index 39b60f8761..0000000000 --- a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/Editing.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { type GroupMemberAvatar } from '@lobechat/types'; -import { Avatar, Block, Flexbox, Input, stopPropagation } from '@lobehub/ui'; -import { type InputRef, message, Popover } from 'antd'; -import { memo, useCallback, useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import EmojiPicker from '@/components/EmojiPicker'; -import GroupAvatar from '@/features/GroupAvatar'; -import { useIsDark } from '@/hooks/useIsDark'; -import { useFileStore } from '@/store/file'; -import { useGlobalStore } from '@/store/global'; -import { globalGeneralSelectors } from '@/store/global/selectors'; -import { useHomeStore } from '@/store/home'; - -const MAX_AVATAR_SIZE = 1024 * 1024; - -interface EditingProps { - avatar?: string; - id: string; - memberAvatars?: GroupMemberAvatar[]; - title: string; - toggleEditing: (visible?: boolean) => void; -} - -const Editing = memo(({ id, title, avatar, memberAvatars, toggleEditing }) => { - const { t } = useTranslation('setting'); - const locale = useGlobalStore(globalGeneralSelectors.currentLanguage); - - const isDarkMode = useIsDark(); - - const editing = useHomeStore((s) => s.groupRenamingId === id); - const uploadWithProgress = useFileStore((s) => s.uploadWithProgress); - - const [newTitle, setNewTitle] = useState(title); - const [newAvatar, setNewAvatar] = useState(avatar); - const [uploading, setUploading] = useState(false); - - const inputRef = useRef(null); - - useEffect(() => { - if (editing) { - const timer = setTimeout(() => { - inputRef.current?.focus(); - }, 100); - return () => clearTimeout(timer); - } - }, [editing]); - - const handleUpdate = useCallback(async () => { - const hasChanges = (newTitle && title !== newTitle) || newAvatar !== avatar; - - if (hasChanges) { - try { - useHomeStore.getState().setGroupUpdatingId(id); - await useHomeStore - .getState() - .renameAgentGroup(id, newTitle || title, newAvatar !== avatar ? newAvatar : undefined); - } finally { - useHomeStore.getState().setGroupUpdatingId(null); - } - } - toggleEditing(false); - }, [newTitle, newAvatar, title, avatar, id, toggleEditing]); - - const handleAvatarUpload = useCallback( - async (file: File) => { - if (file.size > MAX_AVATAR_SIZE) { - message.error(t('settingAgent.avatar.sizeExceeded')); - return; - } - - setUploading(true); - try { - const result = await uploadWithProgress({ file }); - if (result?.url) { - setNewAvatar(result.url); - } - } finally { - setUploading(false); - } - }, - [uploadWithProgress, t], - ); - - const handleAvatarDelete = useCallback(() => { - setNewAvatar(null); - }, []); - - return ( - - ( - e.stopPropagation()} - > - {avatarValue ? ( - - ) : ( - - )} - - )} - onChange={setNewAvatar} - onDelete={handleAvatarDelete} - onUpload={handleAvatarUpload} - /> - setNewTitle(e.target.value)} - onPressEnter={() => handleUpdate()} - onKeyDown={(e) => { - if (e.key === 'Escape') toggleEditing(false); - }} - /> - - } - onOpenChange={(open) => { - if (!open) handleUpdate(); - }} - > -
- - ); -}); - -export default Editing; diff --git a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx b/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx index abcce067e4..58b2ce7fe6 100644 --- a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx +++ b/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx @@ -4,7 +4,7 @@ import { ActionIcon, Icon } from '@lobehub/ui'; import { cssVar } from 'antd-style'; import { Loader2, PinIcon } from 'lucide-react'; import { type CSSProperties, type DragEvent } from 'react'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; @@ -14,7 +14,6 @@ import { useGlobalStore } from '@/store/global'; import { useHomeStore } from '@/store/home'; import Actions from '../Item/Actions'; -import Editing from './Editing'; import { useGroupDropdownMenu } from './useDropdownMenu'; interface GroupItemProps { @@ -24,16 +23,13 @@ interface GroupItemProps { } const GroupItem = memo(({ item, style, className }) => { - const { id, avatar, backgroundColor, groupAvatar, title, pinned } = item; + const { id, avatar, backgroundColor, title, pinned } = item; const { t } = useTranslation('chat'); + const [anchor, setAnchor] = useState(null); const openAgentInNewWindow = useGlobalStore((s) => s.openAgentInNewWindow); - // Get UI state from homeStore (editing, updating) - const [editing, isUpdating] = useHomeStore((s) => [ - s.groupRenamingId === id, - s.groupUpdatingId === id, - ]); + const isUpdating = useHomeStore((s) => s.groupUpdatingId === id); // Get display title with fallback const displayTitle = title || t('untitledAgent'); @@ -62,13 +58,6 @@ const GroupItem = memo(({ item, style, className }) => { [id, openAgentInNewWindow], ); - const toggleEditing = useCallback( - (visible?: boolean) => { - useHomeStore.getState().setGroupRenamingId(visible ? id : null); - }, - [id], - ); - // Memoize pin icon const pinIcon = useMemo( () => @@ -99,39 +88,36 @@ const GroupItem = memo(({ item, style, className }) => { ); }, [isUpdating, avatar, backgroundColor]); + const customAvatar = typeof avatar === 'string' ? avatar : undefined; + const memberAvatars = Array.isArray(avatar) ? avatar : []; + const dropdownMenu = useGroupDropdownMenu({ + anchor, + avatar: customAvatar, id, + memberAvatars, pinned: pinned ?? false, - toggleEditing, + title: displayTitle, }); return ( - <> - - } - className={className} - contextMenuItems={dropdownMenu} - disabled={editing || isUpdating} - draggable={!editing && !isUpdating} - extra={pinIcon} - icon={avatarIcon} - key={id} - style={style} - title={displayTitle} - onDoubleClick={handleDoubleClick} - onDragEnd={handleDragEnd} - onDragStart={handleDragStart} - /> - - + } + className={className} + contextMenuItems={dropdownMenu} + disabled={isUpdating} + draggable={!isUpdating} + extra={pinIcon} + icon={avatarIcon} + key={id} + style={style} title={displayTitle} - toggleEditing={toggleEditing} + onDoubleClick={handleDoubleClick} + onDragEnd={handleDragEnd} + onDragStart={handleDragStart} /> - + ); }); diff --git a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/useDropdownMenu.tsx b/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/useDropdownMenu.tsx index 275b05ca44..848d16341e 100644 --- a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/useDropdownMenu.tsx +++ b/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/useDropdownMenu.tsx @@ -5,19 +5,26 @@ import { LucideCopy, Pen, PictureInPicture2Icon, Pin, PinOff, Trash } from 'luci import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { openEditingPopover } from '@/features/EditingPopover/store'; import { useGlobalStore } from '@/store/global'; import { useHomeStore } from '@/store/home'; interface UseGroupDropdownMenuParams { + anchor: HTMLElement | null; + avatar?: string; id: string; + memberAvatars?: { avatar?: string; background?: string }[]; pinned: boolean; - toggleEditing: (visible?: boolean) => void; + title: string; } export const useGroupDropdownMenu = ({ + anchor, + avatar, id, + memberAvatars, pinned, - toggleEditing, + title, }: UseGroupDropdownMenuParams): (() => MenuProps['items']) => { const { t } = useTranslation('chat'); const { modal, message } = App.useApp(); @@ -44,7 +51,9 @@ export const useGroupDropdownMenu = ({ label: t('rename', { ns: 'common' }), onClick: (info: any) => { info.domEvent?.stopPropagation(); - toggleEditing(true); + if (anchor) { + openEditingPopover({ anchor, avatar, id, memberAvatars, title, type: 'agentGroup' }); + } }, }, { @@ -86,11 +95,14 @@ export const useGroupDropdownMenu = ({ }, ] as MenuProps['items'], [ + anchor, + avatar, + memberAvatars, t, pinned, pinAgentGroup, id, - toggleEditing, + title, duplicateAgentGroup, openAgentInNewWindow, modal, diff --git a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/Editing.tsx b/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/Editing.tsx deleted file mode 100644 index f1788ff64f..0000000000 --- a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/Editing.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { DEFAULT_AVATAR } from '@lobechat/const'; -import { Flexbox, Input, stopPropagation } from '@lobehub/ui'; -import { type InputRef, message, Popover } from 'antd'; -import { memo, useCallback, useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import EmojiPicker from '@/components/EmojiPicker'; -import { useAgentStore } from '@/store/agent'; -import { useFileStore } from '@/store/file'; -import { useGlobalStore } from '@/store/global'; -import { globalGeneralSelectors } from '@/store/global/selectors'; -import { useHomeStore } from '@/store/home'; - -const MAX_AVATAR_SIZE = 1024 * 1024; - -interface EditingProps { - avatar?: string; - id: string; - title: string; - toggleEditing: (visible?: boolean) => void; -} - -const Editing = memo(({ id, title, avatar, toggleEditing }) => { - const { t } = useTranslation('setting'); - const locale = useGlobalStore(globalGeneralSelectors.currentLanguage); - - const editing = useHomeStore((s) => s.agentRenamingId === id); - const uploadWithProgress = useFileStore((s) => s.uploadWithProgress); - - const currentAvatar = avatar || DEFAULT_AVATAR; - - const [newTitle, setNewTitle] = useState(title); - const [newAvatar, setNewAvatar] = useState(currentAvatar); - const [uploading, setUploading] = useState(false); - - const inputRef = useRef(null); - - useEffect(() => { - if (editing) { - const timer = setTimeout(() => { - inputRef.current?.focus(); - }, 100); - return () => clearTimeout(timer); - } - }, [editing]); - - const handleUpdate = useCallback(async () => { - const hasChanges = - (newTitle && title !== newTitle) || (newAvatar && currentAvatar !== newAvatar); - - if (hasChanges) { - try { - useHomeStore.getState().setAgentUpdatingId(id); - - const updates: { avatar?: string; title?: string } = {}; - if (newTitle && title !== newTitle) updates.title = newTitle; - if (newAvatar && currentAvatar !== newAvatar) updates.avatar = newAvatar; - - await useAgentStore.getState().optimisticUpdateAgentMeta(id, updates); - await useHomeStore.getState().refreshAgentList(); - } finally { - useHomeStore.getState().setAgentUpdatingId(null); - } - } - toggleEditing(false); - }, [newTitle, newAvatar, title, currentAvatar, id, toggleEditing]); - - const handleAvatarUpload = useCallback( - async (file: File) => { - if (file.size > MAX_AVATAR_SIZE) { - message.error(t('settingAgent.avatar.sizeExceeded')); - return; - } - - setUploading(true); - try { - const result = await uploadWithProgress({ file }); - if (result?.url) { - setNewAvatar(result.url); - } - } finally { - setUploading(false); - } - }, - [uploadWithProgress, t], - ); - - const handleAvatarDelete = useCallback(() => { - setNewAvatar(DEFAULT_AVATAR); - }, []); - - return ( - - - setNewTitle(e.target.value)} - onPressEnter={() => handleUpdate()} - onKeyDown={(e) => { - if (e.key === 'Escape') toggleEditing(false); - }} - /> - - } - onOpenChange={(open) => { - if (!open) handleUpdate(); - }} - > -
- - ); -}); - -export default Editing; diff --git a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx b/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx index c50bee2dde..d27eeab58d 100644 --- a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +++ b/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx @@ -4,7 +4,7 @@ import { ActionIcon, Icon } from '@lobehub/ui'; import { cssVar } from 'antd-style'; import { Loader2, PinIcon } from 'lucide-react'; import { type CSSProperties, type DragEvent } from 'react'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; @@ -17,7 +17,6 @@ import { useHomeStore } from '@/store/home'; import { useAgentModal } from '../../ModalProvider'; import Actions from '../Item/Actions'; import Avatar from './Avatar'; -import Editing from './Editing'; import { useAgentDropdownMenu } from './useDropdownMenu'; interface AgentItemProps { @@ -30,13 +29,10 @@ const AgentItem = memo(({ item, style, className }) => { const { id, avatar, title, pinned } = item; const { t } = useTranslation('chat'); const { openCreateGroupModal } = useAgentModal(); + const [anchor, setAnchor] = useState(null); const openAgentInNewWindow = useGlobalStore((s) => s.openAgentInNewWindow); - // Get UI state from homeStore (editing, updating) - const [editing, isUpdating] = useHomeStore((s) => [ - s.agentRenamingId === id, - s.agentUpdatingId === id, - ]); + const isUpdating = useHomeStore((s) => s.agentUpdatingId === id); // Separate loading state from chat store - only show loading for this specific agent const isLoading = useChatStore(operationSelectors.isAgentRunning(id)); @@ -72,13 +68,6 @@ const AgentItem = memo(({ item, style, className }) => { openCreateGroupModal(id); }, [id, openCreateGroupModal]); - const toggleEditing = useCallback( - (visible?: boolean) => { - useHomeStore.getState().setAgentRenamingId(visible ? id : null); - }, - [id], - ); - // Memoize pin icon const pinIcon = useMemo( () => @@ -98,41 +87,34 @@ const AgentItem = memo(({ item, style, className }) => { }, [isUpdating, avatar]); const dropdownMenu = useAgentDropdownMenu({ + anchor, + avatar: typeof avatar === 'string' ? avatar : undefined, group: undefined, // TODO: pass group from parent if needed id, openCreateGroupModal: handleOpenCreateGroupModal, pinned: pinned ?? false, - toggleEditing, + title: displayTitle, }); return ( - <> - - } - className={className} - contextMenuItems={dropdownMenu} - disabled={editing || isUpdating} - draggable={!editing && !isUpdating} - extra={pinIcon} - icon={avatarIcon} - key={id} - loading={isLoading} - style={style} - title={displayTitle} - onDoubleClick={handleDoubleClick} - onDragEnd={handleDragEnd} - onDragStart={handleDragStart} - /> - - - + } + className={className} + contextMenuItems={dropdownMenu} + disabled={isUpdating} + draggable={!isUpdating} + extra={pinIcon} + icon={avatarIcon} + key={id} + loading={isLoading} + style={style} title={displayTitle} - toggleEditing={toggleEditing} + onDoubleClick={handleDoubleClick} + onDragEnd={handleDragEnd} + onDragStart={handleDragStart} /> - + ); }); diff --git a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/useDropdownMenu.tsx b/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/useDropdownMenu.tsx index d77847ff98..8926f1234f 100644 --- a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/useDropdownMenu.tsx +++ b/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/useDropdownMenu.tsx @@ -17,24 +17,29 @@ import { import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { openEditingPopover } from '@/features/EditingPopover/store'; import { useGlobalStore } from '@/store/global'; import { useHomeStore } from '@/store/home'; import { homeAgentListSelectors } from '@/store/home/selectors'; interface UseAgentDropdownMenuParams { + anchor: HTMLElement | null; + avatar?: string; group: string | undefined; id: string; openCreateGroupModal: () => void; pinned: boolean; - toggleEditing: (visible?: boolean) => void; + title: string; } export const useAgentDropdownMenu = ({ + anchor, + avatar, group, id, openCreateGroupModal, pinned, - toggleEditing, + title, }: UseAgentDropdownMenuParams): (() => MenuProps['items']) => { const { t } = useTranslation('chat'); const { modal, message } = App.useApp(); @@ -65,7 +70,9 @@ export const useAgentDropdownMenu = ({ label: t('rename', { ns: 'common' }), onClick: (info: any) => { info.domEvent?.stopPropagation(); - toggleEditing(true); + if (anchor) { + openEditingPopover({ anchor, avatar, id, title, type: 'agent' }); + } }, }, { @@ -137,9 +144,11 @@ export const useAgentDropdownMenu = ({ }, ] as MenuProps['items'], [ + anchor, pinned, id, - toggleEditing, + avatar, + title, sessionCustomGroups, group, isDefault, diff --git a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Group/Editing.tsx b/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Group/Editing.tsx deleted file mode 100644 index 8d3a0823f1..0000000000 --- a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Group/Editing.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Input, stopPropagation } from '@lobehub/ui'; -import { type InputRef, Popover } from 'antd'; -import { memo, useCallback, useEffect, useRef, useState } from 'react'; - -import { useHomeStore } from '@/store/home'; - -interface EditingProps { - id: string; - name: string; - toggleEditing: (visible?: boolean) => void; -} - -const Editing = memo(({ id, name, toggleEditing }) => { - const [newName, setNewName] = useState(name); - const [editing, updateGroupName] = useHomeStore((s) => [ - s.groupRenamingId === id, - s.updateGroupName, - ]); - - const inputRef = useRef(null); - - useEffect(() => { - if (editing) { - const timer = setTimeout(() => { - inputRef.current?.focus(); - }, 100); - return () => clearTimeout(timer); - } - }, [editing]); - - const handleUpdate = useCallback(async () => { - if (newName && name !== newName) { - try { - useHomeStore.getState().setGroupUpdatingId(id); - await updateGroupName(id, newName); - } finally { - useHomeStore.getState().setGroupUpdatingId(null); - } - } - toggleEditing(false); - }, [newName, name, id, updateGroupName, toggleEditing]); - - return ( - handleUpdate()} - onChange={(e) => setNewName(e.target.value)} - onClick={stopPropagation} - onPressEnter={() => handleUpdate()} - onKeyDown={(e) => { - if (e.key === 'Escape') toggleEditing(false); - }} - /> - } - > -
- - ); -}); - -export default Editing; diff --git a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Group/Item.tsx b/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Group/Item.tsx index 390595eaa6..7df0941544 100644 --- a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Group/Item.tsx +++ b/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Group/Item.tsx @@ -2,7 +2,7 @@ import { type SidebarGroup } from '@lobechat/types'; import { AccordionItem, ContextMenuTrigger, Flexbox, Icon, Text } from '@lobehub/ui'; import { createStaticStyles } from 'antd-style'; import { HashIcon, Loader2 } from 'lucide-react'; -import React, { memo, useCallback, useMemo } from 'react'; +import React, { memo, useCallback, useMemo, useState } from 'react'; import { useHomeStore } from '@/store/home'; @@ -10,7 +10,6 @@ import { useCreateMenuItems } from '../../../../hooks'; import { useAgentModal } from '../../ModalProvider'; import SessionList from '../List'; import Actions from './Actions'; -import Editing from './Editing'; import { useGroupDropdownMenu } from './useDropdownMenu'; const styles = createStaticStyles(({ css }) => ({ @@ -20,10 +19,8 @@ const styles = createStaticStyles(({ css }) => ({ })); const GroupItem = memo(({ items, id, name }) => { - const [editing, isUpdating] = useHomeStore((s) => [ - s.groupRenamingId === id, - s.groupUpdatingId === id, - ]); + const [anchor, setAnchor] = useState(null); + const isUpdating = useHomeStore((s) => s.groupUpdatingId === id); // Modal management const { openConfigGroupModal } = useAgentModal(); @@ -31,22 +28,16 @@ const GroupItem = memo(({ items, id, name }) => { // Create menu items const { isLoading } = useCreateMenuItems(); - const toggleEditing = useCallback( - (visible?: boolean) => { - useHomeStore.getState().setGroupRenamingId(visible ? id : null); - }, - [id], - ); - const handleOpenConfigGroupModal = useCallback(() => { openConfigGroupModal(); }, [openConfigGroupModal]); const dropdownMenu = useGroupDropdownMenu({ + anchor, id, isCustomGroup: true, + name, openConfigGroupModal: handleOpenConfigGroupModal, - toggleEditing, }); const groupIcon = useMemo(() => { @@ -59,13 +50,15 @@ const GroupItem = memo(({ items, id, name }) => { return ( } - disabled={editing || isUpdating} + disabled={isUpdating} itemKey={id} key={id} paddingBlock={4} paddingInline={'8px 4px'} headerWrapper={(header) => ( - {header} + +
{header}
+
)} title={ @@ -76,7 +69,6 @@ const GroupItem = memo(({ items, id, name }) => { } > -
); diff --git a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Group/useDropdownMenu.tsx b/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Group/useDropdownMenu.tsx index d79741daea..ae4be2361f 100644 --- a/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Group/useDropdownMenu.tsx +++ b/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Group/useDropdownMenu.tsx @@ -4,18 +4,20 @@ import { useMemo } from 'react'; import { useCreateMenuItems, useSessionGroupMenuItems } from '../../../../hooks'; interface GroupDropdownMenuProps { + anchor: HTMLElement | null; id?: string; isCustomGroup?: boolean; isPinned?: boolean; + name?: string; openConfigGroupModal: () => void; - toggleEditing?: (visible?: boolean) => void; } export const useGroupDropdownMenu = ({ + anchor, id, isCustomGroup, isPinned, - toggleEditing, + name, openConfigGroupModal, }: GroupDropdownMenuProps): MenuProps['items'] => { // Session group menu items @@ -29,7 +31,7 @@ export const useGroupDropdownMenu = ({ const createAgentItem = createAgentMenuItem({ groupId: id, isPinned }); const createGroupChatItem = createGroupChatMenuItem({ groupId: id }); const configItem = configGroupMenuItem(openConfigGroupModal); - const renameItem = toggleEditing ? renameGroupMenuItem(toggleEditing) : null; + const renameItem = id && name ? renameGroupMenuItem(id, name, anchor) : null; const deleteItem = id ? deleteGroupMenuItem(id) : null; return [ @@ -41,10 +43,11 @@ export const useGroupDropdownMenu = ({ : [configItem]), ].filter(Boolean) as MenuProps['items']; }, [ + anchor, isCustomGroup, id, isPinned, - toggleEditing, + name, createAgentMenuItem, createGroupChatMenuItem, configGroupMenuItem, diff --git a/src/app/[variants]/(main)/home/_layout/Body/Agent/ModalProvider.tsx b/src/app/[variants]/(main)/home/_layout/Body/Agent/ModalProvider.tsx index 10eae74217..bb0e4fd032 100644 --- a/src/app/[variants]/(main)/home/_layout/Body/Agent/ModalProvider.tsx +++ b/src/app/[variants]/(main)/home/_layout/Body/Agent/ModalProvider.tsx @@ -5,6 +5,7 @@ import { createContext, memo, use, useMemo, useState } from 'react'; import { ChatGroupWizard } from '@/components/ChatGroupWizard'; import { MemberSelectionModal } from '@/components/MemberSelectionModal'; +import EditingPopover from '@/features/EditingPopover'; import ConfigGroupModal from './Modals/ConfigGroupModal'; import CreateGroupModal from './Modals/CreateGroupModal'; @@ -139,6 +140,8 @@ export const AgentModalProvider = memo(({ children }) = await memberSelectionCallbacks.onConfirm?.(selectedAgents); }} /> + + ); }); diff --git a/src/app/[variants]/(main)/home/_layout/hooks/useSessionGroupMenuItems.tsx b/src/app/[variants]/(main)/home/_layout/hooks/useSessionGroupMenuItems.tsx index 9d59f1ee40..476659e9f6 100644 --- a/src/app/[variants]/(main)/home/_layout/hooks/useSessionGroupMenuItems.tsx +++ b/src/app/[variants]/(main)/home/_layout/hooks/useSessionGroupMenuItems.tsx @@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next'; import { useGroupTemplates } from '@/components/ChatGroupWizard/templates'; import { DEFAULT_CHAT_GROUP_CHAT_CONFIG } from '@/const/settings'; +import { openEditingPopover } from '@/features/EditingPopover/store'; import { useAgentStore } from '@/store/agent'; import { useAgentGroupStore } from '@/store/agentGroup'; import { useHomeStore } from '@/store/home'; @@ -38,7 +39,7 @@ export const useSessionGroupMenuItems = () => { * Rename group menu item */ const renameGroupMenuItem = useCallback( - (onToggleEdit: (visible?: boolean) => void): ItemType => { + (groupId: string, groupName: string, anchor: HTMLElement | null): ItemType => { const iconElement = ; return { icon: iconElement, @@ -46,7 +47,9 @@ export const useSessionGroupMenuItems = () => { label: t('sessionGroup.rename'), onClick: (info: any) => { info.domEvent?.stopPropagation(); - onToggleEdit(true); + if (anchor) { + openEditingPopover({ anchor, id: groupId, title: groupName, type: 'group' }); + } }, }; }, diff --git a/src/features/EditingPopover/AgentContent.tsx b/src/features/EditingPopover/AgentContent.tsx new file mode 100644 index 0000000000..0c13768f86 --- /dev/null +++ b/src/features/EditingPopover/AgentContent.tsx @@ -0,0 +1,92 @@ +import { ActionIcon, Avatar, Block, Flexbox, Input, stopPropagation } from '@lobehub/ui'; +import { type InputRef } from 'antd'; +import { Check } from 'lucide-react'; +import { memo, useCallback, useEffect, useRef, useState } from 'react'; + +import EmojiPicker from '@/components/EmojiPicker'; +import { useIsDark } from '@/hooks/useIsDark'; +import { useAgentStore } from '@/store/agent'; +import { useGlobalStore } from '@/store/global'; +import { globalGeneralSelectors } from '@/store/global/selectors'; +import { useHomeStore } from '@/store/home'; + +interface AgentContentProps { + avatar?: string; + id: string; + onClose: () => void; + title: string; +} + +const AgentContent = memo(({ id, title, avatar, onClose }) => { + const locale = useGlobalStore(globalGeneralSelectors.currentLanguage); + const isDarkMode = useIsDark(); + + const currentAvatar = avatar || ''; + + const [newTitle, setNewTitle] = useState(title); + const [newAvatar, setNewAvatar] = useState(currentAvatar); + + const handleUpdate = useCallback(async () => { + const hasChanges = + (newTitle && title !== newTitle) || (newAvatar && currentAvatar !== newAvatar); + + if (hasChanges) { + try { + useHomeStore.getState().setAgentUpdatingId(id); + + const updates: { avatar?: string; title?: string } = {}; + if (newTitle && title !== newTitle) updates.title = newTitle; + if (newAvatar && currentAvatar !== newAvatar) updates.avatar = newAvatar; + + await useAgentStore.getState().optimisticUpdateAgentMeta(id, updates); + await useHomeStore.getState().refreshAgentList(); + } finally { + useHomeStore.getState().setAgentUpdatingId(null); + } + } + onClose(); + }, [newTitle, newAvatar, title, currentAvatar, id, onClose]); + const inputRef = useRef(null); + useEffect(() => { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }); + }); + }, []); + return ( + + ( + + + + )} + onChange={setNewAvatar} + /> + setNewTitle(e.target.value)} + onPressEnter={handleUpdate} + /> + + + ); +}); + +export default AgentContent; diff --git a/src/features/EditingPopover/GroupContent.tsx b/src/features/EditingPopover/GroupContent.tsx new file mode 100644 index 0000000000..f91cd5fbd9 --- /dev/null +++ b/src/features/EditingPopover/GroupContent.tsx @@ -0,0 +1,146 @@ +import { ActionIcon, Avatar, Block, Flexbox, Input, stopPropagation } from '@lobehub/ui'; +import { type InputRef, message } from 'antd'; +import { Check } from 'lucide-react'; +import { memo, useCallback, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import EmojiPicker from '@/components/EmojiPicker'; +import GroupAvatar from '@/features/GroupAvatar'; +import { useIsDark } from '@/hooks/useIsDark'; +import { useFileStore } from '@/store/file'; +import { useGlobalStore } from '@/store/global'; +import { globalGeneralSelectors } from '@/store/global/selectors'; +import { useHomeStore } from '@/store/home'; + +const MAX_AVATAR_SIZE = 1024 * 1024; + +interface GroupContentProps { + avatar?: string; + id: string; + memberAvatars?: { avatar?: string; background?: string }[]; + onClose: () => void; + title: string; + type: 'group' | 'agentGroup'; +} + +const GroupContent = memo( + ({ id, title, avatar, memberAvatars, type, onClose }) => { + const { t } = useTranslation('setting'); + const locale = useGlobalStore(globalGeneralSelectors.currentLanguage); + const isDarkMode = useIsDark(); + const uploadWithProgress = useFileStore((s) => s.uploadWithProgress); + + const isAgentGroup = type === 'agentGroup'; + + const [newTitle, setNewTitle] = useState(title); + const [newAvatar, setNewAvatar] = useState(avatar); + const [uploading, setUploading] = useState(false); + + const handleUpdate = useCallback(async () => { + const titleChanged = newTitle && title !== newTitle; + const avatarChanged = isAgentGroup && newAvatar !== avatar; + + if (titleChanged || avatarChanged) { + try { + useHomeStore.getState().setGroupUpdatingId(id); + + if (type === 'group') { + await useHomeStore.getState().updateGroupName(id, newTitle); + } else { + await useHomeStore + .getState() + .renameAgentGroup(id, newTitle || title, avatarChanged ? newAvatar : undefined); + } + } finally { + useHomeStore.getState().setGroupUpdatingId(null); + } + } + onClose(); + }, [newTitle, newAvatar, title, avatar, id, type, isAgentGroup, onClose]); + + const handleAvatarUpload = useCallback( + async (file: File) => { + if (file.size > MAX_AVATAR_SIZE) { + message.error(t('settingAgent.avatar.sizeExceeded')); + return; + } + + setUploading(true); + try { + const result = await uploadWithProgress({ file }); + if (result?.url) { + setNewAvatar(result.url); + } + } finally { + setUploading(false); + } + }, + [uploadWithProgress, t], + ); + + const handleAvatarDelete = useCallback(() => { + setNewAvatar(null); + }, []); + + const inputRef = useRef(null); + useEffect(() => { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }); + }); + }, []); + + return ( + + {isAgentGroup && ( + ( + + {avatarValue ? ( + + ) : ( + + )} + + )} + onChange={setNewAvatar} + onDelete={handleAvatarDelete} + onUpload={handleAvatarUpload} + /> + )} + setNewTitle(e.target.value)} + onPressEnter={handleUpdate} + /> + + + ); + }, +); + +export default GroupContent; diff --git a/src/features/EditingPopover/index.tsx b/src/features/EditingPopover/index.tsx new file mode 100644 index 0000000000..4e230c336b --- /dev/null +++ b/src/features/EditingPopover/index.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { PopoverPopup, PopoverPortal, PopoverPositioner, PopoverRoot } from '@lobehub/ui'; + +import AgentContent from './AgentContent'; +import GroupContent from './GroupContent'; +import { useEditingPopoverStore } from './store'; + +const EditingPopover = () => { + const target = useEditingPopoverStore((s) => s.target); + const close = useEditingPopoverStore((s) => s.close); + + return ( + { + if (!open) close(); + }} + > + + + + {target?.type === 'agent' ? ( + + ) : target ? ( + + ) : null} + + + + + ); +}; + +export default EditingPopover; diff --git a/src/features/EditingPopover/store.ts b/src/features/EditingPopover/store.ts new file mode 100644 index 0000000000..57c31b0ade --- /dev/null +++ b/src/features/EditingPopover/store.ts @@ -0,0 +1,27 @@ +import { create } from 'zustand'; + +export interface EditingTarget { + anchor: HTMLElement; + avatar?: string; + id: string; + memberAvatars?: { avatar?: string; background?: string }[]; + title: string; + type: 'agent' | 'group' | 'agentGroup'; +} + +interface EditingPopoverState { + close: () => void; + open: (target: EditingTarget) => void; + target: EditingTarget | null; +} + +export const useEditingPopoverStore = create((set) => ({ + close: () => set({ target: null }), + open: (target) => set({ target }), + target: null, +})); + +export const openEditingPopover = (target: EditingTarget) => + useEditingPopoverStore.getState().open(target); + +export const closeEditingPopover = () => useEditingPopoverStore.getState().close(); diff --git a/src/store/home/slices/sidebarUI/action.test.ts b/src/store/home/slices/sidebarUI/action.test.ts index 4f1b75bdac..62ae2cab5d 100644 --- a/src/store/home/slices/sidebarUI/action.test.ts +++ b/src/store/home/slices/sidebarUI/action.test.ts @@ -317,32 +317,6 @@ describe('createSidebarUISlice', () => { }); // ========== UI State Actions ========== - describe('setAgentRenamingId', () => { - it('should set agent renaming id', () => { - const { result } = renderHook(() => useHomeStore()); - - act(() => { - result.current.setAgentRenamingId('agent-123'); - }); - - expect(result.current.agentRenamingId).toBe('agent-123'); - }); - - it('should clear agent renaming id when set to null', () => { - const { result } = renderHook(() => useHomeStore()); - - act(() => { - result.current.setAgentRenamingId('agent-123'); - }); - - act(() => { - result.current.setAgentRenamingId(null); - }); - - expect(result.current.agentRenamingId).toBeNull(); - }); - }); - describe('setAgentUpdatingId', () => { it('should set agent updating id', () => { const { result } = renderHook(() => useHomeStore()); @@ -369,32 +343,6 @@ describe('createSidebarUISlice', () => { }); }); - describe('setGroupRenamingId', () => { - it('should set group renaming id', () => { - const { result } = renderHook(() => useHomeStore()); - - act(() => { - result.current.setGroupRenamingId('group-123'); - }); - - expect(result.current.groupRenamingId).toBe('group-123'); - }); - - it('should clear group renaming id when set to null', () => { - const { result } = renderHook(() => useHomeStore()); - - act(() => { - result.current.setGroupRenamingId('group-123'); - }); - - act(() => { - result.current.setGroupRenamingId(null); - }); - - expect(result.current.groupRenamingId).toBeNull(); - }); - }); - describe('setGroupUpdatingId', () => { it('should set group updating id', () => { const { result } = renderHook(() => useHomeStore()); diff --git a/src/store/home/slices/sidebarUI/action.ts b/src/store/home/slices/sidebarUI/action.ts index d3fb968a23..5dfb235949 100644 --- a/src/store/home/slices/sidebarUI/action.ts +++ b/src/store/home/slices/sidebarUI/action.ts @@ -146,18 +146,10 @@ export class SidebarUIActionImpl { await this.#get().refreshAgentList(); }; - setAgentRenamingId = (id: string | null): void => { - this.#set({ agentRenamingId: id }, false, n('setAgentRenamingId')); - }; - setAgentUpdatingId = (id: string | null): void => { this.#set({ agentUpdatingId: id }, false, n('setAgentUpdatingId')); }; - setGroupRenamingId = (id: string | null): void => { - this.#set({ groupRenamingId: id }, false, n('setGroupRenamingId')); - }; - setGroupUpdatingId = (id: string | null): void => { this.#set({ groupUpdatingId: id }, false, n('setGroupUpdatingId')); }; diff --git a/src/store/home/slices/sidebarUI/initialState.ts b/src/store/home/slices/sidebarUI/initialState.ts index a94fbbda80..90d5512cf8 100644 --- a/src/store/home/slices/sidebarUI/initialState.ts +++ b/src/store/home/slices/sidebarUI/initialState.ts @@ -1,16 +1,8 @@ export interface SidebarUIState { - /** - * ID of the agent currently being renamed - */ - agentRenamingId: string | null; /** * ID of the agent currently being updated */ agentUpdatingId: string | null; - /** - * ID of the group currently being renamed - */ - groupRenamingId: string | null; /** * ID of the group currently being updated */ @@ -18,8 +10,6 @@ export interface SidebarUIState { } export const initialSidebarUIState: SidebarUIState = { - agentRenamingId: null, agentUpdatingId: null, - groupRenamingId: null, groupUpdatingId: null, };