Add support for interrupting messages after they've been sent.

This commit is contained in:
sabaimran 2024-11-12 22:22:45 -08:00
parent d607ad7a27
commit 4a1b1e8b9a
4 changed files with 63 additions and 14 deletions

View file

@ -38,6 +38,7 @@ interface ChatBodyDataProps {
isMobileWidth?: boolean; isMobileWidth?: boolean;
isLoggedIn: boolean; isLoggedIn: boolean;
setImages: (images: string[]) => void; setImages: (images: string[]) => void;
setTriggeredAbort: (triggeredAbort: boolean) => void;
} }
function ChatBodyData(props: ChatBodyDataProps) { function ChatBodyData(props: ChatBodyDataProps) {
@ -160,6 +161,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
setUploadedFiles={props.setUploadedFiles} setUploadedFiles={props.setUploadedFiles}
ref={chatInputRef} ref={chatInputRef}
isResearchModeEnabled={isInResearchMode} isResearchModeEnabled={isInResearchMode}
setTriggeredAbort={props.setTriggeredAbort}
/> />
</div> </div>
</> </>
@ -178,6 +180,10 @@ export default function Chat() {
const [uploadedFiles, setUploadedFiles] = useState<AttachedFileText[] | undefined>(undefined); const [uploadedFiles, setUploadedFiles] = useState<AttachedFileText[] | undefined>(undefined);
const [images, setImages] = useState<string[]>([]); const [images, setImages] = useState<string[]>([]);
const [abortMessageStreamController, setAbortMessageStreamController] =
useState<AbortController | null>(null);
const [triggeredAbort, setTriggeredAbort] = useState(false);
const locationData = useIPLocationData() || { const locationData = useIPLocationData() || {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
}; };
@ -202,6 +208,15 @@ export default function Chat() {
welcomeConsole(); welcomeConsole();
}, []); }, []);
useEffect(() => {
if (triggeredAbort) {
abortMessageStreamController?.abort();
handleAbortedMessage();
setTriggeredAbort(false);
}
}),
[triggeredAbort];
useEffect(() => { useEffect(() => {
if (queryToProcess) { if (queryToProcess) {
const newStreamMessage: StreamMessage = { const newStreamMessage: StreamMessage = {
@ -218,6 +233,7 @@ export default function Chat() {
}; };
setMessages((prevMessages) => [...prevMessages, newStreamMessage]); setMessages((prevMessages) => [...prevMessages, newStreamMessage]);
setProcessQuerySignal(true); setProcessQuerySignal(true);
setAbortMessageStreamController(new AbortController());
} }
}, [queryToProcess]); }, [queryToProcess]);
@ -283,6 +299,17 @@ export default function Chat() {
} }
} }
function handleAbortedMessage() {
const currentMessage = messages.find((message) => !message.completed);
if (!currentMessage) return;
currentMessage.rawResponse = `I've stopped processing this message. If you'd like to continue, please send another message.`;
currentMessage.completed = true;
setMessages([...messages]);
setQueryToProcess("");
setProcessQuerySignal(false);
}
async function chat() { async function chat() {
localStorage.removeItem("message"); localStorage.removeItem("message");
if (!queryToProcess || !conversationId) return; if (!queryToProcess || !conversationId) return;
@ -308,6 +335,7 @@ export default function Chat() {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(chatAPIBody), body: JSON.stringify(chatAPIBody),
signal: abortMessageStreamController?.signal,
}); });
try { try {
@ -321,14 +349,18 @@ export default function Chat() {
// Render error message as current message // Render error message as current message
const errorMessage = (err as Error).message; const errorMessage = (err as Error).message;
const errorName = (err as Error).name;
if (errorMessage.includes("Error in input stream")) if (errorMessage.includes("Error in input stream"))
currentMessage.rawResponse = `Woops! The connection broke while I was writing my thoughts down. Maybe try again in a bit or dislike this message if the issue persists?`; currentMessage.rawResponse = `Woops! The connection broke while I was writing my thoughts down. Maybe try again in a bit or dislike this message if the issue persists?`;
else if (response.status === 429) { else if (response.status === 429) {
"detail" in apiError "detail" in apiError
? (currentMessage.rawResponse = `${apiError.detail}`) ? (currentMessage.rawResponse = `${apiError.detail}`)
: (currentMessage.rawResponse = `I'm a bit overwhelmed at the moment. Could you try again in a bit or dislike this message if the issue persists?`); : (currentMessage.rawResponse = `I'm a bit overwhelmed at the moment. Could you try again in a bit or dislike this message if the issue persists?`);
} else } else if (errorName === "AbortError") {
currentMessage.rawResponse = `I've stopped processing this message. If you'd like to continue, please send the message again.`;
} else {
currentMessage.rawResponse = `Umm, not sure what just happened. I see this error message: ${errorMessage}. Could you try again or dislike this message if the issue persists?`; currentMessage.rawResponse = `Umm, not sure what just happened. I see this error message: ${errorMessage}. Could you try again or dislike this message if the issue persists?`;
}
// Complete message streaming teardown properly // Complete message streaming teardown properly
currentMessage.completed = true; currentMessage.completed = true;
@ -388,6 +420,7 @@ export default function Chat() {
isMobileWidth={isMobileWidth} isMobileWidth={isMobileWidth}
onConversationIdChange={handleConversationIdChange} onConversationIdChange={handleConversationIdChange}
setImages={setImages} setImages={setImages}
setTriggeredAbort={setTriggeredAbort}
/> />
</Suspense> </Suspense>
</div> </div>

View file

@ -76,6 +76,7 @@ interface ChatInputProps {
isLoggedIn: boolean; isLoggedIn: boolean;
agentColor?: string; agentColor?: string;
isResearchModeEnabled?: boolean; isResearchModeEnabled?: boolean;
setTriggeredAbort: (value: boolean) => void;
} }
export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((props, ref) => { export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((props, ref) => {
@ -678,20 +679,33 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button {props.sendDisabled ? (
variant="default" <Button
className={`${!message || recording || "hidden"} ${props.agentColor ? convertToBGClass(props.agentColor) : "bg-orange-300 hover:bg-orange-500"} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`} variant="default"
onClick={() => { className={`${props.agentColor ? convertToBGClass(props.agentColor) : "bg-orange-300 hover:bg-orange-500"} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
setMessage("Listening..."); onClick={() => {
setRecording(!recording); props.setTriggeredAbort(true);
}} }}
disabled={props.sendDisabled} >
> <Stop weight="fill" className="w-6 h-6" />
<Microphone weight="fill" className="w-6 h-6" /> </Button>
</Button> ) : (
<Button
variant="default"
className={`${!message || recording || "hidden"} ${props.agentColor ? convertToBGClass(props.agentColor) : "bg-orange-300 hover:bg-orange-500"} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
onClick={() => {
setMessage("Listening...");
setRecording(!recording);
}}
>
<Microphone weight="fill" className="w-6 h-6" />
</Button>
)}
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
Click to transcribe your message with voice. {props.sendDisabled
? "Click here to stop the streaming."
: "Click to transcribe your message with voice."}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@ -699,7 +713,6 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
<Button <Button
className={`${(!message || recording) && "hidden"} ${props.agentColor ? convertToBGClass(props.agentColor) : "bg-orange-300 hover:bg-orange-500"} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`} className={`${(!message || recording) && "hidden"} ${props.agentColor ? convertToBGClass(props.agentColor) : "bg-orange-300 hover:bg-orange-500"} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
onClick={onSendMessage} onClick={onSendMessage}
disabled={props.sendDisabled}
> >
<ArrowUp className="w-6 h-6" weight="bold" /> <ArrowUp className="w-6 h-6" weight="bold" />
</Button> </Button>

View file

@ -312,6 +312,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
setUploadedFiles={props.setUploadedFiles} setUploadedFiles={props.setUploadedFiles}
agentColor={agents.find((agent) => agent.slug === selectedAgent)?.color} agentColor={agents.find((agent) => agent.slug === selectedAgent)?.color}
ref={chatInputRef} ref={chatInputRef}
setTriggeredAbort={() => {}}
/> />
</div> </div>
)} )}
@ -394,6 +395,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
setUploadedFiles={props.setUploadedFiles} setUploadedFiles={props.setUploadedFiles}
agentColor={agents.find((agent) => agent.slug === selectedAgent)?.color} agentColor={agents.find((agent) => agent.slug === selectedAgent)?.color}
ref={chatInputRef} ref={chatInputRef}
setTriggeredAbort={() => {}}
/> />
</div> </div>
</> </>

View file

@ -101,6 +101,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
agentColor={agentMetadata?.color} agentColor={agentMetadata?.color}
isMobileWidth={props.isMobileWidth} isMobileWidth={props.isMobileWidth}
setUploadedFiles={props.setUploadedFiles} setUploadedFiles={props.setUploadedFiles}
setTriggeredAbort={() => {}}
ref={chatInputRef} ref={chatInputRef}
/> />
</div> </div>