diff --git a/dist-electron/preload/preload.js b/dist-electron/preload/preload.js index 956f473..c4f6639 100644 --- a/dist-electron/preload/preload.js +++ b/dist-electron/preload/preload.js @@ -1 +1,137 @@ -"use strict";const r=require("electron");var i=(e=>(e.EXTERNAL_OPEN="external-open",e.APP_SET_FRAMELESS="app:set-frameless",e.APP_LOAD_PAGE="app:load-page",e.TAB_CREATE="tab:create",e.TAB_LIST="tab:list",e.TAB_NAVIGATE="tab:navigate",e.TAB_RELOAD="tab:reload",e.TAB_BACK="tab:back",e.TAB_FORWARD="tab:forward",e.TAB_SWITCH="tab:switch",e.TAB_CLOSE="tab:close",e.LOG_TO_MAIN="log-to-main",e.READ_FILE="read-file",e.INVOKE="ipc:invoke",e.INVOKE_ASYNC="ipc:invokeAsync",e.APP_MINIMIZE="app:minimize",e.APP_MAXIMIZE="app:maximize",e.APP_QUIT="app:quit",e.FILE_READ="file:read",e.FILE_WRITE="file:write",e.GET_WINDOW_ID="get-window-id",e.CUSTOM_EVENT="custom:event",e.TIME_UPDATE="time:update",e.RENDERER_IS_READY="renderer-ready",e.SHOW_CONTEXT_MENU="show-context-menu",e.START_A_DIALOGUE="start-a-dialogue",e.OPEN_WINDOW="open-window",e.LOG_DEBUG="log-debug",e.LOG_INFO="log-info",e.LOG_WARN="log-warn",e.LOG_ERROR="log-error",e.CONFIG_UPDATED="config-updated",e.SET_CONFIG="set-config",e.GET_CONFIG="get-config",e.UPDATE_CONFIG="update-config",e.SET_THEME_MODE="set-theme-mode",e.GET_THEME_MODE="get-theme-mode",e.IS_DARK_THEME="is-dark-theme",e.THEME_MODE_UPDATED="theme-mode-updated",e.EXECUTE_SCRIPT="execute-script",e.TASK_PROGRESS="task:progress",e.TASK_STARTED="task:started",e.TASK_COMPLETED="task:completed",e.OPEN_CHANNEL="open-channel",e.SCRIPT_LIST="script:list",e.SCRIPT_GET="script:get",e.SCRIPT_SAVE="script:save",e.SCRIPT_DELETE="script:delete",e.SCRIPT_TOGGLE="script:toggle",e.SCRIPT_RUN="script:run",e.SCRIPT_RECORD_START="script:record-start",e.SCRIPT_RECORD_STOP="script:record-stop",e.SCRIPT_CODEGEN="script:codegen",e.GATEWAY_RPC="gateway:rpc",e.GATEWAY_EVENT="gateway:event",e.UPDATE_CHECK="update:check",e.UPDATE_DOWNLOAD="update:download",e.UPDATE_INSTALL="update:install",e.UPDATE_VERSION="update:version",e.UPDATE_STATUS_CHANGED="update:status-changed",e))(i||{});const d={versions:process.versions,external:{open:e=>r.ipcRenderer.invoke("external-open",e)},platform:process.platform,windowMinimize:()=>r.ipcRenderer.invoke("window:minimize"),windowMaximize:()=>r.ipcRenderer.invoke("window:maximize"),windowClose:()=>r.ipcRenderer.invoke("window:close"),windowIsMaximized:()=>r.ipcRenderer.invoke("window:isMaximized"),viewIsReady:()=>r.ipcRenderer.send(i.RENDERER_IS_READY),app:{setFrameless:e=>r.ipcRenderer.invoke(i.APP_SET_FRAMELESS,e),loadPage:e=>r.ipcRenderer.invoke(i.APP_LOAD_PAGE,e)},readFile:e=>r.ipcRenderer.invoke(i.READ_FILE,e),invoke:(e,...n)=>r.ipcRenderer.invoke(e,...n),invokeAsync:(e,...n)=>r.ipcRenderer.invoke(e,...n),on:(e,n)=>{const t=(o,...R)=>n(...R);return r.ipcRenderer.on(e,t),()=>r.ipcRenderer.removeListener(e,t)},send:(e,...n)=>r.ipcRenderer.send(e,...n),getCurrentWindowId:()=>r.ipcRenderer.sendSync(i.GET_WINDOW_ID),logger:{debug:(e,...n)=>r.ipcRenderer.send(i.LOG_DEBUG,e,...n),info:(e,...n)=>r.ipcRenderer.send(i.LOG_INFO,e,...n),warn:(e,...n)=>r.ipcRenderer.send(i.LOG_WARN,e,...n),error:(e,...n)=>r.ipcRenderer.send(i.LOG_ERROR,e,...n)},executeScript:e=>r.ipcRenderer.invoke(i.EXECUTE_SCRIPT,e),onTaskProgress:e=>{const n=(t,o)=>e(o);return r.ipcRenderer.on(i.TASK_PROGRESS,n),()=>r.ipcRenderer.removeListener(i.TASK_PROGRESS,n)},onTaskStarted:e=>{const n=(t,o)=>e(o);return r.ipcRenderer.on(i.TASK_STARTED,n),()=>r.ipcRenderer.removeListener(i.TASK_STARTED,n)},onTaskCompleted:e=>{const n=(t,o)=>e(o);return r.ipcRenderer.on(i.TASK_COMPLETED,n),()=>r.ipcRenderer.removeListener(i.TASK_COMPLETED,n)},openChannel:e=>r.ipcRenderer.invoke(i.OPEN_CHANNEL,e),scriptApi:{list:()=>r.ipcRenderer.invoke(i.SCRIPT_LIST),get:e=>r.ipcRenderer.invoke(i.SCRIPT_GET,e),save:e=>r.ipcRenderer.invoke(i.SCRIPT_SAVE,e),delete:e=>r.ipcRenderer.invoke(i.SCRIPT_DELETE,e),toggle:(e,n)=>r.ipcRenderer.invoke(i.SCRIPT_TOGGLE,e,n),run:e=>r.ipcRenderer.invoke(i.SCRIPT_RUN,e),startRecording:e=>r.ipcRenderer.invoke(i.SCRIPT_RECORD_START,e),stopRecording:()=>r.ipcRenderer.invoke(i.SCRIPT_RECORD_STOP),codegen:(e,n)=>r.ipcRenderer.invoke(i.SCRIPT_CODEGEN,e,n)}};r.contextBridge.exposeInMainWorld("api",d); +"use strict"; +const electron = require("electron"); +var IPC_EVENTS = /* @__PURE__ */ ((IPC_EVENTS2) => { + IPC_EVENTS2["EXTERNAL_OPEN"] = "external-open"; + IPC_EVENTS2["APP_SET_FRAMELESS"] = "app:set-frameless"; + IPC_EVENTS2["APP_LOAD_PAGE"] = "app:load-page"; + IPC_EVENTS2["TAB_CREATE"] = "tab:create"; + IPC_EVENTS2["TAB_LIST"] = "tab:list"; + IPC_EVENTS2["TAB_NAVIGATE"] = "tab:navigate"; + IPC_EVENTS2["TAB_RELOAD"] = "tab:reload"; + IPC_EVENTS2["TAB_BACK"] = "tab:back"; + IPC_EVENTS2["TAB_FORWARD"] = "tab:forward"; + IPC_EVENTS2["TAB_SWITCH"] = "tab:switch"; + IPC_EVENTS2["TAB_CLOSE"] = "tab:close"; + IPC_EVENTS2["LOG_TO_MAIN"] = "log-to-main"; + IPC_EVENTS2["READ_FILE"] = "read-file"; + IPC_EVENTS2["INVOKE"] = "ipc:invoke"; + IPC_EVENTS2["INVOKE_ASYNC"] = "ipc:invokeAsync"; + IPC_EVENTS2["APP_MINIMIZE"] = "app:minimize"; + IPC_EVENTS2["APP_MAXIMIZE"] = "app:maximize"; + IPC_EVENTS2["APP_QUIT"] = "app:quit"; + IPC_EVENTS2["FILE_READ"] = "file:read"; + IPC_EVENTS2["FILE_WRITE"] = "file:write"; + IPC_EVENTS2["GET_WINDOW_ID"] = "get-window-id"; + IPC_EVENTS2["CUSTOM_EVENT"] = "custom:event"; + IPC_EVENTS2["TIME_UPDATE"] = "time:update"; + IPC_EVENTS2["RENDERER_IS_READY"] = "renderer-ready"; + IPC_EVENTS2["SHOW_CONTEXT_MENU"] = "show-context-menu"; + IPC_EVENTS2["START_A_DIALOGUE"] = "start-a-dialogue"; + IPC_EVENTS2["OPEN_WINDOW"] = "open-window"; + IPC_EVENTS2["LOG_DEBUG"] = "log-debug"; + IPC_EVENTS2["LOG_INFO"] = "log-info"; + IPC_EVENTS2["LOG_WARN"] = "log-warn"; + IPC_EVENTS2["LOG_ERROR"] = "log-error"; + IPC_EVENTS2["CONFIG_UPDATED"] = "config-updated"; + IPC_EVENTS2["SET_CONFIG"] = "set-config"; + IPC_EVENTS2["GET_CONFIG"] = "get-config"; + IPC_EVENTS2["UPDATE_CONFIG"] = "update-config"; + IPC_EVENTS2["SET_THEME_MODE"] = "set-theme-mode"; + IPC_EVENTS2["GET_THEME_MODE"] = "get-theme-mode"; + IPC_EVENTS2["IS_DARK_THEME"] = "is-dark-theme"; + IPC_EVENTS2["THEME_MODE_UPDATED"] = "theme-mode-updated"; + IPC_EVENTS2["EXECUTE_SCRIPT"] = "execute-script"; + IPC_EVENTS2["TASK_PROGRESS"] = "task:progress"; + IPC_EVENTS2["TASK_STARTED"] = "task:started"; + IPC_EVENTS2["TASK_COMPLETED"] = "task:completed"; + IPC_EVENTS2["OPEN_CHANNEL"] = "open-channel"; + IPC_EVENTS2["SCRIPT_LIST"] = "script:list"; + IPC_EVENTS2["SCRIPT_GET"] = "script:get"; + IPC_EVENTS2["SCRIPT_SAVE"] = "script:save"; + IPC_EVENTS2["SCRIPT_DELETE"] = "script:delete"; + IPC_EVENTS2["SCRIPT_TOGGLE"] = "script:toggle"; + IPC_EVENTS2["SCRIPT_RUN"] = "script:run"; + IPC_EVENTS2["SCRIPT_RECORD_START"] = "script:record-start"; + IPC_EVENTS2["SCRIPT_RECORD_STOP"] = "script:record-stop"; + IPC_EVENTS2["SCRIPT_CODEGEN"] = "script:codegen"; + IPC_EVENTS2["GATEWAY_RPC"] = "gateway:rpc"; + IPC_EVENTS2["GATEWAY_EVENT"] = "gateway:event"; + IPC_EVENTS2["UPDATE_CHECK"] = "update:check"; + IPC_EVENTS2["UPDATE_DOWNLOAD"] = "update:download"; + IPC_EVENTS2["UPDATE_INSTALL"] = "update:install"; + IPC_EVENTS2["UPDATE_VERSION"] = "update:version"; + IPC_EVENTS2["UPDATE_STATUS_CHANGED"] = "update:status-changed"; + return IPC_EVENTS2; +})(IPC_EVENTS || {}); +const api = { + versions: process.versions, + external: { + open: (url) => electron.ipcRenderer.invoke("external-open", url) + }, + platform: process.platform, + windowMinimize: () => electron.ipcRenderer.invoke("window:minimize"), + windowMaximize: () => electron.ipcRenderer.invoke("window:maximize"), + windowClose: () => electron.ipcRenderer.invoke("window:close"), + windowIsMaximized: () => electron.ipcRenderer.invoke("window:isMaximized"), + viewIsReady: () => electron.ipcRenderer.send(IPC_EVENTS.RENDERER_IS_READY), + app: { + setFrameless: (route) => electron.ipcRenderer.invoke(IPC_EVENTS.APP_SET_FRAMELESS, route), + loadPage: (page) => electron.ipcRenderer.invoke(IPC_EVENTS.APP_LOAD_PAGE, page) + }, + // 通过 IPC 调用主进程 + readFile: (filePath) => electron.ipcRenderer.invoke(IPC_EVENTS.READ_FILE, filePath), + // 异步调用(映射为 electron 的 invoke) + invoke: (channel, ...args) => electron.ipcRenderer.invoke(channel, ...args), + // 异步调用(为了兼容老代码) + invokeAsync: (channel, ...args) => electron.ipcRenderer.invoke(channel, ...args), + // 监听主进程消息 + on: (event, callback) => { + const subscription = (_event, ...args) => callback(...args); + electron.ipcRenderer.on(event, subscription); + return () => electron.ipcRenderer.removeListener(event, subscription); + }, + // 发送消息到主进程 + send: (channel, ...args) => electron.ipcRenderer.send(channel, ...args), + // 获取窗口ID + getCurrentWindowId: () => electron.ipcRenderer.sendSync(IPC_EVENTS.GET_WINDOW_ID), + // 发送日志 + logger: { + debug: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_DEBUG, message, ...meta), + info: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_INFO, message, ...meta), + warn: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_WARN, message, ...meta), + error: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_ERROR, message, ...meta) + }, + // 执行脚本 + executeScript: (params) => electron.ipcRenderer.invoke(IPC_EVENTS.EXECUTE_SCRIPT, params), + // 任务事件 + onTaskProgress: (cb) => { + const subscription = (_event, payload) => cb(payload); + electron.ipcRenderer.on(IPC_EVENTS.TASK_PROGRESS, subscription); + return () => electron.ipcRenderer.removeListener(IPC_EVENTS.TASK_PROGRESS, subscription); + }, + onTaskStarted: (cb) => { + const subscription = (_event, payload) => cb(payload); + electron.ipcRenderer.on(IPC_EVENTS.TASK_STARTED, subscription); + return () => electron.ipcRenderer.removeListener(IPC_EVENTS.TASK_STARTED, subscription); + }, + onTaskCompleted: (cb) => { + const subscription = (_event, payload) => cb(payload); + electron.ipcRenderer.on(IPC_EVENTS.TASK_COMPLETED, subscription); + return () => electron.ipcRenderer.removeListener(IPC_EVENTS.TASK_COMPLETED, subscription); + }, + // 打开渠道 + openChannel: (channels) => electron.ipcRenderer.invoke(IPC_EVENTS.OPEN_CHANNEL, channels), + // 脚本管理 + scriptApi: { + list: () => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_LIST), + get: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_GET, id), + save: (input) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_SAVE, input), + delete: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_DELETE, id), + toggle: (id, enabled) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_TOGGLE, id, enabled), + run: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RUN, id), + startRecording: (url) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_START, url), + stopRecording: () => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_STOP), + codegen: (id, url) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_CODEGEN, id, url) + } +}; +electron.contextBridge.exposeInMainWorld("api", api); diff --git a/src/components/chat/ChatHistoryPanel.tsx b/src/components/chat/ChatHistoryPanel.tsx index 8198732..3472272 100644 --- a/src/components/chat/ChatHistoryPanel.tsx +++ b/src/components/chat/ChatHistoryPanel.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { memo, useEffect, useRef, useState } from 'react'; import type { ChatHistoryBucket } from './types'; import { ChevronDown, @@ -31,7 +31,7 @@ function cx(...classes: Array): string { return classes.filter(Boolean).join(' '); } -export default function ChatHistoryPanel({ +function ChatHistoryPanel({ buckets, selectedConversationId, loading, @@ -50,12 +50,17 @@ export default function ChatHistoryPanel({ useEffect(() => { setCollapsedBuckets((current) => { const next: Record = {}; + const currentKeys = Object.keys(current); + let changed = currentKeys.length !== buckets.length; for (const bucket of buckets) { next[bucket.key] = current[bucket.key] ?? false; + if (current[bucket.key] !== next[bucket.key]) { + changed = true; + } } - return next; + return changed ? next : current; }); }, [buckets]); @@ -299,3 +304,5 @@ export default function ChatHistoryPanel({ ); } + +export default memo(ChatHistoryPanel); diff --git a/src/components/chat/ChatMessageList.tsx b/src/components/chat/ChatMessageList.tsx index 46cc6c2..447b727 100644 --- a/src/components/chat/ChatMessageList.tsx +++ b/src/components/chat/ChatMessageList.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { memo, useEffect, useRef } from 'react'; import type { ChatMessageItem } from './types'; type ChatMessageListProps = { @@ -6,7 +6,7 @@ type ChatMessageListProps = { loading?: boolean; }; -export default function ChatMessageList({ messages, loading }: ChatMessageListProps) { +function ChatMessageList({ messages, loading }: ChatMessageListProps) { const containerRef = useRef(null); useEffect(() => { @@ -90,3 +90,5 @@ export default function ChatMessageList({ messages, loading }: ChatMessageListPr ); } + +export default memo(ChatMessageList); diff --git a/src/components/chat/TaskBoard.tsx b/src/components/chat/TaskBoard.tsx index 4c00ffc..8a055fd 100644 --- a/src/components/chat/TaskBoard.tsx +++ b/src/components/chat/TaskBoard.tsx @@ -1,3 +1,4 @@ +import { memo } from 'react'; import type { TaskItem } from './types'; type TaskBoardProps = { @@ -11,7 +12,7 @@ type TaskBoardProps = { currentTime?: string; }; -export default function TaskBoard({ +function TaskBoard({ activeTab, pendingItems, completedItems, @@ -112,3 +113,11 @@ export default function TaskBoard({ ); } + +export default memo(TaskBoard, (prevProps, nextProps) => ( + prevProps.activeTab === nextProps.activeTab + && prevProps.pendingItems === nextProps.pendingItems + && prevProps.completedItems === nextProps.completedItems + && prevProps.currentDateLabel === nextProps.currentDateLabel + && prevProps.currentTime === nextProps.currentTime +)); diff --git a/src/main.tsx b/src/main.tsx index 4b8046c..a6971e9 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -9,8 +9,14 @@ if (!container) { throw new Error('Missing #app container for React entry.'); } +const app = ; + ReactDOM.createRoot(container).render( - - - , + import.meta.env.DEV + ? app + : ( + + {app} + + ), ); diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index d6402c5..e47d1fa 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import type { RawMessage } from '../../shared/chat-model'; import { extractText, isInternalMessage } from '../../shared/chat-model'; import { taskCenterList, type TaskCenterItem } from '../../constants/taskCenterList'; @@ -123,15 +123,19 @@ function mapMessages(messages: RawMessage[], streamingMessage: RawMessage | null if (message.role === 'user' || message.role === 'assistant') return true; return Boolean(extractText(message).trim()); }) - .map((message): ChatMessageItem => ({ - id: message.id || `msg-${message.timestamp || Math.random()}`, - role: message.role === 'user' ? 'user' : 'assistant', - name: message.role === 'user' ? '你' : 'YINIAN', - time: getMessageTime(message.timestamp), - content: extractText(message), - attachments: message._attachedFiles, - isError: Boolean(message.isError), - })); + .map((message): ChatMessageItem => { + const text = extractText(message); + + return { + id: message.id || `${message.role}-${message.timestamp || 0}-${text.slice(0, 32)}`, + role: message.role === 'user' ? 'user' : 'assistant', + name: message.role === 'user' ? '你' : 'YINIAN', + time: getMessageTime(message.timestamp), + content: text, + attachments: message._attachedFiles, + isError: Boolean(message.isError), + }; + }); if (streamingMessage && extractText(streamingMessage).trim()) { visibleMessages.push({ @@ -148,74 +152,46 @@ function mapMessages(messages: RawMessage[], streamingMessage: RawMessage | null return visibleMessages; } -export default function HomePage() { - const modelsState = useModelsStore(); - const chat = useChatStore(); - const taskState = useTaskStore(); - const channelState = useChannelStore(); +function handleNewConversation(): void { + const currentAgentId = chatStore.getState().currentAgentId; + const defaultAgentId = modelsStore.getState().defaultAgentId; + const targetAgentId = currentAgentId && currentAgentId !== 'local' ? currentAgentId : defaultAgentId; + void chatStore.newSession(targetAgentId || undefined); +} + +function handleSelectConversation(conversationId: string): void { + chatStore.switchSession(conversationId); +} + +function handleRenameConversation(conversationId: string): void { + const chat = chatStore.getState(); + const currentLabel = chat.sessionLabels[conversationId] + || chat.sessions.find((session) => session.key === conversationId)?.displayName + || ''; + const nextLabel = window.prompt('重命名对话', currentLabel); + if (nextLabel) { + chatStore.renameSession(conversationId, nextLabel); + } +} + +function handleDeleteConversation(conversationId: string): void { + const confirmed = window.confirm('确定删除该会话吗?删除后将无法恢复。'); + if (confirmed) { + void chatStore.deleteSession(conversationId); + } +} + +function handleRefreshConversationData(): void { + void modelsStore.load(); + void chatStore.loadSessions(); + void chatStore.loadHistory(); +} + +function HomeChatComposerSection() { + const error = useChatStore((state) => state.error); + const isSending = useChatStore((state) => state.sending); const [inputMessage, setInputMessage] = useState(''); const [attachments, setAttachments] = useState([]); - const [activeTaskTab, setActiveTaskTab] = useState('pending'); - const [taskCenterNotice, setTaskCenterNotice] = useState(null); - const [taskDialogOpen, setTaskDialogOpen] = useState(false); - const [addChannelDialogOpen, setAddChannelDialogOpen] = useState(false); - - useEffect(() => { - void modelsStore.init(); - void chatStore.init(); - void taskStore.init(); - void channelStore.init(); - }, []); - - const currentMs = Date.now(); - const historyBuckets: ChatHistoryBucket[] = HISTORY_BUCKET_META.map((bucket) => ({ - ...bucket, - sessions: [], - })); - const bucketMap = new Map(historyBuckets.map((bucket) => [bucket.key, bucket])); - - for (const session of [...chat.sessions].sort((left, right) => { - const leftTime = chat.sessionLastActivity[left.key] || left.updatedAt || 0; - const rightTime = chat.sessionLastActivity[right.key] || right.updatedAt || 0; - return rightTime - leftTime; - })) { - const activityMs = chat.sessionLastActivity[session.key] || session.updatedAt || 0; - const bucketKey = getHistoryBucket(activityMs, currentMs); - const targetBucket = bucketMap.get(bucketKey); - if (!targetBucket) continue; - - targetBucket.sessions.push({ - conversationId: session.key, - title: chat.sessionLabels[session.key] || session.displayName || session.key, - updatedAt: formatHistoryTime(activityMs, currentMs), - }); - } - - const pendingTasks = getPendingTasks(taskState.tasks); - const completedTasks = getCompletedTasks(taskState.tasks); - const pendingTaskItems: TaskItem[] = pendingTasks.flatMap((task) => task.subTasks.map((subTask) => ({ - id: subTask.id, - removeTaskId: task.id, - title: subTask.name, - description: subTask.message || task.title, - status: subTask.status, - meta: task.title, - }))); - const completedTaskItems: TaskItem[] = completedTasks.map((task) => ({ - id: task.id, - title: task.title, - description: `${task.operation === 'open' ? '开启' : '关闭'} ${getTaskRoomTypeLabel(task)}`, - status: task.status, - meta: `${task.dateRange[0]} ~ ${task.dateRange[1]}`, - })); - const currentTaskSource = activeTaskTab === 'pending' ? pendingTasks : completedTasks; - const latestTask = currentTaskSource[0]; - - const visibleMessages = mapMessages(chat.messages, chat.streamingMessage); - const selectedModelId = modelsState.models.some((model) => model.id === chat.currentAgentId) - ? chat.currentAgentId - : modelsState.defaultAgentId; - const currentModel = modelsState.models.find((model) => model.id === selectedModelId) || null; async function handleSendMessage(): Promise { const sent = await chatStore.sendMessage(inputMessage, attachments); @@ -230,17 +206,140 @@ export default function HomePage() { setAttachments((currentAttachments) => [...currentAttachments, ...stagedFiles]); } + return ( + { + setAttachments((currentAttachments) => currentAttachments.filter((_, currentIndex) => currentIndex !== index)); + }} + onSend={() => { + void handleSendMessage(); + }} + onStop={() => { + void chatStore.abortRun(); + }} + /> + ); +} + +export default function HomePage() { + const models = useModelsStore((state) => state.models); + const modelsLoading = useModelsStore((state) => state.loading); + const defaultAgentId = useModelsStore((state) => state.defaultAgentId); + const chatInitialized = useChatStore((state) => state.initialized); + const chatMessages = useChatStore((state) => state.messages); + const chatLoading = useChatStore((state) => state.loading); + const chatStreamingMessage = useChatStore((state) => state.streamingMessage); + const chatSessions = useChatStore((state) => state.sessions); + const chatCurrentSessionKey = useChatStore((state) => state.currentSessionKey); + const chatCurrentAgentId = useChatStore((state) => state.currentAgentId); + const chatSessionLabels = useChatStore((state) => state.sessionLabels); + const chatSessionLastActivity = useChatStore((state) => state.sessionLastActivity); + const chatGatewayStatus = useChatStore((state) => state.gatewayStatus); + const tasks = useTaskStore((state) => state.tasks); + const availableChannels = useChannelStore((state) => state.availableChannels); + const channelLoading = useChannelStore((state) => state.loading); + const selectedChannels = useChannelStore((state) => state.selectedChannels); + const [activeTaskTab, setActiveTaskTab] = useState('pending'); + const [taskCenterNotice, setTaskCenterNotice] = useState(null); + const [taskDialogOpen, setTaskDialogOpen] = useState(false); + const [addChannelDialogOpen, setAddChannelDialogOpen] = useState(false); + + useEffect(() => { + void modelsStore.init(); + void chatStore.init(); + void taskStore.init(); + void channelStore.init(); + }, []); + + const historyBuckets = useMemo(() => { + const currentMs = Date.now(); + const buckets: ChatHistoryBucket[] = HISTORY_BUCKET_META.map((bucket) => ({ + ...bucket, + sessions: [], + })); + const bucketMap = new Map(buckets.map((bucket) => [bucket.key, bucket])); + + for (const session of [...chatSessions].sort((left, right) => { + const leftTime = chatSessionLastActivity[left.key] || left.updatedAt || 0; + const rightTime = chatSessionLastActivity[right.key] || right.updatedAt || 0; + return rightTime - leftTime; + })) { + const activityMs = chatSessionLastActivity[session.key] || session.updatedAt || 0; + const bucketKey = getHistoryBucket(activityMs, currentMs); + const targetBucket = bucketMap.get(bucketKey); + if (!targetBucket) continue; + + targetBucket.sessions.push({ + conversationId: session.key, + title: chatSessionLabels[session.key] || session.displayName || session.key, + updatedAt: formatHistoryTime(activityMs, currentMs), + }); + } + + return buckets; + }, [chatSessionLabels, chatSessionLastActivity, chatSessions]); + + const visibleMessages = useMemo( + () => mapMessages(chatMessages, chatStreamingMessage), + [chatMessages, chatStreamingMessage], + ); + + const pendingTasks = useMemo(() => getPendingTasks(tasks), [tasks]); + const completedTasks = useMemo(() => getCompletedTasks(tasks), [tasks]); + + const pendingTaskItems = useMemo(() => ( + pendingTasks.flatMap((task) => task.subTasks.map((subTask) => ({ + id: subTask.id, + removeTaskId: task.id, + title: subTask.name, + description: subTask.message || task.title, + status: subTask.status, + meta: task.title, + }))) + ), [pendingTasks]); + + const completedTaskItems = useMemo(() => ( + completedTasks.map((task) => ({ + id: task.id, + title: task.title, + description: `${task.operation === 'open' ? '开启' : '关闭'} ${getTaskRoomTypeLabel(task)}`, + status: task.status, + meta: `${task.dateRange[0]} ~ ${task.dateRange[1]}`, + })) + ), [completedTasks]); + + const currentTaskSource = activeTaskTab === 'pending' ? pendingTasks : completedTasks; + const latestTask = currentTaskSource[0]; + + const selectedModelId = useMemo(() => ( + models.some((model) => model.id === chatCurrentAgentId) + ? chatCurrentAgentId + : defaultAgentId + ), [chatCurrentAgentId, defaultAgentId, models]); + + const currentModel = useMemo( + () => models.find((model) => model.id === selectedModelId) || null, + [models, selectedModelId], + ); + async function handleTaskCenterItem(item: TaskCenterItem): Promise { setTaskCenterNotice(null); if (item.type === 'channel') { - if (!channelState.selectedChannels.length) { + if (!selectedChannels.length) { setTaskCenterNotice('请先在当前项目里配置“已选渠道”,再执行一键打开。'); return; } try { - await invokeIpc(IPC_EVENTS.OPEN_CHANNEL, channelState.selectedChannels); + await invokeIpc(IPC_EVENTS.OPEN_CHANNEL, selectedChannels); setTaskCenterNotice('已触发“打开渠道”操作。'); } catch (error) { setTaskCenterNotice(error instanceof Error ? error.message : String(error)); @@ -251,7 +350,7 @@ export default function HomePage() { setTaskDialogOpen(true); } - async function handleSaveChannels(items: typeof channelState.selectedChannels): Promise { + async function handleSaveChannels(items: typeof selectedChannels): Promise { await channelStore.saveSelectedChannels(items); setTaskCenterNotice('已更新“打开渠道”配置。'); setAddChannelDialogOpen(false); @@ -281,33 +380,16 @@ export default function HomePage() { return (
-
+
{ - void chatStore.newSession(selectedModelId || undefined); - }} - onSelectConversation={(conversationId) => { - chatStore.switchSession(conversationId); - }} - onRenameConversation={(conversationId) => { - const currentLabel = chat.sessionLabels[conversationId] - || chat.sessions.find((session) => session.key === conversationId)?.displayName - || ''; - const nextLabel = window.prompt('重命名对话', currentLabel); - if (nextLabel) { - chatStore.renameSession(conversationId, nextLabel); - } - }} - onDeleteConversation={(conversationId) => { - const confirmed = window.confirm('确定删除该会话吗?删除后将无法恢复。'); - if (confirmed) { - void chatStore.deleteSession(conversationId); - } - }} + loading={!chatInitialized} + selectedConversationId={chatCurrentSessionKey} + onNewChat={handleNewConversation} + onSelectConversation={handleSelectConversation} + onRenameConversation={handleRenameConversation} + onDeleteConversation={handleDeleteConversation} />
@@ -315,7 +397,7 @@ export default function HomePage() {

智能对话

- 网关状态:{chat.gatewayStatus === 'connected' ? '已连接' : chat.gatewayStatus === 'reconnecting' ? '重连中' : '未连接'} + 网关状态:{chatGatewayStatus === 'connected' ? '已连接' : chatGatewayStatus === 'reconnecting' ? '重连中' : '未连接'} {currentModel ? ` · 当前模型:${currentModel.name}` : ''}
@@ -324,13 +406,13 @@ export default function HomePage() { 模型