diff --git a/src/renderer/src/hooks/useSmoothStream.ts b/src/renderer/src/hooks/useSmoothStream.ts index e6943d37a5..eb9112d08a 100644 --- a/src/renderer/src/hooks/useSmoothStream.ts +++ b/src/renderer/src/hooks/useSmoothStream.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef } from 'react' interface UseSmoothStreamOptions { onUpdate: (text: string) => void @@ -7,15 +7,18 @@ interface UseSmoothStreamOptions { initialText?: string } +const languages = ['en-US', 'es-ES', 'zh-CN', 'zh-TW', 'ja-JP', 'ru-RU', 'el-GR', 'fr-FR', 'pt-PT'] +const segmenter = new Intl.Segmenter(languages) + export const useSmoothStream = ({ onUpdate, streamDone, minDelay = 10, initialText = '' }: UseSmoothStreamOptions) => { - const [chunkQueue, setChunkQueue] = useState([]) + const chunkQueueRef = useRef([]) const animationFrameRef = useRef(null) const displayedTextRef = useRef(initialText) const lastUpdateTimeRef = useRef(0) const addChunk = useCallback((chunk: string) => { - const chars = Array.from(chunk) - setChunkQueue((prev) => [...prev, ...(chars || [])]) + const chars = Array.from(segmenter.segment(chunk)).map((s) => s.segment) + chunkQueueRef.current = [...chunkQueueRef.current, ...(chars || [])] }, []) const reset = useCallback( @@ -23,7 +26,7 @@ export const useSmoothStream = ({ onUpdate, streamDone, minDelay = 10, initialTe if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current) } - setChunkQueue([]) + chunkQueueRef.current = [] displayedTextRef.current = newText onUpdate(newText) }, @@ -32,12 +35,16 @@ export const useSmoothStream = ({ onUpdate, streamDone, minDelay = 10, initialTe const renderLoop = useCallback( (currentTime: number) => { - // 1. 如果队列为空,等待下一帧 - if (chunkQueue.length === 0) { - // 如果流还没结束但队列空了,就等待下一帧 - if (!streamDone) { - animationFrameRef.current = requestAnimationFrame(renderLoop) + // 1. 如果队列为空 + if (chunkQueueRef.current.length === 0) { + // 如果流已结束,确保显示最终状态并停止循环 + if (streamDone) { + const finalText = displayedTextRef.current + onUpdate(finalText) + return } + // 如果流还没结束但队列空了,等待下一帧 + animationFrameRef.current = requestAnimationFrame(renderLoop) return } @@ -48,25 +55,29 @@ export const useSmoothStream = ({ onUpdate, streamDone, minDelay = 10, initialTe } lastUpdateTimeRef.current = currentTime - setChunkQueue((prevQueue) => { - // 3. 动态计算本次渲染的字符数 - // 如果队列积压严重,就一次性渲染更多字符来"追赶" - const charsToRenderCount = Math.max(1, Math.floor(prevQueue.length / 5)) // 每次至少渲染1个,最多渲染队列的1/5 + // 3. 动态计算本次渲染的字符数 + let charsToRenderCount = Math.max(1, Math.floor(chunkQueueRef.current.length / 5)) - const charsToRender = prevQueue.slice(0, charsToRenderCount) - displayedTextRef.current += charsToRender.join('') + // 如果流已结束,一次性渲染所有剩余字符 + if (streamDone) { + charsToRenderCount = chunkQueueRef.current.length + } - // 4. 立即更新UI - onUpdate(displayedTextRef.current) + const charsToRender = chunkQueueRef.current.slice(0, charsToRenderCount) + displayedTextRef.current += charsToRender.join('') - // 返回新的队列 - return prevQueue.slice(charsToRenderCount) - }) + // 4. 立即更新UI + onUpdate(displayedTextRef.current) - // 5. 请求下一帧动画 - animationFrameRef.current = requestAnimationFrame(renderLoop) + // 5. 更新队列 + chunkQueueRef.current = chunkQueueRef.current.slice(charsToRenderCount) + + // 6. 如果还有内容需要渲染,继续下一帧 + if (chunkQueueRef.current.length > 0) { + animationFrameRef.current = requestAnimationFrame(renderLoop) + } }, - [chunkQueue, streamDone, onUpdate, minDelay] + [streamDone, onUpdate, minDelay] ) useEffect(() => { @@ -81,22 +92,5 @@ export const useSmoothStream = ({ onUpdate, streamDone, minDelay = 10, initialTe } }, [renderLoop]) - // 当外部流结束,且队列即将变空时,进行最后一次"瞬移"渲染 - useEffect(() => { - if (streamDone && chunkQueue.length > 0) { - const remainingText = chunkQueue.join('') - const finalText = displayedTextRef.current + remainingText - - // 取消正在进行的动画循环 - if (animationFrameRef.current) { - cancelAnimationFrame(animationFrameRef.current) - } - - // 直接更新到最终状态 - onUpdate(finalText) - setChunkQueue([]) // 清空队列 - } - }, [streamDone, chunkQueue, onUpdate]) - return { addChunk, reset } }