LAYER / src /components /ExpertChat.tsx
Claude
Fix pasted-source bug: actually generate a pipeline instead of relabeling the demo
9dc9b9a unverified
Raw
History Blame Contribute Delete
4.28 kB
"use client";
import { useEffect, useState } from "react";
import { Send, Sparkles } from "lucide-react";
type ChatMessage = { role: "user" | "assistant"; content: string };
const SUGGESTIONS = [
"Why is the LLM stage tiered B instead of A?",
"Walk me through the End-of-Speech Pop risk on Avatar.",
"What would it take to fix the Failure 3 cascade?",
];
export default function ExpertChat({
selectedNodeId,
seedQuestion,
onSeedConsumed,
}: {
selectedNodeId: string | null;
seedQuestion?: string | null;
onSeedConsumed?: () => void;
}) {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function send(text?: string) {
const content = (text ?? input).trim();
if (!content || loading) return;
const next = [...messages, { role: "user" as const, content }];
setMessages(next);
setInput("");
setLoading(true);
setError(null);
try {
const res = await fetch("/api/expert", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: next, selectedNodeId }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Request failed");
setMessages([...next, { role: "assistant", content: data.text }]);
} catch (err) {
setError(err instanceof Error ? err.message : "Something went wrong.");
} finally {
setLoading(false);
}
}
useEffect(() => {
if (!seedQuestion) return;
send(seedQuestion);
onSeedConsumed?.();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [seedQuestion]);
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto p-4 flex flex-col gap-3">
{messages.length === 0 && (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 text-sm text-[var(--muted)]">
<Sparkles size={14} className="text-[var(--accent-teal)]" />
Samantha audits this pipeline against 5 failure modes: Uncanny Valley,
End-of-Speech Pop, Cascaded Latency, VRAM Death, and the VASA-1 Problem.
</div>
<div className="flex flex-col gap-1.5 mt-2">
{SUGGESTIONS.map((s) => (
<button
key={s}
onClick={() => send(s)}
className="text-left text-xs text-[var(--muted)] border border-[var(--panel-border)] rounded-lg px-2.5 py-2 hover:text-white hover:border-white/20 transition"
>
{s}
</button>
))}
</div>
</div>
)}
{messages.map((m, i) => (
<div
key={i}
className={`text-sm rounded-lg px-3 py-2 max-w-[92%] whitespace-pre-wrap ${
m.role === "user"
? "self-end bg-[var(--accent-blue)]/20 border border-[var(--accent-blue)]/30"
: "self-start glass-panel"
}`}
>
{m.content}
</div>
))}
{loading && (
<div className="self-start text-xs text-[var(--muted)] px-3">Thinking…</div>
)}
{error && (
<div className="self-start text-xs text-red-300/90 px-3">{error}</div>
)}
</div>
<div className="p-3 border-t border-[var(--panel-border)]">
<div className="glass-pill rounded-xl px-3 py-2 flex items-center gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && send()}
placeholder="Ask Samantha…"
className="flex-1 bg-transparent outline-none text-sm placeholder:text-[var(--muted)]"
/>
<button
onClick={() => send()}
disabled={loading}
className="accent-send size-7 rounded-full flex items-center justify-center text-white disabled:opacity-50"
aria-label="Send"
>
<Send size={13} />
</button>
</div>
</div>
</div>
);
}