import { useState, useEffect, useRef } from "react"; import { createSession, resolveApiUrl, stopSessionRun, streamChat, uploadSessionFiles } from "./api"; const DEFAULT_LLM_PROVIDER = (import.meta.env.VITE_DEFAULT_LLM_PROVIDER || "deepseek").trim().toLowerCase(); export default function ChatBox() { const [sessionId, setSessionId] = useState(null); const [llmProvider, setLlmProvider] = useState(DEFAULT_LLM_PROVIDER); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [isStreaming, setIsStreaming] = useState(false); const [isUploading, setIsUploading] = useState(false); const [selectedFiles, setSelectedFiles] = useState([]); const [uploadedFiles, setUploadedFiles] = useState([]); const [activeAssistantIndex, setActiveAssistantIndex] = useState(null); const cancelRef = useRef(null); const fileInputRef = useRef(null); const folderInputRef = useRef(null); const conversationBottomRef = useRef(null); const reasoningBottomRef = useRef(null); // 初始化 session useEffect(() => { setSessionId(null); createSession(llmProvider).then(setSessionId); return () => cancelRef.current?.(); }, [llmProvider]); // 自动滚动到底部 useEffect(() => { conversationBottomRef.current?.scrollIntoView({ behavior: "smooth" }); reasoningBottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const sendMessage = async () => { if (!input.trim() || isStreaming || !sessionId || isUploading) return; let finalMessage = input; if (selectedFiles.length > 0) { const uploadedSignatures = new Set( uploadedFiles.map((f) => `${f.name}::${f.size}::${f.lastModified}`) ); const pendingFiles = selectedFiles.filter( (f) => !uploadedSignatures.has(`${f.name}::${f.size}::${f.lastModified}`) ); if (pendingFiles.length > 0) { setIsUploading(true); try { const uploadRes = await uploadSessionFiles(sessionId, pendingFiles); const uploadedNow = pendingFiles.map((f, idx) => ({ ...(uploadRes?.files?.[idx] || {}), name: f.name, size: f.size, lastModified: f.lastModified, })); setUploadedFiles((prev) => [...prev, ...uploadedNow]); } catch (err) { setMessages((prev) => [ ...prev, { role: "assistant", steps: [{ type: "error", content: String(err) }], done: true }, ]); setIsUploading(false); return; } setIsUploading(false); } finalMessage = `${finalMessage}\n\n[Use uploaded files: ${selectedFiles .map((f) => f.name) .join(", ")}]`; } const userMsg = { role: "user", content: input }; const agentMsg = { role: "assistant", steps: [], // 每个 step: { type, content, lang?, tool? } done: false, }; setMessages((prev) => { const next = [...prev, userMsg, agentMsg]; setActiveAssistantIndex(next.length - 1); return next; }); setIsStreaming(true); setInput(""); const cancel = streamChat(finalMessage, sessionId, (event) => { if (event.type === "heartbeat") return; if (event.type === "done") { setIsStreaming(false); setMessages((prev) => { const msgs = [...prev]; msgs[msgs.length - 1] = { ...msgs[msgs.length - 1], done: true }; return msgs; }); return; } if (event.type === "error") { setIsStreaming(false); setMessages((prev) => { const msgs = [...prev]; const last = msgs[msgs.length - 1]; msgs[msgs.length - 1] = { ...last, done: true, steps: [...last.steps, event], }; return msgs; }); return; } // 把每个事件追加到最后一条 assistant 消息的 steps 中 setMessages((prev) => { const msgs = [...prev]; const last = msgs[msgs.length - 1]; const steps = last.steps || []; msgs[msgs.length - 1] = { ...last, steps: [...steps, event], }; return msgs; }); }, { llmProvider }); cancelRef.current = cancel; }; const stopCurrentRun = async () => { if (sessionId) { try { await stopSessionRun(sessionId); } catch (err) { console.warn("Failed to notify backend stop:", err); } } cancelRef.current?.(); cancelRef.current = null; setIsStreaming(false); setMessages((prev) => { if (prev.length === 0) return prev; const msgs = [...prev]; const last = msgs[msgs.length - 1]; if (last?.role !== "assistant" || last.done) return prev; msgs[msgs.length - 1] = { ...last, done: true, steps: [...last.steps, { type: "error", content: "Run cancelled by user." }], }; return msgs; }); }; const handleKeyDown = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; const appendSelectedFiles = (incomingFiles) => { setSelectedFiles((prev) => { const merged = [...prev]; const seen = new Set(prev.map((f) => `${f.name}::${f.size}::${f.lastModified}`)); for (const file of incomingFiles) { const key = `${file.name}::${file.size}::${file.lastModified}`; if (!seen.has(key)) { merged.push(file); seen.add(key); } } return merged; }); setUploadedFiles([]); }; const onFileChange = (e) => { const files = Array.from(e.target.files || []); appendSelectedFiles(files); e.target.value = ""; }; const onFolderChange = (e) => { const files = Array.from(e.target.files || []); appendSelectedFiles(files); e.target.value = ""; }; const assistantIndexes = messages .map((msg, idx) => ({ msg, idx })) .filter((item) => item.msg.role === "assistant"); const currentAssistant = activeAssistantIndex != null && messages[activeAssistantIndex]?.role === "assistant" ? messages[activeAssistantIndex] : assistantIndexes.length > 0 ? assistantIndexes[assistantIndexes.length - 1].msg : null; return (
Strata Bio OS
Conversation + Structured Reasoning
{sessionId ? `Session ${sessionId.slice(0, 8)}…` : "Initializing…"}
Conversation
{messages.map((msg, i) => ( msg.role === "assistant" && setActiveAssistantIndex(i)} /> ))} {isStreaming && (
)}
Reasoning
{currentAssistant ? ( ) : (
Run a request to see the agent reasoning breakdown.
)}
{`Backend: ${ llmProvider === "openai" ? "OpenAI" : llmProvider === "deepseek" ? "DeepSeek" : llmProvider === "gemini" ? "Gemini" : "Claude" } | `} {selectedFiles.length > 0 ? `Selected ${selectedFiles.length} file(s): ${selectedFiles .map((f) => f.name) .join(", ")}${uploadedFiles.length > 0 ? " (uploaded)" : ""}` : "Optional: upload one or multiple files for this run"}