mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 21:28:25 +08:00
Merge 6cbe0aefe7 into 19bf36a716
This commit is contained in:
commit
95487d37f9
@ -1524,14 +1524,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/base/markdown-blocks/think-block.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/base/markdown-blocks/video-block.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 5
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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'> & {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user