From a46d0a4c683e0fb43b9adacfd2ebc337a51b0561 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:05:10 -0500 Subject: [PATCH] add ability to stop streaming --- frigate/api/chat.py | 11 ++++++++ web/src/pages/Chat.tsx | 62 ++++++++++++++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/frigate/api/chat.py b/frigate/api/chat.py index 0ecf210991..22815dcecc 100644 --- a/frigate/api/chat.py +++ b/frigate/api/chat.py @@ -1328,6 +1328,9 @@ When a user refers to a specific object they have seen or describe with identify async def stream_body_llm(): nonlocal conversation, stream_tool_calls, stream_iterations while stream_iterations < max_iterations: + if await request.is_disconnected(): + logger.debug("Client disconnected, stopping chat stream") + return logger.debug( f"Streaming LLM (iteration {stream_iterations + 1}/{max_iterations}) " f"with {len(conversation)} message(s)" @@ -1337,6 +1340,9 @@ When a user refers to a specific object they have seen or describe with identify tools=tools if tools else None, tool_choice="auto", ): + if await request.is_disconnected(): + logger.debug("Client disconnected, stopping chat stream") + return kind, value = event if kind == "content_delta": yield ( @@ -1366,6 +1372,11 @@ When a user refers to a specific object they have seen or describe with identify msg.get("content"), pending ) ) + if await request.is_disconnected(): + logger.debug( + "Client disconnected before tool execution" + ) + return ( executed_calls, tool_results, diff --git a/web/src/pages/Chat.tsx b/web/src/pages/Chat.tsx index 5399f10bbe..474aa6d213 100644 --- a/web/src/pages/Chat.tsx +++ b/web/src/pages/Chat.tsx @@ -1,6 +1,6 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { FaArrowUpLong } from "react-icons/fa6"; +import { FaArrowUpLong, FaStop } from "react-icons/fa6"; import { LuCircleAlert } from "react-icons/lu"; import { useTranslation } from "react-i18next"; import { useState, useCallback, useRef, useEffect, useMemo } from "react"; @@ -28,6 +28,7 @@ export default function ChatPage() { const [error, setError] = useState(null); const [attachedEventId, setAttachedEventId] = useState(null); const scrollRef = useRef(null); + const abortRef = useRef(null); useEffect(() => { document.title = t("documentTitle"); @@ -70,12 +71,24 @@ export default function ChatPage() { ...(axios.defaults.headers.common as Record), }; - await streamChatCompletion(url, headers, apiMessages, { - updateMessages: (updater) => setMessages(updater), - onError: (message) => setError(message), - onDone: () => setIsLoading(false), - defaultErrorMessage: t("error"), - }); + const controller = new AbortController(); + abortRef.current = controller; + + await streamChatCompletion( + url, + headers, + apiMessages, + { + updateMessages: (updater) => setMessages(updater), + onError: (message) => setError(message), + onDone: () => { + abortRef.current = null; + setIsLoading(false); + }, + defaultErrorMessage: t("error"), + }, + controller.signal, + ); }, [isLoading, t], ); @@ -106,6 +119,12 @@ export default function ChatPage() { [attachedEventId, input, isLoading, messages, submitConversation], ); + const stopGeneration = useCallback(() => { + abortRef.current?.abort(); + abortRef.current = null; + setIsLoading(false); + }, []); + const handleEditSubmit = useCallback( (messageIndex: number, newContent: string) => { const newList: ChatMessage[] = [ @@ -237,6 +256,7 @@ export default function ChatPage() { attachedEventId={attachedEventId} onClearAttachment={handleClearAttachment} onAttach={setAttachedEventId} + onStop={stopGeneration} recentEventIds={recentEventIds} /> )} @@ -254,6 +274,7 @@ type ChatEntryProps = { attachedEventId: string | null; onClearAttachment: () => void; onAttach: (eventId: string) => void; + onStop: () => void; recentEventIds: string[]; }; @@ -266,6 +287,7 @@ function ChatEntry({ attachedEventId, onClearAttachment, onAttach, + onStop, recentEventIds, }: ChatEntryProps) { const handleKeyDown = (e: React.KeyboardEvent) => { @@ -306,14 +328,24 @@ function ChatEntry({ onKeyDown={handleKeyDown} aria-busy={isLoading} /> - + {isLoading ? ( + + ) : ( + + )} );