🛠️ fix(chat): optimize think tag handling and reasoning panel behavior

BREAKING CHANGE: Refactor message stream processing and think tag handling logic

- Improve automatic collapse behavior of reasoning panel
  - Collapse panel immediately after reasoning content completion
  - Add detection for complete think tag pairs
  - Remove dependency on full content completion

- Enhance think tag processing
  - Unify tag handling logic across stream and stop states
  - Add robust handling for unclosed think tags
  - Prevent markdown rendering errors from malformed tags

- Simplify state management
  - Reduce complexity in collapse condition checks
  - Remove redundant state transitions
  - Improve code readability by removing unnecessary comments

This commit ensures a more stable and predictable behavior in the chat interface, particularly in handling streaming responses and reasoning content display.
This commit is contained in:
Apple\Apple
2025-05-29 12:02:29 +08:00
parent 31ece25252
commit 75c94d9374

View File

@@ -633,8 +633,7 @@ const Playground = () => {
status: 'incomplete', status: 'incomplete',
}; };
} else if (type === 'content') { } else if (type === 'content') {
const shouldCollapseReasoning = !lastMessage.content && lastMessage.reasoningContent && lastMessage.isReasoningExpanded; const shouldCollapseReasoning = !lastMessage.content && lastMessage.reasoningContent;
const newContent = (lastMessage.content || '') + textChunk; const newContent = (lastMessage.content || '') + textChunk;
let shouldCollapseFromThinkTag = false; let shouldCollapseFromThinkTag = false;
@@ -805,57 +804,54 @@ const Playground = () => {
let currentContent = lastMessage.content || ''; let currentContent = lastMessage.content || '';
let currentReasoningContent = lastMessage.reasoningContent || ''; let currentReasoningContent = lastMessage.reasoningContent || '';
let finalDisplayableContent = currentContent; if (currentContent.includes('<think>')) {
let extractedThinking = currentReasoningContent; const thinkTagRegex = /<think>([\s\S]*?)<\/think>/g;
let match;
let thoughtsFromPairedTags = [];
let replyParts = [];
let lastIndex = 0;
const thinkTagRegex = /<think>([\s\S]*?)<\/think>/g; while ((match = thinkTagRegex.exec(currentContent)) !== null) {
let match; replyParts.push(currentContent.substring(lastIndex, match.index));
let thoughtsFromPairedTags = []; thoughtsFromPairedTags.push(match[1]);
let contentParts = []; lastIndex = match.index + match[0].length;
let lastIndex = 0;
thinkTagRegex.lastIndex = 0;
let tempContentForPairExtraction = finalDisplayableContent;
while ((match = thinkTagRegex.exec(tempContentForPairExtraction)) !== null) {
contentParts.push(tempContentForPairExtraction.substring(lastIndex, match.index));
thoughtsFromPairedTags.push(match[1]);
lastIndex = match.index + match[0].length;
}
contentParts.push(tempContentForPairExtraction.substring(lastIndex));
if (thoughtsFromPairedTags.length > 0) {
const pairedThoughtsStr = thoughtsFromPairedTags.join('\n\n---\n\n');
if (extractedThinking) {
extractedThinking += '\n\n---\n\n' + pairedThoughtsStr;
} else {
extractedThinking = pairedThoughtsStr;
} }
} replyParts.push(currentContent.substring(lastIndex));
finalDisplayableContent = contentParts.join('');
const lastOpenThinkIndex = finalDisplayableContent.lastIndexOf('<think>'); if (thoughtsFromPairedTags.length > 0) {
const pairedThoughtsStr = thoughtsFromPairedTags.join('\n\n---\n\n');
if (currentReasoningContent) {
currentReasoningContent += '\n\n---\n\n' + pairedThoughtsStr;
} else {
currentReasoningContent = pairedThoughtsStr;
}
}
currentContent = replyParts.join('');
}
const lastOpenThinkIndex = currentContent.lastIndexOf('<think>');
if (lastOpenThinkIndex !== -1) { if (lastOpenThinkIndex !== -1) {
const fragmentAfterLastOpen = finalDisplayableContent.substring(lastOpenThinkIndex); const fragmentAfterLastOpen = currentContent.substring(lastOpenThinkIndex);
if (!fragmentAfterLastOpen.includes('</think>')) { if (!fragmentAfterLastOpen.includes('</think>')) {
const unclosedThought = fragmentAfterLastOpen.substring('<think>'.length); const unclosedThought = fragmentAfterLastOpen.substring('<think>'.length).trim();
if (unclosedThought.trim()) { if (unclosedThought) {
if (extractedThinking) { if (currentReasoningContent) {
extractedThinking += '\n\n---\n\n' + unclosedThought; currentReasoningContent += '\n\n---\n\n' + unclosedThought;
} else { } else {
extractedThinking = unclosedThought; currentReasoningContent = unclosedThought;
} }
} }
finalDisplayableContent = finalDisplayableContent.substring(0, lastOpenThinkIndex); currentContent = currentContent.substring(0, lastOpenThinkIndex);
} }
} }
finalDisplayableContent = finalDisplayableContent.replace(/<\/?think>/g, '').trim(); currentContent = currentContent.replace(/<\/?think>/g, '').trim();
return [...prevMessage.slice(0, -1), { return [...prevMessage.slice(0, -1), {
...lastMessage, ...lastMessage,
status: 'complete', status: 'complete',
reasoningContent: extractedThinking || null, reasoningContent: currentReasoningContent || null,
content: finalDisplayableContent, content: currentContent,
isReasoningExpanded: false isReasoningExpanded: false
}]; }];
} }
@@ -1023,22 +1019,19 @@ const Playground = () => {
thinkingSource = 'reasoningContent'; thinkingSource = 'reasoningContent';
} }
if (baseContentForDisplay.includes('<think')) { if (baseContentForDisplay.includes('<think>')) {
const thinkTagRegex = /<think>([\s\S]*?)<\/think>/g; const thinkTagRegex = /<think>([\s\S]*?)<\/think>/g;
let match; let match;
let thoughtsFromPairedTags = []; let thoughtsFromPairedTags = [];
let replyParts = []; let replyParts = [];
let lastIndex = 0; let lastIndex = 0;
thinkTagRegex.lastIndex = 0; while ((match = thinkTagRegex.exec(baseContentForDisplay)) !== null) {
let tempContent = baseContentForDisplay; replyParts.push(baseContentForDisplay.substring(lastIndex, match.index));
while ((match = thinkTagRegex.exec(tempContent)) !== null) {
replyParts.push(tempContent.substring(lastIndex, match.index));
thoughtsFromPairedTags.push(match[1]); thoughtsFromPairedTags.push(match[1]);
lastIndex = match.index + match[0].length; lastIndex = match.index + match[0].length;
} }
replyParts.push(tempContent.substring(lastIndex)); replyParts.push(baseContentForDisplay.substring(lastIndex));
if (thoughtsFromPairedTags.length > 0) { if (thoughtsFromPairedTags.length > 0) {
const pairedThoughtsStr = thoughtsFromPairedTags.join('\n\n---\n\n'); const pairedThoughtsStr = thoughtsFromPairedTags.join('\n\n---\n\n');
@@ -1047,34 +1040,25 @@ const Playground = () => {
} else { } else {
combinedThinkingContent = pairedThoughtsStr; combinedThinkingContent = pairedThoughtsStr;
} }
thinkingSource = thinkingSource ? thinkingSource + ' & <think> tags' : '<think> tags';
if (thinkingSource === 'reasoningContent') {
thinkingSource = 'reasoningContent & <think> tags';
} else if (!thinkingSource) {
thinkingSource = '<think> tags';
}
} }
baseContentForDisplay = replyParts.join(''); baseContentForDisplay = replyParts.join('');
} }
if (isThinkingStatus && baseContentForDisplay.includes('<think')) { if (isThinkingStatus) {
const lastOpenThinkIndex = baseContentForDisplay.lastIndexOf('<think>'); const lastOpenThinkIndex = baseContentForDisplay.lastIndexOf('<think>');
if (lastOpenThinkIndex !== -1) { if (lastOpenThinkIndex !== -1) {
const fragmentAfterLastOpen = baseContentForDisplay.substring(lastOpenThinkIndex); const fragmentAfterLastOpen = baseContentForDisplay.substring(lastOpenThinkIndex);
if (!fragmentAfterLastOpen.includes('</think>')) { if (!fragmentAfterLastOpen.includes('</think>')) {
const unclosedThought = fragmentAfterLastOpen.substring('<think>'.length); const unclosedThought = fragmentAfterLastOpen.substring('<think>'.length).trim();
if (unclosedThought.trim()) { if (unclosedThought) {
if (combinedThinkingContent) { if (combinedThinkingContent) {
combinedThinkingContent += '\n\n---\n\n' + unclosedThought; combinedThinkingContent += '\n\n---\n\n' + unclosedThought;
} else { } else {
combinedThinkingContent = unclosedThought; combinedThinkingContent = unclosedThought;
} }
if (thinkingSource && (thinkingSource.includes('<think> tags') || thinkingSource.includes('reasoningContent'))) { thinkingSource = thinkingSource ? thinkingSource + ' + streaming <think>' : 'streaming <think>';
thinkingSource += ' + streaming <think>';
} else if (!thinkingSource) {
thinkingSource = 'streaming <think>';
}
} }
baseContentForDisplay = baseContentForDisplay.substring(0, lastOpenThinkIndex); baseContentForDisplay = baseContentForDisplay.substring(0, lastOpenThinkIndex);
} }
@@ -1082,12 +1066,7 @@ const Playground = () => {
} }
currentExtractedThinkingContent = combinedThinkingContent || null; currentExtractedThinkingContent = combinedThinkingContent || null;
currentDisplayableFinalContent = baseContentForDisplay.replace(/<\/?think>/g, '').trim();
if (typeof baseContentForDisplay === 'string') {
currentDisplayableFinalContent = baseContentForDisplay.replace(/<\/?think>/g, '').trim();
} else {
currentDisplayableFinalContent = "";
}
} }
const headerText = isThinkingStatus ? t('思考中...') : t('思考过程'); const headerText = isThinkingStatus ? t('思考中...') : t('思考过程');
@@ -1161,11 +1140,11 @@ const Playground = () => {
className={`transition-all duration-500 ease-out ${message.isReasoningExpanded ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0' className={`transition-all duration-500 ease-out ${message.isReasoningExpanded ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
} overflow-hidden`} } overflow-hidden`}
> >
{message.isReasoningExpanded && finalExtractedThinkingContent && ( {message.isReasoningExpanded && (
<div className="p-5 pt-4"> <div className="p-5 pt-4">
<div className="bg-white/70 backdrop-blur-sm rounded-xl p-4 shadow-inner overflow-x-auto max-h-50 overflow-y-auto"> <div className="bg-white/70 backdrop-blur-sm rounded-xl p-4 shadow-inner overflow-x-auto max-h-50 overflow-y-auto">
<div className="prose prose-sm prose-purple max-w-none"> <div className="prose prose-sm prose-purple max-w-none">
<MarkdownRender raw={finalExtractedThinkingContent.replace(/<\/?think>/g, '')} /> <MarkdownRender raw={finalExtractedThinkingContent} />
</div> </div>
</div> </div>
</div> </div>