From ed2ee0cb0270e116a2d1e163bc9ca688b90358f0 Mon Sep 17 00:00:00 2001 From: parabala <115564000+parabala@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:46:42 +0800 Subject: [PATCH] fix(frontend): fix history chat cannot continue issue When users click a history task to continue chatting, if WebSocket is not connected at that moment, the task room will not be joined. This causes the chat to not work properly because messages are not synced via WebSocket. Root cause: The original useEffect only handles joinTask when selectedTask changes. If WebSocket is disconnected when selectedTask is set, joinTask returns early with 'Not connected' error. When WebSocket reconnects later, there was no logic to rejoin the task room. Fix: Add a separate useEffect that monitors isConnected state. When WebSocket reconnects and there is a selected task, it automatically rejoins the task room. joinTask has built-in deduplication, so it's safe to call multiple times. Changes: - Add useEffect to handle WebSocket reconnection in taskContext.tsx - When isConnected becomes true and selectedTask exists, rejoin the task room - Update comments to clarify the logic --- .../tasks/components/chat/ChatArea.tsx | 9 ++++-- .../components/chat/useChatStreamHandlers.tsx | 10 ++++++ .../features/tasks/contexts/taskContext.tsx | 31 ++++++++++++------- frontend/src/i18n/locales/en/chat.json | 4 ++- frontend/src/i18n/locales/zh-CN/chat.json | 4 ++- 5 files changed, 41 insertions(+), 17 deletions(-) diff --git a/frontend/src/features/tasks/components/chat/ChatArea.tsx b/frontend/src/features/tasks/components/chat/ChatArea.tsx index 91c1832c7..9e9373fed 100644 --- a/frontend/src/features/tasks/components/chat/ChatArea.tsx +++ b/frontend/src/features/tasks/components/chat/ChatArea.tsx @@ -116,7 +116,7 @@ function ChatAreaContent({ const { quote, clearQuote, formatQuoteForMessage } = useQuote() // Task context - const { selectedTaskDetail, setSelectedTask, accessDenied } = useTaskContext() + const { selectedTask, selectedTaskDetail, setSelectedTask, accessDenied } = useTaskContext() // Use useTaskStateMachine hook for reactive state updates (SINGLE SOURCE OF TRUTH per AGENTS.md) const { state: taskState } = useTaskStateMachine(selectedTaskDetail?.id) @@ -313,12 +313,14 @@ function ChatAreaContent({ useEffect(() => { if (filteredTeams.length === 0) return - // Extract team ID from task detail + // Extract team ID from task detail or selectedTask as fallback + // selectedTask is set immediately when clicking a task from history + // selectedTaskDetail may take time to load and may not include team info const detailTeamId = selectedTaskDetail?.team ? typeof selectedTaskDetail.team === 'number' ? selectedTaskDetail.team : (selectedTaskDetail.team as Team).id - : null + : selectedTask?.team_id || null // Case 1: Sync from task detail (HIGHEST PRIORITY) // Only sync when URL taskId matches taskDetail.id to prevent race conditions @@ -389,6 +391,7 @@ function ChatAreaContent({ } }, [ filteredTeams, + selectedTask, selectedTaskDetail, taskIdFromUrl, selectedTeam, diff --git a/frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx b/frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx index da224381b..2b5b7eae1 100644 --- a/frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx +++ b/frontend/src/features/tasks/components/chat/useChatStreamHandlers.tsx @@ -452,6 +452,16 @@ export function useChatStreamHandlers({ const message = overrideMessage?.trim() || taskInputMessage.trim() if (!message && !shouldHideChatInput) return + // Check if team is selected before sending + if (!selectedTeam?.id) { + toast({ + variant: 'destructive', + title: t('chat:errors.team_not_selected') || '请选择智能体', + description: t('chat:errors.team_not_selected_description') || '请从列表中选择一个智能体后开始对话', + }) + return + } + if (!isAttachmentReadyToSend) { toast({ variant: 'destructive', diff --git a/frontend/src/features/tasks/contexts/taskContext.tsx b/frontend/src/features/tasks/contexts/taskContext.tsx index a7c9a24f5..9fa588512 100644 --- a/frontend/src/features/tasks/contexts/taskContext.tsx +++ b/frontend/src/features/tasks/contexts/taskContext.tsx @@ -711,10 +711,9 @@ export const TaskContextProvider = ({ children }: { children: ReactNode }) => { } } - // Trigger task detail refresh and manage WebSocket room when selectedTask changes - // NOTE: joinTask is called here, and SocketContext's joinTask already has deduplication logic - // (joinedTasksRef.current.has(taskId) check), so multiple calls are safe but wasteful. - // We only call joinTask when isConnected is true to avoid unnecessary calls. + // Effect 1: Handle task selection changes + // When selectedTask changes, leave old room and join new room + // This effect only reacts to selectedTask changes, not isConnected useEffect(() => { const currentTaskId = selectedTask?.id ?? null const previousTaskId = previousTaskIdRef.current @@ -728,8 +727,8 @@ export const TaskContextProvider = ({ children }: { children: ReactNode }) => { previousTaskIdRef.current = currentTaskId if (selectedTask) { - // Only join task room when WebSocket is connected - // This prevents duplicate joins when both selectedTask and isConnected change + // Join task room when WebSocket is connected + // joinTask has built-in deduplication, so it's safe to call multiple times if (isConnected) { joinTask(selectedTask.id) } @@ -738,14 +737,22 @@ export const TaskContextProvider = ({ children }: { children: ReactNode }) => { } else { setSelectedTaskDetail(null) } + // Note: isConnected is intentionally NOT in dependencies - reconnect logic is handled by Effect 2 // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedTask, leaveTask, joinTask, isConnected]) + }, [selectedTask, leaveTask, joinTask]) - // NOTE: Removed separate isConnected useEffect to prevent duplicate joinTask calls. - // The selectedTask useEffect above now handles both cases: - // 1. When selectedTask changes (and isConnected is true) - // 2. When isConnected changes (and selectedTask is set) - // This is because we added isConnected to the dependency array. + // Effect 2: Handle WebSocket reconnection + // When WebSocket reconnects (isConnected changes from false to true), + // rejoin the current task room to resume real-time updates + useEffect(() => { + // Only proceed if WebSocket is connected and there is a selected task + if (isConnected && selectedTask) { + // Rejoin the task room to ensure we receive real-time updates + // joinTask has built-in deduplication (joinedTasksRef), so it's safe to call again + joinTask(selectedTask.id) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isConnected]) // Mark task as viewed when selectedTaskDetail is loaded // This ensures we have the correct status and timestamps from the backend diff --git a/frontend/src/i18n/locales/en/chat.json b/frontend/src/i18n/locales/en/chat.json index 5d7587df5..eb179493a 100644 --- a/frontend/src/i18n/locales/en/chat.json +++ b/frontend/src/i18n/locales/en/chat.json @@ -257,7 +257,9 @@ "container_oom": "Executor out of memory: Please start a new conversation to retry, or contact administrator to increase memory allocation", "container_error": "Executor error: Container has stopped running. Please start a new conversation to retry", "generic_error": "Request failed: Unknown error, please try again or contact administrator", - "copy_error": "Copy error details" + "copy_error": "Copy error details", + "team_not_selected": "No Agent Selected", + "team_not_selected_description": "Please select an agent from the list to start a conversation" }, "clarification": { "title": "Smart Follow-up", diff --git a/frontend/src/i18n/locales/zh-CN/chat.json b/frontend/src/i18n/locales/zh-CN/chat.json index cb7982753..3b99052c1 100644 --- a/frontend/src/i18n/locales/zh-CN/chat.json +++ b/frontend/src/i18n/locales/zh-CN/chat.json @@ -235,7 +235,9 @@ "container_oom": "执行环境内存不足:请新建一个对话任务重试,或联系管理员增加内存配置", "container_error": "执行环境异常:容器已停止运行,请新建一个对话任务重试", "generic_error": "请求失败:未知错误,请重试或联系管理员", - "copy_error": "复制错误详情" + "copy_error": "复制错误详情", + "team_not_selected": "未选择智能体", + "team_not_selected_description": "请从列表中选择一个智能体后开始对话" }, "clarification": { "title": "智能追问 (Smart Follow-up)",