ForgeSight / frontend /src /components /AgentTranscript.jsx
rasAli02's picture
πŸ› Fix: Resolve backend 500 on Vercel by implementing FastAPI and syncing dependencies
1035089
import { useEffect, useState } from "react";
import { Eye, Stethoscope, Wrench, FileText, Share2, CheckCircle2, AlertTriangle, XCircle, WifiOff, Twitter, Linkedin } from "lucide-react";
const ICONS = {
inspector: Eye,
diagnostician: Stethoscope,
action: Wrench,
reporter: FileText,
social: Share2,
};
const VERDICT_CONFIG = {
pass: { label: "PASS", color: "#10B981", Icon: CheckCircle2 },
warn: { label: "WARN", color: "#F59E0B", Icon: AlertTriangle },
fail: { label: "FAIL", color: "#ED1C24", Icon: XCircle },
};
// ── Renderers β€” one per agent role ─────────────────────────────────────────
function InspectorOutput({ parsed, isMock }) {
const vc = VERDICT_CONFIG[parsed?.verdict] || VERDICT_CONFIG.warn;
const defects = parsed?.defects || [];
return (
<div className="space-y-4">
{isMock && (
<div className="flex items-center gap-2 px-3 py-2 bg-yellow-500/10 border border-yellow-500/30 rounded text-yellow-400 font-mono text-xs">
<WifiOff className="w-3 h-3 shrink-0" />
AMD server offline β€” showing demo data. Start the vLLM server for live inference.
</div>
)}
{/* Verdict banner */}
<div className="flex items-center gap-3 p-4 border rounded" style={{ borderColor: `${vc.color}44`, background: `${vc.color}0d` }}>
<vc.Icon className="w-5 h-5" style={{ color: vc.color }} />
<div>
<div className="font-mono text-xs text-zinc-400 mb-0.5">VERDICT</div>
<div className="font-display font-black text-xl tracking-tight" style={{ color: vc.color }}>{vc.label}</div>
</div>
<div className="ml-auto text-right">
<div className="font-mono text-xs text-zinc-400 mb-0.5">CONFIDENCE</div>
<div className="font-display font-black text-xl">{Math.round((parsed?.confidence || 0) * 100)}%</div>
</div>
</div>
{/* Observation */}
{parsed?.observation && (
<div>
<div className="font-mono text-[10px] text-zinc-500 uppercase tracking-wider mb-1.5">Observation</div>
<p className="text-sm text-zinc-300 leading-relaxed">{parsed.observation.replace("[LOCAL MOCK β€” AMD server offline]", "").trim()}</p>
</div>
)}
{/* Defects */}
{defects.length > 0 && (
<div>
<div className="font-mono text-[10px] text-zinc-500 uppercase tracking-wider mb-2">Detected Defects ({defects.length})</div>
<div className="space-y-2">
{defects.map((d, i) => (
<div key={i} className="flex gap-3 p-3 border border-white/10 bg-white/[0.02] rounded">
<div className="shrink-0 w-1.5 rounded-full self-stretch" style={{ background: d.severity === "high" ? "#ED1C24" : d.severity === "medium" ? "#F59E0B" : "#71717A" }} />
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap mb-0.5">
<span className="font-mono text-xs font-bold text-white">{d.type}</span>
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded border" style={{ color: d.severity === "high" ? "#ED1C24" : d.severity === "medium" ? "#F59E0B" : "#71717A", borderColor: "currentColor", background: "transparent" }}>{d.severity?.toUpperCase()}</span>
{d.location && <span className="font-mono text-[10px] text-zinc-500">@ {d.location}</span>}
</div>
<p className="text-xs text-zinc-400">{d.description}</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
function DiagnosticianOutput({ parsed }) {
const factors = parsed?.contributing_factors || [];
return (
<div className="space-y-3">
{parsed?.probable_cause && (
<div>
<div className="font-mono text-[10px] text-zinc-500 uppercase tracking-wider mb-1.5">Root Cause</div>
<p className="text-sm text-zinc-200 leading-relaxed font-medium">{parsed.probable_cause.replace("[LOCAL MOCK]", "").trim()}</p>
</div>
)}
{parsed?.affected_process_step && (
<div>
<div className="font-mono text-[10px] text-zinc-500 uppercase tracking-wider mb-1.5">Affected Process Step</div>
<span className="font-mono text-xs px-2 py-1 border border-white/20 text-zinc-300">{parsed.affected_process_step}</span>
</div>
)}
{factors.length > 0 && (
<div>
<div className="font-mono text-[10px] text-zinc-500 uppercase tracking-wider mb-2">Contributing Factors</div>
<ul className="space-y-1">
{factors.map((f, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-zinc-300">
<span className="text-[#ED1C24] font-mono text-xs mt-0.5 shrink-0">β†’</span>
{f}
</li>
))}
</ul>
</div>
)}
</div>
);
}
function ActionOutput({ parsed }) {
const steps = parsed?.steps || [];
const tools = parsed?.parts_or_tools || [];
const priorityColor = { P0: "#ED1C24", P1: "#F97316", P2: "#F59E0B", P3: "#71717A" }[parsed?.priority] || "#71717A";
return (
<div className="space-y-3">
<div className="flex gap-4 flex-wrap">
{parsed?.priority && (
<div>
<div className="font-mono text-[10px] text-zinc-500 uppercase tracking-wider mb-1">Priority</div>
<span className="font-display font-black text-xl" style={{ color: priorityColor }}>{parsed.priority}</span>
</div>
)}
{parsed?.assignee_role && (
<div>
<div className="font-mono text-[10px] text-zinc-500 uppercase tracking-wider mb-1">Assign To</div>
<span className="font-mono text-sm text-zinc-200">{parsed.assignee_role}</span>
</div>
)}
{parsed?.estimated_minutes && (
<div>
<div className="font-mono text-[10px] text-zinc-500 uppercase tracking-wider mb-1">Est. Time</div>
<span className="font-mono text-sm text-zinc-200">{parsed.estimated_minutes} min</span>
</div>
)}
</div>
{steps.length > 0 && (
<div>
<div className="font-mono text-[10px] text-zinc-500 uppercase tracking-wider mb-2">Work Order Steps</div>
<ol className="space-y-1.5">
{steps.map((s, i) => (
<li key={i} className="flex items-start gap-3 text-sm text-zinc-300">
<span className="font-mono text-xs text-zinc-500 w-5 shrink-0 text-right">{String(i + 1).padStart(2, "0")}.</span>
{s}
</li>
))}
</ol>
</div>
)}
{tools.length > 0 && (
<div>
<div className="font-mono text-[10px] text-zinc-500 uppercase tracking-wider mb-2">Parts / Tools Required</div>
<div className="flex flex-wrap gap-1.5">
{tools.map((t, i) => <span key={i} className="font-mono text-xs px-2 py-1 border border-white/15 text-zinc-400">{t}</span>)}
</div>
</div>
)}
</div>
);
}
function ReporterOutput({ parsed }) {
const tags = parsed?.tags || [];
return (
<div className="space-y-3">
{parsed?.headline && (
<div className="font-display font-black text-2xl tracking-tighter text-white">{parsed.headline.replace("[Mock]", "").trim()}</div>
)}
{parsed?.summary && (
<p className="text-sm text-zinc-300 leading-relaxed">{parsed.summary}</p>
)}
{tags.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{tags.map((t, i) => (
<span key={i} className="font-mono text-[10px] px-2 py-1 border border-[#ED1C24]/40 text-[#ED1C24] bg-[#ED1C24]/5">#{t}</span>
))}
</div>
)}
</div>
);
}
function SocialOutput({ parsed }) {
const xText = parsed?.x_post || "";
const linkedInText = parsed?.linkedin_post || "";
return (
<div className="grid md:grid-cols-2 gap-4">
<div className="p-4 border border-white/5 bg-[#141416] rounded-sm fs-rise">
<div className="flex items-center gap-2 mb-3">
<Twitter className="w-4 h-4 text-[#1DA1F2]" />
<span className="font-mono text-[10px] text-zinc-500 uppercase tracking-widest">X / Twitter</span>
</div>
<p className="text-xs text-zinc-300 font-mono leading-relaxed">{xText}</p>
<div className="mt-4 flex justify-end">
<button
onClick={() => window.open(`https://twitter.com/intent/tweet?text=${encodeURIComponent(xText)}`, '_blank')}
className="font-mono text-[10px] px-2 py-1 border border-white/10 hover:bg-white/5 transition-colors text-zinc-400"
>
Draft Post
</button>
</div>
</div>
<div className="p-4 border border-white/5 bg-[#141416] rounded-sm fs-rise">
<div className="flex items-center gap-2 mb-3">
<Linkedin className="w-4 h-4 text-[#0A66C2]" />
<span className="font-mono text-[10px] text-zinc-500 uppercase tracking-widest">LinkedIn</span>
</div>
<div className="text-[11px] text-zinc-400 font-sans whitespace-pre-wrap leading-relaxed max-h-32 overflow-y-auto pr-2 custom-scrollbar">
{linkedInText}
</div>
<div className="mt-4 flex justify-end">
<button
onClick={() => {
navigator.clipboard.writeText(linkedInText);
alert("LinkedIn text copied!");
}}
className="font-mono text-[10px] px-2 py-1 border border-white/10 hover:bg-white/5 transition-colors text-zinc-400"
>
Copy Text
</button>
</div>
</div>
</div>
);
}
function AgentContent({ agent, isMock }) {
const { role, output } = agent;
const parsed = output?.parsed || {};
if (role === "inspector") return <InspectorOutput parsed={parsed} isMock={isMock} />;
if (role === "diagnostician") return <DiagnosticianOutput parsed={parsed} />;
if (role === "action") return <ActionOutput parsed={parsed} />;
if (role === "reporter") return <ReporterOutput parsed={parsed} />;
if (role === "social") return <SocialOutput parsed={parsed} />;
return <pre className="font-mono text-xs text-zinc-400 whitespace-pre-wrap break-words">{JSON.stringify(parsed, null, 2)}</pre>;
}
// ── Main component ──────────────────────────────────────────────────────────
export default function AgentTranscript({ transcript }) {
const [revealed, setRevealed] = useState(0);
useEffect(() => {
if (!transcript) return;
setRevealed(0);
const agents = transcript.agents || [];
agents.forEach((_, i) => {
setTimeout(() => setRevealed((r) => Math.max(r, i + 1)), i * 400);
});
}, [transcript]);
if (!transcript) return null;
const agents = transcript.agents || [];
// Detect mock mode from first agent's source
const isMock = agents[0]?.output?.source?.includes("mock");
return (
<div className="space-y-0 border border-white/10 bg-[#0d0d10]" data-testid="agent-transcript">
{agents.map((a, idx) => {
const Icon = ICONS[a.role] || Eye;
const isVisible = idx < revealed;
const isActive = idx === revealed - 1;
return (
<div
key={a.role}
className={`border-b border-white/10 last:border-b-0 transition-all duration-500 ${isVisible ? "opacity-100" : "opacity-0"}`}
data-testid={`agent-block-${a.role}`}
>
{/* Agent header */}
<div className={`flex items-center justify-between px-5 py-3 border-b border-white/5 ${isActive ? "bg-[#ED1C24]/5" : "bg-[#141416]"}`}>
<div className="flex items-center gap-3">
<div className={`w-7 h-7 flex items-center justify-center border ${isVisible ? "border-[#ED1C24] text-[#ED1C24]" : "border-white/20 text-zinc-500"}`}>
<Icon className="w-3.5 h-3.5" />
</div>
<div>
<div className="font-display font-bold tracking-tight text-sm text-white">{a.label}</div>
<div className="font-mono text-[10px] text-zinc-500">{a.model}</div>
</div>
</div>
<StatusPill visible={isVisible} active={isActive} />
</div>
{/* Agent body */}
{isVisible && (
<div className="p-5">
<AgentContent agent={a} isMock={isMock} />
</div>
)}
</div>
);
})}
</div>
);
}
function StatusPill({ visible, active }) {
if (!visible) return <span className="fs-chip text-zinc-600">queued</span>;
if (active) return <span className="fs-chip" style={{ color: "#ED1C24", borderColor: "#ED1C24", background: "#ED1C2411" }}>complete</span>;
return <span className="fs-chip fs-chip-pass">complete</span>;
}