import React, { useRef, useState, useEffect } from "react";
import { Send, Sparkles, CornerDownRight, Loader2, ShieldCheck } from "lucide-react";
import { C, FD, FM } from "../theme.js";
import { askTrace } from "../useAnalysis.js";
// "Ask the trace" — a chat bound to ONE session. The local server does
// deterministic retrieval over the engine output, the LOCAL model writes the
// answer (grounded, suggest-never-assert, cites turns). Every answer can open
// the cited turn in the middle pane (Mode B). Nothing leaves the machine.
const SUGGESTIONS = [
"Why was this session so expensive?",
"What drove turn 9?",
"Were there any errors or retries?",
"Where did the agent act on its own?",
];
export default function ChatPanel({ sessionPath, llama, onFocusTurn, sessionLabel }) {
const [msgs, setMsgs] = useState([]);
const [q, setQ] = useState("");
const [busy, setBusy] = useState(false);
const scroller = useRef(null);
useEffect(() => {
// reset the conversation when the session changes — answers are session-bound
setMsgs([]);
}, [sessionPath]);
useEffect(() => {
if (scroller.current) scroller.current.scrollTop = scroller.current.scrollHeight;
}, [msgs, busy]);
async function ask(text) {
const question = (text ?? q).trim();
if (!question || busy) return;
setQ("");
setMsgs((m) => [...m, { role: "user", text: question }]);
setBusy(true);
try {
const res = await askTrace(question, sessionPath);
setMsgs((m) => [...m, { role: "assistant", ...res }]);
if (res.focusTurn != null) onFocusTurn(res.focusTurn, res.focusTool ?? null);
} catch (e) {
setMsgs((m) => [...m, { role: "assistant", answer: `Couldn't answer: ${String(e)}`, citedTurns: [], error: true }]);
} finally {
setBusy(false);
}
}
return (
{/* header */}
Ask Her
{llama ? "LOCAL MODEL" : "MODEL OFF"}
Her reads ONLY this session's trace · cites turns · suggests, never asserts
{/* messages */}
{msgs.length === 0 && (
Try asking:
{SUGGESTIONS.map((s) => (
ask(s)} style={{ cursor: "pointer", fontFamily: FM, fontSize: 12, color: C.text2, border: `1px solid ${C.borderSoft}`, borderRadius: 8, padding: "9px 11px", marginBottom: 7, display: "flex", gap: 7, alignItems: "center" }}>
{s}
))}
)}
{msgs.map((m, i) =>
m.role === "user" ? (
) : (
{m.error ? "ERROR" : `GENERATED${m.model ? " · " + m.model.split("/").pop().slice(0, 22) : ""} · reads the trace`}
{m.answer}
{(() => {
const chips = (m.citations && m.citations.length)
? m.citations
: (m.citedTurns || []).map((tn) => ({ turn: tn, tool: null, label: `turn ${tn}` }));
return chips.length > 0 ? (
{chips.map((c) => (
onFocusTurn(c.turn, c.tool)} title={c.tool != null ? "opens the turn and highlights this tool" : "opens this turn"} style={{ cursor: "pointer", fontFamily: FM, fontSize: 10.5, color: C.orange, background: C.black, border: `1px solid ${C.orangeBd}`, borderRadius: 6, padding: "2px 8px", display: "inline-flex", alignItems: "center", gap: 4 }}>
{c.label} →
))}
) : null;
})()}
)
)}
{busy && (
retrieving from the trace + asking the local model…
)}
{/* input */}
setQ(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && ask()}
placeholder="why did this happen?"
style={{ flex: 1, background: "transparent", border: "none", outline: "none", color: C.text, fontFamily: FM, fontSize: 12.5 }}
/>
ask()} className="lift" style={{ cursor: busy ? "default" : "pointer", width: 30, height: 30, borderRadius: 7, background: q.trim() && !busy ? C.orange : C.elevated, display: "flex", alignItems: "center", justifyContent: "center" }}>
);
}