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

✨ feat: improve thinking state management for better UX in reasoning display

Previously, the "thinking" indicator and loading icon would only disappear
after the entire message generation was complete, which created a poor user
experience where users had to wait for the full response to see that the
reasoning phase had finished.

Changes made:
- Add `isThinkingComplete` field to independently track reasoning state
- Update streaming logic to mark thinking complete when content starts flowing
- Detect closed `<think>` tags to mark reasoning completion
- Modify MessageContent component to use independent thinking state
- Update "思考中..." text and loading icon display conditions
- Ensure thinking state is properly set in all completion scenarios
  (non-stream, errors, manual stop)

Now the thinking section immediately shows as complete when reasoning ends,
rather than waiting for the entire message to finish, providing much better
real-time feedback to users.

Files modified:
- web/src/hooks/useApiRequest.js
- web/src/components/playground/MessageContent.js
- web/src/utils/messageUtils.js
Apple\Apple 9 месяцев назад
Родитель
Сommit
02bc3cde53

+ 3 - 3
web/src/components/playground/MessageContent.js

@@ -128,7 +128,7 @@ const MessageContent = ({
     currentDisplayableFinalContent = baseContentForDisplay.replace(/<\/?think>/g, '').trim();
     currentDisplayableFinalContent = baseContentForDisplay.replace(/<\/?think>/g, '').trim();
   }
   }
 
 
-  const headerText = isThinkingStatus ? t('思考中...') : t('思考过程');
+  const headerText = (isThinkingStatus && !message.isThinkingComplete) ? t('思考中...') : t('思考过程');
   const finalExtractedThinkingContent = currentExtractedThinkingContent;
   const finalExtractedThinkingContent = currentExtractedThinkingContent;
   const finalDisplayableFinalContent = currentDisplayableFinalContent;
   const finalDisplayableFinalContent = currentDisplayableFinalContent;
 
 
