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 <noreply@anthropic.com>
---
 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() {
       <ChatInterface
         conversation={currentConversation}
         onSendMessage={handleSendMessage}
+        onStopGeneration={handleStopGeneration}
         isLoading={isLoading}
+        pendingInput={pendingInput}
+        onPendingInputConsumed={() => setPendingInput(null)}
       />
     </div>
   );
-- 
cgit v1.2.3

