mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Remove half-second wait, rework auto submit (#4282)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -85,6 +85,7 @@ export default function Hub({
|
||||
|
||||
<ChatInput
|
||||
handleSubmit={handleSubmit}
|
||||
autoSubmit={false}
|
||||
chatState={ChatState.Idle}
|
||||
onStop={() => {}}
|
||||
commandHistory={[]}
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user