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)
|
disableSearch?: boolean; // Disable search functionality (for Hub)
|
||||||
showPopularTopics?: boolean; // Show popular chat topics in empty state (for Pair)
|
showPopularTopics?: boolean; // Show popular chat topics in empty state (for Pair)
|
||||||
suppressEmptyState?: boolean; // Suppress empty state content (for transitions)
|
suppressEmptyState?: boolean; // Suppress empty state content (for transitions)
|
||||||
|
autoSubmit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BaseChatContent({
|
function BaseChatContent({
|
||||||
@@ -112,6 +113,7 @@ function BaseChatContent({
|
|||||||
disableSearch = false,
|
disableSearch = false,
|
||||||
showPopularTopics = false,
|
showPopularTopics = false,
|
||||||
suppressEmptyState = false,
|
suppressEmptyState = false,
|
||||||
|
autoSubmit = false,
|
||||||
}: BaseChatProps) {
|
}: BaseChatProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const scrollRef = useRef<ScrollAreaHandle>(null);
|
const scrollRef = useRef<ScrollAreaHandle>(null);
|
||||||
@@ -538,6 +540,7 @@ function BaseChatContent({
|
|||||||
recipeConfig={recipeConfig}
|
recipeConfig={recipeConfig}
|
||||||
recipeAccepted={recipeAccepted}
|
recipeAccepted={recipeAccepted}
|
||||||
initialPrompt={initialPrompt}
|
initialPrompt={initialPrompt}
|
||||||
|
autoSubmit={autoSubmit}
|
||||||
{...customChatInputProps}
|
{...customChatInputProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { FolderKey, ScrollText } from 'lucide-react';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/Tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/Tooltip';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
@@ -84,6 +84,7 @@ interface ChatInputProps {
|
|||||||
recipeConfig?: Recipe | null;
|
recipeConfig?: Recipe | null;
|
||||||
recipeAccepted?: boolean;
|
recipeAccepted?: boolean;
|
||||||
initialPrompt?: string;
|
initialPrompt?: string;
|
||||||
|
autoSubmit: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatInput({
|
export default function ChatInput({
|
||||||
@@ -106,6 +107,7 @@ export default function ChatInput({
|
|||||||
recipeConfig,
|
recipeConfig,
|
||||||
recipeAccepted,
|
recipeAccepted,
|
||||||
initialPrompt,
|
initialPrompt,
|
||||||
|
autoSubmit = false,
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const [_value, setValue] = useState(initialValue);
|
const [_value, setValue] = useState(initialValue);
|
||||||
const [displayValue, setDisplayValue] = useState(initialValue); // For immediate visual feedback
|
const [displayValue, setDisplayValue] = useState(initialValue); // For immediate visual feedback
|
||||||
@@ -358,6 +360,7 @@ export default function ChatInput({
|
|||||||
const [hasUserTyped, setHasUserTyped] = useState(false);
|
const [hasUserTyped, setHasUserTyped] = useState(false);
|
||||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const timeoutRefsRef = useRef<Set<ReturnType<typeof setTimeout>>>(new Set());
|
const timeoutRefsRef = useRef<Set<ReturnType<typeof setTimeout>>>(new Set());
|
||||||
|
const [didAutoSubmit, setDidAutoSubmit] = useState<boolean>(false);
|
||||||
|
|
||||||
// Use shared file drop hook for ChatInput
|
// Use shared file drop hook for ChatInput
|
||||||
const {
|
const {
|
||||||
@@ -368,7 +371,10 @@ export default function ChatInput({
|
|||||||
} = useFileDrop();
|
} = useFileDrop();
|
||||||
|
|
||||||
// Merge local dropped files with parent dropped files
|
// Merge local dropped files with parent dropped files
|
||||||
const allDroppedFiles = [...droppedFiles, ...localDroppedFiles];
|
const allDroppedFiles = useMemo(
|
||||||
|
() => [...droppedFiles, ...localDroppedFiles],
|
||||||
|
[droppedFiles, localDroppedFiles]
|
||||||
|
);
|
||||||
|
|
||||||
const handleRemoveDroppedFile = (idToRemove: string) => {
|
const handleRemoveDroppedFile = (idToRemove: string) => {
|
||||||
// Remove from local dropped files
|
// Remove from local dropped files
|
||||||
@@ -937,69 +943,89 @@ export default function ChatInput({
|
|||||||
return true; // Return true if message was queued
|
return true; // Return true if message was queued
|
||||||
};
|
};
|
||||||
|
|
||||||
const performSubmit = () => {
|
const performSubmit = useCallback(
|
||||||
const validPastedImageFilesPaths = pastedImages
|
(text?: string) => {
|
||||||
.filter((img) => img.filePath && !img.error && !img.isLoading)
|
const validPastedImageFilesPaths = pastedImages
|
||||||
.map((img) => img.filePath as string);
|
.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)
|
let textToSend = text ?? displayValue.trim();
|
||||||
const droppedFilePaths = allDroppedFiles
|
|
||||||
.filter((file) => !file.error && !file.isLoading)
|
|
||||||
.map((file) => file.path);
|
|
||||||
|
|
||||||
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
|
if (textToSend) {
|
||||||
const allFilePaths = [...validPastedImageFilesPaths, ...droppedFilePaths];
|
if (displayValue.trim()) {
|
||||||
if (allFilePaths.length > 0) {
|
LocalMessageStorage.addMessage(displayValue);
|
||||||
const pathsString = allFilePaths.join(' ');
|
} else if (allFilePaths.length > 0) {
|
||||||
textToSend = textToSend ? `${textToSend} ${pathsString}` : pathsString;
|
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);
|
||||||
}
|
}
|
||||||
|
}, [autoSubmit, didAutoSubmit, initialValue, performSubmit]);
|
||||||
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([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (evt: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (evt: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
// If mention popover is open, handle arrow keys and enter
|
// If mention popover is open, handle arrow keys and enter
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export default function Hub({
|
|||||||
|
|
||||||
<ChatInput
|
<ChatInput
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
|
autoSubmit={false}
|
||||||
chatState={ChatState.Idle}
|
chatState={ChatState.Idle}
|
||||||
onStop={() => {}}
|
onStop={() => {}}
|
||||||
commandHistory={[]}
|
commandHistory={[]}
|
||||||
|
|||||||
@@ -121,47 +121,6 @@ export default function Pair({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [location.state, hasProcessedInitialInput, initialMessage]);
|
}, [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
|
// Custom message submit handler
|
||||||
const handleMessageSubmit = (message: string) => {
|
const handleMessageSubmit = (message: string) => {
|
||||||
// This is called after a message is submitted
|
// This is called after a message is submitted
|
||||||
@@ -186,27 +145,20 @@ export default function Pair({
|
|||||||
initialValue,
|
initialValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Custom content before messages
|
|
||||||
const renderBeforeMessages = () => {
|
|
||||||
return <div>{/* Any Pair-specific content before messages can go here */}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<BaseChat
|
||||||
<BaseChat
|
chat={chat}
|
||||||
chat={chat}
|
autoSubmit={shouldAutoSubmit}
|
||||||
setChat={setChat}
|
setChat={setChat}
|
||||||
setView={setView}
|
setView={setView}
|
||||||
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
||||||
enableLocalStorage={true} // Enable local storage for Pair mode
|
enableLocalStorage={true} // Enable local storage for Pair mode
|
||||||
onMessageSubmit={handleMessageSubmit}
|
onMessageSubmit={handleMessageSubmit}
|
||||||
onMessageStreamFinish={handleMessageStreamFinish}
|
onMessageStreamFinish={handleMessageStreamFinish}
|
||||||
renderBeforeMessages={renderBeforeMessages}
|
customChatInputProps={customChatInputProps}
|
||||||
customChatInputProps={customChatInputProps}
|
contentClassName={cn('pr-1 pb-10', (isMobile || sidebarState === 'collapsed') && 'pt-11')} // Use dynamic content class with mobile margin and sidebar state
|
||||||
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
|
||||||
showPopularTopics={!isTransitioningFromHub} // Don't show popular topics while transitioning from Hub
|
suppressEmptyState={isTransitioningFromHub} // Suppress all empty state content while transitioning from Hub
|
||||||
suppressEmptyState={isTransitioningFromHub} // Suppress all empty state content while transitioning from Hub
|
/>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user