mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
♻️ refactor: replace per-item Editing components with singleton EditingPopover (#12327)
* ♻️ 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 <tukon479@gmail.com> * ✅ 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 <tukon479@gmail.com>
This commit is contained in:
@@ -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<void> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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<EditingProps>(({ 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<string | null | undefined>(avatar);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const inputRef = useRef<InputRef>(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 (
|
||||
<Popover
|
||||
arrow={false}
|
||||
open={editing}
|
||||
overlayInnerStyle={{ padding: 4 }}
|
||||
placement="bottomLeft"
|
||||
trigger="click"
|
||||
content={
|
||||
<Flexbox horizontal gap={4} style={{ width: 320 }} onClick={stopPropagation}>
|
||||
<EmojiPicker
|
||||
allowUpload
|
||||
allowDelete={!!newAvatar}
|
||||
loading={uploading}
|
||||
locale={locale}
|
||||
shape={'square'}
|
||||
value={newAvatar ?? undefined}
|
||||
customRender={(avatarValue) => (
|
||||
<Block
|
||||
clickable
|
||||
align={'center'}
|
||||
height={36}
|
||||
justify={'center'}
|
||||
variant={isDarkMode ? 'filled' : 'outlined'}
|
||||
width={36}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{avatarValue ? (
|
||||
<Avatar
|
||||
emojiScaleWithBackground
|
||||
avatar={avatarValue}
|
||||
shape={'square'}
|
||||
size={32}
|
||||
/>
|
||||
) : (
|
||||
<GroupAvatar avatars={memberAvatars || []} size={32} />
|
||||
)}
|
||||
</Block>
|
||||
)}
|
||||
onChange={setNewAvatar}
|
||||
onDelete={handleAvatarDelete}
|
||||
onUpload={handleAvatarUpload}
|
||||
/>
|
||||
<Input
|
||||
defaultValue={title}
|
||||
ref={inputRef}
|
||||
style={{ flex: 1 }}
|
||||
onChange={(e) => setNewTitle(e.target.value)}
|
||||
onPressEnter={() => handleUpdate()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') toggleEditing(false);
|
||||
}}
|
||||
/>
|
||||
</Flexbox>
|
||||
}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) handleUpdate();
|
||||
}}
|
||||
>
|
||||
<div />
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
export default Editing;
|
||||
@@ -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<GroupItemProps>(({ 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<HTMLElement | null>(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<GroupItemProps>(({ 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<GroupItemProps>(({ 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 (
|
||||
<>
|
||||
<Link aria-label={id} to={groupUrl}>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
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}
|
||||
/>
|
||||
</Link>
|
||||
<Editing
|
||||
avatar={groupAvatar || undefined}
|
||||
id={id}
|
||||
memberAvatars={Array.isArray(avatar) ? avatar : undefined}
|
||||
<Link aria-label={id} ref={setAnchor} to={groupUrl}>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<EditingProps>(({ 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<InputRef>(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 (
|
||||
<Popover
|
||||
arrow={false}
|
||||
open={editing}
|
||||
overlayInnerStyle={{ padding: 4 }}
|
||||
placement="bottomLeft"
|
||||
trigger="click"
|
||||
content={
|
||||
<Flexbox horizontal gap={4} style={{ width: 320 }} onClick={stopPropagation}>
|
||||
<EmojiPicker
|
||||
allowUpload
|
||||
allowDelete={!!newAvatar && newAvatar !== DEFAULT_AVATAR}
|
||||
loading={uploading}
|
||||
locale={locale}
|
||||
shape={'square'}
|
||||
size={36}
|
||||
value={newAvatar}
|
||||
onChange={setNewAvatar}
|
||||
onDelete={handleAvatarDelete}
|
||||
onUpload={handleAvatarUpload}
|
||||
/>
|
||||
<Input
|
||||
defaultValue={title}
|
||||
ref={inputRef}
|
||||
style={{ flex: 1 }}
|
||||
onChange={(e) => setNewTitle(e.target.value)}
|
||||
onPressEnter={() => handleUpdate()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') toggleEditing(false);
|
||||
}}
|
||||
/>
|
||||
</Flexbox>
|
||||
}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) handleUpdate();
|
||||
}}
|
||||
>
|
||||
<div />
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
export default Editing;
|
||||
@@ -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<AgentItemProps>(({ item, style, className }) => {
|
||||
const { id, avatar, title, pinned } = item;
|
||||
const { t } = useTranslation('chat');
|
||||
const { openCreateGroupModal } = useAgentModal();
|
||||
const [anchor, setAnchor] = useState<HTMLElement | null>(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<AgentItemProps>(({ 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<AgentItemProps>(({ 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 (
|
||||
<>
|
||||
<Link aria-label={displayTitle} to={agentUrl}>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
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}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<Editing
|
||||
avatar={typeof avatar === 'string' ? avatar : undefined}
|
||||
id={id}
|
||||
<Link aria-label={displayTitle} ref={setAnchor} to={agentUrl}>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<EditingProps>(({ id, name, toggleEditing }) => {
|
||||
const [newName, setNewName] = useState(name);
|
||||
const [editing, updateGroupName] = useHomeStore((s) => [
|
||||
s.groupRenamingId === id,
|
||||
s.updateGroupName,
|
||||
]);
|
||||
|
||||
const inputRef = useRef<InputRef>(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 (
|
||||
<Popover
|
||||
arrow={false}
|
||||
open={editing}
|
||||
overlayInnerStyle={{ padding: 4, width: 320 }}
|
||||
placement="bottomLeft"
|
||||
trigger="click"
|
||||
content={
|
||||
<Input
|
||||
defaultValue={name}
|
||||
ref={inputRef}
|
||||
onBlur={() => handleUpdate()}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
onClick={stopPropagation}
|
||||
onPressEnter={() => handleUpdate()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') toggleEditing(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div />
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
export default Editing;
|
||||
@@ -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<SidebarGroup>(({ items, id, name }) => {
|
||||
const [editing, isUpdating] = useHomeStore((s) => [
|
||||
s.groupRenamingId === id,
|
||||
s.groupUpdatingId === id,
|
||||
]);
|
||||
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
|
||||
const isUpdating = useHomeStore((s) => s.groupUpdatingId === id);
|
||||
|
||||
// Modal management
|
||||
const { openConfigGroupModal } = useAgentModal();
|
||||
@@ -31,22 +28,16 @@ const GroupItem = memo<SidebarGroup>(({ 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<SidebarGroup>(({ items, id, name }) => {
|
||||
return (
|
||||
<AccordionItem
|
||||
action={<Actions dropdownMenu={dropdownMenu} isLoading={isLoading} />}
|
||||
disabled={editing || isUpdating}
|
||||
disabled={isUpdating}
|
||||
itemKey={id}
|
||||
key={id}
|
||||
paddingBlock={4}
|
||||
paddingInline={'8px 4px'}
|
||||
headerWrapper={(header) => (
|
||||
<ContextMenuTrigger items={dropdownMenu}>{header}</ContextMenuTrigger>
|
||||
<ContextMenuTrigger items={dropdownMenu}>
|
||||
<div ref={setAnchor}>{header}</div>
|
||||
</ContextMenuTrigger>
|
||||
)}
|
||||
title={
|
||||
<Flexbox horizontal align="center" gap={6} style={{ overflow: 'hidden' }}>
|
||||
@@ -76,7 +69,6 @@ const GroupItem = memo<SidebarGroup>(({ items, id, name }) => {
|
||||
</Flexbox>
|
||||
}
|
||||
>
|
||||
<Editing id={id} name={name} toggleEditing={toggleEditing} />
|
||||
<SessionList dataSource={items} groupId={id} itemClassName={styles.item} />
|
||||
</AccordionItem>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<AgentModalProviderProps>(({ children }) =
|
||||
await memberSelectionCallbacks.onConfirm?.(selectedAgents);
|
||||
}}
|
||||
/>
|
||||
|
||||
<EditingPopover />
|
||||
</AgentModalContext>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 = <Icon icon={FolderPenIcon} />;
|
||||
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' });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
92
src/features/EditingPopover/AgentContent.tsx
Normal file
92
src/features/EditingPopover/AgentContent.tsx
Normal file
@@ -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<AgentContentProps>(({ 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<InputRef>(null);
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<Flexbox horizontal align={'center'} gap={4} style={{ width: 320 }} onClick={stopPropagation}>
|
||||
<EmojiPicker
|
||||
locale={locale}
|
||||
shape={'square'}
|
||||
value={newAvatar}
|
||||
customRender={(avatarValue) => (
|
||||
<Block
|
||||
clickable
|
||||
align={'center'}
|
||||
height={36}
|
||||
justify={'center'}
|
||||
variant={isDarkMode ? 'filled' : 'outlined'}
|
||||
width={36}
|
||||
onClick={stopPropagation}
|
||||
>
|
||||
<Avatar emojiScaleWithBackground avatar={avatarValue} shape={'square'} size={32} />
|
||||
</Block>
|
||||
)}
|
||||
onChange={setNewAvatar}
|
||||
/>
|
||||
<Input
|
||||
defaultValue={title}
|
||||
ref={inputRef}
|
||||
style={{ flex: 1 }}
|
||||
onChange={(e) => setNewTitle(e.target.value)}
|
||||
onPressEnter={handleUpdate}
|
||||
/>
|
||||
<ActionIcon icon={Check} size={'small'} onClick={handleUpdate} />
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default AgentContent;
|
||||
146
src/features/EditingPopover/GroupContent.tsx
Normal file
146
src/features/EditingPopover/GroupContent.tsx
Normal file
@@ -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<GroupContentProps>(
|
||||
({ 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<string | null | undefined>(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<InputRef>(null);
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flexbox horizontal align={'center'} gap={4} style={{ width: 320 }} onClick={stopPropagation}>
|
||||
{isAgentGroup && (
|
||||
<EmojiPicker
|
||||
allowUpload
|
||||
allowDelete={!!newAvatar}
|
||||
loading={uploading}
|
||||
locale={locale}
|
||||
shape={'square'}
|
||||
value={newAvatar ?? undefined}
|
||||
customRender={(avatarValue) => (
|
||||
<Block
|
||||
clickable
|
||||
align={'center'}
|
||||
height={36}
|
||||
justify={'center'}
|
||||
variant={isDarkMode ? 'filled' : 'outlined'}
|
||||
width={36}
|
||||
onClick={stopPropagation}
|
||||
>
|
||||
{avatarValue ? (
|
||||
<Avatar
|
||||
emojiScaleWithBackground
|
||||
avatar={avatarValue}
|
||||
shape={'square'}
|
||||
size={32}
|
||||
/>
|
||||
) : (
|
||||
<GroupAvatar avatars={memberAvatars || []} size={32} />
|
||||
)}
|
||||
</Block>
|
||||
)}
|
||||
onChange={setNewAvatar}
|
||||
onDelete={handleAvatarDelete}
|
||||
onUpload={handleAvatarUpload}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
defaultValue={title}
|
||||
ref={inputRef}
|
||||
style={{ flex: 1 }}
|
||||
onChange={(e) => setNewTitle(e.target.value)}
|
||||
onPressEnter={handleUpdate}
|
||||
/>
|
||||
<ActionIcon icon={Check} size={'small'} onClick={handleUpdate} />
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default GroupContent;
|
||||
47
src/features/EditingPopover/index.tsx
Normal file
47
src/features/EditingPopover/index.tsx
Normal file
@@ -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 (
|
||||
<PopoverRoot
|
||||
open={target !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) close();
|
||||
}}
|
||||
>
|
||||
<PopoverPortal>
|
||||
<PopoverPositioner anchor={target?.anchor ?? document.body} placement="bottomLeft">
|
||||
<PopoverPopup data-testid="editing-popover" style={{ padding: 4 }}>
|
||||
{target?.type === 'agent' ? (
|
||||
<AgentContent
|
||||
avatar={target.avatar}
|
||||
id={target.id}
|
||||
title={target.title}
|
||||
onClose={close}
|
||||
/>
|
||||
) : target ? (
|
||||
<GroupContent
|
||||
avatar={target.avatar}
|
||||
id={target.id}
|
||||
memberAvatars={target.memberAvatars}
|
||||
title={target.title}
|
||||
type={target.type}
|
||||
onClose={close}
|
||||
/>
|
||||
) : null}
|
||||
</PopoverPopup>
|
||||
</PopoverPositioner>
|
||||
</PopoverPortal>
|
||||
</PopoverRoot>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditingPopover;
|
||||
27
src/features/EditingPopover/store.ts
Normal file
27
src/features/EditingPopover/store.ts
Normal file
@@ -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<EditingPopoverState>((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();
|
||||
@@ -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());
|
||||
|
||||
@@ -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'));
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user