11'use client'
22
3- import { type KeyboardEvent , useEffect , useMemo , useRef } from 'react'
3+ import { type KeyboardEvent , useCallback , useEffect , useMemo , useRef , useState } from 'react'
44import { ArrowUp } from 'lucide-react'
55import { Button } from '@/components/ui/button'
66import { Input } from '@/components/ui/input'
@@ -41,6 +41,13 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
4141 } = useChatStore ( )
4242 const { entries } = useConsoleStore ( )
4343 const messagesEndRef = useRef < HTMLDivElement > ( null )
44+ const inputRef = useRef < HTMLInputElement > ( null )
45+ const timeoutRef = useRef < NodeJS . Timeout | null > ( null )
46+ const abortControllerRef = useRef < AbortController | null > ( null )
47+
48+ // Prompt history state
49+ const [ promptHistory , setPromptHistory ] = useState < string [ ] > ( [ ] )
50+ const [ historyIndex , setHistoryIndex ] = useState ( - 1 )
4451
4552 // Use the execution store state to track if a workflow is executing
4653 const { isExecuting } = useExecutionStore ( )
@@ -62,6 +69,26 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
6269 . sort ( ( a , b ) => new Date ( a . timestamp ) . getTime ( ) - new Date ( b . timestamp ) . getTime ( ) )
6370 } , [ messages , activeWorkflowId ] )
6471
72+ // Memoize user messages for performance
73+ const userMessages = useMemo ( ( ) => {
74+ return workflowMessages
75+ . filter ( ( msg ) => msg . type === 'user' )
76+ . map ( ( msg ) => msg . content )
77+ . filter ( ( content ) : content is string => typeof content === 'string' )
78+ } , [ workflowMessages ] )
79+
80+ // Update prompt history when workflow changes
81+ useEffect ( ( ) => {
82+ if ( ! activeWorkflowId ) {
83+ setPromptHistory ( [ ] )
84+ setHistoryIndex ( - 1 )
85+ return
86+ }
87+
88+ setPromptHistory ( userMessages )
89+ setHistoryIndex ( - 1 )
90+ } , [ activeWorkflowId , userMessages ] )
91+
6592 // Get selected workflow outputs
6693 const selectedOutputs = useMemo ( ( ) => {
6794 if ( ! activeWorkflowId ) return [ ]
@@ -84,6 +111,31 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
84111 return selected
85112 } , [ selectedWorkflowOutputs , activeWorkflowId , setSelectedWorkflowOutput ] )
86113
114+ // Focus input helper with proper cleanup
115+ const focusInput = useCallback ( ( delay = 0 ) => {
116+ if ( timeoutRef . current ) {
117+ clearTimeout ( timeoutRef . current )
118+ }
119+
120+ timeoutRef . current = setTimeout ( ( ) => {
121+ if ( inputRef . current && document . contains ( inputRef . current ) ) {
122+ inputRef . current . focus ( { preventScroll : true } )
123+ }
124+ } , delay )
125+ } , [ ] )
126+
127+ // Cleanup on unmount
128+ useEffect ( ( ) => {
129+ return ( ) => {
130+ if ( timeoutRef . current ) {
131+ clearTimeout ( timeoutRef . current )
132+ }
133+ if ( abortControllerRef . current ) {
134+ abortControllerRef . current . abort ( )
135+ }
136+ }
137+ } , [ ] )
138+
87139 // Auto-scroll to bottom when new messages are added
88140 useEffect ( ( ) => {
89141 if ( messagesEndRef . current ) {
@@ -92,12 +144,26 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
92144 } , [ workflowMessages ] )
93145
94146 // Handle send message
95- const handleSendMessage = async ( ) => {
147+ const handleSendMessage = useCallback ( async ( ) => {
96148 if ( ! chatMessage . trim ( ) || ! activeWorkflowId || isExecuting ) return
97149
98150 // Store the message being sent for reference
99151 const sentMessage = chatMessage . trim ( )
100152
153+ // Add to prompt history if it's not already the most recent
154+ if ( promptHistory . length === 0 || promptHistory [ promptHistory . length - 1 ] !== sentMessage ) {
155+ setPromptHistory ( ( prev ) => [ ...prev , sentMessage ] )
156+ }
157+
158+ // Reset history index
159+ setHistoryIndex ( - 1 )
160+
161+ // Cancel any existing operations
162+ if ( abortControllerRef . current ) {
163+ abortControllerRef . current . abort ( )
164+ }
165+ abortControllerRef . current = new AbortController ( )
166+
101167 // Get the conversationId for this workflow before adding the message
102168 const conversationId = getConversationId ( activeWorkflowId )
103169
@@ -108,8 +174,9 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
108174 type : 'user' ,
109175 } )
110176
111- // Clear input
177+ // Clear input and refocus immediately
112178 setChatMessage ( '' )
179+ focusInput ( 10 )
113180
114181 // Execute the workflow to generate a response, passing the chat message and conversationId as input
115182 const result = await handleRunWorkflow ( {
@@ -223,7 +290,12 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
223290 }
224291 }
225292
226- processStream ( ) . catch ( ( e ) => logger . error ( 'Error processing stream:' , e ) )
293+ processStream ( )
294+ . catch ( ( e ) => logger . error ( 'Error processing stream:' , e ) )
295+ . finally ( ( ) => {
296+ // Restore focus after streaming completes
297+ focusInput ( 100 )
298+ } )
227299 } else if ( result && 'success' in result && result . success && 'logs' in result ) {
228300 const finalOutputs : any [ ] = [ ]
229301
@@ -287,30 +359,72 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
287359 type : 'workflow' ,
288360 } )
289361 }
290- }
362+
363+ // Restore focus after workflow execution completes
364+ focusInput ( 100 )
365+ } , [
366+ chatMessage ,
367+ activeWorkflowId ,
368+ isExecuting ,
369+ promptHistory ,
370+ getConversationId ,
371+ addMessage ,
372+ handleRunWorkflow ,
373+ selectedOutputs ,
374+ setSelectedWorkflowOutput ,
375+ appendMessageContent ,
376+ finalizeMessageStream ,
377+ focusInput ,
378+ ] )
291379
292380 // Handle key press
293- const handleKeyPress = ( e : KeyboardEvent < HTMLInputElement > ) => {
294- if ( e . key === 'Enter' && ! e . shiftKey ) {
295- e . preventDefault ( )
296- handleSendMessage ( )
297- }
298- }
381+ const handleKeyPress = useCallback (
382+ ( e : KeyboardEvent < HTMLInputElement > ) => {
383+ if ( e . key === 'Enter' && ! e . shiftKey ) {
384+ e . preventDefault ( )
385+ handleSendMessage ( )
386+ } else if ( e . key === 'ArrowUp' ) {
387+ e . preventDefault ( )
388+ if ( promptHistory . length > 0 ) {
389+ const newIndex =
390+ historyIndex === - 1 ? promptHistory . length - 1 : Math . max ( 0 , historyIndex - 1 )
391+ setHistoryIndex ( newIndex )
392+ setChatMessage ( promptHistory [ newIndex ] )
393+ }
394+ } else if ( e . key === 'ArrowDown' ) {
395+ e . preventDefault ( )
396+ if ( historyIndex >= 0 ) {
397+ const newIndex = historyIndex + 1
398+ if ( newIndex >= promptHistory . length ) {
399+ setHistoryIndex ( - 1 )
400+ setChatMessage ( '' )
401+ } else {
402+ setHistoryIndex ( newIndex )
403+ setChatMessage ( promptHistory [ newIndex ] )
404+ }
405+ }
406+ }
407+ } ,
408+ [ handleSendMessage , promptHistory , historyIndex , setChatMessage ]
409+ )
299410
300411 // Handle output selection
301- const handleOutputSelection = ( values : string [ ] ) => {
302- // Ensure no duplicates in selection
303- const dedupedValues = [ ...new Set ( values ) ]
304-
305- if ( activeWorkflowId ) {
306- // If array is empty, explicitly set to empty array to ensure complete reset
307- if ( dedupedValues . length === 0 ) {
308- setSelectedWorkflowOutput ( activeWorkflowId , [ ] )
309- } else {
310- setSelectedWorkflowOutput ( activeWorkflowId , dedupedValues )
412+ const handleOutputSelection = useCallback (
413+ ( values : string [ ] ) => {
414+ // Ensure no duplicates in selection
415+ const dedupedValues = [ ...new Set ( values ) ]
416+
417+ if ( activeWorkflowId ) {
418+ // If array is empty, explicitly set to empty array to ensure complete reset
419+ if ( dedupedValues . length === 0 ) {
420+ setSelectedWorkflowOutput ( activeWorkflowId , [ ] )
421+ } else {
422+ setSelectedWorkflowOutput ( activeWorkflowId , dedupedValues )
423+ }
311424 }
312- }
313- }
425+ } ,
426+ [ activeWorkflowId , setSelectedWorkflowOutput ]
427+ )
314428
315429 return (
316430 < div className = 'flex h-full flex-col' >
@@ -349,8 +463,12 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
349463 < div className = '-mt-[1px] relative flex-nonept-3 pb-4' >
350464 < div className = 'flex gap-2' >
351465 < Input
466+ ref = { inputRef }
352467 value = { chatMessage }
353- onChange = { ( e ) => setChatMessage ( e . target . value ) }
468+ onChange = { ( e ) => {
469+ setChatMessage ( e . target . value )
470+ setHistoryIndex ( - 1 ) // Reset history index when typing
471+ } }
354472 onKeyDown = { handleKeyPress }
355473 placeholder = 'Type a message...'
356474 className = 'h-9 flex-1 rounded-lg border-[#E5E5E5] bg-[#FFFFFF] text-muted-foreground shadow-xs focus-visible:ring-0 focus-visible:ring-offset-0 dark:border-[#414141] dark:bg-[#202020]'
0 commit comments