mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2026-03-19 05:11:48 -07:00
refactor: make store smaller
This commit is contained in:
parent
afb9150683
commit
7b770fef88
16 changed files with 563 additions and 423 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@
|
|||
"Title": "設定の問題"
|
||||
},
|
||||
"EmptyStateDescription": "新しいセッションを作成するか、左から既存のセッションを選択して会話を開始してください。",
|
||||
"Error": "会話エラー",
|
||||
"InputPlaceholder": "メッセージを入力、Ctrl+Enterで送信",
|
||||
"Loading": "会話を読み込み中",
|
||||
"NewSession": "新しいセッション",
|
||||
"OpenInWorkspaceTiddler": "{{workspace}}ワークスペースで{{title}}を開く",
|
||||
"Send": "送信",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@
|
|||
"Title": "Проблема с конфигурацией"
|
||||
},
|
||||
"EmptyStateDescription": "Создайте новую сессию или выберите существующую слева, чтобы начать общение.",
|
||||
"Error": "Ошибка диалога",
|
||||
"InputPlaceholder": "Введите сообщение, Ctrl+Enter для отправки",
|
||||
"Loading": "Диалог загружается",
|
||||
"NewSession": "Новая сессия",
|
||||
"OpenInWorkspaceTiddler": "Открыть {{title}} в рабочей области {{workspace}}",
|
||||
"Send": "Отправить",
|
||||
|
|
|
|||
|
|
@ -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": "最近关闭的标签页"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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<ChatTabContentProps> = ({ 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<ChatTabContentProps> = ({ 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<ChatTabContentProps> = ({ tab }) => {
|
|||
<ChatHeader>
|
||||
<Title variant='h6'>{agent?.name || t(tab.title)}</Title>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
{loading && <CircularProgress size={20} sx={{ mr: 1 }} />}
|
||||
{loading && <CircularProgress size={20} sx={{ mr: 1 }} color='primary' />}
|
||||
<Tooltip title={t('Preference.ModelParameters', { ns: 'agent' })}>
|
||||
<IconButton onClick={openParametersDialog} size='small'>
|
||||
<TuneIcon />
|
||||
|
|
@ -143,38 +162,44 @@ export const ChatTabContent: React.FC<ChatTabContentProps> = ({ tab }) => {
|
|||
</ChatHeader>
|
||||
|
||||
<MessagesContainer>
|
||||
{loading && messages.length === 0 ? (
|
||||
<Box sx={{ textAlign: 'center', mt: 4 }}>
|
||||
<CircularProgress size={40} />
|
||||
<Typography variant='body1' sx={{ mt: 2 }}>
|
||||
{t('Chat.Loading')}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : error ? (
|
||||
<Box sx={{ textAlign: 'center', color: 'error.main', mt: 4 }}>
|
||||
<Typography variant='body1'>
|
||||
{t('Chat.Error')}: {error.message}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : messages.length === 0 ? (
|
||||
<Box sx={{ textAlign: 'center', color: 'text.secondary', mt: 4 }}>
|
||||
<SmartToyIcon sx={{ fontSize: 48, opacity: 0.5 }} />
|
||||
<Typography variant='body1' sx={{ mt: 2 }}>
|
||||
{t('Chat.StartConversation')}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
messages.map(message => (
|
||||
<MessageBubble key={message.id} $isUser={message.role === 'user'}>
|
||||
<MessageAvatar $isUser={message.role === 'user'}>
|
||||
{message.role === 'user' ? <PersonIcon /> : <SmartToyIcon />}
|
||||
</MessageAvatar>
|
||||
<MessageContent $isUser={message.role === 'user'}>
|
||||
<Typography variant='body1'>{message.content}</Typography>
|
||||
</MessageContent>
|
||||
</MessageBubble>
|
||||
))
|
||||
)}
|
||||
{loading && messages.length === 0
|
||||
? (
|
||||
<Box sx={{ textAlign: 'center', mt: 4 }}>
|
||||
<CircularProgress size={40} color='primary' />
|
||||
<Typography variant='body1' sx={{ mt: 2 }}>
|
||||
{t('Chat.Loading')}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
: error
|
||||
? (
|
||||
<Box sx={{ textAlign: 'center', color: 'error.main', mt: 4 }}>
|
||||
<Typography variant='body1'>
|
||||
{t('Chat.Error')}: {error.message}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
: messages.length === 0
|
||||
? (
|
||||
<Box sx={{ textAlign: 'center', color: 'text.secondary', mt: 4 }}>
|
||||
<SmartToyIcon sx={{ fontSize: 48, opacity: 0.5 }} />
|
||||
<Typography variant='body1' sx={{ mt: 2 }}>
|
||||
{t('Chat.StartConversation')}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
: (
|
||||
messages.map(message => (
|
||||
<MessageBubble key={message.id} $isUser={message.role === 'user'}>
|
||||
<MessageAvatar $isUser={message.role === 'user'}>
|
||||
{message.role === 'user' ? <PersonIcon /> : <SmartToyIcon />}
|
||||
</MessageAvatar>
|
||||
<MessageContent $isUser={message.role === 'user'}>
|
||||
<Typography variant='body1'>{message.content}</Typography>
|
||||
</MessageContent>
|
||||
</MessageBubble>
|
||||
))
|
||||
)}
|
||||
</MessagesContainer>
|
||||
|
||||
<InputContainer>
|
||||
|
|
|
|||
|
|
@ -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<Error | null>(null);
|
||||
const [agent, setAgent] = useState<AgentInstance | null>(null);
|
||||
const [messages, setMessages] = useState<AgentInstanceMessage[]>([]);
|
||||
|
||||
// 获取 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<AgentInstance>) => {
|
||||
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,
|
||||
};
|
||||
};
|
||||
152
src/pages/Agent/store/agentChatStore.ts
Normal file
152
src/pages/Agent/store/agentChatStore.ts
Normal file
|
|
@ -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<void>;
|
||||
subscribeToUpdates: (agentId: string) => (() => void) | undefined;
|
||||
sendMessage: (agentId: string, content: string) => Promise<void>;
|
||||
createAgent: (agentDefinitionId?: string) => Promise<AgentInstance | null>;
|
||||
updateAgent: (agentId: string, data: Partial<AgentInstance>) => Promise<AgentInstance | null>;
|
||||
cancelAgent: (agentId: string) => Promise<void>;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useAgentChatStore = create<AgentChatState>((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<AgentInstance>) => {
|
||||
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 });
|
||||
},
|
||||
}));
|
||||
|
|
@ -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<TabItem> & { insertPosition?: number }) => Promise<TabItem>;
|
||||
closeTab: (tabId: string) => void;
|
||||
setActiveTab: (tabId: string) => void;
|
||||
pinTab: (tabId: string, isPinned: boolean) => void;
|
||||
updateTabData: (tabId: string, data: Partial<TabItem>) => void;
|
||||
transformTabType: (tabId: string, newType: TabType, initialData?: Record<string, unknown>) => 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<TabsState>((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<TabsState>((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<IChatTab>).agentDefId,
|
||||
(dataWithoutPosition as Partial<IChatTab>).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<TabsState>((set, get) => ({
|
|||
newTab = {
|
||||
...tabBase,
|
||||
type: TabType.WEB,
|
||||
title: initialData.title || 'agent.tabTitle.newWeb',
|
||||
url: (initialData as Partial<IWebTab>).url || 'about:blank',
|
||||
title: dataWithoutPosition.title || 'agent.tabTitle.newWeb',
|
||||
url: (dataWithoutPosition as Partial<IWebTab>).url || 'about:blank',
|
||||
} as IWebTab;
|
||||
} else {
|
||||
newTab = {
|
||||
...tabBase,
|
||||
type: TabType.NEW_TAB,
|
||||
title: initialData.title || 'agent.tabTitle.newTab',
|
||||
favorites: (initialData as Partial<INewTab>).favorites || [],
|
||||
title: dataWithoutPosition.title || 'agent.tabTitle.newTab',
|
||||
favorites: (dataWithoutPosition as Partial<INewTab>).favorites || [],
|
||||
} as INewTab;
|
||||
}
|
||||
|
||||
return newTab;
|
||||
},
|
||||
|
||||
// 关闭标签页
|
||||
closeTab: () => {},
|
||||
|
||||
// 设置激活的标签页
|
||||
setActiveTab: () => {},
|
||||
|
||||
// 固定/取消固定标签页
|
||||
pinTab: () => {},
|
||||
|
||||
// 更新标签页数据
|
||||
updateTabData: async () => {},
|
||||
|
||||
// 转换标签页类型
|
||||
transformTabType: async () => {},
|
||||
});
|
||||
|
||||
/**
|
||||
* 标签页基础操作中间件
|
||||
*/
|
||||
export const basicActionsMiddleware: StateCreator<
|
||||
TabsState,
|
||||
[],
|
||||
[],
|
||||
Pick<TabsState, 'addTab' | 'closeTab' | 'setActiveTab' | 'pinTab' | 'updateTabData' | 'transformTabType'>
|
||||
> = (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<TabsState>((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<TabsState>((set, get) => ({
|
|||
},
|
||||
|
||||
// 更新标签页数据
|
||||
updateTabData: async (tabId: string, data: Partial<TabItem>) => {
|
||||
updateTabData: (tabId: string, data: Partial<TabItem>) => {
|
||||
set(state => {
|
||||
const timestamp = Date.now();
|
||||
const updatedTabs = state.tabs.map(tab => {
|
||||
|
|
@ -343,6 +219,7 @@ export const useTabStore = create<TabsState>((set, get) => ({
|
|||
});
|
||||
},
|
||||
|
||||
// 转换标签页类型
|
||||
transformTabType: async (tabId: string, newType: TabType, initialData: Record<string, unknown> = {}) => {
|
||||
// 如果要转换为 CHAT 类型,需要先创建 agent
|
||||
if (newType === TabType.CHAT) {
|
||||
|
|
@ -361,7 +238,7 @@ export const useTabStore = create<TabsState>((set, get) => ({
|
|||
|
||||
const oldTab = state.tabs[tabIndex];
|
||||
const timestamp = Date.now();
|
||||
|
||||
|
||||
// 创建新标签页的通用基础属性
|
||||
const baseProps = {
|
||||
id: oldTab.id,
|
||||
|
|
@ -412,39 +289,4 @@ export const useTabStore = create<TabsState>((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)),
|
||||
}));
|
||||
},
|
||||
}));
|
||||
});
|
||||
121
src/pages/Agent/store/tabStore/actions/historyActions.ts
Normal file
121
src/pages/Agent/store/tabStore/actions/historyActions.ts
Normal file
|
|
@ -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<TabsState, 'closeTabs' | 'restoreClosedTab' | 'hasClosedTabs'>
|
||||
> = (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;
|
||||
},
|
||||
});
|
||||
47
src/pages/Agent/store/tabStore/actions/splitViewActions.ts
Normal file
47
src/pages/Agent/store/tabStore/actions/splitViewActions.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { StateCreator } from 'zustand';
|
||||
import { TabsState } from '../types';
|
||||
|
||||
/**
|
||||
* 并排视图操作中间件
|
||||
*/
|
||||
export const splitViewActionsMiddleware: StateCreator<
|
||||
TabsState,
|
||||
[],
|
||||
[],
|
||||
Pick<TabsState, 'addToSplitView' | 'removeFromSplitView' | 'clearSplitView' | 'updateSplitRatio'>
|
||||
> = (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)),
|
||||
}));
|
||||
},
|
||||
});
|
||||
18
src/pages/Agent/store/tabStore/actions/utilityActions.ts
Normal file
18
src/pages/Agent/store/tabStore/actions/utilityActions.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { StateCreator } from 'zustand';
|
||||
import { TabsState } from '../types';
|
||||
|
||||
/**
|
||||
* 标签页工具函数中间件
|
||||
*/
|
||||
export const utilityActionsMiddleware: StateCreator<
|
||||
TabsState,
|
||||
[],
|
||||
[],
|
||||
Pick<TabsState, 'getTabIndex'>
|
||||
> = (_set, get) => ({
|
||||
// 获取标签页在列表中的索引
|
||||
getTabIndex: (tabId: string) => {
|
||||
const state = get();
|
||||
return state.tabs.findIndex(tab => tab.id === tabId);
|
||||
},
|
||||
});
|
||||
31
src/pages/Agent/store/tabStore/index.ts
Normal file
31
src/pages/Agent/store/tabStore/index.ts
Normal file
|
|
@ -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<TabsState>()((...api) => ({
|
||||
tabs: initialTabs,
|
||||
activeTabId: firstActiveTab?.id || null,
|
||||
splitViewIds: [],
|
||||
splitRatio: 50, // 默认 50%/50% 拆分比例
|
||||
closedTabs: [], // 已关闭的标签页
|
||||
|
||||
// 组合所有中间件
|
||||
...basicActionsMiddleware(...api),
|
||||
...splitViewActionsMiddleware(...api),
|
||||
...historyActionsMiddleware(...api),
|
||||
...utilityActionsMiddleware(...api),
|
||||
}));
|
||||
47
src/pages/Agent/store/tabStore/types.ts
Normal file
47
src/pages/Agent/store/tabStore/types.ts
Normal file
|
|
@ -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<TabItem> & { insertPosition?: number }) => Promise<TabItem>;
|
||||
closeTab: (tabId: string) => void;
|
||||
setActiveTab: (tabId: string) => void;
|
||||
pinTab: (tabId: string, isPinned: boolean) => void;
|
||||
updateTabData: (tabId: string, data: Partial<TabItem>) => void;
|
||||
transformTabType: (tabId: string, newType: TabType, initialData?: Record<string, unknown>) => 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;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"exclude": ["template/**/*.js"],
|
||||
"exclude": ["template/**/*.js", "src/services/wiki/plugin/**/*.js"],
|
||||
"include": ["src"],
|
||||
"ts-node": {
|
||||
"files": true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue