From 5f3055804a4982e1b2657d06fb79093014be23a6 Mon Sep 17 00:00:00 2001 From: Douglas Date: Thu, 23 Jul 2026 18:16:41 +0100 Subject: [PATCH 1/9] Refactor/setting dialog design and dashboard --- package.json | 5 - src/components/AddWorker/ToolSelect.tsx | 5 +- src/components/AddWorker/index.tsx | 8 +- .../ChatBox/BottomBox/ApprovalModeSelect.tsx | 173 --- src/components/ChatBox/BottomBox/InputBox.tsx | 4 +- .../ChatBox/BottomBox/ModelSelect.tsx | 17 +- .../ChatBox/BottomBox/PickerPanel.tsx | 8 +- .../BottomBox/ThinkingEffortSelect.tsx | 147 --- .../ChatBox/MessageItem/FeedbackCard.tsx | 119 -- .../ChatBox/MessageItem/SummaryMarkDown.tsx | 150 --- .../MessageItem/TaskCompletionCard.tsx | 94 -- src/components/ChatBox/README.md | 8 +- src/components/ChatBox/TaskBox/TaskType.tsx | 46 - src/components/ChatBox/index.tsx | 14 +- .../GroupedHistoryView/ProjectDialog.tsx | 289 ----- .../GroupedHistoryView/ProjectGroup.tsx | 553 --------- .../Dashboard/GroupedHistoryView/TaskItem.tsx | 224 ---- .../Dashboard/GroupedHistoryView/index.tsx | 781 ------------ src/components/Dashboard/HistoryTabsNav.tsx | 260 ---- .../Dashboard/IntegrationList/index.tsx | 14 +- src/components/Dashboard/VerticalNav.tsx | 102 -- src/components/Dialog/SearchHistoryDialog.tsx | 172 --- .../Dispatch/DispatchChannelCard.tsx | 114 -- src/components/ErrorBoundary/index.tsx | 104 -- src/components/HistorySidebar/SearchInput.tsx | 54 - src/components/HistorySidebar/index.tsx | 754 ------------ src/{pages => components}/Home/Projects.tsx | 10 +- src/{pages => components}/Home/Spaces.tsx | 0 src/{pages => components}/Home/Tasks.tsx | 10 +- src/{pages => components}/Home/Triggers.tsx | 10 +- .../Home/components/HomeHubBoard.tsx | 14 +- .../Home/components/HomeHubBoardCard.tsx | 0 .../Home/components/HomeHubCard.tsx | 4 +- .../Home/components/HomeHubGrid.tsx | 0 .../Home/components/HomeHubItemShared.tsx | 0 .../Home/components/HomeHubListItem.tsx | 0 .../Home/components/HomeHubListTable.tsx | 8 +- .../Home/components/HomeHubToolbar.tsx | 2 +- src/{pages => components}/Home/context.tsx | 0 .../Home/hooks/useHomeHubCounts.ts | 0 .../Home/hooks/useHomeHubNavigation.ts | 0 .../Home/hooks/useHomeHubProjects.ts | 0 .../Home/hooks/useHomeHubTriggers.ts | 0 .../Home/hooks/useSpaceLabel.ts | 0 src/{pages => components}/Home/index.tsx | 6 +- src/{pages => components}/Home/utils.ts | 0 .../Home/utils/boardStatus.ts | 0 src/components/InstallStep/Carousel.tsx | 246 ---- src/components/Layout/index.tsx | 14 +- src/components/Layout/tokenAliases.ts | 60 - src/components/Notification/index.tsx | 75 -- .../ProjectPageSidebar/BottomAction.tsx | 104 -- .../Session/HeaderBox/ChatTimeline.tsx | 253 ---- src/components/Session/SingleAgent/index.tsx | 15 - .../Workforce/FoldedPanel/AgentDetailPane.tsx | 636 ---------- .../Session/Workforce/FoldedPanel/index.tsx | 524 -------- .../Settings/Appearance/index.tsx} | 337 +++--- .../Settings/Browser/index.tsx} | 351 +++--- src/components/Settings/Channels/index.tsx | 41 + .../Settings}/Connectors/ConnectorGateway.tsx | 582 +++++---- .../components/AddCustomConnectorPage.tsx} | 323 +++-- .../components/ConnectorBrowserPage.tsx} | 289 ++--- .../components/GoogleSearchPanel.tsx | 0 .../Connectors/components/MCPConfigDialog.tsx | 0 .../Connectors/components/MCPDeleteDialog.tsx | 94 ++ .../Connectors/components/MCPEnvDialog.tsx | 55 +- .../Settings}/Connectors/components/types.ts | 0 .../Settings}/Connectors/components/utils.ts | 0 .../Settings}/Connectors/index.tsx | 0 .../Settings/Cookies/index.tsx} | 277 ++--- .../Settings/Dialog/SettingsDialog.tsx | 113 ++ .../Settings/Dialog/SettingsDialogContent.tsx | 171 +++ .../Settings/Dialog/SettingsDialogHeader.tsx | 80 ++ .../Dialog/SettingsDialogOverlay.tsx} | 31 +- .../Settings/Dialog/SettingsDialogSidebar.tsx | 84 ++ .../Settings/Dialog/settingsNavigation.ts | 158 +++ src/components/Settings/Extension/index.tsx | 39 + .../Settings/General/index.tsx} | 121 +- src/components/Settings/Memory/index.tsx | 41 + .../Settings/Models}/ConfigModelCard.tsx | 0 .../components/ProviderModelCombobox.tsx | 39 +- .../Settings/Models/index.tsx} | 1064 +++++++++-------- .../Settings/Models}/localModels.ts | 7 - src/components/Settings/Privacy/index.tsx | 122 ++ .../Settings/SettingsContentShell.tsx} | 39 +- .../Settings/SettingsHeaderContext.tsx | 107 ++ .../Settings/SettingsPage.tsx} | 27 +- .../SettingsPageLoading.tsx} | 31 +- src/components/Settings/SettingsSection.tsx | 60 + .../Skills}/components/SkillDeleteDialog.tsx | 0 .../Skills}/components/SkillListItem.tsx | 14 +- .../Skills}/components/SkillUploadDialog.tsx | 14 +- .../Settings/Skills/index.tsx} | 162 +-- .../Settings/SubAgents/index.tsx} | 138 ++- .../Settings/WorkspaceProfile/index.tsx | 167 +++ src/{client => components/Settings}/index.ts | 13 +- src/components/SpacesHub/index.tsx | 414 ------- src/components/Toast/creditsToast.tsx | 3 +- src/components/TopBar/index.tsx | 406 +++---- src/components/Trigger/ActivityLogItem.tsx | 316 ----- .../Workspace/WorkspaceSessionModeToggle.tsx | 15 - src/components/Workspace/index.tsx | 7 +- src/components/ui/SplitText/SplitText.d.ts | 60 - src/components/ui/SplitText/SplitText.jsx | 218 ---- src/components/ui/alert.tsx | 123 -- src/components/ui/alertDialog.tsx | 49 +- .../ui/animate-ui/icons/alarm-clock.tsx | 260 ---- src/components/ui/animate-ui/icons/blocks.tsx | 174 --- src/components/ui/animate-ui/icons/bot.tsx | 185 --- .../ui/animate-ui/icons/compass.tsx | 106 -- src/components/ui/animate-ui/icons/hammer.tsx | 97 -- src/components/ui/animate-ui/icons/orbit.tsx | 119 -- src/components/ui/animate-ui/icons/pin.tsx | 130 -- src/components/ui/animate-ui/icons/radio.tsx | 127 -- .../ui/animate-ui/icons/settings.tsx | 110 -- .../ui/animate-ui/icons/sparkle.tsx | 104 -- src/components/ui/carousel.tsx | 298 ----- src/components/ui/dialog.tsx | 110 +- src/components/ui/separator.tsx | 43 - src/components/ui/sheet.tsx | 158 --- src/components/ui/sidebar.tsx | 788 ------------ src/components/ui/tabs.tsx | 23 +- src/hooks/useInitialChatPanelLayout.ts | 46 - src/hooks/useInitialWorkforceFoldedLayout.ts | 56 - src/hooks/useIntegrationManagement.ts | 12 + src/i18n/locales/ar/agents.json | 8 +- src/i18n/locales/ar/connectors.json | 14 +- src/i18n/locales/ar/layout.json | 16 +- src/i18n/locales/ar/setting.json | 49 +- src/i18n/locales/de/agents.json | 8 +- src/i18n/locales/de/connectors.json | 14 +- src/i18n/locales/de/layout.json | 16 +- src/i18n/locales/de/setting.json | 49 +- src/i18n/locales/en-us/agents.json | 8 +- src/i18n/locales/en-us/connectors.json | 14 +- src/i18n/locales/en-us/layout.json | 16 +- src/i18n/locales/en-us/setting.json | 49 +- src/i18n/locales/es/agents.json | 8 +- src/i18n/locales/es/connectors.json | 14 +- src/i18n/locales/es/layout.json | 16 +- src/i18n/locales/es/setting.json | 49 +- src/i18n/locales/fr/agents.json | 8 +- src/i18n/locales/fr/connectors.json | 14 +- src/i18n/locales/fr/layout.json | 16 +- src/i18n/locales/fr/setting.json | 49 +- src/i18n/locales/it/agents.json | 8 +- src/i18n/locales/it/connectors.json | 14 +- src/i18n/locales/it/layout.json | 16 +- src/i18n/locales/it/setting.json | 49 +- src/i18n/locales/ja/agents.json | 8 +- src/i18n/locales/ja/connectors.json | 14 +- src/i18n/locales/ja/layout.json | 16 +- src/i18n/locales/ja/setting.json | 49 +- src/i18n/locales/ko/agents.json | 8 +- src/i18n/locales/ko/connectors.json | 14 +- src/i18n/locales/ko/layout.json | 16 +- src/i18n/locales/ko/setting.json | 49 +- src/i18n/locales/ru/agents.json | 8 +- src/i18n/locales/ru/connectors.json | 14 +- src/i18n/locales/ru/layout.json | 16 +- src/i18n/locales/ru/setting.json | 49 +- src/i18n/locales/zh-Hans/agents.json | 8 +- src/i18n/locales/zh-Hans/connectors.json | 14 +- src/i18n/locales/zh-Hans/layout.json | 16 +- src/i18n/locales/zh-Hans/setting.json | 49 +- src/i18n/locales/zh-Hant/agents.json | 8 +- src/i18n/locales/zh-Hant/connectors.json | 14 +- src/i18n/locales/zh-Hant/layout.json | 16 +- src/i18n/locales/zh-Hant/setting.json | 49 +- src/lib/applyDefaultModelSelection.ts | 4 - src/lib/env.ts | 14 - src/mocks/triggerMockData.ts | 206 ---- src/pages/Agents/Memory.tsx | 46 - src/pages/Agents/index.tsx | 107 -- src/pages/Browser/Extension.tsx | 47 - src/pages/Browser/index.tsx | 103 -- src/pages/Channels/index.tsx | 87 -- src/pages/Connectors/Search.tsx | 241 ---- .../Connectors/components/CookieManager.tsx | 236 ---- .../Connectors/components/MCPDeleteDialog.tsx | 56 - src/pages/History.tsx | 225 ---- src/pages/Home.tsx | 70 ++ src/pages/Setting/API.tsx | 142 --- src/pages/Setting/Privacy.tsx | 138 --- src/pages/Setting/index.tsx | 207 ---- src/routers/index.tsx | 12 +- src/shared/modelProviderImages.ts | 2 +- src/store/globalStore.ts | 50 - src/store/settingsDialogStore.ts | 84 ++ test/unit/components/MCPDeleteDialog.test.tsx | 71 ++ test/unit/components/Setting/utils.test.ts | 2 +- test/unit/components/SettingsDialog.test.tsx | 266 +++++ test/unit/pages/Browser/CDP.test.tsx | 9 +- test/unit/store/settingsDialogStore.test.ts | 64 + 194 files changed, 5151 insertions(+), 14253 deletions(-) delete mode 100644 src/components/ChatBox/BottomBox/ApprovalModeSelect.tsx delete mode 100644 src/components/ChatBox/BottomBox/ThinkingEffortSelect.tsx delete mode 100644 src/components/ChatBox/MessageItem/FeedbackCard.tsx delete mode 100644 src/components/ChatBox/MessageItem/SummaryMarkDown.tsx delete mode 100644 src/components/ChatBox/MessageItem/TaskCompletionCard.tsx delete mode 100644 src/components/ChatBox/TaskBox/TaskType.tsx delete mode 100644 src/components/Dashboard/GroupedHistoryView/ProjectDialog.tsx delete mode 100644 src/components/Dashboard/GroupedHistoryView/ProjectGroup.tsx delete mode 100644 src/components/Dashboard/GroupedHistoryView/TaskItem.tsx delete mode 100644 src/components/Dashboard/GroupedHistoryView/index.tsx delete mode 100644 src/components/Dashboard/HistoryTabsNav.tsx delete mode 100644 src/components/Dashboard/VerticalNav.tsx delete mode 100644 src/components/Dialog/SearchHistoryDialog.tsx delete mode 100644 src/components/Dispatch/DispatchChannelCard.tsx delete mode 100644 src/components/ErrorBoundary/index.tsx delete mode 100644 src/components/HistorySidebar/SearchInput.tsx delete mode 100644 src/components/HistorySidebar/index.tsx rename src/{pages => components}/Home/Projects.tsx (95%) rename src/{pages => components}/Home/Spaces.tsx (100%) rename src/{pages => components}/Home/Tasks.tsx (94%) rename src/{pages => components}/Home/Triggers.tsx (95%) rename src/{pages => components}/Home/components/HomeHubBoard.tsx (83%) rename src/{pages => components}/Home/components/HomeHubBoardCard.tsx (100%) rename src/{pages => components}/Home/components/HomeHubCard.tsx (99%) rename src/{pages => components}/Home/components/HomeHubGrid.tsx (100%) rename src/{pages => components}/Home/components/HomeHubItemShared.tsx (100%) rename src/{pages => components}/Home/components/HomeHubListItem.tsx (100%) rename src/{pages => components}/Home/components/HomeHubListTable.tsx (91%) rename src/{pages => components}/Home/components/HomeHubToolbar.tsx (98%) rename src/{pages => components}/Home/context.tsx (100%) rename src/{pages => components}/Home/hooks/useHomeHubCounts.ts (100%) rename src/{pages => components}/Home/hooks/useHomeHubNavigation.ts (100%) rename src/{pages => components}/Home/hooks/useHomeHubProjects.ts (100%) rename src/{pages => components}/Home/hooks/useHomeHubTriggers.ts (100%) rename src/{pages => components}/Home/hooks/useSpaceLabel.ts (100%) rename src/{pages => components}/Home/index.tsx (98%) rename src/{pages => components}/Home/utils.ts (100%) rename src/{pages => components}/Home/utils/boardStatus.ts (100%) delete mode 100644 src/components/InstallStep/Carousel.tsx delete mode 100644 src/components/Layout/tokenAliases.ts delete mode 100644 src/components/Notification/index.tsx delete mode 100644 src/components/ProjectPageSidebar/BottomAction.tsx delete mode 100644 src/components/Session/HeaderBox/ChatTimeline.tsx delete mode 100644 src/components/Session/SingleAgent/index.tsx delete mode 100644 src/components/Session/Workforce/FoldedPanel/AgentDetailPane.tsx delete mode 100644 src/components/Session/Workforce/FoldedPanel/index.tsx rename src/{pages/Setting/Appearance.tsx => components/Settings/Appearance/index.tsx} (56%) rename src/{pages/Browser/CDP.tsx => components/Settings/Browser/index.tsx} (50%) create mode 100644 src/components/Settings/Channels/index.tsx rename src/{pages => components/Settings}/Connectors/ConnectorGateway.tsx (77%) rename src/{pages/Connectors/components/AddCustomConnectorDialog.tsx => components/Settings/Connectors/components/AddCustomConnectorPage.tsx} (55%) rename src/{pages/Connectors/components/AddConnectorDialog.tsx => components/Settings/Connectors/components/ConnectorBrowserPage.tsx} (89%) rename src/{pages => components/Settings}/Connectors/components/GoogleSearchPanel.tsx (100%) rename src/{pages => components/Settings}/Connectors/components/MCPConfigDialog.tsx (100%) create mode 100644 src/components/Settings/Connectors/components/MCPDeleteDialog.tsx rename src/{pages => components/Settings}/Connectors/components/MCPEnvDialog.tsx (88%) rename src/{pages => components/Settings}/Connectors/components/types.ts (100%) rename src/{pages => components/Settings}/Connectors/components/utils.ts (100%) rename src/{pages => components/Settings}/Connectors/index.tsx (100%) rename src/{pages/Browser/Cookies.tsx => components/Settings/Cookies/index.tsx} (51%) create mode 100644 src/components/Settings/Dialog/SettingsDialog.tsx create mode 100644 src/components/Settings/Dialog/SettingsDialogContent.tsx create mode 100644 src/components/Settings/Dialog/SettingsDialogHeader.tsx rename src/{hooks/use-mobile.tsx => components/Settings/Dialog/SettingsDialogOverlay.tsx} (57%) create mode 100644 src/components/Settings/Dialog/SettingsDialogSidebar.tsx create mode 100644 src/components/Settings/Dialog/settingsNavigation.ts create mode 100644 src/components/Settings/Extension/index.tsx rename src/{pages/Setting/General.tsx => components/Settings/General/index.tsx} (75%) create mode 100644 src/components/Settings/Memory/index.tsx rename src/{pages/Agents => components/Settings/Models}/ConfigModelCard.tsx (100%) rename src/{pages/Agents => components/Settings/Models}/components/ProviderModelCombobox.tsx (83%) rename src/{pages/Agents/Models.tsx => components/Settings/Models/index.tsx} (77%) rename src/{pages/Agents => components/Settings/Models}/localModels.ts (93%) create mode 100644 src/components/Settings/Privacy/index.tsx rename src/{hooks/use-app-version.tsx => components/Settings/SettingsContentShell.tsx} (54%) create mode 100644 src/components/Settings/SettingsHeaderContext.tsx rename src/{store/sidebarStore.ts => components/Settings/SettingsPage.tsx} (66%) rename src/components/{ProjectPageSidebar/NavListSessionRows.tsx => Settings/SettingsPageLoading.tsx} (50%) create mode 100644 src/components/Settings/SettingsSection.tsx rename src/{pages/Agents => components/Settings/Skills}/components/SkillDeleteDialog.tsx (100%) rename src/{pages/Agents => components/Settings/Skills}/components/SkillListItem.tsx (96%) rename src/{pages/Agents => components/Settings/Skills}/components/SkillUploadDialog.tsx (98%) rename src/{pages/Agents/Skills.tsx => components/Settings/Skills/index.tsx} (69%) rename src/{pages/Agents/SubAgents.tsx => components/Settings/SubAgents/index.tsx} (75%) create mode 100644 src/components/Settings/WorkspaceProfile/index.tsx rename src/{client => components/Settings}/index.ts (74%) delete mode 100644 src/components/SpacesHub/index.tsx delete mode 100644 src/components/Trigger/ActivityLogItem.tsx delete mode 100644 src/components/Workspace/WorkspaceSessionModeToggle.tsx delete mode 100644 src/components/ui/SplitText/SplitText.d.ts delete mode 100644 src/components/ui/SplitText/SplitText.jsx delete mode 100644 src/components/ui/alert.tsx delete mode 100644 src/components/ui/animate-ui/icons/alarm-clock.tsx delete mode 100644 src/components/ui/animate-ui/icons/blocks.tsx delete mode 100644 src/components/ui/animate-ui/icons/bot.tsx delete mode 100644 src/components/ui/animate-ui/icons/compass.tsx delete mode 100644 src/components/ui/animate-ui/icons/hammer.tsx delete mode 100644 src/components/ui/animate-ui/icons/orbit.tsx delete mode 100644 src/components/ui/animate-ui/icons/pin.tsx delete mode 100644 src/components/ui/animate-ui/icons/radio.tsx delete mode 100644 src/components/ui/animate-ui/icons/settings.tsx delete mode 100644 src/components/ui/animate-ui/icons/sparkle.tsx delete mode 100644 src/components/ui/carousel.tsx delete mode 100644 src/components/ui/separator.tsx delete mode 100644 src/components/ui/sheet.tsx delete mode 100644 src/components/ui/sidebar.tsx delete mode 100644 src/hooks/useInitialChatPanelLayout.ts delete mode 100644 src/hooks/useInitialWorkforceFoldedLayout.ts delete mode 100644 src/lib/env.ts delete mode 100644 src/mocks/triggerMockData.ts delete mode 100644 src/pages/Agents/Memory.tsx delete mode 100644 src/pages/Agents/index.tsx delete mode 100644 src/pages/Browser/Extension.tsx delete mode 100644 src/pages/Browser/index.tsx delete mode 100644 src/pages/Channels/index.tsx delete mode 100644 src/pages/Connectors/Search.tsx delete mode 100644 src/pages/Connectors/components/CookieManager.tsx delete mode 100644 src/pages/Connectors/components/MCPDeleteDialog.tsx delete mode 100644 src/pages/History.tsx create mode 100644 src/pages/Home.tsx delete mode 100644 src/pages/Setting/API.tsx delete mode 100644 src/pages/Setting/Privacy.tsx delete mode 100644 src/pages/Setting/index.tsx delete mode 100644 src/store/globalStore.ts create mode 100644 src/store/settingsDialogStore.ts create mode 100644 test/unit/components/MCPDeleteDialog.test.tsx create mode 100644 test/unit/components/SettingsDialog.test.tsx create mode 100644 test/unit/store/settingsDialogStore.test.ts diff --git a/package.json b/package.json index 8d5e0828..a0cef39e 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "@electron/notarize": "^2.5.0", "@emotion/is-prop-valid": "^1.3.1", "@fontsource/inter": "^5.2.5", - "@gsap/react": "^2.1.2", "@microsoft/fetch-event-source": "^2.0.1", "@monaco-editor/loader": "^1.5.0", "@monaco-editor/react": "^4.7.0", @@ -71,7 +70,6 @@ "@radix-ui/react-popover": "^1.1.13", "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-select": "^2.2.5", - "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.4", "@radix-ui/react-tabs": "^1.1.11", @@ -95,11 +93,8 @@ "dompurify": "^3.2.7", "electron-log": "^5.4.0", "electron-updater": "^6.3.9", - "embla-carousel-autoplay": "^8.6.0", - "embla-carousel-react": "^8.6.0", "framer-motion": "^12.17.0", "github-markdown-css": "^5.8.1", - "gsap": "^3.13.0", "koffi": "^2.14.1", "lodash-es": "^4.17.21", "lottie-web": "^5.13.0", diff --git a/src/components/AddWorker/ToolSelect.tsx b/src/components/AddWorker/ToolSelect.tsx index 930cef73..942247d5 100644 --- a/src/components/AddWorker/ToolSelect.tsx +++ b/src/components/AddWorker/ToolSelect.tsx @@ -31,6 +31,7 @@ import { useHost } from '@/host'; import { capitalizeFirstLetter, getProxyBaseURL } from '@/lib'; import { cn } from '@/lib/utils'; import { useAuthStore } from '@/store/authStore'; +import { openSettingsDialog } from '@/store/settingsDialogStore'; import type { TFunction } from 'i18next'; import { CircleAlert, X } from 'lucide-react'; import { @@ -43,7 +44,6 @@ import { useState, } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; import { Checkbox } from '../ui/checkbox'; import { Textarea } from '../ui/textarea'; import { TooltipSimple } from '../ui/tooltip'; @@ -260,7 +260,6 @@ const ToolSelect = forwardRef< const host = useHost(); const electronAPI = host?.electronAPI; const { t } = useTranslation(); - const navigate = useNavigate(); // state management - remove internal selected state, use parent passed initialSelectedTools const [keyword, setKeyword] = useState(''); const { email } = useAuthStore(); @@ -943,7 +942,7 @@ const ToolSelect = forwardRef< variant="ghost" size="xs" buttonContent="text" - onClick={() => navigate('/history?tab=connectors')} + onClick={() => openSettingsDialog('connectors')} > {t('chat.input-attach-manage-connectors')} diff --git a/src/components/AddWorker/index.tsx b/src/components/AddWorker/index.tsx index 894bde08..12067814 100644 --- a/src/components/AddWorker/index.tsx +++ b/src/components/AddWorker/index.tsx @@ -15,6 +15,10 @@ import { mcpList as fetchMcpConfig } from '@/api/brain'; import { fetchPost, proxyFetchGet } from '@/api/http'; import githubIcon from '@/assets/icon/github.svg'; +import { + getLocalPlatformName, + LOCAL_MODEL_OPTIONS, +} from '@/components/Settings/Models/localModels'; import { Button } from '@/components/ui/button'; import { Dialog, @@ -41,10 +45,6 @@ import { buildAgentModelConfig, buildAgentModelConfigFromProvider, } from '@/lib/modelConfig'; -import { - getLocalPlatformName, - LOCAL_MODEL_OPTIONS, -} from '@/pages/Agents/localModels'; import { useAuthStore, useWorkerList } from '@/store/authStore'; import { useCloudModelStore } from '@/store/cloudModelStore'; import { Bot, Edit, Eye, EyeOff } from 'lucide-react'; diff --git a/src/components/ChatBox/BottomBox/ApprovalModeSelect.tsx b/src/components/ChatBox/BottomBox/ApprovalModeSelect.tsx deleted file mode 100644 index 7bbb0097..00000000 --- a/src/components/ChatBox/BottomBox/ApprovalModeSelect.tsx +++ /dev/null @@ -1,173 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -/** - * Approval-mode picker for the chat input bar — same pill-trigger shell as - * `ThinkingEffortSelect` / `ModelSelect` / `ProjectModeToggle` so the - * controls read as one family in the `BoxFooter` row. - * - * UI only for now: it holds its own local state and is not wired to the - * human-toolkit / approval backend. - */ - -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { cn } from '@/lib/utils'; -import { Check, ChevronDown, ShieldCheck, TriangleAlert } from 'lucide-react'; -import { useState } from 'react'; - -export type ApprovalMode = 'manual' | 'skip'; - -interface ApprovalOption { - value: ApprovalMode; - label: string; - icon: typeof ShieldCheck; -} - -const APPROVAL_OPTIONS: ApprovalOption[] = [ - { value: 'manual', label: 'Manual Approval', icon: ShieldCheck }, - { value: 'skip', label: 'Skip all approval', icon: TriangleAlert }, -]; - -// Keep in sync with `DropdownMenuContent`'s `w-[180px]` below. -const MENU_CONTENT_WIDTH_CLASS = 'w-[180px]'; - -const triggerShellClass = cn( - 'rounded-xl px-2 py-1 inline-flex max-w-[min(100%,320px)] shrink-0 items-center gap-1.5', - 'bg-ds-bg-neutral-default-default text-ds-text-neutral-default-default' -); - -export interface ApprovalModeSelectProps { - disabled?: boolean; - /** Shows the current mode in the same read-only shell as the model/mode controls. */ - readOnly?: boolean; - /** When true, hides the text label and shows only the icon (narrow footer). */ - compact?: boolean; - className?: string; -} - -export function ApprovalModeSelect({ - disabled, - readOnly = false, - compact = false, - className, -}: ApprovalModeSelectProps) { - const [value, setValue] = useState('manual'); - - const current = - APPROVAL_OPTIONS.find((o) => o.value === value) ?? APPROVAL_OPTIONS[0]; - const CurrentIcon = current.icon; - - if (readOnly) { - return ( -
- - - {!compact && ( - - {current.label} - - )} - -
- ); - } - - return ( - - - - - - {APPROVAL_OPTIONS.map((option) => { - const OptionIcon = option.icon; - return ( - setValue(option.value)} - className="flex items-center justify-between gap-2" - > - - - {option.label} - - {value === option.value && ( - - )} - - ); - })} - - - ); -} diff --git a/src/components/ChatBox/BottomBox/InputBox.tsx b/src/components/ChatBox/BottomBox/InputBox.tsx index 6a3c3335..2af0b0c6 100644 --- a/src/components/ChatBox/BottomBox/InputBox.tsx +++ b/src/components/ChatBox/BottomBox/InputBox.tsx @@ -24,8 +24,8 @@ import { cn } from '@/lib/utils'; import type { TriggerInput } from '@/types'; import { ArrowRight, + Cable, FileText, - Hammer, Image, Paperclip, UploadCloud, @@ -546,7 +546,7 @@ export const Inputbox = ({ )} onClick={onToggleConnectorPanel} > - + )} diff --git a/src/components/ChatBox/BottomBox/ModelSelect.tsx b/src/components/ChatBox/BottomBox/ModelSelect.tsx index 8292aa3d..b7a8f5ff 100644 --- a/src/components/ChatBox/BottomBox/ModelSelect.tsx +++ b/src/components/ChatBox/BottomBox/ModelSelect.tsx @@ -19,6 +19,10 @@ import { proxyFetchGet } from '@/api/http'; import folderIcon from '@/assets/logo/eigent_icon_rich.svg'; +import { + getLocalPlatformName, + LOCAL_MODEL_OPTIONS, +} from '@/components/Settings/Models/localModels'; import { DropdownMenu, DropdownMenuContent, @@ -31,17 +35,12 @@ import { import { createHost } from '@/host/createHost'; import { applyDefaultModelSelection, - DEFAULT_MODEL_CONFIGURE_PATH, isDefaultModelConfigured, type DefaultModelCategory, } from '@/lib/applyDefaultModelSelection'; import { INIT_PROVODERS } from '@/lib/llm'; import { getProviderValid } from '@/lib/providerStatus'; import { cn } from '@/lib/utils'; -import { - getLocalPlatformName, - LOCAL_MODEL_OPTIONS, -} from '@/pages/Agents/localModels'; import { getModelImage, needsInvertModelImage, @@ -49,6 +48,7 @@ import { import { useAuthStore } from '@/store/authStore'; import { useCloudModelStore } from '@/store/cloudModelStore'; import { useProjectRuntimeStore } from '@/store/projectRuntimeStore'; +import { openSettingsDialog } from '@/store/settingsDialogStore'; import { useSpaceStore } from '@/store/spaceStore'; import type { Provider } from '@/types'; @@ -63,7 +63,6 @@ import { import type { Dispatch, SetStateAction } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; export interface ModelSelectProps { disabled?: boolean; @@ -93,7 +92,6 @@ export function ModelSelect({ readOnly = false, }: ModelSelectProps) { const { t } = useTranslation(); - const navigate = useNavigate(); const { modelType, cloud_model_type, @@ -412,7 +410,7 @@ export function ModelSelect({ localProviderIds, }) ) { - navigate(DEFAULT_MODEL_CONFIGURE_PATH); + openSettingsDialog('models'); return; } if (projectId) { @@ -474,7 +472,6 @@ export function ModelSelect({ localProviderIds, localPlatform, localTypes, - navigate, projectId, setProjectModel, setModelType, @@ -668,7 +665,7 @@ export function ModelSelect({ if (isConfigured) { handleCodexSetDefault(); } else { - navigate(DEFAULT_MODEL_CONFIGURE_PATH); + openSettingsDialog('models'); } return; } diff --git a/src/components/ChatBox/BottomBox/PickerPanel.tsx b/src/components/ChatBox/BottomBox/PickerPanel.tsx index 4ce5cc3d..9f35d71b 100644 --- a/src/components/ChatBox/BottomBox/PickerPanel.tsx +++ b/src/components/ChatBox/BottomBox/PickerPanel.tsx @@ -30,11 +30,11 @@ import { import { skillNameToDirName } from '@/lib/skillToolkit'; import { cn } from '@/lib/utils'; import { useServerCapabilityStore } from '@/store/serverCapabilityStore'; +import { openSettingsDialog } from '@/store/settingsDialogStore'; import { useSkillsStore } from '@/store/skillsStore'; import { Check, Plus, Wrench } from 'lucide-react'; import { Fragment, useEffect, useMemo, useState, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; /** * An item shown in a picker panel. `token` is the exact string inserted inline @@ -223,7 +223,6 @@ export function ConnectorPickerPanel({ onToggleItem, }: WiredPickerPanelProps) { const { t } = useTranslation(); - const navigate = useNavigate(); const [builtInItems, setBuiltInItems] = useState([]); const [openItems, setOpenItems] = useState([]); const [yourMcps, setYourMcps] = useState([]); @@ -380,7 +379,7 @@ export function ConnectorPickerPanel({ loading={loading} emptyLabel={t('chat.no-connectors-added')} emptyActionLabel={t('chat.input-attach-manage-connectors')} - onEmptyAction={() => navigate('/history?tab=connectors')} + onEmptyAction={() => openSettingsDialog('connectors')} /> ); } @@ -391,7 +390,6 @@ export function SkillPickerPanel({ onToggleItem, }: WiredPickerPanelProps) { const { t } = useTranslation(); - const navigate = useNavigate(); const skills = useSkillsStore((s) => s.skills); const items = useMemo( @@ -428,7 +426,7 @@ export function SkillPickerPanel({ }} emptyLabel={t('chat.no-skills-added')} emptyActionLabel={t('chat.input-attach-manage-skills')} - onEmptyAction={() => navigate('/history?tab=agents')} + onEmptyAction={() => openSettingsDialog('skills')} /> ); } diff --git a/src/components/ChatBox/BottomBox/ThinkingEffortSelect.tsx b/src/components/ChatBox/BottomBox/ThinkingEffortSelect.tsx deleted file mode 100644 index 31391450..00000000 --- a/src/components/ChatBox/BottomBox/ThinkingEffortSelect.tsx +++ /dev/null @@ -1,147 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -/** - * Reasoning/thinking effort picker for the chat input bar — same pill-trigger - * shell as `ModelSelect` / `ProjectModeToggle` so the three controls - * read as one family in the `BoxFooter` row. - */ - -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { cn } from '@/lib/utils'; -import { ThinkingEffort, type ThinkingEffortType } from '@/types/constants'; -import { Check, ChevronDown } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; - -export interface ThinkingEffortSelectProps { - value: ThinkingEffortType; - onValueChange?: (effort: ThinkingEffortType) => void; - disabled?: boolean; - /** Shows the current effort in the same read-only shell as the model/mode controls. */ - readOnly?: boolean; - className?: string; -} - -const EFFORT_OPTIONS: ThinkingEffortType[] = [ - ThinkingEffort.LIGHT, - ThinkingEffort.MEDIUM, - ThinkingEffort.HIGH, - ThinkingEffort.EXTRA_HIGH, - ThinkingEffort.ULTRA, -]; - -// Keep in sync with `DropdownMenuContent`'s `w-[160px]` below. -const MENU_CONTENT_WIDTH_CLASS = 'w-[160px]'; - -const triggerShellClass = cn( - 'rounded-xl px-2 py-1 inline-flex max-w-[min(100%,320px)] shrink-0 items-center gap-1.5', - 'bg-ds-bg-neutral-default-default text-ds-text-neutral-default-default' -); - -export function ThinkingEffortSelect({ - value, - onValueChange, - disabled, - readOnly = false, - className, -}: ThinkingEffortSelectProps) { - const { t } = useTranslation(); - - const effortLabel = (effort: ThinkingEffortType) => - t(`layout.thinking-effort-${effort}`); - - const currentLabel = effortLabel(value); - - if (readOnly) { - return ( -
- - - {currentLabel} - - -
- ); - } - - return ( - - - - - - {EFFORT_OPTIONS.map((effort) => ( - onValueChange?.(effort)} - className="flex items-center justify-between" - > - {effortLabel(effort)} - {value === effort && ( - - )} - - ))} - - - ); -} diff --git a/src/components/ChatBox/MessageItem/FeedbackCard.tsx b/src/components/ChatBox/MessageItem/FeedbackCard.tsx deleted file mode 100644 index a5424410..00000000 --- a/src/components/ChatBox/MessageItem/FeedbackCard.tsx +++ /dev/null @@ -1,119 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import { Button } from '@/components/ui/button'; -import { Check, Copy } from 'lucide-react'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { toast } from 'sonner'; - -const COPIED_RESET_MS = 2000; - -interface FeedbackCardProps { - id: string; - title: string; - content: string; - onConfirm?: () => void; - onSkip?: () => void; - className?: string; -} - -export function FeedbackCard({ - id, - title, - content, - onConfirm, - onSkip, - className, -}: FeedbackCardProps) { - const [_isHovered, setIsHovered] = useState(false); - const [copied, setCopied] = useState(false); - const timeoutRef = useRef(null); - const { t } = useTranslation(); - - const handleCopy = useCallback(async () => { - try { - await navigator.clipboard.writeText(content); - toast.success(t('setting.copied-to-clipboard')); - setCopied(true); - if (timeoutRef.current !== null) { - clearTimeout(timeoutRef.current); - } - timeoutRef.current = window.setTimeout(() => { - setCopied(false); - timeoutRef.current = null; - }, COPIED_RESET_MS); - } catch { - toast.error(t('setting.failed-to-copy-to-clipboard')); - } - }, [content, t]); - - useEffect(() => { - return () => { - if (timeoutRef.current !== null) { - clearTimeout(timeoutRef.current); - } - }; - }, []); - - return ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - {/* Copy button - appears on hover */} -
- -
- - {/* Title */} -

- {title} -

- - {/* Content */} -

- {content} -

- - {/* Action buttons */} -
- - -
-
- ); -} diff --git a/src/components/ChatBox/MessageItem/SummaryMarkDown.tsx b/src/components/ChatBox/MessageItem/SummaryMarkDown.tsx deleted file mode 100644 index 1a7fe48d..00000000 --- a/src/components/ChatBox/MessageItem/SummaryMarkDown.tsx +++ /dev/null @@ -1,150 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import { isHtmlDocument } from '@/lib/htmlFontStyles'; -import { useEffect, useState } from 'react'; -import ReactMarkdown from 'react-markdown'; - -export const SummaryMarkDown = ({ - content, - speed = 15, - onTyping, - enableTypewriter = true, -}: { - content: string; - speed?: number; - onTyping?: () => void; - enableTypewriter?: boolean; -}) => { - const [displayedContent, setDisplayedContent] = useState(''); - const [_isTyping, setIsTyping] = useState(true); - - useEffect(() => { - if (!enableTypewriter) { - setDisplayedContent(content); - setIsTyping(false); - return; - } - - setDisplayedContent(''); - setIsTyping(true); - let index = 0; - - const timer = setInterval(() => { - if (index < content.length) { - setDisplayedContent(content.slice(0, index + 1)); - index++; - if (onTyping) { - onTyping(); - } - } else { - setIsTyping(false); - clearInterval(timer); - } - }, speed); - - return () => clearInterval(timer); - }, [content, speed, onTyping, enableTypewriter]); - - // If content is a pure HTML document, render in a styled pre block - if (isHtmlDocument(content)) { - // Trim leading whitespace from each line for consistent alignment - const formattedHtml = displayedContent - .split('\n') - .map((line) => line.trimStart()) - .join('\n') - .trim(); - return ( -
-
-          {formattedHtml}
-        
-
- ); - } - - return ( -
- ( -

- {children} -

- ), - h2: ({ children }) => ( -

- {children} -

- ), - h3: ({ children }) => ( -

- {children} -

- ), - p: ({ children }) => ( -

- {children} -

- ), - ul: ({ children }) => ( -
    - {children} -
- ), - ol: ({ children }) => ( -
    - {children} -
- ), - li: ({ children }) => ( -
  • - {children} -
  • - ), - code: ({ children }) => ( - - {children} - - ), - pre: ({ children }) => ( -
    -              {children}
    -            
    - ), - blockquote: ({ children }) => ( -
    - {children} -
    - ), - strong: ({ children }) => ( - - {children} - - ), - em: ({ children }) => ( - - {children} - - ), - hr: () => ( -
    - ), - }} - > - {displayedContent} -
    -
    - ); -}; diff --git a/src/components/ChatBox/MessageItem/TaskCompletionCard.tsx b/src/components/ChatBox/MessageItem/TaskCompletionCard.tsx deleted file mode 100644 index 3a7ee19e..00000000 --- a/src/components/ChatBox/MessageItem/TaskCompletionCard.tsx +++ /dev/null @@ -1,94 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import { TriggerDialog } from '@/components/Trigger/TriggerDialog'; -import { Button } from '@/components/ui/button'; -import { motion } from 'framer-motion'; -import { Plus, X } from 'lucide-react'; -import React, { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -interface TaskCompletionCardProps { - taskPrompt?: string; - onRerun?: () => void; - onDismiss?: () => void; -} - -export const TaskCompletionCard: React.FC = ({ - taskPrompt = '', - onDismiss, -}) => { - const { t } = useTranslation(); - const [isTriggerDialogOpen, setIsTriggerDialogOpen] = useState(false); - - const handleAddTrigger = () => { - setIsTriggerDialogOpen(true); - }; - - return ( - <> - - {onDismiss && ( - - )} -
    -
    - {t('chat.task-completed-card-title')} -
    -
    - {t('chat.task-completed-card-subtitle')} -
    -
    - -
    - - {/* Trigger Dialog */} - - - ); -}; - -export default TaskCompletionCard; diff --git a/src/components/ChatBox/README.md b/src/components/ChatBox/README.md index 0487927a..5d0b21a7 100644 --- a/src/components/ChatBox/README.md +++ b/src/components/ChatBox/README.md @@ -19,7 +19,6 @@ ChatBox/ ├── TaskBox/ # Plan / run task UI │ ├── TaskCard.tsx # Plan list, progress, filter, expand (workforce subtasks) │ ├── TaskItem.tsx # Editable line in the plan -│ ├── TaskType.tsx # Task type indicator │ ├── TypeCardSkeleton.tsx # Loading skeleton while the plan is forming │ └── StreamingTaskList.tsx │ @@ -28,13 +27,10 @@ ChatBox/ │ ├── UserMessageRichContent.tsx │ ├── AgentMessageCard.tsx │ ├── NoticeCard.tsx -│ ├── FeedbackCard.tsx -│ ├── TaskCompletionCard.tsx │ ├── SplittingProgressRow.tsx # “Splitting tasks” + token tick during decompose │ ├── TaskWorkLogAccordion.tsx # Work log: tool / agent lines (workforce) │ ├── FloatingAction.tsx # Pause / skip (used from ProjectSection) │ ├── MarkDown.tsx -│ ├── SummaryMarkDown.tsx │ └── TokenUtils.tsx # Token animation + splitting elapsed formatting │ └── BottomBox/ # Composer and chrome above input @@ -67,7 +63,7 @@ ChatBox/ ### `UserQueryGroup.tsx` - A **single user turn**: user content, then downstream UI driven by `AgentStep` / `ChatTaskStatus` (e.g. splitting, task card, agent completion, notices). -- Imports from `TaskBox/` and `MessageItem/`; this is the main place new message *shapes* are routed. +- Imports from `TaskBox/` and `MessageItem/`; this is the main place new message _shapes_ are routed. ## TaskBox (`TaskBox/`) @@ -82,9 +78,7 @@ ChatBox/ - **`AgentMessageCard`**: Assistant markdown, optional typewriter, attachments. - **`SplittingProgressRow`**: Shown while the task is in the splitting phase; uses store `taskTime` / `elapsed` when present, with a per-session wall-clock fallback when not. - **`TaskWorkLogAccordion`**: Collapsible work log for running/finished/paused task (tool activate/deactivate, agent lines); Framer `height: auto` for expand, stable segment keys from the merged log. -- **`TaskCompletionCard`**: Completion / summary style card when appropriate. - **`NoticeCard`**: Chain-of-thought or notice-style content. -- **`FeedbackCard`**: Thumbs / feedback when enabled. - **`FloatingAction`**: Compact floating controls (wired from `ProjectSection`). - **`TokenUtils`**: Animated token number and `formatSplittingElapsed` helpers. diff --git a/src/components/ChatBox/TaskBox/TaskType.tsx b/src/components/ChatBox/TaskBox/TaskType.tsx deleted file mode 100644 index 9ea0c390..00000000 --- a/src/components/ChatBox/TaskBox/TaskType.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import { useTranslation } from 'react-i18next'; -export const TaskType = ({ type }: { type: 1 | 2 | 3 }) => { - const { t } = useTranslation(); - const typeMap = { - 1: { - label: t('layout.task-splitting'), - textColor: 'text-ds-text-status-splitting-strong-default', - bgColor: 'bg-ds-bg-status-splitting-subtle-default', - dotColor: 'bg-ds-text-status-splitting-strong-default', - }, - 2: { - label: t('layout.task-running'), - textColor: 'text-ds-text-status-running-default-default', - bgColor: 'bg-ds-bg-status-running-subtle-default', - dotColor: 'bg-ds-text-status-running-default-default', - }, - 3: { - label: t('layout.task-completed'), - textColor: 'text-ds-text-neutral-default-default', - bgColor: 'bg-transparent', - dotColor: 'bg-ds-text-neutral-default-default', - }, - }; - return ( -
    -
    - {typeMap[type].label} -
    - ); -}; diff --git a/src/components/ChatBox/index.tsx b/src/components/ChatBox/index.tsx index 104460ea..e1327291 100644 --- a/src/components/ChatBox/index.tsx +++ b/src/components/ChatBox/index.tsx @@ -33,6 +33,7 @@ import { proxyUpdateTriggerExecution } from '@/service/triggerApi'; import { useAuthStore } from '@/store/authStore'; import { buildProjectContinuationContext } from '@/store/chatStore'; import { usePageTabStore } from '@/store/pageTabStore'; +import { openSettingsDialog } from '@/store/settingsDialogStore'; import { useSpaceStore } from '@/store/spaceStore'; import { ExecutionStatus } from '@/types'; import { AgentStep, ChatTaskStatus, SessionMode } from '@/types/constants'; @@ -45,7 +46,7 @@ import { useState, } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useSearchParams } from 'react-router-dom'; import { toast } from 'sonner'; import BottomBox from './BottomBox'; import { ProjectChatContainer } from './ProjectChatContainer'; @@ -370,11 +371,9 @@ export default function ChatBox(): JSX.Element { ((messageStr?: string, taskId?: string) => Promise) | null >(null); - const navigate = useNavigate(); - const handleSelectModel = useCallback(() => { - navigate('/history?tab=agents'); - }, [navigate]); + openSettingsDialog('models'); + }, []); // Task time tracking const [, setTaskTime] = useState( @@ -534,7 +533,7 @@ export default function ChatBox(): JSX.Element { return; } toast.error('Please select a model first.'); - navigate('/history?tab=agents'); + openSettingsDialog('models'); return; } @@ -572,7 +571,6 @@ export default function ChatBox(): JSX.Element { hasModel, isCloudUsageLimited, cloudUsageLimitMessage, - navigate, t, ] ); @@ -645,7 +643,7 @@ export default function ChatBox(): JSX.Element { return; } toast.error('Please select a model first.'); - navigate('/history?tab=agents'); + openSettingsDialog('models'); return; } diff --git a/src/components/Dashboard/GroupedHistoryView/ProjectDialog.tsx b/src/components/Dashboard/GroupedHistoryView/ProjectDialog.tsx deleted file mode 100644 index d7b0dd76..00000000 --- a/src/components/Dashboard/GroupedHistoryView/ProjectDialog.tsx +++ /dev/null @@ -1,289 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogContentSection, - DialogFooter, - DialogHeader, -} from '@/components/ui/dialog'; -import { Input } from '@/components/ui/input'; -import { useSpaceStore } from '@/store/spaceStore'; -import { ProjectGroup } from '@/types/history'; -import { - CheckCircle, - Clock, - Hash, - ListChecks, - Loader2, - LoaderCircle, -} from 'lucide-react'; -import { useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import TaskItem from './TaskItem'; - -interface ProjectDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - project: ProjectGroup; - onProjectRename: (projectId: string, newName: string) => void; - onTaskSelect: ( - projectId: string, - question: string, - historyId: string, - project?: ProjectGroup - ) => void; - onTaskDelete: (taskId: string) => void; - onTaskShare: (taskId: string) => void; - activeTaskId?: string; -} - -export default function ProjectDialog({ - open, - onOpenChange, - project, - onProjectRename, - onTaskSelect, - onTaskDelete, - onTaskShare, - activeTaskId, -}: ProjectDialogProps) { - const { t } = useTranslation(); - const updateProjectMeta = useSpaceStore((s) => s.updateProjectMeta); - const [projectName, setProjectName] = useState( - project.project_name || t('layout.new-project') - ); - const [isSaving, setIsSaving] = useState(false); - const saveTimeoutRef = useRef(null); - const lastSavedNameRef = useRef( - project.project_name || t('layout.new-project') - ); - - // Update state when project changes - useEffect(() => { - const name = project.project_name || t('layout.new-project'); - // Use setTimeout to defer state update and avoid cascading renders - setTimeout(() => { - setProjectName(name); - }, 0); - lastSavedNameRef.current = name; - }, [project.project_name, project.project_id, t]); - - // Auto-save with debouncing - useEffect(() => { - // Clear any existing timeout - if (saveTimeoutRef.current) { - clearTimeout(saveTimeoutRef.current); - } - - const trimmedName = projectName.trim(); - - // Only save if the name has actually changed and is not empty - if (trimmedName && trimmedName !== lastSavedNameRef.current) { - // Use setTimeout to defer state update and avoid cascading renders - setTimeout(() => { - setIsSaving(true); - }, 0); - - // Debounce: wait 800ms after user stops typing - saveTimeoutRef.current = setTimeout(() => { - // Update via callback (for history API) - onProjectRename(project.project_id, trimmedName); - - updateProjectMeta(project.project_id, { name: trimmedName }); - - lastSavedNameRef.current = trimmedName; - setIsSaving(false); - }, 800); - } else if (!trimmedName) { - // If empty, don't show saving state - // Use setTimeout to defer state update and avoid cascading renders - setTimeout(() => { - setIsSaving(false); - }, 0); - } - - // Cleanup timeout on unmount or when projectName changes - return () => { - if (saveTimeoutRef.current) { - clearTimeout(saveTimeoutRef.current); - } - }; - }, [projectName, project.project_id, onProjectRename, updateProjectMeta]); - - // Cleanup on unmount - useEffect(() => { - return () => { - if (saveTimeoutRef.current) { - clearTimeout(saveTimeoutRef.current); - } - }; - }, []); - - return ( - - e.stopPropagation()} - onMouseDown={(e) => e.stopPropagation()} - > - e.stopPropagation()} - onMouseDown={(e) => e.stopPropagation()} - /> - - e.stopPropagation()} - onMouseDown={(e) => e.stopPropagation()} - > - {/* Project Name Section - Inline Edit with Auto-Save */} -
    e.stopPropagation()} - onMouseDown={(e) => e.stopPropagation()} - > - -
    e.stopPropagation()} - onMouseDown={(e) => e.stopPropagation()} - > - setProjectName(e.target.value)} - onClick={(e) => e.stopPropagation()} - onMouseDown={(e) => e.stopPropagation()} - onFocus={(e) => e.stopPropagation()} - placeholder={t('layout.enter-project-name')} - /> - {isSaving ? ( - - ) : ( - <> - )} -
    -
    - - {/* Project Stats */} -
    -
    - - {t('layout.total-tokens')} - -
    - - - {project.total_tokens.toLocaleString()} - -
    -
    - -
    - - {t('layout.total-tasks')} - -
    - - - {project.task_count} - -
    -
    - -
    - - {t('layout.completed')} - -
    - - - {project.total_completed_tasks} - -
    -
    - -
    - - {t('layout.ongoing')} - -
    - - - {project.total_ongoing_tasks} - -
    -
    -
    - - {/* Tasks List */} -
    -
    - {project.tasks.length > 0 ? ( - project.tasks.map((task, index) => ( - - onTaskSelect( - project.project_id, - task.question, - task.id.toString(), - project - ) - } - onDelete={() => onTaskDelete(task.id.toString())} - onShare={() => onTaskShare(task.id.toString())} - isLast={index === project.tasks.length - 1} - showActions={false} - /> - )) - ) : ( -
    - - {t('layout.no-tasks-in-project')} -
    - )} -
    -
    -
    - - e.stopPropagation()} - onMouseDown={(e) => e.stopPropagation()} - > - - -
    -
    - ); -} diff --git a/src/components/Dashboard/GroupedHistoryView/ProjectGroup.tsx b/src/components/Dashboard/GroupedHistoryView/ProjectGroup.tsx deleted file mode 100644 index bd551ed5..00000000 --- a/src/components/Dashboard/GroupedHistoryView/ProjectGroup.tsx +++ /dev/null @@ -1,553 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import tokenDarkIcon from '@/assets/custom/token-dark.svg'; -import tokenLightIcon from '@/assets/custom/token-light.svg'; -import { formatTokenCount } from '@/components/ChatBox/MessageItem/TokenUtils'; -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { Tag } from '@/components/ui/tag'; -import { TooltipSimple } from '@/components/ui/tooltip'; -import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; -import { - buildTaskQuestionsById, - computeProjectFreshnessAnchor, - loadProjectFromHistory, -} from '@/lib/replay'; -import { useAuthStore } from '@/store/authStore'; -import { useProjectRuntimeStore } from '@/store/projectRuntimeStore'; -import { ChatTaskStatus } from '@/types/constants'; -import { ProjectGroup as ProjectGroupType } from '@/types/history'; -import { motion } from 'framer-motion'; -import { - FolderCheck, - FolderClock, - ListChecks, - Loader2, - MoreHorizontal, - Trash2, - Zap, -} from 'lucide-react'; -import React, { useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; -import ProjectDialog from './ProjectDialog'; - -const compactCountFormatter = new Intl.NumberFormat('en', { - notation: 'compact', - maximumFractionDigits: 1, -}); - -const formatCompactCount = (value?: number) => - compactCountFormatter.format(value || 0).replace('.0', ''); - -const toFiniteNumber = (value: unknown): number | null => { - if (typeof value === 'number') { - return Number.isFinite(value) ? value : null; - } - if (typeof value === 'string') { - const parsed = Number(value.replace(/,/g, '').trim()); - return Number.isFinite(parsed) ? parsed : null; - } - return null; -}; - -const resolveProjectTokenCount = (project: ProjectGroupType): number => { - const direct = toFiniteNumber(project.total_tokens as unknown); - if (direct !== null) { - return direct; - } - - // Fallback for inconsistent payloads where project-level total is missing/invalid. - return (project.tasks || []).reduce( - (sum, task) => - sum + (toFiniteNumber((task as { tokens?: unknown }).tokens) ?? 0), - 0 - ); -}; - -interface ProjectGroupProps { - project: ProjectGroupType; - onTaskSelect: ( - projectId: string, - question: string, - historyId: string, - project?: ProjectGroupType - ) => void; - onTaskDelete: (taskId: string) => void; - onTaskShare: (taskId: string) => void; - activeTaskId?: string; - searchValue?: string; - isOngoing?: boolean; - onOngoingTaskPause?: (taskId: string) => void; - onOngoingTaskResume?: (taskId: string) => void; - onProjectEdit?: (projectId: string) => void; - onProjectDelete?: (projectId: string) => void; - onProjectRename?: (projectId: string, newName: string) => void; - viewMode?: 'grid' | 'list'; -} - -export default function ProjectGroup({ - project, - onTaskSelect, - onTaskDelete, - onTaskShare, - activeTaskId, - searchValue = '', - isOngoing = false, - onOngoingTaskPause: _onOngoingTaskPause, - onOngoingTaskResume: _onOngoingTaskResume, - onProjectEdit: _onProjectEdit, - onProjectDelete, - onProjectRename, - viewMode = 'grid', -}: ProjectGroupProps) { - const { t } = useTranslation(); - const { appearance } = useAuthStore(); - const navigate = useNavigate(); - const projectStore = useProjectRuntimeStore(); - const { chatStore } = useChatStoreAdapter(); - const tokenIcon = appearance === 'dark' ? tokenDarkIcon : tokenLightIcon; - const totalTokenCount = resolveProjectTokenCount(project); - const [_isExpanded, _setIsExpanded] = useState(true); - const [isDialogOpen, setIsDialogOpen] = useState(false); - const [isLoadingProject, setIsLoadingProject] = useState(false); - - // Filter tasks based on search value - const filteredTasks = project.tasks.filter((task) => - task.question?.toLowerCase().includes(searchValue.toLowerCase()) - ); - - // Calculate if project has issues (requiring human in the loop) - // Find tasks in chatStore where task_id matches any task in the project - const hasHumanInLoop = useMemo(() => { - if (!chatStore?.tasks || !project.tasks?.length) return false; - - // Get all task_ids from the project, filtering out undefined/null values - const projectTaskIds = project.tasks - .map((task) => task.task_id) - .filter((id): id is string => !!id); - - // Check if any task in chatStore with matching task_id has pending status - return Object.entries(chatStore.tasks).some( - ([taskId, task]) => - projectTaskIds.includes(taskId) && - task.status === ChatTaskStatus.PENDING - ); - }, [chatStore?.tasks, project.tasks]); - const _hasIssue = hasHumanInLoop; - - // Handler to navigate to project - const handleProjectClick = async (e: React.MouseEvent) => { - // Don't trigger if clicking on interactive elements (buttons, dropdowns) - if ((e.target as HTMLElement).closest('button, [role="menuitem"]')) { - return; - } - - // Check if project exists in store - const existingProject = projectStore.getProjectById(project.project_id); - - if (existingProject) { - // Project exists, just activate it and navigate - projectStore.setActiveProject(project.project_id); - navigate('/'); - } else { - // Project doesn't exist, load final state (no replay animation) - const firstTask = project.tasks?.[0]; - if (firstTask) { - const question = firstTask.question || project.last_prompt || ''; - const historyId = firstTask.id?.toString() || ''; - const taskIdsList = project.tasks - ?.map((t) => t.task_id) - .filter(Boolean) || [project.project_id]; - - setIsLoadingProject(true); - try { - await loadProjectFromHistory( - projectStore, - navigate, - project.project_id, - question, - historyId, - taskIdsList, - project.project_name, - project.space_id, - buildTaskQuestionsById(project.tasks), - computeProjectFreshnessAnchor(project) - ); - } catch (error) { - console.error('Failed to load project:', error); - } finally { - setIsLoadingProject(false); - } - } else { - // No tasks to replay - project has triggers but no tasks - // Create an empty project with this ID and navigate to it - projectStore.createProject( - project.project_name || 'Project', - 'Project with triggers', - project.project_id - ); - navigate('/'); - } - } - }; - - // Don't render if no tasks match the search - if (searchValue && filteredTasks.length === 0) { - return null; - } - - const _formatDate = (dateString: string) => { - const date = new Date(dateString); - const now = new Date(); - const diffInDays = Math.floor( - (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24) - ); - - if (diffInDays === 0) return t('layout.today'); - if (diffInDays === 1) return t('layout.yesterday'); - if (diffInDays < 7) return `${diffInDays} ${t('layout.days-ago')}`; - - return date.toLocaleDateString(); - }; - - // Calculate agent count (placeholder - count unique agents from tasks if available) - const _agentCount = - project.tasks?.length > 0 - ? new Set(project.tasks.map((t) => t.model_type || 'default')).size - : 0; - - // Trigger count is 0 for now (disabled) - // const _triggerCount = 0; - - // Handle project rename - const handleProjectRename = (projectId: string, newName: string) => { - if (onProjectRename) { - onProjectRename(projectId, newName); - } - }; - - // Grid view - Card-based design - if (viewMode === 'grid') { - return ( - - {isLoadingProject && ( -
    - -
    - )} - {/* Project Card */} -
    - {/* Header with menu */} -
    -
    -
    - {isOngoing ? ( - - ) : ( - - )} - - {/* Status badges */} -
    - {/* TODO: Add ongoing badge after finish state management is implemented */} - {/* {isOngoing && ( - - - - {t("layout.ongoing")} - - - )} */} - - {/* {!isOngoing && hasIssue && ( - - - {t("layout.issue") || "Issue"} - - - )} */} -
    -
    - {project.project_name}

    - } - className="pointer-events-auto select-text text-wrap break-words bg-ds-bg-neutral-strong-default px-2 text-label-xs shadow-perfect" - > - - {project.project_name} - -
    -
    - - {/* Menu button */} - - - - - - {onProjectDelete && ( - { - e.stopPropagation(); - onProjectDelete(project.project_id); - }} - className="cursor-pointer bg-ds-bg-neutral-subtle-default text-ds-text-error-default-default hover:bg-ds-bg-neutral-subtle-hover hover:text-ds-text-error-default-default focus:text-ds-text-error-default-default data-[highlighted]:text-ds-text-error-default-default [&>svg]:text-ds-icon-error-default-default hover:[&>svg]:text-ds-icon-error-default-default focus:[&>svg]:text-ds-icon-error-default-default data-[highlighted]:[&>svg]:text-ds-icon-error-default-default" - > - - {t('layout.delete')} - - )} - - -
    - - {/* Project Dialog */} - - - {/* Footer with stats */} - -
    - - - - - {formatTokenCount(totalTokenCount)} - - - - - - - - {formatCompactCount(project.task_count)} - - - - - - - - - {formatCompactCount(project.total_triggers)} - - - -
    -
    -
    -
    - ); - } - - // List view - Original horizontal layout - return ( -
    - {isLoadingProject && ( -
    - -
    - )} - {/* Project */} -
    - {/* Start: Folder icon and project name - Fixed width */} -
    - {isOngoing ? ( - - ) : ( - - )} - {project.project_name}

    - } - className="pointer-events-auto select-text text-wrap break-words bg-ds-bg-neutral-strong-default px-2 text-label-xs shadow-perfect" - > - - {project.project_name} - -
    -
    - - {/* Middle: Project, Trigger, Agent tags - Aligned to right */} -
    - - - - {formatTokenCount(totalTokenCount)} - - - - - - - - {formatCompactCount(project.task_count)} - - - - - - - - - {formatCompactCount(project.total_triggers)} - - - -
    - - {/* End: Status and menu */} -
    - {/* Status tag */} - {/* {isOngoing && ( - - - {t("layout.ongoing")} - - )} */} - - {/* {!isOngoing && hasIssue && ( - - {t("layout.issue") || "Issue"} - - )} */} - - {/* Menu button */} - - - - - - {onProjectDelete && ( - onProjectDelete(project.project_id)} - className="cursor-pointer bg-ds-bg-neutral-subtle-default text-ds-text-error-default-default hover:bg-ds-bg-neutral-subtle-hover hover:text-ds-text-error-default-default focus:text-ds-text-error-default-default data-[highlighted]:text-ds-text-error-default-default [&>svg]:text-ds-icon-error-default-default hover:[&>svg]:text-ds-icon-error-default-default focus:[&>svg]:text-ds-icon-error-default-default data-[highlighted]:[&>svg]:text-ds-icon-error-default-default" - > - - {t('layout.delete')} - - )} - - -
    -
    - - {/* Project Dialog */} - -
    - ); -} diff --git a/src/components/Dashboard/GroupedHistoryView/TaskItem.tsx b/src/components/Dashboard/GroupedHistoryView/TaskItem.tsx deleted file mode 100644 index a63b4316..00000000 --- a/src/components/Dashboard/GroupedHistoryView/TaskItem.tsx +++ /dev/null @@ -1,224 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import { Button } from '@/components/ui/button'; -import { - Popover, - PopoverClose, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; -import { Tag } from '@/components/ui/tag'; -import { TooltipSimple } from '@/components/ui/tooltip'; -import { formatDateTime } from '@/lib/utils'; -import { ChatTaskStatus } from '@/types/constants'; -import { HistoryTask } from '@/types/history'; -import { - CheckCircle, - CirclePause, - CirclePlay, - Clock, - Ellipsis, - Hash, - ListChecks, - Share, - Trash2, -} from 'lucide-react'; -import { useTranslation } from 'react-i18next'; - -interface TaskItemProps { - task: HistoryTask; - isActive: boolean; - onSelect: () => void; - onDelete: () => void; - onShare: () => void; - isLast: boolean; - isOngoing?: boolean; - onPause?: () => void; - onResume?: () => void; - showActions?: boolean; -} - -export default function TaskItem({ - task, - isActive, - onSelect, - onDelete, - onShare, - isLast, - isOngoing = false, - onPause, - onResume, - showActions = true, -}: TaskItemProps) { - const { t } = useTranslation(); - - // Check if task is paused (for ongoing tasks) - const isPaused = (task as any)._taskData?.status === ChatTaskStatus.PAUSE; - - const getStatusTag = (status: number) => { - // ChatStatus enum: ongoing = 1, done = 2 - switch (status) { - case 1: // ChatStatus.ongoing - return ( - - - {t('layout.running')} - - ); - case 2: // ChatStatus.done - return ( - - - {t('layout.completed')} - - ); - default: // Unknown status - return ( - - - {t('layout.unknown')} - - ); - } - }; - - const formatDate = (dateString?: string) => { - if (!dateString) return ''; - return formatDateTime(dateString, 'MMM dd, yyyy HH:mm'); - }; - - return ( -
    -
    - - - - -
    - -
    - {task.summary || task.question} -
    -
    - {t('layout.created')}: {formatDate(task.created_at)} -
    -
    - {t('chat.token')}:{' '} - {task.tokens ? task.tokens.toLocaleString() : '0'} -
    -
    - } - > - - {task.summary || task.question || t('layout.new-project')} - - -
    -
    - -
    - {!isOngoing && getStatusTag(task.status)} - - - - {task.tokens ? task.tokens.toLocaleString() : '0'} - - - {isOngoing && (onPause || onResume) && ( - { - e.stopPropagation(); - if (isPaused && onResume) { - onResume(); - } else if (!isPaused && onPause) { - onPause(); - } - }} - > - {isPaused ? : } - {isPaused ? t('layout.continue') : t('layout.pause')} - - )} - - {showActions && ( - - - - - -
    - {!isOngoing && ( - - - - )} - - - - -
    -
    -
    - )} -
    - - ); -} diff --git a/src/components/Dashboard/GroupedHistoryView/index.tsx b/src/components/Dashboard/GroupedHistoryView/index.tsx deleted file mode 100644 index 065c392b..00000000 --- a/src/components/Dashboard/GroupedHistoryView/index.tsx +++ /dev/null @@ -1,781 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import { proxyFetchDelete, proxyFetchPut } from '@/api/http'; -import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Tag } from '@/components/ui/tag'; -import { useHost } from '@/host'; -import { isLegacySpace } from '@/lib/spaceLabel'; -import { fetchGroupedHistoryTasks } from '@/service/historyApi'; -import { getAuthStore, useAuthStore } from '@/store/authStore'; -import { useGlobalStore } from '@/store/globalStore'; -import { useProjectRuntimeStore } from '@/store/projectRuntimeStore'; -import { useSpaceStore } from '@/store/spaceStore'; -import { ProjectGroup as ProjectGroupType } from '@/types/history'; -import { AnimatePresence, motion } from 'framer-motion'; -import { - Folder, - FolderKanban, - FolderOpen, - LayoutGrid, - List, - ListChecks, - Zap, -} from 'lucide-react'; -import type { Dispatch, SetStateAction } from 'react'; -import { useCallback, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import ProjectGroup from './ProjectGroup'; - -/** Session cache so remounts (e.g. dialogs) show the project list immediately without refetching. */ -let groupedHistorySnapshot: { - email: string | null; - scopeKey: string | null; - projects: ProjectGroupType[]; - triggerKey: number | string; -} | null = null; - -interface GroupedHistoryViewProps { - /** When true, grid/list tabs are hidden (e.g. controlled from a parent sidebar). */ - hideViewSwitcher?: boolean; - /** Active Space filter is still available, but the Hub/history default is all Spaces. */ - spaceScope?: 'all' | 'active'; - searchValue?: string; - onTaskSelect: ( - projectId: string, - question: string, - historyId: string, - project?: ProjectGroupType - ) => void; - onTaskDelete: (historyId: string, callback: () => void) => void; - onTaskShare: (taskId: string) => void; - activeTaskId?: string; - refreshTrigger?: number; // For triggering refresh from parent - ongoingTasks?: { [taskId: string]: any }; // Add ongoing tasks from chatStore - onOngoingTaskClick?: (taskId: string) => void; // Callback for clicking ongoing tasks - onOngoingTaskPause?: (taskId: string) => void; // Callback for pausing ongoing tasks - onOngoingTaskResume?: (taskId: string) => void; // Callback for resuming ongoing tasks - onOngoingTaskDelete?: (taskId: string) => void; // Callback for deleting ongoing tasks - onProjectEdit?: (projectId: string) => void; // Callback for editing a project - onProjectDelete?: (projectId: string, callback: () => Promise) => void; // Callback for deleting a project with async callback -} - -export default function GroupedHistoryView({ - hideViewSwitcher = false, - spaceScope = 'all', - searchValue = '', - onTaskSelect, - onTaskDelete, - onTaskShare, - activeTaskId, - refreshTrigger, - ongoingTasks: _ongoingTasks = {}, - onOngoingTaskClick: _onOngoingTaskClick, - onOngoingTaskPause, - onOngoingTaskResume, - onOngoingTaskDelete: _onOngoingTaskDelete, - onProjectEdit, - onProjectDelete, -}: GroupedHistoryViewProps) { - const { t } = useTranslation(); - const email = useAuthStore((s) => s.email); - const host = useHost(); - const ipcRenderer = host?.ipcRenderer; - const triggerKey = refreshTrigger ?? '_default'; - const activeSpaceId = useSpaceStore((s) => s.activeSpaceId); - const spacesById = useSpaceStore((s) => s.spaces); - const scopeKey = - spaceScope === 'active' ? (activeSpaceId ?? null) : '__all_spaces__'; - - const [projects, setProjects] = useState(() => { - const snap = groupedHistorySnapshot; - if ( - snap && - snap.email === (email ?? null) && - snap.scopeKey === scopeKey && - snap.triggerKey === triggerKey - ) { - return snap.projects; - } - return []; - }); - const [loading, setLoading] = useState(() => { - const snap = groupedHistorySnapshot; - return !( - snap && - snap.email === (email ?? null) && - snap.scopeKey === scopeKey && - snap.triggerKey === triggerKey - ); - }); - const { history_type, setHistoryType } = useGlobalStore(); - const projectStore = useProjectRuntimeStore(); - - // Default to list view if not set - const viewType = history_type || 'list'; - - useEffect(() => { - const u = email ?? null; - if ( - groupedHistorySnapshot && - groupedHistorySnapshot.email === u && - groupedHistorySnapshot.scopeKey === scopeKey && - groupedHistorySnapshot.triggerKey === triggerKey - ) { - groupedHistorySnapshot = { - ...groupedHistorySnapshot, - scopeKey, - projects, - }; - } - }, [projects, email, triggerKey, scopeKey]); - - const loadProjects = useCallback( - async (opts?: { silent?: boolean }) => { - if (!opts?.silent) setLoading(true); - const u = email ?? null; - try { - const snapshotSetter: Dispatch> = ( - action - ) => { - setProjects((prev) => { - const next = - typeof action === 'function' - ? (action as (p: ProjectGroupType[]) => ProjectGroupType[])( - prev - ) - : action; - groupedHistorySnapshot = { - email: u, - scopeKey, - projects: next, - triggerKey, - }; - return next; - }); - }; - await fetchGroupedHistoryTasks(snapshotSetter, { - spaceId: spaceScope === 'active' ? activeSpaceId : undefined, - }); - } catch (error) { - console.error('Failed to load grouped projects:', error); - } finally { - if (!opts?.silent) setLoading(false); - } - }, - [email, triggerKey, activeSpaceId, scopeKey, spaceScope] - ); - - const onDelete = (historyId: string) => { - try { - onTaskDelete(historyId, () => { - setProjects((prevProjects) => { - // Create new project objects instead of mutating existing ones - return prevProjects - .map((project) => { - const filteredTasks = project.tasks.filter( - (task) => String(task.id) !== historyId - ); - return { - ...project, - tasks: filteredTasks, - task_count: filteredTasks.length, - total_tokens: filteredTasks.reduce( - (sum, task) => sum + (task.tokens || 0), - 0 - ), - }; - }) - .filter((project) => project.tasks.length > 0); - }); - }); - } catch (error) { - console.error('Failed to delete task:', error); - } - }; - - const handleProjectEdit = (projectId: string) => { - if (onProjectEdit) { - onProjectEdit(projectId); - } - }; - - const handleProjectDelete = (projectId: string) => { - // Create the deletion callback that will be executed after confirmation - const deleteCallback = async () => { - try { - // Find the project in our existing data - const targetProject = projects.find( - (project) => project.project_id === projectId - ); - - if ( - targetProject && - targetProject.tasks && - targetProject.tasks.length > 0 - ) { - console.log( - `Deleting project ${projectId} with ${targetProject.tasks.length} tasks` - ); - - // Delete each task one by one - for (const history of targetProject.tasks) { - try { - await proxyFetchDelete(`/api/v1/chat/history/${history.id}`); - console.log(`Successfully deleted task ${history.task_id}`); - - // Also delete local files for this task if available (via Electron IPC) - const { email } = getAuthStore(); - if (history.task_id && ipcRenderer) { - try { - await ipcRenderer.invoke( - 'delete-task-files', - email, - history.task_id, - history.project_id ?? undefined - ); - console.log( - `Successfully cleaned up local files for task ${history.task_id}` - ); - } catch (error) { - console.warn( - `Local file cleanup failed for task ${history.task_id}:`, - error - ); - } - } - } catch (error) { - console.error(`Failed to delete task ${history.task_id}:`, error); - } - } - - // Remove from projectStore - projectStore.removeProject(projectId); - - // Update local state to remove the project - setProjects((prevProjects) => - prevProjects.filter((project) => project.project_id !== projectId) - ); - - console.log(`Completed deletion of project ${projectId}`); - } else if (targetProject) { - // Project exists but has no tasks, just remove from store - console.log( - `Project ${projectId} has no tasks, removing from store only` - ); - projectStore.removeProject(projectId); - setProjects((prevProjects) => - prevProjects.filter((project) => project.project_id !== projectId) - ); - } else { - console.warn(`Project ${projectId} not found`); - } - } catch (error) { - console.error('Failed to delete project:', error); - throw error; // Re-throw to let parent handle errors - } - }; - - // Call parent callback with the deletion callback (for confirmation dialog) - if (onProjectDelete) { - onProjectDelete(projectId, deleteCallback); - } else { - // If no parent callback, execute deletion directly - deleteCallback(); - } - }; - - const handleProjectRename = async (projectId: string, newName: string) => { - setProjects((prevProjects) => { - return prevProjects.map((project) => { - if (project.project_id === projectId) { - return { - ...project, - project_name: newName, - }; - } - return project; - }); - }); - - // Call API to update project name - try { - const response = await proxyFetchPut( - `/api/v1/chat/project/${projectId}/name?new_name=${encodeURIComponent(newName)}` - ); - - if (response && response.code !== undefined && response.code !== 0) { - console.error(`Failed to update project name: ${response.code}`); - // Optionally: revert the local change if API call fails - } else { - console.log( - `Successfully updated project ${projectId} name to ${newName}` - ); - } - } catch (error) { - console.error(`Error updating project name:`, error); - // Optionally: revert the local change if API call fails - } - }; - - useEffect(() => { - const u = email ?? null; - const snap = groupedHistorySnapshot; - if ( - snap && - snap.email === u && - snap.scopeKey === scopeKey && - snap.triggerKey === triggerKey - ) { - setProjects(snap.projects); - setLoading(false); - void loadProjects({ silent: true }); - return; - } - void loadProjects(); - }, [activeSpaceId, email, triggerKey, loadProjects, scopeKey]); - - // Filter projects based on search value - const filteredProjects = projects.filter((project) => { - if (!searchValue) return true; - - // Check if project name matches - if ( - project.project_name?.toLowerCase().includes(searchValue.toLowerCase()) - ) { - return true; - } - - // Check if any task in the project matches - return project.tasks.some((task) => - task.question?.toLowerCase().includes(searchValue.toLowerCase()) - ); - }); - - // Get all projects from projectStore and find empty ones - const allProjectsFromStore = projectStore.getAllProjects( - spaceScope === 'active' ? (activeSpaceId ?? undefined) : undefined - ); - const emptyProjects = allProjectsFromStore.filter((project) => - projectStore.isEmptyProject(project) - ); - - // Get IDs of projects already in filteredProjects to avoid duplicates - const filteredProjectIds = new Set(filteredProjects.map((p) => p.project_id)); - - // Convert empty projects from projectStore format to ProjectGroup format - // Filter out any that already exist in filteredProjects - const emptyProjectGroups: ProjectGroupType[] = emptyProjects - .filter((project) => !filteredProjectIds.has(project.id)) - .map((project) => ({ - project_id: project.id, - space_id: project.spaceId, - project_name: project.name, - total_tokens: 0, - task_count: 0, - latest_task_date: new Date(project.updatedAt).toISOString(), - last_prompt: '', - tasks: [], - total_completed_tasks: 0, - total_triggers: 0, - total_ongoing_tasks: 0, - average_tokens_per_task: 0, - })); - - // Combine filtered projects with empty projects from store - const allProjects = [...emptyProjectGroups, ...filteredProjects]; - const projectsBySpace = allProjects.reduce< - Record< - string, - { - spaceId: string; - spaceName: string; - isLegacy: boolean; - latestDate: number; - projects: ProjectGroupType[]; - } - > - >((acc, project) => { - const spaceId = project.space_id || activeSpaceId || 'unknown'; - const space = spacesById[spaceId]; - const isLegacy = space ? isLegacySpace(space) : false; - const latestDate = new Date(project.latest_task_date || 0).getTime(); - if (!acc[spaceId]) { - acc[spaceId] = { - spaceId, - spaceName: - space?.name?.trim() || - (isLegacy - ? t('layout.spaces-hub-legacy-tag') - : t('layout.spaces-untitled')), - isLegacy, - latestDate: Number.isFinite(latestDate) ? latestDate : 0, - projects: [], - }; - } - acc[spaceId].latestDate = Math.max( - acc[spaceId].latestDate, - Number.isFinite(latestDate) ? latestDate : 0 - ); - acc[spaceId].projects.push(project); - return acc; - }, {}); - const projectSections = Object.values(projectsBySpace).sort( - (a, b) => b.latestDate - a.latestDate - ); - - // Shimmer animation styles - // Shimmer animation styles - const shimmerStyle = { - background: - 'linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--ds-text-neutral-subtle-default) 40%, transparent) 50%, transparent 100%)', - backgroundSize: '200% 100%', - animation: 'shimmer 1.5s infinite', - }; - - // Skeleton component for list card loading state - const ListCardSkeleton = () => ( -
    -
    - {/* Start: Folder icon and project name skeleton */} -
    -
    -
    -
    -
    -
    -
    -
    - - {/* Middle: Tags skeleton */} -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - {/* End: Menu skeleton */} -
    -
    -
    -
    -
    -
    -
    - ); - - if (loading) { - return ( -
    - {/* Keyframe animation for shimmer effect */} - - - {/* Summary skeleton */} -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - {/* List skeleton cards */} -
    - {[1, 2, 3, 4, 5].map((i) => ( - - ))} -
    -
    - ); - } - - if (allProjects.length === 0) { - return ( -
    - -
    - {searchValue - ? t('dashboard.no-projects-match-search') - : t('dashboard.no-projects-found')} -
    - {searchValue && ( -
    - {t('dashboard.try-different-search')} -
    - )} -
    - ); - } - - const renderProjectGroup = ( - project: ProjectGroupType, - viewMode: 'grid' | 'list' - ) => ( - 0} - onOngoingTaskPause={onOngoingTaskPause} - onOngoingTaskResume={onOngoingTaskResume} - onProjectEdit={handleProjectEdit} - onProjectDelete={handleProjectDelete} - onProjectRename={handleProjectRename} - viewMode={viewMode} - /> - ); - - return ( -
    - {/* Summary */} -
    -
    - - - {t('layout.projects')} - - {allProjects.length} - - - - - {t('layout.total-tasks')} - {allProjects.reduce( - (total, project) => total + project.task_count, - 0 - )} - - - - - - {' '} - {t('layout.triggers') || 'Triggers'} - - {allProjects.reduce( - (total, project) => total + (project.total_triggers || 0), - 0 - )} - -
    -
    - {!hideViewSwitcher && ( - - setHistoryType(value as 'grid' | 'list') - } - > - - -
    - - {t('dashboard.grid')} -
    -
    - -
    - - {t('dashboard.list')} -
    -
    -
    -
    - )} -
    -
    - - - - {viewType === 'grid' ? ( - // Grid layout for project cards - - - {projectSections.map((section) => ( - -
    - - - {section.spaceName} - - {section.isLegacy ? ( - - ) : null} - - {section.projects.length} - -
    -
    - {section.projects.map((project) => ( -
    - {renderProjectGroup(project, 'grid')} -
    - ))} -
    -
    - ))} -
    -
    - ) : ( - // List layout for projects - - - {projectSections.map((section) => ( - -
    - - - {section.spaceName} - - {section.isLegacy ? ( - - ) : null} - - {section.projects.length} - -
    - {section.projects.map((project) => ( -
    - {renderProjectGroup(project, 'list')} -
    - ))} -
    - ))} -
    -
    - )} -
    -
    -
    - ); -} diff --git a/src/components/Dashboard/HistoryTabsNav.tsx b/src/components/Dashboard/HistoryTabsNav.tsx deleted file mode 100644 index 076c153b..00000000 --- a/src/components/Dashboard/HistoryTabsNav.tsx +++ /dev/null @@ -1,260 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import { Blocks } from '@/components/ui/animate-ui/icons/blocks'; -import { Bot } from '@/components/ui/animate-ui/icons/bot'; -import { Compass } from '@/components/ui/animate-ui/icons/compass'; -import { Hammer } from '@/components/ui/animate-ui/icons/hammer'; -import { AnimateIcon } from '@/components/ui/animate-ui/icons/icon'; -import { Radio } from '@/components/ui/animate-ui/icons/radio'; -import { Settings } from '@/components/ui/animate-ui/icons/settings'; -import { cn } from '@/lib/utils'; -import { motion } from 'framer-motion'; -import type { ReactNode } from 'react'; -import { useCallback, useLayoutEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -const underlineSlideTransition = { - type: 'spring' as const, - stiffness: 420, - damping: 34, - mass: 0.55, -}; - -const underlineInstantTransition = { duration: 0 }; - -export const HISTORY_TAB_IDS = [ - 'home', - 'agents', - 'channels', - 'connectors', - 'browser', - 'settings', -] as const; - -export type HistoryTabId = (typeof HISTORY_TAB_IDS)[number]; - -export function isHistoryTabId(value: string): value is HistoryTabId { - return (HISTORY_TAB_IDS as readonly string[]).includes(value); -} - -type TabConfig = { - id: HistoryTabId; - icon: ReactNode; - iconAnimateOnHover: boolean | string; -}; - -const HISTORY_TABS: TabConfig[] = [ - { id: 'home', icon: , iconAnimateOnHover: 'default' }, - { id: 'agents', icon: , iconAnimateOnHover: 'default' }, - { id: 'channels', icon: , iconAnimateOnHover: 'default' }, - { id: 'connectors', icon: , iconAnimateOnHover: 'default' }, - { id: 'browser', icon: , iconAnimateOnHover: 'default' }, - { id: 'settings', icon: , iconAnimateOnHover: 'default' }, -]; - -const tabButtonClass = - 'group relative z-10 inline-flex h-8 min-h-8 shrink-0 items-center gap-1 rounded-lg px-2 text-label-sm font-bold transition-colors'; - -const iconSlotClass = - 'inline-flex size-4 shrink-0 items-center justify-center [&_svg]:size-4'; - -export type HistoryTabsNavProps = { - activeTab: HistoryTabId; - onChange: (value: string) => void; - className?: string; -}; - -export function HistoryTabsNav({ - activeTab, - onChange, - className, -}: HistoryTabsNavProps) { - const { t } = useTranslation(); - const navRef = useRef(null); - const [hoveredTab, setHoveredTab] = useState(null); - const [hoverRect, setHoverRect] = useState({ - left: 0, - top: 0, - width: 0, - height: 0, - }); - const [activeLine, setActiveLine] = useState({ - left: 0, - top: 0, - width: 0, - }); - /** False until first layout; enter uses fade-in instead of spring from (0,0). */ - const [underlineEntered, setUnderlineEntered] = useState(false); - const isFirstUnderlinePositionRef = useRef(true); - - const updateActiveLine = useCallback(() => { - const nav = navRef.current; - if (!nav) return; - const el = nav.querySelector( - `[data-history-tab="${activeTab}"]` - ); - if (!el) return; - const r = el.getBoundingClientRect(); - const nr = nav.getBoundingClientRect(); - const gapPx = 8; - const next = { - left: r.left - nr.left, - top: r.bottom - nr.top + gapPx, - width: r.width, - }; - if (next.width <= 0) return; - setActiveLine(next); - if (isFirstUnderlinePositionRef.current) { - isFirstUnderlinePositionRef.current = false; - requestAnimationFrame(() => setUnderlineEntered(true)); - } - }, [activeTab]); - - useLayoutEffect(() => { - updateActiveLine(); - const nav = navRef.current; - if (!nav) return; - const onResize = () => updateActiveLine(); - window.addEventListener('resize', onResize); - const ro = new ResizeObserver(onResize); - ro.observe(nav); - return () => { - window.removeEventListener('resize', onResize); - ro.disconnect(); - }; - }, [updateActiveLine]); - - const updateHoverRect = useCallback((el: HTMLElement) => { - const nav = navRef.current; - if (!nav) return; - const r = el.getBoundingClientRect(); - const nr = nav.getBoundingClientRect(); - setHoverRect({ - left: r.left - nr.left, - top: r.top - nr.top, - width: r.width, - height: r.height, - }); - }, []); - - useLayoutEffect(() => { - if (!hoveredTab) return; - const el = navRef.current?.querySelector( - `[data-history-tab="${hoveredTab}"]` - ); - if (el) updateHoverRect(el); - }, [activeTab, hoveredTab, updateHoverRect]); - - useLayoutEffect(() => { - if (!hoveredTab) return; - const el = navRef.current?.querySelector( - `[data-history-tab="${hoveredTab}"]` - ); - if (!el || !navRef.current) return; - const nav = navRef.current; - const onResize = () => updateHoverRect(el); - window.addEventListener('resize', onResize); - const ro = new ResizeObserver(onResize); - ro.observe(nav); - return () => { - window.removeEventListener('resize', onResize); - ro.disconnect(); - }; - }, [hoveredTab, updateHoverRect]); - - return ( -
    setHoveredTab(null)} - role="tablist" - > - - {activeLine.width > 0 && ( - - )} - {HISTORY_TABS.map(({ id, icon, iconAnimateOnHover }) => ( - - - - ))} -
    - ); -} diff --git a/src/components/Dashboard/IntegrationList/index.tsx b/src/components/Dashboard/IntegrationList/index.tsx index 1336576f..2c3bc82f 100644 --- a/src/components/Dashboard/IntegrationList/index.tsx +++ b/src/components/Dashboard/IntegrationList/index.tsx @@ -23,6 +23,7 @@ import { import { CircleAlert, Settings2 } from 'lucide-react'; import ellipseIcon from '@/assets/mcp/Ellipse-25.svg'; +import { MCPEnvDialog } from '@/components/Settings/Connectors/components/MCPEnvDialog'; import { Select, SelectContent, @@ -37,7 +38,6 @@ import { import { getProxyBaseURL } from '@/lib'; import { OAuth } from '@/lib/oauth'; import { cn } from '@/lib/utils'; -import { MCPEnvDialog } from '@/pages/Connectors/components/MCPEnvDialog'; import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -364,7 +364,7 @@ export default function IntegrationList({ } > {isSelectMode ? ( -
    +
    {selectWithCheckbox && ( {item.name}
    ) : ( -
    -
    +
    +
    {showStatusDot && (
    -
    +
    {showConfigButton && ( - - - {t('dashboard.search-dialog')} - - - - {t('dashboard.no-results')} - {history_type === 'grid' ? ( -
    - -
    - ) : ( - - {historyTasks.map((task) => ( - - handleSetActive( - task.task_id, - task.question, - String(task.id) - ) - } - > - -
    - {task.question} -
    -
    - ))} -
    - )} - -
    -
    - - ); -} diff --git a/src/components/Dispatch/DispatchChannelCard.tsx b/src/components/Dispatch/DispatchChannelCard.tsx deleted file mode 100644 index 828166dc..00000000 --- a/src/components/Dispatch/DispatchChannelCard.tsx +++ /dev/null @@ -1,114 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import type { WebSocketConnectionStatus } from '@/store/triggerStore'; -import type { ReactNode } from 'react'; - -function statusDotClass(status: WebSocketConnectionStatus): string { - switch (status) { - case 'connected': - return 'bg-green-500'; - case 'connecting': - return 'bg-yellow-400 animate-pulse'; - case 'unhealthy': - case 'disconnected': - default: - return 'bg-red-500'; - } -} - -interface DispatchChannelCardProps { - name: string; - icon?: string; - leading?: ReactNode; - disabled?: boolean; - connectionStatus?: WebSocketConnectionStatus; - badgeText?: string; - action?: { - label: string; - icon?: ReactNode; - onClick: () => void; - }; -} - -export function DispatchChannelCard({ - name, - icon, - leading, - disabled, - connectionStatus, - badgeText, - action, -}: DispatchChannelCardProps) { - return ( -
    -
    -
    - {leading} - {icon && ( - - )} - - {name} - -
    - {connectionStatus && ( - - )} -
    - -
    - {badgeText ? ( - - {badgeText} - - ) : ( -
    - )} - - {action && ( - - )} -
    -
    - ); -} diff --git a/src/components/ErrorBoundary/index.tsx b/src/components/ErrorBoundary/index.tsx deleted file mode 100644 index 37b4d10a..00000000 --- a/src/components/ErrorBoundary/index.tsx +++ /dev/null @@ -1,104 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import { Button } from '@/components/ui/button'; -import { AlertTriangle, RefreshCw } from 'lucide-react'; -import { Component, ErrorInfo, ReactNode } from 'react'; - -interface Props { - children: ReactNode; - fallback?: ReactNode; -} - -interface State { - hasError: boolean; - error: Error | null; - errorInfo: ErrorInfo | null; -} - -export class ErrorBoundary extends Component { - constructor(props: Props) { - super(props); - this.state = { hasError: false, error: null, errorInfo: null }; - } - - static getDerivedStateFromError(error: Error): State { - return { hasError: true, error, errorInfo: null }; - } - - componentDidCatch(error: Error, errorInfo: ErrorInfo) { - this.setState({ error, errorInfo }); - console.error('ErrorBoundary caught an error:', error, errorInfo); - - // Log error details for debugging - console.error('Error Stack:', error.stack); - console.error('Component Stack:', errorInfo.componentStack); - } - - handleReset = () => { - this.setState({ hasError: false, error: null, errorInfo: null }); - window.location.reload(); - }; - - render() { - if (this.state.hasError) { - // If a custom fallback is provided, use it - if (this.props.fallback) { - return this.props.fallback; - } - - // Default error UI - return ( -
    -
    -
    - -
    -
    -

    - Something went wrong -

    -

    - An unexpected error occurred. Please try refreshing the page. -

    -
    - {this.state.error && ( -
    -

    - Error details: -

    -

    - {this.state.error.toString()} -

    -
    - )} -
    - -
    -
    -
    - ); - } - - return this.props.children; - } -} diff --git a/src/components/HistorySidebar/SearchInput.tsx b/src/components/HistorySidebar/SearchInput.tsx deleted file mode 100644 index 06ab8f9f..00000000 --- a/src/components/HistorySidebar/SearchInput.tsx +++ /dev/null @@ -1,54 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import type { InputSize } from '@/components/ui/input'; -import { Input } from '@/components/ui/input'; -import { Search } from 'lucide-react'; -import type { ReactNode } from 'react'; -import { useTranslation } from 'react-i18next'; - -interface SearchInputProps { - value: string; - onChange: (e: React.ChangeEvent) => void; - placeholder?: string; - size?: InputSize; - leadingIcon?: ReactNode; -} - -export default function SearchInput({ - value, - onChange, - placeholder, - size = 'sm', - leadingIcon, -}: SearchInputProps) { - const { t } = useTranslation(); - - return ( -
    - - ) - } - /> -
    - ); -} diff --git a/src/components/HistorySidebar/index.tsx b/src/components/HistorySidebar/index.tsx deleted file mode 100644 index 0e915129..00000000 --- a/src/components/HistorySidebar/index.tsx +++ /dev/null @@ -1,754 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import { proxyFetchDelete } from '@/api/http'; -import tokenDarkIcon from '@/assets/custom/token-dark.svg'; -import tokenLightIcon from '@/assets/custom/token-light.svg'; -import { formatTokenCount } from '@/components/ChatBox/MessageItem/TokenUtils'; -import { Button } from '@/components/ui/button'; -import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; -import { useHost } from '@/host'; -import { - buildTaskQuestionsById, - computeProjectFreshnessAnchor, - loadProjectFromHistory, -} from '@/lib'; -import { share } from '@/lib/share'; -import { fetchGroupedHistoryTasks } from '@/service/historyApi'; -import { getAuthStore, useAuthStore } from '@/store/authStore'; -import { useSidebarStore } from '@/store/sidebarStore'; -import { useSpaceStore } from '@/store/spaceStore'; -import { ChatTaskStatus } from '@/types/constants'; -import { HistoryTask, ProjectGroup } from '@/types/history'; -import { AnimatePresence, motion } from 'framer-motion'; -import { - Ellipsis, - FolderCheck, - FolderClock, - ListChecks, - Plus, - Share, - Trash2, - Zap, -} from 'lucide-react'; -import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; -import AlertDialog from '../ui/alertDialog'; -import { - Popover, - PopoverClose, - PopoverContent, - PopoverTrigger, -} from '../ui/popover'; -import { Tag } from '../ui/tag'; -import { TooltipSimple } from '../ui/tooltip'; -import SearchInput from './SearchInput'; - -const compactCountFormatter = new Intl.NumberFormat('en', { - notation: 'compact', - maximumFractionDigits: 1, - minimumFractionDigits: 0, -}); - -const formatCompactCount = (value?: number) => - compactCountFormatter.format(value || 0); - -const toFiniteNumber = (value: unknown): number | null => { - if (typeof value === 'number') { - return Number.isFinite(value) ? value : null; - } - if (typeof value === 'string') { - const parsed = Number(value.replace(/,/g, '').trim()); - return Number.isFinite(parsed) ? parsed : null; - } - return null; -}; - -const resolveProjectTokenCount = ( - project: Pick -): number => { - const direct = toFiniteNumber(project.total_tokens as unknown); - if (direct !== null) { - return direct; - } - - return (project.tasks || []).reduce( - (sum, task) => - sum + (toFiniteNumber((task as { tokens?: unknown }).tokens) ?? 0), - 0 - ); -}; - -export default function HistorySidebar() { - const { t } = useTranslation(); - const host = useHost(); - const ipcRenderer = host?.ipcRenderer; - const { appearance } = useAuthStore(); - const tokenIcon = appearance === 'dark' ? tokenDarkIcon : tokenLightIcon; - const { isOpen, close } = useSidebarStore(); - const navigate = useNavigate(); - //Get Chatstore for the active project's task - const { chatStore, projectStore } = useChatStoreAdapter(); - const [searchValue, setSearchValue] = useState(''); - const [_historyOpen, setHistoryOpen] = useState(true); - const [historyTasks, setHistoryTasks] = useState([]); - const [deleteModalOpen, setDeleteModalOpen] = useState(false); - const [anchorStyle, setAnchorStyle] = useState<{ - left: number; - top: number; - } | null>(null); - const panelRef = useRef(null); - const [currentProjectId, setCurrentProjectId] = useState(''); - const activeSpaceId = useSpaceStore((s) => s.activeSpaceId); - - useEffect(() => { - if (!chatStore) return; - fetchGroupedHistoryTasks(setHistoryTasks, { spaceId: activeSpaceId }); - }, [chatStore, chatStore?.updateCount, activeSpaceId]); - - // Group ongoing tasks by project - const ongoingProjects = useMemo(() => { - if (!chatStore) return []; - const projectMap = new Map(); - - // Iterate through all projects - const allProjects = projectStore.getAllProjects(activeSpaceId ?? undefined); - allProjects.forEach((project) => { - // Get all chat stores for this project - const chatStores = projectStore.getAllChatStores(project.id); - - let hasOngoingTasks = false; - let totalTokens = 0; - let taskCount = 0; - let lastPrompt = ''; - - // Check all chat stores for ongoing tasks - chatStores.forEach(({ chatStore: cs }) => { - const csState = cs.getState(); - Object.keys(csState.tasks || {}).forEach((taskId) => { - const task = csState.tasks[taskId]; - // Only include ongoing tasks - if (task.status !== ChatTaskStatus.FINISHED && !task.type) { - hasOngoingTasks = true; - taskCount++; - if (task.tokens) { - totalTokens += task.tokens; - } - if (!lastPrompt && task.messages?.[0]?.content) { - lastPrompt = task.messages[0].content; - } - } - }); - }); - - // Only add project if it has ongoing tasks - if (hasOngoingTasks) { - projectMap.set(project.id, { - project_id: project.id, - project_name: project.name, - tasks: [], - task_count: taskCount, - total_tokens: totalTokens, - total_triggers: - historyTasks.find((item) => item.project_id === project.id) - ?.total_triggers || 0, - space_id: project.spaceId, - last_prompt: lastPrompt, - isOngoing: true, - }); - } - }); - - return Array.from(projectMap.values()); - }, [projectStore, chatStore, historyTasks, activeSpaceId]); - - const handleSearch = (e: React.ChangeEvent) => { - if (e.target.value) { - setHistoryOpen(true); - } - setSearchValue(e.target.value); - }; - - const createChat = () => { - close(); - //Create a new project - //Handles refocusing id & non duplicate logic internally - projectStore.createProject('new project'); - navigate('/'); - }; - - const handleLoadProject = async ( - projectId: string, - question: string, - historyId: string - ) => { - close(); - const project = historyTasks.find((p) => p.project_id === projectId); - const taskIdsList = project?.tasks.map( - (task: HistoryTask) => task.task_id - ) || [projectId]; - - // If no tasks to replay, create an empty project - if (!taskIdsList || taskIdsList.length === 0) { - projectStore.createProject( - project?.project_name || 'Project', - 'Project with triggers but no tasks', - projectId - ); - navigate('/'); - return; - } - - await loadProjectFromHistory( - projectStore, - navigate, - projectId, - question, - historyId, - taskIdsList, - project?.project_name, - project?.space_id, - buildTaskQuestionsById(project?.tasks), - computeProjectFreshnessAnchor(project) - ); - }; - - const handleDelete = (id: string) => { - console.log('Delete task:', id); - setCurrentProjectId(id); - setDeleteModalOpen(true); - }; - - // Deletes whole Project - const confirmDelete = async () => { - await deleteWholeProject(currentProjectId); - setHistoryTasks((list) => - list.filter((item) => item.project_id !== currentProjectId) - ); - setCurrentProjectId(''); - setDeleteModalOpen(false); - }; - - const _deleteHistoryTask = async ( - project: ProjectGroup, - historyId: string - ) => { - try { - const res = await proxyFetchDelete(`/api/v1/chat/history/${historyId}`); - console.log(res); - // also delete local files for this task if available (via Electron IPC) - const { email } = getAuthStore(); - const history = project.tasks.find( - (item: HistoryTask) => String(item.id) === historyId - ); - if (history?.task_id && ipcRenderer) { - try { - //TODO(file): rename endpoint to use project_id - //TODO(history): make sure to sync to projectId when updating endpoint - await ipcRenderer.invoke( - 'delete-task-files', - email, - history.task_id, - history.project_id ?? undefined - ); - } catch (error) { - console.warn('Local file cleanup failed:', error); - } - } - } catch (error) { - console.error('Failed to delete history task:', error); - } - }; - - // Deletes whole project by using the tasks from historyTasks state - const deleteWholeProject = async (projectId: string) => { - try { - // Find the project in our existing data - const targetProject = historyTasks.find( - (project) => project.project_id === projectId - ); - - if (targetProject && targetProject.tasks) { - console.log( - `Found project ${projectId} with ${targetProject.tasks.length} tasks to delete` - ); - - // Delete each task one by one - for (const history of targetProject.tasks) { - console.log( - `Deleting task: ${history.task_id} (history ID: ${history.id})` - ); - try { - const deleteRes = await proxyFetchDelete( - `/api/v1/chat/history/${history.id}` - ); - console.log( - `Successfully deleted task ${history.task_id}:`, - deleteRes - ); - - // Also delete local files for this task if available (via Electron IPC) - const { email } = getAuthStore(); - if (history.task_id && ipcRenderer) { - try { - await ipcRenderer.invoke( - 'delete-task-files', - email, - history.task_id, - history.project_id ?? undefined - ); - console.log( - `Successfully cleaned up local files for task ${history.task_id}` - ); - } catch (error) { - console.warn( - `Local file cleanup failed for task ${history.task_id}:`, - error - ); - } - } - } catch (error) { - console.error(`Failed to delete task ${history.task_id}:`, error); - } - } - - projectStore.removeProject(projectId); - console.log(`Completed deletion of project ${projectId}`); - } else { - console.warn(`Project ${projectId} not found or has no tasks`); - } - } catch (error) { - console.error('Failed to delete whole project:', error); - } - }; - - const handleShare = async (taskId: string) => { - close(); - share(taskId); - }; - - const handleSetActive = ( - projectId: string, - question: string, - historyId: string - ) => { - const project = projectStore.getProjectById(projectId); - //If project exists - if (project) { - // if there is record, show result - projectStore.setHistoryId(projectId, historyId); - projectStore.setActiveProject(projectId); - navigate(`/`); - close(); - } else { - // if there is no record, load final state (no replay animation) - handleLoadProject(projectId, question, historyId); - } - }; - - useLayoutEffect(() => { - const PANEL_WIDTH = 360; - const GAP = 8; - const MARGIN = 8; - - const updateAnchor = () => { - const sidebarTitleEl = document.getElementById( - 'sidebar-active-task-title-btn' - ); - const topBarTitleEl = document.getElementById('active-task-title-btn'); - - let anchorEl: HTMLElement | null = null; - if (sidebarTitleEl) { - const r = sidebarTitleEl.getBoundingClientRect(); - if (r.width > 0 && r.height > 0) { - anchorEl = sidebarTitleEl; - } - } - if (!anchorEl && topBarTitleEl) { - anchorEl = topBarTitleEl; - } - - if (anchorEl) { - const rect = anchorEl.getBoundingClientRect(); - let left = rect.left; - if (left + PANEL_WIDTH > window.innerWidth - MARGIN) { - left = window.innerWidth - MARGIN - PANEL_WIDTH; - } - if (left < MARGIN) { - left = MARGIN; - } - const top = rect.bottom + GAP; - setAnchorStyle({ left, top }); - } else { - setAnchorStyle(null); - } - }; - - if (isOpen) { - updateAnchor(); - window.addEventListener('resize', updateAnchor); - } - - return () => { - window.removeEventListener('resize', updateAnchor); - }; - }, [isOpen]); - - if (!chatStore) { - return
    Loading...
    ; - } - - return ( - - {isOpen && anchorStyle && ( - <> - {/* alert dialog */} - setDeleteModalOpen(false)} - onConfirm={confirmDelete} - title={t('layout.delete-task')} - message={t('layout.are-you-sure-you-want-to-delete')} - confirmText={t('layout.delete')} - cancelText={t('layout.cancel')} - /> - {/* background cover */} - - {/* History panel below project title (sidebar when expanded, else TopBar) */} - -
    - {/* Search */} - - -
    -
    -
    - {/* Ongoing Projects */} - {ongoingProjects - .filter( - (project) => - project.last_prompt - ?.toLowerCase() - .includes(searchValue.toLowerCase()) || - project.project_name - ?.toLowerCase() - .includes(searchValue.toLowerCase()) - ) - .map((project) => ( -
    { - projectStore.setActiveProject(project.project_id); - navigate(`/`); - close(); - }} - className="duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] relative flex w-full max-w-full cursor-pointer items-center justify-between gap-sm rounded-xl border border-solid border-ds-border-neutral-subtle-default bg-ds-bg-neutral-default-default px-4 py-3 shadow-history-item transition-colors hover:bg-ds-bg-neutral-default-hover" - > - - -
    - - {project.project_name || t('layout.new-project')} -
    - } - > - - {project.project_name || t('layout.new-project')} - - -
    - -
    - - - - - {formatTokenCount( - resolveProjectTokenCount(project) - )} - - - - - - - - - {formatCompactCount(project.task_count)} - - - - - - - - - {formatCompactCount(project.total_triggers)} - - - -
    - - - - - - -
    - - - - - - - -
    -
    -
    -
    - ))} - - {/* History Projects */} - {historyTasks - .filter( - (project) => - project.last_prompt - ?.toLowerCase() - .includes(searchValue.toLowerCase()) || - project.project_name - ?.toLowerCase() - .includes(searchValue.toLowerCase()) - ) - .map((project) => ( -
    { - handleSetActive( - project.project_id, - project.last_prompt, - project.project_id - ); - }} - key={project.project_id} - className="duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] relative flex w-full max-w-full cursor-pointer items-center justify-between gap-sm rounded-xl border border-solid border-ds-border-neutral-subtle-default bg-ds-bg-neutral-default-default px-4 py-3 shadow-history-item transition-colors hover:bg-ds-bg-neutral-default-hover" - > - - -
    - - {project.last_prompt || - project.project_name || - t('layout.new-project')} -
    - } - > - - {project.last_prompt || - project.project_name || - t('layout.new-project')} - - -
    - -
    - - - - - {formatTokenCount( - resolveProjectTokenCount(project) - )} - - - - - - - - - {formatCompactCount(project.task_count)} - - - - - - - - - {formatCompactCount(project.total_triggers)} - - - -
    - - - - - - -
    - - - - - - - -
    -
    -
    -
    - ))} -
    -
    - - - )} - - ); -} diff --git a/src/pages/Home/Projects.tsx b/src/components/Home/Projects.tsx similarity index 95% rename from src/pages/Home/Projects.tsx rename to src/components/Home/Projects.tsx index c9192d6c..f84b6963 100644 --- a/src/pages/Home/Projects.tsx +++ b/src/components/Home/Projects.tsx @@ -158,7 +158,7 @@ export default function Projects() { if (projectsLoading) { return ( -
    +
    {t('layout.loading')}
    @@ -167,17 +167,17 @@ export default function Projects() { } return ( -
    -
    +
    +
    {projects.length === 0 ? ( -
    +
    {t('dashboard.no-projects-found')}
    ) : filteredProjects.length === 0 ? ( -
    +
    {t('layout.search-no-results')}
    diff --git a/src/pages/Home/Spaces.tsx b/src/components/Home/Spaces.tsx similarity index 100% rename from src/pages/Home/Spaces.tsx rename to src/components/Home/Spaces.tsx diff --git a/src/pages/Home/Tasks.tsx b/src/components/Home/Tasks.tsx similarity index 94% rename from src/pages/Home/Tasks.tsx rename to src/components/Home/Tasks.tsx index 029d9abf..55b16607 100644 --- a/src/pages/Home/Tasks.tsx +++ b/src/components/Home/Tasks.tsx @@ -155,7 +155,7 @@ export default function Tasks() { if (projectsLoading) { return ( -
    +
    {t('layout.loading')}
    @@ -164,17 +164,17 @@ export default function Tasks() { } return ( -
    -
    +
    +
    {tasks.length === 0 ? ( -
    +
    {t('dashboard.no-tasks-found') || t('layout.total-tasks')}
    ) : filteredTasks.length === 0 ? ( -
    +
    {t('layout.search-no-results')}
    diff --git a/src/pages/Home/Triggers.tsx b/src/components/Home/Triggers.tsx similarity index 95% rename from src/pages/Home/Triggers.tsx rename to src/components/Home/Triggers.tsx index 5262a20b..650bdfee 100644 --- a/src/pages/Home/Triggers.tsx +++ b/src/components/Home/Triggers.tsx @@ -183,7 +183,7 @@ export default function Triggers() { if (triggersLoading) { return ( -
    +
    {t('layout.loading')}
    @@ -192,17 +192,17 @@ export default function Triggers() { } return ( -
    -
    +
    +
    {triggers.length === 0 ? ( -
    +
    {t('triggers.no-triggers') || t('layout.triggers')}
    ) : filteredTriggers.length === 0 ? ( -
    +
    {t('layout.search-no-results')}
    diff --git a/src/pages/Home/components/HomeHubBoard.tsx b/src/components/Home/components/HomeHubBoard.tsx similarity index 83% rename from src/pages/Home/components/HomeHubBoard.tsx rename to src/components/Home/components/HomeHubBoard.tsx index ae4b0529..6860c755 100644 --- a/src/pages/Home/components/HomeHubBoard.tsx +++ b/src/components/Home/components/HomeHubBoard.tsx @@ -58,7 +58,7 @@ export default function HomeHubBoard({ return (
    @@ -70,13 +70,13 @@ export default function HomeHubBoard({
    @@ -92,7 +92,7 @@ export default function HomeHubBoard({ @@ -100,11 +100,11 @@ export default function HomeHubBoard({
    -
    +
    {items.length > 0 ? ( items ) : ( -
    +
    {t('layout.home-board-column-empty')}
    )} diff --git a/src/pages/Home/components/HomeHubBoardCard.tsx b/src/components/Home/components/HomeHubBoardCard.tsx similarity index 100% rename from src/pages/Home/components/HomeHubBoardCard.tsx rename to src/components/Home/components/HomeHubBoardCard.tsx diff --git a/src/pages/Home/components/HomeHubCard.tsx b/src/components/Home/components/HomeHubCard.tsx similarity index 99% rename from src/pages/Home/components/HomeHubCard.tsx rename to src/components/Home/components/HomeHubCard.tsx index 114f207e..40b554ac 100644 --- a/src/pages/Home/components/HomeHubCard.tsx +++ b/src/components/Home/components/HomeHubCard.tsx @@ -325,7 +325,7 @@ function ProjectItemContent({ className="relative" > {loading ? ( -
    +
    ) : null} @@ -417,7 +417,7 @@ function TaskItemContent({ className={loading ? 'relative' : undefined} > {loading ? ( -
    +
    ) : null} diff --git a/src/pages/Home/components/HomeHubGrid.tsx b/src/components/Home/components/HomeHubGrid.tsx similarity index 100% rename from src/pages/Home/components/HomeHubGrid.tsx rename to src/components/Home/components/HomeHubGrid.tsx diff --git a/src/pages/Home/components/HomeHubItemShared.tsx b/src/components/Home/components/HomeHubItemShared.tsx similarity index 100% rename from src/pages/Home/components/HomeHubItemShared.tsx rename to src/components/Home/components/HomeHubItemShared.tsx diff --git a/src/pages/Home/components/HomeHubListItem.tsx b/src/components/Home/components/HomeHubListItem.tsx similarity index 100% rename from src/pages/Home/components/HomeHubListItem.tsx rename to src/components/Home/components/HomeHubListItem.tsx diff --git a/src/pages/Home/components/HomeHubListTable.tsx b/src/components/Home/components/HomeHubListTable.tsx similarity index 91% rename from src/pages/Home/components/HomeHubListTable.tsx rename to src/components/Home/components/HomeHubListTable.tsx index a9dceba0..754b2cb8 100644 --- a/src/pages/Home/components/HomeHubListTable.tsx +++ b/src/components/Home/components/HomeHubListTable.tsx @@ -70,13 +70,13 @@ export default function HomeHubListTable({ const gridClass = HOME_HUB_LIST_GRID_CLASS[kind]; return ( -
    -
    +
    +
    {columns.map((column) => ( @@ -84,7 +84,7 @@ export default function HomeHubListTable({ ))}
    -
    {children}
    +
    {children}
    ); } diff --git a/src/pages/Home/components/HomeHubToolbar.tsx b/src/components/Home/components/HomeHubToolbar.tsx similarity index 98% rename from src/pages/Home/components/HomeHubToolbar.tsx rename to src/components/Home/components/HomeHubToolbar.tsx index 476926a1..fd9dcb59 100644 --- a/src/pages/Home/components/HomeHubToolbar.tsx +++ b/src/components/Home/components/HomeHubToolbar.tsx @@ -204,7 +204,7 @@ export default function HomeHubToolbar({ return (
    diff --git a/src/pages/Home/context.tsx b/src/components/Home/context.tsx similarity index 100% rename from src/pages/Home/context.tsx rename to src/components/Home/context.tsx diff --git a/src/pages/Home/hooks/useHomeHubCounts.ts b/src/components/Home/hooks/useHomeHubCounts.ts similarity index 100% rename from src/pages/Home/hooks/useHomeHubCounts.ts rename to src/components/Home/hooks/useHomeHubCounts.ts diff --git a/src/pages/Home/hooks/useHomeHubNavigation.ts b/src/components/Home/hooks/useHomeHubNavigation.ts similarity index 100% rename from src/pages/Home/hooks/useHomeHubNavigation.ts rename to src/components/Home/hooks/useHomeHubNavigation.ts diff --git a/src/pages/Home/hooks/useHomeHubProjects.ts b/src/components/Home/hooks/useHomeHubProjects.ts similarity index 100% rename from src/pages/Home/hooks/useHomeHubProjects.ts rename to src/components/Home/hooks/useHomeHubProjects.ts diff --git a/src/pages/Home/hooks/useHomeHubTriggers.ts b/src/components/Home/hooks/useHomeHubTriggers.ts similarity index 100% rename from src/pages/Home/hooks/useHomeHubTriggers.ts rename to src/components/Home/hooks/useHomeHubTriggers.ts diff --git a/src/pages/Home/hooks/useSpaceLabel.ts b/src/components/Home/hooks/useSpaceLabel.ts similarity index 100% rename from src/pages/Home/hooks/useSpaceLabel.ts rename to src/components/Home/hooks/useSpaceLabel.ts diff --git a/src/pages/Home/index.tsx b/src/components/Home/index.tsx similarity index 98% rename from src/pages/Home/index.tsx rename to src/components/Home/index.tsx index 5b0e2d34..7845ef9c 100644 --- a/src/pages/Home/index.tsx +++ b/src/components/Home/index.tsx @@ -122,7 +122,7 @@ export default function HomeHub() { const handleTabChange = (tabId: string) => { if (!HOME_SECTIONS.includes(tabId as HomeSection)) return; - navigate(`?tab=home§ion=${tabId}`, { replace: true }); + navigate(`?section=${tabId}`, { replace: true }); }; const handleDelete = (id: string, callback?: () => void) => { @@ -265,14 +265,14 @@ export default function HomeHub() { cancelText={t('layout.cancel')} /> -
    +
    -
    +
    {activeTab === 'spaces' && } {activeTab === 'projects' && } {activeTab === 'tasks' && } diff --git a/src/pages/Home/utils.ts b/src/components/Home/utils.ts similarity index 100% rename from src/pages/Home/utils.ts rename to src/components/Home/utils.ts diff --git a/src/pages/Home/utils/boardStatus.ts b/src/components/Home/utils/boardStatus.ts similarity index 100% rename from src/pages/Home/utils/boardStatus.ts rename to src/components/Home/utils/boardStatus.ts diff --git a/src/components/InstallStep/Carousel.tsx b/src/components/InstallStep/Carousel.tsx deleted file mode 100644 index 72ce0216..00000000 --- a/src/components/InstallStep/Carousel.tsx +++ /dev/null @@ -1,246 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import addWorkerVideo from '@/assets/add_worker.mp4'; -import dynamicWorkforceVideo from '@/assets/dynamic_workforce.mp4'; -import localModelVideo from '@/assets/local_model.mp4'; -import { Button } from '@/components/ui/button'; -import { CardContent } from '@/components/ui/card'; -import { - Carousel, - CarouselContent, - CarouselItem, -} from '@/components/ui/carousel'; -import { useAuthStore } from '@/store/authStore'; -import { Pause, Play } from 'lucide-react'; -import React, { useEffect, useRef, useState } from 'react'; - -export const CarouselStep: React.FC = () => { - const { setInitState: _setInitState } = useAuthStore(); - const [currentSlide, setCurrentSlide] = useState(0); - const [_isHovered, setIsHovered] = useState(false); - const [api, setApi] = useState(null); - const [isDismissed, _setIsDismissed] = useState(false); - const videoRefs = useRef<(HTMLVideoElement | null)[]>([]); - const [isPaused, setIsPaused] = useState(false); - const videoEndTimeoutRef = useRef(null); - - // listen to carousel change - useEffect(() => { - if (!api) return; - - const onSelect = () => { - // Clear any pending video end timeout when slide changes manually - if (videoEndTimeoutRef.current) { - clearTimeout(videoEndTimeoutRef.current); - videoEndTimeoutRef.current = null; - } - setCurrentSlide(api.selectedScrollSnap()); - }; - - api.on('select', onSelect); - return () => { - api.off('select', onSelect); - }; - }, [api]); - - // Cleanup timeout on unmount - useEffect(() => { - return () => { - if (videoEndTimeoutRef.current) { - clearTimeout(videoEndTimeoutRef.current); - } - }; - }, []); - - // click indicator to jump to corresponding slide - const scrollTo = (index: number) => { - if (api) { - api.scrollTo(index); - } - }; - - // mouse hover control - const handleMouseEnter = () => { - setIsHovered(true); - }; - - const handleMouseLeave = () => { - setIsHovered(false); - }; - - const handleIndicatorHover = (index: number) => { - scrollTo(index); - }; - - const handleTogglePause = () => { - const newPausedState = !isPaused; - setIsPaused(newPausedState); - - const currentVideo = videoRefs.current[currentSlide]; - if (currentVideo) { - if (newPausedState) { - currentVideo.pause(); - } else { - currentVideo.play().catch((err) => { - console.warn('video.play() error:', err); - }); - } - } - }; - - const carouselItems = [ - { - title: 'Dynamic Workforce break it down, get task done', - video: dynamicWorkforceVideo, - }, - { - title: 'Add worker with pluggable MCP', - video: addWorkerVideo, - }, - { - title: 'Private and secure with local model settings', - video: localModelVideo, - }, - ]; - - useEffect(() => { - if (!api) return; - - const video = videoRefs.current[currentSlide]; - if (video) { - const tryPlay = () => { - video.currentTime = 0; - if (!isPaused) { - video.play().catch((err) => { - console.warn('video.play() error:', err); - }); - } - }; - - if (video.readyState >= 1) { - // metadata already loaded - tryPlay(); - } else { - // wait for metadata to load before playing - const handler = () => { - tryPlay(); - video.removeEventListener('loadedmetadata', handler); - }; - video.addEventListener('loadedmetadata', handler); - } - } - }, [currentSlide, api, isPaused]); - - // If carousel is dismissed, don't show anything - // The actual transition to 'done' will be handled by useInstallationSetup - // when both installation and backend are ready - if (isDismissed) { - return null; - } - - return ( -
    -
    -
    - {carouselItems[currentSlide].title} -
    - - - - {carouselItems.map((_, index) => ( - -
    - -
    -
    -
    -
    -
    - ))} -
    -
    -
    -
    -
    - {carouselItems.map((item, index) => ( -
    handleIndicatorHover(index)} - className={`duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] h-1 w-32 cursor-pointer rounded-full transition-colors ${ - index === currentSlide - ? 'bg-ds-bg-neutral-default-default' - : 'bg-ds-bg-neutral-subtle-default hover:bg-ds-bg-neutral-default-default' - }`} - >
    - ))} -
    - -
    -
    - ); -}; diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 7c1283c4..e4382286 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -23,8 +23,8 @@ import { useInstallationUI } from '@/store/installationStore'; import { useEffect, useState } from 'react'; import { Outlet } from 'react-router-dom'; import CloseNoticeDialog from '../Dialog/CloseNotice'; -import HistorySidebar from '../HistorySidebar'; import InstallationErrorDialog from '../InstallStep/InstallationErrorDialog/InstallationErrorDialog'; +import SettingsDialog from '../Settings'; const Layout = () => { const host = useHost(); @@ -93,7 +93,7 @@ const Layout = () => { const shouldShowMainContent = !actualShouldShowInstallScreen; return ( -
    +
    { >
    -
    + +
    {/* Installation screen */} {actualShouldShowInstallScreen && } {/* Main app content */} - {shouldShowMainContent && ( - <> - - - - )} + {shouldShowMainContent && } {(backendError || (error && installationState === 'error')) && ( ; - -function asCssVarMap(map: Record<`--${string}`, string>): CssVarMap { - return map as CssVarMap; -} - -export function mergeLayoutAliasStyles( - base: CssVarMap, - style?: React.CSSProperties -): React.CSSProperties { - return style ? ({ ...base, ...style } as React.CSSProperties) : base; -} - -// Shared layout-level aliases for TopBar, HistorySidebar, and ProjectPageSidebar. -export const productLayoutTokenAliases = asCssVarMap({ - '--border-secondary': 'var(--ds-border-neutral-default-default)', - '--border-disabled': 'var(--ds-border-neutral-subtle-default)', - - '--text-heading': 'var(--ds-text-neutral-default-default)', - '--text-body': 'var(--ds-text-neutral-default-default)', - '--text-label': 'var(--ds-text-neutral-muted-default)', - '--text-primary': 'var(--ds-text-neutral-default-default)', - '--text-secondary': 'var(--ds-text-neutral-muted-default)', - '--text-information': 'var(--ds-text-status-splitting-strong-default)', - '--text-error': 'var(--ds-text-status-error-strong-default)', - - '--icon-primary': 'var(--ds-icon-neutral-default-default)', - '--icon-secondary': 'var(--ds-icon-neutral-muted-default)', - '--icon-information': 'var(--ds-icon-status-splitting-default-default)', - '--icon-success': 'var(--ds-icon-status-completed-default-default)', - '--icon-warning': 'var(--ds-icon-status-pending-default-default)', - '--icon-caution': 'var(--ds-icon-status-error-default-default)', - - '--project-surface': 'var(--ds-bg-neutral-default-default)', - '--project-surface-hover': 'var(--ds-bg-neutral-default-hover)', - - '--dropdown-bg': 'var(--ds-bg-neutral-strong-default)', - '--dropdown-border': 'var(--ds-border-neutral-default-default)', - - '--button-transparent-fill-default': 'transparent', - '--button-transparent-fill-hover': 'var(--ds-bg-neutral-default-hover)', - '--button-transparent-fill-active': 'var(--ds-bg-neutral-default-active)', - '--button-transparent-text-default': 'var(--ds-text-neutral-default-default)', -}); diff --git a/src/components/Notification/index.tsx b/src/components/Notification/index.tsx deleted file mode 100644 index 5c9e868c..00000000 --- a/src/components/Notification/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import { useEffect } from 'react'; -import { createPortal } from 'react-dom'; -import { useTranslation } from 'react-i18next'; - -export type NotificationPanelProps = { - open: boolean; - onOpenChange: (open: boolean) => void; -}; - -export default function NotificationPanel({ - open, - onOpenChange, -}: NotificationPanelProps) { - const { t } = useTranslation(); - - useEffect(() => { - if (!open) return; - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') onOpenChange(false); - }; - window.addEventListener('keydown', onKeyDown); - return () => window.removeEventListener('keydown', onKeyDown); - }, [open, onOpenChange]); - - if (!open || typeof document === 'undefined') { - return null; - } - - return createPortal( - <> - - -
    -
    -
    - ); -} diff --git a/src/components/Session/HeaderBox/ChatTimeline.tsx b/src/components/Session/HeaderBox/ChatTimeline.tsx deleted file mode 100644 index a0402cc9..00000000 --- a/src/components/Session/HeaderBox/ChatTimeline.tsx +++ /dev/null @@ -1,253 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import { - getTaskListShelfTone, - isTaskListRowFailureState, -} from '@/lib/taskLifecycleUi'; -import { cn } from '@/lib/utils'; -import type { ChatStore } from '@/store/chatStore'; -import { ChatTaskStatus } from '@/types/constants'; -import { useEffect, useLayoutEffect, useRef, useState } from 'react'; - -const TASK_LIST_ROW_BG = { - information: 'bg-ds-bg-splitting-subtle-default hover:brightness-[0.98]', - success: 'bg-ds-bg-completed-subtle-default hover:brightness-[0.98]', - caution: 'bg-ds-bg-error-subtle-default hover:brightness-[0.98]', - idle: 'bg-transparent hover:bg-ds-bg-neutral-default-hover', -} as const; - -/** Brief success highlight after a run completes; then idle (transparent). */ -const TASK_LIST_SUCCESS_HIGHLIGHT_MS = 1800; - -/** Horizontal drift speed for task query hover (~6px/s, capped) — readable marquee, not a snap. */ -const TASK_QUERY_SCROLL_PX_PER_SEC = 16; -const TASK_QUERY_SCROLL_MIN_MS = 10_000; -const TASK_QUERY_SCROLL_MAX_MS = 90_000; - -function taskQueryScrollDurationMs(scrollPx: number): number { - if (scrollPx <= 0) return 300; - const proportional = (scrollPx / TASK_QUERY_SCROLL_PX_PER_SEC) * 1000; - return Math.min( - TASK_QUERY_SCROLL_MAX_MS, - Math.max(TASK_QUERY_SCROLL_MIN_MS, Math.round(proportional)) - ); -} - -function taskUserQueryLabel(task: ChatStore['tasks'][string]): string { - const firstUser = task.messages.find((m) => m.role === 'user'); - const text = firstUser?.content?.trim() ?? ''; - return text || '…'; -} - -function TaskQueryScrollLabel({ - queryLabel, - rowHovered, -}: { - queryLabel: string; - rowHovered: boolean; -}) { - const outerRef = useRef(null); - const innerRef = useRef(null); - const [scrollPx, setScrollPx] = useState(0); - - useLayoutEffect(() => { - const outer = outerRef.current; - const inner = innerRef.current; - if (!outer || !inner) return; - - const measure = () => { - setScrollPx(Math.max(0, inner.scrollWidth - outer.clientWidth)); - }; - - measure(); - const ro = new ResizeObserver(measure); - ro.observe(outer); - return () => ro.disconnect(); - }, [queryLabel]); - - const slide = rowHovered && scrollPx > 0; - const slideMs = taskQueryScrollDurationMs(scrollPx); - - return ( -
    - - {queryLabel} - -
    - ); -} - -function ChatTimelineRow({ - task, - firstUserMessageId, - active, - setScrollToQueryId, -}: { - task: ChatStore['tasks'][string]; - firstUserMessageId: string | null; - active: boolean; - setScrollToQueryId: (id: string) => void; -}) { - const [rowHovered, setRowHovered] = useState(false); - const [successHighlight, setSuccessHighlight] = useState(false); - const mountedRef = useRef(false); - const prevStatusRef = useRef(task.status); - - const queryLabel = taskUserQueryLabel(task); - const shelfTone = getTaskListShelfTone(task); - const failed = isTaskListRowFailureState(task); - const activeWork = shelfTone === 'splitting' || shelfTone === 'running'; - const finishedSuccess = - !failed && task.status === ChatTaskStatus.FINISHED && task.type !== ''; - - useEffect(() => { - if (!mountedRef.current) { - mountedRef.current = true; - prevStatusRef.current = task.status; - return; - } - const prev = prevStatusRef.current; - prevStatusRef.current = task.status; - - if ( - finishedSuccess && - prev !== ChatTaskStatus.FINISHED && - task.status === ChatTaskStatus.FINISHED - ) { - setSuccessHighlight(true); - const id = window.setTimeout( - () => setSuccessHighlight(false), - TASK_LIST_SUCCESS_HIGHLIGHT_MS - ); - return () => clearTimeout(id); - } - }, [task.status, finishedSuccess]); - - const rowBg = failed - ? TASK_LIST_ROW_BG.caution - : successHighlight && finishedSuccess - ? TASK_LIST_ROW_BG.success - : activeWork - ? TASK_LIST_ROW_BG.information - : TASK_LIST_ROW_BG.idle; - - return ( - - ); -} - -/** Initial fold state for the task timeline rail (session chrome `useState` should match). */ -export const CHAT_TIMELINE_DEFAULT_COLLAPSED = true; - -/** Below this chat column width, the timeline uses a header dropdown instead of the left rail. */ -export const CHAT_TIMELINE_BREAKPOINT_PX = 600; - -export type ChatTimelineEntry = { - chatId: string; - taskId: string; - task: ChatStore['tasks'][string]; - firstUserMessageId: string | null; -}; - -export interface ChatTimelineProps { - collapsed: boolean; - entries: ChatTimelineEntry[]; - activeTaskId: string | null | undefined; - setScrollToQueryId: (id: string) => void; - title: string; - emptyLabel: string; -} - -export function ChatTimeline({ - collapsed, - entries, - activeTaskId, - setScrollToQueryId, - title, - emptyLabel, -}: ChatTimelineProps) { - return ( -
    -
    - - {title} - -
    -
    - {entries.length === 0 ? ( -

    - {emptyLabel} -

    - ) : ( -
    - {entries.map(({ chatId, taskId, task, firstUserMessageId }) => ( - - ))} -
    - )} -
    -
    - ); -} diff --git a/src/components/Session/SingleAgent/index.tsx b/src/components/Session/SingleAgent/index.tsx deleted file mode 100644 index 65ac8a97..00000000 --- a/src/components/Session/SingleAgent/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -export { SingleAgentSidePanel } from './SingleAgentSidePanel'; diff --git a/src/components/Session/Workforce/FoldedPanel/AgentDetailPane.tsx b/src/components/Session/Workforce/FoldedPanel/AgentDetailPane.tsx deleted file mode 100644 index fa80db3f..00000000 --- a/src/components/Session/Workforce/FoldedPanel/AgentDetailPane.tsx +++ /dev/null @@ -1,636 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import Folder from '@/components/Folder'; -import { TaskState, type TaskStateType } from '@/components/TaskState'; -import Terminal from '@/components/Terminal'; -import { TaskLogPanelContent } from '@/components/WorkFlow/TaskLogPanelContent'; -import { getAgentToolkitLabels } from '@/components/WorkFlow/agentToolkitLabels'; -import { agentMap, type WorkflowAgentType } from '@/components/WorkFlow/agents'; -import { HoverScrollText } from '@/components/ui/HoverScrollText'; -import ShinyText from '@/components/ui/ShinyText/ShinyText'; -import { Button } from '@/components/ui/button'; -import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; -import { useHost } from '@/host'; -import { getToolkitIcon } from '@/lib/toolkitIcons'; -import { cn } from '@/lib/utils'; -import { - AgentStatusValue, - ChatTaskStatus, - TaskStatus, -} from '@/types/constants'; -import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'; -import { - ChevronDown, - ChevronLeft, - Circle, - CircleCheckBig, - CircleSlash, - CircleSlash2, - LoaderCircle, - TriangleAlert, -} from 'lucide-react'; -import { useCallback, useEffect, useState } from 'react'; - -function getTaskIdDisplay(taskId: string): string { - const list = taskId.split('.'); - let idStr = ''; - list.shift(); - list.forEach((i: string, index: number) => { - idStr += Number(i) + (index === list.length - 1 ? '' : '.'); - }); - return idStr; -} - -const PANEL_EASE_OUT: [number, number, number, number] = [0.23, 1, 0.32, 1]; - -const foldedTaskLogPanelVariants = { - initial: { transform: 'translateX(100%)' }, - animate: { - transform: 'translateX(0)', - transition: { duration: 0.24, ease: PANEL_EASE_OUT }, - }, - exit: { - transform: 'translateX(100%)', - transition: { duration: 0.18, ease: PANEL_EASE_OUT }, - }, -}; - -const foldedTaskLogContentVariants = { - initial: { opacity: 0, transform: 'translateX(-12px)' }, - animate: { - opacity: 1, - transform: 'translateX(0)', - transition: { duration: 0.2, ease: PANEL_EASE_OUT }, - }, - exit: { - opacity: 0, - transform: 'translateX(-8px)', - transition: { duration: 0.16, ease: PANEL_EASE_OUT }, - }, -}; - -export function AgentDetailPane({ - agent, - onTakeManualFollowControl, -}: { - agent: Agent; - /** Fires when user clicks task filter tabs (take control from auto-follow in folded workforce). */ - onTakeManualFollowControl?: () => void; -}) { - const shouldReduceMotion = useReducedMotion(); - const host = useHost(); - const { chatStore } = useChatStoreAdapter(); - const [selectedState, setSelectedState] = useState('all'); - const [filterTasks, setFilterTasks] = useState>( - [] - ); - const [toolkitHovered, setToolkitHovered] = useState(false); - /** Task accordion: collapsed = 1 header row + 2-line clamped title; expanded = full title + extras. */ - const [expandedTaskIds, setExpandedTaskIds] = useState< - Record - >({}); - const [detailTask, setDetailTask] = useState(null); - - useEffect(() => { - const tasks = agent?.tasks || []; - if (selectedState === 'all') { - setFilterTasks(tasks); - return; - } - setFilterTasks( - tasks.filter((task) => { - switch (selectedState) { - case 'done': - return task.status === TaskStatus.COMPLETED && !task.reAssignTo; - case 'reassigned': - return !!task.reAssignTo; - case 'ongoing': - return ( - task.status !== TaskStatus.FAILED && - task.status !== TaskStatus.COMPLETED && - task.status !== TaskStatus.SKIPPED && - task.status !== TaskStatus.WAITING && - task.status !== TaskStatus.EMPTY && - !task.reAssignTo - ); - case 'pending': - return ( - (task.status === TaskStatus.SKIPPED || - task.status === TaskStatus.WAITING || - task.status === TaskStatus.EMPTY) && - !task.reAssignTo - ); - case 'failed': - return task.status === TaskStatus.FAILED; - default: - return false; - } - }) - ); - }, [selectedState, agent?.tasks]); - - useEffect(() => { - setSelectedState('all'); - setExpandedTaskIds({}); - setDetailTask(null); - }, [agent.agent_id]); - - const activeTaskId = chatStore?.activeTaskId as string | undefined; - - const toolkitLabels = getAgentToolkitLabels(agent); - const toolkitLine = toolkitLabels.join(' '); - const wfType = agent.type as WorkflowAgentType; - const preset = agentMap[wfType]; - - const browserImages = (agent.activeWebviewIds || []) - .filter((img) => img?.img) - .slice(0, 4); - const browserImageGridClass = - browserImages.length === 1 - ? 'grid-cols-1 grid-rows-1' - : browserImages.length === 2 - ? 'grid-cols-2 grid-rows-1' - : 'grid-cols-2 grid-rows-2'; - const browserPlaceholderCount = - browserImages.length >= 3 ? Math.max(0, 4 - browserImages.length) : 0; - - const terminalTasks = (agent?.tasks || []) - .filter((task) => task.terminal && task.terminal.length > 0) - .slice(0, 4); - const terminalGridClass = - terminalTasks.length === 1 - ? 'grid-cols-1 grid-rows-1' - : terminalTasks.length === 2 - ? 'grid-cols-2 grid-rows-1' - : 'grid-cols-2 grid-rows-2'; - const terminalPlaceholderCount = - terminalTasks.length >= 3 ? Math.max(0, 4 - terminalTasks.length) : 0; - - const taskLogPanelVariants = shouldReduceMotion - ? { - initial: { opacity: 0, transform: 'translateX(0)' }, - animate: { - opacity: 1, - transform: 'translateX(0)', - transition: { duration: 0.16, ease: PANEL_EASE_OUT }, - }, - exit: { - opacity: 0, - transform: 'translateX(0)', - transition: { duration: 0.14, ease: PANEL_EASE_OUT }, - }, - } - : foldedTaskLogPanelVariants; - const taskLogContentVariants = shouldReduceMotion - ? { - initial: { opacity: 0, transform: 'translateX(0)' }, - animate: { - opacity: 1, - transform: 'translateX(0)', - transition: { duration: 0.16, ease: PANEL_EASE_OUT }, - }, - exit: { - opacity: 0, - transform: 'translateX(0)', - transition: { duration: 0.14, ease: PANEL_EASE_OUT }, - }, - } - : foldedTaskLogContentVariants; - - const focusAgent = useCallback(() => { - if (!chatStore?.activeTaskId) return; - chatStore.setActiveWorkspace(chatStore.activeTaskId, agent.agent_id); - host?.electronAPI?.hideAllWebview?.(); - }, [chatStore, host, agent.agent_id]); - - if (!chatStore) { - return null; - } - - return ( -
    -
    -
    - {preset?.name ?? agent.name} -
    -
    setToolkitHovered(true)} - onPointerLeave={() => setToolkitHovered(false)} - > - -
    - {agent.tasks && agent.tasks.length > 0 && ( -
    -
    - - task.status === TaskStatus.COMPLETED && !task.reAssignTo - ).length || 0 - } - reAssignTo={ - agent.tasks?.filter((task) => task.reAssignTo)?.length || 0 - } - progress={ - agent.tasks?.filter( - (task) => - task.status !== TaskStatus.FAILED && - task.status !== TaskStatus.COMPLETED && - task.status !== TaskStatus.SKIPPED && - task.status !== TaskStatus.WAITING && - task.status !== TaskStatus.EMPTY && - !task.reAssignTo - ).length || 0 - } - skipped={ - agent.tasks?.filter( - (task) => - (task.status === TaskStatus.SKIPPED || - task.status === TaskStatus.WAITING || - task.status === TaskStatus.EMPTY) && - !task.reAssignTo - ).length || 0 - } - failed={ - agent.tasks?.filter( - (task) => task.status === TaskStatus.FAILED - ).length || 0 - } - selectedState={selectedState} - onStateChange={(state) => { - onTakeManualFollowControl?.(); - setSelectedState(state); - }} - clickable={true} - /> -
    -
    - )} -
    - - {/* Content: agent summary + task list; task log slides in from the right */} -
    -
    e.stopPropagation()} - > -
    - {browserImages.length > 0 && ( -
    - {browserImages.map((img, index) => ( -
    - {agent.type} -
    - ))} - {Array.from({ length: browserPlaceholderCount }).map( - (_, index) => ( -
    - ) - )} -
    - )} - {agent.type === 'document_agent' && - agent.tasks && - agent.tasks.length > 0 && ( -
    -
    - -
    -
    - )} - - {agent.type === 'developer_agent' && terminalTasks.length > 0 && ( -
    - {terminalTasks.map((task) => ( -
    -
    - -
    -
    - ))} - {Array.from({ length: terminalPlaceholderCount }).map( - (_, index) => ( -
    - ) - )} -
    - )} -
    - -
    - {agent.tasks && - filterTasks.map((task) => { - const lastActiveToolkit = task.toolkits - ?.filter( - (tool: { toolkitName?: string }) => - tool.toolkitName !== 'notice' - ) - .at(-1); - const isExpanded = Boolean(expandedTaskIds[task.id]); - - return ( -
    setDetailTask(task)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - setDetailTask(task); - } - }} - className={cn( - 'duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] flex cursor-pointer flex-col gap-1 rounded-xl border border-solid px-6 py-sm transition-[background-color,border-color]', - task.reAssignTo - ? 'bg-ds-bg-status-blocked-subtle-default' - : task.status === TaskStatus.COMPLETED - ? 'bg-ds-bg-status-completed-subtle-default' - : task.status === TaskStatus.FAILED - ? 'bg-ds-bg-status-error-subtle-default' - : task.status === TaskStatus.RUNNING - ? 'bg-ds-bg-status-running-subtle-default' - : task.status === TaskStatus.BLOCKED - ? 'bg-ds-bg-status-blocked-subtle-default' - : task.status === TaskStatus.SKIPPED || - task.status === TaskStatus.WAITING || - task.status === TaskStatus.EMPTY - ? 'bg-ds-bg-status-pending-subtle-default' - : 'bg-ds-bg-status-running-subtle-default', - task.status === TaskStatus.COMPLETED - ? 'hover:border-ds-border-status-completed-default-focus' - : task.status === TaskStatus.FAILED - ? 'hover:border-ds-border-status-error-default-focus' - : task.status === TaskStatus.RUNNING - ? 'hover:border-ds-border-neutral-strong-default' - : task.status === TaskStatus.BLOCKED - ? 'hover:border-ds-border-status-blocked-default-focus' - : task.status === TaskStatus.SKIPPED || - task.status === TaskStatus.WAITING || - task.status === TaskStatus.EMPTY - ? 'hover:border-ds-border-status-pending-default-hover' - : 'hover:border-ds-border-neutral-default-focus', - 'border-transparent' - )} - > -
    -
    -
    - {task.reAssignTo ? ( - - ) : ( - <> - {task.status === TaskStatus.RUNNING && ( - - )} - {task.status === TaskStatus.SKIPPED && ( - - )} - {task.status === TaskStatus.COMPLETED && ( - - )} - {task.status === TaskStatus.FAILED && ( - - )} - {task.status === TaskStatus.BLOCKED && ( - - )} - {(task.status === TaskStatus.EMPTY || - task.status === TaskStatus.WAITING) && ( - - )} - - )} -
    -
    - - No. {getTaskIdDisplay(task.id)} - - {task.reAssignTo ? ( -
    - Reassigned to {task.reAssignTo} -
    - ) : ( - (task.failure_count ?? 0) > 0 && ( -
    - Attempt {task.failure_count} -
    - ) - )} -
    -
    - -
    -
    -
    - {task.content ?? ''} -
    -
    - {task.status === TaskStatus.RUNNING && isExpanded && ( -
    - {lastActiveToolkit?.toolkitStatus === - AgentStatusValue.RUNNING && ( -
    - {getToolkitIcon( - lastActiveToolkit.toolkitName ?? '' - )} -
    - -
    -
    - )} -
    - )} -
    - ); - })} -
    -
    - - - {detailTask && ( - -
    - - - No. {getTaskIdDisplay(detailTask.id)} - -
    -
    e.stopPropagation()} - className="scrollbar scrollbar-always-visible min-h-0 flex-1 overflow-y-auto pb-2 pl-3 pr-1" - > - - - - - -
    -
    - )} -
    -
    -
    - ); -} diff --git a/src/components/Session/Workforce/FoldedPanel/index.tsx b/src/components/Session/Workforce/FoldedPanel/index.tsx deleted file mode 100644 index 6da9369a..00000000 --- a/src/components/Session/Workforce/FoldedPanel/index.tsx +++ /dev/null @@ -1,524 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import { PlanTaskBox } from '@/components/ChatBox/TaskBox/PlanTaskBox'; -import { isPlanSplittingPhase } from '@/components/ChatBox/TaskBox/PlanTaskBox/utils'; -import { TaskCard } from '@/components/ChatBox/TaskBox/TaskCard'; -import { BASE_WORKFLOW_AGENTS } from '@/components/WorkFlow/baseWorkers'; -import { - FoldedAgentCard, - isBaseWorkflowAgent, -} from '@/components/Workspace/FoldedAgentCard'; -import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; -import { useHost } from '@/host'; -import { useAuthStore, useWorkerList } from '@/store/authStore'; -import { AgentStep, ChatTaskStatus, TaskStatus } from '@/types/constants'; -import { AnimatePresence, motion } from 'framer-motion'; -import { - useCallback, - useEffect, - useMemo, - useRef, - useState, - useSyncExternalStore, -} from 'react'; -import { useTranslation } from 'react-i18next'; - -import { AgentDetailPane } from './AgentDetailPane'; - -const FOLDED_LAYOUT_TRANSITION = { - duration: 0.28, - ease: [0.4, 0, 0.2, 1] as const, -}; - -const EMPTY_TASK_ASSIGNING: Agent[] = []; - -/** After this long without pointer/scroll activity on the folded panel, resume auto-following the working agent. */ -const FOLDED_MANUAL_IDLE_RESUME_MS = 5 * 60 * 1000; - -/** True once any assigned sub-task has left waiting/empty (execution has begun or finished a unit of work). */ -function hasAgentTaskExecutionStarted(agents: Agent[]): boolean { - return agents.some((agent) => - (agent.tasks ?? []).some((t) => { - const s = t.status; - if (!s || s === TaskStatus.WAITING) return false; - return true; - }) - ); -} - -/** - * Prefer the last agent in list order that has an actively running (or blocked) sub-task — "latest" in rail order. - * Falls back to any agent with in-flight work (same sense as TaskState "ongoing"), then null. - */ -function pickLatestWorkingAgentId(agents: Agent[]): string | null { - const isRunningOrBlocked = (s: TaskInfo['status']) => - s === TaskStatus.RUNNING || s === TaskStatus.BLOCKED; - - for (let i = agents.length - 1; i >= 0; i--) { - const a = agents[i]; - if ((a.tasks ?? []).some((t) => isRunningOrBlocked(t.status))) { - return a.agent_id; - } - } - for (let i = agents.length - 1; i >= 0; i--) { - const a = agents[i]; - const hasOngoing = (a.tasks ?? []).some((t) => { - if (t.reAssignTo) return false; - const s = t.status; - return ( - !!s && - s !== TaskStatus.FAILED && - s !== TaskStatus.COMPLETED && - s !== TaskStatus.SKIPPED && - s !== TaskStatus.WAITING - ); - }); - if (hasOngoing) return a.agent_id; - } - return null; -} - -export interface FoldedPanelProps { - /** When true, do not push global activeWorkspace/activeAgent from the folded rail (expanded overlay shows workflow-only). */ - pauseAgentWorkspaceSync?: boolean; -} - -/** - * Narrow-column workforce layout when the workspace chat panel is visible. - * - `initial`: workforce list before any sub-task has started executing (agents may already exist on the task). - * - `task-live`: icon rail + detail pane once at least one assigned sub-task is past waiting. - */ -export default function FoldedPanel({ - pauseAgentWorkspaceSync = false, -}: FoldedPanelProps) { - const { t } = useTranslation(); - const host = useHost(); - const { chatStore, projectStore } = useChatStoreAdapter(); - const workerList = useWorkerList(); - const { setWorkerList } = useAuthStore(); - const [detailAgentId, setDetailAgentId] = useState(null); - /** User chose an agent in the rail; pause auto-follow until idle timeout. */ - const [manualFollowPaused, setManualFollowPaused] = useState(false); - const idleResumeTimerRef = useRef | null>(null); - - const clearIdleResumeTimer = useCallback(() => { - if (idleResumeTimerRef.current) { - clearTimeout(idleResumeTimerRef.current); - idleResumeTimerRef.current = null; - } - }, []); - - const resetManualIdleResumeTimer = useCallback(() => { - if (idleResumeTimerRef.current) { - clearTimeout(idleResumeTimerRef.current); - idleResumeTimerRef.current = null; - } - idleResumeTimerRef.current = setTimeout(() => { - idleResumeTimerRef.current = null; - setManualFollowPaused(false); - }, FOLDED_MANUAL_IDLE_RESUME_MS); - }, []); - - const onDeleteUserAgent = useCallback( - (agentId: string) => { - setWorkerList(workerList.filter((w) => w.agent_id !== agentId)); - }, - [workerList, setWorkerList] - ); - - const activeTaskId = chatStore?.activeTaskId as string | undefined; - const activeTask = activeTaskId ? chatStore?.tasks[activeTaskId] : undefined; - const taskAssigning = activeTask?.taskAssigning ?? EMPTY_TASK_ASSIGNING; - - const activeChatStore = projectStore.getActiveChatStore(); - const streamingDecomposeText = useSyncExternalStore( - (callback) => { - if (!activeChatStore) return () => {}; - return activeChatStore.subscribe(callback); - }, - () => { - if (!activeChatStore) return ''; - const state = activeChatStore.getState(); - const taskId = state.activeTaskId; - if (!taskId || !state.tasks[taskId]) return ''; - return state.tasks[taskId].streamingDecomposeText || ''; - }, - () => '' - ); - - const activeProjectId = projectStore.activeProjectId; - const taskPanelChatId = - activeProjectId && projectStore.projects[activeProjectId] - ? (projectStore.projects[activeProjectId].activeChatId ?? undefined) - : undefined; - - const { taskCardVisible, taskType } = useMemo(() => { - const fallback = { - taskCardVisible: false, - taskType: 1 as 1 | 2 | 3, - }; - if (!activeTask || !activeTaskId) return fallback; - - const messages = activeTask.messages; - const isHumanReply = - !!activeTask.activeAsk || - (() => { - const userMessages = messages.filter((m: any) => m.role === 'user'); - const lastUser = userMessages[userMessages.length - 1]; - if (!lastUser) return false; - const userMessageIndex = messages.findIndex( - (m: any) => m.id === lastUser.id - ); - if (userMessageIndex > 0) { - const prevMessage = messages[userMessageIndex - 1]; - return ( - prevMessage?.role === 'agent' && prevMessage?.step === AgentStep.ASK - ); - } - return false; - })(); - - const anyToSubTasksMessage = messages.find( - (m: any) => m.step === AgentStep.TO_SUB_TASKS - ); - const isSkeletonPhaseLocal = - (activeTask.status !== ChatTaskStatus.FINISHED && - activeTask.status !== ChatTaskStatus.RUNNING && - !anyToSubTasksMessage && - !activeTask.hasWaitComfirm && - messages.length > 0) || - (!!activeTask.isTakeControl && !anyToSubTasksMessage); - - let lastUserIndex = -1; - for (let i = messages.length - 1; i >= 0; i--) { - if (messages[i].role === 'user') { - lastUserIndex = i; - break; - } - } - const afterLastUser = - lastUserIndex >= 0 ? messages.slice(lastUserIndex + 1) : []; - const hasTaskPlanForCurrentTurn = afterLastUser.some( - (m: any) => m.step === AgentStep.TO_SUB_TASKS - ); - - const isDecomposing = streamingDecomposeText.length > 0; - const shouldShowFallbackTask = - lastUserIndex >= 0 && - !hasTaskPlanForCurrentTurn && - !activeTask.hasWaitComfirm && - !isDecomposing && - activeTask.status !== ChatTaskStatus.FINISHED; - - const taskLike = hasTaskPlanForCurrentTurn || shouldShowFallbackTask; - - const taskCardVisibleLocal = - taskLike && !isSkeletonPhaseLocal && !isHumanReply; - - const toSub = [...messages] - .reverse() - .find((m: any) => m.step === AgentStep.TO_SUB_TASKS); - const taskTypeLocal = (toSub?.taskType as 1 | 2 | 3) || 1; - - return { - taskCardVisible: taskCardVisibleLocal, - taskType: taskTypeLocal, - }; - }, [activeTask, activeTaskId, streamingDecomposeText]); - - const sortedAgents = useMemo(() => { - const base = [...BASE_WORKFLOW_AGENTS, ...workerList].filter( - (worker) => !taskAssigning.find((a) => a.type === worker.type) - ); - const allAgents = [...taskAssigning, ...base]; - return [...allAgents].sort((a, b) => { - const aHas = a.tasks && a.tasks.length > 0; - const bHas = b.tasks && b.tasks.length > 0; - if (aHas && !bHas) return -1; - if (!aHas && bHas) return 1; - return 0; - }); - }, [taskAssigning, workerList]); - - const isTaskLiveLayout = hasAgentTaskExecutionStarted(taskAssigning); - - useEffect(() => { - if (!manualFollowPaused || !isTaskLiveLayout) { - clearIdleResumeTimer(); - return; - } - resetManualIdleResumeTimer(); - return clearIdleResumeTimer; - }, [ - manualFollowPaused, - isTaskLiveLayout, - resetManualIdleResumeTimer, - clearIdleResumeTimer, - ]); - - const onFoldedPanelEngagement = useCallback(() => { - if (!manualFollowPaused || !isTaskLiveLayout) return; - resetManualIdleResumeTimer(); - }, [manualFollowPaused, isTaskLiveLayout, resetManualIdleResumeTimer]); - - const detailAgent = useMemo( - () => - detailAgentId - ? (sortedAgents.find((a) => a.agent_id === detailAgentId) ?? null) - : null, - [detailAgentId, sortedAgents] - ); - - /** Match main chat: task execution has started (not PENDING planning). */ - const isMainTaskStarted = - !!activeTask && - (activeTask.status === ChatTaskStatus.RUNNING || - activeTask.status === ChatTaskStatus.FINISHED || - activeTask.status === ChatTaskStatus.PAUSE); - - const hasUnconfirmedPlan = Boolean( - activeTask?.messages.some( - (m: any) => m.step === AgentStep.TO_SUB_TASKS && !m.isConfirm - ) - ); - const showPlanTaskBox = Boolean( - activeTaskId && - activeTask && - activeChatStore && - !activeTask.hasWaitComfirm && - !isMainTaskStarted && - (isPlanSplittingPhase(activeTask) || - streamingDecomposeText.length > 0 || - hasUnconfirmedPlan) - ); - - /** TaskCard only after the main task has started and an agent is selected (not detail-pane "Select an agent"). */ - const showFoldedTaskCard = - taskCardVisible && isMainTaskStarted && detailAgent != null; - - useEffect(() => { - if (pauseAgentWorkspaceSync) { - return; - } - if (!isTaskLiveLayout) { - setDetailAgentId(null); - setManualFollowPaused(false); - return; - } - if (manualFollowPaused) { - setDetailAgentId((prev) => { - if (prev && sortedAgents.some((a) => a.agent_id === prev)) return prev; - return ( - sortedAgents.find((a) => (a.tasks?.length ?? 0) > 0)?.agent_id ?? - sortedAgents[0]?.agent_id ?? - null - ); - }); - return; - } - - const workingId = pickLatestWorkingAgentId(sortedAgents); - const agentFromWorkspace = - activeTask?.activeWorkspace && - sortedAgents.some((a) => a.agent_id === activeTask.activeWorkspace) - ? activeTask.activeWorkspace - : null; - const agentFromActiveAgent = - activeTask?.activeAgent && - sortedAgents.some((a) => a.agent_id === activeTask.activeAgent) - ? activeTask.activeAgent - : null; - const fallback = - agentFromWorkspace ?? - agentFromActiveAgent ?? - sortedAgents.find((a) => (a.tasks?.length ?? 0) > 0)?.agent_id ?? - sortedAgents[0]?.agent_id ?? - null; - const resolved = workingId ?? fallback; - - if (!resolved) return; - - setDetailAgentId((prev) => (prev === resolved ? prev : resolved)); - - const taskId = chatStore?.activeTaskId; - if (!taskId || !chatStore) return; - if ( - activeTask?.activeWorkspace === resolved && - activeTask?.activeAgent === resolved - ) { - return; - } - chatStore.setActiveWorkspace(taskId, resolved); - chatStore.setActiveAgent(taskId, resolved); - host?.electronAPI?.hideAllWebview?.(); - }, [ - isTaskLiveLayout, - manualFollowPaused, - sortedAgents, - activeTask?.activeAgent, - activeTask?.activeWorkspace, - chatStore, - activeTaskId, - pauseAgentWorkspaceSync, - host, - ]); - - const onSelectAgent = useCallback( - (agentId: string) => { - if (!chatStore?.activeTaskId) return; - chatStore.setActiveWorkspace(chatStore.activeTaskId, agentId); - chatStore.setActiveAgent(chatStore.activeTaskId, agentId); - host?.electronAPI?.hideAllWebview?.(); - }, - [chatStore, host] - ); - - if (!chatStore) { - return null; - } - - const activeAgentId = activeTask?.activeAgent; - - return ( -
    -
    - - {isTaskLiveLayout ? ( - -
    -
    - {sortedAgents.map((agent) => ( -
    - { - setManualFollowPaused(true); - setDetailAgentId(agent.agent_id); - onSelectAgent(agent.agent_id); - }} - showUserAgentOverflow={false} - /> -
    - ))} -
    -
    - {showFoldedTaskCard && - activeTaskId && - activeTask && - chatStore && ( -
    - { - chatStore.addTaskInfo(); - }} - onUpdateTask={(taskIndex, content) => { - chatStore.updateTaskInfo(taskIndex, content); - }} - onSaveTask={() => { - chatStore.saveTaskInfo(); - }} - onDeleteTask={(taskIndex) => { - chatStore.deleteTaskInfo(taskIndex); - }} - clickable - /> -
    - )} - {showPlanTaskBox && activeTaskId && activeChatStore ? ( -
    - -
    - ) : null} -
    - {detailAgent ? ( - - setManualFollowPaused(true) - } - /> - ) : ( -
    - {t('chat.select-agent')} -
    - )} -
    -
    - ) : ( - -
    - {showPlanTaskBox && activeTaskId && activeChatStore ? ( - - ) : null} - {sortedAgents.map((agent) => ( - onSelectAgent(agent.agent_id)} - showUserAgentOverflow={!isBaseWorkflowAgent(agent)} - onDeleteUserAgent={onDeleteUserAgent} - /> - ))} -
    -
    - )} -
    -
    -
    - ); -} diff --git a/src/pages/Setting/Appearance.tsx b/src/components/Settings/Appearance/index.tsx similarity index 56% rename from src/pages/Setting/Appearance.tsx rename to src/components/Settings/Appearance/index.tsx index 479e73b7..7f0ab72d 100644 --- a/src/pages/Setting/Appearance.tsx +++ b/src/components/Settings/Appearance/index.tsx @@ -44,6 +44,8 @@ import { useAuthStore, type WorkspaceMainBackground } from '@/store/authStore'; import { Monitor, Moon, RotateCcw, Sun } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import SettingsPage from '../SettingsPage'; +import SettingsSection from '../SettingsSection'; const DEFAULT_EDITABLE_THEME_IDS = [ 'eigent', @@ -72,16 +74,6 @@ function buildMergedCatalog(customThemeCatalog: ThemeCatalog): ThemeCatalog { }; } -function formatThemeLabel(id: string): string { - if (id === 'whale') return 'Whale'; - if (id === 'custom') return 'Custom'; - if (id === 'camel') return 'CAMEL'; - if (id === 'claw') return 'Claw'; - if (id === 'starfish') return 'Starfish'; - if (!id) return id; - return id.charAt(0).toUpperCase() + id.slice(1); -} - function ColorSeedEditor({ label, value, @@ -91,6 +83,7 @@ function ColorSeedEditor({ value: string; onChange: (value: string) => void; }) { + const { t } = useTranslation(); const normalizedPreview = normalizeHexColor(value) ?? 'var(--colors-black-100)'; @@ -113,46 +106,42 @@ function ColorSeedEditor({ }; return ( -
    -
    +
    + {label} -
    -
    + +
    onChange(e.target.value)} - note={ - normalizeHexColor(value) - ? '' - : 'Hex format: six digits (e.g. 1a2b3c)' - } + note={normalizeHexColor(value) ? '' : t('setting.hex-color-format')} />
    @@ -190,12 +179,13 @@ function ContrastSlider({ value: number; onChange: (v: number) => void; }) { + const { t } = useTranslation(); return ( -
    -
    - Contrast -
    -
    +
    + + {t('setting.theme-contrast')} + +
    onChange(Number(e.target.value))} - className="h-2 bg-ds-bg-neutral-subtle-disabled accent-ds-bg-brand-default-default my-auto w-full cursor-pointer appearance-none rounded-full" - aria-label="Theme contrast" + className="my-auto h-2 w-full cursor-pointer appearance-none rounded-full bg-ds-bg-neutral-subtle-disabled accent-ds-bg-brand-default-default" + aria-label={t('setting.theme-contrast')} /> -
    + {value} -
    +
    ); @@ -250,16 +240,16 @@ export default function AppearanceSettings() { () => [ ...DEFAULT_EDITABLE_THEME_IDS.map((id) => ({ id, - label: formatThemeLabel(id), + label: t(`setting.theme-${id}`), isDefault: true, })), ...CUSTOM_THEME_IDS.map((id) => ({ id, - label: formatThemeLabel(id), + label: t(`setting.theme-${id}`), isDefault: false, })), ], - [] + [t] ); const allowedThemeIds = useMemo( @@ -407,156 +397,139 @@ export default function AppearanceSettings() { }; return ( -
    -
    -
    -
    -
    - {t('setting.appearance-tab')} -
    -
    -
    -
    + + + + setAppearanceMode(value as 'light' | 'dark' | 'system') + } + > + + + + + {t('setting.light')} + + + + + + {t('setting.dark')} + + + + + + {t('setting.system-default')} + + + + + -
    -
    -
    - Mode + +
    +
    + + + {themeOptions.map((option) => ( + + + {option.label} + + + ))} + +
    - - setAppearanceMode(value as 'light' | 'dark' | 'system') - } +
    -
    -
    -
    - Theme Customization -
    -
    - -
    -
    - - - {themeOptions.map((option) => ( - -
    - {option.label} -
    -
    - ))} -
    -
    -
    - - -
    - -
    - - - - -
    +
    + + + +
    + -
    -
    -
    - {t('setting.workspace-main-background')} -
    -
    - {t('setting.workspace-main-background-description')} -
    -
    - + +
    + + {t('setting.workspace-main-background-description')} +
    -
    -
    + +
    + ); } diff --git a/src/pages/Browser/CDP.tsx b/src/components/Settings/Browser/index.tsx similarity index 50% rename from src/pages/Browser/CDP.tsx rename to src/components/Settings/Browser/index.tsx index c76d148a..0c913fea 100644 --- a/src/pages/Browser/CDP.tsx +++ b/src/components/Settings/Browser/index.tsx @@ -15,12 +15,21 @@ import { fetchDelete, fetchGet, fetchPost } from '@/api/http'; import AlertDialog from '@/components/ui/alertDialog'; import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from '@/components/ui/dialog'; import { useHost } from '@/host'; import { Globe, Link2, Loader2, Plus, Trash2 } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; import { toast } from 'sonner'; +import SettingsPage from '../SettingsPage'; +import SettingsPageLoading from '../SettingsPageLoading'; +import SettingsSection from '../SettingsSection'; interface CdpBrowser { id: string; @@ -36,6 +45,8 @@ export default function CDP() { const { t } = useTranslation(); const [searchParams, setSearchParams] = useSearchParams(); const [cdpBrowsers, setCdpBrowsers] = useState([]); + const [browsersLoading, setBrowsersLoading] = useState(true); + const [browsersError, setBrowsersError] = useState(null); const [deletingBrowser, setDeletingBrowser] = useState(null); const [browserToRemove, setBrowserToRemove] = useState( null @@ -45,8 +56,11 @@ export default function CDP() { const [connectChecking, setConnectChecking] = useState(false); const [connectError, setConnectError] = useState(''); const isDesktopMode = !!electronAPI?.getCdpBrowsers; + const failedToLoadBrowsers = t('layout.failed-to-load-browsers'); - const loadCdpBrowsers = async () => { + const loadCdpBrowsers = useCallback(async () => { + setBrowsersLoading(true); + setBrowsersError(null); try { if (electronAPI?.getCdpBrowsers) { const browsers = await electronAPI.getCdpBrowsers(); @@ -58,12 +72,17 @@ export default function CDP() { setCdpBrowsers(Array.isArray(browsers) ? browsers : []); } catch (error) { console.error('Failed to load CDP browsers:', error); + setBrowsersError( + error instanceof Error ? error.message : failedToLoadBrowsers + ); + } finally { + setBrowsersLoading(false); } - }; + }, [electronAPI, failedToLoadBrowsers]); useEffect(() => { - loadCdpBrowsers(); - }, [electronAPI]); + void loadCdpBrowsers(); + }, [loadCdpBrowsers]); useEffect(() => { if (!electronAPI?.onCdpPoolChanged) return; @@ -127,7 +146,7 @@ export default function CDP() { id: 'launch-browser', }); } - }, [t]); + }, [electronAPI, isDesktopMode, loadCdpBrowsers, t]); useEffect(() => { if (searchParams.get('browserAction') !== 'launch') return; @@ -178,7 +197,7 @@ export default function CDP() { const addResult = await electronAPI.addCdpBrowser( portNum, true, - `External Browser (${portNum})` + t('layout.external-browser-name', { port: portNum }) ); if (!addResult?.success) { setConnectError( @@ -189,7 +208,7 @@ export default function CDP() { } else { const connectResult = await fetchPost('/browser/cdp/connect', { port: portNum, - name: `External Browser (${portNum})`, + name: t('layout.external-browser-name', { port: portNum }), }); if (!connectResult?.success) { setConnectError( @@ -210,7 +229,7 @@ export default function CDP() { }; return ( -
    + setBrowserToRemove(null)} @@ -221,7 +240,9 @@ export default function CDP() { }} title={t('layout.remove-browser')} message={t('layout.remove-browser-confirm', { - name: browserToRemove?.name || `Browser ${browserToRemove?.port}`, + name: + browserToRemove?.name || + t('layout.browser-name', { port: browserToRemove?.port }), port: browserToRemove?.port, })} confirmText={t('layout.remove')} @@ -229,151 +250,177 @@ export default function CDP() { confirmVariant="caution" /> - {/* Header Section */} -
    -
    -
    -
    - {t('layout.cdp-browser-connection')} -
    -
    -
    -
    - -
    -
    - {/* Header Section */} -
    -
    - {t('layout.cdp-browser-pool')} -
    -
    - - -
    -
    - {/* Content Section */} -
    - {cdpBrowsers.length > 0 ? ( -
    - {cdpBrowsers.map((browser) => ( -
    -
    -
    -
    - - {browser.name || `Browser ${browser.port}`} - - - {t('layout.port')} {browser.port} - -
    -
    - -
    - ))} -
    - ) : ( -
    - -
    - {t('layout.no-browsers-in-pool')} -
    -

    - {t('layout.add-browsers-hint')} -

    -
    - )} -
    -
    -
    - - {showConnectDialog && ( -
    -
    -
    + + +
    -

    - {t('layout.connect-existing-browser-description')} -

    - { - setConnectPort(event.target.value); - setConnectError(''); - }} - placeholder={t('layout.enter-port-number')} - className="rounded-lg border-ds-border-neutral-muted-disabled bg-ds-bg-neutral-default-default px-4 py-2 text-body-sm text-ds-text-neutral-default-default focus:border-ds-border-brand-default-focus w-full border outline-none" - onKeyDown={(event) => { - if (event.key === 'Enter') handleCheckAndConnect(); - }} - /> - {connectError && ( -

    - {connectError} -

    - )} -
    - - -
    +
    + } + boxClassName="min-h-[200px] gap-2" + > +
    + {browsersLoading && cdpBrowsers.length === 0 ? ( + + ) : browsersError && cdpBrowsers.length === 0 ? ( +
    + {browsersError} + +
    + ) : cdpBrowsers.length > 0 ? ( +
    + {cdpBrowsers.map((browser) => ( +
    +
    +
    +
    + + {browser.name || + t('layout.browser-name', { port: browser.port })} + + + {t('layout.port')} {browser.port} + +
    +
    + +
    + ))} +
    + ) : ( +
    + + + {t('layout.no-browsers-in-pool')} + + + {t('layout.add-browsers-hint')} + +
    + )}
    - )} -
    + + + { + if (!open && connectChecking) return; + setShowConnectDialog(open); + }} + > + { + if (connectChecking) event.preventDefault(); + }} + onPointerDownOutside={(event) => { + if (connectChecking) event.preventDefault(); + }} + > + + + {t('layout.connect-existing-browser')} + + + + + {t('layout.connect-existing-browser-description')} + + + { + setConnectPort(event.target.value); + setConnectError(''); + }} + placeholder={t('layout.enter-port-number')} + className="w-full rounded-lg border border-ds-border-neutral-muted-disabled bg-ds-bg-neutral-default-default px-4 py-2 text-body-sm text-ds-text-neutral-default-default outline-none focus:border-ds-border-brand-default-focus" + onKeyDown={(event) => { + if (event.key === 'Enter') void handleCheckAndConnect(); + }} + /> + {connectError && ( + + {connectError} + + )} +
    + + +
    +
    +
    + ); } diff --git a/src/components/Settings/Channels/index.tsx b/src/components/Settings/Channels/index.tsx new file mode 100644 index 00000000..16944fbf --- /dev/null +++ b/src/components/Settings/Channels/index.tsx @@ -0,0 +1,41 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { MessageSquare } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import SettingsPage from '../SettingsPage'; +import SettingsSection from '../SettingsSection'; + +export default function Channels() { + const { t } = useTranslation(); + + return ( + + +
    + +
    + + {t('layout.coming-soon')} + + + {t('layout.channels-overview-coming-soon-description')} + +
    +
    + ); +} diff --git a/src/pages/Connectors/ConnectorGateway.tsx b/src/components/Settings/Connectors/ConnectorGateway.tsx similarity index 77% rename from src/pages/Connectors/ConnectorGateway.tsx rename to src/components/Settings/Connectors/ConnectorGateway.tsx index 9acdcc63..2193ffd6 100644 --- a/src/pages/Connectors/ConnectorGateway.tsx +++ b/src/components/Settings/Connectors/ConnectorGateway.tsx @@ -39,6 +39,14 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Switch } from '@/components/ui/switch'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; import { useIntegrationManagement, type IntegrationItem, @@ -49,20 +57,19 @@ import { useAuthStore } from '@/store/authStore'; import { useServerCapabilityStore } from '@/store/serverCapabilityStore'; import type { TFunction } from 'i18next'; import { - BadgeCheck, ChevronDown, Ellipsis, ExternalLink, - Hammer, Pencil, Plus, RefreshCw, Server, - Settings, Trash2, Wrench, } from 'lucide-react'; import { + lazy, + Suspense, useCallback, useEffect, useLayoutEffect, @@ -73,15 +80,20 @@ import { import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; import { toast } from 'sonner'; -import AddConnectorDialog, { - ProviderIcon, +import { + SettingsHeaderActions, + useSettingsHeader, +} from '../SettingsHeaderContext'; +import SettingsPage from '../SettingsPage'; +import SettingsSection from '../SettingsSection'; +import ConnectorBrowserPage, { actionLabel, isConnectedProvider, providerActionCount, + ProviderIcon, providerLabel, type AddConnectorTarget, -} from './components/AddConnectorDialog'; -import AddCustomConnectorDialog from './components/AddCustomConnectorDialog'; +} from './components/ConnectorBrowserPage'; import { GoogleSearchPanel } from './components/GoogleSearchPanel'; import MCPConfigDialog from './components/MCPConfigDialog'; import MCPDeleteDialog from './components/MCPDeleteDialog'; @@ -94,6 +106,13 @@ import { arrayToArgsJson, parseArgsToArray } from './components/utils'; const IS_LOCAL_MODE = import.meta.env.VITE_USE_LOCAL_PROXY === 'true'; const OVERVIEW_ID = '__overview__'; +type ConnectorPage = 'overview' | 'browse' | 'custom'; +const loadAddCustomConnectorPage = () => + import('./components/AddCustomConnectorPage'); +const AddCustomConnectorPage = lazy(loadAddCustomConnectorPage); +const preloadAddCustomConnectorPage = () => { + void loadAddCustomConnectorPage().catch(() => undefined); +}; const HIDDEN_BUILT_INS = new Set([ 'RAG', 'X(Twitter)', @@ -110,8 +129,6 @@ const RECOMMENDED_SERVICE_KEYS = [ ['google_drive', 'googledrive', 'google-drive'], ['github'], ['google_calendar', 'googlecalendar', 'google-calendar'], - ['stripe'], - ['feishu', 'lark'], ] as const; type ConnectorListItem = @@ -322,6 +339,8 @@ async function resolveOpenProviderByName( export default function ConnectorGateway() { const { t } = useTranslation(); + const connectorHeaderTitle = t('layout.connectors'); + const { setHeaderOverride } = useSettingsHeader(); const [searchParams, setSearchParams] = useSearchParams(); const { checkAgentTool, modelType } = useAuthStore(); const capabilityStatus = useServerCapabilityStore((state) => state.status); @@ -337,23 +356,23 @@ export default function ConnectorGateway() { const [openConnections, setOpenConnections] = useState( [] ); + const [openConnectionsLoaded, setOpenConnectionsLoaded] = + useState(IS_LOCAL_MODE); const [selectedId, setSelectedId] = useState(OVERVIEW_ID); const [recommendedProviders, setRecommendedProviders] = useState< ConnectorProvider[] >([]); - const [recommendedLoading, setRecommendedLoading] = useState(false); + const [recommendedLoading, setRecommendedLoading] = useState(!IS_LOCAL_MODE); const [openDetail, setOpenDetail] = useState(null); const [listQuery, setListQuery] = useState(''); const [loadingOpen, setLoadingOpen] = useState(false); - const [loadingCustom, setLoadingCustom] = useState(false); - const [loadingBuiltIns, setLoadingBuiltIns] = useState(false); + const [loadingCustom, setLoadingCustom] = useState(true); + const [loadingBuiltIns, setLoadingBuiltIns] = useState(true); const [detailLoading, setDetailLoading] = useState(false); const [actionLoading, setActionLoading] = useState(false); const [pageError, setPageError] = useState(null); - const [browseDialogOpen, setBrowseDialogOpen] = useState(false); - const [browseDialogTarget, setBrowseDialogTarget] = - useState(null); - const [customDialogOpen, setCustomDialogOpen] = useState(false); + const [connectorPage, setConnectorPage] = useState('overview'); + const [browseTarget, setBrowseTarget] = useState(null); const [showConfig, setShowConfig] = useState(null); const [configForm, setConfigForm] = useState(null); const [configError, setConfigError] = useState(null); @@ -367,6 +386,7 @@ export default function ConnectorGateway() { const { installed: rawBuiltInInstalled, configs, + configsLoading, fetchInstalled: refreshBuiltIns, saveEnvAndConfig, handleUninstall, @@ -426,9 +446,11 @@ export default function ConnectorGateway() { const loadOpenConnections = useCallback(async () => { if (!connectorGatewayEnabled) { setOpenConnections([]); + setOpenConnectionsLoaded(true); return; } setLoadingOpen(true); + setOpenConnectionsLoaded(false); try { setOpenConnections(await fetchConnectedProviders()); } catch (error: any) { @@ -436,6 +458,7 @@ export default function ConnectorGateway() { setOpenConnections([]); } finally { setLoadingOpen(false); + setOpenConnectionsLoaded(true); } }, [connectorGatewayEnabled, t]); @@ -462,7 +485,11 @@ export default function ConnectorGateway() { }, [capabilityStatus, connectorGatewayEnabled, loadOpenConnections]); useEffect(() => { - if (capabilityStatus !== 'ready' || !connectorGatewayEnabled) { + if (capabilityStatus === 'idle' || capabilityStatus === 'loading') { + setRecommendedLoading(true); + return; + } + if (!connectorGatewayEnabled) { setRecommendedProviders([]); setRecommendedLoading(false); return; @@ -623,10 +650,10 @@ export default function ConnectorGateway() { return; } if (section === 'your-mcp') { - setCustomDialogOpen(true); + setConnectorPage('custom'); } else { - setBrowseDialogTarget(null); - setBrowseDialogOpen(true); + setBrowseTarget(null); + setConnectorPage('browse'); } const next = new URLSearchParams(searchParams); next.delete('connectorAction'); @@ -660,30 +687,38 @@ export default function ConnectorGateway() { ); }, [connectorItems, listQuery, t]); - const openBrowseDialog = (target: AddConnectorTarget = null) => { - // Non-local hosts always use Connector Gateway providers — never Built-in. - // Open the dialog immediately and resolve the matching hosted provider in the - // background so the button never feels dead. - if (!IS_LOCAL_MODE && target?.source === 'builtin') { - setBrowseDialogTarget(null); - setBrowseDialogOpen(true); - if (connectorGatewayEnabled) { - const builtInKey = target.item.key; - void resolveOpenProviderByName(builtInKey).then((provider) => { - if (provider) { - setBrowseDialogTarget({ source: 'open', provider }); - } - }); + const openBrowsePage = useCallback( + (target: AddConnectorTarget = null) => { + // Non-local hosts always use Connector Gateway providers — never Built-in. + // Open the dialog immediately and resolve the matching hosted provider in the + // background so the button never feels dead. + if (!IS_LOCAL_MODE && target?.source === 'builtin') { + setBrowseTarget(null); + setConnectorPage('browse'); + if (connectorGatewayEnabled) { + const builtInKey = target.item.key; + void resolveOpenProviderByName(builtInKey).then((provider) => { + if (provider) { + setBrowseTarget({ source: 'open', provider }); + } + }); + } + return; } - return; - } - setBrowseDialogTarget(target); - setBrowseDialogOpen(true); - }; + setBrowseTarget(target); + setConnectorPage('browse'); + }, + [connectorGatewayEnabled] + ); - const openCustomDialog = () => { - setCustomDialogOpen(true); - }; + const openCustomPage = useCallback(() => { + setConnectorPage('custom'); + }, []); + + const closeConnectorSubpage = useCallback(() => { + setBrowseTarget(null); + setConnectorPage('overview'); + }, []); const openRecommendedConnector = (provider: ConnectorProvider) => { const existing = connectorItems.find( @@ -694,9 +729,24 @@ export default function ConnectorGateway() { setSelectedId(existing.id); return; } - openBrowseDialog({ source: 'open', provider }); + openBrowsePage({ source: 'open', provider }); }; + useEffect(() => { + if (connectorPage !== 'overview') return; + setHeaderOverride( + selected + ? { + title: selected.name, + onBack: () => setSelectedId(OVERVIEW_ID), + } + : { + title: connectorHeaderTitle, + } + ); + return () => setHeaderOverride(null); + }, [connectorHeaderTitle, connectorPage, selected, setHeaderOverride]); + const handleInstalled = useCallback( async (hint: ConnectorInstallHint) => { preferredSelectionRef.current = hint; @@ -848,7 +898,14 @@ export default function ConnectorGateway() { } }; - const pageLoading = loadingOpen || loadingCustom || loadingBuiltIns; + const pageLoading = + capabilityStatus === 'idle' || + capabilityStatus === 'loading' || + (connectorGatewayEnabled && !openConnectionsLoaded) || + configsLoading || + loadingOpen || + loadingCustom || + loadingBuiltIns; const renderListIcon = (item: ConnectorListItem) => { if (item.source === 'open') { @@ -914,7 +971,7 @@ export default function ConnectorGateway() { variant="ghost" size="sm" onClick={() => - openBrowseDialog( + openBrowsePage( item.source === 'open' ? { source: 'open', provider: item.provider } : { source: 'builtin', item: item.item } @@ -1142,7 +1199,7 @@ export default function ConnectorGateway() { }; const renderDetailPanel = (item: ConnectorListItem) => ( -
    +
    {renderDetailHeader(item)}
    {item.source === 'open' @@ -1154,130 +1211,213 @@ export default function ConnectorGateway() {
    ); - const renderOverviewPanel = () => { - const count = connectorItems.length; - return ( -
    -
    - - {pageLoading && count === 0 ? '—' : count} - - - {count === 1 - ? t('connectors.count-one') - : t('connectors.count-other')} - + const renderRecommendedConnectors = () => { + if (recommendedLoading && recommendedProviders.length === 0) { + return ( +
    + {Array.from({ length: 6 }).map((_, index) => ( +
    + ))}
    + ); + } -
    - - {t('connectors.recommended')} - - {recommendedLoading && recommendedProviders.length === 0 ? ( -
    - {Array.from({ length: 8 }).map((_, index) => ( -
    - ))} -
    - ) : recommendedProviders.length === 0 ? ( -
    - {connectorGatewayEnabled - ? t('connectors.no-recommended') - : t('connectors.gateway-unavailable')} -
    - ) : ( -
    - {recommendedProviders.map((provider) => { - const liveProvider = - openConnections.find( - (item) => item.service === provider.service - ) || provider; - const connected = isConnectedProvider(liveProvider); - const existing = connectorItems.find( - (item) => - item.source === 'open' && - item.provider.service === provider.service - ); - return ( - - ); - })} -
    - )} + if (recommendedProviders.length === 0) { + return ( +
    + {connectorGatewayEnabled + ? t('connectors.no-recommended') + : t('connectors.gateway-unavailable')}
    + ); + } + + return ( +
    + {recommendedProviders.slice(0, 6).map((provider) => { + const liveProvider = + openConnections.find((item) => item.service === provider.service) || + provider; + return ( + + ); + })}
    ); }; + const renderConnectorTable = () => { + if (pageLoading && connectorItems.length === 0) { + return ( +
    +
    + {Array.from({ length: 4 }).map((_, index) => ( +
    + ))} +
    + ); + } + + if (visibleItems.length === 0) { + return ( +
    + {t('connectors.no-matching')} +
    + ); + } + + return ( + + + + + {t('connectors.title')} + + {t('connectors.type')} + + {t('connectors.status')} + + + + + {visibleItems.map((item) => ( + setSelectedId(item.id)} + className="cursor-pointer border-ds-border-neutral-muted-default hover:bg-ds-bg-neutral-subtle-default" + > + + + + + + {sourceLabel(item, t)} + + + + + {item.active + ? t('connectors.connected') + : t('connectors.not-connected')} + + + + ))} + +
    + ); + }; + + if (connectorPage === 'browse') { + return ( + + ); + } + + if (connectorPage === 'custom') { + return ( + + + {t('setting.loading', { + defaultValue: 'Loading connector settings', + })} + +
    +
    +
    + } + > + + + ); + } + return ( -
    -
    -

    - {t('connectors.title')} -

    -
    + + {!selected ? ( + setListQuery(event.target.value)} placeholder={t('connectors.search-placeholder')} + searchTooltip={t('connectors.search-placeholder')} /> - - -
    -
    - + + ) : null} {pageError ? ( -
    +
    {pageError} - - {pageLoading && connectorItems.length === 0 ? ( - Array.from({ length: 4 }).map((_, index) => ( -
    - )) - ) : visibleItems.length === 0 ? ( - listQuery.trim() ? ( -
    - - {t('connectors.no-matching')} - -
    - ) : null - ) : ( - visibleItems.map((item) => { - const active = selectedId === item.id; - return ( - - ); - }) - )} -
    - - -
    - {selectedId === OVERVIEW_ID || !selected - ? renderOverviewPanel() - : renderDetailPanel(selected)} -
    -
    - - { - setBrowseDialogOpen(next); - if (!next) setBrowseDialogTarget(null); - }} - onInstalled={handleInstalled} - saveBuiltInValue={saveEnvAndConfig} - refreshBuiltIns={refreshBuiltIns} - /> - - + } + boxClassName="overflow-hidden p-0" + > + {renderConnectorTable()} + + + )} -
    + ); } diff --git a/src/pages/Connectors/components/AddCustomConnectorDialog.tsx b/src/components/Settings/Connectors/components/AddCustomConnectorPage.tsx similarity index 55% rename from src/pages/Connectors/components/AddCustomConnectorDialog.tsx rename to src/components/Settings/Connectors/components/AddCustomConnectorPage.tsx index 24f0db45..773af97c 100644 --- a/src/pages/Connectors/components/AddCustomConnectorDialog.tsx +++ b/src/components/Settings/Connectors/components/AddCustomConnectorPage.tsx @@ -19,13 +19,7 @@ import { proxyFetchPost, proxyFetchPut, } from '@/api/http'; -import { - Dialog, - DialogContent, - DialogContentSection, - DialogFooter, - DialogHeader, -} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import loader from '@monaco-editor/loader'; @@ -35,6 +29,9 @@ import * as monaco from 'monaco-editor'; import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; +import { useSettingsHeader } from '../../SettingsHeaderContext'; +import SettingsPage from '../../SettingsPage'; +import SettingsSection from '../../SettingsSection'; import type { ConnectorInstallHint, MCPUserItem } from './types'; if (typeof globalThis !== 'undefined') { @@ -69,20 +66,20 @@ const LOCAL_MCP_EXAMPLE = `{ } }`; -interface AddCustomConnectorDialogProps { - open: boolean; +interface AddCustomConnectorPageProps { customMcps: MCPUserItem[]; - onOpenChange: (open: boolean) => void; + onBack: () => void; onInstalled: (hint: ConnectorInstallHint) => void | Promise; } -export default function AddCustomConnectorDialog({ - open, +export default function AddCustomConnectorPage({ customMcps, - onOpenChange, + onBack, onInstalled, -}: AddCustomConnectorDialogProps) { +}: AddCustomConnectorPageProps) { const { t } = useTranslation(); + const customHeaderTitle = t('connectors.custom-title'); + const { setHeaderOverride } = useSettingsHeader(); const [customType, setCustomType] = useState<'local' | 'remote'>('local'); const [localJson, setLocalJson] = useState(LOCAL_MCP_EXAMPLE); const [remoteName, setRemoteName] = useState(''); @@ -91,28 +88,13 @@ export default function AddCustomConnectorDialog({ const [formError, setFormError] = useState(null); const [jsonError, setJsonError] = useState(null); - useEffect(() => { - if (!open) return; - setCustomType('local'); - setRemoteName(''); - setRemoteUrl(''); - setFormError(null); - setJsonError(null); - setSaving(false); - try { - setLocalJson(JSON.stringify(JSON.parse(LOCAL_MCP_EXAMPLE), null, 4)); - } catch { - setLocalJson(LOCAL_MCP_EXAMPLE); - } - }, [open]); - useEffect(() => { setJsonError(null); }, [localJson]); - const closeDialog = useCallback(() => { - onOpenChange(false); - }, [onOpenChange]); + const closePage = useCallback(() => { + onBack(); + }, [onBack]); const installCustom = useCallback(async () => { setSaving(true); @@ -188,7 +170,7 @@ export default function AddCustomConnectorDialog({ } toast.success(t('connectors.custom-installed', { num: names.length })); await onInstalled({ source: 'custom', key: names[0] }); - closeDialog(); + closePage(); return; } @@ -246,14 +228,14 @@ export default function AddCustomConnectorDialog({ } toast.success(t('connectors.installed-toast', { name })); await onInstalled({ source: 'custom', key: name }); - closeDialog(); + closePage(); } catch (error: any) { setFormError(error?.message || t('connectors.install-custom-failed')); } finally { setSaving(false); } }, [ - closeDialog, + closePage, customMcps, customType, localJson, @@ -263,149 +245,142 @@ export default function AddCustomConnectorDialog({ t, ]); + const changeCustomType = useCallback( + (value: string) => { + const next = value as 'local' | 'remote'; + setCustomType(next); + setFormError(null); + if (next !== 'local') return; + try { + setLocalJson(JSON.stringify(JSON.parse(localJson), null, 4)); + setJsonError(null); + } catch (error: any) { + setJsonError( + t('connectors.json-format-error', { + message: error?.message || String(error), + }) + ); + } + }, + [localJson, t] + ); + useEffect(() => { + setHeaderOverride({ + title: customHeaderTitle, + onBack: closePage, + }); + return () => setHeaderOverride(null); + }, [closePage, customHeaderTitle, setHeaderOverride]); + return ( - { - if (!next) closeDialog(); - }} - > - + - - - -
    -
    - { - const next = value as 'local' | 'remote'; - setCustomType(next); - setFormError(null); - if (next !== 'local') return; - try { - setLocalJson( - JSON.stringify(JSON.parse(localJson), null, 4) - ); - setJsonError(null); - } catch (error: any) { - setJsonError( - t('connectors.json-format-error', { - message: error?.message || String(error), - }) - ); - } - }} - > - - - - - {t('connectors.source-local')} - - - - - - {t('connectors.source-remote')} - - - - - -
    - {t('connectors.custom-warning')} -
    - - {customType === 'local' ? ( -
    - - {t('connectors.local-json-desc')}{' '} - - {t('connectors.learn-more')} - +
    +
    + + + + + + {t('connectors.source-local')} - {jsonError ? ( - - {jsonError} - - ) : null} -
    - setLocalJson(value ?? '')} - options={{ - minimap: { enabled: false }, - fontSize: 14, - scrollBeyondLastLine: false, - readOnly: saving, - automaticLayout: true, - }} - /> -
    -
    - ) : ( -
    - setRemoteName(event.target.value)} - placeholder={t('connectors.remote-name-placeholder')} - leadingIcon={} - /> - setRemoteUrl(event.target.value)} - placeholder="https://example.com/mcp" - leadingIcon={} - note={t('connectors.remote-url-note')} - /> -
    - )} - - {formError ? ( -
    - {formError} -
    - ) : null} -
    + + + + + {t('connectors.source-remote')} + + + +
    - void installCustom()} - confirmButtonDisabled={saving} - /> - - -
    +
    + {t('connectors.custom-warning')} +
    + + {customType === 'local' ? ( +
    + + {t('connectors.local-json-desc')}{' '} + + {t('connectors.learn-more')} + + + {jsonError ? ( + + {jsonError} + + ) : null} +
    + setLocalJson(value ?? '')} + options={{ + minimap: { enabled: false }, + fontSize: 14, + scrollBeyondLastLine: false, + readOnly: saving, + automaticLayout: true, + }} + /> +
    +
    + ) : ( +
    + setRemoteName(event.target.value)} + placeholder={t('connectors.remote-name-placeholder')} + leadingIcon={} + /> + setRemoteUrl(event.target.value)} + placeholder="https://example.com/mcp" + leadingIcon={} + note={t('connectors.remote-url-note')} + /> +
    + )} + + {formError ? ( +
    + {formError} +
    + ) : null} +
    + + +
    +
    + + ); } diff --git a/src/pages/Connectors/components/AddConnectorDialog.tsx b/src/components/Settings/Connectors/components/ConnectorBrowserPage.tsx similarity index 89% rename from src/pages/Connectors/components/AddConnectorDialog.tsx rename to src/components/Settings/Connectors/components/ConnectorBrowserPage.tsx index 17e0499a..62ead569 100644 --- a/src/pages/Connectors/components/AddConnectorDialog.tsx +++ b/src/components/Settings/Connectors/components/ConnectorBrowserPage.tsx @@ -29,13 +29,6 @@ import { proxyFetchGet } from '@/api/http'; import SearchInput from '@/components/Dashboard/SearchInput'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogContentSection, - DialogFooter, - DialogHeader, -} from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Skeleton } from '@/components/ui/skeleton'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; @@ -63,15 +56,20 @@ import { useMemo, useRef, useState, - type UIEvent, } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; +import { + SettingsHeaderActions, + useSettingsHeader, +} from '../../SettingsHeaderContext'; +import SettingsPage from '../../SettingsPage'; +import SettingsSection from '../../SettingsSection'; import type { ConnectorInstallHint } from './types'; export type { ConnectorInstallHint }; -/** Large enough to fill the dialog viewport and avoid immediate load-more waterfalls. */ +/** Large enough to fill the settings viewport and avoid load-more waterfalls. */ const MARKET_PAGE_SIZE = 60; export type AddConnectorTarget = @@ -86,15 +84,14 @@ interface StoredConfig { config_value?: string; } -interface AddConnectorDialogProps { - open: boolean; +interface ConnectorBrowserPageProps { connectorGatewayEnabled: boolean; localMode: boolean; builtInItems: IntegrationItem[]; builtInInstalled: Record; configs: StoredConfig[]; initialTarget?: AddConnectorTarget; - onOpenChange: (open: boolean) => void; + onBack: () => void; onInstalled: (hint: ConnectorInstallHint) => void | Promise; saveBuiltInValue: ( provider: string, @@ -300,20 +297,21 @@ function CatalogLoadingBanner({ label }: { label: string }) { ); } -export default function AddConnectorDialog({ - open, +export default function ConnectorBrowserPage({ connectorGatewayEnabled, localMode, builtInItems, builtInInstalled, configs, initialTarget = null, - onOpenChange, + onBack, onInstalled, saveBuiltInValue, refreshBuiltIns, -}: AddConnectorDialogProps) { +}: ConnectorBrowserPageProps) { const { t } = useTranslation(); + const browseHeaderTitle = t('connectors.browse'); + const { setHeaderOverride } = useSettingsHeader(); const shouldReduceMotion = useReducedMotion(); const [browseSource, setBrowseSource] = useState<'open' | 'builtin'>( connectorGatewayEnabled || !localMode ? 'open' : 'builtin' @@ -369,7 +367,6 @@ export default function AddConnectorDialog({ useEffect(() => stopOAuthPolling, [stopOAuthPolling]); useEffect(() => { - if (!open) return; // Built-in is only available in local mode. Hosted / non-local always uses // Connector Gateway providers. const allowBuiltin = localMode; @@ -397,7 +394,7 @@ export default function AddConnectorDialog({ setFormError(null); setAuthorizationPending(false); setActionsExpanded(false); - }, [connectorGatewayEnabled, initialTarget, localMode, open]); + }, [connectorGatewayEnabled, initialTarget, localMode]); useEffect(() => { setActionsExpanded(false); @@ -446,7 +443,7 @@ export default function AddConnectorDialog({ append: boolean, options: { soft?: boolean; bypassCache?: boolean } = {} ) => { - if (!open || !connectorGatewayEnabled || browseSource !== 'open') return; + if (!connectorGatewayEnabled || browseSource !== 'open') return; const requestId = ++catalogRequestIdRef.current; const soft = options.soft === true; if (append) { @@ -485,14 +482,7 @@ export default function AddConnectorDialog({ } } }, - [ - applyCatalogPage, - browseSource, - connectorGatewayEnabled, - debouncedQuery, - open, - t, - ] + [applyCatalogPage, browseSource, connectorGatewayEnabled, debouncedQuery, t] ); const loadNextCatalogPage = useCallback(() => { @@ -522,22 +512,9 @@ export default function AddConnectorDialog({ selectedProvider, ]); - const handleBrowseScroll = useCallback( - (event: UIEvent) => { - if (browseSource !== 'open') return; - const element = event.currentTarget; - const distanceToBottom = - element.scrollHeight - element.scrollTop - element.clientHeight; - if (distanceToBottom <= 280) { - loadNextCatalogPage(); - } - }, - [browseSource, loadNextCatalogPage] - ); - // Hydrate from cache before paint to avoid empty-state / skeleton flash on open. useLayoutEffect(() => { - if (!open || !connectorGatewayEnabled || browseSource !== 'open') return; + if (!connectorGatewayEnabled || browseSource !== 'open') return; const cached = getCachedConnectorProviders({ page: 1, pageSize: MARKET_PAGE_SIZE, @@ -553,16 +530,10 @@ export default function AddConnectorDialog({ setPage(1); setCatalogError(null); setCatalogLoading(true); - }, [ - applyCatalogPage, - browseSource, - connectorGatewayEnabled, - debouncedQuery, - open, - ]); + }, [applyCatalogPage, browseSource, connectorGatewayEnabled, debouncedQuery]); useEffect(() => { - if (!open || !connectorGatewayEnabled || browseSource !== 'open') return; + if (!connectorGatewayEnabled || browseSource !== 'open') return; const cached = getCachedConnectorProviders({ page: 1, pageSize: MARKET_PAGE_SIZE, @@ -573,13 +544,7 @@ export default function AddConnectorDialog({ return; } void loadCatalogPage(1, false); - }, [ - browseSource, - connectorGatewayEnabled, - debouncedQuery, - loadCatalogPage, - open, - ]); + }, [browseSource, connectorGatewayEnabled, debouncedQuery, loadCatalogPage]); useEffect(() => { if (browseSource !== 'open') return; @@ -592,7 +557,7 @@ export default function AddConnectorDialog({ if (!entries[0]?.isIntersecting) return; loadNextCatalogPage(); }, - { root, rootMargin: '160px' } + { root: null, rootMargin: '160px' } ); observer.observe(sentinel); return () => observer.disconnect(); @@ -669,18 +634,18 @@ export default function AddConnectorDialog({ }); }, [builtInItems, debouncedQuery]); - const closeDialog = useCallback(() => { + const closePage = useCallback(() => { stopOAuthPolling(); - onOpenChange(false); - }, [onOpenChange, stopOAuthPolling]); + onBack(); + }, [onBack, stopOAuthPolling]); const finishInstall = useCallback( async (hint: ConnectorInstallHint) => { stopOAuthPolling(); await onInstalled(hint); - closeDialog(); + closePage(); }, - [closeDialog, onInstalled, stopOAuthPolling] + [closePage, onInstalled, stopOAuthPolling] ); const startOAuthPolling = useCallback( @@ -852,7 +817,7 @@ export default function AddConnectorDialog({ setFormError(null); }; - const goBackToBrowse = () => { + const goBackToBrowse = useCallback(() => { stopOAuthPolling(); setSelectedProvider(null); setSelectedBuiltIn(null); @@ -866,76 +831,78 @@ export default function AddConnectorDialog({ browseScrollRef.current.scrollTop = savedScrollTopRef.current; } }, 0); - }; + }, [stopOAuthPolling]); const showingDetail = Boolean(selectedProvider || selectedBuiltIn); + const detailOpenedDirectly = initialTarget !== null; + + useEffect(() => { + setHeaderOverride({ + title: selectedProvider + ? providerLabel(selectedProvider) + : selectedBuiltIn + ? selectedBuiltIn.name + : browseHeaderTitle, + onBack: + showingDetail && !detailOpenedDirectly ? goBackToBrowse : closePage, + }); + return () => setHeaderOverride(null); + }, [ + browseHeaderTitle, + closePage, + detailOpenedDirectly, + goBackToBrowse, + selectedBuiltIn, + selectedProvider, + setHeaderOverride, + showingDetail, + ]); return ( - { - if (!next) closeDialog(); - }} - > - + {!showingDetail ? ( + + setQuery(event.target.value)} + placeholder={t('connectors.search-connectors')} + searchTooltip={t('connectors.search-connectors')} + /> + {localMode && connectorGatewayEnabled ? ( + + ) : null} + {localMode ? ( + + ) : null} + + ) : null} + - - {!showingDetail ? ( - -
    - setQuery(event.target.value)} - placeholder={t('connectors.search-connectors')} - /> -
    - - {localMode ? ( -
    - {connectorGatewayEnabled ? ( - - ) : null} - -
    - ) : null} - +
    {browseSource === 'open' ? ( !connectorGatewayEnabled ? ( @@ -994,7 +961,7 @@ export default function AddConnectorDialog({ key={provider.service} type="button" onClick={() => openProvider(provider)} - className="group flex h-20 items-center gap-3 rounded-2xl border border-solid border-transparent bg-ds-bg-neutral-default-default px-4 py-3 text-left transition-colors hover:border-ds-border-neutral-default-default hover:bg-ds-bg-neutral-default-hover" + className="group flex h-20 items-center gap-3 rounded-2xl border border-solid border-transparent bg-ds-bg-neutral-subtle-default px-4 py-3 text-left transition-colors hover:border-ds-border-neutral-default-default hover:bg-ds-bg-neutral-default-hover" >
    @@ -1070,7 +1037,7 @@ export default function AddConnectorDialog({ setSelectedBuiltIn(item); setFormError(null); }} - className="group flex h-20 items-center gap-3 rounded-2xl border border-solid border-transparent bg-ds-bg-neutral-default-default px-4 py-3 text-left transition-colors hover:border-ds-border-neutral-default-default hover:bg-ds-bg-neutral-subtle-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ds-ring-brand-default-focus" + className="group flex h-20 items-center gap-3 rounded-2xl border border-solid border-transparent bg-ds-bg-neutral-subtle-default px-4 py-3 text-left transition-colors hover:border-ds-border-neutral-default-default hover:bg-ds-bg-neutral-subtle-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ds-ring-brand-default-focus" >
    @@ -1101,10 +1068,10 @@ export default function AddConnectorDialog({
    )}
    - +
    ) : selectedProvider ? ( <> - +
    {detailLoading ? (
    @@ -1282,29 +1249,28 @@ export default function AddConnectorDialog({ ) : null}
    )} - - +
    + + +
    ) : selectedBuiltIn ? ( <> - +
    @@ -1430,22 +1396,23 @@ export default function AddConnectorDialog({
    ) : null}
    - - void installBuiltIn()} - confirmButtonDisabled={saving || authorizationPending} - /> +
    +
    + + +
    ) : null} - -
    + + ); } diff --git a/src/pages/Connectors/components/GoogleSearchPanel.tsx b/src/components/Settings/Connectors/components/GoogleSearchPanel.tsx similarity index 100% rename from src/pages/Connectors/components/GoogleSearchPanel.tsx rename to src/components/Settings/Connectors/components/GoogleSearchPanel.tsx diff --git a/src/pages/Connectors/components/MCPConfigDialog.tsx b/src/components/Settings/Connectors/components/MCPConfigDialog.tsx similarity index 100% rename from src/pages/Connectors/components/MCPConfigDialog.tsx rename to src/components/Settings/Connectors/components/MCPConfigDialog.tsx diff --git a/src/components/Settings/Connectors/components/MCPDeleteDialog.tsx b/src/components/Settings/Connectors/components/MCPDeleteDialog.tsx new file mode 100644 index 00000000..ab587e93 --- /dev/null +++ b/src/components/Settings/Connectors/components/MCPDeleteDialog.tsx @@ -0,0 +1,94 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from '@/components/ui/dialog'; +import { useTranslation } from 'react-i18next'; +import type { MCPUserItem } from './types'; + +interface MCPDeleteDialogProps { + open: boolean; + target: MCPUserItem | null; + onCancel: () => void; + onConfirm: () => void; + loading: boolean; +} + +export default function MCPDeleteDialog({ + open, + target, + onCancel, + onConfirm, + loading, +}: MCPDeleteDialogProps) { + const { t } = useTranslation(); + if (!open || !target) return null; + + return ( + { + if (!nextOpen && !loading) onCancel(); + }} + > + { + if (loading) event.preventDefault(); + }} + onPointerDownOutside={(event) => { + if (loading) event.preventDefault(); + }} + className="gap-4 p-6" + > + + {t('setting.confirm-delete')} + + + + {t('setting.are-you-sure-you-want-to-delete')}{' '} + {target.mcp_name}? + + +
    + + +
    +
    +
    + ); +} diff --git a/src/pages/Connectors/components/MCPEnvDialog.tsx b/src/components/Settings/Connectors/components/MCPEnvDialog.tsx similarity index 88% rename from src/pages/Connectors/components/MCPEnvDialog.tsx rename to src/components/Settings/Connectors/components/MCPEnvDialog.tsx index ca63cba9..61c496f7 100644 --- a/src/pages/Connectors/components/MCPEnvDialog.tsx +++ b/src/components/Settings/Connectors/components/MCPEnvDialog.tsx @@ -54,18 +54,12 @@ export async function google_check(apiKey: string, searchEngineId: string) { if ('items' in data) { return { success: true, - message: 'Google key is valid ✅', sample: data.items[0], }; - } else { - return { - success: false, - message: 'Google key invalid ❌', - error: data.error, - }; } + return { success: false, error: data.error }; } catch (err: any) { - return { success: false, message: `Google check failed: ${err.message}` }; + return { success: false, error: err }; } } @@ -90,14 +84,12 @@ export async function exa_check(apiKey: string) { if ('results' in data) { return { success: true, - message: 'Exa key is valid ✅', sample: data.results[0], }; - } else { - return { success: false, message: 'Exa key invalid ❌', error: data }; } + return { success: false, error: data }; } catch (err: any) { - return { success: false, message: `Exa check failed: ${err.message}` }; + return { success: false, error: err }; } } @@ -194,7 +186,9 @@ export const MCPEnvDialog: FC = ({ if (field?.required && (!field.value || field.value.trim() === '')) { updatedEnvValues[key] = { ...field, - error: `${getTitleContent(key)} is required`, + error: t('connectors.field-required', { + field: getTitleContent(key), + }), }; hasErrors = true; } @@ -209,11 +203,11 @@ export const MCPEnvDialog: FC = ({ const getTitleContent = (key: string) => { if (key === 'SEARCH_ENGINE_ID') { - return 'Search Engine ID'; + return t('connectors.search-engine-id'); } else if (key === 'GOOGLE_API_KEY') { - return 'Google API Key'; + return t('connectors.google-api-key'); } else if (key === 'EXA_API_KEY') { - return 'Exa API Key'; + return t('connectors.exa-api-key'); } return key; }; @@ -242,8 +236,9 @@ export const MCPEnvDialog: FC = ({ env['SEARCH_ENGINE_ID'] ); if (!result.success) { - setFieldError('GOOGLE_API_KEY', result.message); - setFieldError('SEARCH_ENGINE_ID', result.message); + const errorMessage = t('connectors.google-check-failed'); + setFieldError('GOOGLE_API_KEY', errorMessage); + setFieldError('SEARCH_ENGINE_ID', errorMessage); setIsValidating(false); return; } @@ -253,7 +248,7 @@ export const MCPEnvDialog: FC = ({ if (env['EXA_API_KEY']) { const result = await exa_check(env['EXA_API_KEY']); if (!result.success) { - setFieldError('EXA_API_KEY', result.message); + setFieldError('EXA_API_KEY', t('connectors.exa-check-failed')); setIsValidating(false); return; } @@ -308,19 +303,19 @@ export const MCPEnvDialog: FC = ({ })} /> -
    -
    +
    +
    {getCategoryIcon(activeMcp?.category?.name)}
    -
    + {activeMcp?.name} -
    +
    {getGithubRepoName(activeMcp?.home_page) && (
    github = ({ verticalAlign: 'middle', }} /> - + {getGithubRepoName(activeMcp?.home_page)}
    @@ -337,7 +332,7 @@ export const MCPEnvDialog: FC = ({
    -
    +
    {Object.keys(activeMcp?.install_command?.env || {}).map((key) => { const getNoteContent = () => { let noteContent = envValues[key]?.tip || ''; @@ -347,7 +342,7 @@ export const MCPEnvDialog: FC = ({ } else if (key === 'GOOGLE_API_KEY') { noteContent += ` ${t('setting.get-it-from')}: https://console.cloud.google.com/apis/credentials`; } else if (key === 'EXA_API_KEY') { - noteContent += ` ${t('setting.get-it-from')}: https://exa.ai (Optional)`; + noteContent += ` ${t('setting.get-it-from')}: https://exa.ai (${t('connectors.optional')})`; } return noteContent; @@ -362,7 +357,9 @@ export const MCPEnvDialog: FC = ({ required={envValues[key]?.required || false} state={envValues[key]?.error ? 'error' : 'default'} type={showKeys[key] ? 'text' : 'password'} - placeholder={`Enter ${getTitleContent(key)}`} + placeholder={t('connectors.enter-field', { + field: getTitleContent(key), + })} value={envValues[key]?.value || ''} backIcon={ showKeys[key] ? ( @@ -388,7 +385,7 @@ export const MCPEnvDialog: FC = ({ cancelButtonVariant="ghost" showConfirmButton confirmButtonText={ - isValidating ? 'Validating...' : t('setting.connect') + isValidating ? t('setting.verifying') : t('setting.connect') } onConfirm={handleConfigureMcpEnvSetting} confirmButtonVariant="primary" diff --git a/src/pages/Connectors/components/types.ts b/src/components/Settings/Connectors/components/types.ts similarity index 100% rename from src/pages/Connectors/components/types.ts rename to src/components/Settings/Connectors/components/types.ts diff --git a/src/pages/Connectors/components/utils.ts b/src/components/Settings/Connectors/components/utils.ts similarity index 100% rename from src/pages/Connectors/components/utils.ts rename to src/components/Settings/Connectors/components/utils.ts diff --git a/src/pages/Connectors/index.tsx b/src/components/Settings/Connectors/index.tsx similarity index 100% rename from src/pages/Connectors/index.tsx rename to src/components/Settings/Connectors/index.tsx diff --git a/src/pages/Browser/Cookies.tsx b/src/components/Settings/Cookies/index.tsx similarity index 51% rename from src/pages/Browser/Cookies.tsx rename to src/components/Settings/Cookies/index.tsx index d934c0b1..8d75d5d4 100644 --- a/src/pages/Browser/Cookies.tsx +++ b/src/components/Settings/Cookies/index.tsx @@ -18,9 +18,12 @@ import { Button } from '@/components/ui/button'; import { useHost } from '@/host'; import { SITE_URL } from '@/lib'; import { Cookie, Plus, RefreshCw, Trash2 } from 'lucide-react'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; +import SettingsPage from '../SettingsPage'; +import SettingsPageLoading from '../SettingsPageLoading'; +import SettingsSection from '../SettingsSection'; interface CookieDomain { domain: string; @@ -39,12 +42,12 @@ export default function Cookies() { const electronAPI = host?.electronAPI; const { t } = useTranslation(); const [loginLoading, setLoginLoading] = useState(false); - const [cookiesLoading, setCookiesLoading] = useState(false); + const [cookiesLoading, setCookiesLoading] = useState(true); const [cookieDomains, setCookieDomains] = useState([]); const [deletingDomain, setDeletingDomain] = useState(null); const [deletingAll, setDeletingAll] = useState(false); const [showRestartDialog, setShowRestartDialog] = useState(false); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [, setHasUnsavedChanges] = useState(false); const getMainDomain = (domain: string): string => { const cleanDomain = domain.startsWith('.') ? domain.substring(1) : domain; @@ -80,10 +83,6 @@ export default function Cookies() { .sort((a, b) => a.mainDomain.localeCompare(b.mainDomain)); }; - useEffect(() => { - handleLoadCookies(); - }, []); - const handleBrowserLogin = async () => { setLoginLoading(true); try { @@ -94,7 +93,7 @@ export default function Cookies() { const response = await fetchPost('/browser/login'); if (response) { - toast.success('Browser opened successfully for login'); + toast.success(t('layout.browser-opened')); const checkInterval = setInterval(async () => { try { const statusResponse = await fetchGet('/browser/status'); @@ -112,7 +111,7 @@ export default function Cookies() { if (newCookieCount > currentCookieCount) { const addedCount = newCookieCount - currentCookieCount; toast.success( - `Added ${addedCount} cookie${addedCount !== 1 ? 's' : ''}` + t('layout.cookies-added', { count: addedCount }) ); setHasUnsavedChanges(true); setShowRestartDialog(true); @@ -130,13 +129,13 @@ export default function Cookies() { }, 500); } } catch (error: any) { - toast.error(error?.message || 'Failed to open browser'); + toast.error(error?.message || t('layout.failed-to-open-browser')); } finally { setLoginLoading(false); } }; - const handleLoadCookies = async () => { + const handleLoadCookies = useCallback(async () => { setCookiesLoading(true); try { const response = await fetchGet('/browser/cookies'); @@ -147,12 +146,16 @@ export default function Cookies() { setCookieDomains([]); } } catch (error: any) { - toast.error(error?.message || 'Failed to load cookies'); + toast.error(error?.message || t('layout.failed-to-load-cookies')); setCookieDomains([]); } finally { setCookiesLoading(false); } - }; + }, [t]); + + useEffect(() => { + void handleLoadCookies(); + }, [handleLoadCookies]); const handleDeleteMainDomain = async ( mainDomain: string, @@ -165,7 +168,9 @@ export default function Cookies() { ); await Promise.all(deletePromises); - toast.success(`Deleted cookies for ${mainDomain} and all subdomains`); + toast.success( + t('layout.deleted-cookies-for-domain', { domain: mainDomain }) + ); const domainsToRemove = new Set(subdomains.map((item) => item.domain)); setCookieDomains((prev) => prev.filter((item) => !domainsToRemove.has(item.domain)) @@ -175,7 +180,10 @@ export default function Cookies() { setShowRestartDialog(true); } catch (error: any) { toast.error( - error?.message || `Failed to delete cookies for ${mainDomain}` + error?.message || + t('layout.failed-to-delete-cookies-for-domain', { + domain: mainDomain, + }) ); } finally { setDeletingDomain(null); @@ -186,13 +194,13 @@ export default function Cookies() { setDeletingAll(true); try { await fetchDelete('/browser/cookies'); - toast.success('Deleted all cookies'); + toast.success(t('layout.deleted-all-cookies')); setCookieDomains([]); setHasUnsavedChanges(true); setShowRestartDialog(true); } catch (error: any) { - toast.error(error?.message || 'Failed to delete all cookies'); + toast.error(error?.message || t('layout.failed-to-delete-all-cookies')); } finally { setDeletingAll(false); } @@ -202,7 +210,7 @@ export default function Cookies() { if (electronAPI?.restartApp) { electronAPI.restartApp(); } else { - toast.error('Restart function not available'); + toast.error(t('layout.restart-not-available')); } }; @@ -212,140 +220,137 @@ export default function Cookies() { }; return ( -
    + setShowRestartDialog(false)} onConfirm={handleConfirmRestart} - title="Cookies Updated" - message="Cookies have been updated. Would you like to restart the application to use the new cookies?" - confirmText="Yes, Restart" - cancelText="No, Add More" + title={t('layout.cookies-updated')} + message={t('layout.cookies-updated-message')} + confirmText={t('layout.yes-restart')} + cancelText={t('layout.no-add-more')} confirmVariant="information" /> -
    -
    - {t('layout.browser-cookie-management')} -
    -
    + + + {t('layout.browser-cookies-description')} + +
    +
    +
    + {cookieDomains.length > 0 && ( + + {groupDomainsByMain(cookieDomains).length} + + )} +
    -
    -
    -
    - {t('layout.browser-cookies-description')} -
    -
    -
    -
    -
    - {t('layout.cookie-domains')} -
    - {cookieDomains.length > 0 && ( -
    - {groupDomainsByMain(cookieDomains).length} -
    - )} -
    - -
    - {cookieDomains.length > 0 && ( - - )} +
    + {cookieDomains.length > 0 && ( - -
    + )} + +
    - - {cookieDomains.length > 0 ? ( -
    - {groupDomainsByMain(cookieDomains).map((group, index) => ( -
    -
    - - {group.mainDomain} - - - {group.totalCookies} Cookie - {group.totalCookies !== 1 ? 's' : ''} - -
    - -
    - ))} -
    - ) : ( -
    - -
    - {t('layout.no-cookies-saved-yet')} -
    -

    - {t('layout.no-cookies-saved-yet-description')} -

    -
    - )}
    -
    -
    - For more information, check out our - - {t('layout.privacy-policy')} - + {cookiesLoading && cookieDomains.length === 0 ? ( + + ) : cookieDomains.length > 0 ? ( +
    + {groupDomainsByMain(cookieDomains).map((group, index) => ( +
    +
    + + {group.mainDomain} + + + {t('layout.cookie-count', { + count: group.totalCookies, + })} + +
    + +
    + ))} +
    + ) : ( +
    + + + {t('layout.no-cookies-saved-yet')} + + + {t('layout.no-cookies-saved-yet-description')} + +
    + )}
    -
    -
    + + + + {t('layout.for-more-info')} + + {t('layout.privacy-policy')} + + + ); } diff --git a/src/components/Settings/Dialog/SettingsDialog.tsx b/src/components/Settings/Dialog/SettingsDialog.tsx new file mode 100644 index 00000000..dec80cee --- /dev/null +++ b/src/components/Settings/Dialog/SettingsDialog.tsx @@ -0,0 +1,113 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { useSettingsDialogStore } from '@/store/settingsDialogStore'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'; +import { useTranslation } from 'react-i18next'; +import { SettingsHeaderProvider } from '../SettingsHeaderContext'; +import SettingsDialogContent from './SettingsDialogContent'; +import SettingsDialogHeader from './SettingsDialogHeader'; +import SettingsDialogOverlay from './SettingsDialogOverlay'; +import SettingsDialogSidebar from './SettingsDialogSidebar'; + +export default function SettingsDialog() { + const { t } = useTranslation(); + const shouldReduceMotion = useReducedMotion(); + const isOpen = useSettingsDialogStore((state) => state.isOpen); + const activeSection = useSettingsDialogStore((state) => state.activeSection); + const closeSettings = useSettingsDialogStore((state) => state.closeSettings); + const setActiveSection = useSettingsDialogStore( + (state) => state.setActiveSection + ); + + return ( + { + if (!open) closeSettings(); + }} + > + +
    + + {isOpen ? ( + <> + + + + + + + + {t('layout.settings', { defaultValue: 'Settings' })} + + + + + {t('layout.settings-dialog-description', { + defaultValue: + 'Configure Workspace, Device, and application settings.', + })} + + + + +
    + + + + +
    +
    +
    + + ) : null} +
    +
    +
    +
    + ); +} diff --git a/src/components/Settings/Dialog/SettingsDialogContent.tsx b/src/components/Settings/Dialog/SettingsDialogContent.tsx new file mode 100644 index 00000000..dfe18889 --- /dev/null +++ b/src/components/Settings/Dialog/SettingsDialogContent.tsx @@ -0,0 +1,171 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import type { SettingsSectionId } from '@/store/settingsDialogStore'; +import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'; +import { LoaderCircle } from 'lucide-react'; +import { lazy, type RefObject, Suspense, useLayoutEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import SettingsContentShell from '../SettingsContentShell'; +import WorkspaceProfileSettings from '../WorkspaceProfile'; + +const sectionLoaders = { + models: () => import('../Models'), + skills: () => import('../Skills'), + 'sub-agents': () => import('../SubAgents'), + memory: () => import('../Memory'), + connectors: () => import('../Connectors'), + channels: () => import('../Channels'), + 'browser-connections': () => import('../Browser'), + 'browser-plugins': () => import('../Extension'), + cookies: () => import('../Cookies'), + general: () => import('../General'), + appearance: () => import('../Appearance'), + privacy: () => import('../Privacy'), +} satisfies Partial< + Record Promise<{ default: React.ComponentType }>> +>; + +const Models = lazy(sectionLoaders.models); +const Skills = lazy(sectionLoaders.skills); +const SubAgents = lazy(sectionLoaders['sub-agents']); +const Memory = lazy(sectionLoaders.memory); +const Connectors = lazy(sectionLoaders.connectors); +const Channels = lazy(sectionLoaders.channels); +const BrowserConnections = lazy(sectionLoaders['browser-connections']); +const BrowserPlugins = lazy(sectionLoaders['browser-plugins']); +const Cookies = lazy(sectionLoaders.cookies); +const General = lazy(sectionLoaders.general); +const Appearance = lazy(sectionLoaders.appearance); +const Privacy = lazy(sectionLoaders.privacy); + +export function preloadSettingsSection(section: SettingsSectionId) { + void sectionLoaders[section as keyof typeof sectionLoaders]?.().catch( + () => undefined + ); +} + +interface SettingsDialogContentProps { + activeSection: SettingsSectionId; +} + +function SectionFallback() { + const { t } = useTranslation(); + + return ( +
    + + + {t('setting.loading', { defaultValue: 'Loading settings' })} + +
    + ); +} + +function SettingsSection({ section }: { section: SettingsSectionId }) { + switch (section) { + case 'workspace-profile': + return ; + case 'models': + return ; + case 'sub-agents': + return ; + case 'connectors': + return ; + case 'skills': + return ; + case 'channels': + return ; + case 'memory': + return ; + case 'browser-connections': + return ; + case 'browser-plugins': + return ; + case 'cookies': + return ; + case 'general': + return ; + case 'appearance': + return ; + case 'privacy': + return ; + } +} + +interface AnimatedSettingsSectionProps { + section: SettingsSectionId; + scrollRef: RefObject; + shouldReduceMotion: boolean | null; +} + +function AnimatedSettingsSection({ + section, + scrollRef, + shouldReduceMotion, +}: AnimatedSettingsSectionProps) { + useLayoutEffect(() => { + if (scrollRef.current) scrollRef.current.scrollTop = 0; + }, [scrollRef]); + + return ( + + }> + + + + ); +} + +export default function SettingsDialogContent({ + activeSection, +}: SettingsDialogContentProps) { + const scrollRef = useRef(null); + const shouldReduceMotion = useReducedMotion(); + + return ( + + + + + + ); +} diff --git a/src/components/Settings/Dialog/SettingsDialogHeader.tsx b/src/components/Settings/Dialog/SettingsDialogHeader.tsx new file mode 100644 index 00000000..85f28961 --- /dev/null +++ b/src/components/Settings/Dialog/SettingsDialogHeader.tsx @@ -0,0 +1,80 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { cn } from '@/lib/utils'; +import type { SettingsSectionId } from '@/store/settingsDialogStore'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { ArrowLeft, X } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { useSettingsHeader } from '../SettingsHeaderContext'; +import { getSettingsNavigationItem } from './settingsNavigation'; + +interface SettingsDialogHeaderProps { + activeSection: SettingsSectionId; +} + +export default function SettingsDialogHeader({ + activeSection, +}: SettingsDialogHeaderProps) { + const { t } = useTranslation(); + const { headerOverride, setHeaderActionsElement } = useSettingsHeader(); + const item = getSettingsNavigationItem(activeSection); + const title = + headerOverride?.title ?? + t(item.labelKey, { defaultValue: item.defaultLabel }); + + return ( +
    +
    + {headerOverride?.onBack ? ( + <> + + + {title} + + + ) : headerOverride?.hideTitle ? null : ( + + {title} + + )} + +
    +
    + + + + +
    + ); +} diff --git a/src/hooks/use-mobile.tsx b/src/components/Settings/Dialog/SettingsDialogOverlay.tsx similarity index 57% rename from src/hooks/use-mobile.tsx rename to src/components/Settings/Dialog/SettingsDialogOverlay.tsx index 9f20bae0..82f54c86 100644 --- a/src/hooks/use-mobile.tsx +++ b/src/components/Settings/Dialog/SettingsDialogOverlay.tsx @@ -12,24 +12,21 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +import { cn } from '@/lib/utils'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; import * as React from 'react'; -const MOBILE_BREAKPOINT = 768; +const SettingsDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); -export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState( - undefined - ); +SettingsDialogOverlay.displayName = 'SettingsDialogOverlay'; - React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); - const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); - }; - mql.addEventListener('change', onChange); - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); - return () => mql.removeEventListener('change', onChange); - }, []); - - return !!isMobile; -} +export default SettingsDialogOverlay; diff --git a/src/components/Settings/Dialog/SettingsDialogSidebar.tsx b/src/components/Settings/Dialog/SettingsDialogSidebar.tsx new file mode 100644 index 00000000..43455c0b --- /dev/null +++ b/src/components/Settings/Dialog/SettingsDialogSidebar.tsx @@ -0,0 +1,84 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { cn } from '@/lib/utils'; +import type { SettingsSectionId } from '@/store/settingsDialogStore'; +import { useTranslation } from 'react-i18next'; +import { preloadSettingsSection } from './SettingsDialogContent'; +import { SETTINGS_NAVIGATION } from './settingsNavigation'; + +interface SettingsDialogSidebarProps { + activeSection: SettingsSectionId; + onSectionChange: (section: SettingsSectionId) => void; +} + +export default function SettingsDialogSidebar({ + activeSection, + onSectionChange, +}: SettingsDialogSidebarProps) { + const { t } = useTranslation(); + + return ( + + ); +} diff --git a/src/components/Settings/Dialog/settingsNavigation.ts b/src/components/Settings/Dialog/settingsNavigation.ts new file mode 100644 index 00000000..ac48c07e --- /dev/null +++ b/src/components/Settings/Dialog/settingsNavigation.ts @@ -0,0 +1,158 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import type { + SettingsScope, + SettingsSectionId, +} from '@/store/settingsDialogStore'; +import type { LucideIcon } from 'lucide-react'; +import { + Bot, + Brain, + Cable, + Cookie, + Eye, + Globe, + LayoutGrid, + Palette, + Puzzle, + Radio, + Settings2, + UsersRound, + WandSparkles, +} from 'lucide-react'; + +export interface SettingsNavigationItem { + id: SettingsSectionId; + labelKey: string; + defaultLabel: string; + icon: LucideIcon; +} + +export interface SettingsNavigationGroup { + scope: SettingsScope; + labelKey: string; + defaultLabel: string; + items: SettingsNavigationItem[]; +} + +export const SETTINGS_NAVIGATION: SettingsNavigationGroup[] = [ + { + scope: 'workspace', + labelKey: 'layout.workspace', + defaultLabel: 'Workspace', + items: [ + { + id: 'workspace-profile', + labelKey: 'layout.workspace-profile', + defaultLabel: 'Workspace profile', + icon: LayoutGrid, + }, + { + id: 'models', + labelKey: 'setting.models', + defaultLabel: 'Models', + icon: Bot, + }, + { + id: 'sub-agents', + labelKey: 'agents.sub-agents', + defaultLabel: 'Sub-agents', + icon: UsersRound, + }, + { + id: 'connectors', + labelKey: 'layout.connectors', + defaultLabel: 'Connectors', + icon: Cable, + }, + { + id: 'skills', + labelKey: 'agents.skills', + defaultLabel: 'Skills', + icon: WandSparkles, + }, + { + id: 'channels', + labelKey: 'layout.channels', + defaultLabel: 'Channels', + icon: Radio, + }, + { + id: 'memory', + labelKey: 'agents.memory', + defaultLabel: 'Memory', + icon: Brain, + }, + ], + }, + { + scope: 'device', + labelKey: 'layout.device', + defaultLabel: 'Device', + items: [ + { + id: 'browser-connections', + labelKey: 'layout.browser', + defaultLabel: 'Browser', + icon: Globe, + }, + { + id: 'browser-plugins', + labelKey: 'layout.browser-extension', + defaultLabel: 'Extension', + icon: Puzzle, + }, + { + id: 'cookies', + labelKey: 'layout.browser-cookie', + defaultLabel: 'Cookies', + icon: Cookie, + }, + ], + }, + { + scope: 'settings', + labelKey: 'layout.settings', + defaultLabel: 'Settings', + items: [ + { + id: 'general', + labelKey: 'setting.general', + defaultLabel: 'General', + icon: Settings2, + }, + { + id: 'appearance', + labelKey: 'setting.appearance-tab', + defaultLabel: 'Appearance', + icon: Palette, + }, + { + id: 'privacy', + labelKey: 'setting.privacy', + defaultLabel: 'Privacy', + icon: Eye, + }, + ], + }, +]; + +export function getSettingsNavigationItem(section: SettingsSectionId) { + for (const group of SETTINGS_NAVIGATION) { + const item = group.items.find((candidate) => candidate.id === section); + if (item) return item; + } + return SETTINGS_NAVIGATION[0].items[0]; +} diff --git a/src/components/Settings/Extension/index.tsx b/src/components/Settings/Extension/index.tsx new file mode 100644 index 00000000..4b73bf59 --- /dev/null +++ b/src/components/Settings/Extension/index.tsx @@ -0,0 +1,39 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { Puzzle } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import SettingsPage from '../SettingsPage'; +import SettingsSection from '../SettingsSection'; + +export default function Extension() { + const { t } = useTranslation(); + + return ( + + + +
    + {t('layout.coming-soon')} +
    + + {t('layout.browser-plugins-description')} + +
    +
    + ); +} diff --git a/src/pages/Setting/General.tsx b/src/components/Settings/General/index.tsx similarity index 75% rename from src/pages/Setting/General.tsx rename to src/components/Settings/General/index.tsx index 3379a9c8..a59f5a25 100644 --- a/src/pages/Setting/General.tsx +++ b/src/components/Settings/General/index.tsx @@ -34,8 +34,18 @@ import { } from '@/components/ui/select'; import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; import { useHost } from '@/host'; +import SettingsPage from '../SettingsPage'; +import SettingsSection from '../SettingsSection'; -export default function SettingGeneral() { +type GeneralSettingsSection = 'all' | 'profile' | 'language' | 'network-proxy'; + +interface SettingGeneralProps { + section?: GeneralSettingsSection; +} + +export default function SettingGeneral({ + section = 'all', +}: SettingGeneralProps) { const { t } = useTranslation(); const host = useHost(); const authStore = useAuthStore(); @@ -57,6 +67,7 @@ export default function SettingGeneral() { // Proxy configuration state const [proxyUrl, setProxyUrl] = useState(''); + const [proxyLoading, setProxyLoading] = useState(true); const [isProxySaving, setIsProxySaving] = useState(false); const [proxyNeedsRestart, setProxyNeedsRestart] = useState(false); @@ -110,18 +121,20 @@ export default function SettingGeneral() { useEffect(() => { // Load proxy configuration from global env const loadProxyConfig = async () => { - if (host?.electronAPI?.readGlobalEnv) { - try { + try { + if (host?.electronAPI?.readGlobalEnv) { const result = await host.electronAPI.readGlobalEnv('HTTP_PROXY'); if (result?.value) { setProxyUrl(result.value); } - } catch (_error) { - console.log('No proxy configured'); } + } catch (_error) { + console.log('No proxy configured'); + } finally { + setProxyLoading(false); } }; - loadProxyConfig(); + void loadProxyConfig(); }, [host]); // Save proxy configuration @@ -180,25 +193,15 @@ export default function SettingGeneral() { }; return ( -
    - {/* Header Section */} -
    -
    -
    -
    - {t('setting.general')} -
    -
    -
    -
    - {/* Content Section */} -
    - {/* Profile Section */} -
    + + {/* Profile Section */} + {(section === 'all' || section === 'profile') && ( +
    -
    - {t('setting.profile')} -
    -
    +
    + )} - {/* Language Section */} -
    -
    -
    - {t('setting.language')} -
    -
    + {/* Language Section */} + {(section === 'all' || section === 'language') && ( + -
    + + )} - {/* Network Proxy Section */} -
    + {/* Network Proxy Section */} + {(section === 'all' || section === 'network-proxy') && ( +
    -
    - {t('setting.network-proxy')} -
    -
    +
    {t('setting.network-proxy-description')}
    @@ -294,6 +310,7 @@ export default function SettingGeneral() { }} className="flex-1" size="default" + disabled={proxyLoading} note={ proxyNeedsRestart ? t('setting.proxy-restart-hint') : undefined } @@ -306,18 +323,20 @@ export default function SettingGeneral() { ? () => host?.electronAPI?.restartApp() : handleSaveProxy } - disabled={!proxyNeedsRestart && isProxySaving} + disabled={proxyLoading || (!proxyNeedsRestart && isProxySaving)} > - {proxyNeedsRestart - ? t('setting.restart-to-apply') - : isProxySaving - ? t('setting.saving') - : t('setting.save')} + {proxyLoading + ? t('setting.loading') + : proxyNeedsRestart + ? t('setting.restart-to-apply') + : isProxySaving + ? t('setting.saving') + : t('setting.save')} } /> -
    -
    -
    + + )} + ); } diff --git a/src/components/Settings/Memory/index.tsx b/src/components/Settings/Memory/index.tsx new file mode 100644 index 00000000..e95b9a41 --- /dev/null +++ b/src/components/Settings/Memory/index.tsx @@ -0,0 +1,41 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { Brain } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import SettingsPage from '../SettingsPage'; +import SettingsSection from '../SettingsSection'; + +export default function Memory() { + const { t } = useTranslation(); + + return ( + + +
    + +
    + + {t('layout.coming-soon')} + + + {t('agents.memory-coming-soon-description')} + +
    +
    + ); +} diff --git a/src/pages/Agents/ConfigModelCard.tsx b/src/components/Settings/Models/ConfigModelCard.tsx similarity index 100% rename from src/pages/Agents/ConfigModelCard.tsx rename to src/components/Settings/Models/ConfigModelCard.tsx diff --git a/src/pages/Agents/components/ProviderModelCombobox.tsx b/src/components/Settings/Models/components/ProviderModelCombobox.tsx similarity index 83% rename from src/pages/Agents/components/ProviderModelCombobox.tsx rename to src/components/Settings/Models/components/ProviderModelCombobox.tsx index 9f01c737..04accc95 100644 --- a/src/pages/Agents/components/ProviderModelCombobox.tsx +++ b/src/components/Settings/Models/components/ProviderModelCombobox.tsx @@ -90,25 +90,20 @@ export function ProviderModelCombobox({ const selectDisabled = disabled || (!hasAnyModels && !orphanValue); const emptyMessage = loading - ? t('setting.loading', { defaultValue: 'Loading...' }) + ? t('setting.loading-models') : disabled - ? (disabledReason ?? - t('setting.enter-api-key-first', { - defaultValue: 'Enter API Key first.', - })) - : t('setting.click-refresh-to-load-models', { - defaultValue: 'Click refresh to load models.', - }); + ? (disabledReason ?? t('setting.enter-api-key-first')) + : t('setting.click-refresh-to-load-models'); return (
    {title ? ( -
    + {title} -
    + ) : null} -
    +