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
This commit is contained in:
Apple\Apple
2025-05-31 01:12:45 +08:00
parent f7a16c6ca5
commit 02bc3cde53
3 changed files with 17 additions and 3 deletions

View File

@@ -128,7 +128,7 @@ const MessageContent = ({
currentDisplayableFinalContent = baseContentForDisplay.replace(/<\/?think>/g, '').trim();
}
const headerText = isThinkingStatus ? t('思考中...') : t('思考过程');
const headerText = (isThinkingStatus && !message.isThinkingComplete) ? t('思考中...') : t('思考过程');
const finalExtractedThinkingContent = currentExtractedThinkingContent;
const finalDisplayableFinalContent = currentDisplayableFinalContent;
@@ -192,7 +192,7 @@ const MessageContent = ({
</div>
</div>
<div className="flex items-center gap-2 sm:gap-3">
{isThinkingStatus && (
{isThinkingStatus && !message.isThinkingComplete && (
<div className="flex items-center gap-1 sm:gap-2">
<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">
@@ -200,7 +200,7 @@ const MessageContent = ({
</Typography.Text>
</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">
{message.isReasoningExpanded ?
<ChevronUp size={styleState.isMobile ? 12 : 16} className="text-purple-600" /> :

View File

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

View File

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