diff --git a/.gitignore b/.gitignore index e898998b1c9..7b3a5945f50 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,7 @@ CLAUDE.local.md coverage .vitest-cache vitest.config.*.timestamp-* +.context/vitest-temp/ # TypeScript incremental build .tsbuildinfo diff --git a/src/renderer/src/Router.tsx b/src/renderer/src/Router.tsx index 5390e86d0a5..f681499c1da 100644 --- a/src/renderer/src/Router.tsx +++ b/src/renderer/src/Router.tsx @@ -9,6 +9,7 @@ import { ErrorBoundary } from './components/ErrorBoundary' import TabsContainer from './components/Tab/TabContainer' import NavigationHandler from './handler/NavigationHandler' import { useNavbarPosition } from './hooks/useSettings' +import AgentPage from './pages/agents/AgentPage' import CodeToolsPage from './pages/code/CodeToolsPage' import FilesPage from './pages/files/FilesPage' import HomePage from './pages/home/HomePage' @@ -31,6 +32,7 @@ const Router: FC = () => { } /> + } /> } /> } /> } /> diff --git a/src/renderer/src/pages/home/Tabs/components/AddButton.tsx b/src/renderer/src/components/AddButton.tsx similarity index 100% rename from src/renderer/src/pages/home/Tabs/components/AddButton.tsx rename to src/renderer/src/components/AddButton.tsx diff --git a/src/renderer/src/components/Popups/agent/AgentModal.tsx b/src/renderer/src/components/Popups/agent/AgentModal.tsx index 5e1d97bbd8b..24b7b117a19 100644 --- a/src/renderer/src/components/Popups/agent/AgentModal.tsx +++ b/src/renderer/src/components/Popups/agent/AgentModal.tsx @@ -7,7 +7,7 @@ import { permissionModeCards } from '@renderer/config/agent' import { isWin } from '@renderer/config/constant' import { useAgents } from '@renderer/hooks/agents/useAgents' import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent' -import SelectAgentBaseModelButton from '@renderer/pages/home/components/SelectAgentBaseModelButton' +import SelectAgentBaseModelButton from '@renderer/pages/agents/components/SelectAgentBaseModelButton' import type { AddAgentForm, AgentEntity, diff --git a/src/renderer/src/components/Tab/TabContainer.tsx b/src/renderer/src/components/Tab/TabContainer.tsx index 8b11f4f5dda..0ce426af117 100644 --- a/src/renderer/src/components/Tab/TabContainer.tsx +++ b/src/renderer/src/components/Tab/TabContainer.tsx @@ -28,6 +28,7 @@ import { LayoutGrid, Monitor, Moon, + MousePointerClick, NotepadText, Palette, Settings, @@ -86,9 +87,12 @@ const getTabIcon = ( return } + // TODO: Add TabId as type instead of string switch (tabId) { case 'home': return + case 'agents': + return case 'store': return case 'translate': @@ -240,35 +244,38 @@ const TabsContainer: React.FC = ({ children }) => { gap={'6px'} onSortEnd={onSortEnd} className="tabs-sortable" - renderItem={(tab) => ( - handleTabClick(tab)} - onAuxClick={(e) => { - if (e.button === 1 && tab.id !== 'home') { - e.preventDefault() - e.stopPropagation() - closeTab(tab.id) - } - }}> - - {tab.id && {getTabIcon(tab.id, minapps, minAppsCache)}} - {getTabTitle(tab.id)} - - {tab.id !== 'home' && ( - { + renderItem={(tab) => { + const isClosable = tab.id !== 'home' && tab.id !== 'agents' + return ( + handleTabClick(tab)} + onAuxClick={(e) => { + if (e.button === 1 && isClosable) { + e.preventDefault() e.stopPropagation() closeTab(tab.id) - }}> - - - )} - - )} + } + }}> + + {tab.id && {getTabIcon(tab.id, minapps, minAppsCache)}} + {getTabTitle(tab.id)} + + {isClosable && ( + { + e.stopPropagation() + closeTab(tab.id) + }}> + + + )} + + ) + }} /> diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx index 6d03e47ca8a..0c38a51c0d8 100644 --- a/src/renderer/src/components/app/Sidebar.tsx +++ b/src/renderer/src/components/app/Sidebar.tsx @@ -22,6 +22,7 @@ import { MessageSquare, Monitor, Moon, + MousePointerClick, NotepadText, Palette, Settings, @@ -126,10 +127,11 @@ const MainMenus: FC = () => { const { theme } = useTheme() const isRoute = (path: string): string => (pathname === path && !minappShow ? 'active' : '') - const isRoutes = (path: string): string => (pathname.startsWith(path) && !minappShow ? 'active' : '') + const isRoutes = (path: string): string => (pathname.startsWith(path) && path !== '/' && !minappShow ? 'active' : '') const iconMap = { assistants: , + agents: , store: , paintings: , translate: , @@ -143,6 +145,7 @@ const MainMenus: FC = () => { const pathMap = { assistants: '/', + agents: '/agents', store: '/store', paintings: `/paintings/${defaultPaintingProvider}`, translate: '/translate', diff --git a/src/renderer/src/config/sidebar.ts b/src/renderer/src/config/sidebar.ts index 815fe80a2f7..0fb01ecb9ad 100644 --- a/src/renderer/src/config/sidebar.ts +++ b/src/renderer/src/config/sidebar.ts @@ -6,6 +6,7 @@ import type { SidebarIcon } from '@renderer/types' */ export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [ 'assistants', + 'agents', 'store', 'paintings', 'translate', diff --git a/src/renderer/src/hooks/agents/useActiveAgent.ts b/src/renderer/src/hooks/agents/useActiveAgent.ts index f522ac7f28f..2c9649ac283 100644 --- a/src/renderer/src/hooks/agents/useActiveAgent.ts +++ b/src/renderer/src/hooks/agents/useActiveAgent.ts @@ -1,8 +1,24 @@ +import { useAppDispatch } from '@renderer/store' +import { setActiveAgentId as setActiveAgentIdAction } from '@renderer/store/runtime' +import { useCallback } from 'react' + import { useRuntime } from '../useRuntime' import { useAgent } from './useAgent' +import { useAgentSessionInitializer } from './useAgentSessionInitializer' export const useActiveAgent = () => { const { chat } = useRuntime() const { activeAgentId } = chat - return useAgent(activeAgentId) + const dispatch = useAppDispatch() + const { initializeAgentSession } = useAgentSessionInitializer() + + const setActiveAgentId = useCallback( + async (id: string) => { + dispatch(setActiveAgentIdAction(id)) + await initializeAgentSession(id) + }, + [dispatch, initializeAgentSession] + ) + + return { ...useAgent(activeAgentId), setActiveAgentId } } diff --git a/src/renderer/src/hooks/agents/useAgentSessionInitializer.ts b/src/renderer/src/hooks/agents/useAgentSessionInitializer.ts index 98609f5b29e..bd151a58d64 100644 --- a/src/renderer/src/hooks/agents/useAgentSessionInitializer.ts +++ b/src/renderer/src/hooks/agents/useAgentSessionInitializer.ts @@ -1,8 +1,8 @@ import { loggerService } from '@logger' import { useRuntime } from '@renderer/hooks/useRuntime' import { useAppDispatch } from '@renderer/store' -import { setActiveSessionIdAction, setActiveTopicOrSessionAction } from '@renderer/store/runtime' -import { useCallback, useEffect } from 'react' +import { setActiveSessionIdAction } from '@renderer/store/runtime' +import { useCallback, useEffect, useRef } from 'react' import { useAgentClient } from './useAgentClient' @@ -19,6 +19,10 @@ export const useAgentSessionInitializer = () => { const { chat } = useRuntime() const { activeAgentId, activeSessionIdMap } = chat + // Use a ref to keep the callback stable across activeSessionIdMap changes + const activeSessionIdMapRef = useRef(activeSessionIdMap) + activeSessionIdMapRef.current = activeSessionIdMap + /** * Initialize session for the given agent by loading its sessions * and setting the latest one as active @@ -28,11 +32,9 @@ export const useAgentSessionInitializer = () => { if (!agentId) return try { - // Check if this agent already has an active session - const currentSessionId = activeSessionIdMap[agentId] - if (currentSessionId) { - // Session already exists, just switch to session view - dispatch(setActiveTopicOrSessionAction('session')) + // Check if this agent has already been initialized (key exists in map) + if (agentId in activeSessionIdMapRef.current) { + // Already initialized, nothing to do return } @@ -46,19 +48,15 @@ export const useAgentSessionInitializer = () => { // Set the latest session as active dispatch(setActiveSessionIdAction({ agentId, sessionId: latestSession.id })) - dispatch(setActiveTopicOrSessionAction('session')) } else { - // No sessions exist, we might want to create one - // But for now, just switch to session view and let the Sessions component handle it - dispatch(setActiveTopicOrSessionAction('session')) + // Mark as initialized with no session (null vs undefined distinction) + dispatch(setActiveSessionIdAction({ agentId, sessionId: null })) } } catch (error) { logger.error('Failed to initialize agent session:', error as Error) - // Even if loading fails, switch to session view - dispatch(setActiveTopicOrSessionAction('session')) } }, - [client, dispatch, activeSessionIdMap] + [client, dispatch] ) /** @@ -66,13 +64,12 @@ export const useAgentSessionInitializer = () => { */ useEffect(() => { if (activeAgentId) { - // Check if we need to initialize this agent's session - const hasActiveSession = activeSessionIdMap[activeAgentId] - if (!hasActiveSession) { + // Check if we need to initialize this agent's session (key not yet in map) + if (!(activeAgentId in activeSessionIdMapRef.current)) { initializeAgentSession(activeAgentId) } } - }, [activeAgentId, activeSessionIdMap, initializeAgentSession]) + }, [activeAgentId, initializeAgentSession]) return { initializeAgentSession diff --git a/src/renderer/src/hooks/agents/useAgents.ts b/src/renderer/src/hooks/agents/useAgents.ts index 5c60ef10419..4738206d690 100644 --- a/src/renderer/src/hooks/agents/useAgents.ts +++ b/src/renderer/src/hooks/agents/useAgents.ts @@ -98,7 +98,7 @@ export const useAgents = () => { ) return { - agents: data ?? [], + agents: data, error, isLoading, addAgent, diff --git a/src/renderer/src/hooks/agents/useCreateDefaultSession.ts b/src/renderer/src/hooks/agents/useCreateDefaultSession.ts index 7f1a9bcc587..071e5e129bb 100644 --- a/src/renderer/src/hooks/agents/useCreateDefaultSession.ts +++ b/src/renderer/src/hooks/agents/useCreateDefaultSession.ts @@ -2,7 +2,7 @@ import { loggerService } from '@logger' import { useAgent } from '@renderer/hooks/agents/useAgent' import { useSessions } from '@renderer/hooks/agents/useSessions' import { useAppDispatch } from '@renderer/store' -import { setActiveSessionIdAction, setActiveTopicOrSessionAction } from '@renderer/store/runtime' +import { setActiveSessionIdAction } from '@renderer/store/runtime' import type { CreateSessionForm } from '@renderer/types' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -36,7 +36,6 @@ export const useCreateDefaultSession = (agentId: string | null) => { if (created) { dispatch(setActiveSessionIdAction({ agentId, sessionId: created.id })) - dispatch(setActiveTopicOrSessionAction('session')) } return created diff --git a/src/renderer/src/hooks/useApiServer.ts b/src/renderer/src/hooks/useApiServer.ts index bdf9f9d8de4..35c3b35466b 100644 --- a/src/renderer/src/hooks/useApiServer.ts +++ b/src/renderer/src/hooks/useApiServer.ts @@ -1,5 +1,6 @@ import { loggerService } from '@logger' import { useAppDispatch, useAppSelector } from '@renderer/store' +import { setApiServerRunningAction } from '@renderer/store/runtime' import { setApiServerEnabled as setApiServerEnabledAction } from '@renderer/store/settings' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -34,10 +35,17 @@ export const useApiServer = () => { const apiServerConfig = useAppSelector((state) => state.settings.apiServer) const dispatch = useAppDispatch() - // Initial state - no longer optimistic, wait for actual status - const [apiServerRunning, setApiServerRunning] = useState(false) + const apiServerRunning = useAppSelector((state) => state.runtime.apiServerRunning) + // Is checking the API server status const [apiServerLoading, setApiServerLoading] = useState(true) + const setApiServerRunning = useCallback( + (running: boolean) => { + dispatch(setApiServerRunningAction(running)) + }, + [dispatch] + ) + const setApiServerEnabled = useCallback( (enabled: boolean) => { dispatch(setApiServerEnabledAction(enabled)) @@ -59,7 +67,7 @@ export const useApiServer = () => { } finally { setApiServerLoading(false) } - }, [apiServerConfig.enabled, setApiServerEnabled]) + }, [apiServerConfig.enabled, setApiServerEnabled, setApiServerLoading, setApiServerRunning]) const startApiServer = useCallback(async () => { if (apiServerLoading) return @@ -78,7 +86,7 @@ export const useApiServer = () => { } finally { setApiServerLoading(false) } - }, [apiServerLoading, setApiServerEnabled, t]) + }, [apiServerLoading, setApiServerEnabled, setApiServerLoading, setApiServerRunning, t]) const stopApiServer = useCallback(async () => { if (apiServerLoading) return @@ -97,7 +105,7 @@ export const useApiServer = () => { } finally { setApiServerLoading(false) } - }, [apiServerLoading, setApiServerEnabled, t]) + }, [apiServerLoading, setApiServerEnabled, setApiServerLoading, setApiServerRunning, t]) const restartApiServer = useCallback(async () => { if (apiServerLoading) return @@ -116,7 +124,7 @@ export const useApiServer = () => { } finally { setApiServerLoading(false) } - }, [apiServerLoading, checkApiServerStatus, setApiServerEnabled, t]) + }, [apiServerLoading, checkApiServerStatus, setApiServerEnabled, setApiServerLoading, t]) useEffect(() => { checkApiServerStatus() diff --git a/src/renderer/src/i18n/label.ts b/src/renderer/src/i18n/label.ts index 2b3d7d34edb..e9a2b04a2fe 100644 --- a/src/renderer/src/i18n/label.ts +++ b/src/renderer/src/i18n/label.ts @@ -150,7 +150,8 @@ const titleKeyMap = { paintings: 'title.paintings', settings: 'title.settings', translate: 'title.translate', - openclaw: 'openclaw.title' + openclaw: 'openclaw.title', + agents: 'agent.sidebar_title' } as const export const getTitleLabel = (key: string): string => { @@ -181,6 +182,7 @@ export const getThemeModeLabel = (key: string): string => { const sidebarIconKeyMap = { assistants: 'assistants.title', + agents: 'agent.sidebar_title', store: 'assistants.presets.title', paintings: 'paintings.title', translate: 'translate.title', diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 9319868191c..ec138a27bda 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -38,6 +38,10 @@ "edit": { "title": "Edit Agent" }, + "empty": { + "description": "Create an agent to handle complex tasks with AI-powered tools", + "title": "No agents yet" + }, "get": { "error": { "failed": "Failed to get the agent.", @@ -295,6 +299,7 @@ } } }, + "sidebar_title": "Agents", "todo": { "panel": { "title": "{{completed}}/{{total}} tasks completed" @@ -395,7 +400,11 @@ } }, "warning": { - "enable_server": "Enable API Server to use agents." + "enable_and_start": "Enable & Start", + "enable_server": "Enable API Server to use agents.", + "enable_server_description": "The API server must be enabled for agents to work. You can enable it directly or configure it in settings.", + "server_not_running": "API Server is enabled but not running. Please check the server configuration.", + "server_not_running_description": "The API server needs to be running for agents to work. You can start it directly or check the settings." } }, "apiServer": { @@ -749,6 +758,7 @@ } }, "alerts": { + "create_agent": "Create an agent to get started", "create_session": "Create a session", "select_agent": "Select an agent" }, @@ -1385,6 +1395,7 @@ "selected": "Selected", "selectedItems": "Selected {{count}} items", "selectedMessages": "Selected {{count}} messages", + "sessions": "Sessions", "settings": "Settings", "sort": { "pinyin": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index a4aa644a6bf..3eeed76844e 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -38,6 +38,10 @@ "edit": { "title": "编辑 Agent" }, + "empty": { + "description": "创建一个 Agent,让 AI 帮你处理复杂任务", + "title": "还没有 Agent" + }, "get": { "error": { "failed": "获取智能体失败", @@ -295,6 +299,7 @@ } } }, + "sidebar_title": "智能体", "todo": { "panel": { "title": "已完成 {{completed}}/{{total}} 个任务" @@ -395,7 +400,11 @@ } }, "warning": { - "enable_server": "请启用 API 服务器以使用智能体功能" + "enable_and_start": "启用并启动", + "enable_server": "请启用 API 服务器以使用智能体功能", + "enable_server_description": "智能体功能需要启用 API 服务器。你可以直接启用,或前往设置页面进行配置。", + "server_not_running": "API 服务器已启用但未运行,请检查服务器配置。", + "server_not_running_description": "智能体功能需要 API 服务器运行。你可以直接启动服务器,或前往设置页面检查配置。" } }, "apiServer": { @@ -749,6 +758,7 @@ } }, "alerts": { + "create_agent": "请创建一个智能体以开始使用", "create_session": "请创建会话", "select_agent": "请选择智能体" }, @@ -1385,6 +1395,7 @@ "selected": "已选择", "selectedItems": "已选择 {{count}} 项", "selectedMessages": "选中 {{count}} 条消息", + "sessions": "会话", "settings": "设置", "sort": { "pinyin": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index f8b5dd61a88..560a3476391 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -38,6 +38,10 @@ "edit": { "title": "編輯 Agent" }, + "empty": { + "description": "建立 Agent 以透過 AI 驅動的工具處理複雜任務", + "title": "尚無 Agent" + }, "get": { "error": { "failed": "無法取得 Agent。", @@ -295,6 +299,7 @@ } } }, + "sidebar_title": "Agents", "todo": { "panel": { "title": "已完成 {{completed}}/{{total}} 個任務" @@ -395,7 +400,11 @@ } }, "warning": { - "enable_server": "啟用 API 伺服器以使用 Agent。" + "enable_and_start": "啟用並啟動", + "enable_server": "啟用 API 伺服器以使用 Agent。", + "enable_server_description": "智能體功能需要啟用 API 伺服器。你可以直接啟用,或前往設定頁面進行配置。", + "server_not_running": "API 伺服器已啟用但未執行。請檢查伺服器設定。", + "server_not_running_description": "智能體功能需要 API 伺服器運行。你可以直接啟動伺服器,或前往設定頁面檢查配置。" } }, "apiServer": { @@ -749,6 +758,7 @@ } }, "alerts": { + "create_agent": "建立 Agent 以開始", "create_session": "建立工作階段", "select_agent": "選擇一個智能體" }, @@ -1385,6 +1395,7 @@ "selected": "已選擇", "selectedItems": "已選擇 {{count}} 項", "selectedMessages": "已選取 {{count}} 則訊息", + "sessions": "會話", "settings": "設定", "sort": { "pinyin": { diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index 442f9816fce..935d50dc8d4 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -38,6 +38,10 @@ "edit": { "title": "Agent bearbeiten" }, + "empty": { + "description": "Erstellen Sie einen Agenten zur Bewältigung komplexer Aufgaben mit KI-gestützten Tools.", + "title": "Noch keine Agenten" + }, "get": { "error": { "failed": "Agent abrufen fehlgeschlagen", @@ -295,6 +299,7 @@ } } }, + "sidebar_title": "Agenten", "todo": { "panel": { "title": "{{completed}}/{{total}} Aufgaben abgeschlossen" @@ -395,7 +400,11 @@ } }, "warning": { - "enable_server": "Bitte aktivieren Sie den API-Server, um Agent-Funktionen zu verwenden" + "enable_and_start": "Aktivieren & Starten", + "enable_server": "Bitte aktivieren Sie den API-Server, um Agent-Funktionen zu verwenden", + "enable_server_description": "Der API-Server muss aktiviert sein, damit Agents funktionieren. Sie können ihn direkt aktivieren oder in den Einstellungen konfigurieren.", + "server_not_running": "API-Server ist aktiviert, läuft aber nicht. Bitte überprüfen Sie die Serverkonfiguration.", + "server_not_running_description": "Der API-Server muss laufen, damit die Agents funktionieren. Sie können ihn direkt starten oder die Einstellungen überprüfen." } }, "apiServer": { @@ -749,6 +758,7 @@ } }, "alerts": { + "create_agent": "Erstellen Sie einen Agenten, um loszulegen.", "create_session": "Erstelle eine Sitzung", "select_agent": "Wählen Sie einen Agenten" }, @@ -1385,6 +1395,7 @@ "selected": "Ausgewählt", "selectedItems": "{{count}} Elemente ausgewählt", "selectedMessages": "{{count}} Nachrichten ausgewählt", + "sessions": "Sitzungen", "settings": "Einstellungen", "sort": { "pinyin": { diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index e33d995a4e6..712ee5b5a11 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -38,6 +38,10 @@ "edit": { "title": "Επεξεργαστής Agent" }, + "empty": { + "description": "Δημιουργήστε έναν πράκτορα για τον χειρισμό σύνθετων εργασιών με εργαλεία που υποστηρίζονται από τεχνητή νοημοσύνη.", + "title": "Δεν υπάρχουν ακόμα πράκτορες" + }, "get": { "error": { "failed": "Αποτυχία λήψης του πράκτορα.", @@ -295,6 +299,7 @@ } } }, + "sidebar_title": "Πράκτορες", "todo": { "panel": { "title": "{{completed}}/{{total}} εργασίες ολοκληρώθηκαν" @@ -395,7 +400,11 @@ } }, "warning": { - "enable_server": "Ενεργοποίηση του διακομιστή API για χρήση πρακτόρων." + "enable_and_start": "Ενεργοποίηση & Εκκίνηση", + "enable_server": "Ενεργοποίηση του διακομιστή API για χρήση πρακτόρων.", + "enable_server_description": "Ο διακομιστής API πρέπει να είναι ενεργοποιημένος για να λειτουργήσουν οι πράκτορες. Μπορείτε να τον ενεργοποιήσετε απευθείας ή να τον ρυθμίσετε στις ρυθμίσεις.", + "server_not_running": "Ο API Server είναι ενεργοποιημένος αλλά δεν εκτελείται. Παρακαλούμε ελέγξτε τη διαμόρφωση του διακομιστή.", + "server_not_running_description": "Ο διακομιστής API πρέπει να βρίσκεται σε λειτουργία για να λειτουργήσουν οι πράκτορες. Μπορείτε να τον εκκινήσετε απευθείας ή να ελέγξετε τις ρυθμίσεις." } }, "apiServer": { @@ -749,6 +758,7 @@ } }, "alerts": { + "create_agent": "Δημιουργήστε έναν πράκτορα για να ξεκινήσετε.", "create_session": "Δημιουργία συνεδρίας", "select_agent": "Επιλέξτε έναν πράκτορα" }, @@ -1385,6 +1395,7 @@ "selected": "Επιλεγμένο", "selectedItems": "Επιλέχθηκαν {{count}} αντικείμενα", "selectedMessages": "Επιλέχθηκαν {{count}} μηνύματα", + "sessions": "Συνεδρίες", "settings": "Ρυθμίσεις", "sort": { "pinyin": { diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 3bc4005b7d8..c83f7063b08 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -38,6 +38,10 @@ "edit": { "title": "Agent de edición" }, + "empty": { + "description": "Crea un agente para gestionar tareas complejas con herramientas impulsadas por IA.", + "title": "Aún no hay agentes" + }, "get": { "error": { "failed": "No se pudo obtener el agente.", @@ -295,6 +299,7 @@ } } }, + "sidebar_title": "Agentes", "todo": { "panel": { "title": "{{completed}}/{{total}} tareas completadas" @@ -395,7 +400,11 @@ } }, "warning": { - "enable_server": "Habilitar el servidor API para usar agentes." + "enable_and_start": "Habilitar e iniciar", + "enable_server": "Habilitar el servidor API para usar agentes.", + "enable_server_description": "El servidor de la API debe estar habilitado para que los agentes funcionen. Puede habilitarlo directamente o configurarlo en los ajustes.", + "server_not_running": "El servidor API está habilitado pero no se está ejecutando. Por favor, compruebe la configuración del servidor.", + "server_not_running_description": "El servidor de la API debe estar en ejecución para que los agentes funcionen. Puede iniciarlo directamente o comprobar los ajustes." } }, "apiServer": { @@ -749,6 +758,7 @@ } }, "alerts": { + "create_agent": "Crea un agente para empezar", "create_session": "Crear una sesión", "select_agent": "Selecciona un agente" }, @@ -1385,6 +1395,7 @@ "selected": "Seleccionado", "selectedItems": "{{count}} elementos seleccionados", "selectedMessages": "{{count}} mensajes seleccionados", + "sessions": "Sesiones", "settings": "Configuración", "sort": { "pinyin": { diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 1821226c003..53c6c36be1b 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -38,6 +38,10 @@ "edit": { "title": "Éditer Agent" }, + "empty": { + "description": "Créez un agent pour gérer des tâches complexes avec des outils alimentés par l'IA", + "title": "Pas encore d'agents" + }, "get": { "error": { "failed": "Échec de l'obtention de l'agent.", @@ -295,6 +299,7 @@ } } }, + "sidebar_title": "Agents", "todo": { "panel": { "title": "{{completed}}/{{total}} tâches terminées" @@ -395,7 +400,11 @@ } }, "warning": { - "enable_server": "Permettre au serveur API d'utiliser des agents." + "enable_and_start": "Activer & Démarrer", + "enable_server": "Permettre au serveur API d'utiliser des agents.", + "enable_server_description": "Le serveur API doit être activé pour que les agents fonctionnent. Vous pouvez l'activer directement ou le configurer dans les paramètres.", + "server_not_running": "Le serveur API est activé mais ne fonctionne pas. Veuillez vérifier la configuration du serveur.", + "server_not_running_description": "Le serveur API doit être en cours d'exécution pour que les agents fonctionnent. Vous pouvez le démarrer directement ou vérifier les paramètres." } }, "apiServer": { @@ -749,6 +758,7 @@ } }, "alerts": { + "create_agent": "Créer un agent pour commencer", "create_session": "Créer une session", "select_agent": "Sélectionnez un agent" }, @@ -1385,6 +1395,7 @@ "selected": "Sélectionné", "selectedItems": "{{count}} éléments sélectionnés", "selectedMessages": "{{count}} messages sélectionnés", + "sessions": "Sessions", "settings": "Paramètres", "sort": { "pinyin": { diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index bf7f24adeb5..9af3a319591 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -38,6 +38,10 @@ "edit": { "title": "編集エージェント" }, + "empty": { + "description": "AI搭載ツールで複雑なタスクを処理するエージェントを作成する", + "title": "エージェントがまだありません" + }, "get": { "error": { "failed": "エージェントの取得に失敗しました。", @@ -295,6 +299,7 @@ } } }, + "sidebar_title": "エージェント", "todo": { "panel": { "title": "{{completed}}/{{total}} タスク完了" @@ -395,7 +400,11 @@ } }, "warning": { - "enable_server": "APIサーバーがエージェントを使用できるようにする。" + "enable_and_start": "有効化 & 開始", + "enable_server": "APIサーバーがエージェントを使用できるようにする。", + "enable_server_description": "エージェントを動作させるには、API サーバーを有効にする必要があります。直接有効にするか、設定から構成することができます。", + "server_not_running": "APIサーバーは有効になっていますが、実行されていません。サーバー設定を確認してください。", + "server_not_running_description": "エージェントを動作させるには、APIサーバーが稼働している必要があります。直接起動するか、設定を確認してください。" } }, "apiServer": { @@ -749,6 +758,7 @@ } }, "alerts": { + "create_agent": "開始するにはエージェントを作成してください", "create_session": "セッションを作成", "select_agent": "エージェントを選択してください" }, @@ -1385,6 +1395,7 @@ "selected": "選択済み", "selectedItems": "{{count}}件の項目を選択しました", "selectedMessages": "{{count}}件のメッセージを選択しました", + "sessions": "セッション", "settings": "設定", "sort": { "pinyin": { diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 8a331476462..a6c420ab62a 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -38,6 +38,10 @@ "edit": { "title": "Agent Editor" }, + "empty": { + "description": "Crie um agente para lidar com tarefas complexas com ferramentas baseadas em IA", + "title": "Ainda não há agentes" + }, "get": { "error": { "failed": "Falha ao obter o agente.", @@ -295,6 +299,7 @@ } } }, + "sidebar_title": "Agentes", "todo": { "panel": { "title": "{{completed}}/{{total}} tarefas concluídas" @@ -395,7 +400,11 @@ } }, "warning": { - "enable_server": "Ativar o Servidor de API para usar agentes." + "enable_and_start": "Ativar & Iniciar", + "enable_server": "Ativar o Servidor de API para usar agentes.", + "enable_server_description": "O servidor de API deve estar habilitado para que os agentes funcionem. Você pode habilitá-lo diretamente ou configurá-lo nas configurações.", + "server_not_running": "O Servidor API está ativado, mas não está em execução. Verifique a configuração do servidor.", + "server_not_running_description": "O servidor da API precisa estar em execução para que os agentes funcionem. Você pode iniciá-lo diretamente ou verificar as configurações." } }, "apiServer": { @@ -749,6 +758,7 @@ } }, "alerts": { + "create_agent": "Crie um agente para começar", "create_session": "Criar uma sessão", "select_agent": "Selecione um agente" }, @@ -1385,6 +1395,7 @@ "selected": "Selecionado", "selectedItems": "{{count}} itens selecionados", "selectedMessages": "{{count}} mensagens selecionadas", + "sessions": "Sessões", "settings": "Configurações", "sort": { "pinyin": { diff --git a/src/renderer/src/i18n/translate/ro-ro.json b/src/renderer/src/i18n/translate/ro-ro.json index 000bc0c9568..2872ef6427c 100644 --- a/src/renderer/src/i18n/translate/ro-ro.json +++ b/src/renderer/src/i18n/translate/ro-ro.json @@ -38,6 +38,10 @@ "edit": { "title": "Editează agentul" }, + "empty": { + "description": "Creează un agent pentru a gestiona sarcini complexe cu instrumente bazate pe IA", + "title": "Niciun agent încă" + }, "get": { "error": { "failed": "Nu s-a putut obține agentul.", @@ -295,6 +299,7 @@ } } }, + "sidebar_title": "Agenți", "todo": { "panel": { "title": "{{completed}}/{{total}} sarcini finalizate" @@ -395,7 +400,11 @@ } }, "warning": { - "enable_server": "Activează serverul API pentru a folosi agenți." + "enable_and_start": "Activează și pornește", + "enable_server": "Activează serverul API pentru a folosi agenți.", + "enable_server_description": "Serverul API trebuie să fie activat pentru ca agenții să funcționeze. Îl puteți activa direct sau îl puteți configura în setări.", + "server_not_running": "Serverul API este activat, dar nu rulează. Vă rugăm să verificați configurația serverului.", + "server_not_running_description": "Serverul API trebuie să ruleze pentru ca agenții să funcționeze. Îl puteți porni direct sau puteți verifica setările." } }, "apiServer": { @@ -749,6 +758,7 @@ } }, "alerts": { + "create_agent": "Creează un agent pentru a începe", "create_session": "Creează o sesiune", "select_agent": "Selectați un agent" }, @@ -1385,6 +1395,7 @@ "selected": "Selectat", "selectedItems": "{{count}} elemente selectate", "selectedMessages": "{{count}} mesaje selectate", + "sessions": "Sesiuni", "settings": "Setări", "sort": { "pinyin": { diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index c84f783fab6..cbcde0429f3 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -38,6 +38,10 @@ "edit": { "title": "Редактировать агент" }, + "empty": { + "description": "Создайте агента для решения сложных задач с помощью инструментов на базе ИИ", + "title": "Агентов пока нет" + }, "get": { "error": { "failed": "Не удалось получить агента.", @@ -295,6 +299,7 @@ } } }, + "sidebar_title": "Агенты", "todo": { "panel": { "title": "{{completed}}/{{total}} задач выполнено" @@ -395,7 +400,11 @@ } }, "warning": { - "enable_server": "Разрешить серверу API использовать агентов." + "enable_and_start": "Включить и запустить", + "enable_server": "Разрешить серверу API использовать агентов.", + "enable_server_description": "Для работы агентов необходимо включить API-сервер. Вы можете включить его напрямую или в настройках.", + "server_not_running": "Сервер API включен, но не запущен. Пожалуйста, проверьте конфигурацию сервера.", + "server_not_running_description": "Для работы агентов необходимо, чтобы API-сервер был запущен. Вы можете запустить его напрямую или проверить настройки." } }, "apiServer": { @@ -749,6 +758,7 @@ } }, "alerts": { + "create_agent": "Создайте агента, чтобы начать работу", "create_session": "Создать сессию", "select_agent": "Выберите агента" }, @@ -1385,6 +1395,7 @@ "selected": "Выбрано", "selectedItems": "Выбрано {{count}} элементов", "selectedMessages": "Выбрано {{count}} сообщений", + "sessions": "Сессии", "settings": "Настройки", "sort": { "pinyin": { diff --git a/src/renderer/src/pages/agents/AgentChat.tsx b/src/renderer/src/pages/agents/AgentChat.tsx new file mode 100644 index 00000000000..5c55b410258 --- /dev/null +++ b/src/renderer/src/pages/agents/AgentChat.tsx @@ -0,0 +1,145 @@ +import { QuickPanelProvider } from '@renderer/components/QuickPanel' +import { useActiveAgent } from '@renderer/hooks/agents/useActiveAgent' +import { useAgents } from '@renderer/hooks/agents/useAgents' +import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession' +import { useRuntime } from '@renderer/hooks/useRuntime' +import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings' +import { useShortcut } from '@renderer/hooks/useShortcuts' +import { useShowTopics } from '@renderer/hooks/useStore' +import { cn } from '@renderer/utils' +import { buildAgentSessionTopicId } from '@renderer/utils/agentSession' +import { Alert, Spin } from 'antd' +import { AnimatePresence, motion } from 'motion/react' +import type { PropsWithChildren } from 'react' +import { useTranslation } from 'react-i18next' + +import { PinnedTodoPanel } from '../home/Inputbar/components/PinnedTodoPanel' +import ChatNavigation from '../home/Messages/ChatNavigation' +import NarrowLayout from '../home/Messages/NarrowLayout' +import AgentChatNavbar from './components/AgentChatNavbar' +import AgentSessionInputbar from './components/AgentSessionInputbar' +import AgentSessionMessages from './components/AgentSessionMessages' +import Sessions from './components/Sessions' + +const AgentChat = () => { + const { t } = useTranslation() + const { messageNavigation, topicPosition } = useSettings() + const { showTopics } = useShowTopics() + const { chat } = useRuntime() + const { activeAgentId, activeSessionIdMap } = chat + const activeSessionId = activeAgentId ? activeSessionIdMap[activeAgentId] : null + // undefined = session not yet initialized, null = initialized but no sessions + const isSessionInitialized = !activeAgentId || activeAgentId in activeSessionIdMap + const { agent: activeAgent, isLoading: isAgentLoading } = useActiveAgent() + const { isLoading: isAgentsLoading, agents } = useAgents() + const { createDefaultSession } = useCreateDefaultSession(activeAgentId) + + // Don't show select/create alerts while data is still loading + // apiServerRunning is guaranteed by AgentPage guard + const isInitializing = + isAgentsLoading || isAgentLoading || !isSessionInitialized || !agents || (!activeAgentId && agents.length > 0) + + const showRightSessions = topicPosition === 'right' && showTopics && !!activeAgentId + + useShortcut( + 'new_topic', + () => { + void createDefaultSession() + }, + { + enabled: true, + preventDefault: true, + enableOnFormTags: true + } + ) + + if (isInitializing) { + return ( + + + + ) + } + + // Initialized — agents.length === 0 is handled by AgentPage + if (!activeAgentId) { + return ( + +
+ +
+
+ ) + } + + if (!activeSessionId) { + return ( + +
+ +
+
+ ) + } + + return ( + + + {/* Main Chat */} +
+ {/* Header */} +
+ {activeAgent && } +
+ + {/* Messages */} +
+ +
+ + + +
+ {messageNavigation === 'buttons' && } +
+ {/* Inputbar */} + +
+
+ + {/* Sessions Panel */} + + {showRightSessions && ( + +
+ +
+
+ )} +
+
+ ) +} + +const Container = ({ children, className }: PropsWithChildren<{ className?: string }>) => { + const { isTopNavbar } = useNavbarPosition() + + return ( +
+ {children} +
+ ) +} + +export default AgentChat diff --git a/src/renderer/src/pages/agents/AgentNavbar.tsx b/src/renderer/src/pages/agents/AgentNavbar.tsx new file mode 100644 index 00000000000..60bc9430179 --- /dev/null +++ b/src/renderer/src/pages/agents/AgentNavbar.tsx @@ -0,0 +1,117 @@ +import { Navbar, NavbarCenter, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar' +import { HStack } from '@renderer/components/Layout' +import NavbarIcon from '@renderer/components/NavbarIcon' +import SearchPopup from '@renderer/components/Popups/SearchPopup' +import { modelGenerating } from '@renderer/hooks/useRuntime' +import { useSettings } from '@renderer/hooks/useSettings' +import { useShortcut } from '@renderer/hooks/useShortcuts' +import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore' +import { useAppDispatch } from '@renderer/store' +import { setNarrowMode } from '@renderer/store/settings' +import { Tooltip } from 'antd' +import { t } from 'i18next' +import { Menu, PanelLeftClose, PanelRightClose, Search } from 'lucide-react' +import { AnimatePresence, motion } from 'motion/react' + +import UpdateAppButton from '../home/components/UpdateAppButton' +import AgentSidePanelDrawer from './components/AgentSidePanelDrawer' + +const AgentNavbar = () => { + const { showAssistants, toggleShowAssistants } = useShowAssistants() + const { showTopics, toggleShowTopics } = useShowTopics() + const { narrowMode, topicPosition } = useSettings() + const dispatch = useAppDispatch() + + useShortcut('toggle_show_assistants', toggleShowAssistants) + + useShortcut('search_message', () => { + SearchPopup.show() + }) + + const handleNarrowModeToggle = async () => { + await modelGenerating() + dispatch(setNarrowMode(!narrowMode)) + } + + return ( + + + {showAssistants && ( + + + + + + + + + + )} + + {!showAssistants && ( + + + toggleShowAssistants()}> + + + + AgentSidePanelDrawer.show()} style={{ marginRight: 5 }}> + + + + )} + + + + + + SearchPopup.show()}> + + + + + + + + + {topicPosition === 'right' && !showTopics && ( + + + + + + )} + {topicPosition === 'right' && showTopics && ( + + + + + + )} + + + + ) +} + +export default AgentNavbar diff --git a/src/renderer/src/pages/agents/AgentPage.tsx b/src/renderer/src/pages/agents/AgentPage.tsx new file mode 100644 index 00000000000..5e89693b1aa --- /dev/null +++ b/src/renderer/src/pages/agents/AgentPage.tsx @@ -0,0 +1,105 @@ +import { ErrorBoundary } from '@renderer/components/ErrorBoundary' +import { useActiveAgent } from '@renderer/hooks/agents/useActiveAgent' +import { useAgents } from '@renderer/hooks/agents/useAgents' +import { useApiServer } from '@renderer/hooks/useApiServer' +import { useRuntime } from '@renderer/hooks/useRuntime' +import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings' +import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore' +import { cn } from '@renderer/utils' +import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, SECOND_MIN_WINDOW_WIDTH } from '@shared/config/constant' +import { AnimatePresence, motion } from 'motion/react' +import type { PropsWithChildren } from 'react' +import { useEffect } from 'react' + +import AgentChat from './AgentChat' +import AgentNavbar from './AgentNavbar' +import AgentSidePanel from './AgentSidePanel' +import { AgentEmpty, AgentServerDisabled, AgentServerStopped } from './components/status' + +const AgentPage = () => { + const { isLeftNavbar } = useNavbarPosition() + const { showAssistants } = useShowAssistants() + const { showTopics } = useShowTopics() + const { topicPosition } = useSettings() + const { chat } = useRuntime() + const { activeAgentId } = chat + const { agents } = useAgents() + const { setActiveAgentId } = useActiveAgent() + const { apiServerConfig, apiServerRunning, apiServerLoading } = useApiServer() + + // Auto-select first agent when none is active + useEffect(() => { + if (!activeAgentId && agents && agents.length > 0) { + setActiveAgentId(agents[0].id) + } + }, [activeAgentId, agents, setActiveAgentId]) + + useEffect(() => { + const canMinimize = topicPosition === 'left' ? !showAssistants : !showAssistants && !showTopics + window.api.window.setMinimumSize(canMinimize ? SECOND_MIN_WINDOW_WIDTH : MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT) + return () => { + window.api.window.resetMinimumSize() + } + }, [showAssistants, showTopics, topicPosition]) + + if (!apiServerConfig.enabled) { + return ( + + + + ) + } + + if (!apiServerLoading && !apiServerRunning) { + return ( + + + + ) + } + + if (agents && agents.length === 0) { + return ( + + + + ) + } + + return ( + + {isLeftNavbar && } +
+ + {showAssistants && ( + + + + + + )} + + + + +
+
+ ) +} + +const Container = ({ children, className }: PropsWithChildren<{ className?: string }>) => { + return ( +
+ {children} +
+ ) +} + +export default AgentPage diff --git a/src/renderer/src/pages/home/components/ChatNavBar/Tools/SettingsTab/AgentSettingsTab.tsx b/src/renderer/src/pages/agents/AgentSettingsTab.tsx similarity index 98% rename from src/renderer/src/pages/home/components/ChatNavBar/Tools/SettingsTab/AgentSettingsTab.tsx rename to src/renderer/src/pages/agents/AgentSettingsTab.tsx index 5af653adc7e..d272515d17e 100644 --- a/src/renderer/src/pages/home/components/ChatNavBar/Tools/SettingsTab/AgentSettingsTab.tsx +++ b/src/renderer/src/pages/agents/AgentSettingsTab.tsx @@ -1,4 +1,3 @@ -import { loggerService } from '@logger' import EditableNumber from '@renderer/components/EditableNumber' import Scrollbar from '@renderer/components/Scrollbar' import Selector from '@renderer/components/Selector' @@ -6,7 +5,6 @@ import { HelpTooltip } from '@renderer/components/TooltipIcons' import { UNKNOWN } from '@renderer/config/translate' import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import { useTheme } from '@renderer/context/ThemeProvider' -import { useRuntime } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' import useTranslate from '@renderer/hooks/useTranslate' import { SettingDivider, SettingRow, SettingRowTitle } from '@renderer/pages/settings' @@ -46,11 +44,7 @@ import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -const logger = loggerService.withContext('AgentSettingsTab') - const AgentSettingsTab = () => { - const { chat } = useRuntime() - const { messageStyle, fontSize, language } = useSettings() const { theme } = useTheme() const { themeNames } = useCodeStyle() @@ -116,12 +110,6 @@ const AgentSettingsTab = () => { [dispatch, theme, codeEditor.enabled] ) - const isAgentSettings = chat.activeTopicOrSession === 'session' - if (!isAgentSettings) { - logger.warn('AgentSettingsTab is rendered when not session activated.') - return null - } - return ( diff --git a/src/renderer/src/pages/agents/AgentSidePanel.tsx b/src/renderer/src/pages/agents/AgentSidePanel.tsx new file mode 100644 index 00000000000..a56c7723aa7 --- /dev/null +++ b/src/renderer/src/pages/agents/AgentSidePanel.tsx @@ -0,0 +1,138 @@ +import AddButton from '@renderer/components/AddButton' +import AgentModalPopup from '@renderer/components/Popups/agent/AgentModal' +import Scrollbar from '@renderer/components/Scrollbar' +import { useActiveAgent } from '@renderer/hooks/agents/useActiveAgent' +import { useAgents } from '@renderer/hooks/agents/useAgents' +import { useApiServer } from '@renderer/hooks/useApiServer' +import { useRuntime } from '@renderer/hooks/useRuntime' +import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings' +import type { AgentEntity } from '@renderer/types' +import { cn } from '@renderer/utils' +import type { FC } from 'react' +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' + +import AgentItem from './components/AgentItem' +import Sessions from './components/Sessions' + +interface AgentSidePanelProps { + onSelectItem?: () => void +} + +const AgentSidePanel = ({ onSelectItem }: AgentSidePanelProps) => { + const { t } = useTranslation() + const { agents, deleteAgent, isLoading, error } = useAgents() + const { apiServerRunning, startApiServer } = useApiServer() + const { chat } = useRuntime() + const { activeAgentId } = chat + const { setActiveAgentId } = useActiveAgent() + const { isLeftNavbar } = useNavbarPosition() + const { topicPosition } = useSettings() + + const sessionsOnRight = topicPosition === 'right' + const [tab, setTab] = useState<'agents' | 'sessions'>('agents') + + const handleAgentPress = useCallback( + (agentId: string) => { + setActiveAgentId(agentId) + onSelectItem?.() + }, + [setActiveAgentId, onSelectItem] + ) + + const handleAddAgent = useCallback(() => { + !apiServerRunning && startApiServer() + AgentModalPopup.show({ + afterSubmit: (agent: AgentEntity) => { + setActiveAgentId(agent.id) + } + }) + }, [apiServerRunning, startApiServer, setActiveAgentId]) + + return ( +
+ {/* Tabs */} + {!sessionsOnRight && ( +
+ setTab('agents')}> + {t('agent.sidebar_title')} + + setTab('sessions')}> + {t('common.sessions')} + +
+ )} + + {/* Content */} +
+ {(sessionsOnRight || tab === 'agents') && ( + +
+ {t('agent.sidebar_title')} +
+
+ {isLoading && ( +
{t('common.loading')}
+ )} + {error &&
{error.message}
} + {!isLoading && + !error && + agents?.map((agent) => ( + deleteAgent(agent.id)} + onPress={() => handleAgentPress(agent.id)} + /> + ))} +
+
+ )} + {!sessionsOnRight && tab === 'sessions' && activeAgentId && ( + + )} + {!sessionsOnRight && tab === 'sessions' && !activeAgentId && ( +
+ {t('chat.alerts.select_agent')} +
+ )} +
+
+ ) +} + +const TabButton: FC<{ active: boolean; onClick: () => void; children: React.ReactNode }> = ({ + active, + onClick, + children +}) => ( + +) + +export default AgentSidePanel diff --git a/src/renderer/src/pages/home/components/ChatNavBar/ChatNavbarContent/AgentContent.tsx b/src/renderer/src/pages/agents/components/AgentChatNavbar/AgentContent.tsx similarity index 63% rename from src/renderer/src/pages/home/components/ChatNavBar/ChatNavbarContent/AgentContent.tsx rename to src/renderer/src/pages/agents/components/AgentChatNavbar/AgentContent.tsx index 5ae758ce6fd..d9e5b7f1309 100644 --- a/src/renderer/src/pages/home/components/ChatNavBar/ChatNavbarContent/AgentContent.tsx +++ b/src/renderer/src/pages/agents/components/AgentChatNavbar/AgentContent.tsx @@ -1,22 +1,32 @@ import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer' +import NavbarIcon from '@renderer/components/NavbarIcon' import { useActiveSession } from '@renderer/hooks/agents/useActiveSession' import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession' +import { useNavbarPosition } from '@renderer/hooks/useSettings' +import { useShowAssistants } from '@renderer/hooks/useStore' import { AgentSettingsPopup, SessionSettingsPopup } from '@renderer/pages/settings/AgentSettings' import { AgentLabel, SessionLabel } from '@renderer/pages/settings/AgentSettings/shared' import type { AgentEntity, ApiModel } from '@renderer/types' +import { Tooltip } from 'antd' +import { t } from 'i18next' import { ChevronRight } from 'lucide-react' +import { Menu, PanelLeftClose, PanelRightClose } from 'lucide-react' +import { AnimatePresence, motion } from 'motion/react' import { useCallback } from 'react' -import SelectAgentBaseModelButton from '../../SelectAgentBaseModelButton' -import Tools from '../Tools' +import AgentSidePanelDrawer from '../AgentSidePanelDrawer' +import SelectAgentBaseModelButton from '../SelectAgentBaseModelButton' import OpenExternalAppButton from './OpenExternalAppButton' import SessionWorkspaceMeta from './SessionWorkspaceMeta' +import Tools from './Tools' type AgentContentProps = { activeAgent: AgentEntity } const AgentContent = ({ activeAgent }: AgentContentProps) => { + const { showAssistants, toggleShowAssistants } = useShowAssistants() + const { isTopNavbar } = useNavbarPosition() const { session: activeSession } = useActiveSession() const { updateModel } = useUpdateSession(activeAgent?.id ?? null) @@ -29,9 +39,36 @@ const AgentContent = ({ activeAgent }: AgentContentProps) => { ) return ( - <> -
- +
+
+ {isTopNavbar && showAssistants && ( + + + + + + )} + {isTopNavbar && !showAssistants && ( + + toggleShowAssistants()} style={{ marginRight: 8 }}> + + + + )} + + {!showAssistants && isTopNavbar && ( + + AgentSidePanelDrawer.show()} style={{ marginRight: 5 }}> + + + + )} + +
{/* Agent Label */}
{ )}
- +
) } diff --git a/src/renderer/src/pages/home/components/ChatNavBar/ChatNavbarContent/OpenExternalAppButton.tsx b/src/renderer/src/pages/agents/components/AgentChatNavbar/OpenExternalAppButton.tsx similarity index 100% rename from src/renderer/src/pages/home/components/ChatNavBar/ChatNavbarContent/OpenExternalAppButton.tsx rename to src/renderer/src/pages/agents/components/AgentChatNavbar/OpenExternalAppButton.tsx diff --git a/src/renderer/src/pages/home/components/ChatNavBar/ChatNavbarContent/SessionWorkspaceMeta.tsx b/src/renderer/src/pages/agents/components/AgentChatNavbar/SessionWorkspaceMeta.tsx similarity index 100% rename from src/renderer/src/pages/home/components/ChatNavBar/ChatNavbarContent/SessionWorkspaceMeta.tsx rename to src/renderer/src/pages/agents/components/AgentChatNavbar/SessionWorkspaceMeta.tsx diff --git a/src/renderer/src/pages/agents/components/AgentChatNavbar/Tools/SettingsButton.tsx b/src/renderer/src/pages/agents/components/AgentChatNavbar/Tools/SettingsButton.tsx new file mode 100644 index 00000000000..2a885855ba8 --- /dev/null +++ b/src/renderer/src/pages/agents/components/AgentChatNavbar/Tools/SettingsButton.tsx @@ -0,0 +1,32 @@ +import NavbarIcon from '@renderer/components/NavbarIcon' +import { Drawer, Tooltip } from 'antd' +import { t } from 'i18next' +import { Settings2 } from 'lucide-react' +import { useState } from 'react' + +import AgentSettingsTab from '../../../AgentSettingsTab' + +const SettingsButton = () => { + const [settingsOpen, setSettingsOpen] = useState(false) + + return ( + <> + + setSettingsOpen(true)}> + + + + setSettingsOpen(false)} + width="var(--assistants-width)" + closable={false} + styles={{ body: { padding: 0, paddingTop: 'var(--navbar-height)' } }}> + + + + ) +} + +export default SettingsButton diff --git a/src/renderer/src/pages/agents/components/AgentChatNavbar/Tools/index.tsx b/src/renderer/src/pages/agents/components/AgentChatNavbar/Tools/index.tsx new file mode 100644 index 00000000000..85bb1eb60b9 --- /dev/null +++ b/src/renderer/src/pages/agents/components/AgentChatNavbar/Tools/index.tsx @@ -0,0 +1,69 @@ +import { HStack } from '@renderer/components/Layout' +import NavbarIcon from '@renderer/components/NavbarIcon' +import SearchPopup from '@renderer/components/Popups/SearchPopup' +import { modelGenerating } from '@renderer/hooks/useRuntime' +import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings' +import { useShowTopics } from '@renderer/hooks/useStore' +import { useAppDispatch } from '@renderer/store' +import { setNarrowMode } from '@renderer/store/settings' +import { Tooltip } from 'antd' +import { PanelLeftClose, PanelRightClose, Search } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import SettingsButton from './SettingsButton' + +const Tools = () => { + const { t } = useTranslation() + const { showTopics, toggleShowTopics } = useShowTopics() + const { isTopNavbar } = useNavbarPosition() + const { topicPosition, narrowMode } = useSettings() + const dispatch = useAppDispatch() + + const handleNarrowModeToggle = async () => { + await modelGenerating() + dispatch(setNarrowMode(!narrowMode)) + } + + return ( + + + {isTopNavbar && ( + + + + + + )} + {isTopNavbar && ( + + SearchPopup.show()}> + + + + )} + {isTopNavbar && topicPosition === 'right' && !showTopics && ( + + + + + + )} + {isTopNavbar && topicPosition === 'right' && showTopics && ( + + + + + + )} + + ) +} + +const NarrowIcon = styled(NavbarIcon)` + @media (max-width: 1000px) { + display: none; + } +` + +export default Tools diff --git a/src/renderer/src/pages/agents/components/AgentChatNavbar/index.tsx b/src/renderer/src/pages/agents/components/AgentChatNavbar/index.tsx new file mode 100644 index 00000000000..5cde28baf44 --- /dev/null +++ b/src/renderer/src/pages/agents/components/AgentChatNavbar/index.tsx @@ -0,0 +1,45 @@ +import { NavbarHeader } from '@renderer/components/app/Navbar' +import SearchPopup from '@renderer/components/Popups/SearchPopup' +import { useSettings } from '@renderer/hooks/useSettings' +import { useShortcut } from '@renderer/hooks/useShortcuts' +import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore' +import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' +import type { AgentEntity } from '@renderer/types' +import { cn } from '@renderer/utils' + +import AgentContent from './AgentContent' + +interface Props { + activeAgent: AgentEntity + className?: string +} + +const AgentChatNavbar = ({ activeAgent, className }: Props) => { + const { toggleShowAssistants } = useShowAssistants() + const { topicPosition } = useSettings() + const { toggleShowTopics } = useShowTopics() + + useShortcut('toggle_show_assistants', toggleShowAssistants) + + useShortcut('toggle_show_topics', () => { + if (topicPosition === 'right') { + toggleShowTopics() + } else { + EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR) + } + }) + + useShortcut('search_message', () => { + SearchPopup.show() + }) + + return ( + +
+ +
+
+ ) +} + +export default AgentChatNavbar diff --git a/src/renderer/src/pages/home/Tabs/components/AgentItem.tsx b/src/renderer/src/pages/agents/components/AgentItem.tsx similarity index 85% rename from src/renderer/src/pages/home/Tabs/components/AgentItem.tsx rename to src/renderer/src/pages/agents/components/AgentItem.tsx index f35d9fc2674..881a981d1b4 100644 --- a/src/renderer/src/pages/home/Tabs/components/AgentItem.tsx +++ b/src/renderer/src/pages/agents/components/AgentItem.tsx @@ -8,7 +8,6 @@ import { cn } from '@renderer/utils' import type { MenuProps } from 'antd' import { Dropdown, Tooltip } from 'antd' import { Bot, MoreVertical } from 'lucide-react' -import type { FC } from 'react' import { memo, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -21,7 +20,7 @@ interface AgentItemProps { onPress: () => void } -const AgentItem: FC = ({ agent, isActive, onDelete, onPress }) => { +const AgentItem = ({ agent, isActive, onDelete, onPress }: AgentItemProps) => { const { t } = useTranslation() const { clickAssistantToShowTopic, topicPosition, assistantIconType } = useSettings() const [isHovered, setIsHovered] = useState(false) @@ -87,7 +86,7 @@ const AgentItem: FC = ({ agent, isActive, onDelete, onPress }) = trigger={['click']} popupRender={(menu) =>
e.stopPropagation()}>{menu}
}> - + )} @@ -105,9 +104,9 @@ export const Container: React.FC<{ isActive?: boolean } & React.HTMLAttributes (
> = ({ className, ...props }) => (
) @@ -128,7 +127,7 @@ export const AgentNameWrapper: React.FC> = export const MenuButton: React.FC> = ({ className, ...props }) => (
> = ({ ...pro export const SessionCount: React.FC> = ({ className, ...props }) => (
) diff --git a/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx b/src/renderer/src/pages/agents/components/AgentSessionInputbar.tsx similarity index 97% rename from src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx rename to src/renderer/src/pages/agents/components/AgentSessionInputbar.tsx index c5a1f771d01..d5de6457338 100644 --- a/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx +++ b/src/renderer/src/pages/agents/components/AgentSessionInputbar.tsx @@ -10,6 +10,17 @@ import { getModel } from '@renderer/hooks/useModel' import { useSettings } from '@renderer/hooks/useSettings' import { useTextareaResize } from '@renderer/hooks/useTextareaResize' import { useTimer } from '@renderer/hooks/useTimer' +import { InputbarCore } from '@renderer/pages/home/Inputbar/components/InputbarCore' +import { + InputbarToolsProvider, + useInputbarToolsDispatch, + useInputbarToolsInternalDispatch, + useInputbarToolsState +} from '@renderer/pages/home/Inputbar/context/InputbarToolsProvider' +import InputbarTools from '@renderer/pages/home/Inputbar/InputbarTools' +import { getInputbarConfig } from '@renderer/pages/home/Inputbar/registry' +import type { ToolContext } from '@renderer/pages/home/Inputbar/types' +import { TopicType } from '@renderer/pages/home/Inputbar/types' import { CacheService } from '@renderer/services/CacheService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { pauseTrace } from '@renderer/services/SpanManagerService' @@ -32,18 +43,6 @@ import { useTranslation } from 'react-i18next' import styled from 'styled-components' import { v4 as uuid } from 'uuid' -import { InputbarCore } from './components/InputbarCore' -import { - InputbarToolsProvider, - useInputbarToolsDispatch, - useInputbarToolsInternalDispatch, - useInputbarToolsState -} from './context/InputbarToolsProvider' -import InputbarTools from './InputbarTools' -import { getInputbarConfig } from './registry' -import type { ToolContext } from './types' -import { TopicType } from './types' - const logger = loggerService.withContext('AgentSessionInputbar') const DRAFT_CACHE_TTL = 24 * 60 * 60 * 1000 // 24 hours @@ -55,7 +54,7 @@ type Props = { sessionId: string } -const AgentSessionInputbar: FC = ({ agentId, sessionId }) => { +const AgentSessionInputbar = ({ agentId, sessionId }: Props) => { const { session } = useSession(agentId, sessionId) // FIXME: 不应该使用ref将action传到context提供给tool,权宜之计 const actionsRef = useRef({ diff --git a/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx b/src/renderer/src/pages/agents/components/AgentSessionMessages.tsx similarity index 87% rename from src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx rename to src/renderer/src/pages/agents/components/AgentSessionMessages.tsx index 7f7900b8c59..bdd2fa5a2c2 100644 --- a/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx +++ b/src/renderer/src/pages/agents/components/AgentSessionMessages.tsx @@ -4,19 +4,17 @@ import { useSession } from '@renderer/hooks/agents/useSession' import { useTopicMessages } from '@renderer/hooks/useMessageOperations' import useScrollPosition from '@renderer/hooks/useScrollPosition' import { useSettings } from '@renderer/hooks/useSettings' +import MessageAnchorLine from '@renderer/pages/home/Messages/MessageAnchorLine' +import MessageGroup from '@renderer/pages/home/Messages/MessageGroup' +import NarrowLayout from '@renderer/pages/home/Messages/NarrowLayout' +import PermissionModeDisplay from '@renderer/pages/home/Messages/PermissionModeDisplay' +import { MessagesContainer, ScrollContainer } from '@renderer/pages/home/Messages/shared' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { getGroupedMessages } from '@renderer/services/MessagesService' import { type Topic, TopicType } from '@renderer/types' import { buildAgentSessionTopicId } from '@renderer/utils/agentSession' import { Spin } from 'antd' import { memo, useCallback, useEffect, useMemo, useRef } from 'react' -import styled from 'styled-components' - -import MessageAnchorLine from './MessageAnchorLine' -import MessageGroup from './MessageGroup' -import NarrowLayout from './NarrowLayout' -import PermissionModeDisplay from './PermissionModeDisplay' -import { MessagesContainer, ScrollContainer } from './shared' const logger = loggerService.withContext('AgentSessionMessages') @@ -25,7 +23,7 @@ type Props = { sessionId: string } -const AgentSessionMessages: React.FC = ({ agentId, sessionId }) => { +const AgentSessionMessages = ({ agentId, sessionId }: Props) => { const { session } = useSession(agentId, sessionId) const sessionTopicId = useMemo(() => buildAgentSessionTopicId(sessionId), [sessionId]) // Use the same hook as Messages.tsx for consistent behavior @@ -101,9 +99,9 @@ const AgentSessionMessages: React.FC = ({ agentId, sessionId }) => { ) : session ? ( ) : ( - +
- +
)} @@ -113,13 +111,6 @@ const AgentSessionMessages: React.FC = ({ agentId, sessionId }) => { ) } -const LoadingState = styled.div` - display: flex; - justify-content: center; - align-items: center; - padding: 20px 0; -` - const FALLBACK_TIMESTAMP = '1970-01-01T00:00:00.000Z' export default memo(AgentSessionMessages) diff --git a/src/renderer/src/pages/agents/components/AgentSidePanelDrawer.tsx b/src/renderer/src/pages/agents/components/AgentSidePanelDrawer.tsx new file mode 100644 index 00000000000..b557b8aab1c --- /dev/null +++ b/src/renderer/src/pages/agents/components/AgentSidePanelDrawer.tsx @@ -0,0 +1,71 @@ +import { TopView } from '@renderer/components/TopView' +import { isMac } from '@renderer/config/constant' +import { useTimer } from '@renderer/hooks/useTimer' +import { Drawer } from 'antd' +import { useState } from 'react' + +import AgentSidePanel from '../AgentSidePanel' + +interface Props { + resolve: () => void +} + +const PopupContainer = ({ resolve }: Props) => { + const [open, setOpen] = useState(true) + const { setTimeoutTimer } = useTimer() + + const onClose = () => { + setOpen(false) + setTimeoutTimer('onClose', resolve, 300) + } + + AgentSidePanelDrawer.hide = onClose + + return ( + + + + ) +} + +const TopViewKey = 'AgentSidePanelDrawer' + +export default class AgentSidePanelDrawer { + static topviewId = 0 + static hide() { + TopView.hide(TopViewKey) + } + static show() { + return new Promise((resolve) => { + TopView.show( + { + resolve() + TopView.hide(TopViewKey) + }} + />, + TopViewKey + ) + }) + } +} diff --git a/src/renderer/src/pages/home/components/SelectAgentBaseModelButton.tsx b/src/renderer/src/pages/agents/components/SelectAgentBaseModelButton.tsx similarity index 94% rename from src/renderer/src/pages/home/components/SelectAgentBaseModelButton.tsx rename to src/renderer/src/pages/agents/components/SelectAgentBaseModelButton.tsx index a272cdb0efe..b16d577275e 100644 --- a/src/renderer/src/pages/home/components/SelectAgentBaseModelButton.tsx +++ b/src/renderer/src/pages/agents/components/SelectAgentBaseModelButton.tsx @@ -11,7 +11,7 @@ import { apiModelAdapter } from '@renderer/utils/model' import type { ButtonProps } from 'antd' import { Button } from 'antd' import { ChevronsUpDown } from 'lucide-react' -import type { CSSProperties, FC } from 'react' +import type { CSSProperties } from 'react' import { useTranslation } from 'react-i18next' interface Props { @@ -34,7 +34,7 @@ interface Props { containerClassName?: string } -const SelectAgentBaseModelButton: FC = ({ +const SelectAgentBaseModelButton = ({ agentBase: agent, onSelect, isDisabled, @@ -45,7 +45,7 @@ const SelectAgentBaseModelButton: FC = ({ fontSize = 12, iconSize = 14, containerClassName -}) => { +}: Props) => { const { t } = useTranslation() const model = useApiModel({ id: agent?.model }) @@ -89,7 +89,7 @@ const SelectAgentBaseModelButton: FC = ({
- + {model ? model.name : t('button.select_model')} {providerName ? ' | ' + providerName : ''}
diff --git a/src/renderer/src/pages/home/Tabs/components/SessionItem.tsx b/src/renderer/src/pages/agents/components/SessionItem.tsx similarity index 98% rename from src/renderer/src/pages/home/Tabs/components/SessionItem.tsx rename to src/renderer/src/pages/agents/components/SessionItem.tsx index 19d1b6a7585..b2a8c937014 100644 --- a/src/renderer/src/pages/home/Tabs/components/SessionItem.tsx +++ b/src/renderer/src/pages/agents/components/SessionItem.tsx @@ -16,7 +16,6 @@ import { buildAgentSessionTopicId } from '@renderer/utils/agentSession' import type { MenuProps } from 'antd' import { Dropdown, Tooltip } from 'antd' import { MenuIcon, Sparkles, XIcon } from 'lucide-react' -import type { FC } from 'react' import React, { memo, startTransition, useDeferredValue, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -31,7 +30,7 @@ interface SessionItemProps { onPress: () => void } -const SessionItem: FC = ({ session, agentId, onDelete, onPress }) => { +const SessionItem = ({ session, agentId, onDelete, onPress }: SessionItemProps) => { const { t } = useTranslation() const { chat } = useRuntime() const { updateSession } = useUpdateSession(agentId) diff --git a/src/renderer/src/pages/home/Tabs/components/Sessions.tsx b/src/renderer/src/pages/agents/components/Sessions.tsx similarity index 94% rename from src/renderer/src/pages/home/Tabs/components/Sessions.tsx rename to src/renderer/src/pages/agents/components/Sessions.tsx index 44702f0560f..7d39d885619 100644 --- a/src/renderer/src/pages/home/Tabs/components/Sessions.tsx +++ b/src/renderer/src/pages/agents/components/Sessions.tsx @@ -1,10 +1,11 @@ +import AddButton from '@renderer/components/AddButton' import { DynamicVirtualList, type DynamicVirtualListRef } from '@renderer/components/VirtualList' import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession' import { useSessions } from '@renderer/hooks/agents/useSessions' import { useRuntime } from '@renderer/hooks/useRuntime' import { useAppDispatch } from '@renderer/store' import { newMessagesActions } from '@renderer/store/newMessage' -import { setActiveSessionIdAction, setActiveTopicOrSessionAction } from '@renderer/store/runtime' +import { setActiveSessionIdAction } from '@renderer/store/runtime' import { buildAgentSessionTopicId } from '@renderer/utils/agentSession' import { formatErrorMessage } from '@renderer/utils/error' import { Alert, Button, Spin } from 'antd' @@ -14,17 +15,17 @@ import { memo, useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import AddButton from './AddButton' import SessionItem from './SessionItem' interface SessionsProps { agentId: string + onSelectItem?: () => void } const LOAD_MORE_THRESHOLD = 100 const SCROLL_THROTTLE_DELAY = 150 -const Sessions: React.FC = ({ agentId }) => { +const Sessions = ({ agentId, onSelectItem }: SessionsProps) => { const { t } = useTranslation() const { sessions, isLoading, error, deleteSession, hasMore, loadMore, isLoadingMore, isValidating, reload } = useSessions(agentId) @@ -77,7 +78,6 @@ const Sessions: React.FC = ({ agentId }) => { const setActiveSessionId = useCallback( (agentId: string, sessionId: string | null) => { dispatch(setActiveSessionIdAction({ agentId, sessionId })) - dispatch(setActiveTopicOrSessionAction('session')) }, [dispatch] ) @@ -172,7 +172,10 @@ const Sessions: React.FC = ({ agentId }) => { session={session} agentId={agentId} onDelete={() => handleDeleteSession(session.id)} - onPress={() => setActiveSessionId(agentId, session.id)} + onPress={() => { + setActiveSessionId(agentId, session.id) + onSelectItem?.() + }} /> )} diff --git a/src/renderer/src/pages/agents/components/status/AgentEmpty.tsx b/src/renderer/src/pages/agents/components/status/AgentEmpty.tsx new file mode 100644 index 00000000000..c0369a6a8d3 --- /dev/null +++ b/src/renderer/src/pages/agents/components/status/AgentEmpty.tsx @@ -0,0 +1,41 @@ +import AgentModalPopup from '@renderer/components/Popups/agent/AgentModal' +import { useActiveAgent } from '@renderer/hooks/agents/useActiveAgent' +import { useApiServer } from '@renderer/hooks/useApiServer' +import type { AgentEntity } from '@renderer/types' +import { Button } from 'antd' +import { Bot, Plus } from 'lucide-react' +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' + +import AgentStatusScreen from './AgentStatusScreen' + +const AgentEmpty = () => { + const { t } = useTranslation() + const { apiServerRunning, startApiServer } = useApiServer() + const { setActiveAgentId } = useActiveAgent() + + const handleAddAgent = useCallback(() => { + !apiServerRunning && startApiServer() + AgentModalPopup.show({ + afterSubmit: (agent: AgentEntity) => { + setActiveAgentId(agent.id) + } + }) + }, [apiServerRunning, startApiServer, setActiveAgentId]) + + return ( + } onClick={handleAddAgent}> + {t('agent.add.title')} + + } + /> + ) +} + +export default AgentEmpty diff --git a/src/renderer/src/pages/agents/components/status/AgentServerDisabled.tsx b/src/renderer/src/pages/agents/components/status/AgentServerDisabled.tsx new file mode 100644 index 00000000000..41d5e406238 --- /dev/null +++ b/src/renderer/src/pages/agents/components/status/AgentServerDisabled.tsx @@ -0,0 +1,39 @@ +import { useApiServer } from '@renderer/hooks/useApiServer' +import { Button } from 'antd' +import { ServerOff, Settings } from 'lucide-react' +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' + +import AgentStatusScreen from './AgentStatusScreen' + +const AgentServerDisabled = () => { + const { t } = useTranslation() + const { startApiServer } = useApiServer() + const navigate = useNavigate() + + const handleGoToSettings = useCallback(() => { + navigate('/settings/api-server') + }, [navigate]) + + return ( + + + + + } + /> + ) +} + +export default AgentServerDisabled diff --git a/src/renderer/src/pages/agents/components/status/AgentServerStopped.tsx b/src/renderer/src/pages/agents/components/status/AgentServerStopped.tsx new file mode 100644 index 00000000000..e07eb52a400 --- /dev/null +++ b/src/renderer/src/pages/agents/components/status/AgentServerStopped.tsx @@ -0,0 +1,39 @@ +import { useApiServer } from '@renderer/hooks/useApiServer' +import { Button } from 'antd' +import { ServerCrash, Settings } from 'lucide-react' +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' + +import AgentStatusScreen from './AgentStatusScreen' + +const AgentServerStopped = () => { + const { t } = useTranslation() + const { startApiServer } = useApiServer() + const navigate = useNavigate() + + const handleGoToSettings = useCallback(() => { + navigate('/settings/api-server') + }, [navigate]) + + return ( + + + + + } + /> + ) +} + +export default AgentServerStopped diff --git a/src/renderer/src/pages/agents/components/status/AgentStatusScreen.tsx b/src/renderer/src/pages/agents/components/status/AgentStatusScreen.tsx new file mode 100644 index 00000000000..b690d531423 --- /dev/null +++ b/src/renderer/src/pages/agents/components/status/AgentStatusScreen.tsx @@ -0,0 +1,30 @@ +import type { LucideIcon } from 'lucide-react' +import { motion } from 'motion/react' +import type { ReactNode } from 'react' + +interface AgentStatusScreenProps { + icon: LucideIcon + iconClassName: string + title: string + description: string + actions: ReactNode +} + +const AgentStatusScreen = ({ icon: Icon, iconClassName, title, description, actions }: AgentStatusScreenProps) => { + return ( + + +
+

{title}

+

{description}

+
+
{actions}
+
+ ) +} + +export default AgentStatusScreen diff --git a/src/renderer/src/pages/agents/components/status/index.ts b/src/renderer/src/pages/agents/components/status/index.ts new file mode 100644 index 00000000000..1492d7f9481 --- /dev/null +++ b/src/renderer/src/pages/agents/components/status/index.ts @@ -0,0 +1,3 @@ +export { default as AgentEmpty } from './AgentEmpty' +export { default as AgentServerDisabled } from './AgentServerDisabled' +export { default as AgentServerStopped } from './AgentServerStopped' diff --git a/src/renderer/src/pages/home/Chat.tsx b/src/renderer/src/pages/home/Chat.tsx index abf6eb4ab36..a7370a01eeb 100644 --- a/src/renderer/src/pages/home/Chat.tsx +++ b/src/renderer/src/pages/home/Chat.tsx @@ -7,10 +7,8 @@ import PromptPopup from '@renderer/components/Popups/PromptPopup' import { SelectModelPopup } from '@renderer/components/Popups/SelectModelPopup' import { QuickPanelProvider } from '@renderer/components/QuickPanel' import { isEmbeddingModel, isRerankModel, isWebSearchModel } from '@renderer/config/models' -import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession' import { useAssistant } from '@renderer/hooks/useAssistant' import { useChatContext } from '@renderer/hooks/useChatContext' -import { useRuntime } from '@renderer/hooks/useRuntime' import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings' import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShowTopics } from '@renderer/hooks/useStore' @@ -18,24 +16,19 @@ import { useTimer } from '@renderer/hooks/useTimer' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import type { Assistant, Model, Topic } from '@renderer/types' import { classNames } from '@renderer/utils' -import { buildAgentSessionTopicId } from '@renderer/utils/agentSession' -import { Alert, Flex } from 'antd' +import { Flex } from 'antd' import { debounce } from 'lodash' import { AnimatePresence, motion } from 'motion/react' import type { FC } from 'react' -import React, { useCallback, useState } from 'react' +import React, { useState } from 'react' import { useHotkeys } from 'react-hotkeys-hook' import { useTranslation } from 'react-i18next' import styled from 'styled-components' import ChatNavbar from './components/ChatNavBar' -import AgentSessionInputbar from './Inputbar/AgentSessionInputbar' -import { PinnedTodoPanel } from './Inputbar/components/PinnedTodoPanel' import Inputbar from './Inputbar/Inputbar' -import AgentSessionMessages from './Messages/AgentSessionMessages' import ChatNavigation from './Messages/ChatNavigation' import Messages from './Messages/Messages' -import NarrowLayout from './Messages/NarrowLayout' import Tabs from './Tabs' const logger = loggerService.withContext('Chat') @@ -54,12 +47,6 @@ const Chat: FC = (props) => { const { showTopics } = useShowTopics() const { isMultiSelectMode } = useChatContext(props.activeTopic) const { isTopNavbar } = useNavbarPosition() - const { chat } = useRuntime() - const { activeTopicOrSession, activeAgentId, activeSessionIdMap } = chat - const activeSessionId = activeAgentId ? activeSessionIdMap[activeAgentId] : null - const { apiServer } = useSettings() - const sessionAgentId = activeTopicOrSession === 'session' ? activeAgentId : null - const { createDefaultSession } = useCreateDefaultSession(sessionAgentId) const mainRef = React.useRef(null) const contentSearchRef = React.useRef(null) @@ -98,21 +85,6 @@ const Chat: FC = (props) => { } }) - useShortcut( - 'new_topic', - () => { - if (activeTopicOrSession !== 'session' || !activeAgentId) { - return - } - void createDefaultSession() - }, - { - enabled: activeTopicOrSession === 'session', - preventDefault: true, - enableOnFormTags: true - } - ) - useShortcut('select_model', async () => { const modelFilter = (m: Model) => !isEmbeddingModel(m) && !isRerankModel(m) const selectedModel = await SelectModelPopup.show({ model: assistant?.model, filter: modelFilter }) @@ -178,20 +150,6 @@ const Chat: FC = (props) => { const mainHeight = isTopNavbar ? 'calc(100vh - var(--navbar-height) - 6px)' : 'calc(100vh - var(--navbar-height))' - // TODO: more info - const AgentInvalid = useCallback(() => { - return - }, [t]) - - // TODO: more info - const SessionInvalid = useCallback(() => { - return ( -
- -
- ) - }, [t]) - return ( @@ -217,47 +175,23 @@ const Chat: FC = (props) => {
- {activeTopicOrSession === 'topic' && ( - <> - - } - filter={contentSearchFilter} - includeUser={filterIncludeUser} - onIncludeUserChange={userOutlinedItemClickHandler} - /> - {messageNavigation === 'buttons' && } - - - )} - {activeTopicOrSession === 'session' && !activeAgentId && } - {activeTopicOrSession === 'session' && activeAgentId && !activeSessionId && } - {activeTopicOrSession === 'session' && activeAgentId && activeSessionId && ( - <> - {!apiServer.enabled ? ( - - ) : ( - <> - - - - - - - - )} - {messageNavigation === 'buttons' && } - - - )} + + } + filter={contentSearchFilter} + includeUser={filterIncludeUser} + onIncludeUserChange={userOutlinedItemClickHandler} + /> + {messageNavigation === 'buttons' && } + {isMultiSelectMode && }
@@ -311,9 +245,4 @@ const Main = styled(Flex)` position: relative; ` -const PinnedTodoPanelWrapper = styled.div` - margin-top: auto; - padding: 0 18px 8px 18px; -` - export default Chat diff --git a/src/renderer/src/pages/home/HomePage.tsx b/src/renderer/src/pages/home/HomePage.tsx index d52104425fc..92beb8574bb 100644 --- a/src/renderer/src/pages/home/HomePage.tsx +++ b/src/renderer/src/pages/home/HomePage.tsx @@ -1,12 +1,9 @@ import { ErrorBoundary } from '@renderer/components/ErrorBoundary' -import { useAgentSessionInitializer } from '@renderer/hooks/agents/useAgentSessionInitializer' import { useAssistants } from '@renderer/hooks/useAssistant' -import { useRuntime } from '@renderer/hooks/useRuntime' import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings' import { useActiveTopic } from '@renderer/hooks/useTopic' import NavigationService from '@renderer/services/NavigationService' import { newMessagesActions } from '@renderer/store/newMessage' -import { setActiveAgentId, setActiveTopicOrSessionAction } from '@renderer/store/runtime' import type { Assistant, Topic } from '@renderer/types' import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, SECOND_MIN_WINDOW_WIDTH } from '@shared/config/constant' import { AnimatePresence, motion } from 'motion/react' @@ -27,9 +24,6 @@ const HomePage: FC = () => { const navigate = useNavigate() const { isLeftNavbar } = useNavbarPosition() - // Initialize agent session hook - useAgentSessionInitializer() - const location = useLocation() const state = location.state @@ -39,26 +33,20 @@ const HomePage: FC = () => { const { activeTopic, setActiveTopic: _setActiveTopic } = useActiveTopic(activeAssistant?.id ?? '', state?.topic) const { showAssistants, showTopics, topicPosition } = useSettings() const dispatch = useDispatch() - const { chat } = useRuntime() - const { activeTopicOrSession } = chat _activeAssistant = activeAssistant const setActiveAssistant = useCallback( - // TODO: allow to set it as null. (newAssistant: Assistant) => { if (newAssistant.id === activeAssistant?.id) return startTransition(() => { _setActiveAssistant(newAssistant) - if (newAssistant.id !== 'fake') { - dispatch(setActiveAgentId(null)) - } // 同步更新 active topic,避免不必要的重新渲染 const newTopic = newAssistant.topics[0] _setActiveTopic((prev) => (newTopic?.id === prev.id ? prev : newTopic)) }) }, - [_setActiveTopic, activeAssistant?.id, dispatch] + [_setActiveTopic, activeAssistant?.id] ) const setActiveTopic = useCallback( @@ -66,7 +54,6 @@ const HomePage: FC = () => { startTransition(() => { _setActiveTopic((prev) => (newTopic?.id === prev.id ? prev : newTopic)) dispatch(newMessagesActions.setTopicFulfilled({ topicId: newTopic.id, fulfilled: false })) - dispatch(setActiveTopicOrSessionAction('topic')) }) }, [_setActiveTopic, dispatch] @@ -100,7 +87,6 @@ const HomePage: FC = () => { setActiveTopic={setActiveTopic} setActiveAssistant={setActiveAssistant} position="left" - activeTopicOrSession={activeTopicOrSession} /> )} diff --git a/src/renderer/src/pages/home/Messages/MessageHeader.tsx b/src/renderer/src/pages/home/Messages/MessageHeader.tsx index 4b45b25dfbd..998477d583d 100644 --- a/src/renderer/src/pages/home/Messages/MessageHeader.tsx +++ b/src/renderer/src/pages/home/Messages/MessageHeader.tsx @@ -21,6 +21,7 @@ import { Sparkle } from 'lucide-react' import type { FC } from 'react' import { memo, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { useLocation } from 'react-router-dom' import styled from 'styled-components' import MessageTokens from './MessageTokens' @@ -43,9 +44,10 @@ const MessageHeader: FC = memo(({ assistant, model, message, topic, isGro const { theme } = useTheme() const { userName, sidebarIcons } = useSettings() const { chat } = useRuntime() - const { activeTopicOrSession, activeAgentId } = chat + const { activeAgentId } = chat const { agent } = useAgent(activeAgentId) - const isAgentView = activeTopicOrSession === 'session' + const { pathname } = useLocation() + const isAgentView = pathname.startsWith('/agents') const { t } = useTranslation() const { isBubbleStyle } = useMessageStyle() const { openMinappById } = useMinappPopup() diff --git a/src/renderer/src/pages/home/Navbar.tsx b/src/renderer/src/pages/home/Navbar.tsx index 2eaeacb74e3..3bfb44b787b 100644 --- a/src/renderer/src/pages/home/Navbar.tsx +++ b/src/renderer/src/pages/home/Navbar.tsx @@ -26,16 +26,9 @@ interface Props { setActiveTopic: (topic: Topic) => void setActiveAssistant: (assistant: Assistant) => void position: 'left' | 'right' - activeTopicOrSession?: 'topic' | 'session' } -const HeaderNavbar: FC = ({ - activeAssistant, - setActiveAssistant, - activeTopic, - setActiveTopic, - activeTopicOrSession -}) => { +const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => { const { showAssistants, toggleShowAssistants } = useShowAssistants() const { topicPosition, narrowMode } = useSettings() const { showTopics, toggleShowTopics } = useShowTopics() @@ -121,10 +114,9 @@ const HeaderNavbar: FC = ({ diff --git a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx index 79d7f64d7a4..a09e562eedd 100644 --- a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx @@ -1,24 +1,21 @@ +import { createSelector } from '@reduxjs/toolkit' import Scrollbar from '@renderer/components/Scrollbar' -import { useAgents } from '@renderer/hooks/agents/useAgents' -import { useApiServer } from '@renderer/hooks/useApiServer' import { useAssistants } from '@renderer/hooks/useAssistant' import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets' -import { useRuntime } from '@renderer/hooks/useRuntime' import { useAssistantsTabSortType } from '@renderer/hooks/useStore' import { useTags } from '@renderer/hooks/useTags' -import type { Assistant, AssistantsSortType, Topic } from '@renderer/types' +import type { RootState } from '@renderer/store' +import { useAppSelector } from '@renderer/store' +import type { Assistant, AssistantsSortType } from '@renderer/types' import type { FC } from 'react' -import { useCallback, useRef, useState } from 'react' +import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' +import * as tinyPinyin from 'tiny-pinyin' -import UnifiedAddButton from './components/UnifiedAddButton' -import { UnifiedList } from './components/UnifiedList' -import { UnifiedTagGroups } from './components/UnifiedTagGroups' -import { useActiveAgent } from './hooks/useActiveAgent' -import { useUnifiedGrouping } from './hooks/useUnifiedGrouping' -import { useUnifiedItems } from './hooks/useUnifiedItems' -import { useUnifiedSorting } from './hooks/useUnifiedSorting' +import AssistantAddButton from './components/AssistantAddButton' +import { AssistantList } from './components/AssistantList' +import { AssistantTagGroups } from './components/AssistantTagGroups' interface AssistantsTabProps { activeAssistant: Assistant @@ -27,52 +24,89 @@ interface AssistantsTabProps { onCreateDefaultAssistant: () => void } +const selectTagsOrder = createSelector( + [(state: RootState) => state.assistants], + (assistants) => assistants.tagsOrder ?? [] +) + const AssistantsTab: FC = (props) => { const { activeAssistant, setActiveAssistant, onCreateAssistant, onCreateDefaultAssistant } = props const containerRef = useRef(null) - const { apiServerConfig } = useApiServer() - const apiServerEnabled = apiServerConfig.enabled - const { chat } = useRuntime() const { t } = useTranslation() - // Agent related hooks - const { agents, deleteAgent, isLoading: agentsLoading, error: agentsError } = useAgents() - const { activeAgentId } = chat - const { setActiveAgentId } = useActiveAgent() - // Assistant related hooks const { assistants, removeAssistant, copyAssistant, updateAssistants } = useAssistants() const { addAssistantPreset } = useAssistantPresets() const { collapsedTags, toggleTagCollapse } = useTags() const { assistantsTabSortType = 'list', setAssistantsTabSortType } = useAssistantsTabSortType() const [dragging, setDragging] = useState(false) - - // Unified items management - const { unifiedItems, handleUnifiedListReorder } = useUnifiedItems({ - agents, - assistants, - apiServerEnabled, - agentsLoading, - agentsError, - updateAssistants - }) + const savedTagsOrder = useAppSelector(selectTagsOrder) // Sorting - const { sortByPinyinAsc, sortByPinyinDesc } = useUnifiedSorting({ - unifiedItems, - updateAssistants - }) + const sortByPinyin = useCallback( + (isAscending: boolean) => { + const sorted = [...assistants].sort((a, b) => { + const pinyinA = tinyPinyin.convertToPinyin(a.name, '', true) + const pinyinB = tinyPinyin.convertToPinyin(b.name, '', true) + return isAscending ? pinyinA.localeCompare(pinyinB) : pinyinB.localeCompare(pinyinA) + }) + updateAssistants(sorted) + }, + [assistants, updateAssistants] + ) + + const sortByPinyinAsc = useCallback(() => sortByPinyin(true), [sortByPinyin]) + const sortByPinyinDesc = useCallback(() => sortByPinyin(false), [sortByPinyin]) // Grouping - const { groupedUnifiedItems, handleUnifiedGroupReorder } = useUnifiedGrouping({ - unifiedItems, - assistants, - agents, - apiServerEnabled, - agentsLoading, - agentsError, - updateAssistants - }) + const groupedAssistantItems = useMemo(() => { + const groups = new Map() + + assistants.forEach((assistant) => { + const tags = assistant.tags?.length ? assistant.tags : [t('assistants.tags.untagged')] + tags.forEach((tag) => { + if (!groups.has(tag)) { + groups.set(tag, []) + } + groups.get(tag)!.push(assistant) + }) + }) + + const untaggedKey = t('assistants.tags.untagged') + const sortedGroups = Array.from(groups.entries()).sort(([tagA], [tagB]) => { + if (tagA === untaggedKey) return -1 + if (tagB === untaggedKey) return 1 + + if (savedTagsOrder.length > 0) { + const indexA = savedTagsOrder.indexOf(tagA) + const indexB = savedTagsOrder.indexOf(tagB) + if (indexA !== -1 && indexB !== -1) return indexA - indexB + if (indexA !== -1) return -1 + if (indexB !== -1) return 1 + } + + return 0 + }) + + return sortedGroups.map(([tag, items]) => ({ tag, items })) + }, [assistants, t, savedTagsOrder]) + + const handleAssistantGroupReorder = useCallback( + (tag: string, newGroupList: Assistant[]) => { + let insertIndex = 0 + const updatedAssistants = assistants.map((a) => { + const tags = a.tags?.length ? a.tags : [t('assistants.tags.untagged')] + if (tags.includes(tag)) { + const replaced = newGroupList[insertIndex] + insertIndex += 1 + return replaced || a + } + return a + }) + updateAssistants(updatedAssistants) + }, + [assistants, t, updateAssistants] + ) const onDeleteAssistant = useCallback( (assistant: Assistant) => { @@ -98,53 +132,22 @@ const AssistantsTab: FC = (props) => { [setAssistantsTabSortType] ) - const handleAgentPress = useCallback( - (agentId: string) => { - setActiveAgentId(agentId) - // TODO: should allow it to be null - setActiveAssistant({ - id: 'fake', - name: '', - prompt: '', - topics: [ - { - id: 'fake', - assistantId: 'fake', - name: 'fake', - createdAt: '', - updatedAt: '', - messages: [] - } as unknown as Topic - ], - type: 'chat' - }) - }, - [setActiveAgentId, setActiveAssistant] - ) - return ( - + {assistantsTabSortType === 'tags' ? ( - setDragging(true)} onDragEnd={() => setDragging(false)} onToggleTagCollapse={toggleTagCollapse} onAssistantSwitch={setActiveAssistant} onAssistantDelete={onDeleteAssistant} - onAgentDelete={deleteAgent} - onAgentPress={handleAgentPress} addPreset={addAssistantPreset} copyAssistant={copyAssistant} onCreateDefaultAssistant={onCreateDefaultAssistant} @@ -153,18 +156,15 @@ const AssistantsTab: FC = (props) => { sortByPinyinDesc={sortByPinyinDesc} /> ) : ( - setDragging(true)} onDragEnd={() => setDragging(false)} onAssistantSwitch={setActiveAssistant} onAssistantDelete={onDeleteAssistant} - onAgentDelete={deleteAgent} - onAgentPress={handleAgentPress} addPreset={addAssistantPreset} copyAssistant={copyAssistant} onCreateDefaultAssistant={onCreateDefaultAssistant} diff --git a/src/renderer/src/pages/home/Tabs/SessionsTab.tsx b/src/renderer/src/pages/home/Tabs/SessionsTab.tsx deleted file mode 100644 index 3fd3881d9cf..00000000000 --- a/src/renderer/src/pages/home/Tabs/SessionsTab.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useRuntime } from '@renderer/hooks/useRuntime' -import { useSettings } from '@renderer/hooks/useSettings' -import { cn } from '@renderer/utils' -import { Alert } from 'antd' -import { AnimatePresence, motion } from 'framer-motion' -import type { FC } from 'react' -import { memo } from 'react' -import { useTranslation } from 'react-i18next' - -import Sessions from './components/Sessions' - -interface SessionsTabProps {} - -const SessionsTab: FC = () => { - const { chat } = useRuntime() - const { activeAgentId } = chat - const { t } = useTranslation() - const { apiServer } = useSettings() - - if (!apiServer.enabled) { - return - } - - if (!activeAgentId) { - return - } - - return ( - - - - - - ) -} - -export default memo(SessionsTab) diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index 64006303571..6aee3540bbd 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -1,11 +1,7 @@ -import { useRuntime } from '@renderer/hooks/useRuntime' import type { Assistant, Topic } from '@renderer/types' import type { FC } from 'react' import { Topics } from './components/Topics' -import SessionsTab from './SessionsTab' - -// const logger = loggerService.withContext('TopicsTab') interface Props { assistant: Assistant @@ -15,15 +11,7 @@ interface Props { } const TopicsTab: FC = (props) => { - const { chat } = useRuntime() - const { activeTopicOrSession } = chat - if (activeTopicOrSession === 'topic') { - return - } - if (activeTopicOrSession === 'session') { - return - } - return 'Not a valid state.' + return } export default TopicsTab diff --git a/src/renderer/src/pages/home/Tabs/components/AssistantAddButton.tsx b/src/renderer/src/pages/home/Tabs/components/AssistantAddButton.tsx new file mode 100644 index 00000000000..7d6a67a4bd3 --- /dev/null +++ b/src/renderer/src/pages/home/Tabs/components/AssistantAddButton.tsx @@ -0,0 +1,19 @@ +import AddButton from '@renderer/components/AddButton' +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' + +interface AssistantAddButtonProps { + onCreateAssistant: () => void +} + +const AssistantAddButton: FC = ({ onCreateAssistant }) => { + const { t } = useTranslation() + + return ( +
+ {t('chat.add.assistant.title')} +
+ ) +} + +export default AssistantAddButton diff --git a/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx index 926b0644823..a82141ce4fc 100644 --- a/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx +++ b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx @@ -6,8 +6,6 @@ import { useSettings } from '@renderer/hooks/useSettings' import { useTags } from '@renderer/hooks/useTags' import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' -import { useAppDispatch } from '@renderer/store' -import { setActiveTopicOrSessionAction } from '@renderer/store/runtime' import type { Assistant, AssistantsSortType } from '@renderer/types' import { cn, uuid } from '@renderer/utils' import { hasTopicPendingRequests } from '@renderer/utils/queue' @@ -70,7 +68,6 @@ const AssistantItem: FC = ({ const [isPending, setIsPending] = useState(false) const [isHovered, setIsHovered] = useState(false) - const dispatch = useAppDispatch() useEffect(() => { if (isActive) { @@ -140,8 +137,7 @@ const AssistantItem: FC = ({ } } onSwitch(assistant) - dispatch(setActiveTopicOrSessionAction('topic')) - }, [clickAssistantToShowTopic, onSwitch, assistant, dispatch, topicPosition]) + }, [clickAssistantToShowTopic, onSwitch, assistant, topicPosition]) const assistantName = useMemo(() => assistant.name || t('chat.default.name'), [assistant.name, t]) const fullAssistantName = useMemo( @@ -177,7 +173,7 @@ const AssistantItem: FC = ({ trigger={['click']} popupRender={(menu) =>
e.stopPropagation()}>{menu}
}> - + )} @@ -402,9 +398,9 @@ const Container = ({
{children} @@ -418,7 +414,7 @@ const AssistantNameRow = ({ }: PropsWithChildren<{} & React.HTMLAttributes>) => (
+ className={cn('flex min-w-0 flex-1 flex-row items-center gap-2 text-(--color-text) text-[13px]', className)}> {children}
) @@ -443,7 +439,7 @@ const MenuButton = ({
{children} diff --git a/src/renderer/src/pages/home/Tabs/components/AssistantList.tsx b/src/renderer/src/pages/home/Tabs/components/AssistantList.tsx new file mode 100644 index 00000000000..0b746a12931 --- /dev/null +++ b/src/renderer/src/pages/home/Tabs/components/AssistantList.tsx @@ -0,0 +1,86 @@ +import { DraggableList } from '@renderer/components/DraggableList' +import type { Assistant, AssistantsSortType } from '@renderer/types' +import type { FC } from 'react' +import { useCallback } from 'react' + +import AssistantItem from './AssistantItem' + +interface AssistantListProps { + items: Assistant[] + activeAssistantId: string + sortBy: AssistantsSortType + onReorder: (newList: Assistant[]) => void + onDragStart: () => void + onDragEnd: () => void + onAssistantSwitch: (assistant: Assistant) => void + onAssistantDelete: (assistant: Assistant) => void + addPreset: (assistant: Assistant) => void + copyAssistant: (assistant: Assistant) => void + onCreateDefaultAssistant: () => void + handleSortByChange: (sortType: AssistantsSortType) => void + sortByPinyinAsc: () => void + sortByPinyinDesc: () => void +} + +export const AssistantList: FC = (props) => { + const { + items, + activeAssistantId, + sortBy, + onReorder, + onDragStart, + onDragEnd, + onAssistantSwitch, + onAssistantDelete, + addPreset, + copyAssistant, + onCreateDefaultAssistant, + handleSortByChange, + sortByPinyinAsc, + sortByPinyinDesc + } = props + + const renderAssistantItem = useCallback( + (assistant: Assistant) => { + return ( + + ) + }, + [ + activeAssistantId, + sortBy, + onAssistantSwitch, + onAssistantDelete, + addPreset, + copyAssistant, + onCreateDefaultAssistant, + handleSortByChange, + sortByPinyinAsc, + sortByPinyinDesc + ] + ) + + return ( + `assistant-${assistant.id}`} + onUpdate={onReorder} + onDragStart={onDragStart} + onDragEnd={onDragEnd}> + {renderAssistantItem} + + ) +} diff --git a/src/renderer/src/pages/home/Tabs/components/UnifiedTagGroups.tsx b/src/renderer/src/pages/home/Tabs/components/AssistantTagGroups.tsx similarity index 56% rename from src/renderer/src/pages/home/Tabs/components/UnifiedTagGroups.tsx rename to src/renderer/src/pages/home/Tabs/components/AssistantTagGroups.tsx index e27599e1fa3..148513bd8ab 100644 --- a/src/renderer/src/pages/home/Tabs/components/UnifiedTagGroups.tsx +++ b/src/renderer/src/pages/home/Tabs/components/AssistantTagGroups.tsx @@ -4,30 +4,25 @@ import type { FC } from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import type { UnifiedItem } from '../hooks/useUnifiedItems' -import AgentItem from './AgentItem' import AssistantItem from './AssistantItem' import { TagGroup } from './TagGroup' interface GroupedItems { tag: string - items: UnifiedItem[] + items: Assistant[] } -interface UnifiedTagGroupsProps { +interface AssistantTagGroupsProps { groupedItems: GroupedItems[] activeAssistantId: string - activeAgentId: string | null sortBy: AssistantsSortType collapsedTags: Record - onGroupReorder: (tag: string, newList: UnifiedItem[]) => void + onGroupReorder: (tag: string, newList: Assistant[]) => void onDragStart: () => void onDragEnd: () => void onToggleTagCollapse: (tag: string) => void onAssistantSwitch: (assistant: Assistant) => void onAssistantDelete: (assistant: Assistant) => void - onAgentDelete: (agentId: string) => void - onAgentPress: (agentId: string) => void addPreset: (assistant: Assistant) => void copyAssistant: (assistant: Assistant) => void onCreateDefaultAssistant: () => void @@ -36,11 +31,10 @@ interface UnifiedTagGroupsProps { sortByPinyinDesc: () => void } -export const UnifiedTagGroups: FC = (props) => { +export const AssistantTagGroups: FC = (props) => { const { groupedItems, activeAssistantId, - activeAgentId, sortBy, collapsedTags, onGroupReorder, @@ -49,8 +43,6 @@ export const UnifiedTagGroups: FC = (props) => { onToggleTagCollapse, onAssistantSwitch, onAssistantDelete, - onAgentDelete, - onAgentPress, addPreset, copyAssistant, onCreateDefaultAssistant, @@ -61,45 +53,30 @@ export const UnifiedTagGroups: FC = (props) => { const { t } = useTranslation() - const renderUnifiedItem = useCallback( - (item: UnifiedItem) => { - if (item.type === 'agent') { - return ( - onAgentDelete(item.data.id)} - onPress={() => onAgentPress(item.data.id)} - /> - ) - } else { - return ( - - ) - } + const renderAssistantItem = useCallback( + (assistant: Assistant) => { + return ( + + ) }, [ - activeAgentId, activeAssistantId, sortBy, onAssistantSwitch, onAssistantDelete, - onAgentDelete, - onAgentPress, addPreset, copyAssistant, onCreateDefaultAssistant, @@ -120,11 +97,11 @@ export const UnifiedTagGroups: FC = (props) => { showTitle={group.tag !== t('assistants.tags.untagged')}> `${item.type}-${item.data.id}`} + itemKey={(assistant) => `assistant-${assistant.id}`} onUpdate={(newList) => onGroupReorder(group.tag, newList)} onDragStart={onDragStart} onDragEnd={onDragEnd}> - {renderUnifiedItem} + {renderAssistantItem} ))} diff --git a/src/renderer/src/pages/home/Tabs/components/Topics.tsx b/src/renderer/src/pages/home/Tabs/components/Topics.tsx index 61fd37512c2..a22ccf9cfa0 100644 --- a/src/renderer/src/pages/home/Tabs/components/Topics.tsx +++ b/src/renderer/src/pages/home/Tabs/components/Topics.tsx @@ -1,3 +1,4 @@ +import AddButton from '@renderer/components/AddButton' import AssistantAvatar from '@renderer/components/Avatar/AssistantAvatar' import type { DraggableVirtualListRef } from '@renderer/components/DraggableList' import { DraggableVirtualList } from '@renderer/components/DraggableList' @@ -59,7 +60,6 @@ import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components' -import AddButton from './AddButton' import { TopicManagePanel, useTopicManageMode } from './TopicManageMode' interface Props { diff --git a/src/renderer/src/pages/home/Tabs/components/UnifiedAddButton.tsx b/src/renderer/src/pages/home/Tabs/components/UnifiedAddButton.tsx deleted file mode 100644 index fecdb25cf00..00000000000 --- a/src/renderer/src/pages/home/Tabs/components/UnifiedAddButton.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import AddAssistantOrAgentPopup from '@renderer/components/Popups/AddAssistantOrAgentPopup' -import AgentModalPopup from '@renderer/components/Popups/agent/AgentModal' -import { useApiServer } from '@renderer/hooks/useApiServer' -import { useAppDispatch } from '@renderer/store' -import { setActiveTopicOrSessionAction } from '@renderer/store/runtime' -import type { AgentEntity, Assistant, Topic } from '@renderer/types' -import type { FC } from 'react' -import { useCallback } from 'react' -import { useTranslation } from 'react-i18next' - -import AddButton from './AddButton' - -interface UnifiedAddButtonProps { - onCreateAssistant: () => void - setActiveAssistant: (a: Assistant) => void - setActiveAgentId: (id: string) => void -} - -const UnifiedAddButton: FC = ({ onCreateAssistant, setActiveAssistant, setActiveAgentId }) => { - const { t } = useTranslation() - const dispatch = useAppDispatch() - const { apiServerRunning, startApiServer } = useApiServer() - - const afterCreate = useCallback( - (a: AgentEntity) => { - // TODO: should allow it to be null - setActiveAssistant({ - id: 'fake', - name: '', - prompt: '', - topics: [ - { - id: 'fake', - assistantId: 'fake', - name: 'fake', - createdAt: '', - updatedAt: '', - messages: [] - } as unknown as Topic - ], - type: 'chat' - }) - setActiveAgentId(a.id) - dispatch(setActiveTopicOrSessionAction('session')) - }, - [dispatch, setActiveAgentId, setActiveAssistant] - ) - - const handleAddButtonClick = async () => { - AddAssistantOrAgentPopup.show({ - onSelect: (type) => { - if (type === 'assistant') { - onCreateAssistant() - } - - if (type === 'agent') { - !apiServerRunning && startApiServer() - AgentModalPopup.show({ afterSubmit: afterCreate }) - } - } - }) - } - - return ( -
- {t('chat.add.assistant.title')} -
- ) -} - -export default UnifiedAddButton diff --git a/src/renderer/src/pages/home/Tabs/components/UnifiedList.tsx b/src/renderer/src/pages/home/Tabs/components/UnifiedList.tsx deleted file mode 100644 index a5dda02fddb..00000000000 --- a/src/renderer/src/pages/home/Tabs/components/UnifiedList.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { DraggableList } from '@renderer/components/DraggableList' -import type { Assistant, AssistantsSortType } from '@renderer/types' -import type { FC } from 'react' -import { useCallback } from 'react' - -import type { UnifiedItem } from '../hooks/useUnifiedItems' -import AgentItem from './AgentItem' -import AssistantItem from './AssistantItem' - -interface UnifiedListProps { - items: UnifiedItem[] - activeAssistantId: string - activeAgentId: string | null - sortBy: AssistantsSortType - onReorder: (newList: UnifiedItem[]) => void - onDragStart: () => void - onDragEnd: () => void - onAssistantSwitch: (assistant: Assistant) => void - onAssistantDelete: (assistant: Assistant) => void - onAgentDelete: (agentId: string) => void - onAgentPress: (agentId: string) => void - addPreset: (assistant: Assistant) => void - copyAssistant: (assistant: Assistant) => void - onCreateDefaultAssistant: () => void - handleSortByChange: (sortType: AssistantsSortType) => void - sortByPinyinAsc: () => void - sortByPinyinDesc: () => void -} - -export const UnifiedList: FC = (props) => { - const { - items, - activeAssistantId, - activeAgentId, - sortBy, - onReorder, - onDragStart, - onDragEnd, - onAssistantSwitch, - onAssistantDelete, - onAgentDelete, - onAgentPress, - addPreset, - copyAssistant, - onCreateDefaultAssistant, - handleSortByChange, - sortByPinyinAsc, - sortByPinyinDesc - } = props - - const renderUnifiedItem = useCallback( - (item: UnifiedItem) => { - if (item.type === 'agent') { - return ( - onAgentDelete(item.data.id)} - onPress={() => onAgentPress(item.data.id)} - /> - ) - } else { - return ( - - ) - } - }, - [ - activeAgentId, - activeAssistantId, - sortBy, - onAssistantSwitch, - onAssistantDelete, - onAgentDelete, - onAgentPress, - addPreset, - copyAssistant, - onCreateDefaultAssistant, - handleSortByChange, - sortByPinyinAsc, - sortByPinyinDesc - ] - ) - - return ( - `${item.type}-${item.data.id}`} - onUpdate={onReorder} - onDragStart={onDragStart} - onDragEnd={onDragEnd}> - {renderUnifiedItem} - - ) -} diff --git a/src/renderer/src/pages/home/Tabs/hooks/useActiveAgent.ts b/src/renderer/src/pages/home/Tabs/hooks/useActiveAgent.ts deleted file mode 100644 index 8f65af437d1..00000000000 --- a/src/renderer/src/pages/home/Tabs/hooks/useActiveAgent.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useAgentSessionInitializer } from '@renderer/hooks/agents/useAgentSessionInitializer' -import { useAppDispatch } from '@renderer/store' -import { setActiveAgentId as setActiveAgentIdAction } from '@renderer/store/runtime' -import { useCallback } from 'react' - -export const useActiveAgent = () => { - const dispatch = useAppDispatch() - const { initializeAgentSession } = useAgentSessionInitializer() - - const setActiveAgentId = useCallback( - async (id: string) => { - dispatch(setActiveAgentIdAction(id)) - await initializeAgentSession(id) - }, - [dispatch, initializeAgentSession] - ) - - return { setActiveAgentId } -} diff --git a/src/renderer/src/pages/home/Tabs/hooks/useUnifiedGrouping.ts b/src/renderer/src/pages/home/Tabs/hooks/useUnifiedGrouping.ts deleted file mode 100644 index cff7b865eb7..00000000000 --- a/src/renderer/src/pages/home/Tabs/hooks/useUnifiedGrouping.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit' -import type { RootState } from '@renderer/store' -import { useAppDispatch, useAppSelector } from '@renderer/store' -import { setUnifiedListOrder } from '@renderer/store/assistants' -import type { AgentEntity, Assistant } from '@renderer/types' -import { useCallback, useMemo } from 'react' -import { useTranslation } from 'react-i18next' - -import type { UnifiedItem } from './useUnifiedItems' - -interface UseUnifiedGroupingOptions { - unifiedItems: UnifiedItem[] - assistants: Assistant[] - agents: AgentEntity[] - apiServerEnabled: boolean - agentsLoading: boolean - agentsError: Error | null - updateAssistants: (assistants: Assistant[]) => void -} - -export const useUnifiedGrouping = (options: UseUnifiedGroupingOptions) => { - const { unifiedItems, assistants, agents, apiServerEnabled, agentsLoading, agentsError, updateAssistants } = options - const { t } = useTranslation() - const dispatch = useAppDispatch() - - // Selector to get tagsOrder from Redux store - const selectTagsOrder = useMemo( - () => createSelector([(state: RootState) => state.assistants], (assistants) => assistants.tagsOrder ?? []), - [] - ) - const savedTagsOrder = useAppSelector(selectTagsOrder) - - // Group unified items by tags - const groupedUnifiedItems = useMemo(() => { - const groups = new Map() - - unifiedItems.forEach((item) => { - if (item.type === 'agent') { - // Agents go to untagged group - const groupKey = t('assistants.tags.untagged') - if (!groups.has(groupKey)) { - groups.set(groupKey, []) - } - groups.get(groupKey)!.push(item) - } else { - // Assistants use their tags - const tags = item.data.tags?.length ? item.data.tags : [t('assistants.tags.untagged')] - tags.forEach((tag) => { - if (!groups.has(tag)) { - groups.set(tag, []) - } - groups.get(tag)!.push(item) - }) - } - }) - - // Sort groups: untagged first, then by savedTagsOrder - const untaggedKey = t('assistants.tags.untagged') - const sortedGroups = Array.from(groups.entries()).sort(([tagA], [tagB]) => { - if (tagA === untaggedKey) return -1 - if (tagB === untaggedKey) return 1 - - if (savedTagsOrder.length > 0) { - const indexA = savedTagsOrder.indexOf(tagA) - const indexB = savedTagsOrder.indexOf(tagB) - - if (indexA !== -1 && indexB !== -1) { - return indexA - indexB - } - - if (indexA !== -1) return -1 - - if (indexB !== -1) return 1 - } - - return 0 - }) - - return sortedGroups.map(([tag, items]) => ({ tag, items })) - }, [unifiedItems, t, savedTagsOrder]) - - const handleUnifiedGroupReorder = useCallback( - (tag: string, newGroupList: UnifiedItem[]) => { - // Extract only assistants from the new list for updating - const newAssistants = newGroupList.filter((item) => item.type === 'assistant').map((item) => item.data) - - // Update assistants state - let insertIndex = 0 - const updatedAssistants = assistants.map((a) => { - const tags = a.tags?.length ? a.tags : [t('assistants.tags.untagged')] - if (tags.includes(tag)) { - const replaced = newAssistants[insertIndex] - insertIndex += 1 - return replaced || a - } - return a - }) - updateAssistants(updatedAssistants) - - // Rebuild unified order and save to Redux - const newUnifiedItems: UnifiedItem[] = [] - const availableAgents = new Map() - const availableAssistants = new Map() - - if (apiServerEnabled && !agentsLoading && !agentsError) { - agents.forEach((agent) => availableAgents.set(agent.id, agent)) - } - updatedAssistants.forEach((assistant) => availableAssistants.set(assistant.id, assistant)) - - // Reconstruct order based on current groupedUnifiedItems structure - groupedUnifiedItems.forEach((group) => { - if (group.tag === tag) { - // Use the new group list for this tag - newGroupList.forEach((item) => { - newUnifiedItems.push(item) - if (item.type === 'agent') { - availableAgents.delete(item.data.id) - } else { - availableAssistants.delete(item.data.id) - } - }) - } else { - // Keep existing order for other tags - group.items.forEach((item) => { - newUnifiedItems.push(item) - if (item.type === 'agent') { - availableAgents.delete(item.data.id) - } else { - availableAssistants.delete(item.data.id) - } - }) - } - }) - - // Add any remaining items - availableAgents.forEach((agent) => newUnifiedItems.push({ type: 'agent', data: agent })) - availableAssistants.forEach((assistant) => newUnifiedItems.push({ type: 'assistant', data: assistant })) - - // Save to Redux - const orderToSave = newUnifiedItems.map((item) => ({ - type: item.type, - id: item.data.id - })) - dispatch(setUnifiedListOrder(orderToSave)) - }, - [ - assistants, - t, - updateAssistants, - apiServerEnabled, - agentsLoading, - agentsError, - agents, - groupedUnifiedItems, - dispatch - ] - ) - - return { - groupedUnifiedItems, - handleUnifiedGroupReorder - } -} diff --git a/src/renderer/src/pages/home/Tabs/hooks/useUnifiedItems.ts b/src/renderer/src/pages/home/Tabs/hooks/useUnifiedItems.ts deleted file mode 100644 index 1fd35c3b311..00000000000 --- a/src/renderer/src/pages/home/Tabs/hooks/useUnifiedItems.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { useAppDispatch, useAppSelector } from '@renderer/store' -import { setUnifiedListOrder } from '@renderer/store/assistants' -import type { AgentEntity, Assistant } from '@renderer/types' -import { useCallback, useMemo } from 'react' - -export type UnifiedItem = { type: 'agent'; data: AgentEntity } | { type: 'assistant'; data: Assistant } - -interface UseUnifiedItemsOptions { - agents: AgentEntity[] - assistants: Assistant[] - apiServerEnabled: boolean - agentsLoading: boolean - agentsError: Error | null - updateAssistants: (assistants: Assistant[]) => void -} - -export const useUnifiedItems = (options: UseUnifiedItemsOptions) => { - const { agents, assistants, apiServerEnabled, agentsLoading, agentsError, updateAssistants } = options - const dispatch = useAppDispatch() - const unifiedListOrder = useAppSelector((state) => state.assistants.unifiedListOrder || []) - - // Create unified items list (agents + assistants) with saved order - const unifiedItems = useMemo(() => { - const items: UnifiedItem[] = [] - - // Collect all available items - const availableAgents = new Map() - const availableAssistants = new Map() - - if (apiServerEnabled && !agentsLoading && !agentsError) { - agents.forEach((agent) => availableAgents.set(agent.id, agent)) - } - assistants.forEach((assistant) => availableAssistants.set(assistant.id, assistant)) - - // Apply saved order - unifiedListOrder.forEach((item) => { - if (item.type === 'agent' && availableAgents.has(item.id)) { - items.push({ type: 'agent', data: availableAgents.get(item.id)! }) - availableAgents.delete(item.id) - } else if (item.type === 'assistant' && availableAssistants.has(item.id)) { - items.push({ type: 'assistant', data: availableAssistants.get(item.id)! }) - availableAssistants.delete(item.id) - } - }) - - // Add new items (not in saved order) to the beginning - const newItems: UnifiedItem[] = [] - availableAgents.forEach((agent) => newItems.push({ type: 'agent', data: agent })) - availableAssistants.forEach((assistant) => newItems.push({ type: 'assistant', data: assistant })) - items.unshift(...newItems) - - return items - }, [agents, assistants, apiServerEnabled, agentsLoading, agentsError, unifiedListOrder]) - - const handleUnifiedListReorder = useCallback( - (newList: UnifiedItem[]) => { - // Save the unified order to Redux - const orderToSave = newList.map((item) => ({ - type: item.type, - id: item.data.id - })) - dispatch(setUnifiedListOrder(orderToSave)) - - // Extract and update assistants order - const newAssistants = newList.filter((item) => item.type === 'assistant').map((item) => item.data) - updateAssistants(newAssistants) - }, - [dispatch, updateAssistants] - ) - - return { - unifiedItems, - handleUnifiedListReorder - } -} diff --git a/src/renderer/src/pages/home/Tabs/hooks/useUnifiedSorting.ts b/src/renderer/src/pages/home/Tabs/hooks/useUnifiedSorting.ts deleted file mode 100644 index 533427813f6..00000000000 --- a/src/renderer/src/pages/home/Tabs/hooks/useUnifiedSorting.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useAppDispatch } from '@renderer/store' -import { setUnifiedListOrder } from '@renderer/store/assistants' -import type { Assistant } from '@renderer/types' -import { useCallback } from 'react' -import * as tinyPinyin from 'tiny-pinyin' - -import type { UnifiedItem } from './useUnifiedItems' - -interface UseUnifiedSortingOptions { - unifiedItems: UnifiedItem[] - updateAssistants: (assistants: Assistant[]) => void -} - -export const useUnifiedSorting = (options: UseUnifiedSortingOptions) => { - const { unifiedItems, updateAssistants } = options - const dispatch = useAppDispatch() - - const sortUnifiedItemsByPinyin = useCallback((items: UnifiedItem[], isAscending: boolean) => { - return [...items].sort((a, b) => { - const nameA = a.type === 'agent' ? a.data.name || a.data.id : a.data.name - const nameB = b.type === 'agent' ? b.data.name || b.data.id : b.data.name - const pinyinA = tinyPinyin.convertToPinyin(nameA, '', true) - const pinyinB = tinyPinyin.convertToPinyin(nameB, '', true) - return isAscending ? pinyinA.localeCompare(pinyinB) : pinyinB.localeCompare(pinyinA) - }) - }, []) - - const sortByPinyinAsc = useCallback(() => { - const sorted = sortUnifiedItemsByPinyin(unifiedItems, true) - const orderToSave = sorted.map((item) => ({ - type: item.type, - id: item.data.id - })) - dispatch(setUnifiedListOrder(orderToSave)) - // Also update assistants order - const newAssistants = sorted.filter((item) => item.type === 'assistant').map((item) => item.data) - updateAssistants(newAssistants) - }, [unifiedItems, sortUnifiedItemsByPinyin, dispatch, updateAssistants]) - - const sortByPinyinDesc = useCallback(() => { - const sorted = sortUnifiedItemsByPinyin(unifiedItems, false) - const orderToSave = sorted.map((item) => ({ - type: item.type, - id: item.data.id - })) - dispatch(setUnifiedListOrder(orderToSave)) - // Also update assistants order - const newAssistants = sorted.filter((item) => item.type === 'assistant').map((item) => item.data) - updateAssistants(newAssistants) - }, [unifiedItems, sortUnifiedItemsByPinyin, dispatch, updateAssistants]) - - return { - sortByPinyinAsc, - sortByPinyinDesc - } -} diff --git a/src/renderer/src/pages/home/Tabs/index.tsx b/src/renderer/src/pages/home/Tabs/index.tsx index 4504550a168..b77e289e369 100644 --- a/src/renderer/src/pages/home/Tabs/index.tsx +++ b/src/renderer/src/pages/home/Tabs/index.tsx @@ -3,8 +3,6 @@ import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings' import { useShowTopics } from '@renderer/hooks/useStore' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' -import { useAppDispatch } from '@renderer/store' -import { setActiveAgentId, setActiveTopicOrSessionAction } from '@renderer/store/runtime' import type { Assistant, Topic } from '@renderer/types' import type { Tab } from '@renderer/types/chat' import { classNames, uuid } from '@renderer/utils' @@ -43,7 +41,6 @@ const HomeTabs: FC = ({ const { toggleShowTopics } = useShowTopics() const { isLeftNavbar } = useNavbarPosition() const { t } = useTranslation() - const dispatch = useAppDispatch() const [tab, setTab] = useState(position === 'left' ? _tab || 'assistants' : 'topic') const borderStyle = '0.5px solid var(--color-border)' @@ -62,8 +59,6 @@ const HomeTabs: FC = ({ const assistant = await AddAssistantPopup.show() if (assistant) { setActiveAssistant(assistant) - dispatch(setActiveAgentId(null)) - dispatch(setActiveTopicOrSessionAction('topic')) } } @@ -71,8 +66,6 @@ const HomeTabs: FC = ({ const assistant = { ...defaultAssistant, id: uuid() } addAssistant(assistant) setActiveAssistant(assistant) - dispatch(setActiveAgentId(null)) - dispatch(setActiveTopicOrSessionAction('topic')) } useEffect(() => { diff --git a/src/renderer/src/pages/home/components/ChatNavBar/ChatNavbarContent/index.tsx b/src/renderer/src/pages/home/components/ChatNavBar/ChatNavbarContent/index.tsx index f3f020bec6d..c4ee197d22e 100644 --- a/src/renderer/src/pages/home/components/ChatNavBar/ChatNavbarContent/index.tsx +++ b/src/renderer/src/pages/home/components/ChatNavBar/ChatNavbarContent/index.tsx @@ -1,9 +1,6 @@ -import { useActiveAgent } from '@renderer/hooks/agents/useActiveAgent' -import { useRuntime } from '@renderer/hooks/useRuntime' import type { Assistant } from '@renderer/types' import type { FC } from 'react' -import AgentContent from './AgentContent' import TopicContent from './TopicContent' interface Props { @@ -11,14 +8,9 @@ interface Props { } const ChatNavbarContent: FC = ({ assistant }) => { - const { chat } = useRuntime() - const { activeTopicOrSession } = chat - const { agent: activeAgent } = useActiveAgent() - return (
- {activeTopicOrSession === 'topic' && } - {activeTopicOrSession === 'session' && activeAgent && } +
) } diff --git a/src/renderer/src/pages/home/components/ChatNavBar/Tools/SettingsButton.tsx b/src/renderer/src/pages/home/components/ChatNavBar/Tools/SettingsButton.tsx index 4a68bcf8408..48cf9aaf754 100644 --- a/src/renderer/src/pages/home/components/ChatNavBar/Tools/SettingsButton.tsx +++ b/src/renderer/src/pages/home/components/ChatNavBar/Tools/SettingsButton.tsx @@ -1,4 +1,3 @@ -import { useRuntime } from '@renderer/hooks/useRuntime' import type { Assistant } from '@renderer/types' import { Drawer, Tooltip } from 'antd' import { t } from 'i18next' @@ -7,7 +6,7 @@ import type { FC } from 'react' import { useState } from 'react' import NavbarIcon from '../../../../../components/NavbarIcon' -import { AgentSettingsTab, AssistantSettingsTab } from './SettingsTab' +import { AssistantSettingsTab } from './SettingsTab' interface Props { assistant?: Assistant @@ -15,10 +14,6 @@ interface Props { const SettingsButton: FC = ({ assistant }) => { const [settingsOpen, setSettingsOpen] = useState(false) - const { chat } = useRuntime() - - const isTopicSettings = chat.activeTopicOrSession === 'topic' - const isAgentSettings = chat.activeTopicOrSession === 'session' return ( <> @@ -34,8 +29,7 @@ const SettingsButton: FC = ({ assistant }) => { width="var(--assistants-width)" closable={false} styles={{ body: { padding: 0, paddingTop: 'var(--navbar-height)' } }}> - {isTopicSettings && assistant && } - {isAgentSettings && } + {assistant && } ) diff --git a/src/renderer/src/pages/home/components/ChatNavBar/Tools/SettingsTab/AssistantSettingsTab.tsx b/src/renderer/src/pages/home/components/ChatNavBar/Tools/SettingsTab/AssistantSettingsTab.tsx index 39fc216f03a..3b1ace5cc92 100644 --- a/src/renderer/src/pages/home/components/ChatNavBar/Tools/SettingsTab/AssistantSettingsTab.tsx +++ b/src/renderer/src/pages/home/components/ChatNavBar/Tools/SettingsTab/AssistantSettingsTab.tsx @@ -1,4 +1,3 @@ -import { loggerService } from '@logger' import EditableNumber from '@renderer/components/EditableNumber' import Scrollbar from '@renderer/components/Scrollbar' import Selector from '@renderer/components/Selector' @@ -9,7 +8,6 @@ import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import { useTheme } from '@renderer/context/ThemeProvider' import { useAssistant } from '@renderer/hooks/useAssistant' import { useProvider } from '@renderer/hooks/useProvider' -import { useRuntime } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' import useTranslate from '@renderer/hooks/useTranslate' import { SettingDivider, SettingRow, SettingRowTitle } from '@renderer/pages/settings' @@ -62,14 +60,11 @@ import styled from 'styled-components' import GroqSettingsGroup from './GroqSettingsGroup' import OpenAISettingsGroup from './OpenAISettingsGroup' -const logger = loggerService.withContext('AssistantSettingsTab') - interface Props { assistant: Assistant } const AssistantSettingsTab = (props: Props) => { - const { chat } = useRuntime() const { assistant } = useAssistant(props.assistant.id) const { provider } = useProvider(assistant.model.provider) @@ -150,13 +145,6 @@ const AssistantSettingsTab = (props: Props) => { isSupportServiceTierProvider(provider) || (isSupportVerbosityModel(model) && isSupportVerbosityProvider(provider)) - const isTopicSettings = chat.activeTopicOrSession === 'topic' - - if (!isTopicSettings) { - logger.warn('AssistantSettingsTab is rendered when not topic activated.') - return null - } - return ( {showOpenAiSettings && ( diff --git a/src/renderer/src/pages/home/components/ChatNavBar/Tools/SettingsTab/index.tsx b/src/renderer/src/pages/home/components/ChatNavBar/Tools/SettingsTab/index.tsx index 9cac560a6cc..96c2514dbd2 100644 --- a/src/renderer/src/pages/home/components/ChatNavBar/Tools/SettingsTab/index.tsx +++ b/src/renderer/src/pages/home/components/ChatNavBar/Tools/SettingsTab/index.tsx @@ -1,4 +1,3 @@ -import AgentSettingsTab from './AgentSettingsTab' import AssistantSettingsTab from './AssistantSettingsTab' -export { AgentSettingsTab, AssistantSettingsTab } +export { AssistantSettingsTab } diff --git a/src/renderer/src/pages/settings/AgentSettings/components/ModelSetting.tsx b/src/renderer/src/pages/settings/AgentSettings/components/ModelSetting.tsx index a23bd7da5ae..54e17c2591b 100644 --- a/src/renderer/src/pages/settings/AgentSettings/components/ModelSetting.tsx +++ b/src/renderer/src/pages/settings/AgentSettings/components/ModelSetting.tsx @@ -1,5 +1,5 @@ import { HelpTooltip } from '@renderer/components/TooltipIcons' -import SelectAgentBaseModelButton from '@renderer/pages/home/components/SelectAgentBaseModelButton' +import SelectAgentBaseModelButton from '@renderer/pages/agents/components/SelectAgentBaseModelButton' import type { AgentBaseWithId, ApiModel, UpdateAgentFunctionUnion } from '@renderer/types' import { useTranslation } from 'react-i18next' diff --git a/src/renderer/src/pages/settings/DisplaySettings/SidebarIconsManager.tsx b/src/renderer/src/pages/settings/DisplaySettings/SidebarIconsManager.tsx index 0f016c877f7..19febe8007d 100644 --- a/src/renderer/src/pages/settings/DisplaySettings/SidebarIconsManager.tsx +++ b/src/renderer/src/pages/settings/DisplaySettings/SidebarIconsManager.tsx @@ -14,6 +14,7 @@ import { Languages, LayoutGrid, MessageSquareQuote, + MousePointerClick, NotepadText, Palette, Sparkle @@ -117,6 +118,7 @@ const SidebarIconsManager: FC = ({ () => ({ assistants: , + agents: , store: , paintings: , translate: , diff --git a/src/renderer/src/store/assistants.ts b/src/renderer/src/store/assistants.ts index 02a39bfa930..729d3d3a876 100644 --- a/src/renderer/src/store/assistants.ts +++ b/src/renderer/src/store/assistants.ts @@ -31,6 +31,7 @@ export interface AssistantsState { tagsOrder: string[] collapsedTags: Record presets: AssistantPreset[] + /** @deprecated should be removed in v2 */ unifiedListOrder: Array<{ type: 'agent' | 'assistant'; id: string }> } diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 0f4c061c1ab..0d5708c5099 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -86,7 +86,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 202, + version: 203, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index aefe18a652d..870482da390 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -3315,6 +3315,39 @@ const migrateConfig = { logger.error('migrate 202 error', error as Error) return state } + }, + '203': (state: RootState) => { + try { + if (state.settings && state.settings.sidebarIcons) { + // Add 'agents' to visible icons if not already present + if (!state.settings.sidebarIcons.visible.includes('agents')) { + // Insert after 'assistants' if present, otherwise append + const assistantsIndex = state.settings.sidebarIcons.visible.indexOf('assistants') + if (assistantsIndex !== -1) { + state.settings.sidebarIcons.visible = [ + ...state.settings.sidebarIcons.visible.slice(0, assistantsIndex + 1), + 'agents', + ...state.settings.sidebarIcons.visible.slice(assistantsIndex + 1) + ] + } else { + state.settings.sidebarIcons.visible = [...state.settings.sidebarIcons.visible, 'agents'] + } + } + } + + // Add 'agents' tab if not already present + if (state.tabs && !state.tabs.tabs.some((tab: { id: string }) => tab.id === 'agents')) { + const homeIndex = state.tabs.tabs.findIndex((tab: { id: string }) => tab.id === 'home') + const insertIndex = homeIndex !== -1 ? homeIndex + 1 : state.tabs.tabs.length + state.tabs.tabs.splice(insertIndex, 0, { id: 'agents', path: '/agents' }) + } + + logger.info('migrate 203 success') + return state + } catch (error) { + logger.error('migrate 203 error', error as Error) + return state + } } } diff --git a/src/renderer/src/store/runtime.ts b/src/renderer/src/store/runtime.ts index f251bf7da6c..7e75ae40c89 100644 --- a/src/renderer/src/store/runtime.ts +++ b/src/renderer/src/store/runtime.ts @@ -29,8 +29,6 @@ export interface ChatState { /** UI state. Map agent id to active session id. * null represents no active session */ activeSessionIdMap: Record - /** meanwhile active Assistants or Agents */ - activeTopicOrSession: 'topic' | 'session' /** topic ids that are currently being renamed */ renamingTopics: string[] /** topic ids that are newly renamed */ @@ -77,6 +75,9 @@ export interface RuntimeState { detectedRegion: MinAppRegion | null /** Query whether a task is processing or not. undefined and false share same semantics. */ loadingMap: Record + // Migrated from useApiServer, it's global state now + /** Is the api server running */ + apiServerRunning: boolean } export interface ExportState { @@ -112,7 +113,6 @@ const initialState: RuntimeState = { selectedMessageIds: [], activeTopic: null, activeAgentId: null, - activeTopicOrSession: 'topic', activeSessionIdMap: {}, renamingTopics: [], newlyRenamedTopics: [] @@ -121,7 +121,8 @@ const initialState: RuntimeState = { activeSearches: {} }, detectedRegion: null, - loadingMap: {} + loadingMap: {}, + apiServerRunning: false } const runtimeSlice = createSlice({ @@ -188,9 +189,6 @@ const runtimeSlice = createSlice({ const { agentId, sessionId } = action.payload state.chat.activeSessionIdMap[agentId] = sessionId }, - setActiveTopicOrSessionAction: (state, action: PayloadAction<'topic' | 'session'>) => { - state.chat.activeTopicOrSession = action.payload - }, setRenamingTopics: (state, action: PayloadAction) => { state.chat.renamingTopics = action.payload }, @@ -218,6 +216,9 @@ const runtimeSlice = createSlice({ }, setDetectedRegion: (state, action: PayloadAction) => { state.detectedRegion = action.payload + }, + setApiServerRunningAction: (state, action: PayloadAction) => { + state.apiServerRunning = action.payload } } }) @@ -242,7 +243,6 @@ export const { setActiveTopic, setActiveAgentId, setActiveSessionIdAction, - setActiveTopicOrSessionAction, setRenamingTopics, setNewlyRenamedTopics, startLoadingAction, @@ -251,7 +251,8 @@ export const { setActiveSearches, setWebSearchStatus, // Region detection - setDetectedRegion + setDetectedRegion, + setApiServerRunningAction } = runtimeSlice.actions export default runtimeSlice.reducer diff --git a/src/renderer/src/store/tabs.ts b/src/renderer/src/store/tabs.ts index c539cf20a0d..2c45cd21d1a 100644 --- a/src/renderer/src/store/tabs.ts +++ b/src/renderer/src/store/tabs.ts @@ -32,6 +32,10 @@ const initialState: TabsState = { { id: 'home', path: '/' + }, + { + id: 'agents', + path: '/agents' } ], activeTabId: 'home' diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 00f9b93e086..cc91a1e472c 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -655,6 +655,7 @@ export const isAutoDetectionMethod = (method: string): method is AutoDetectionMe export type SidebarIcon = | 'assistants' + | 'agents' | 'store' | 'paintings' | 'translate'