/** * WorthDoing.ai * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: src/components/wd/Timeline.tsx * Description: Live investigation timeline — renders real AgentEvents as they stream in. */ "use client"; import { memo } from "react"; import { cn } from "@/lib/utils"; import type { UiAgentEvent } from "@/lib/ui/api"; type Tone = "verdict" | "signal" | "rust" | "tele" | "ink" | "soft"; const TONE_DOT: Record = { verdict: "bg-verdict", signal: "bg-signal", rust: "bg-rust", tele: "bg-tele", ink: "bg-ink", soft: "bg-ink-soft/50", }; function describe(e: UiAgentEvent): { tone: Tone; label: string; body?: string; mono?: boolean } | null { const p = e.payload as Record & Record; switch (e.type) { case "investigation.started": return { tone: "ink", label: "Investigation started", body: String(p.objective ?? "") }; case "agent.plan": return { tone: "ink", label: "Agent", body: String(p.text ?? "") }; case "phase.changed": return { tone: "signal", label: `Phase → ${String(p.to)}` }; case "search.started": return { tone: "tele", label: `Searching (${String(p.intent)})`, body: `“${String(p.query)}”`, mono: true }; case "search.completed": return { tone: "tele", label: `Search returned ${String(p.resultCount)} results`, body: `“${String(p.query)}”`, mono: true }; case "scrape.started": return { tone: "tele", label: "Reading", body: String(p.url), mono: true }; case "scrape.completed": { const cached = p.fromCache ? " (cache)" : ""; return { tone: "tele", label: `Read ${String(p.wordCount)} words${cached}`, body: String(p.title ?? p.url), mono: false }; } case "scrape.failed": return { tone: "rust", label: "Scrape failed", body: String(p.url), mono: true }; case "crawl.started": return { tone: "tele", label: `Crawling up to ${String(p.limit)} pages`, body: String(p.url), mono: true }; case "crawl.completed": return { tone: "tele", label: `Crawled ${String(p.pageCount)} pages`, body: String(p.url), mono: true }; case "crawl.failed": return { tone: "rust", label: "Crawl failed", body: String(p.url), mono: true }; case "extract.started": return { tone: "tele", label: "Extracting structured data", body: (p.urls as string[])?.join(", "), mono: true }; case "extract.completed": return { tone: "tele", label: "Extraction complete" }; case "extract.failed": return { tone: "rust", label: "Extraction failed" }; case "hypothesis.created": return { tone: "verdict", label: `Hypothesis formed · ${Math.round(Number(p.confidence) * 100)}%`, body: String(p.title) }; case "hypothesis.updated": { const delta = Number(p.confidenceDelta ?? 0); const arrow = delta > 0 ? "▲" : delta < 0 ? "▼" : "→"; const tone: Tone = delta < 0 ? "signal" : "verdict"; return { tone, label: `Hypothesis ${arrow} ${Math.round(Number(p.confidence) * 100)}% (${delta >= 0 ? "+" : ""}${Math.round(delta * 100)})`, body: `${String(p.title)} — ${String(p.rationale ?? "")}`, }; } case "hypothesis.rejected": return { tone: "rust", label: "Hypothesis rejected", body: `${String(p.title)} — ${String(p.reason ?? "")}` }; case "evidence.saved": { const kind = String(p.kind); const tone: Tone = kind === "contradict" ? "rust" : kind === "support" ? "verdict" : "soft"; return { tone, label: `Evidence saved (${kind})`, body: String(p.summary) }; } case "opportunity.created": return { tone: "verdict", label: `Opportunity detected · Worth ${String(p.worthScore)}`, body: String(p.title), }; case "report.started": return { tone: "ink", label: "Writing report", body: String(p.title) }; case "report.completed": return { tone: "ink", label: `Report complete (${String(p.citations)} citations)`, body: String(p.title) }; case "budget.updated": return null; // reflected in the stats bar, not the feed case "agent.error": return { tone: "rust", label: `Error in ${String(p.tool ?? "step")}`, body: String(p.message ?? "") }; case "investigation.completed": return { tone: "ink", label: `Investigation complete — ${String(p.outcome ?? "")}`, body: String(p.conclusion ?? "") }; case "investigation.failed": return { tone: "rust", label: "Investigation failed", body: String(p.message ?? "") }; case "report.delta": return null; // streamed into the report pane, not the feed default: return null; } } export const Timeline = memo(function Timeline({ events }: { events: UiAgentEvent[] }) { const items = events .map((e) => ({ e, d: describe(e) })) .filter((x): x is { e: UiAgentEvent; d: NonNullable> } => x.d !== null); if (items.length === 0) { return (

waiting for the agent’s first move…

); } return (
    {items.map(({ e, d }) => (
  1. {d.label} {new Date(e.createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" })}
    {d.body && (

    {d.body.length > 600 ? `${d.body.slice(0, 600)}…` : d.body}

    )}
  2. ))}
); });