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/api.js | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) (limited to 'frontend/src/api.js') diff --git a/frontend/src/api.js b/frontend/src/api.js index 87ec685..a53ed90 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -73,7 +73,7 @@ export const api = { * @param {function} onEvent - Callback function for each event: (eventType, data) => void * @returns {Promise} */ - async sendMessageStream(conversationId, content, onEvent) { + async sendMessageStream(conversationId, content, onEvent, signal) { const response = await fetch( `${API_BASE}/api/conversations/${conversationId}/message/stream`, { @@ -82,6 +82,7 @@ export const api = { 'Content-Type': 'application/json', }, body: JSON.stringify({ content }), + signal, } ); @@ -91,22 +92,30 @@ export const api = { const reader = response.body.getReader(); const decoder = new TextDecoder(); + let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; - const chunk = decoder.decode(value); - const lines = chunk.split('\n'); + buffer += decoder.decode(value, { stream: true }); - for (const line of lines) { - if (line.startsWith('data: ')) { - const data = line.slice(6); - try { - const event = JSON.parse(data); - onEvent(event.type, event); - } catch (e) { - console.error('Failed to parse SSE event:', e); + // Split on double newline (SSE event boundary) + const parts = buffer.split('\n\n'); + // Last part may be incomplete — keep it in buffer + buffer = parts.pop(); + + for (const part of parts) { + const lines = part.split('\n'); + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6); + try { + const event = JSON.parse(data); + onEvent(event.type, event); + } catch (e) { + console.error('Failed to parse SSE event:', e); + } } } } -- cgit v1.2.3