import { useEffect, useRef, useState } from "react"; import { BrainCircuit, FileUp, Send, Square } from "lucide-react"; import { api, streamChat } from "../lib/api.js"; import MessageBubble from "./MessageBubble.jsx"; const SUGGESTIONS = [ { title: "Search the web", prompt: "Search the web for the latest breakthroughs in AI agents and summarize the top 3." }, { title: "Check the weather", prompt: "What's the weather like in Patiala right now?" }, { title: "Crunch numbers", prompt: "What is (1.07 ** 30) * 25000? Explain what this means for compound interest." }, { title: "Ask your documents", prompt: "Summarize the key points from my uploaded documents." }, ]; // In the public demo a sample report is preloaded, so the prompts point at it // and show retrieval with citations on the very first message. const DEMO_SUGGESTIONS = [ { title: "Query the preloaded report", prompt: "What was Northwind's 2026 revenue and gross margin?", }, { title: "Multi-step retrieval", prompt: "Summarize the autonomy research section and the three key results.", }, { title: "Live weather tool", prompt: "What's the weather in Amsterdam right now?", }, { title: "Precise arithmetic", prompt: "What is (1.07 ** 30) * 25000? Explain what this means for compound interest.", }, ]; export default function ChatView({ appInfo, sessionId, session, ensureSession, onSessionMeta, onOpenDocs, }) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [streaming, setStreaming] = useState(false); const [error, setError] = useState(""); const [model, setModel] = useState(""); const abortRef = useRef(null); const scrollRef = useRef(null); const textareaRef = useRef(null); // Session ids this view created itself while sending. Reloading history for // those would wipe the reply that is still streaming into local state. const selfCreatedRef = useRef(null); const effectiveModel = model || session?.model || appInfo?.default_model || ""; const suggestions = appInfo?.demo_mode ? DEMO_SUGGESTIONS : SUGGESTIONS; useEffect(() => { if (!sessionId) { setMessages([]); return; } if (selfCreatedRef.current === sessionId) { // Skip exactly once, for the transition that created this session. selfCreatedRef.current = null; return; } api(`/api/sessions/${sessionId}/messages`) .then((list) => setMessages( list.map((m) => ({ id: m.id, role: m.role, content: m.content, tools: safeParse(m.tool_calls_json) || [], citations: safeParse(m.citations_json) || [], meta: m.role === "assistant" ? { model: m.model, input_tokens: m.input_tokens, output_tokens: m.output_tokens, cost_usd: m.cost_usd, latency_ms: m.latency_ms, } : null, })) ) ) .catch((err) => setError(err.message)); }, [sessionId]); useEffect(() => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, [messages, streaming]); function safeParse(json) { try { return json ? JSON.parse(json) : null; } catch { return null; } } function autoResize() { const el = textareaRef.current; if (!el) return; el.style.height = "auto"; el.style.height = Math.min(el.scrollHeight, 200) + "px"; } async function send(text) { const content = (text ?? input).trim(); if (!content || streaming) return; setError(""); setInput(""); requestAnimationFrame(autoResize); setStreaming(true); setMessages((prev) => [ ...prev, { id: `u-${Date.now()}`, role: "user", content, tools: [], citations: [] }, { id: `a-${Date.now()}`, role: "assistant", content: "", tools: [], citations: [], meta: null, live: true, }, ]); const controller = new AbortController(); abortRef.current = controller; try { const id = await ensureSession(); selfCreatedRef.current = id; await streamChat( id, { content, model: effectiveModel || undefined }, { onEvent: (event) => { setMessages((prev) => { const next = [...prev]; const last = { ...next[next.length - 1] }; if (event.type === "token") { last.content += event.text; } else if (event.type === "tool_start") { last.tools = [ ...last.tools, { name: event.name, arguments: event.arguments, status: "running" }, ]; } else if (event.type === "tool_end") { last.tools = last.tools.map((tool) => tool.name === event.name && tool.status === "running" ? { ...tool, status: "done", result_preview: event.result_preview } : tool ); } else if (event.type === "citations") { last.citations = event.citations; } else if (event.type === "usage") { last.meta = event; } else if (event.type === "title") { onSessionMeta(id, { title: event.title }); } else if (event.type === "done") { last.live = false; } else if (event.type === "error") { setError(event.message); last.live = false; } next[next.length - 1] = last; return next; }); }, }, controller.signal ); } catch (err) { if (err.name !== "AbortError") setError(err.message); } finally { setStreaming(false); abortRef.current = null; setMessages((prev) => prev.map((m) => (m.live ? { ...m, live: false } : m)) ); } } function stop() { abortRef.current?.abort(); } const showEmpty = messages.length === 0; return ( <>
I'm Synapse. I can search the web, check the weather, do precise math, and answer questions about your uploaded documents, with sources cited.