From 1e0a9e3c157ed8c5aeb39349227c6d5adc3b6af3 Mon Sep 17 00:00:00 2001 From: qixinbo Date: Sun, 15 Mar 2026 18:41:58 +0800 Subject: [PATCH] add pause button --- frontend/src/components/ChatInterface.tsx | 65 +++++++++++++++++------ 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/ChatInterface.tsx b/frontend/src/components/ChatInterface.tsx index b14f897..b6ecb5d 100644 --- a/frontend/src/components/ChatInterface.tsx +++ b/frontend/src/components/ChatInterface.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { User, Loader2, Sparkles, Search, ArrowUp, ChevronDown, Table, Paperclip, Check, X, File as FileIcon } from "lucide-react"; +import { User, Loader2, Sparkles, Search, ArrowUp, ChevronDown, Table, Paperclip, Check, X, File as FileIcon, Square } from "lucide-react"; import { api } from "@/lib/api"; import { type ChartSpec } from "@/store/visualizationStore"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; @@ -82,6 +82,7 @@ export function ChatInterface() { const [activeDataFile, setActiveDataFile] = useState(null); const [isUploading, setIsUploading] = useState(false); const fileInputRef = useRef(null); + const abortControllerRef = useRef(null); useEffect(() => { fetchModels(); @@ -228,6 +229,20 @@ export function ChatInterface() { } }, [messages]); + const handleForceStop = () => { + const controller = abortControllerRef.current; + if (!controller) return; + controller.abort(); + setIsLoading(false); + setMessages((prev) => + prev.map((msg) => + msg.awaitingFirstToken + ? { ...msg, awaitingFirstToken: false, content: msg.content || "已中断输出" } + : msg + ) + ); + }; + const handleSend = async () => { if (!input.trim() || isLoading) return; @@ -242,6 +257,8 @@ export function ChatInterface() { setAttachedFile(null); } + const controller = new AbortController(); + abortControllerRef.current = controller; setIsLoading(true); try { @@ -278,6 +295,7 @@ export function ChatInterface() { prefer_sql_chart: preferSqlChart, file_url: fileUrl, }), + signal: controller.signal, }); if (!response.ok || !response.body) { @@ -363,7 +381,7 @@ export function ChatInterface() { source, prefer_sql_chart: preferSqlChart, file_url: fileUrl, - }); + }, { signal: controller.signal }); const fallbackViz = fallback.viz ? buildMessageViz(fallback.viz) : undefined; setMessages((prev) => prev.map((msg) => @@ -391,7 +409,7 @@ export function ChatInterface() { file_url: fileUrl, session_id: activeSessionKey, model_id: selectedModelId - }); + }, { signal: controller.signal }); if (response.error) { setMessages(prev => [...prev, { @@ -415,12 +433,25 @@ export function ChatInterface() { } } } catch (error: any) { + if (error?.name === "AbortError" || String(error?.message || "").toLowerCase().includes("aborted")) { + setMessages((prev) => + prev.map((msg) => + msg.awaitingFirstToken + ? { ...msg, awaitingFirstToken: false, content: msg.content || "已中断输出" } + : msg + ) + ); + return; + } setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: 'assistant', content: `Sorry, something went wrong: ${error.message}` }]); } finally { + if (abortControllerRef.current === controller) { + abortControllerRef.current = null; + } setIsLoading(false); window.dispatchEvent(new Event("nanobot:sessions-changed")); } @@ -574,16 +605,18 @@ export function ChatInterface() { {isUploading ? : } @@ -691,16 +724,18 @@ export function ChatInterface() { disabled={isLoading} />