Skip to content

Commit 92a998d

Browse files
authored
improvement(chat-panel): added the ability to reuse queries, focus cursor for continuous conversation (#756)
* improvement(chat-panel): added the ability to reuse queries, focus cursor for continuous conversation * fix build
1 parent a7c8f5d commit 92a998d

File tree

1 file changed

+142
-24
lines changed
  • apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat

1 file changed

+142
-24
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/chat.tsx

Lines changed: 142 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { type KeyboardEvent, useEffect, useMemo, useRef } from 'react'
3+
import { type KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { ArrowUp } from 'lucide-react'
55
import { Button } from '@/components/ui/button'
66
import { 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

Comments
 (0)