add pause button
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from "react";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
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 { api } from "@/lib/api";
|
||||||
import { type ChartSpec } from "@/store/visualizationStore";
|
import { type ChartSpec } from "@/store/visualizationStore";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
@@ -82,6 +82,7 @@ export function ChatInterface() {
|
|||||||
const [activeDataFile, setActiveDataFile] = useState<DataFileContext | null>(null);
|
const [activeDataFile, setActiveDataFile] = useState<DataFileContext | null>(null);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchModels();
|
fetchModels();
|
||||||
@@ -228,6 +229,20 @@ export function ChatInterface() {
|
|||||||
}
|
}
|
||||||
}, [messages]);
|
}, [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 () => {
|
const handleSend = async () => {
|
||||||
if (!input.trim() || isLoading) return;
|
if (!input.trim() || isLoading) return;
|
||||||
|
|
||||||
@@ -242,6 +257,8 @@ export function ChatInterface() {
|
|||||||
setAttachedFile(null);
|
setAttachedFile(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
abortControllerRef.current = controller;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -278,6 +295,7 @@ export function ChatInterface() {
|
|||||||
prefer_sql_chart: preferSqlChart,
|
prefer_sql_chart: preferSqlChart,
|
||||||
file_url: fileUrl,
|
file_url: fileUrl,
|
||||||
}),
|
}),
|
||||||
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok || !response.body) {
|
if (!response.ok || !response.body) {
|
||||||
@@ -363,7 +381,7 @@ export function ChatInterface() {
|
|||||||
source,
|
source,
|
||||||
prefer_sql_chart: preferSqlChart,
|
prefer_sql_chart: preferSqlChart,
|
||||||
file_url: fileUrl,
|
file_url: fileUrl,
|
||||||
});
|
}, { signal: controller.signal });
|
||||||
const fallbackViz = fallback.viz ? buildMessageViz(fallback.viz) : undefined;
|
const fallbackViz = fallback.viz ? buildMessageViz(fallback.viz) : undefined;
|
||||||
setMessages((prev) =>
|
setMessages((prev) =>
|
||||||
prev.map((msg) =>
|
prev.map((msg) =>
|
||||||
@@ -391,7 +409,7 @@ export function ChatInterface() {
|
|||||||
file_url: fileUrl,
|
file_url: fileUrl,
|
||||||
session_id: activeSessionKey,
|
session_id: activeSessionKey,
|
||||||
model_id: selectedModelId
|
model_id: selectedModelId
|
||||||
});
|
}, { signal: controller.signal });
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
setMessages(prev => [...prev, {
|
setMessages(prev => [...prev, {
|
||||||
@@ -415,12 +433,25 @@ export function ChatInterface() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} 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, {
|
setMessages(prev => [...prev, {
|
||||||
id: (Date.now() + 1).toString(),
|
id: (Date.now() + 1).toString(),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: `Sorry, something went wrong: ${error.message}`
|
content: `Sorry, something went wrong: ${error.message}`
|
||||||
}]);
|
}]);
|
||||||
} finally {
|
} finally {
|
||||||
|
if (abortControllerRef.current === controller) {
|
||||||
|
abortControllerRef.current = null;
|
||||||
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
window.dispatchEvent(new Event("nanobot:sessions-changed"));
|
window.dispatchEvent(new Event("nanobot:sessions-changed"));
|
||||||
}
|
}
|
||||||
@@ -574,16 +605,18 @@ export function ChatInterface() {
|
|||||||
{isUploading ? <Loader2 className="h-5 w-5 animate-spin" /> : <Paperclip className="h-5 w-5" />}
|
{isUploading ? <Loader2 className="h-5 w-5 animate-spin" /> : <Paperclip className="h-5 w-5" />}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSend}
|
onClick={isLoading ? handleForceStop : handleSend}
|
||||||
size="icon"
|
size="icon"
|
||||||
disabled={!input.trim() || isLoading}
|
disabled={isLoading ? false : !input.trim()}
|
||||||
className={`h-9 w-9 rounded-full transition-all ${
|
className={`h-9 w-9 rounded-full transition-all ${
|
||||||
input.trim()
|
isLoading
|
||||||
|
? 'bg-red-100 text-red-700 hover:bg-red-200'
|
||||||
|
: input.trim()
|
||||||
? 'bg-zinc-100 text-zinc-900 hover:bg-zinc-200'
|
? 'bg-zinc-100 text-zinc-900 hover:bg-zinc-200'
|
||||||
: 'bg-zinc-50 text-zinc-300 cursor-not-allowed'
|
: 'bg-zinc-50 text-zinc-300 cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ArrowUp className="h-5 w-5" />}
|
{isLoading ? <Square className="h-4 w-4" /> : <ArrowUp className="h-5 w-5" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -691,16 +724,18 @@ export function ChatInterface() {
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSend}
|
onClick={isLoading ? handleForceStop : handleSend}
|
||||||
size="icon"
|
size="icon"
|
||||||
disabled={!input.trim() || isLoading}
|
disabled={isLoading ? false : !input.trim()}
|
||||||
className={`h-9 w-9 rounded-lg shrink-0 transition-all ${
|
className={`h-9 w-9 rounded-lg shrink-0 transition-all ${
|
||||||
input.trim()
|
isLoading
|
||||||
|
? 'bg-red-600 hover:bg-red-700 text-white shadow-md'
|
||||||
|
: input.trim()
|
||||||
? 'bg-blue-600 hover:bg-blue-700 text-white shadow-md'
|
? 'bg-blue-600 hover:bg-blue-700 text-white shadow-md'
|
||||||
: 'bg-zinc-100 text-zinc-300 hover:bg-zinc-100 cursor-not-allowed'
|
: 'bg-zinc-100 text-zinc-300 hover:bg-zinc-100 cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ArrowUp className="h-5 w-5" />}
|
{isLoading ? <Square className="h-4 w-4" /> : <ArrowUp className="h-5 w-5" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user