From c8fae0256c91a0ebe495270aa15baa2f27211268 Mon Sep 17 00:00:00 2001 From: haoyuren <13851610112@163.com> Date: Thu, 12 Feb 2026 12:45:24 -0600 Subject: Multi-turn conversation, stop generation, SSE fix, and UI improvements - Multi-turn context: all council stages now receive conversation history (user messages + Stage 3 chairman responses) for coherent follow-ups - Stop generation: abort streaming mid-request, recover query to input box - SSE parsing: buffer-based chunking to prevent JSON split across packets - Atomic storage: user + assistant messages saved together after completion, preventing dangling messages on abort - GFM markdown: tables, strikethrough via remark-gfm plugin + table styles - Performance: memo user messages and completed assistant messages, only re-render the active streaming message - Model config: gpt-5.2, claude-opus-4.6 as chairman - Always show input box for multi-turn conversations Co-Authored-By: Claude Opus 4.6 --- frontend/src/App.jsx | 54 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 8 deletions(-) (limited to 'frontend/src/App.jsx') diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1954155..8b7a547 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import Sidebar from './components/Sidebar'; import ChatInterface from './components/ChatInterface'; import { api } from './api'; @@ -9,6 +9,8 @@ function App() { const [currentConversationId, setCurrentConversationId] = useState(null); const [currentConversation, setCurrentConversation] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [pendingInput, setPendingInput] = useState(null); + const abortControllerRef = useRef(null); // Load conversations on mount useEffect(() => { @@ -57,9 +59,36 @@ function App() { setCurrentConversationId(id); }; + const handleStopGeneration = () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + + // Recover the last user message into the input box and remove the incomplete pair + setCurrentConversation((prev) => { + const messages = [...prev.messages]; + // Find the last user message to recover its content + let recoveredContent = ''; + // Remove trailing assistant message (incomplete) + if (messages.length > 0 && messages[messages.length - 1].role === 'assistant') { + messages.pop(); + } + // Remove the user message and recover its text + if (messages.length > 0 && messages[messages.length - 1].role === 'user') { + const userMsg = messages.pop(); + recoveredContent = userMsg.content; + } + setPendingInput(recoveredContent); + return { ...prev, messages }; + }); + } + }; + const handleSendMessage = async (content) => { if (!currentConversationId) return; + const controller = new AbortController(); + abortControllerRef.current = controller; setIsLoading(true); try { // Optimistically add user message to UI @@ -91,6 +120,7 @@ function App() { // Send message with streaming await api.sendMessageStream(currentConversationId, content, (eventType, event) => { + if (controller.signal.aborted) return; switch (eventType) { case 'stage1_start': setCurrentConversation((prev) => { @@ -169,15 +199,20 @@ function App() { default: console.log('Unknown event type:', eventType); } - }); + }, controller.signal); } catch (error) { - console.error('Failed to send message:', error); - // Remove optimistic messages on error - setCurrentConversation((prev) => ({ - ...prev, - messages: prev.messages.slice(0, -2), - })); + if (error.name === 'AbortError') { + // User stopped generation — handleStopGeneration already cleaned up messages + } else { + console.error('Failed to send message:', error); + // Remove optimistic messages on error + setCurrentConversation((prev) => ({ + ...prev, + messages: prev.messages.slice(0, -2), + })); + } setIsLoading(false); + abortControllerRef.current = null; } }; @@ -192,7 +227,10 @@ function App() { setPendingInput(null)} /> ); -- cgit v1.2.3