11import { Loader } from '@/components/common/Loader'
2- import { Text , Box } from '@radix-ui/themes'
2+ import { Text } from '@radix-ui/themes'
33import clsx from 'clsx'
44import { useFrappeEventListener } from 'frappe-react-sdk'
5- import { useEffect , useState , useRef } from 'react'
6- import { BiChevronDown , BiChevronRight , BiBrain } from 'react-icons/bi'
5+ import { useEffect , useState } from 'react'
76
87type Props = {
98 channelID : string
@@ -40,38 +39,24 @@ const AIEvent = ({ channelID }: Props) => {
4039 const [ isNewThread , setIsNewThread ] = useState ( initialState . isNewThread )
4140 const [ thinkingStartTime , setThinkingStartTime ] = useState ( initialState . thinkingStartTime )
4241
43- // Streaming state
44- const [ streamedText , setStreamedText ] = useState ( "" )
45- const [ isStreaming , setIsStreaming ] = useState ( false )
46- const streamContainerRef = useRef < HTMLDivElement > ( null )
47-
48- // Thinking/reasoning state
49- const [ isThinking , setIsThinking ] = useState ( false )
50- const [ thinkingContent , setThinkingContent ] = useState ( "" )
51- const [ streamingThinking , setStreamingThinking ] = useState ( "" ) // Real-time thinking display
52- const [ showThinkingContent , setShowThinkingContent ] = useState ( false )
53- const thinkingContainerRef = useRef < HTMLDivElement > ( null )
5442
5543 useFrappeEventListener ( "ai_event" , ( data ) => {
5644 if ( data . channel_id === channelID ) {
5745 setAIEvent ( data . text )
5846 setShowAIEvent ( true )
59- setIsNewThread ( false )
60- setThinkingStartTime ( Date . now ( ) )
61- // Reset streaming when a new thinking event comes in
62- setStreamedText ( "" )
63- setIsStreaming ( false )
64- setIsThinking ( false )
65- setThinkingContent ( "" )
47+ setIsNewThread ( false ) // Reset flag when we get a real event
48+ setThinkingStartTime ( Date . now ( ) ) // Reset timer for new thinking messages
6649 }
6750 } )
6851
6952 useFrappeEventListener ( "ai_event_clear" , ( data ) => {
7053 if ( data . channel_id === channelID ) {
7154 const timeSinceThinking = thinkingStartTime ? Date . now ( ) - thinkingStartTime : 0 ;
72- const MIN_DISPLAY_TIME = 500 ;
55+ const MIN_DISPLAY_TIME = 2000 ; // Minimum 2 seconds display time
7356
74- if ( thinkingStartTime && timeSinceThinking < MIN_DISPLAY_TIME && ! isStreaming ) {
57+ // For all messages (including new threads), ensure minimum display time of 2 seconds
58+ if ( thinkingStartTime && timeSinceThinking < MIN_DISPLAY_TIME ) {
59+ // Schedule the clear for later to meet minimum display time
7560 const remainingTime = MIN_DISPLAY_TIME - timeSinceThinking ;
7661 setTimeout ( ( ) => {
7762 setAIEvent ( "" )
@@ -80,160 +65,29 @@ const AIEvent = ({ channelID }: Props) => {
8065 return ;
8166 }
8267
68+ // If enough time has passed, clear immediately
8369 setAIEvent ( "" )
8470 setIsNewThread ( false )
8571 }
8672 } )
8773
88- // Listen for thinking start
89- useFrappeEventListener ( "ai_thinking_start" , ( data ) => {
90- if ( data . channel_id === channelID ) {
91- setIsThinking ( true )
92- setThinkingContent ( "" )
93- setStreamingThinking ( "" )
94- setShowAIEvent ( true )
95- setAIEvent ( "" ) // Clear "thinking" text, we'll show reasoning indicator
96- }
97- } )
98-
99- // Listen for thinking tokens (real-time reasoning display)
100- useFrappeEventListener ( "ai_thinking_token" , ( data ) => {
101- if ( data . channel_id === channelID ) {
102- setStreamingThinking ( prev => prev + data . token )
103- setShowAIEvent ( true )
104- }
105- } )
106-
107- // Listen for thinking end (includes captured thinking content)
108- useFrappeEventListener ( "ai_thinking_end" , ( data ) => {
109- if ( data . channel_id === channelID ) {
110- setIsThinking ( false )
111- if ( data . thinking_content ) {
112- setThinkingContent ( data . thinking_content )
113- }
114- setStreamingThinking ( "" ) // Clear streaming thinking when done
115- }
116- } )
11774
118- // Listen for streaming tokens
119- useFrappeEventListener ( "ai_token" , ( data ) => {
120- if ( data . channel_id === channelID ) {
121- setIsStreaming ( true )
122- setShowAIEvent ( true )
123- setAIEvent ( "" )
124- setStreamedText ( prev => prev + data . token )
125- }
126- } )
127-
128- // Listen for stream end
129- useFrappeEventListener ( "ai_stream_done" , ( data ) => {
130- if ( data . channel_id === channelID ) {
131- setTimeout ( ( ) => {
132- setIsStreaming ( false )
133- setStreamedText ( "" )
134- setShowAIEvent ( false )
135- setIsThinking ( false )
136- setThinkingContent ( "" )
137- setShowThinkingContent ( false )
138- } , 300 )
139- }
140- } )
141-
142- // Auto-scroll streaming container
14375 useEffect ( ( ) => {
144- if ( streamContainerRef . current && isStreaming ) {
145- streamContainerRef . current . scrollTop = streamContainerRef . current . scrollHeight
146- }
147- } , [ streamedText , isStreaming ] )
148-
149- // Auto-scroll thinking container
150- useEffect ( ( ) => {
151- if ( thinkingContainerRef . current && isThinking ) {
152- thinkingContainerRef . current . scrollTop = thinkingContainerRef . current . scrollHeight
153- }
154- } , [ streamingThinking , isThinking ] )
155-
156- useEffect ( ( ) => {
157- if ( ! aiEvent && ! isStreaming && ! isThinking && showAIEvent ) {
76+ if ( ! aiEvent && showAIEvent ) {
15877 setTimeout ( ( ) => {
15978 setShowAIEvent ( false )
16079 } , 300 )
16180 }
162- } , [ aiEvent , isStreaming , isThinking ] )
163-
164- // Show either thinking indicator, reasoning indicator, or streaming text
165- const hasContent = aiEvent || isThinking || streamingThinking || ( isStreaming && streamedText )
81+ } , [ aiEvent ] )
16682
16783 return (
16884 < div className = { clsx (
16985 'w-full transition-all duration-300 ease-ease-out-quart' ,
170- showAIEvent && hasContent ? 'translate-y-0 opacity-100 z-50 sm:pb-0 pb-16' : 'translate-y-full opacity-0 h-0'
86+ showAIEvent ? 'translate-y-0 opacity-100 z-50 sm:pb-0 pb-16' : 'translate-y-full opacity-0 h-0'
17187 ) } >
172- < div className = "py-2 px-2 bg-white dark:bg-gray-2" >
173- { /* Initial thinking indicator (before streaming starts) */ }
174- { aiEvent && ! isStreaming && ! isThinking && (
175- < div className = "flex items-center gap-2" >
176- < Loader />
177- < Text size = '2' > { aiEvent } </ Text >
178- </ div >
179- ) }
180-
181- { /* Reasoning/thinking with real-time streaming */ }
182- { ( isThinking || streamingThinking ) && (
183- < div className = "space-y-1" >
184- < div className = "flex items-center gap-2 text-purple-600 dark:text-purple-400" >
185- < BiBrain className = "w-4 h-4 animate-pulse" />
186- < Text size = '2' weight = "medium" > Thinking...</ Text >
187- </ div >
188- { streamingThinking && (
189- < div
190- ref = { thinkingContainerRef }
191- className = "max-h-32 overflow-y-auto p-2 bg-purple-50 dark:bg-purple-900/20 rounded"
192- >
193- < Text size = '1' className = "whitespace-pre-wrap font-mono text-gray-600 dark:text-gray-300" >
194- { streamingThinking }
195- < span className = "inline-block w-1.5 h-3 ml-0.5 bg-purple-500 animate-pulse" />
196- </ Text >
197- </ div >
198- ) }
199- </ div >
200- ) }
201-
202- { /* Captured thinking content (collapsible) */ }
203- { ! isThinking && thinkingContent && (
204- < div className = "mb-2" >
205- < button
206- onClick = { ( ) => setShowThinkingContent ( ! showThinkingContent ) }
207- className = "flex items-center gap-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
208- >
209- { showThinkingContent ? (
210- < BiChevronDown className = "w-4 h-4" />
211- ) : (
212- < BiChevronRight className = "w-4 h-4" />
213- ) }
214- < BiBrain className = "w-3 h-3" />
215- < span > Reasoning</ span >
216- </ button >
217- { showThinkingContent && (
218- < Box className = "mt-1 p-2 bg-purple-50 dark:bg-purple-900/20 rounded text-xs text-gray-600 dark:text-gray-300 max-h-32 overflow-y-auto" >
219- < pre className = "whitespace-pre-wrap font-mono" > { thinkingContent } </ pre >
220- </ Box >
221- ) }
222- </ div >
223- ) }
224-
225- { /* Streaming text display */ }
226- { isStreaming && streamedText && (
227- < div
228- ref = { streamContainerRef }
229- className = "max-h-48 overflow-y-auto"
230- >
231- < Text size = '2' className = "whitespace-pre-wrap leading-relaxed" >
232- { streamedText }
233- < span className = "inline-block w-2 h-4 ml-0.5 bg-gray-800 dark:bg-gray-200 animate-pulse" />
234- </ Text >
235- </ div >
236- ) }
88+ < div className = "flex items-center gap-2 py-2 px-2 bg-white dark:bg-gray-2" >
89+ < Loader />
90+ < Text size = '2' > { aiEvent } </ Text >
23791 </ div >
23892 </ div >
23993 )
0 commit comments