refactor: simplify reasoning handling in ThreadContent and related hooks

- Remove legacy <think> tag parsing and accumulation of reasoning chunks in the main text buffer.
- Rely exclusively on `streamEvents` to derive reasoning and loading state.
- Update loading logic to account for both tool calls and reasoning events.
- Adjust memo dependencies and return values to avoid stale references.
- Update `useChat` and `completion.ts` to stop mutating the accumulated text with reasoning, keeping the logic purely event‑driven.
- Ensure the ThinkingBlock always renders from the structured steps list, improving consistency and eliminating duplicate content.
This commit is contained in:
Akarshan 2025-10-29 18:58:35 +05:30
parent 37c4a65dbd
commit 0a5e107d0f
No known key found for this signature in database
GPG Key ID: D75C9634A870665F
3 changed files with 27 additions and 71 deletions

View File

@ -146,66 +146,11 @@ export const ThreadContent = memo(
isReasoningActiveLoading,
hasReasoningSteps,
} = useMemo(() => {
const thinkStartTag = '<think>'
const thinkEndTag = '</think>'
let currentFinalText = ''
let currentReasoning = ''
let hasSteps = false
// With the streaming functions updated, the text variable now only contains the final output.
const currentFinalText = text.trim()
const currentReasoning = '' // Reasoning is now only derived from streamEvents/allSteps
const firstThinkStart = text.indexOf(thinkStartTag)
const lastThinkStart = text.lastIndexOf(thinkStartTag)
const lastThinkEnd = text.lastIndexOf(thinkEndTag)
// Check if there's an unclosed <think> tag
const hasOpenThink = lastThinkStart > lastThinkEnd
if (firstThinkStart === -1) {
// No <think> tags at all - everything is final output
currentFinalText = text
} else if (hasOpenThink && isStreamingThisThread) {
// CASE 1: There's an open <think> tag during streaming
// Everything from FIRST <think> onward is reasoning
hasSteps = true
// Text before first <think> is final output
currentFinalText = text.substring(0, firstThinkStart)
// Everything from first <think> onward is reasoning
const reasoningText = text.substring(firstThinkStart)
// Extract content from all <think> blocks (both closed and open)
const reasoningRegex = /<think>([\s\S]*?)(?:<\/think>|$)/g
const matches = [...reasoningText.matchAll(reasoningRegex)]
const reasoningParts = matches.map((match) => cleanReasoning(match[1]))
currentReasoning = reasoningParts.join('\n\n')
} else {
// CASE 2: All <think> tags are closed
// Extract reasoning from inside tags, everything else is final output
hasSteps = true
const reasoningRegex = /<think>[\s\S]*?<\/think>/g
const matches = [...text.matchAll(reasoningRegex)]
let lastIndex = 0
// Build final output from text between/outside <think> blocks
for (const match of matches) {
currentFinalText += text.substring(lastIndex, match.index)
lastIndex = match.index + match[0].length
}
// Add remaining text after last </think>
currentFinalText += text.substring(lastIndex)
// Extract reasoning content
const reasoningParts = matches.map((match) => {
const content = match[0].replace(/<think>|<\/think>/g, '')
return cleanReasoning(content)
})
currentReasoning = reasoningParts.join('\n\n')
}
// Check for tool calls
// Check for tool calls or reasoning events in metadata to determine steps/loading
const isToolCallsPresent = !!(
item.metadata &&
'tool_calls' in item.metadata &&
@ -213,19 +158,29 @@ export const ThreadContent = memo(
item.metadata.tool_calls.length > 0
)
hasSteps = hasSteps || isToolCallsPresent
// Check for any reasoning chunks in the streamEvents
const hasReasoningEvents = !!(
item.metadata &&
'streamEvents' in item.metadata &&
Array.isArray(item.metadata.streamEvents) &&
item.metadata.streamEvents.some(
(e: StreamEvent) => e.type === 'reasoning_chunk'
)
)
// Loading if streaming and no final output yet
const hasSteps = isToolCallsPresent || hasReasoningEvents
// Loading if streaming, no final output yet, but we expect steps (reasoning or tool calls)
const loading =
isStreamingThisThread && currentFinalText.trim().length === 0
isStreamingThisThread && currentFinalText.length === 0 && hasSteps
return {
finalOutputText: currentFinalText.trim(),
finalOutputText: currentFinalText,
streamedReasoningText: currentReasoning,
isReasoningActiveLoading: loading,
hasReasoningSteps: hasSteps,
}
}, [item.content, isStreamingThisThread, item.metadata, text])
}, [item.metadata, text, isStreamingThisThread])
const isToolCalls =
item.metadata &&
@ -516,7 +471,6 @@ export const ThreadContent = memo(
// END: Constructing allSteps
// ====================================================================
// FIX: Determine which text prop to pass to ThinkingBlock
// If we have streamEvents, rely on 'steps' and pass an empty text buffer.
const streamingTextBuffer = useMemo(() => {
const streamEvents = item.metadata?.streamEvents
@ -528,9 +482,11 @@ export const ThreadContent = memo(
return ''
}
// Otherwise, rely on the raw text buffer for rendering (used during initial stream fallback)
return streamedReasoningText
}, [item.metadata?.streamEvents, streamedReasoningText]) // Use the object reference for dependency array
// Since we no longer concatenate reasoning to the main text,
// the only time we'd rely on text buffer is if streamEvents fails to load.
// For robustness, we can simply return an empty string to force use of 'steps'.
return ''
}, [item.metadata?.streamEvents]) // Use the object reference for dependency array
// ====================================================================
// Determine if we should show the thinking block

View File

@ -671,7 +671,7 @@ export const useChat = () => {
const deltaReasoning =
reasoningProcessor.processReasoningChunk(part)
if (deltaReasoning) {
accumulatedText += deltaReasoning
// accumulatedText += deltaReasoning
// Track reasoning event
streamEvents.push({
timestamp: Date.now(),
@ -705,7 +705,7 @@ export const useChat = () => {
// Only finalize and flush if not aborted
if (!abortController.signal.aborted) {
// Finalize reasoning (close any open think tags)
accumulatedText += reasoningProcessor.finalize()
// accumulatedText += reasoningProcessor.finalize()
// Ensure any pending buffered content is rendered at the end
flushIfPending()
}

View File

@ -777,7 +777,7 @@ export const postMessageProcessing = async (
const deltaContent = chunk.choices[0]?.delta?.content || ''
if (textContent?.text) {
if (deltaReasoning) textContent.text.value += deltaReasoning
// if (deltaReasoning) textContent.text.value += deltaReasoning
if (deltaContent) {
textContent.text.value += deltaContent
followUpText += deltaContent