✨ 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:
@@ -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" /> :
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export const createLoadingAssistantMessage = () => createMessage(
|
|||||||
{
|
{
|
||||||
reasoningContent: '',
|
reasoningContent: '',
|
||||||
isReasoningExpanded: true,
|
isReasoningExpanded: true,
|
||||||
|
isThinkingComplete: false,
|
||||||
status: 'loading'
|
status: 'loading'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user