"use client"; import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react"; import CopyButton from "@/components/CopyButton"; import type { Evidence } from "@/lib/articles/evidence"; import AddToVideo, { type VideoProjectLink } from "./AddToVideo"; // One citation's evidence: what it quotes, who and when, the transcript around // it with the cited cues marked, and the media that plays it -- the prepared // clip, a fetched window or a saved file, seeked to the cited second -- or, // when there is none on disk, the MCP line that fetches it through the editor. export type EvidenceHandle = { togglePlay: () => void }; const fmt = (s: number) => { const m = Math.floor(s / 60); const r = Math.floor(s % 60); return `${m}:${String(r).padStart(2, "0")}`; }; export function useEvidence(site: string, report: string, cite: string | null) { const [ev, setEv] = useState(null); const [error, setError] = useState(null); useEffect(() => { if (!cite) return; let live = true; setEv(null); setError(null); fetch(`/api/sites/evidence?${new URLSearchParams({ site, report, cite })}`, { cache: "no-store" }) .then(async (r) => { const j = await r.json(); if (!r.ok) throw new Error(j.error ?? r.statusText); if (live) setEv(j); }) .catch((err) => live && setError(err instanceof Error ? err.message : String(err))); return () => { live = false; }; }, [site, report, cite]); return { ev, error }; } const EvidencePanel = forwardRef< EvidenceHandle, { ev: Evidence | null; error: string | null; number?: number; autoPlay?: boolean; projects?: VideoProjectLink[] } >( function EvidencePanel({ ev, error, number, autoPlay = false, projects = [] }, ref) { const media = useRef(null); useImperativeHandle(ref, () => ({ togglePlay: () => { const m = media.current; if (!m) return; if (m.paused) void m.play().catch(() => {}); else m.pause(); }, })); if (error) return

{error}

; if (!ev) return

loading…

; const play = ev.play; // File time of a record second: the cited span starts `offset` into the file. const fileT = (t: number) => (play && ev.start !== undefined ? Math.max(0, t - ev.start + play.offset) : 0); const startAt = play ? (play.kind === "prepared" ? 0 : Math.max(0, play.offset - 3)) : 0; const seek = (t: number) => { if (media.current) { media.current.currentTime = fileT(t); void media.current.play().catch(() => {}); } }; return (
{number !== undefined && [{number}]}“{ev.quote}”
{[ev.speaker ?? ev.record?.channelTitle, ev.date, ev.label].filter(Boolean).join(" · ")} {ev.start !== undefined && ev.end !== undefined && ( @ {fmt(ev.start)}–{fmt(ev.end)} )}
{ev.record?.title &&
{ev.record.title}
} {ev.originalUrl && ( original ↗ )} {play ? (
{play.audio ? (
) : ev.fetchLine ? (
not on disk — fetch it through the editor:
{ev.fetchLine}
) : null} {ev.post && (
{ev.post.author &&
{ev.post.author}
} {ev.post.text &&

{ev.post.text}

} {ev.post.shot && ( // eslint-disable-next-line @next/next/no-img-element capture of the post )}
)} {ev.cues.length > 0 ? (
    {ev.cues.map((c) => (
  1. ))}
) : ev.cuesNote ? (
{ev.cuesNote}
) : null}
); }, ); export default EvidencePanel;