diff --git a/frontend/src/components/ChatInterface.tsx b/frontend/src/components/ChatInterface.tsx index 234b8b5..155c889 100644 --- a/frontend/src/components/ChatInterface.tsx +++ b/frontend/src/components/ChatInterface.tsx @@ -69,17 +69,37 @@ interface SessionData { } export function ChatInterface() { - const [messages, setMessages] = useState([]); + const [messagesBySession, setMessagesBySession] = useState>({}); const [input, setInput] = useState(""); const [selectedDataSource, setSelectedDataSource] = useState(""); const [availableSkills, setAvailableSkills] = useState([]); const [selectedSkillIds, setSelectedSkillIds] = useState([]); const [isMenuOpen, setIsMenuOpen] = useState(false); - const [isLoading, setIsLoading] = useState(false); const scrollRef = useRef(null); const location = useLocation(); const { currentProject } = useProjectStore(); + const setMessagesForSession = (sessionKey: string, updater: React.SetStateAction) => { + 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>({}); + const isLoading = loadingBySession[activeSessionKey] || false; + + const generatingSessionsRef = useRef>({}); + const abortControllersRef = useRef>({}); + // Model selection state const [models, setModels] = useState([]); const [selectedModelId, setSelectedModelId] = useState(""); @@ -88,16 +108,11 @@ export function ChatInterface() { // Data Source selection state 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 const [attachedFile, setAttachedFile] = useState(null); const [activeDataFile, setActiveDataFile] = useState(null); const [isUploading, setIsUploading] = useState(false); const fileInputRef = useRef(null); - const abortControllerRef = useRef(null); useEffect(() => { fetchModels(); @@ -147,7 +162,10 @@ export function ChatInterface() { useEffect(() => { 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([]); try { const data = await api.get(`/nanobot/sessions/${activeSessionKey}`); @@ -158,9 +176,9 @@ export function ChatInterface() { content: m.content, viz: m.viz ? buildMessageViz(m.viz) : undefined, })); - setMessages(formattedMessages); + setMessagesForSession(activeSessionKey, formattedMessages); } else { - setMessages([]); + setMessagesForSession(activeSessionKey, []); } const restoredFile = data.metadata?.active_data_file || null; const restoredSource = data.metadata?.selected_data_source || ""; @@ -169,12 +187,12 @@ export function ChatInterface() { setAttachedFile(null); } catch (e) { console.error("Failed to fetch session messages", e); - setMessages([]); + setMessagesForSession(activeSessionKey, []); setActiveDataFile(null); setSelectedDataSource(""); setAttachedFile(null); } finally { - setIsLoading(false); + setIsLoadingForSession(activeSessionKey, false); } }; @@ -345,11 +363,12 @@ export function ChatInterface() { }, [messages]); const handleForceStop = () => { - const controller = abortControllerRef.current; + const controller = abortControllersRef.current[activeSessionKey]; if (!controller) return; controller.abort(); - setIsLoading(false); - setMessages((prev) => + setIsLoadingForSession(activeSessionKey, false); + generatingSessionsRef.current[activeSessionKey] = false; + setMessagesForSession(activeSessionKey, (prev) => prev.map((msg) => msg.awaitingFirstToken ? { ...msg, awaitingFirstToken: false, content: msg.content || "已中断输出" } @@ -361,8 +380,9 @@ export function ChatInterface() { const handleSend = async () => { if (!input.trim() || isLoading) return; + const targetSessionKey = activeSessionKey; const newMessage: Message = { id: Date.now().toString(), role: 'user', content: input }; - setMessages(prev => [...prev, newMessage]); + setMessagesForSession(targetSessionKey, prev => [...prev, newMessage]); setInput(""); let messagePayload = newMessage.content; @@ -373,12 +393,13 @@ export function ChatInterface() { } const controller = new AbortController(); - abortControllerRef.current = controller; - setIsLoading(true); + abortControllersRef.current[targetSessionKey] = controller; + generatingSessionsRef.current[targetSessionKey] = true; + setIsLoadingForSession(targetSessionKey, true); try { const assistantId = (Date.now() + 1).toString(); - setMessages(prev => [...prev, { + setMessagesForSession(targetSessionKey, prev => [...prev, { id: assistantId, role: "assistant", content: "", @@ -405,7 +426,7 @@ export function ChatInterface() { }, body: JSON.stringify({ message: messagePayload, - session_id: activeSessionKey, + session_id: targetSessionKey, model_id: effectiveModelId, skill_ids: selectedSkillIds, source, @@ -452,7 +473,7 @@ export function ChatInterface() { if (payload.type === "delta" && payload.content) { streamedText = `${streamedText}${payload.content}`; - setMessages((prev) => + setMessagesForSession(targetSessionKey, (prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, content: streamedText, awaitingFirstToken: false } : msg ) @@ -461,7 +482,7 @@ export function ChatInterface() { if (payload.type === "final" && payload.content) { streamedText = payload.content; - setMessages((prev) => + setMessagesForSession(targetSessionKey, (prev) => prev.map((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") { streamedViz = buildMessageViz(payload); - setMessages((prev) => + setMessagesForSession(targetSessionKey, (prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, viz: streamedViz || undefined } : msg ) @@ -494,7 +515,7 @@ export function ChatInterface() { }; }>("/nanobot/chat", { message: messagePayload, - session_id: activeSessionKey, + session_id: targetSessionKey, model_id: effectiveModelId, skill_ids: selectedSkillIds, source, @@ -503,7 +524,7 @@ export function ChatInterface() { route_mode: "auto", }, { signal: controller.signal }); const fallbackViz = fallback.viz ? buildMessageViz(fallback.viz) : undefined; - setMessages((prev) => + setMessagesForSession(targetSessionKey, (prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, content: fallback.response || "暂无回复", awaitingFirstToken: false, viz: fallbackViz } : msg ) @@ -511,7 +532,7 @@ export function ChatInterface() { } } catch (error: any) { if (error?.name === "AbortError" || String(error?.message || "").toLowerCase().includes("aborted")) { - setMessages((prev) => + setMessagesForSession(targetSessionKey, (prev) => prev.map((msg) => msg.awaitingFirstToken ? { ...msg, awaitingFirstToken: false, content: msg.content || "已中断输出" } @@ -520,16 +541,17 @@ export function ChatInterface() { ); return; } - setMessages(prev => [...prev, { + setMessagesForSession(targetSessionKey, prev => [...prev, { id: (Date.now() + 1).toString(), role: 'assistant', content: `Sorry, something went wrong: ${error.message}` }]); } finally { - if (abortControllerRef.current === controller) { - abortControllerRef.current = null; + if (abortControllersRef.current[targetSessionKey] === controller) { + delete abortControllersRef.current[targetSessionKey]; } - setIsLoading(false); + generatingSessionsRef.current[targetSessionKey] = false; + setIsLoadingForSession(targetSessionKey, false); window.dispatchEvent(new Event("nanobot:sessions-changed")); } };