"use client"; import { useEffect, useRef, useState } from "react"; import { Loader2 } from "lucide-react"; import type { SpecDoc } from "@/lib/types"; import { anchor } from "@/lib/resolveSpecRef"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; function Section({ title, subtitle, children }: { title: string; subtitle?: string; children: React.ReactNode }) { return ( {title} {subtitle && {subtitle}} {children} ); } function useHighlight(target: string | null, token: number, ready: boolean) { const [hot, setHot] = useState(null); useEffect(() => { if (!target || !ready) return; const el = document.getElementById(target); if (!el) return; // let the DOM paint the freshly-rendered spec before scrolling const raf = requestAnimationFrame(() => { el.scrollIntoView({ behavior: "smooth", block: "center" }); setHot(target); }); const t = setTimeout(() => setHot(null), 2200); return () => { cancelAnimationFrame(raf); clearTimeout(t); }; }, [target, token, ready]); return hot; } const hl = (id: string, hot: string | null) => cn( "scroll-mt-4 rounded-md transition-colors", hot === id && "ring-2 ring-primary bg-primary/5", ); export function SpecPanel({ projectId, runId, highlight, highlightToken = 0, }: { projectId: string; runId: string | null; highlight: string | null; highlightToken?: number; }) { const [spec, setSpec] = useState(null); const [state, setState] = useState<"loading" | "idle" | "empty">("loading"); const containerRef = useRef(null); const hot = useHighlight(highlight, highlightToken, state === "idle"); useEffect(() => { if (!runId) { setState("empty"); return; } setState("loading"); fetch(`/api/projects/${projectId}/runs/${runId}/spec`, { cache: "no-store" }) .then((r) => (r.ok ? r.json() : Promise.reject())) .then((d: SpecDoc) => { setSpec(d); setState("idle"); }) .catch(() => setState("empty")); }, [projectId, runId]); if (state === "loading") { return (
Loading spec…
); } if (state === "empty" || !spec) { return ( The expected-structure spec appears once a run has snapshotted it. ); } const reads = spec.read_structure?.reads ?? []; const r2 = reads.find((r) => r.read === "R2"); return (
{reads.map((rd) => (
{rd.read} {rd.cycles ? `${rd.cycles} cycles · ` : ""} {(rd.segments ?? []).map((s) => s.name).join(" + ") || "—"}
))}
{r2?.readthrough_chain && r2.readthrough_chain.length > 0 && (
{r2.readthrough_chain.map((c) => (
{c.name} {c.type && {c.type}}
{c.notes &&

{c.notes}

}
))}
)}
{(spec.oligos ?? []).map((o) => (
{o.oligo_id} {o.role && ( {o.role} )}
{o.sequence && (

{o.sequence}

)}
))}
{spec.library_generation && spec.library_generation.length > 0 && (
    {spec.library_generation.map((s) => (
  1. {s.step}

    {s.title}

    {s.note &&

    {s.note}

    }
  2. ))}
)} {spec.whitelists && Object.keys(spec.whitelists).length > 0 && (
{Object.entries(spec.whitelists).map(([key, wl]) => (
{key}

{wl.name} · {wl.count?.toLocaleString()} barcodes × {wl.length} nt

))}
)} {spec.platform_params && (
{Object.entries(spec.platform_params).map(([field, val]) => (
{field}
{typeof val === "object" ? JSON.stringify(val) : String(val)}
))}
)} {spec.final_library?.annotated_library_sequence && (
            {spec.final_library.annotated_library_sequence}
          
)}
); }