NeuroScope / frontend /src /components /CircuitQueryBox.js
Gaurav711's picture
feat: Migrate NeuroScope v2 to Firebase and Google Gemini Explainer
ca6c0e5
Raw
History Blame Contribute Delete
6.81 kB
import { useState, useRef, useEffect } from "react";
import { Send, Sparkles } from "lucide-react";
import api from "@/lib/api";
const SUGGESTIONS = [
"Why is the hallucination risk highest at step 1?",
"Did internal state at step 1 causally affect step 3?",
"Which features drift the most across the trajectory?",
"Summarize the cross-step patch matrix findings.",
];
export default function CircuitQueryBox({ runId, trajectoryReady }) {
const [msgs, setMsgs] = useState([]);
const [val, setVal] = useState("");
const [busy, setBusy] = useState(false);
const endRef = useRef(null);
useEffect(() => {
if (!runId) return;
api.listQueries(runId)
.then((d) => {
const ordered = (d.queries || []).slice().reverse();
setMsgs(
ordered.flatMap((q) => [
{ role: "user", text: q.query },
{ role: "assistant", text: q.answer },
]),
);
})
.catch(() => {});
}, [runId]);
useEffect(() => {
endRef.current?.scrollIntoView({ behavior: "smooth" });
}, [msgs.length]);
async function send(text) {
const t = (text ?? val).trim();
if (!t || !trajectoryReady) return;
setMsgs((m) => [...m, { role: "user", text: t }]);
setVal("");
setBusy(true);
try {
const res = await api.query(runId, { query: t });
setMsgs((m) => [...m, { role: "assistant", text: res.answer }]);
} catch (e) {
setMsgs((m) => [
...m,
{ role: "assistant", text: `[error: ${e.message}]` },
]);
} finally {
setBusy(false);
}
}
return (
<div
className="ns-card flex h-[400px] flex-col"
data-testid="nl-circuit-query-box"
>
<header
className="flex items-center gap-2 px-4 pt-3.5 pb-2"
style={{ borderBottom: "1px solid var(--ns-border-subtle)" }}
>
<Sparkles size={14} color="var(--ns-accent)" />
<span className="ns-section-title">NL circuit query</span>
<span className="font-mono text-[10px] text-[color:var(--ns-fg-muted)]">
grounded in run artifacts • claude-sonnet-4-5
</span>
</header>
<div className="flex-1 overflow-y-auto px-4 py-3 text-[12px] leading-6">
{msgs.length === 0 ? (
<div className="space-y-2">
<div className="text-[11px] text-[color:var(--ns-fg-muted)]">
Ask any question about the trajectory. Suggestions:
</div>
{SUGGESTIONS.map((s, i) => (
<button
key={i}
type="button"
onClick={() => send(s)}
className="block w-full rounded-md border px-2.5 py-1.5 text-left font-mono text-[11px] text-[color:var(--ns-fg-secondary)] hover:bg-[color:var(--ns-bg-surface-2)]"
style={{ borderColor: "var(--ns-border-subtle)" }}
data-testid={`nl-query-suggestion-${i}`}
>
{s}
</button>
))}
</div>
) : (
msgs.map((m, i) => (
<div
key={i}
className="mb-3"
data-testid="nl-circuit-query-message"
>
<div
className="mb-0.5 font-mono text-[10px] uppercase tracking-wider"
style={{
color:
m.role === "user"
? "var(--ns-accent)"
: "var(--ns-mint)",
}}
>
{m.role === "user" ? "you" : "neuroscope"}
</div>
<div
className="whitespace-pre-wrap text-[color:var(--ns-fg-primary)]"
style={{ fontSize: 12 }}
>
{m.text}
</div>
</div>
))
)}
{busy && (
<div className="font-mono text-[10px] text-[color:var(--ns-fg-muted)]">
thinking… (model + sae + patch context being summarized for the LLM)
</div>
)}
<div ref={endRef} />
</div>
<div
className="flex items-center gap-2 px-3 py-2"
style={{ borderTop: "1px solid var(--ns-border-subtle)" }}
>
<input
value={val}
onChange={(e) => setVal(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && send()}
placeholder={
trajectoryReady
? "e.g. why is risk highest at step 3?"
: "trajectory still running…"
}
disabled={!trajectoryReady || busy}
className="flex-1 rounded-md border bg-transparent px-2 py-1.5 font-mono text-[12px] text-[color:var(--ns-fg-primary)] placeholder:text-[color:var(--ns-fg-faint)] focus:outline-none"
style={{ borderColor: "var(--ns-border-subtle)" }}
data-testid="nl-circuit-query-input"
/>
<button
type="button"
onClick={() => send()}
disabled={!trajectoryReady || busy || !val.trim()}
className="flex h-7 items-center gap-1 rounded-md px-2.5 text-[11px]"
style={{
background: trajectoryReady && val.trim() ? "var(--ns-accent)" : "var(--ns-bg-surface-2)",
color: trajectoryReady && val.trim() ? "var(--ns-bg-canvas)" : "var(--ns-fg-muted)",
}}
data-testid="nl-circuit-query-send-button"
>
<Send size={11} /> send
</button>
</div>
</div>
);
}