Remove half-second wait, rework auto submit (#4282)

This commit is contained in:
Jack Amadeo
2025-08-22 11:45:34 -04:00
committed by GitHub
parent 24b76baa55
commit 0f1d3f68bd
4 changed files with 105 additions and 123 deletions
+3
View File
@@ -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<ScrollAreaHandle>(null);
@@ -538,6 +540,7 @@ function BaseChatContent({
recipeConfig={recipeConfig}
recipeAccepted={recipeAccepted}
initialPrompt={initialPrompt}
autoSubmit={autoSubmit}
{...customChatInputProps}
/>
</div>
+87 -61
View File
@@ -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
@@ -358,6 +360,7 @@ export default function ChatInput({
const [hasUserTyped, setHasUserTyped] = useState(false);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const timeoutRefsRef = useRef<Set<ReturnType<typeof setTimeout>>>(new Set());
const [didAutoSubmit, setDidAutoSubmit] = useState<boolean>(false);
// Use shared file drop hook for ChatInput
const {
@@ -368,7 +371,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
@@ -937,69 +943,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);
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);
// 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();
let textToSend = 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(' '));
}
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);
}
setDisplayValue('');
setValue('');
setPastedImages([]);
setHistoryIndex(-1);
setSavedInput('');
setIsInGlobalHistory(false);
setHasUserTyped(false);
// 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,
]
);
useEffect(() => {
if (!!autoSubmit && !didAutoSubmit) {
setDidAutoSubmit(true);
performSubmit(initialValue);
}
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
);
// 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);
// 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([]);
}
}
};
}, [autoSubmit, didAutoSubmit, initialValue, performSubmit]);
const handleKeyDown = (evt: React.KeyboardEvent<HTMLTextAreaElement>) => {
// If mention popover is open, handle arrow keys and enter
+1
View File
@@ -85,6 +85,7 @@ export default function Hub({
<ChatInput
handleSubmit={handleSubmit}
autoSubmit={false}
chatState={ChatState.Idle}
onStop={() => {}}
commandHistory={[]}
+14 -62
View File
@@ -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 <div>{/* Any Pair-specific content before messages can go here */}</div>;
};
return (
<>
<BaseChat
chat={chat}
setChat={setChat}
setView={setView}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
enableLocalStorage={true} // Enable local storage for Pair mode
onMessageSubmit={handleMessageSubmit}
onMessageStreamFinish={handleMessageStreamFinish}
renderBeforeMessages={renderBeforeMessages}
customChatInputProps={customChatInputProps}
contentClassName={cn('pr-1 pb-10', (isMobile || sidebarState === 'collapsed') && 'pt-11')} // Use dynamic content class with mobile margin and sidebar state
showPopularTopics={!isTransitioningFromHub} // Don't show popular topics while transitioning from Hub
suppressEmptyState={isTransitioningFromHub} // Suppress all empty state content while transitioning from Hub
/>
</>
<BaseChat
chat={chat}
autoSubmit={shouldAutoSubmit}
setChat={setChat}
setView={setView}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
enableLocalStorage={true} // Enable local storage for Pair mode
onMessageSubmit={handleMessageSubmit}
onMessageStreamFinish={handleMessageStreamFinish}
customChatInputProps={customChatInputProps}
contentClassName={cn('pr-1 pb-10', (isMobile || sidebarState === 'collapsed') && 'pt-11')} // Use dynamic content class with mobile margin and sidebar state
showPopularTopics={!isTransitioningFromHub} // Don't show popular topics while transitioning from Hub
suppressEmptyState={isTransitioningFromHub} // Suppress all empty state content while transitioning from Hub
/>
);
}