From 7b770fef88ad146c072903b1348077aafe94de23 Mon Sep 17 00:00:00 2001 From: lin onetwo Date: Tue, 13 May 2025 14:21:58 +0800 Subject: [PATCH] refactor: make store smaller --- localization/locales/en/agent.json | 2 + localization/locales/fr/agent.json | 2 + localization/locales/ja/agent.json | 2 + localization/locales/ru/agent.json | 2 + localization/locales/zh_CN/agent.json | 24 +- package.json | 2 +- .../TabContent/TabTypes/ChatTabContent.tsx | 109 ++++--- .../TabContent/hooks/useAgentChat.ts | 153 ---------- src/pages/Agent/store/agentChatStore.ts | 152 ++++++++++ .../actions/basicActions.ts} | 272 ++++-------------- .../store/tabStore/actions/historyActions.ts | 121 ++++++++ .../tabStore/actions/splitViewActions.ts | 47 +++ .../store/tabStore/actions/utilityActions.ts | 18 ++ src/pages/Agent/store/tabStore/index.ts | 31 ++ src/pages/Agent/store/tabStore/types.ts | 47 +++ tsconfig.json | 2 +- 16 files changed, 563 insertions(+), 423 deletions(-) delete mode 100644 src/pages/Agent/components/TabContent/hooks/useAgentChat.ts create mode 100644 src/pages/Agent/store/agentChatStore.ts rename src/pages/Agent/store/{tabStore.ts => tabStore/actions/basicActions.ts} (52%) create mode 100644 src/pages/Agent/store/tabStore/actions/historyActions.ts create mode 100644 src/pages/Agent/store/tabStore/actions/splitViewActions.ts create mode 100644 src/pages/Agent/store/tabStore/actions/utilityActions.ts create mode 100644 src/pages/Agent/store/tabStore/index.ts create mode 100644 src/pages/Agent/store/tabStore/types.ts diff --git a/localization/locales/en/agent.json b/localization/locales/en/agent.json index 2d5aca63..f1d7e8d7 100644 --- a/localization/locales/en/agent.json +++ b/localization/locales/en/agent.json @@ -26,7 +26,9 @@ "Title": "Configuration Issue" }, "EmptyStateDescription": "Create a new session or select an existing one from the left to start chatting.", + "Error": "Dialogue error", "InputPlaceholder": "Type a message, Ctrl+Enter to send", + "Loading": "Dialogue loading", "NewSession": "New Session", "OpenInWorkspaceTiddler": "Open {{title}} in {{workspace}} workspace", "Send": "Send", diff --git a/localization/locales/fr/agent.json b/localization/locales/fr/agent.json index c7ccbcc4..7a033d7f 100644 --- a/localization/locales/fr/agent.json +++ b/localization/locales/fr/agent.json @@ -25,7 +25,9 @@ "Title": "Problème de configuration" }, "EmptyStateDescription": "Créez une nouvelle session ou sélectionnez-en une existante à gauche pour commencer à discuter.", + "Error": "Erreur de dialogue", "InputPlaceholder": "Tapez un message, Ctrl+Entrée pour envoyer", + "Loading": "Chargement de la conversation en cours", "NewSession": "Nouvelle session", "OpenInWorkspaceTiddler": "Ouvrir {{title}} dans l'espace de travail {{workspace}}", "Send": "Envoyer", diff --git a/localization/locales/ja/agent.json b/localization/locales/ja/agent.json index e3d175ef..03d7ea8b 100644 --- a/localization/locales/ja/agent.json +++ b/localization/locales/ja/agent.json @@ -26,7 +26,9 @@ "Title": "設定の問題" }, "EmptyStateDescription": "新しいセッションを作成するか、左から既存のセッションを選択して会話を開始してください。", + "Error": "会話エラー", "InputPlaceholder": "メッセージを入力、Ctrl+Enterで送信", + "Loading": "会話を読み込み中", "NewSession": "新しいセッション", "OpenInWorkspaceTiddler": "{{workspace}}ワークスペースで{{title}}を開く", "Send": "送信", diff --git a/localization/locales/ru/agent.json b/localization/locales/ru/agent.json index 28a7c181..7fb32f07 100644 --- a/localization/locales/ru/agent.json +++ b/localization/locales/ru/agent.json @@ -26,7 +26,9 @@ "Title": "Проблема с конфигурацией" }, "EmptyStateDescription": "Создайте новую сессию или выберите существующую слева, чтобы начать общение.", + "Error": "Ошибка диалога", "InputPlaceholder": "Введите сообщение, Ctrl+Enter для отправки", + "Loading": "Диалог загружается", "NewSession": "Новая сессия", "OpenInWorkspaceTiddler": "Открыть {{title}} в рабочей области {{workspace}}", "Send": "Отправить", diff --git a/localization/locales/zh_CN/agent.json b/localization/locales/zh_CN/agent.json index 89f490a8..6231f959 100644 --- a/localization/locales/zh_CN/agent.json +++ b/localization/locales/zh_CN/agent.json @@ -26,7 +26,9 @@ "Title": "配置问题" }, "EmptyStateDescription": "创建一个新会话或者从左侧选择一个已有会话来开始对话。", + "Error": "对话错误", "InputPlaceholder": "输入消息,Ctrl+Enter 发送", + "Loading": "对话加载中", "NewSession": "新建会话", "OpenInWorkspaceTiddler": "在{{workspace}}工作区中打开{{title}}", "Send": "发送", @@ -76,16 +78,6 @@ "QuickAccess": "快速访问", "SearchPlaceholder": "搜索标签页或智能体..." }, - "Search": { - "AvailableAgents": "可用的智能体", - "FailedToCreateChatWithAgent": "无法创建与智能体的对话", - "FailedToFetchAgents": "获取智能体列表失败", - "NoAgentsFound": "未找到智能体", - "NoClosedTabsFound": "没有最近关闭的标签页", - "NoTabsFound": "没有找到标签页", - "OpenTabs": "打开的标签页", - "RecentlyClosedTabs": "最近关闭的标签页" - }, "Preference": { "AIModelSelection": "AI 模型选择", "AIModelSelectionDescription": "选择您想要使用的 AI 提供商和模型", @@ -101,8 +93,8 @@ "BaseURLRequired": "API 地址为必填项", "Cancel": "取消", "CancelAddProvider": "取消添加", - "ConfigureProvider": "配置 {{provider}}", "ConfigureModelParameters": "配置参数", + "ConfigureProvider": "配置 {{provider}}", "CreateCustomModel": "创建自定义模型", "Custom": "自定义接口", "CustomProvider": "自定义提供方", @@ -164,5 +156,15 @@ "TemperatureDescription": "较低的值会产生更确定性、更集中的响应,较高的值会产生更多样化、更创造性的响应", "TopP": "Top P", "TopPDescription": "控制响应的随机性。较低的值使响应更确定,较高的值允许更多的可能性" + }, + "Search": { + "AvailableAgents": "可用的智能体", + "FailedToCreateChatWithAgent": "无法创建与智能体的对话", + "FailedToFetchAgents": "获取智能体列表失败", + "NoAgentsFound": "未找到智能体", + "NoClosedTabsFound": "没有最近关闭的标签页", + "NoTabsFound": "没有找到标签页", + "OpenTabs": "打开的标签页", + "RecentlyClosedTabs": "最近关闭的标签页" } } diff --git a/package.json b/package.json index 5eeac142..bc30bdd2 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "winston-transport": "4.9.0", "wouter": "^3.6.0", "zod": "^3.24.3", - "zustand": "^5.0.3", + "zustand": "^5.0.4", "zx": "8.3.1" }, "optionalDependencies": { diff --git a/src/pages/Agent/components/TabContent/TabTypes/ChatTabContent.tsx b/src/pages/Agent/components/TabContent/TabTypes/ChatTabContent.tsx index 0e433e70..63953e2e 100644 --- a/src/pages/Agent/components/TabContent/TabTypes/ChatTabContent.tsx +++ b/src/pages/Agent/components/TabContent/TabTypes/ChatTabContent.tsx @@ -4,14 +4,14 @@ import SendIcon from '@mui/icons-material/Send'; import SmartToyIcon from '@mui/icons-material/SmartToy'; import TuneIcon from '@mui/icons-material/Tune'; import { Avatar, Box, Button, CircularProgress, IconButton, TextField, Tooltip, Typography } from '@mui/material'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { ModelParametersDialog } from '@/pages/Preferences/sections/ExternalAPI/components/ModelParametersDialog'; import { useTaskConfigManagement } from '@/pages/Preferences/sections/ExternalAPI/useAIConfigManagement'; +import { useAgentChatStore } from '../../../store/agentChatStore'; import { IChatTab } from '../../../types/tab'; -import { useAgentChat } from '../hooks/useAgentChat'; interface ChatTabContentProps { tab: IChatTab; @@ -89,10 +89,29 @@ export const ChatTabContent: React.FC = ({ tab }) => { const [parametersDialogOpen, setParametersDialogOpen] = useState(false); const agentId = tab.agentId; const agentDefId = tab.agentDefId; - - // 使用自定义 hook 获取和管理 Agent 数据 - const { messages, loading, error, sendMessage, agent } = useAgentChat(agentId, agentDefId); - + + // Use store to get and manage Agent data + const { messages, loading, error, agent, fetchAgent, subscribeToUpdates, sendMessage, createAgent } = useAgentChatStore(); + + // Initialize and subscribe to updates + useEffect(() => { + let cleanupSubscription: (() => void) | undefined; + + if (agentId) { + // If we have agentId, fetch and subscribe to updates + void fetchAgent(agentId); + cleanupSubscription = subscribeToUpdates(agentId); + } else if (agentDefId) { + // If we don't have agentId but have agentDefId, create a new Agent + void createAgent(agentDefId); + } + + // Cleanup subscription when component unmounts + return () => { + if (cleanupSubscription) cleanupSubscription(); + }; + }, [agentId, agentDefId, fetchAgent, subscribeToUpdates, createAgent]); + const { config, handleConfigChange } = useTaskConfigManagement({ agentId: agent?.id, agentDefId: agent?.agentDefId, @@ -111,15 +130,15 @@ export const ChatTabContent: React.FC = ({ tab }) => { }, []); const handleSendMessage = useCallback(async () => { - if (!inputMessage.trim()) return; + if (!inputMessage.trim() || !agentId) return; try { - await sendMessage(inputMessage); + await sendMessage(agentId, inputMessage); setInputMessage(''); } catch (err) { console.error('Error sending message:', err); } - }, [inputMessage, sendMessage]); + }, [inputMessage, sendMessage, agentId]); const handleKeyPress = useCallback((event: React.KeyboardEvent) => { if (event.key === 'Enter' && !event.shiftKey) { @@ -133,7 +152,7 @@ export const ChatTabContent: React.FC = ({ tab }) => { {agent?.name || t(tab.title)} - {loading && } + {loading && } @@ -143,38 +162,44 @@ export const ChatTabContent: React.FC = ({ tab }) => { - {loading && messages.length === 0 ? ( - - - - {t('Chat.Loading')} - - - ) : error ? ( - - - {t('Chat.Error')}: {error.message} - - - ) : messages.length === 0 ? ( - - - - {t('Chat.StartConversation')} - - - ) : ( - messages.map(message => ( - - - {message.role === 'user' ? : } - - - {message.content} - - - )) - )} + {loading && messages.length === 0 + ? ( + + + + {t('Chat.Loading')} + + + ) + : error + ? ( + + + {t('Chat.Error')}: {error.message} + + + ) + : messages.length === 0 + ? ( + + + + {t('Chat.StartConversation')} + + + ) + : ( + messages.map(message => ( + + + {message.role === 'user' ? : } + + + {message.content} + + + )) + )} diff --git a/src/pages/Agent/components/TabContent/hooks/useAgentChat.ts b/src/pages/Agent/components/TabContent/hooks/useAgentChat.ts deleted file mode 100644 index af90b35f..00000000 --- a/src/pages/Agent/components/TabContent/hooks/useAgentChat.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* eslint-disable unicorn/prevent-abbreviations */ -import { AgentInstance, AgentInstanceMessage } from '@services/agentInstance/interface'; -import { useCallback, useEffect, useState } from 'react'; - -/** - * 用于处理 Agent 聊天功能的 Hook - * 使用 agentId 从后端获取数据,并提供操作聊天所需的方法 - */ -export const useAgentChat = (agentId?: string, agentDefId?: string) => { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [agent, setAgent] = useState(null); - const [messages, setMessages] = useState([]); - - // 获取 Agent 实例 - const fetchAgent = useCallback(async () => { - if (!agentId) return; - - try { - setLoading(true); - setError(null); - const result = await window.service.agentInstance.getAgent(agentId); - if (result) { - setAgent(result); - setMessages(result.messages); - } - } catch (err) { - setError(err instanceof Error ? err : new Error(String(err))); - console.error('Failed to fetch agent:', err); - } finally { - setLoading(false); - } - }, [agentId]); - - // 订阅 Agent 更新 - const subscribeToUpdates = useCallback(() => { - if (!agentId) return; - - try { - const subscription = window.observables.agentInstance.subscribeToAgentUpdates(agentId).subscribe({ - next: (updatedAgent) => { - if (updatedAgent) { - setAgent(updatedAgent); - setMessages(updatedAgent.messages); - } - }, - error: (err) => { - console.error('Error in agent subscription:', err); - setError(err instanceof Error ? err : new Error(String(err))); - }, - }); - - // 返回清除函数 - return () => { - subscription.unsubscribe(); - }; - } catch (err) { - console.error('Failed to subscribe to agent updates:', err); - setError(err instanceof Error ? err : new Error(String(err))); - } - }, [agentId]); - - // 发送消息到 Agent - const sendMessage = useCallback(async (content: string) => { - if (!agentId) { - setError(new Error('No agent ID provided')); - return; - } - - try { - setLoading(true); - await window.service.agentInstance.sendMsgToAgent(agentId, { text: content }); - } catch (err) { - setError(err instanceof Error ? err : new Error(String(err))); - console.error('Failed to send message:', err); - } finally { - setLoading(false); - } - }, [agentId]); - - // 创建新的 Agent 实例 - const createAgent = useCallback(async () => { - try { - setLoading(true); - const newAgent = await window.service.agentInstance.createAgent(agentDefId); - setAgent(newAgent); - setMessages(newAgent.messages); - return newAgent; - } catch (err) { - setError(err instanceof Error ? err : new Error(String(err))); - console.error('Failed to create agent:', err); - return null; - } finally { - setLoading(false); - } - }, [agentDefId]); - - // 更新 Agent - const updateAgent = useCallback(async (data: Partial) => { - if (!agentId) { - setError(new Error('No agent ID provided')); - return; - } - - try { - setLoading(true); - const updatedAgent = await window.service.agentInstance.updateAgent(agentId, data); - setAgent(updatedAgent); - setMessages(updatedAgent.messages); - return updatedAgent; - } catch (err) { - setError(err instanceof Error ? err : new Error(String(err))); - console.error('Failed to update agent:', err); - return null; - } finally { - setLoading(false); - } - }, [agentId]); - - // 取消当前操作 - const cancelAgent = useCallback(async () => { - if (!agentId) return; - - try { - await window.service.agentInstance.cancelAgent(agentId); - } catch (err) { - console.error('Failed to cancel agent:', err); - } - }, [agentId]); - - // 初始化 - useEffect(() => { - if (agentId) { - void fetchAgent(); - const cleanup = subscribeToUpdates(); - return cleanup; - } else if (agentDefId) { - // 如果没有 agentId 但有 agentDefId,则创建新的 Agent - createAgent().catch(console.error); - } - }, [agentId, agentDefId, fetchAgent, subscribeToUpdates, createAgent]); - - return { - agent, - messages, - loading, - error, - sendMessage, - createAgent, - updateAgent, - cancelAgent, - }; -}; diff --git a/src/pages/Agent/store/agentChatStore.ts b/src/pages/Agent/store/agentChatStore.ts new file mode 100644 index 00000000..12caeab8 --- /dev/null +++ b/src/pages/Agent/store/agentChatStore.ts @@ -0,0 +1,152 @@ +import { AgentInstance, AgentInstanceMessage } from '@services/agentInstance/interface'; +import { create } from 'zustand'; + +interface AgentChatState { + // State + loading: boolean; + error: Error | null; + agent: AgentInstance | null; + messages: AgentInstanceMessage[]; + + // Actions + fetchAgent: (agentId: string) => Promise; + subscribeToUpdates: (agentId: string) => (() => void) | undefined; + sendMessage: (agentId: string, content: string) => Promise; + createAgent: (agentDefinitionId?: string) => Promise; + updateAgent: (agentId: string, data: Partial) => Promise; + cancelAgent: (agentId: string) => Promise; + clearError: () => void; +} + +export const useAgentChatStore = create((set) => ({ + // Initial state + loading: false, + error: null, + agent: null, + messages: [], + + // Fetch agent instance + fetchAgent: async (agentId: string) => { + if (!agentId) return; + + try { + set({ loading: true, error: null }); + const result = await window.service.agentInstance.getAgent(agentId); + if (result) { + set({ agent: result, messages: result.messages }); + } + } catch (error) { + set({ error: error instanceof Error ? error : new Error(String(error)) }); + console.error('Failed to fetch agent:', error); + } finally { + set({ loading: false }); + } + }, + + // Subscribe to agent updates + subscribeToUpdates: (agentId: string) => { + if (!agentId) return undefined; + + try { + const subscription = window.observables.agentInstance.subscribeToAgentUpdates(agentId).subscribe({ + next: (updatedAgent) => { + if (updatedAgent) { + set({ agent: updatedAgent, messages: updatedAgent.messages }); + } + }, + error: (error) => { + console.error('Error in agent subscription:', error); + set({ error: error instanceof Error ? error : new Error(String(error)) }); + }, + }); + + // Return cleanup function + return () => { + subscription.unsubscribe(); + }; + } catch (error) { + console.error('Failed to subscribe to agent updates:', error); + set({ error: error instanceof Error ? error : new Error(String(error)) }); + return undefined; + } + }, + + // Send message to agent + sendMessage: async (agentId: string, content: string) => { + if (!agentId) { + set({ error: new Error('No agent ID provided') }); + return; + } + + try { + set({ loading: true }); + await window.service.agentInstance.sendMsgToAgent(agentId, { text: content }); + } catch (error) { + set({ error: error instanceof Error ? error : new Error(String(error)) }); + console.error('Failed to send message:', error); + } finally { + set({ loading: false }); + } + }, + + // Create new agent instance + createAgent: async (agentDefinitionId?: string) => { + try { + set({ loading: true }); + const newAgent = await window.service.agentInstance.createAgent(agentDefinitionId); + set({ + agent: newAgent, + messages: newAgent.messages, + error: null, + }); + return newAgent; + } catch (error) { + set({ error: error instanceof Error ? error : new Error(String(error)) }); + console.error('Failed to create agent:', error); + return null; + } finally { + set({ loading: false }); + } + }, + + // Update agent + updateAgent: async (agentId: string, data: Partial) => { + if (!agentId) { + set({ error: new Error('No agent ID provided') }); + return null; + } + + try { + set({ loading: true }); + const updatedAgent = await window.service.agentInstance.updateAgent(agentId, data); + set({ + agent: updatedAgent, + messages: updatedAgent.messages, + error: null, + }); + return updatedAgent; + } catch (error) { + set({ error: error instanceof Error ? error : new Error(String(error)) }); + console.error('Failed to update agent:', error); + return null; + } finally { + set({ loading: false }); + } + }, + + // Cancel current operation + cancelAgent: async (agentId: string) => { + if (!agentId) return; + + try { + await window.service.agentInstance.cancelAgent(agentId); + } catch (error) { + console.error('Failed to cancel agent:', error); + } + }, + + // Clear error + clearError: () => { + set({ error: null }); + }, +})); diff --git a/src/pages/Agent/store/tabStore.ts b/src/pages/Agent/store/tabStore/actions/basicActions.ts similarity index 52% rename from src/pages/Agent/store/tabStore.ts rename to src/pages/Agent/store/tabStore/actions/basicActions.ts index 3f96e809..12a4dd07 100644 --- a/src/pages/Agent/store/tabStore.ts +++ b/src/pages/Agent/store/tabStore/actions/basicActions.ts @@ -1,64 +1,20 @@ import { nanoid } from 'nanoid'; -import { create } from 'zustand'; -import { IChatTab, INewTab, IWebTab, TabItem, TabState, TabType } from '../types/tab'; -import { createInitialTabs } from './initialData'; +import { StateCreator } from 'zustand'; +import { IChatTab, INewTab, IWebTab, TabItem, TabState, TabType } from '../../../types/tab'; +import { MAX_CLOSED_TABS, TabsState } from '../types'; -type TabCloseDirection = 'above' | 'below' | 'other'; - -interface TabsState { - // All tabs - tabs: TabItem[]; - // ID of the currently active tab - activeTabId: string | null; - // IDs of tabs displayed side by side - splitViewIds: string[]; - // Split ratio for side-by-side view (20-80) - splitRatio: number; - // Recently closed tabs (for restoration) - closedTabs: TabItem[]; - - // Operation methods - addTab: (tabType: TabType, initialData?: Partial & { insertPosition?: number }) => Promise; - closeTab: (tabId: string) => void; - setActiveTab: (tabId: string) => void; - pinTab: (tabId: string, isPinned: boolean) => void; - updateTabData: (tabId: string, data: Partial) => void; - transformTabType: (tabId: string, newType: TabType, initialData?: Record) => void; - - // Side-by-side tabs related - addToSplitView: (tabId: string) => void; - removeFromSplitView: (tabId: string) => void; - clearSplitView: () => void; - updateSplitRatio: (ratio: number) => void; - - // Batch close and restore tab functions - closeTabs: (direction: TabCloseDirection, fromTabId: string) => void; - restoreClosedTab: () => void; - hasClosedTabs: () => boolean; - - // Utility methods - getTabIndex: (tabId: string) => number; -} - -// Initialize tab data -const initialTabs = createInitialTabs(); -const firstActiveTab = initialTabs.find(tab => tab.state === TabState.ACTIVE); - -// Maximum number of closed tabs to save -const MAX_CLOSED_TABS = 10; - -export const useTabStore = create((set, get) => ({ - tabs: initialTabs, - activeTabId: firstActiveTab?.id || null, - splitViewIds: [], - splitRatio: 50, // Default 50%/50% split ratio - closedTabs: [], // Closed tabs - - // Add new tab +/** + * 创建标签页基础操作 + */ +export const createBasicActions = (): Pick< + TabsState, + 'addTab' | 'closeTab' | 'setActiveTab' | 'pinTab' | 'updateTabData' | 'transformTabType' +> => ({ + // 添加新标签页 addTab: async (tabType: TabType, initialData = {}) => { const timestamp = Date.now(); - const { insertPosition } = initialData; - delete initialData.insertPosition; // Remove from data passed to tab + const dataWithoutPosition = { ...initialData }; + delete dataWithoutPosition.insertPosition; let newTab: TabItem; @@ -68,18 +24,18 @@ export const useTabStore = create((set, get) => ({ isPinned: false, createdAt: timestamp, updatedAt: timestamp, - ...initialData, + ...dataWithoutPosition, }; // 如果是聊天类型的标签页,需要先创建 agent 实例 if (tabType === TabType.CHAT) { const agent = await window.service.agentInstance.createAgent( - (initialData as Partial).agentDefId, + (dataWithoutPosition as Partial).agentDefId, ); newTab = { ...tabBase, type: TabType.CHAT, - title: initialData.title || agent.name, + title: dataWithoutPosition.title || agent.name, agentDefId: agent.agentDefId, agentId: agent.id, } as IChatTab; @@ -87,18 +43,51 @@ export const useTabStore = create((set, get) => ({ newTab = { ...tabBase, type: TabType.WEB, - title: initialData.title || 'agent.tabTitle.newWeb', - url: (initialData as Partial).url || 'about:blank', + title: dataWithoutPosition.title || 'agent.tabTitle.newWeb', + url: (dataWithoutPosition as Partial).url || 'about:blank', } as IWebTab; } else { newTab = { ...tabBase, type: TabType.NEW_TAB, - title: initialData.title || 'agent.tabTitle.newTab', - favorites: (initialData as Partial).favorites || [], + title: dataWithoutPosition.title || 'agent.tabTitle.newTab', + favorites: (dataWithoutPosition as Partial).favorites || [], } as INewTab; } + return newTab; + }, + + // 关闭标签页 + closeTab: () => {}, + + // 设置激活的标签页 + setActiveTab: () => {}, + + // 固定/取消固定标签页 + pinTab: () => {}, + + // 更新标签页数据 + updateTabData: async () => {}, + + // 转换标签页类型 + transformTabType: async () => {}, +}); + +/** + * 标签页基础操作中间件 + */ +export const basicActionsMiddleware: StateCreator< + TabsState, + [], + [], + Pick +> = (set, _get) => ({ + // 添加新标签页 + addTab: async (tabType: TabType, initialData = {}) => { + const newTab = await createBasicActions().addTab(tabType, initialData); + const { insertPosition } = initialData; + set(state => { const updatedTabs = state.tabs.map(tab => ({ ...tab, @@ -173,119 +162,6 @@ export const useTabStore = create((set, get) => ({ }); }, - // 批量关闭标签页 - closeTabs: (direction: TabCloseDirection, fromTabId: string) => { - set(state => { - const tabIndex = state.tabs.findIndex(tab => tab.id === fromTabId); - if (tabIndex === -1) return state; - - // 获取要保留的标签页ID列表 - const tabsToKeep: TabItem[] = []; - const tabsToClose: TabItem[] = []; - - // 根据不同方向,确定要关闭的标签页 - state.tabs.forEach((tab, index) => { - // 固定的标签页永远不关闭 - if (tab.isPinned) { - tabsToKeep.push(tab); - return; - } - - switch (direction) { - case 'above': - if (index <= tabIndex) { - tabsToKeep.push(tab); - } else { - tabsToClose.push(tab); - } - break; - case 'below': - if (index >= tabIndex || index < state.tabs.findIndex(t => !t.isPinned)) { - tabsToKeep.push(tab); - } else { - tabsToClose.push(tab); - } - break; - case 'other': - if (index === tabIndex || tab.isPinned) { - tabsToKeep.push(tab); - } else { - tabsToClose.push(tab); - } - break; - } - }); - - // 更新激活的标签页ID - let newActiveTabId = state.activeTabId; - if (!tabsToKeep.some(tab => tab.id === newActiveTabId)) { - const targetTab = tabsToKeep.find(tab => tab.id === fromTabId); - if (targetTab) { - newActiveTabId = targetTab.id; - - // 设置新激活标签页的状态 - tabsToKeep.forEach(tab => { - tab.state = tab.id === newActiveTabId ? TabState.ACTIVE : TabState.INACTIVE; - }); - } else { - newActiveTabId = tabsToKeep.length > 0 ? tabsToKeep[0].id : null; - } - } - - // 从并排视图中移除已关闭的标签页 - const newSplitViewIds = state.splitViewIds.filter(id => tabsToKeep.some(tab => tab.id === id)); - - // 添加到关闭标签页历史 - const newClosedTabs = [...tabsToClose, ...state.closedTabs].slice(0, MAX_CLOSED_TABS); - - return { - tabs: tabsToKeep, - activeTabId: newActiveTabId, - splitViewIds: newSplitViewIds, - closedTabs: newClosedTabs, - }; - }); - }, - - // 恢复最近关闭的标签页 - restoreClosedTab: () => { - set(state => { - if (state.closedTabs.length === 0) return state; - - const [tabToRestore, ...remainingClosedTabs] = state.closedTabs; - - // 设置所有标签页为非活动状态 - const updatedTabs = state.tabs.map(tab => ({ - ...tab, - state: TabState.INACTIVE, - })); - - // 恢复标签页为活动状态 - const restoredTab = { - ...tabToRestore, - state: TabState.ACTIVE, - createdAt: Date.now(), // 更新创建时间,让它排序在前面 - }; - - return { - tabs: [...updatedTabs, restoredTab], - activeTabId: restoredTab.id, - closedTabs: remainingClosedTabs, - }; - }); - }, - - // 检查是否有已关闭的标签页 - hasClosedTabs: () => { - return get().closedTabs.length > 0; - }, - - // 获取标签页在列表中的索引 - getTabIndex: (tabId: string) => { - const state = get(); - return state.tabs.findIndex(tab => tab.id === tabId); - }, - // 设置激活的标签页 setActiveTab: (tabId: string) => { set(state => { @@ -318,7 +194,7 @@ export const useTabStore = create((set, get) => ({ }, // 更新标签页数据 - updateTabData: async (tabId: string, data: Partial) => { + updateTabData: (tabId: string, data: Partial) => { set(state => { const timestamp = Date.now(); const updatedTabs = state.tabs.map(tab => { @@ -343,6 +219,7 @@ export const useTabStore = create((set, get) => ({ }); }, + // 转换标签页类型 transformTabType: async (tabId: string, newType: TabType, initialData: Record = {}) => { // 如果要转换为 CHAT 类型,需要先创建 agent if (newType === TabType.CHAT) { @@ -361,7 +238,7 @@ export const useTabStore = create((set, get) => ({ const oldTab = state.tabs[tabIndex]; const timestamp = Date.now(); - + // 创建新标签页的通用基础属性 const baseProps = { id: oldTab.id, @@ -412,39 +289,4 @@ export const useTabStore = create((set, get) => ({ }; }); }, - - // 添加到并排视图 - addToSplitView: (tabId: string) => { - set(state => { - // 最多同时显示两个并排标签页 - if (state.splitViewIds.includes(tabId) || state.splitViewIds.length >= 2) { - return state; - } - - return { - splitViewIds: [...state.splitViewIds, tabId], - }; - }); - }, - - // 从并排视图中移除 - removeFromSplitView: (tabId: string) => { - set(state => ({ - splitViewIds: state.splitViewIds.filter(id => id !== tabId), - })); - }, - - // 清空并排视图 - clearSplitView: () => { - set(() => ({ - splitViewIds: [], - })); - }, - - // 更新分割比例 - updateSplitRatio: (ratio: number) => { - set(() => ({ - splitRatio: Math.max(20, Math.min(80, ratio)), - })); - }, -})); +}); diff --git a/src/pages/Agent/store/tabStore/actions/historyActions.ts b/src/pages/Agent/store/tabStore/actions/historyActions.ts new file mode 100644 index 00000000..53dc22b7 --- /dev/null +++ b/src/pages/Agent/store/tabStore/actions/historyActions.ts @@ -0,0 +1,121 @@ +import { StateCreator } from 'zustand'; +import { TabItem, TabState } from '../../../types/tab'; +import { MAX_CLOSED_TABS, TabCloseDirection, TabsState } from '../types'; + +/** + * 标签页历史操作中间件 + */ +export const historyActionsMiddleware: StateCreator< + TabsState, + [], + [], + Pick +> = (set, get) => ({ + // 批量关闭标签页 + closeTabs: (direction: TabCloseDirection, fromTabId: string) => { + set(state => { + const tabIndex = state.tabs.findIndex(tab => tab.id === fromTabId); + if (tabIndex === -1) return state; + + // 获取要保留的标签页ID列表 + const tabsToKeep: TabItem[] = []; + const tabsToClose: TabItem[] = []; + + // 根据不同方向,确定要关闭的标签页 + state.tabs.forEach((tab, index) => { + // 固定的标签页永远不关闭 + if (tab.isPinned) { + tabsToKeep.push(tab); + return; + } + + switch (direction) { + case 'above': + if (index <= tabIndex) { + tabsToKeep.push(tab); + } else { + tabsToClose.push(tab); + } + break; + case 'below': + if (index >= tabIndex || index < state.tabs.findIndex(t => !t.isPinned)) { + tabsToKeep.push(tab); + } else { + tabsToClose.push(tab); + } + break; + case 'other': + // 固定标签页的处理已在前面完成,这里只需处理与当前标签页相同的情况 + if (index === tabIndex) { + tabsToKeep.push(tab); + } else { + tabsToClose.push(tab); + } + break; + } + }); + + // 更新激活的标签页ID + let newActiveTabId = state.activeTabId; + if (!tabsToKeep.some(tab => tab.id === newActiveTabId)) { + const targetTab = tabsToKeep.find(tab => tab.id === fromTabId); + if (targetTab) { + newActiveTabId = targetTab.id; + + // 设置新激活标签页的状态 + tabsToKeep.forEach(tab => { + tab.state = tab.id === newActiveTabId ? TabState.ACTIVE : TabState.INACTIVE; + }); + } else { + newActiveTabId = tabsToKeep.length > 0 ? tabsToKeep[0].id : null; + } + } + + // 从并排视图中移除已关闭的标签页 + const newSplitViewIds = state.splitViewIds.filter(id => tabsToKeep.some(tab => tab.id === id)); + + // 添加到关闭标签页历史 + const newClosedTabs = [...tabsToClose, ...state.closedTabs].slice(0, MAX_CLOSED_TABS); + + return { + tabs: tabsToKeep, + activeTabId: newActiveTabId, + splitViewIds: newSplitViewIds, + closedTabs: newClosedTabs, + }; + }); + }, + + // 恢复最近关闭的标签页 + restoreClosedTab: () => { + set(state => { + if (state.closedTabs.length === 0) return state; + + const [tabToRestore, ...remainingClosedTabs] = state.closedTabs; + + // 设置所有标签页为非活动状态 + const updatedTabs = state.tabs.map(tab => ({ + ...tab, + state: TabState.INACTIVE, + })); + + // 恢复标签页为活动状态 + const restoredTab = { + ...tabToRestore, + state: TabState.ACTIVE, + createdAt: Date.now(), // 更新创建时间,让它排序在前面 + }; + + return { + tabs: [...updatedTabs, restoredTab], + activeTabId: restoredTab.id, + closedTabs: remainingClosedTabs, + }; + }); + }, + + // 检查是否有已关闭的标签页 + hasClosedTabs: () => { + return get().closedTabs.length > 0; + }, +}); diff --git a/src/pages/Agent/store/tabStore/actions/splitViewActions.ts b/src/pages/Agent/store/tabStore/actions/splitViewActions.ts new file mode 100644 index 00000000..c2734b92 --- /dev/null +++ b/src/pages/Agent/store/tabStore/actions/splitViewActions.ts @@ -0,0 +1,47 @@ +import { StateCreator } from 'zustand'; +import { TabsState } from '../types'; + +/** + * 并排视图操作中间件 + */ +export const splitViewActionsMiddleware: StateCreator< + TabsState, + [], + [], + Pick +> = (set) => ({ + // 添加到并排视图 + addToSplitView: (tabId: string) => { + set(state => { + // 最多同时显示两个并排标签页 + if (state.splitViewIds.includes(tabId) || state.splitViewIds.length >= 2) { + return state; + } + + return { + splitViewIds: [...state.splitViewIds, tabId], + }; + }); + }, + + // 从并排视图中移除 + removeFromSplitView: (tabId: string) => { + set(state => ({ + splitViewIds: state.splitViewIds.filter(id => id !== tabId), + })); + }, + + // 清空并排视图 + clearSplitView: () => { + set(() => ({ + splitViewIds: [], + })); + }, + + // 更新分割比例 + updateSplitRatio: (ratio: number) => { + set(() => ({ + splitRatio: Math.max(20, Math.min(80, ratio)), + })); + }, +}); diff --git a/src/pages/Agent/store/tabStore/actions/utilityActions.ts b/src/pages/Agent/store/tabStore/actions/utilityActions.ts new file mode 100644 index 00000000..b88febc6 --- /dev/null +++ b/src/pages/Agent/store/tabStore/actions/utilityActions.ts @@ -0,0 +1,18 @@ +import { StateCreator } from 'zustand'; +import { TabsState } from '../types'; + +/** + * 标签页工具函数中间件 + */ +export const utilityActionsMiddleware: StateCreator< + TabsState, + [], + [], + Pick +> = (_set, get) => ({ + // 获取标签页在列表中的索引 + getTabIndex: (tabId: string) => { + const state = get(); + return state.tabs.findIndex(tab => tab.id === tabId); + }, +}); diff --git a/src/pages/Agent/store/tabStore/index.ts b/src/pages/Agent/store/tabStore/index.ts new file mode 100644 index 00000000..d49c1283 --- /dev/null +++ b/src/pages/Agent/store/tabStore/index.ts @@ -0,0 +1,31 @@ +import { create } from 'zustand'; +import { TabState } from '../../types/tab'; +import { createInitialTabs } from '../initialData'; +import { basicActionsMiddleware } from './actions/basicActions'; +import { historyActionsMiddleware } from './actions/historyActions'; +import { splitViewActionsMiddleware } from './actions/splitViewActions'; +import { utilityActionsMiddleware } from './actions/utilityActions'; +import { TabsState } from './types'; + +/** + * 初始化标签页数据 + */ +const initialTabs = createInitialTabs(); +const firstActiveTab = initialTabs.find(tab => tab.state === TabState.ACTIVE); + +/** + * 创建并导出标签页 Store + */ +export const useTabStore = create()((...api) => ({ + tabs: initialTabs, + activeTabId: firstActiveTab?.id || null, + splitViewIds: [], + splitRatio: 50, // 默认 50%/50% 拆分比例 + closedTabs: [], // 已关闭的标签页 + + // 组合所有中间件 + ...basicActionsMiddleware(...api), + ...splitViewActionsMiddleware(...api), + ...historyActionsMiddleware(...api), + ...utilityActionsMiddleware(...api), +})); diff --git a/src/pages/Agent/store/tabStore/types.ts b/src/pages/Agent/store/tabStore/types.ts new file mode 100644 index 00000000..975d8bcb --- /dev/null +++ b/src/pages/Agent/store/tabStore/types.ts @@ -0,0 +1,47 @@ +import { TabItem, TabType } from '../../types/tab'; + +/** + * 标签页关闭方向 + */ +export type TabCloseDirection = 'above' | 'below' | 'other'; + +/** + * 标签页 Store 的状态接口 + */ +export interface TabsState { + // All tabs + tabs: TabItem[]; + // ID of the currently active tab + activeTabId: string | null; + // IDs of tabs displayed side by side + splitViewIds: string[]; + // Split ratio for side-by-side view (20-80) + splitRatio: number; + // Recently closed tabs (for restoration) + closedTabs: TabItem[]; + + // 基础操作方法 + addTab: (tabType: TabType, initialData?: Partial & { insertPosition?: number }) => Promise; + closeTab: (tabId: string) => void; + setActiveTab: (tabId: string) => void; + pinTab: (tabId: string, isPinned: boolean) => void; + updateTabData: (tabId: string, data: Partial) => void; + transformTabType: (tabId: string, newType: TabType, initialData?: Record) => void; + + // 并排视图相关方法 + addToSplitView: (tabId: string) => void; + removeFromSplitView: (tabId: string) => void; + clearSplitView: () => void; + updateSplitRatio: (ratio: number) => void; + + // 批量关闭和恢复标签页方法 + closeTabs: (direction: TabCloseDirection, fromTabId: string) => void; + restoreClosedTab: () => void; + hasClosedTabs: () => boolean; + + // 工具方法 + getTabIndex: (tabId: string) => number; +} + +// 常量 +export const MAX_CLOSED_TABS = 10; diff --git a/tsconfig.json b/tsconfig.json index c743845e..04e8a053 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "exclude": ["template/**/*.js"], + "exclude": ["template/**/*.js", "src/services/wiki/plugin/**/*.js"], "include": ["src"], "ts-node": { "files": true,