@@ -8,7 +8,6 @@ import { isFunctionMessage } from '@/ProChat/utils/message';
88import { setNamespace } from '@/ProChat/utils/storeDebug' ;
99import { nanoid } from '@/ProChat/utils/uuid' ;
1010import { ChatMessage } from '@/types/message' ;
11- import { startTransition } from 'react' ;
1211
1312import { initialModelConfig } from '@/ProChat/store/initialState' ;
1413import { ChatStreamPayload } from '@/ProChat/types/chat' ;
@@ -110,6 +109,16 @@ export interface ChatAction {
110109 */
111110 updateMessageContent : ( id : string , content : string ) => Promise < void > ;
112111
112+ /**
113+ * 创建一条平滑输出的内容
114+ */
115+ createSmoothMessage : ( id : string ) => {
116+ startAnimation : ( speed ?: number ) => Promise < void > ;
117+ stopAnimation : ( ) => void ;
118+ outputQueue : string [ ] ;
119+ isAnimationActive : boolean ;
120+ } ;
121+
113122 /**
114123 * 获取当前 loading 生成的消息 id
115124 * @returns 消息 id | undefined
@@ -152,8 +161,14 @@ export const chatAction: StateCreator<ChatStore, [['zustand/devtools', never]],
152161 onChatsChange ?.( nextChats ) ;
153162 } ,
154163 generateMessage : async ( messages , assistantId ) => {
155- const { dispatchMessage, toggleChatLoading, config, defaultModelFetcher } = get ( ) ;
156-
164+ const {
165+ dispatchMessage,
166+ toggleChatLoading,
167+ config,
168+ defaultModelFetcher,
169+ createSmoothMessage,
170+ updateMessageContent,
171+ } = get ( ) ;
157172 const abortController = toggleChatLoading (
158173 true ,
159174 assistantId ,
@@ -203,23 +218,33 @@ export const chatAction: StateCreator<ChatStore, [['zustand/devtools', never]],
203218 let output = '' ;
204219 let isFunctionCall = false ;
205220
221+ const { startAnimation, stopAnimation, outputQueue, isAnimationActive } =
222+ createSmoothMessage ( assistantId ) ;
223+
206224 await fetchSSE ( fetcher , {
207225 onErrorHandle : ( error ) => {
208226 dispatchMessage ( { id : assistantId , key : 'error' , type : 'updateMessage' , value : error } ) ;
209227 } ,
228+ onAbort : async ( ) => {
229+ stopAnimation ( ) ;
230+ } ,
231+ onFinish : async ( content ) => {
232+ stopAnimation ( ) ;
233+ if ( outputQueue . length > 0 && ! isFunctionCall ) {
234+ await startAnimation ( 15 ) ;
235+ }
236+ await updateMessageContent ( assistantId , content ) ;
237+ } ,
210238 onMessageHandle : ( text ) => {
211239 output += text ;
212- if ( ! abortController . signal . aborted ) {
213- startTransition ( ( ) => {
214- dispatchMessage ( {
215- id : assistantId ,
216- key : 'content' ,
217- type : 'updateMessage' ,
218- value : output ,
219- } ) ;
220- } ) ;
221- } else {
240+
241+ if ( ! isAnimationActive && ! isFunctionCall ) startAnimation ( ) ;
242+
243+ if ( abortController ?. signal . aborted ) {
244+ // aborted 后停止当前输出
222245 return ;
246+ } else {
247+ outputQueue . push ( text ) ;
223248 }
224249
225250 // TODO: need a function call judge callback
@@ -230,9 +255,20 @@ export const chatAction: StateCreator<ChatStore, [['zustand/devtools', never]],
230255 } ,
231256 } ) ;
232257
233- startTransition ( ( ) => {
234- toggleChatLoading ( false , undefined , t ( 'generateMessage(end)' ) as string ) ;
235- } ) ;
258+ let timeoutId ; // 用于存储轮询队列的计时器id
259+ const checkAndToggleChatLoading = ( ) => {
260+ clearTimeout ( timeoutId ) ; // 清除任何现有的计时器
261+ // 等待队列内容输出完毕
262+ if ( outputQueue === undefined || outputQueue . length === 0 ) {
263+ // 当队列为空时
264+ toggleChatLoading ( false , undefined , t ( 'generateMessage(end)' ) as string ) ;
265+ } else {
266+ // 如果队列不为空,则设置一个延迟或者使用某种形式的轮询来再次检查队列
267+ timeoutId = setTimeout ( checkAndToggleChatLoading , 30 ) ; // CHECK_INTERVAL 是毫秒数,代表检查间隔时间
268+ }
269+ } ;
270+
271+ checkAndToggleChatLoading ( ) ;
236272
237273 return { isFunctionCall } ;
238274 } ,
@@ -380,6 +416,71 @@ export const chatAction: StateCreator<ChatStore, [['zustand/devtools', never]],
380416 return nanoid ( ) ;
381417 } ,
382418
419+ createSmoothMessage : ( id ) => {
420+ const { dispatchMessage } = get ( ) ;
421+
422+ let buffer = '' ;
423+ // why use queue: https://shareg.pt/GLBrjpK
424+ let outputQueue : string [ ] = [ ] ;
425+
426+ // eslint-disable-next-line no-undef
427+ let animationTimeoutId : NodeJS . Timeout | null = null ;
428+ let isAnimationActive = false ;
429+
430+ // when you need to stop the animation, call this function
431+ const stopAnimation = ( ) => {
432+ isAnimationActive = false ;
433+ if ( animationTimeoutId !== null ) {
434+ clearTimeout ( animationTimeoutId ) ;
435+ animationTimeoutId = null ;
436+ }
437+ } ;
438+
439+ // define startAnimation function to display the text in buffer smooth
440+ // when you need to start the animation, call this function
441+ const startAnimation = ( speed = 2 ) =>
442+ new Promise < void > ( ( resolve ) => {
443+ if ( isAnimationActive ) {
444+ resolve ( ) ;
445+ return ;
446+ }
447+
448+ isAnimationActive = true ;
449+
450+ const updateText = ( ) => {
451+ // 如果动画已经不再激活,则停止更新文本
452+ if ( ! isAnimationActive ) {
453+ clearTimeout ( animationTimeoutId ! ) ;
454+ animationTimeoutId = null ;
455+ resolve ( ) ;
456+ }
457+
458+ // 如果还有文本没有显示
459+ // 检查队列中是否有字符待显示
460+ if ( outputQueue . length > 0 ) {
461+ // 从队列中获取前两个字符(如果存在)
462+ const charsToAdd = outputQueue . splice ( 0 , speed ) . join ( '' ) ;
463+ buffer += charsToAdd ;
464+
465+ // 更新消息内容,这里可能需要结合实际情况调整
466+ dispatchMessage ( { id, key : 'content' , type : 'updateMessage' , value : buffer } ) ;
467+
468+ // 设置下一个字符的延迟
469+ animationTimeoutId = setTimeout ( updateText , 16 ) ; // 16 毫秒的延迟模拟打字机效果
470+ } else {
471+ // 当所有字符都显示完毕时,清除动画状态
472+ isAnimationActive = false ;
473+ animationTimeoutId = null ;
474+ resolve ( ) ;
475+ }
476+ } ;
477+
478+ updateText ( ) ;
479+ } ) ;
480+
481+ return { startAnimation, stopAnimation, outputQueue, isAnimationActive } ;
482+ } ,
483+
383484 getChatLoadingId : ( ) => {
384485 const { chatLoadingId } = get ( ) ;
385486 return chatLoadingId ;
0 commit comments