Просмотр исходного кода

🛠️ fix(chat): enhance message generation stop behavior

This commit improves the handling of message generation interruption in the
playground chat interface, ensuring both content and thinking process are
properly preserved.

Key changes:
- Preserve existing content when stopping message generation
- Handle both direct reasoningContent and <think> tag formats
- Extract and merge thinking process from unclosed <think> tags
- Maintain consistent thinking chain format with separators
- Auto-collapse reasoning panel after stopping for cleaner UI

Technical details:
- Modified onStopGenerator to properly handle SSE connection closure
- Added regex pattern to extract thinking content from <think> tags
- Implemented proper state management for incomplete messages
- Ensured all content types are preserved in their respective fields

This fix resolves the issue where thinking chain content would be lost
when stopping message generation mid-stream.
Apple\Apple 9 месяцев назад
Родитель
Сommit
22af6af9c7
1 измененных файлов с 54 добавлено и 1 удалено
  1. 54 1
      web/src/pages/Playground/Playground.js

+ 54 - 1
web/src/pages/Playground/Playground.js

@@ -1,4 +1,4 @@
-import React, { useCallback, useContext, useEffect, useState } from 'react';
+import React, { useCallback, useContext, useEffect, useState, useRef } from 'react';
 import { useSearchParams } from 'react-router-dom';
 import { useSearchParams } from 'react-router-dom';
 import { UserContext } from '../../context/User/index.js';
 import { UserContext } from '../../context/User/index.js';
 import {
 import {
@@ -100,6 +100,7 @@ const Playground = () => {
   const [groups, setGroups] = useState([]);
   const [groups, setGroups] = useState([]);
   const [showSettings, setShowSettings] = useState(true);
   const [showSettings, setShowSettings] = useState(true);
   const [styleState, styleDispatch] = useContext(StyleContext);
   const [styleState, styleDispatch] = useContext(StyleContext);
+  const sseSourceRef = useRef(null);
 
 
   const handleInputChange = (name, value) => {
   const handleInputChange = (name, value) => {
     setInputs((inputs) => ({ ...inputs, [name]: value }));
     setInputs((inputs) => ({ ...inputs, [name]: value }));
@@ -210,9 +211,13 @@ const Playground = () => {
       payload: JSON.stringify(payload),
       payload: JSON.stringify(payload),
     });
     });
 
 
+    // 保存 source 引用以便后续停止生成
+    sseSourceRef.current = source;
+
     source.addEventListener('message', (e) => {
     source.addEventListener('message', (e) => {
       if (e.data === '[DONE]') {
       if (e.data === '[DONE]') {
         source.close();
         source.close();
+        sseSourceRef.current = null;
         completeMessage();
         completeMessage();
         return;
         return;
       }
       }
@@ -240,6 +245,7 @@ const Playground = () => {
       const errorMessage = e.data || t('请求发生错误');
       const errorMessage = e.data || t('请求发生错误');
       streamMessageUpdate(errorMessage, 'content');
       streamMessageUpdate(errorMessage, 'content');
       completeMessage('error');
       completeMessage('error');
+      sseSourceRef.current = null;
       source.close();
       source.close();
     });
     });
 
 
@@ -352,6 +358,51 @@ const Playground = () => {
     });
     });
   }, [setMessage]);
   }, [setMessage]);
 
 
+  const onStopGenerator = useCallback(() => {
+    if (sseSourceRef.current) {
+      sseSourceRef.current.close();
+      sseSourceRef.current = null;
+      setMessage((prevMessage) => {
+        const lastMessage = prevMessage[prevMessage.length - 1];
+        if (lastMessage.status === 'loading' || lastMessage.status === 'incomplete') {
+          let content = lastMessage.content || '';
+          let reasoningContent = lastMessage.reasoningContent || '';
+
+          // 处理 <think> 标签格式的思维链
+          if (content.includes('<think>')) {
+            const thinkTagRegex = /<think>([\s\S]*?)(?:<\/think>|$)/g;
+            let thoughts = [];
+            let replyParts = [];
+            let lastIndex = 0;
+            let match;
+
+            while ((match = thinkTagRegex.exec(content)) !== null) {
+              replyParts.push(content.substring(lastIndex, match.index));
+              thoughts.push(match[1]);
+              lastIndex = match.index + match[0].length;
+            }
+            replyParts.push(content.substring(lastIndex));
+
+            // 更新内容和思维链
+            content = replyParts.join('').trim();
+            if (thoughts.length > 0) {
+              reasoningContent = thoughts.join('\n\n---\n\n');
+            }
+          }
+
+          return [...prevMessage.slice(0, -1), {
+            ...lastMessage,
+            status: 'complete',
+            reasoningContent: reasoningContent,
+            content: content,
+            isReasoningExpanded: false  // 停止时折叠思维链面板
+          }];
+        }
+        return prevMessage;
+      });
+    }
+  }, [setMessage]);
+
   const SettingsToggle = () => {
   const SettingsToggle = () => {
     if (!styleState.isMobile) return null;
     if (!styleState.isMobile) return null;
     return (
     return (
@@ -664,6 +715,8 @@ const Playground = () => {
             chats={message}
             chats={message}
             onMessageSend={onMessageSend}
             onMessageSend={onMessageSend}
             showClearContext
             showClearContext
+            showStopGenerate
+            onStopGenerator={onStopGenerator}
             onClear={() => {
             onClear={() => {
               setMessage([]);
               setMessage([]);
             }}
             }}