// SEIS+ Internals — AI Chat panel (inline or sidebar). function ChatPanel({ open, onToggle, user, inline, onRunCommand, onYoloRun, instanceId }) { const [models, setModels] = React.useState([]); const [model, setModel] = React.useState("claude-sonnet-4.6"); const [messages, setMessages] = React.useState([]); const [input, setInput] = React.useState(""); const [loading, setLoading] = React.useState(false); const [streaming, setStreaming] = React.useState(""); const [yolo, setYolo] = React.useState(false); const [yoloMsg, setYoloMsg] = React.useState(""); const [chats, setChats] = React.useState([]); const [activeId, setActiveId] = React.useState(null); const bottomRef = React.useRef(null); const yoloIter = React.useRef(0); const yoloAbort = React.useRef(false); const saveTimer = React.useRef(null); // Force-save on unmount/close React.useEffect(() => { return () => { clearTimeout(saveTimer.current); if (activeId && messages.length > 0) { fetch(`/api/chat/sessions/${activeId}`, { method: "PUT", credentials: "include", keepalive: true, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages }), }).catch(() => {}); } }; }, [activeId, messages]); const pendingSave = React.useRef(null); // Load chats from API on mount / instance change React.useEffect(() => { if (!open || !instanceId) return; fetch(`/api/chat/sessions?instance_id=${encodeURIComponent(instanceId)}`, { credentials: "include" }) .then(r => r.json()) .then(list => { setChats(list); if (list.length > 0) { const last = list[list.length - 1]; setActiveId(last.id); fetch(`/api/chat/sessions/${last.id}`, { credentials: "include" }) .then(r => r.json()) .then(d => { setMessages(d.messages || []); if (d.model) setModel(d.model); }) .catch(() => setMessages([])); } else { setActiveId(null); setMessages([]); } }) .catch(() => { setChats([]); setMessages([]); }); }, [open, instanceId]); // Auto-save messages to DB (debounced) React.useEffect(() => { if (!activeId || messages.length === 0) return; clearTimeout(saveTimer.current); saveTimer.current = setTimeout(async () => { try { const userMsgs = messages.filter(m => m.role === "user"); const title = userMsgs.length === 1 ? userMsgs[0].content.slice(0, 40) : ""; const body = { messages }; if (title) body.title = title; await fetch(`/api/chat/sessions/${activeId}`, { method: "PUT", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (title) setChats(prev => prev.map(c => c.id === activeId ? { ...c, title } : c)); } catch {} }, 1500); }, [messages, activeId]); // New chat const newChat = async () => { try { const r = await fetch("/api/chat/sessions", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ instanceId, title: `Chat ${chats.length + 1}`, model, messages: [] }), }); const d = await r.json(); setChats(prev => [...prev, { id: d.id, title: `Chat ${chats.length + 1}`, model, instanceId }]); setActiveId(d.id); setMessages([]); setInput(""); } catch {} }; // Switch chat — load full messages const switchChat = async (id) => { if (id === activeId) return; try { const r = await fetch(`/api/chat/sessions/${id}`, { credentials: "include" }); if (!r.ok) return; const d = await r.json(); setActiveId(d.id); setMessages(d.messages || []); if (d.model) setModel(d.model); setInput(""); } catch {} }; React.useEffect(() => { if (!open) return; fetch("/api/chat/models", { credentials: "include" }) .then(r => r.json()) .then(m => Array.isArray(m) && m.length && setModels(m)) .catch(() => {}); }, [open]); React.useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, streaming, yoloMsg]); // ── Extract bash blocks ── const extractBash = (text) => { const blocks = []; const re = /```(?:bash|sh|shell)\s*\n([\s\S]*?)```/g; let m; while ((m = re.exec(text)) !== null) { blocks.push(m[1].trim()); } return blocks; }; // ── Agentic YOLO loop ── const yoloLoop = async (text) => { if (!onYoloRun) return; const blocks = extractBash(text); if (blocks.length === 0 || yoloAbort.current) return; yoloIter.current++; const iter = yoloIter.current; setYoloMsg(""); const allOutputs = []; for (const cmd of blocks) { if (yoloAbort.current || yoloIter.current !== iter) return; setYoloMsg(`▶▶ YOLO running: ${cmd.slice(0, 60)}${cmd.length > 60 ? "..." : ""}`); try { const out = await onYoloRun(cmd); allOutputs.push(`> ${cmd}\n${out || "(no output)"}`); } catch (e) { allOutputs.push(`> ${cmd}\nERROR: ${e.message}`); } } setYoloMsg(""); if (yoloAbort.current || yoloIter.current !== iter || allOutputs.length === 0) return; // Send output back as user message const context = `Command output (iteration ${Math.floor(iter)}):\n${allOutputs.join("\n\n")}\n\nContinue or suggest next steps.`; setMessages(prev => [...prev, { role: "user", content: context }]); setLoading(true); setStreaming(""); try { const all = [...messages, { role: "user", content: context }].map(m => ({ role: m.role, content: m.content })); const r = await fetch("/api/chat", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model, messages: all, stream: true, instanceId }), }); if (!r.ok) { const err = await r.json().catch(() => ({})); setMessages(prev => [...prev, { role: "assistant", content: `Error: ${err.detail || r.status}` }]); setLoading(false); return; } const reader = r.body.getReader(); const decoder = new TextDecoder(); let full = ""; let buffer = ""; let errorMsg = null; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { const s = line.trim(); if (!s || !s.startsWith("data:")) continue; const json = s.slice(5).trim(); if (json === "[DONE]") continue; try { const parsed = JSON.parse(json); const delta = parsed.choices?.[0]?.delta; const content = delta?.content; if (content != null && content !== "") { full += content; setStreaming(full); } if (parsed.error) errorMsg = parsed.error.message || parsed.error; } catch {} } } if (errorMsg) { setMessages(prev => [...prev, { role: "assistant", content: `Error: ${errorMsg}` }]); } else { const response = full || "(no response — model may be unavailable)"; setMessages(prev => [...prev, { role: "assistant", content: response }]); // Continue YOLO loop if still enabled (max 3 iterations, 2s delay) if (yolo && !yoloAbort.current && full && onYoloRun && iter < 3) { await new Promise(r => setTimeout(r, 2000)); setTimeout(() => yoloLoop(response), 200); } } } catch (e) { setMessages(prev => [...prev, { role: "assistant", content: `Network error: ${e.message}` }]); } setStreaming(""); setLoading(false); }; // ── Trigger YOLO when new assistant message arrives ── React.useEffect(() => { if (!yolo || !onYoloRun || loading) return; const last = messages[messages.length - 1]; if (!last || last.role !== "assistant" || last._yoloRan) return; // Mark to avoid re-triggering messages[messages.length - 1]._yoloRan = true; setTimeout(() => yoloLoop(last.content), 400); }, [messages, yolo, loading]); // ── Toggle YOLO off when panel closes ── React.useEffect(() => { if (!open) { yoloAbort.current = true; setYolo(false); setYoloMsg(""); } else yoloAbort.current = false; }, [open]); // ── Send message ── const send = async () => { const text = input.trim(); if (!text || loading) return; setInput(""); const userMsg = { role: "user", content: text }; const updated = [...messages, userMsg]; setMessages(updated); setLoading(true); setStreaming(""); yoloIter.current++; try { const all = updated.map(m => ({ role: m.role, content: m.content })); const r = await fetch("/api/chat", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model, messages: all, stream: true, instanceId }), }); if (!r.ok) { const err = await r.json().catch(() => ({})); setMessages(prev => [...prev, { role: "assistant", content: `Error: ${err.detail || r.status}` }]); setLoading(false); return; } const reader = r.body.getReader(); const decoder = new TextDecoder(); let full = ""; let buffer = ""; let errorMsg = null; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { const s = line.trim(); if (!s || !s.startsWith("data:")) continue; const json = s.slice(5).trim(); if (json === "[DONE]") continue; try { const parsed = JSON.parse(json); const delta = parsed.choices?.[0]?.delta; const content = delta?.content; if (content != null && content !== "") { full += content; setStreaming(full); } if (parsed.error) errorMsg = parsed.error.message || parsed.error; } catch {} } } if (errorMsg) { setMessages(prev => [...prev, { role: "assistant", content: `Error: ${errorMsg}` }]); } else { const resp = full || "(no response — model may be unavailable)"; setMessages(prev => [...prev, { role: "assistant", content: resp }]); } } catch (e) { setMessages(prev => [...prev, { role: "assistant", content: `Network error: ${e.message}` }]); } setStreaming(""); setLoading(false); }; // ── Export chat ── const exportChat = () => { const text = messages.map(m => `### ${m.role === "user" ? `@${user?.handle || "you"}` : "ASSISTANT"}\n${m.content}`).join("\n\n---\n\n"); const blob = new Blob([text], { type: "text/markdown" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `chat-${new Date().toISOString().slice(0,10)}.md`; a.click(); URL.revokeObjectURL(url); }; const handleKey = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }; if (!open) return null; const renderContent = (text) => { const parts = text.split(/(```[\s\S]*?```)/g); return parts.map((p, i) => { if (p.startsWith("```")) { const m = p.match(/^```(\w*)\n?([\s\S]*?)```$/); const lang = (m?.[1] || "").toLowerCase(); const code = m?.[2] || p.replace(/^```\w*\n?/, "").replace(/```$/, ""); const isBash = lang === "bash" || lang === "sh" || lang === "shell"; return (
{code}
{t.slice(1, -1)};
return {t};
});
};
const header = (