From 6d4d7d371f69c3f44338cc21680e21c27fd395f3 Mon Sep 17 00:00:00 2001 From: Akarshan Date: Sun, 19 Oct 2025 09:41:36 +0530 Subject: [PATCH] fix: don't stream thought steps --- web-app/src/containers/ThinkingBlock.tsx | 69 ++++++++---------------- web-app/src/containers/ThreadContent.tsx | 57 +++++++++++++++----- 2 files changed, 65 insertions(+), 61 deletions(-) diff --git a/web-app/src/containers/ThinkingBlock.tsx b/web-app/src/containers/ThinkingBlock.tsx index 808d238d7..b6319c7a3 100644 --- a/web-app/src/containers/ThinkingBlock.tsx +++ b/web-app/src/containers/ThinkingBlock.tsx @@ -1,10 +1,9 @@ import { ChevronDown, ChevronUp, Loader, Check } from 'lucide-react' import { create } from 'zustand' import { RenderMarkdown } from './RenderMarkdown' -import { useAppState } from '@/hooks/useAppState' +// import { useAppState } from '@/hooks/useAppState' import { useTranslation } from '@/i18n/react-i18next-compat' -// import { extractThinkingContent } from '@/lib/utils' -import { useMemo, useState, useEffect } from 'react' +import { useMemo } from 'react' import { cn } from '@/lib/utils' // Define ThoughtStep type @@ -50,26 +49,20 @@ const formatDuration = (ms: number) => { const ThinkingBlock = ({ id, - text, + // text, // Unused internally steps = [], loading: propLoading, duration, }: Props) => { const thinkingState = useThinkingStore((state) => state.thinkingState) const setThinkingState = useThinkingStore((state) => state.setThinkingState) - const isStreamingApp = useAppState((state) => !!state.streamingContent) const { t } = useTranslation() - // Determine actual loading state - const hasThinkTag = text.includes('') && !text.includes('') - const hasAnalysisChannel = - text.includes('<|channel|>analysis<|message|>') && - !text.includes('<|start|>assistant<|channel|>final<|message|>') - - const loading = - propLoading ?? ((hasThinkTag || hasAnalysisChannel) && isStreamingApp) + // Actual loading state comes from prop, determined by whether final text started streaming (Req 2) + const loading = propLoading // Set default expansion state: expanded if loading, collapsed if done. + // If loading transitions to false (textSegment starts), this defaults to collapsed if state is absent. const isExpanded = thinkingState[id] ?? (loading ? true : false) // Filter out the 'done' step for streaming display @@ -78,38 +71,21 @@ const ThinkingBlock = ({ [steps] ) - // Get the current step being streamed (last step that's not 'done') - const currentStreamingStepIndex = stepsWithoutDone.length - 1 - const currentStep = - currentStreamingStepIndex >= 0 - ? stepsWithoutDone[currentStreamingStepIndex] - : null + const N = stepsWithoutDone.length - // Track which step index we're displaying - const [displayedStepIndex, setDisplayedStepIndex] = useState( - currentStreamingStepIndex - ) - const [transitioning, setTransitioning] = useState(false) - - // When a new step arrives during streaming, animate the transition - useEffect(() => { - if (loading && currentStreamingStepIndex > displayedStepIndex) { - setTransitioning(true) - - const timeout = setTimeout(() => { - setDisplayedStepIndex(currentStreamingStepIndex) - setTransitioning(false) - }, 150) - - return () => clearTimeout(timeout) - } else if (!loading) { - // When streaming stops, ensure we show the final state - setDisplayedStepIndex(currentStreamingStepIndex) + // Determine the step to display in the condensed streaming view (Req 3) + // Show step N-2 when N >= 2 (i.e., when step N-1 is streaming, show the previously finished step) + const stepToRenderWhenStreaming = useMemo(() => { + if (!loading) return null // Only apply this logic when actively loading + if (N >= 2) { + // Show the penultimate step (index N-2) + return stepsWithoutDone[N - 2] } - }, [currentStreamingStepIndex, loading, displayedStepIndex]) + return null + }, [loading, N, stepsWithoutDone]) // Determine if the block is truly empty (streaming started but no content/steps yet) - const isStreamingEmpty = loading && stepsWithoutDone.length === 0 + const isStreamingEmpty = loading && N === 0 // If loading started but no content or steps have arrived yet, display the non-expandable 'Thinking...' block if (isStreamingEmpty) { @@ -236,20 +212,19 @@ const ThinkingBlock = ({ - {/* Streaming/Condensed View - shows current step one at a time */} - {loading && currentStep && displayedStepIndex >= 0 && ( + {/* Streaming/Condensed View - shows previous finished step */} + {loading && stepToRenderWhenStreaming && (
{/* Bullet point */}
- {/* Current step content */} - {renderStepContent(currentStep, displayedStepIndex)} + {/* Previous completed step content */} + {renderStepContent(stepToRenderWhenStreaming, N - 2)}
diff --git a/web-app/src/containers/ThreadContent.tsx b/web-app/src/containers/ThreadContent.tsx index f7c8102f2..6888dfdc7 100644 --- a/web-app/src/containers/ThreadContent.tsx +++ b/web-app/src/containers/ThreadContent.tsx @@ -7,7 +7,6 @@ import { useAppState } from '@/hooks/useAppState' import { cn } from '@/lib/utils' import { useMessages } from '@/hooks/useMessages' import ThinkingBlock from '@/containers/ThinkingBlock' -// import ToolCallBlock from '@/containers/ToolCallBlock' import { useChat } from '@/hooks/useChat' import { EditMessageDialog, @@ -252,27 +251,36 @@ export const ThreadContent = memo( | { avatar?: React.ReactNode; name?: React.ReactNode } | undefined - // START: Constructing allSteps for ThinkingBlock (Req 5) + // Constructing allSteps for ThinkingBlock const allSteps: ThoughtStep[] = useMemo(() => { const steps: ThoughtStep[] = [] - // 1. Extract thought paragraphs + // Extract thought paragraphs from reasoningSegment. We assume these are ordered + // relative to tool calls. const thoughtText = extractThinkingContent(reasoningSegment || '') const thoughtParagraphs = thoughtText ? thoughtText .split(/\n\s*\n/) .filter((s) => s.trim().length > 0) - .map((content) => ({ - type: 'thought' as const, - content: content.trim(), - })) + .map((content) => content.trim()) : [] - steps.push(...thoughtParagraphs) - // 2. Extract tool steps + let thoughtIndex = 0 + + // 2. Interleave tool steps and thought steps if (isToolCalls && item.metadata?.tool_calls) { const toolCalls = item.metadata.tool_calls as ToolCall[] + for (const call of toolCalls) { + // Check for thought chunk preceding this tool call + if (thoughtIndex < thoughtParagraphs.length) { + steps.push({ + type: 'thought', + content: thoughtParagraphs[thoughtIndex], + }) + thoughtIndex++ + } + // Tool Call Step steps.push({ type: 'tool_call', @@ -295,9 +303,23 @@ export const ThreadContent = memo( } } - // 3. Add Done step if not streaming + // Add remaining thoughts (e.g., final answer formulation thought) + while (thoughtIndex < thoughtParagraphs.length) { + steps.push({ + type: 'thought', + content: thoughtParagraphs[thoughtIndex], + }) + thoughtIndex++ + } + + // Add Done step if not streaming AND the reasoning/tooling process is concluded const totalTime = item.metadata?.totalThinkingTime as number | undefined - if (!isStreamingThisThread && (hasReasoning || isToolCalls)) { + + // If the thread is not streaming, and we had steps or final text output, we add 'done'. + if ( + !isStreamingThisThread && + (hasReasoning || isToolCalls || textSegment) + ) { steps.push({ type: 'done', content: 'Done', @@ -312,11 +334,18 @@ export const ThreadContent = memo( item.metadata, isStreamingThisThread, hasReasoning, + textSegment, ]) // END: Constructing allSteps - // Determine if we should show the thinking block (has reasoning OR tool calls) - const shouldShowThinkingBlock = hasReasoning || isToolCalls + // Determine if reasoning phase is actively loading (Req 2) + // Loading is true only if streaming is happening AND we haven't started outputting final text yet. + const isReasoningActiveLoading = + isStreamingThisThread && textSegment.length === 0 + + // Determine if we should show the thinking block (has reasoning OR tool calls OR currently loading reasoning) + const shouldShowThinkingBlock = + hasReasoning || isToolCalls || isReasoningActiveLoading return ( @@ -462,7 +491,7 @@ export const ThreadContent = memo( } text={reasoningSegment || ''} steps={allSteps} - loading={isStreamingThisThread} + loading={isReasoningActiveLoading} // Req 2: False if textSegment is starting duration={ item.metadata?.totalThinkingTime as number | undefined }