From eb757e441b5f8462bec9aca2b085639577c6866a Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Wed, 21 Jan 2026 12:51:03 +0100 Subject: [PATCH 1/2] Initial commit --- .../AIStateIndicator/hooks/useAIState.ts | 9 +++++++++ src/components/Message/StreamedMessageText.tsx | 15 ++++++++++++--- .../Message/hooks/useMessageTextStreaming.ts | 12 ++++++++++-- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/components/AIStateIndicator/hooks/useAIState.ts b/src/components/AIStateIndicator/hooks/useAIState.ts index 2934c25cf..a3de43a63 100644 --- a/src/components/AIStateIndicator/hooks/useAIState.ts +++ b/src/components/AIStateIndicator/hooks/useAIState.ts @@ -6,6 +6,7 @@ export const AIStates = { ExternalSources: 'AI_STATE_EXTERNAL_SOURCES', Generating: 'AI_STATE_GENERATING', Idle: 'AI_STATE_IDLE', + Stop: 'AI_STATE_STOP', Thinking: 'AI_STATE_THINKING', }; @@ -37,9 +38,17 @@ export const useAIState = (channel?: Channel): { aiState: AIState } => { } }); + const indicatorStoppedListener = channel.on('ai_indicator.stop', (event) => { + const { cid } = event; + if (channel.cid === cid) { + setAiState(AIStates.Stop); + } + }); + return () => { indicatorChangedListener.unsubscribe(); indicatorClearedListener.unsubscribe(); + indicatorStoppedListener.unsubscribe(); }; }, [channel]); diff --git a/src/components/Message/StreamedMessageText.tsx b/src/components/Message/StreamedMessageText.tsx index 3f66e98fb..311087409 100644 --- a/src/components/Message/StreamedMessageText.tsx +++ b/src/components/Message/StreamedMessageText.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import type { MessageTextProps } from './MessageText'; import { MessageText } from './MessageText'; -import { useMessageContext } from '../../context'; +import { useChannelStateContext, useMessageContext } from '../../context'; +import { AIStates, useAIState } from '../AIStateIndicator'; import { useMessageTextStreaming } from './hooks'; export type StreamedMessageTextProps = Pick< @@ -22,14 +23,22 @@ export const StreamedMessageText = (props: StreamedMessageTextProps) => { streamingLetterIntervalMs, } = props; const { message: messageFromContext } = useMessageContext('StreamedMessageText'); + const { channel } = useChannelStateContext(); + const { aiState } = useAIState(channel); const message = messageFromProps || messageFromContext; const { text = '' } = message; - const { streamedMessageText } = useMessageTextStreaming({ + const { stopGenerating, streamedMessageText } = useMessageTextStreaming({ renderingLetterCount, streamingLetterIntervalMs, text, }); + useEffect(() => { + if (aiState === AIStates.Stop) { + stopGenerating(); + } + }, [aiState, stopGenerating]); + return ( { +}: UseMessageTextStreamingProps) => { const [streamedMessageText, setStreamedMessageText] = useState(text); const textCursor = useRef(text.length); useEffect(() => { const textLength = text.length; + const interval = setInterval(() => { if (!text || textCursor.current >= textLength) { clearInterval(interval); + return; } const newCursorValue = textCursor.current + renderingLetterCount; const newText = text.substring(0, newCursorValue); @@ -43,5 +46,10 @@ export const useMessageTextStreaming = ({ }; }, [streamingLetterIntervalMs, renderingLetterCount, text]); - return { streamedMessageText }; + const stopGenerating = useStableCallback(() => { + textCursor.current = text.length; + setStreamedMessageText(text); + }); + + return { stopGenerating, streamedMessageText } as const; }; From d0bfb102456ccf5e0fc9c13c95e011a4e75d1397 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Wed, 21 Jan 2026 15:34:50 +0100 Subject: [PATCH 2/2] Rename & behavior adjustment --- src/components/Message/StreamedMessageText.tsx | 12 +++++------- .../Message/hooks/useMessageTextStreaming.ts | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/Message/StreamedMessageText.tsx b/src/components/Message/StreamedMessageText.tsx index 311087409..218ea2ba6 100644 --- a/src/components/Message/StreamedMessageText.tsx +++ b/src/components/Message/StreamedMessageText.tsx @@ -4,7 +4,6 @@ import type { MessageTextProps } from './MessageText'; import { MessageText } from './MessageText'; import { useChannelStateContext, useMessageContext } from '../../context'; -import { AIStates, useAIState } from '../AIStateIndicator'; import { useMessageTextStreaming } from './hooks'; export type StreamedMessageTextProps = Pick< @@ -24,20 +23,19 @@ export const StreamedMessageText = (props: StreamedMessageTextProps) => { } = props; const { message: messageFromContext } = useMessageContext('StreamedMessageText'); const { channel } = useChannelStateContext(); - const { aiState } = useAIState(channel); const message = messageFromProps || messageFromContext; const { text = '' } = message; - const { stopGenerating, streamedMessageText } = useMessageTextStreaming({ + const { skipAnimation, streamedMessageText } = useMessageTextStreaming({ renderingLetterCount, streamingLetterIntervalMs, text, }); useEffect(() => { - if (aiState === AIStates.Stop) { - stopGenerating(); - } - }, [aiState, stopGenerating]); + channel?.on('ai_indicator.stop', () => { + skipAnimation(); + }); + }, [channel, skipAnimation]); return ( { + const skipAnimation = useStableCallback(() => { textCursor.current = text.length; setStreamedMessageText(text); }); - return { stopGenerating, streamedMessageText } as const; + return { skipAnimation, streamedMessageText } as const; };