"use client"; // Canvas for the Research tier — selected topic detail: title, description, // outcome chip, assigned agents grid, status pill, and the state-appropriate // primary action button (start → submit-review → request-publish). import { useEffect, useState } from "react"; import type { Agent } from "@/lib/api/schemas"; import { getTopic, requestPublish, startTopic, submitReview, type TopicDetail, type TopicStatus, } from "@/lib/api/research"; const mono = "ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace"; const STATUS_COLOR: Record = { standby: "#8a8a92", processing: "#5ec8d8", reviewing: "#ffb44a", publishing: "#c98af0", published: "#5fd08a", }; function nextAction(status: TopicStatus): { label: string; run: (id: string) => Promise; } | null { switch (status) { case "standby": return { label: "Start research", run: startTopic }; case "processing": return { label: "Submit for review", run: submitReview }; case "reviewing": return { label: "Request publish", run: requestPublish }; default: return null; } } export function ResearchCanvas({ selectedId, agents, refreshKey, onChanged, }: { selectedId: string | null; agents: Agent[]; refreshKey: number; onChanged: () => void; }) { const [topic, setTopic] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [acting, setActing] = useState(false); useEffect(() => { let alive = true; const load = async () => { if (!selectedId) { if (alive) setTopic(null); return; } setLoading(true); setError(null); try { const d = await getTopic(selectedId); if (alive) setTopic(d); } catch (e) { if (alive) setError(e instanceof Error ? e.message : "load failed"); } finally { if (alive) setLoading(false); } }; load(); return () => { alive = false; }; }, [selectedId, refreshKey]); if (!selectedId) { return ; } if (loading && !topic) { return ( Loading topic… ); } if (error) { return {error}; } if (!topic) { return ; } const dot = STATUS_COLOR[topic.status]; const action = nextAction(topic.status); const agentById = new Map(agents.map((a) => [a.id, a])); async function runAction() { if (!action) return; setActing(true); try { await action.run(topic!.id); onChanged(); } catch (e) { setError(e instanceof Error ? e.message : "action failed"); } finally { setActing(false); } } return (
{/* Header */}
{topic.status.toUpperCase()} · {topic.outcome_kind.replace("_", " ")} {topic.published_at && ( <> · published {new Date(topic.published_at).toLocaleDateString()} )}

{topic.title}

{/* Agents */}
Agents ({topic.agents.length})
{topic.agents.length === 0 ? (

None assigned yet.

) : (
{topic.agents.map((slot) => { const a = agentById.get(slot.agent_id); return (
{a?.name ?? "(missing agent)"}
{slot.role_slot ?? a?.job_title ?? "—"}
); })}
)}
{/* Description */}
Description
            {topic.description}
          
{/* Action */} {action && (
{error && ( {error} )}
)}
); } function Placeholder() { return (
Research
Pick a topic on the left, or hit the + button in the sidebar to launch the wizard.
); } function PlaceholderText({ children, color = "#8a8a92", }: { children: React.ReactNode; color?: string; }) { return (
{children}
); } const sectionHeader: React.CSSProperties = { fontFamily: mono, fontSize: 10, letterSpacing: ".12em", color: "#5a5a62", textTransform: "uppercase", }; const hintStyle: React.CSSProperties = { fontFamily: mono, fontSize: 12, color: "#8a8a92", };