diff --git a/ui/desktop/src/components/BaseChat.tsx b/ui/desktop/src/components/BaseChat.tsx index ccb48b0f611b..6d5a164423c4 100644 --- a/ui/desktop/src/components/BaseChat.tsx +++ b/ui/desktop/src/components/BaseChat.tsx @@ -93,6 +93,7 @@ interface BaseChatProps { disableSearch?: boolean; // Disable search functionality (for Hub) showPopularTopics?: boolean; // Show popular chat topics in empty state (for Pair) suppressEmptyState?: boolean; // Suppress empty state content (for transitions) + autoSubmit?: boolean; } function BaseChatContent({ @@ -112,6 +113,7 @@ function BaseChatContent({ disableSearch = false, showPopularTopics = false, suppressEmptyState = false, + autoSubmit = false, }: BaseChatProps) { const location = useLocation(); const scrollRef = useRef(null); @@ -538,6 +540,7 @@ function BaseChatContent({ recipeConfig={recipeConfig} recipeAccepted={recipeAccepted} initialPrompt={initialPrompt} + autoSubmit={autoSubmit} {...customChatInputProps} /> diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index cea578b0386f..26ec356b3182 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useEffect, useMemo } from 'react'; +import React, { useRef, useState, useEffect, useMemo, useCallback } from 'react'; import { FolderKey, ScrollText } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/Tooltip'; import { Button } from './ui/button'; @@ -84,6 +84,7 @@ interface ChatInputProps { recipeConfig?: Recipe | null; recipeAccepted?: boolean; initialPrompt?: string; + autoSubmit: boolean; } export default function ChatInput({ @@ -106,6 +107,7 @@ export default function ChatInput({ recipeConfig, recipeAccepted, initialPrompt, + autoSubmit = false, }: ChatInputProps) { const [_value, setValue] = useState(initialValue); const [displayValue, setDisplayValue] = useState(initialValue); // For immediate visual feedback @@ -357,6 +359,7 @@ export default function ChatInput({ const [hasUserTyped, setHasUserTyped] = useState(false); const textAreaRef = useRef(null); const timeoutRefsRef = useRef>>(new Set()); + const [didAutoSubmit, setDidAutoSubmit] = useState(false); // Use shared file drop hook for ChatInput const { @@ -367,7 +370,10 @@ export default function ChatInput({ } = useFileDrop(); // Merge local dropped files with parent dropped files - const allDroppedFiles = [...droppedFiles, ...localDroppedFiles]; + const allDroppedFiles = useMemo( + () => [...droppedFiles, ...localDroppedFiles], + [droppedFiles, localDroppedFiles] + ); const handleRemoveDroppedFile = (idToRemove: string) => { // Remove from local dropped files @@ -936,69 +942,89 @@ export default function ChatInput({ return true; // Return true if message was queued }; - const performSubmit = () => { - const validPastedImageFilesPaths = pastedImages - .filter((img) => img.filePath && !img.error && !img.isLoading) - .map((img) => img.filePath as string); - - // Get paths from all dropped files (both parent and local) - const droppedFilePaths = allDroppedFiles - .filter((file) => !file.error && !file.isLoading) - .map((file) => file.path); - - let textToSend = displayValue.trim(); + const performSubmit = useCallback( + (text?: string) => { + const validPastedImageFilesPaths = pastedImages + .filter((img) => img.filePath && !img.error && !img.isLoading) + .map((img) => img.filePath as string); + // Get paths from all dropped files (both parent and local) + const droppedFilePaths = allDroppedFiles + .filter((file) => !file.error && !file.isLoading) + .map((file) => file.path); + + let textToSend = text ?? displayValue.trim(); + + // Combine pasted images and dropped files + const allFilePaths = [...validPastedImageFilesPaths, ...droppedFilePaths]; + if (allFilePaths.length > 0) { + const pathsString = allFilePaths.join(' '); + textToSend = textToSend ? `${textToSend} ${pathsString}` : pathsString; + } - // Combine pasted images and dropped files - const allFilePaths = [...validPastedImageFilesPaths, ...droppedFilePaths]; - if (allFilePaths.length > 0) { - const pathsString = allFilePaths.join(' '); - textToSend = textToSend ? `${textToSend} ${pathsString}` : pathsString; - } + if (textToSend) { + if (displayValue.trim()) { + LocalMessageStorage.addMessage(displayValue); + } else if (allFilePaths.length > 0) { + LocalMessageStorage.addMessage(allFilePaths.join(' ')); + } - if (textToSend) { - if (displayValue.trim()) { - LocalMessageStorage.addMessage(displayValue); - } else if (allFilePaths.length > 0) { - LocalMessageStorage.addMessage(allFilePaths.join(' ')); - } + handleSubmit( + new CustomEvent('submit', { detail: { value: textToSend } }) as unknown as React.FormEvent + ); - handleSubmit( - new CustomEvent('submit', { detail: { value: textToSend } }) as unknown as React.FormEvent - ); + // Auto-resume queue after sending a NON-interruption message (if it was paused due to interruption) + if ( + queuePausedRef.current && + lastInterruption && + textToSend && + !detectInterruption(textToSend) + ) { + queuePausedRef.current = false; + setLastInterruption(null); + } - // Auto-resume queue after sending a NON-interruption message (if it was paused due to interruption) - if ( - queuePausedRef.current && - lastInterruption && - textToSend && - !detectInterruption(textToSend) - ) { - queuePausedRef.current = false; - setLastInterruption(null); - } + setDisplayValue(''); + setValue(''); + setPastedImages([]); + setHistoryIndex(-1); + setSavedInput(''); + setIsInGlobalHistory(false); + setHasUserTyped(false); - setDisplayValue(''); - setValue(''); - setPastedImages([]); - setHistoryIndex(-1); - setSavedInput(''); - setIsInGlobalHistory(false); - setHasUserTyped(false); + // Clear draft when message is sent + if (chatContext && chatContext.clearDraft) { + chatContext.clearDraft(); + } - // Clear draft when message is sent - if (chatContext && chatContext.clearDraft) { - chatContext.clearDraft(); + // Clear both parent and local dropped files after processing + if (onFilesProcessed && droppedFiles.length > 0) { + onFilesProcessed(); + } + if (localDroppedFiles.length > 0) { + setLocalDroppedFiles([]); + } } + }, + [ + allDroppedFiles, + chatContext, + displayValue, + droppedFiles.length, + handleSubmit, + lastInterruption, + localDroppedFiles.length, + onFilesProcessed, + pastedImages, + setLocalDroppedFiles, + ] + ); - // Clear both parent and local dropped files after processing - if (onFilesProcessed && droppedFiles.length > 0) { - onFilesProcessed(); - } - if (localDroppedFiles.length > 0) { - setLocalDroppedFiles([]); - } + useEffect(() => { + if (!!autoSubmit && !didAutoSubmit) { + setDidAutoSubmit(true); + performSubmit(initialValue); } - }; + }, [autoSubmit, didAutoSubmit, initialValue, performSubmit]); const handleKeyDown = (evt: React.KeyboardEvent) => { // If mention popover is open, handle arrow keys and enter diff --git a/ui/desktop/src/components/hub.tsx b/ui/desktop/src/components/hub.tsx index 5548963afb66..f250e4d26647 100644 --- a/ui/desktop/src/components/hub.tsx +++ b/ui/desktop/src/components/hub.tsx @@ -85,6 +85,7 @@ export default function Hub({ {}} commandHistory={[]} diff --git a/ui/desktop/src/components/pair.tsx b/ui/desktop/src/components/pair.tsx index f360b4c837bf..13512ae3ddab 100644 --- a/ui/desktop/src/components/pair.tsx +++ b/ui/desktop/src/components/pair.tsx @@ -121,47 +121,6 @@ export default function Pair({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.state, hasProcessedInitialInput, initialMessage]); - // Auto-submit the initial message after it's been set and component is ready - useEffect(() => { - if (shouldAutoSubmit && initialMessage) { - // Wait for the component to be fully rendered - const timer = setTimeout(() => { - // Try to trigger form submission programmatically - const textarea = document.querySelector( - 'textarea[data-testid="chat-input"]' - ) as HTMLTextAreaElement; - const form = textarea?.closest('form'); - - if (textarea && form) { - // Set the textarea value - textarea.value = initialMessage; - // eslint-disable-next-line no-undef - textarea.dispatchEvent(new Event('input', { bubbles: true })); - - // Focus the textarea - textarea.focus(); - - // Simulate Enter key press to trigger submission - const enterEvent = new KeyboardEvent('keydown', { - key: 'Enter', - code: 'Enter', - keyCode: 13, - which: 13, - bubbles: true, - }); - textarea.dispatchEvent(enterEvent); - - setShouldAutoSubmit(false); - } - }, 500); // Give more time for the component to fully mount - - return () => clearTimeout(timer); - } - - // Return undefined when condition is not met - return undefined; - }, [shouldAutoSubmit, initialMessage]); - // Custom message submit handler const handleMessageSubmit = (message: string) => { // This is called after a message is submitted @@ -186,27 +145,20 @@ export default function Pair({ initialValue, }; - // Custom content before messages - const renderBeforeMessages = () => { - return
{/* Any Pair-specific content before messages can go here */}
; - }; - return ( - <> - - + ); }