fix(web): preserve completed thought duration

This commit is contained in:
Maa-ly 2026-05-08 09:59:28 +00:00
parent af754f497a
commit b7e8a9a6bf
2 changed files with 78 additions and 20 deletions

View File

@ -163,6 +163,59 @@ describe('ThinkBlock', () => {
expect(screen.getByText(/Thought/)).toBeInTheDocument()
})
it('preserves the final elapsed time when completion is detected after timers were paused', () => {
const startTime = new Date('2026-05-08T00:00:00.000Z')
vi.setSystemTime(startTime)
const { rerender } = render(
<ChatContextProvider
config={undefined}
isResponding={true}
chatList={[]}
showPromptLog={false}
questionIcon={undefined}
answerIcon={undefined}
onSend={undefined}
onRegenerate={undefined}
onAnnotationEdited={undefined}
onAnnotationAdded={undefined}
onAnnotationRemoved={undefined}
onFeedback={undefined}
>
<ThinkBlock data-think={true}>
<p>Thinking content</p>
</ThinkBlock>
</ChatContextProvider>,
)
act(() => {
vi.setSystemTime(new Date(startTime.getTime() + 1500))
})
rerender(
<ChatContextProvider
config={undefined}
isResponding={true}
chatList={[]}
showPromptLog={false}
questionIcon={undefined}
answerIcon={undefined}
onSend={undefined}
onRegenerate={undefined}
onAnnotationEdited={undefined}
onAnnotationAdded={undefined}
onAnnotationRemoved={undefined}
onFeedback={undefined}
>
<ThinkBlock data-think={true}>
<p>Thinking content[ENDTHINKFLAG]</p>
</ThinkBlock>
</ChatContextProvider>,
)
expect(screen.getByText(/Thought\(1\.5s\)/)).toBeInTheDocument()
})
it('should stop timer when isResponding is undefined (historical conversation outside active response)', () => {
// Render without ChatContextProvider — simulates historical conversation
render(

View File

@ -4,53 +4,61 @@ import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useChatContext } from '../chat/chat/context'
const hasEndThink = (children: any): boolean => {
const hasEndThink = (children: React.ReactNode): boolean => {
if (typeof children === 'string')
return children.includes('[ENDTHINKFLAG]')
if (Array.isArray(children))
return children.some(child => hasEndThink(child))
if (children?.props?.children)
if (React.isValidElement<{ children?: React.ReactNode }>(children) && children.props.children)
return hasEndThink(children.props.children)
return false
}
const removeEndThink = (children: any): any => {
const removeEndThink = (children: React.ReactNode): React.ReactNode => {
if (typeof children === 'string')
return children.replace('[ENDTHINKFLAG]', '')
if (Array.isArray(children))
return children.map(child => removeEndThink(child))
if (children?.props?.children) {
if (React.isValidElement<{ children?: React.ReactNode }>(children) && children.props.children) {
return React.cloneElement(
children,
{
...children.props,
children: removeEndThink(children.props.children),
},
undefined,
removeEndThink(children.props.children),
)
}
return children
}
const useThinkTimer = (children: any) => {
const getElapsedSeconds = (startTime: number, endTime: number) => {
return Math.floor((endTime - startTime) / 100) / 10
}
const useThinkTimer = (children: React.ReactNode) => {
const { isResponding } = useChatContext()
const endThinkDetected = hasEndThink(children)
const [startTime] = useState(() => Date.now())
const [elapsedTime, setElapsedTime] = useState(0)
const [isComplete, setIsComplete] = useState(() => endThinkDetected)
const timerRef = useRef<NodeJS.Timeout | null>(null)
const finalElapsedTimeRef = useRef<number | null>(null)
const completionDetected = endThinkDetected || !isResponding
if (completionDetected && finalElapsedTimeRef.current === null)
finalElapsedTimeRef.current = getElapsedSeconds(startTime, Date.now())
const isComplete = finalElapsedTimeRef.current !== null
useEffect(() => {
if (isComplete)
return
timerRef.current = setInterval(() => {
setElapsedTime(Math.floor((Date.now() - startTime) / 100) / 10)
setElapsedTime(getElapsedSeconds(startTime, Date.now()))
}, 100)
return () => {
@ -59,15 +67,12 @@ const useThinkTimer = (children: any) => {
}
}, [startTime, isComplete])
useEffect(() => {
// Stop timer when:
// 1. Content has [ENDTHINKFLAG] marker (normal completion)
// 2. isResponding is not true (false = user clicked stop, undefined = historical conversation)
if (endThinkDetected || !isResponding)
setIsComplete(true)
}, [endThinkDetected, isResponding])
return { elapsedTime, isComplete }
return {
elapsedTime: finalElapsedTimeRef.current === null
? elapsedTime
: Math.max(elapsedTime, finalElapsedTimeRef.current),
isComplete,
}
}
type ThinkBlockProps = React.ComponentProps<'details'> & {