"use client"; import { useEffect, useState } from "react"; import { buttonVariants } from "@/components/ui/button"; // --------------------------------------------------------------------------- // "Tag cited videos as …" // // A walked report is a set of adjudicated claims about specific recordings. // That is exactly what the archive's curated tags record, so this hands the // judgement over instead of making somebody re-derive it there — whole project, // or one section at a time when the sections mean different things ("on mic" in // A, "discussed" in C). // // IT ASKS THE EDITOR; IT DOES NOT WRITE THE CORPUS. The POST goes to this // tool's own route, which forwards to /api/ops/tag-videos with the shared // WORKER_TOKEN — the same convention report-to-video/fetch-via-editor.mjs uses // for a clip window. The token never reaches the browser, which is why the // vocabulary is fetched through that route too rather than from the editor // directly. // // WHAT IT REPORTS IS THE EDITOR'S NUMBER. `changed` is how many videos actually // moved: re-tagging what is already tagged changes nothing, and printing "90 // tagged" over an answer of 0 would be this page lying about the archive. // --------------------------------------------------------------------------- type State = { configured: boolean; reachable: boolean; editor: string; tags: { id: string; label: string }[]; sections: { letter: string; clips: number; videos: number }[]; videos: number; clips: number; missing: string[]; }; export default function TagCitedButton({ project }: { project: string }) { const [state, setState] = useState(null); const [tag, setTag] = useState(""); const [section, setSection] = useState(""); const [op, setOp] = useState<"add" | "remove">("add"); const [busy, setBusy] = useState(false); const [msg, setMsg] = useState(null); const [failed, setFailed] = useState(false); useEffect(() => { let live = true; void fetch(`/api/report/tag?project=${encodeURIComponent(project)}`, { cache: "no-store", }) .then((r) => r.json()) .then((s: State & { error?: string }) => { if (!live || s.error) return; setState(s); setTag((t) => t || s.tags[0]?.id || ""); }) .catch(() => {}); return () => { live = false; }; }, [project]); if (!state) return null; const scopeVideos = section ? (state.sections.find((s) => s.letter === section)?.videos ?? 0) : state.videos; async function run() { setBusy(true); setFailed(false); setMsg(null); try { const res = await fetch("/api/report/tag", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ project, tag, op, ...(section ? { section } : {}), }), }); const body = (await res.json().catch(() => ({}))) as { error?: string; changed?: number; videos?: number; missing?: string[]; }; if (!res.ok || body.error) { setFailed(true); setMsg(body.error ?? `HTTP ${res.status}`); return; } const verb = op === "add" ? "tagged" : "untagged"; setMsg( `${verb} ${body.changed ?? 0} of ${body.videos ?? 0} cited video${ (body.videos ?? 0) === 1 ? "" : "s" }` + (body.missing?.length ? ` — ${body.missing.length} clip(s) name no channel: ${body.missing.join(", ")}` : ""), ); } finally { setBusy(false); } } return (
tag cited videos {!state.configured ? ( WORKER_TOKEN is not set here, so the editor cannot be asked. ) : !state.reachable ? ( no editor at {state.editor} ) : state.tags.length === 0 ? ( the archive defines no tags yet — define one on the editor’s Tags page ) : ( <> )} {msg && ( {msg} )}
); }