fix: don't stream thought steps
This commit is contained in:
parent
44b62a1d19
commit
6d4d7d371f
@ -1,10 +1,9 @@
|
|||||||
import { ChevronDown, ChevronUp, Loader, Check } from 'lucide-react'
|
import { ChevronDown, ChevronUp, Loader, Check } from 'lucide-react'
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { RenderMarkdown } from './RenderMarkdown'
|
import { RenderMarkdown } from './RenderMarkdown'
|
||||||
import { useAppState } from '@/hooks/useAppState'
|
// import { useAppState } from '@/hooks/useAppState'
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
// import { extractThinkingContent } from '@/lib/utils'
|
import { useMemo } from 'react'
|
||||||
import { useMemo, useState, useEffect } from 'react'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
// Define ThoughtStep type
|
// Define ThoughtStep type
|
||||||
@ -50,26 +49,20 @@ const formatDuration = (ms: number) => {
|
|||||||
|
|
||||||
const ThinkingBlock = ({
|
const ThinkingBlock = ({
|
||||||
id,
|
id,
|
||||||
text,
|
// text, // Unused internally
|
||||||
steps = [],
|
steps = [],
|
||||||
loading: propLoading,
|
loading: propLoading,
|
||||||
duration,
|
duration,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const thinkingState = useThinkingStore((state) => state.thinkingState)
|
const thinkingState = useThinkingStore((state) => state.thinkingState)
|
||||||
const setThinkingState = useThinkingStore((state) => state.setThinkingState)
|
const setThinkingState = useThinkingStore((state) => state.setThinkingState)
|
||||||
const isStreamingApp = useAppState((state) => !!state.streamingContent)
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
// Determine actual loading state
|
// Actual loading state comes from prop, determined by whether final text started streaming (Req 2)
|
||||||
const hasThinkTag = text.includes('<think>') && !text.includes('</think>')
|
const loading = propLoading
|
||||||
const hasAnalysisChannel =
|
|
||||||
text.includes('<|channel|>analysis<|message|>') &&
|
|
||||||
!text.includes('<|start|>assistant<|channel|>final<|message|>')
|
|
||||||
|
|
||||||
const loading =
|
|
||||||
propLoading ?? ((hasThinkTag || hasAnalysisChannel) && isStreamingApp)
|
|
||||||
|
|
||||||
// Set default expansion state: expanded if loading, collapsed if done.
|
// 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)
|
const isExpanded = thinkingState[id] ?? (loading ? true : false)
|
||||||
|
|
||||||
// Filter out the 'done' step for streaming display
|
// Filter out the 'done' step for streaming display
|
||||||
@ -78,38 +71,21 @@ const ThinkingBlock = ({
|
|||||||
[steps]
|
[steps]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get the current step being streamed (last step that's not 'done')
|
const N = stepsWithoutDone.length
|
||||||
const currentStreamingStepIndex = stepsWithoutDone.length - 1
|
|
||||||
const currentStep =
|
|
||||||
currentStreamingStepIndex >= 0
|
|
||||||
? stepsWithoutDone[currentStreamingStepIndex]
|
|
||||||
: null
|
|
||||||
|
|
||||||
// Track which step index we're displaying
|
// Determine the step to display in the condensed streaming view (Req 3)
|
||||||
const [displayedStepIndex, setDisplayedStepIndex] = useState(
|
// Show step N-2 when N >= 2 (i.e., when step N-1 is streaming, show the previously finished step)
|
||||||
currentStreamingStepIndex
|
const stepToRenderWhenStreaming = useMemo(() => {
|
||||||
)
|
if (!loading) return null // Only apply this logic when actively loading
|
||||||
const [transitioning, setTransitioning] = useState(false)
|
if (N >= 2) {
|
||||||
|
// Show the penultimate step (index N-2)
|
||||||
// When a new step arrives during streaming, animate the transition
|
return stepsWithoutDone[N - 2]
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}, [currentStreamingStepIndex, loading, displayedStepIndex])
|
return null
|
||||||
|
}, [loading, N, stepsWithoutDone])
|
||||||
|
|
||||||
// Determine if the block is truly empty (streaming started but no content/steps yet)
|
// 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 loading started but no content or steps have arrived yet, display the non-expandable 'Thinking...' block
|
||||||
if (isStreamingEmpty) {
|
if (isStreamingEmpty) {
|
||||||
@ -236,20 +212,19 @@ const ThinkingBlock = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Streaming/Condensed View - shows current step one at a time */}
|
{/* Streaming/Condensed View - shows previous finished step */}
|
||||||
{loading && currentStep && displayedStepIndex >= 0 && (
|
{loading && stepToRenderWhenStreaming && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-2 pl-6 pr-4 text-main-view-fg/60 transition-opacity duration-150 ease-in',
|
'mt-2 pl-6 pr-4 text-main-view-fg/60 transition-opacity duration-150 ease-in'
|
||||||
transitioning ? 'opacity-0' : 'opacity-100'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative border-l border-dashed border-main-view-fg/20 ml-1.5">
|
<div className="relative border-l border-dashed border-main-view-fg/20 ml-1.5">
|
||||||
<div className="relative pl-6 pb-2">
|
<div className="relative pl-6 pb-2">
|
||||||
{/* Bullet point */}
|
{/* Bullet point */}
|
||||||
<div className="absolute left-[-5px] top-[10px] size-2 rounded-full bg-main-view-fg/60" />
|
<div className="absolute left-[-5px] top-[10px] size-2 rounded-full bg-main-view-fg/60" />
|
||||||
{/* Current step content */}
|
{/* Previous completed step content */}
|
||||||
{renderStepContent(currentStep, displayedStepIndex)}
|
{renderStepContent(stepToRenderWhenStreaming, N - 2)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { useAppState } from '@/hooks/useAppState'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useMessages } from '@/hooks/useMessages'
|
import { useMessages } from '@/hooks/useMessages'
|
||||||
import ThinkingBlock from '@/containers/ThinkingBlock'
|
import ThinkingBlock from '@/containers/ThinkingBlock'
|
||||||
// import ToolCallBlock from '@/containers/ToolCallBlock'
|
|
||||||
import { useChat } from '@/hooks/useChat'
|
import { useChat } from '@/hooks/useChat'
|
||||||
import {
|
import {
|
||||||
EditMessageDialog,
|
EditMessageDialog,
|
||||||
@ -252,27 +251,36 @@ export const ThreadContent = memo(
|
|||||||
| { avatar?: React.ReactNode; name?: React.ReactNode }
|
| { avatar?: React.ReactNode; name?: React.ReactNode }
|
||||||
| undefined
|
| undefined
|
||||||
|
|
||||||
// START: Constructing allSteps for ThinkingBlock (Req 5)
|
// Constructing allSteps for ThinkingBlock
|
||||||
const allSteps: ThoughtStep[] = useMemo(() => {
|
const allSteps: ThoughtStep[] = useMemo(() => {
|
||||||
const steps: ThoughtStep[] = []
|
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 thoughtText = extractThinkingContent(reasoningSegment || '')
|
||||||
const thoughtParagraphs = thoughtText
|
const thoughtParagraphs = thoughtText
|
||||||
? thoughtText
|
? thoughtText
|
||||||
.split(/\n\s*\n/)
|
.split(/\n\s*\n/)
|
||||||
.filter((s) => s.trim().length > 0)
|
.filter((s) => s.trim().length > 0)
|
||||||
.map((content) => ({
|
.map((content) => content.trim())
|
||||||
type: 'thought' as const,
|
|
||||||
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) {
|
if (isToolCalls && item.metadata?.tool_calls) {
|
||||||
const toolCalls = item.metadata.tool_calls as ToolCall[]
|
const toolCalls = item.metadata.tool_calls as ToolCall[]
|
||||||
|
|
||||||
for (const call of toolCalls) {
|
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
|
// Tool Call Step
|
||||||
steps.push({
|
steps.push({
|
||||||
type: 'tool_call',
|
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
|
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({
|
steps.push({
|
||||||
type: 'done',
|
type: 'done',
|
||||||
content: 'Done',
|
content: 'Done',
|
||||||
@ -312,11 +334,18 @@ export const ThreadContent = memo(
|
|||||||
item.metadata,
|
item.metadata,
|
||||||
isStreamingThisThread,
|
isStreamingThisThread,
|
||||||
hasReasoning,
|
hasReasoning,
|
||||||
|
textSegment,
|
||||||
])
|
])
|
||||||
// END: Constructing allSteps
|
// END: Constructing allSteps
|
||||||
|
|
||||||
// Determine if we should show the thinking block (has reasoning OR tool calls)
|
// Determine if reasoning phase is actively loading (Req 2)
|
||||||
const shouldShowThinkingBlock = hasReasoning || isToolCalls
|
// 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 (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@ -462,7 +491,7 @@ export const ThreadContent = memo(
|
|||||||
}
|
}
|
||||||
text={reasoningSegment || ''}
|
text={reasoningSegment || ''}
|
||||||
steps={allSteps}
|
steps={allSteps}
|
||||||
loading={isStreamingThisThread}
|
loading={isReasoningActiveLoading} // Req 2: False if textSegment is starting
|
||||||
duration={
|
duration={
|
||||||
item.metadata?.totalThinkingTime as number | undefined
|
item.metadata?.totalThinkingTime as number | undefined
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user