/** * Search-box.ai * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: apps/web/components/ResearchGraph.tsx * Description: Signature "bloom" — the question expands into objectives and claims as real events arrive. */ "use client"; import type { Claim } from "@search-box/shared"; const W = 980; const QX = 84; function truncate(s: string, n: number): string { return s.length > n ? `${s.slice(0, n - 1)}…` : s; } const STATUS_COLOR: Record = { supported: "var(--teal)", contradicted: "var(--accent)", uncertain: "var(--amber)", exploring: "var(--ink-faint)" }; /** * Deterministic layout: objectives fan out from the question node; claims * bloom further right, each visually attached to an objective branch. * Every element appears because a real backend event arrived. */ export function ResearchGraph({ objectives, claims, live, hasContradiction }: { objectives: string[]; claims: Claim[]; live: boolean; hasContradiction: boolean; }) { const nObj = objectives.length; const rows = Math.max(nObj, 1); const rowGap = Math.min(46, 190 / rows); const height = Math.max(150, rows * rowGap + 70); const qy = height / 2; const objY = (i: number) => qy + (i - (nObj - 1) / 2) * rowGap; const objX = 470; const claimX = 830; const claimY = (i: number) => qy + (i - (claims.length - 1) / 2) * Math.min(52, (height - 60) / Math.max(claims.length, 1)); return (
{/* objective branches */} {objectives.map((o, i) => { const y = objY(i); return ( {truncate(o, 44)} ); })} {/* claim blooms */} {claims.map((c, i) => { const anchor = nObj > 0 ? objY(i % nObj) : qy; const y = claimY(i); const color = STATUS_COLOR[c.status] ?? "var(--ink-faint)"; return ( {truncate(c.text, 22)} · {Math.round(c.confidence * 100)}% ); })} {/* the question node */} {live && } question
); }