@@ -192,7 +192,7 @@ const MessageContent = ({
               </div>
               </div>
             </div>
             </div>
             <div className="flex items-center gap-2 sm:gap-3">
             <div className="flex items-center gap-2 sm:gap-3">
-              {isThinkingStatus && (
+              {isThinkingStatus && !message.isThinkingComplete && (
                 <div className="flex items-center gap-1 sm:gap-2">
                 <div className="flex items-center gap-1 sm:gap-2">
                   <Loader2 className="animate-spin text-purple-500" size={styleState.isMobile ? 14 : 18} />
                   <Loader2 className="animate-spin text-purple-500" size={styleState.isMobile ? 14 : 18} />
                   <Typography.Text className="text-purple-600 text-xs sm:text-sm font-medium">
                   <Typography.Text className="text-purple-600 text-xs sm:text-sm font-medium">
@@ -200,7 +200,7 @@ const MessageContent = ({
                   </Typography.Text>
                   </Typography.Text>
                 </div>
                 </div>
               )}
               )}
-              {!isThinkingStatus && (
+              {(!isThinkingStatus || message.isThinkingComplete) && (
                 <div className="w-5 h-5 sm:w-6 sm:h-6 rounded-full bg-purple-100 flex items-center justify-center">
                 <div className="w-5 h-5 sm:w-6 sm:h-6 rounded-full bg-purple-100 flex items-center justify-center">
                   {message.isReasoningExpanded ?
                   {message.isReasoningExpanded ?
                     <ChevronUp size={styleState.isMobile ? 12 : 16} className="text-purple-600" /> :
                     <ChevronUp size={styleState.isMobile ? 12 : 16} className="text-purple-600" /> :

+ 13 - 0
web/src/hooks/useApiRequest.js

@@ -42,25 +42,34 @@ export const useApiRequest = (
             ...newMessage,
             ...newMessage,
             reasoningContent: (lastMessage.reasoningContent || '') + textChunk,
             reasoningContent: (lastMessage.reasoningContent || '') + textChunk,
             status: MESSAGE_STATUS.INCOMPLETE,
             status: MESSAGE_STATUS.INCOMPLETE,
+            isThinkingComplete: false,
           };
           };
         } else if (type === 'content') {
         } else if (type === 'content') {
           const shouldCollapseReasoning = !lastMessage.content && lastMessage.reasoningContent;
           const shouldCollapseReasoning = !lastMessage.content && lastMessage.reasoningContent;
           const newContent = (lastMessage.content || '') + textChunk;
           const newContent = (lastMessage.content || '') + textChunk;
 
 
           let shouldCollapseFromThinkTag = false;
           let shouldCollapseFromThinkTag = false;
+          let thinkingCompleteFromTags = lastMessage.isThinkingComplete;
+
           if (lastMessage.isReasoningExpanded && newContent.includes('</think>')) {
           if (lastMessage.isReasoningExpanded && newContent.includes('</think>')) {
             const thinkMatches = newContent.match(/<think>/g);
             const thinkMatches = newContent.match(/<think>/g);
             const thinkCloseMatches = newContent.match(/<\/think>/g);
             const thinkCloseMatches = newContent.match(/<\/think>/g);
             if (thinkMatches && thinkCloseMatches &&
             if (thinkMatches && thinkCloseMatches &&
               thinkCloseMatches.length >= thinkMatches.length) {
               thinkCloseMatches.length >= thinkMatches.length) {
               shouldCollapseFromThinkTag = true;
               shouldCollapseFromThinkTag = true;
+              thinkingCompleteFromTags = true; // think标签闭合也标记思考完成
             }
             }
           }
           }
 
 
+          // 如果开始接收content内容,且之前有reasoning内容,或者think标签已闭合,则标记思考完成
+          const isThinkingComplete = (lastMessage.reasoningContent && !lastMessage.isThinkingComplete) ||
+            thinkingCompleteFromTags;
+
           newMessage = {
           newMessage = {
             ...newMessage,
             ...newMessage,
             content: newContent,
             content: newContent,
             status: MESSAGE_STATUS.INCOMPLETE,
             status: MESSAGE_STATUS.INCOMPLETE,
+            isThinkingComplete: isThinkingComplete,
             isReasoningExpanded: (shouldCollapseReasoning || shouldCollapseFromThinkTag)
             isReasoningExpanded: (shouldCollapseReasoning || shouldCollapseFromThinkTag)
               ? false : lastMessage.isReasoningExpanded,
               ? false : lastMessage.isReasoningExpanded,
           };
           };
@@ -86,6 +95,7 @@ export const useApiRequest = (
         {
         {
           ...lastMessage,
           ...lastMessage,
           status: status,
           status: status,
+          isThinkingComplete: true,
           isReasoningExpanded: false
           isReasoningExpanded: false
         }
         }
       ];
       ];
@@ -158,6 +168,7 @@ export const useApiRequest = (
               content: processed.content,
               content: processed.content,
               reasoningContent: processed.reasoningContent,
               reasoningContent: processed.reasoningContent,
               status: MESSAGE_STATUS.COMPLETE,
               status: MESSAGE_STATUS.COMPLETE,
+              isThinkingComplete: true,
               isReasoningExpanded: false
               isReasoningExpanded: false
             };
             };
           }
           }
@@ -182,6 +193,7 @@ export const useApiRequest = (
             ...lastMessage,
             ...lastMessage,
             content: t('请求发生错误: ') + error.message,
             content: t('请求发生错误: ') + error.message,
             status: MESSAGE_STATUS.ERROR,
             status: MESSAGE_STATUS.ERROR,
+            isThinkingComplete: true,
             isReasoningExpanded: false
             isReasoningExpanded: false
           };
           };
         }
         }
@@ -333,6 +345,7 @@ export const useApiRequest = (
               status: MESSAGE_STATUS.COMPLETE,
               status: MESSAGE_STATUS.COMPLETE,
               reasoningContent: processed.reasoningContent || null,
               reasoningContent: processed.reasoningContent || null,
               content: processed.content,
               content: processed.content,
+              isThinkingComplete: true,
               isReasoningExpanded: false
               isReasoningExpanded: false
             }
             }
           ];
           ];

+ 1 - 0
web/src/utils/messageUtils.js

@@ -106,6 +106,7 @@ export const createLoadingAssistantMessage = () => createMessage(
   {
   {
     reasoningContent: '',
     reasoningContent: '',
     isReasoningExpanded: true,
     isReasoningExpanded: true,
+    isThinkingComplete: false,
     status: 'loading'
     status: 'loading'
   }
   }
 );
 );