fix: answer persistent

This commit is contained in:
qixinbo
2026-03-17 20:12:48 +08:00
parent 68a44f3837
commit cd764fad43
+52 -30
View File
@@ -69,17 +69,37 @@ interface SessionData {
} }
export function ChatInterface() { export function ChatInterface() {
const [messages, setMessages] = useState<Message[]>([]); const [messagesBySession, setMessagesBySession] = useState<Record<string, Message[]>>({});
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const [selectedDataSource, setSelectedDataSource] = useState<string>(""); const [selectedDataSource, setSelectedDataSource] = useState<string>("");
const [availableSkills, setAvailableSkills] = useState<Skill[]>([]); const [availableSkills, setAvailableSkills] = useState<Skill[]>([]);
const [selectedSkillIds, setSelectedSkillIds] = useState<string[]>([]); const [selectedSkillIds, setSelectedSkillIds] = useState<string[]>([]);
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const location = useLocation(); const location = useLocation();
const { currentProject } = useProjectStore(); const { currentProject } = useProjectStore();
const setMessagesForSession = (sessionKey: string, updater: React.SetStateAction<Message[]>) => {
setMessagesBySession(prev => {
const current = prev[sessionKey] || [];
const next = typeof updater === 'function' ? (updater as (msgs: Message[]) => Message[])(current) : updater;
return { ...prev, [sessionKey]: next };
});
};
const setIsLoadingForSession = (sessionKey: string, loading: boolean) => {
setLoadingBySession(prev => ({ ...prev, [sessionKey]: loading }));
};
const queryParams = new URLSearchParams(location.search);
const activeSessionKey = queryParams.get("session") || "api:default";
const messages = messagesBySession[activeSessionKey] || [];
const [loadingBySession, setLoadingBySession] = useState<Record<string, boolean>>({});
const isLoading = loadingBySession[activeSessionKey] || false;
const generatingSessionsRef = useRef<Record<string, boolean>>({});
const abortControllersRef = useRef<Record<string, AbortController>>({});
// Model selection state // Model selection state
const [models, setModels] = useState<ModelConfig[]>([]); const [models, setModels] = useState<ModelConfig[]>([]);
const [selectedModelId, setSelectedModelId] = useState<string>(""); const [selectedModelId, setSelectedModelId] = useState<string>("");
@@ -88,16 +108,11 @@ export function ChatInterface() {
// Data Source selection state // Data Source selection state
const [availableDataSources, setAvailableDataSources] = useState<{id: string, name: string}[]>([]); const [availableDataSources, setAvailableDataSources] = useState<{id: string, name: string}[]>([]);
// Try to parse active session from URL query
const queryParams = new URLSearchParams(location.search);
const activeSessionKey = queryParams.get("session") || "api:default";
// File upload state // File upload state
const [attachedFile, setAttachedFile] = useState<DataFileContext | null>(null); const [attachedFile, setAttachedFile] = useState<DataFileContext | null>(null);
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();
@@ -147,7 +162,10 @@ export function ChatInterface() {
useEffect(() => { useEffect(() => {
const fetchSessionData = async () => { const fetchSessionData = async () => {
setIsLoading(true); if (generatingSessionsRef.current[activeSessionKey]) {
return; // Do not fetch if we are currently generating for this session
}
setIsLoadingForSession(activeSessionKey, true);
setSelectedSkillIds([]); setSelectedSkillIds([]);
try { try {
const data = await api.get<SessionData>(`/nanobot/sessions/${activeSessionKey}`); const data = await api.get<SessionData>(`/nanobot/sessions/${activeSessionKey}`);
@@ -158,9 +176,9 @@ export function ChatInterface() {
content: m.content, content: m.content,
viz: m.viz ? buildMessageViz(m.viz) : undefined, viz: m.viz ? buildMessageViz(m.viz) : undefined,
})); }));
setMessages(formattedMessages); setMessagesForSession(activeSessionKey, formattedMessages);
} else { } else {
setMessages([]); setMessagesForSession(activeSessionKey, []);
} }
const restoredFile = data.metadata?.active_data_file || null; const restoredFile = data.metadata?.active_data_file || null;
const restoredSource = data.metadata?.selected_data_source || ""; const restoredSource = data.metadata?.selected_data_source || "";
@@ -169,12 +187,12 @@ export function ChatInterface() {
setAttachedFile(null); setAttachedFile(null);
} catch (e) { } catch (e) {
console.error("Failed to fetch session messages", e); console.error("Failed to fetch session messages", e);
setMessages([]); setMessagesForSession(activeSessionKey, []);
setActiveDataFile(null); setActiveDataFile(null);
setSelectedDataSource(""); setSelectedDataSource("");
setAttachedFile(null); setAttachedFile(null);
} finally { } finally {
setIsLoading(false); setIsLoadingForSession(activeSessionKey, false);
} }
}; };
@@ -345,11 +363,12 @@ export function ChatInterface() {
}, [messages]); }, [messages]);
const handleForceStop = () => { const handleForceStop = () => {
const controller = abortControllerRef.current; const controller = abortControllersRef.current[activeSessionKey];
if (!controller) return; if (!controller) return;
controller.abort(); controller.abort();
setIsLoading(false); setIsLoadingForSession(activeSessionKey, false);
setMessages((prev) => generatingSessionsRef.current[activeSessionKey] = false;
setMessagesForSession(activeSessionKey, (prev) =>
prev.map((msg) => prev.map((msg) =>
msg.awaitingFirstToken msg.awaitingFirstToken
? { ...msg, awaitingFirstToken: false, content: msg.content || "已中断输出" } ? { ...msg, awaitingFirstToken: false, content: msg.content || "已中断输出" }
@@ -361,8 +380,9 @@ export function ChatInterface() {
const handleSend = async () => { const handleSend = async () => {
if (!input.trim() || isLoading) return; if (!input.trim() || isLoading) return;
const targetSessionKey = activeSessionKey;
const newMessage: Message = { id: Date.now().toString(), role: 'user', content: input }; const newMessage: Message = { id: Date.now().toString(), role: 'user', content: input };
setMessages(prev => [...prev, newMessage]); setMessagesForSession(targetSessionKey, prev => [...prev, newMessage]);
setInput(""); setInput("");
let messagePayload = newMessage.content; let messagePayload = newMessage.content;
@@ -373,12 +393,13 @@ export function ChatInterface() {
} }
const controller = new AbortController(); const controller = new AbortController();
abortControllerRef.current = controller; abortControllersRef.current[targetSessionKey] = controller;
setIsLoading(true); generatingSessionsRef.current[targetSessionKey] = true;
setIsLoadingForSession(targetSessionKey, true);
try { try {
const assistantId = (Date.now() + 1).toString(); const assistantId = (Date.now() + 1).toString();
setMessages(prev => [...prev, { setMessagesForSession(targetSessionKey, prev => [...prev, {
id: assistantId, id: assistantId,
role: "assistant", role: "assistant",
content: "", content: "",
@@ -405,7 +426,7 @@ export function ChatInterface() {
}, },
body: JSON.stringify({ body: JSON.stringify({
message: messagePayload, message: messagePayload,
session_id: activeSessionKey, session_id: targetSessionKey,
model_id: effectiveModelId, model_id: effectiveModelId,
skill_ids: selectedSkillIds, skill_ids: selectedSkillIds,
source, source,
@@ -452,7 +473,7 @@ export function ChatInterface() {
if (payload.type === "delta" && payload.content) { if (payload.type === "delta" && payload.content) {
streamedText = `${streamedText}${payload.content}`; streamedText = `${streamedText}${payload.content}`;
setMessages((prev) => setMessagesForSession(targetSessionKey, (prev) =>
prev.map((msg) => prev.map((msg) =>
msg.id === assistantId ? { ...msg, content: streamedText, awaitingFirstToken: false } : msg msg.id === assistantId ? { ...msg, content: streamedText, awaitingFirstToken: false } : msg
) )
@@ -461,7 +482,7 @@ export function ChatInterface() {
if (payload.type === "final" && payload.content) { if (payload.type === "final" && payload.content) {
streamedText = payload.content; streamedText = payload.content;
setMessages((prev) => setMessagesForSession(targetSessionKey, (prev) =>
prev.map((msg) => prev.map((msg) =>
msg.id === assistantId ? { ...msg, content: payload.content || "", awaitingFirstToken: false, viz: streamedViz ?? msg.viz } : msg msg.id === assistantId ? { ...msg, content: payload.content || "", awaitingFirstToken: false, viz: streamedViz ?? msg.viz } : msg
) )
@@ -474,7 +495,7 @@ export function ChatInterface() {
if (payload.type === "viz") { if (payload.type === "viz") {
streamedViz = buildMessageViz(payload); streamedViz = buildMessageViz(payload);
setMessages((prev) => setMessagesForSession(targetSessionKey, (prev) =>
prev.map((msg) => prev.map((msg) =>
msg.id === assistantId ? { ...msg, viz: streamedViz || undefined } : msg msg.id === assistantId ? { ...msg, viz: streamedViz || undefined } : msg
) )
@@ -494,7 +515,7 @@ export function ChatInterface() {
}; };
}>("/nanobot/chat", { }>("/nanobot/chat", {
message: messagePayload, message: messagePayload,
session_id: activeSessionKey, session_id: targetSessionKey,
model_id: effectiveModelId, model_id: effectiveModelId,
skill_ids: selectedSkillIds, skill_ids: selectedSkillIds,
source, source,
@@ -503,7 +524,7 @@ export function ChatInterface() {
route_mode: "auto", route_mode: "auto",
}, { signal: controller.signal }); }, { signal: controller.signal });
const fallbackViz = fallback.viz ? buildMessageViz(fallback.viz) : undefined; const fallbackViz = fallback.viz ? buildMessageViz(fallback.viz) : undefined;
setMessages((prev) => setMessagesForSession(targetSessionKey, (prev) =>
prev.map((msg) => prev.map((msg) =>
msg.id === assistantId ? { ...msg, content: fallback.response || "暂无回复", awaitingFirstToken: false, viz: fallbackViz } : msg msg.id === assistantId ? { ...msg, content: fallback.response || "暂无回复", awaitingFirstToken: false, viz: fallbackViz } : msg
) )
@@ -511,7 +532,7 @@ export function ChatInterface() {
} }
} catch (error: any) { } catch (error: any) {
if (error?.name === "AbortError" || String(error?.message || "").toLowerCase().includes("aborted")) { if (error?.name === "AbortError" || String(error?.message || "").toLowerCase().includes("aborted")) {
setMessages((prev) => setMessagesForSession(targetSessionKey, (prev) =>
prev.map((msg) => prev.map((msg) =>
msg.awaitingFirstToken msg.awaitingFirstToken
? { ...msg, awaitingFirstToken: false, content: msg.content || "已中断输出" } ? { ...msg, awaitingFirstToken: false, content: msg.content || "已中断输出" }
@@ -520,16 +541,17 @@ export function ChatInterface() {
); );
return; return;
} }
setMessages(prev => [...prev, { setMessagesForSession(targetSessionKey, 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) { if (abortControllersRef.current[targetSessionKey] === controller) {
abortControllerRef.current = null; delete abortControllersRef.current[targetSessionKey];
} }
setIsLoading(false); generatingSessionsRef.current[targetSessionKey] = false;
setIsLoadingForSession(targetSessionKey, false);
window.dispatchEvent(new Event("nanobot:sessions-changed")); window.dispatchEvent(new Event("nanobot:sessions-changed"));
} }
}; };