Skip to content

Commit ea922ea

Browse files
committed
refactor: extract and clean <think> tags in ThreadContent
Add a helper `extractContentAndClean` that pulls out the content between `<think>` tags and removes all auxiliary tags from the final output. Update the message rendering logic to use this helper for finalized messages that lack explicit stream events, ensuring that reasoning and final output are correctly separated and displayed. Adjust the reasoning detection to consider extracted reasoning as well as stream events, clean the copy button to use the actual final output, and eliminate duplicate `StreamEvent` type definitions. These changes improve message parsing accuracy and simplify the component’s handling of legacy messages that embed both reasoning and results in the same string.
1 parent 388a9f9 commit ea922ea

File tree

1 file changed

+63
-23
lines changed

1 file changed

+63
-23
lines changed

web-app/src/containers/ThreadContent.tsx

Lines changed: 63 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,34 @@ const cleanReasoning = (content: string) => {
5757
.trim()
5858
}
5959

60+
// Helper function to extract content within <think> tags and strip all auxiliary tags from the final output
61+
const extractContentAndClean = (
62+
rawText: string
63+
): { reasoningText: string; finalOutput: string } => {
64+
// Regex to match content within <think>...</think> tags
65+
const thinkTagRegex = /<think>([\s\S]*?)<\/think>/g
66+
67+
let reasoningText = ''
68+
let finalOutput = rawText
69+
70+
// Extract content within <think> tags for streamedReasoningText
71+
const thinkMatches = [...rawText.matchAll(thinkTagRegex)]
72+
if (thinkMatches.length > 0) {
73+
// Join all reasoning parts separated by newlines
74+
reasoningText = thinkMatches
75+
.map((match) => match[1])
76+
.join('\n\n')
77+
.trim()
78+
}
79+
80+
// 2. Strip ALL auxiliary tags from finalOutput
81+
finalOutput = finalOutput
82+
.replace(thinkTagRegex, '') // Remove <think> tags and content
83+
.trim()
84+
85+
return { reasoningText, finalOutput }
86+
}
87+
6088
const CopyButton = ({ text }: { text: string }) => {
6189
const [copied, setCopied] = useState(false)
6290
const { t } = useTranslation()
@@ -140,15 +168,38 @@ export const ThreadContent = memo(
140168
return { files: [], cleanPrompt: text }
141169
}, [text, item.role])
142170

171+
type StreamEvent = {
172+
timestamp: number
173+
type: 'reasoning_chunk' | 'tool_call' | 'tool_output'
174+
data: any
175+
}
176+
143177
const {
144178
finalOutputText,
145179
streamedReasoningText,
146180
isReasoningActiveLoading,
147181
hasReasoningSteps,
148182
} = useMemo(() => {
149-
// With the streaming functions updated, the text variable now only contains the final output.
150-
const currentFinalText = text.trim()
151-
const currentReasoning = '' // Reasoning is now only derived from streamEvents/allSteps
183+
let currentFinalText = text.trim()
184+
let currentReasoning = '' // Reasoning is now only derived from streamEvents/allSteps
185+
186+
// Extract raw streamEvents and check for finalized state
187+
const streamEvents = (item.metadata?.streamEvents as StreamEvent[]) || []
188+
const isMessageFinalized = !isStreamingThisThread
189+
190+
// If the message is finalized AND there are no streamEvents,
191+
// we assume the 'text' contains the full dump (reasoning + output + tool tags)
192+
if (isMessageFinalized && streamEvents.length === 0) {
193+
// Use the new helper to separate reasoning (from <think>) and clean the final output
194+
const { reasoningText, finalOutput } = extractContentAndClean(text)
195+
currentFinalText = finalOutput
196+
currentReasoning = reasoningText
197+
} else {
198+
// Otherwise, trust the streamEvents path (if present) or the current text is the final output
199+
// We clean the current text just in case, but it should be clean in streaming mode
200+
const { finalOutput } = extractContentAndClean(text)
201+
currentFinalText = finalOutput
202+
}
152203

153204
// Check for tool calls or reasoning events in metadata to determine steps/loading
154205
const isToolCallsPresent = !!(
@@ -158,15 +209,10 @@ export const ThreadContent = memo(
158209
item.metadata.tool_calls.length > 0
159210
)
160211

161-
// Check for any reasoning chunks in the streamEvents
162-
const hasReasoningEvents = !!(
163-
item.metadata &&
164-
'streamEvents' in item.metadata &&
165-
Array.isArray(item.metadata.streamEvents) &&
166-
item.metadata.streamEvents.some(
167-
(e: StreamEvent) => e.type === 'reasoning_chunk'
168-
)
169-
)
212+
// Check for any reasoning chunks in the streamEvents OR if we extracted reasoning from text
213+
const hasReasoningEvents =
214+
streamEvents.some((e: StreamEvent) => e.type === 'reasoning_chunk') ||
215+
currentReasoning.length > 0 // Added check for extracted reasoning
170216

171217
const hasSteps = isToolCallsPresent || hasReasoningEvents
172218

@@ -262,12 +308,6 @@ export const ThreadContent = memo(
262308
| { avatar?: React.ReactNode; name?: React.ReactNode }
263309
| undefined
264310

265-
type StreamEvent = {
266-
timestamp: number
267-
type: 'reasoning_chunk' | 'tool_call' | 'tool_output'
268-
data: any
269-
}
270-
271311
// Constructing allSteps for ThinkingBlock - CHRONOLOGICAL approach
272312
const allSteps: ReActStep[] = useMemo(() => {
273313
const steps: ReActStep[] = []
@@ -376,7 +416,7 @@ export const ThreadContent = memo(
376416

377417
const rawReasoningContent = streamedReasoningText || ''
378418
const reasoningParagraphs = rawReasoningContent
379-
? rawReasoningContent
419+
? rawReasoningContent // streamedReasoningText is now populated from <think> tags if present
380420
.split(/\n\s*\n/)
381421
.filter((s) => s.trim().length > 0)
382422
.map((content) => content.trim())
@@ -653,7 +693,7 @@ export const ThreadContent = memo(
653693
/>
654694
)}
655695

656-
{(
696+
{
657697
<div className="flex items-center gap-2 text-main-view-fg/60 text-xs">
658698
<div className={cn('flex items-center gap-2')}>
659699
<div
@@ -668,10 +708,10 @@ export const ThreadContent = memo(
668708
item.updateMessage && item.updateMessage(item, message)
669709
}
670710
/>
671-
<CopyButton text={item.content?.[0]?.text.value || ''} />
711+
<CopyButton text={finalOutputText || ''} />{' '}
712+
{/* Use finalOutputText for copy */}
672713
<DeleteMessageDialog onDelete={removeMessage} />
673714
<MessageMetadataDialog metadata={item.metadata} />
674-
675715
{item.isLastMessage && selectedModel && (
676716
<Tooltip>
677717
<TooltipTrigger asChild>
@@ -697,7 +737,7 @@ export const ThreadContent = memo(
697737
/>
698738
</div>
699739
</div>
700-
)}
740+
}
701741
</>
702742
)}
703743

0 commit comments

Comments
 (0)