Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { type KeyboardEvent, useEffect, useMemo, useRef } from 'react'
import { type KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { ArrowUp } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
Expand Down Expand Up @@ -41,6 +41,13 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
} = useChatStore()
const { entries } = useConsoleStore()
const messagesEndRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const abortControllerRef = useRef<AbortController | null>(null)

// Prompt history state
const [promptHistory, setPromptHistory] = useState<string[]>([])
const [historyIndex, setHistoryIndex] = useState(-1)

// Use the execution store state to track if a workflow is executing
const { isExecuting } = useExecutionStore()
Expand All @@ -62,6 +69,26 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
}, [messages, activeWorkflowId])

// Memoize user messages for performance
const userMessages = useMemo(() => {
return workflowMessages
.filter((msg) => msg.type === 'user')
.map((msg) => msg.content)
.filter((content): content is string => typeof content === 'string')
}, [workflowMessages])

// Update prompt history when workflow changes
useEffect(() => {
if (!activeWorkflowId) {
setPromptHistory([])
setHistoryIndex(-1)
return
}

setPromptHistory(userMessages)
setHistoryIndex(-1)
}, [activeWorkflowId, userMessages])

// Get selected workflow outputs
const selectedOutputs = useMemo(() => {
if (!activeWorkflowId) return []
Expand All @@ -84,6 +111,31 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
return selected
}, [selectedWorkflowOutputs, activeWorkflowId, setSelectedWorkflowOutput])

// Focus input helper with proper cleanup
const focusInput = useCallback((delay = 0) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}

timeoutRef.current = setTimeout(() => {
if (inputRef.current && document.contains(inputRef.current)) {
inputRef.current.focus({ preventScroll: true })
}
}, delay)
}, [])

// Cleanup on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
}
}, [])

// Auto-scroll to bottom when new messages are added
useEffect(() => {
if (messagesEndRef.current) {
Expand All @@ -92,12 +144,26 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
}, [workflowMessages])

// Handle send message
const handleSendMessage = async () => {
const handleSendMessage = useCallback(async () => {
if (!chatMessage.trim() || !activeWorkflowId || isExecuting) return

// Store the message being sent for reference
const sentMessage = chatMessage.trim()

// Add to prompt history if it's not already the most recent
if (promptHistory.length === 0 || promptHistory[promptHistory.length - 1] !== sentMessage) {
setPromptHistory((prev) => [...prev, sentMessage])
}

// Reset history index
setHistoryIndex(-1)

// Cancel any existing operations
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
abortControllerRef.current = new AbortController()

// Get the conversationId for this workflow before adding the message
const conversationId = getConversationId(activeWorkflowId)

Expand All @@ -108,8 +174,9 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
type: 'user',
})

// Clear input
// Clear input and refocus immediately
setChatMessage('')
focusInput(10)

// Execute the workflow to generate a response, passing the chat message and conversationId as input
const result = await handleRunWorkflow({
Expand Down Expand Up @@ -223,7 +290,12 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
}
}

processStream().catch((e) => logger.error('Error processing stream:', e))
processStream()
.catch((e) => logger.error('Error processing stream:', e))
.finally(() => {
// Restore focus after streaming completes
focusInput(100)
})
} else if (result && 'success' in result && result.success && 'logs' in result) {
const finalOutputs: any[] = []

Expand Down Expand Up @@ -287,30 +359,72 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
type: 'workflow',
})
}
}

// Restore focus after workflow execution completes
focusInput(100)
}, [
chatMessage,
activeWorkflowId,
isExecuting,
promptHistory,
getConversationId,
addMessage,
handleRunWorkflow,
selectedOutputs,
setSelectedWorkflowOutput,
appendMessageContent,
finalizeMessageStream,
focusInput,
])

// Handle key press
const handleKeyPress = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSendMessage()
}
}
const handleKeyPress = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSendMessage()
} else if (e.key === 'ArrowUp') {
e.preventDefault()
if (promptHistory.length > 0) {
const newIndex =
historyIndex === -1 ? promptHistory.length - 1 : Math.max(0, historyIndex - 1)
setHistoryIndex(newIndex)
setChatMessage(promptHistory[newIndex])
}
} else if (e.key === 'ArrowDown') {
e.preventDefault()
if (historyIndex >= 0) {
const newIndex = historyIndex + 1
if (newIndex >= promptHistory.length) {
setHistoryIndex(-1)
setChatMessage('')
} else {
setHistoryIndex(newIndex)
setChatMessage(promptHistory[newIndex])
}
}
}
},
[handleSendMessage, promptHistory, historyIndex, setChatMessage]
)

// Handle output selection
const handleOutputSelection = (values: string[]) => {
// Ensure no duplicates in selection
const dedupedValues = [...new Set(values)]

if (activeWorkflowId) {
// If array is empty, explicitly set to empty array to ensure complete reset
if (dedupedValues.length === 0) {
setSelectedWorkflowOutput(activeWorkflowId, [])
} else {
setSelectedWorkflowOutput(activeWorkflowId, dedupedValues)
const handleOutputSelection = useCallback(
(values: string[]) => {
// Ensure no duplicates in selection
const dedupedValues = [...new Set(values)]

if (activeWorkflowId) {
// If array is empty, explicitly set to empty array to ensure complete reset
if (dedupedValues.length === 0) {
setSelectedWorkflowOutput(activeWorkflowId, [])
} else {
setSelectedWorkflowOutput(activeWorkflowId, dedupedValues)
}
}
}
}
},
[activeWorkflowId, setSelectedWorkflowOutput]
)

return (
<div className='flex h-full flex-col'>
Expand Down Expand Up @@ -349,8 +463,12 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
<div className='-mt-[1px] relative flex-nonept-3 pb-4'>
<div className='flex gap-2'>
<Input
ref={inputRef}
value={chatMessage}
onChange={(e) => setChatMessage(e.target.value)}
onChange={(e) => {
setChatMessage(e.target.value)
setHistoryIndex(-1) // Reset history index when typing
}}
onKeyDown={handleKeyPress}
placeholder='Type a message...'
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]'
Expand Down