"use client"; // The per-video digest review surface — the thing that makes a validation run // readable. // // Until this existed the digest layer had nowhere for a human to LOOK at what it // produced: the channel Digest stage is a queue-and-count card by its own // admission, and the per-video page had no digest reference at all. A ~100-video // validation run cannot mean anything while its output is invisible, and // STATE.md's own lesson from this layer is that "the digest layer's failure // modes live at the wiring, not in the pure functions." // // So this panel leads with the things that are wrong rather than the things that // are right: warnings first, then provenance (WHICH configuration produced // this), then the items. During a bake-off-driven validation the provenance line // is the entire point — two videos can carry visually similar chapters from // completely different engines, models or prompt versions. // // THE FRAME IS NOW OperationPanel'S: the section, the heading and the freshness // pill are drawn once for every operation. This is the digest's BODY, and the // only thing about it that is digest-specific. import { useState, useTransition } from "react"; import { StreamActionLog } from "yt-dlp-transcript-common/components/StreamActionLog"; import { digestBucketAction, saveDigestOverridesAction, type DigestLaneChoice, } from "../../../digestActions"; import type { DigestChapter, DigestSectionState, DigestWarning, EffectiveDigest, } from "yt-dlp-transcript-common/lib/digest"; import { cancelJobAction } from "../../../../../jobs/actions"; export type DigestBodyData = { digest: EffectiveDigest; hasMachineDigest: boolean; // One row per section KIND the current settings ask for, so a section that // was never generated is as visible as one that is stale. Computed on the // server by digestSectionStates — the SAME fold the digest operation's // state() counts, so this panel and the channel's work list cannot disagree. sections: DigestSectionState[]; // What a regeneration would produce right now, for the "stale because…" line. target: { appId: string; model: string; promptVersion: number; promptVariant?: string; contextHash: string; }; hasOverrides: boolean; note: string; remoteEnabled: boolean; }; export function DigestBody({ slug, videoId, data, }: { slug: string; videoId: string; data: DigestBodyData; }) { const [lane, setLane] = useState("local"); const { digest, sections, target } = data; return ( <> {data.digest.derivedFrom && ( )} {!data.hasMachineDigest ? (

No digest has been generated for this video yet. Run one below, or sweep the whole channel from the channel page's Digest stage.

) : ( <> )}
digestBucketAction(slug, [videoId], lane)} cancelAction={cancelJobAction} buttonLabel="Regenerate digest" runningLabel="Generating…" label="Regenerate digest" extraControls={ } />

Regeneration only ever rewrites ai-digest.json. Hand corrections live in ai-digest.overrides.json and are never touched by it — which is what lets a correction survive a re-sweep of a corpus too large to generate twice.

); } // A borrowed digest must NEVER be presented as native. Sharing only happens at // near-zero measured offset, so the offset is shown as the reason it was // considered safe rather than left implicit. function SharedFrom({ derivedFrom, }: { derivedFrom: NonNullable; }) { const [channelSlug, id] = derivedFrom.slug.split("/"); return (

Shared from a duplicate. This digest was generated for{" "} {derivedFrom.slug} , the canonical member of its cluster, and copied here.

Cue timings were measured{" "} {derivedFrom.offsetSeconds.toFixed(2)}s apart at the sampled anchors, which is why placing it here was considered safe. Regenerating the canonical member re-applies the share.

); } // Warnings lead. This is the array Phase 11a's review queue is built on, and // showing it per-video first is what makes a validation run readable: a section // generated from 3 of 5 chunks looks perfectly healthy until you read these. function Warnings({ warnings }: { warnings: DigestWarning[] }) { if (warnings.length === 0) { return (

No warnings — every chunk parsed and every item passed the guards.

); } const byCode = new Map(); for (const w of warnings) { const list = byCode.get(w.code) ?? []; list.push(w); byCode.set(w.code, list); } return (

{warnings.length} warning{warnings.length === 1 ? "" : "s"}

    {Array.from(byCode.entries()).map(([code, list]) => (
  • {code} × {list.length} {" "} ({list[0].section} {list[0].chunk !== undefined ? `, chunk ${list[0].chunk}` : ""}) {list[0].value && ( {" "} — first: {list[0].value} )} {list[0].detail && ( — {list[0].detail} )}
  • ))}
); } // WHICH configuration produced this. During a bake-off-driven validation, two // videos can carry similar-looking chapters from entirely different engines. function SectionProvenance({ sections, target, }: { sections: DigestSectionState[]; target: DigestBodyData["target"]; }) { return (
{sections.map((s) => (
{s.kind} {!s.present ? ( never generated ) : s.fresh ? ( current ) : ( stale )}
{s.provenance ? (
{s.provenance.chunks !== undefined && ( )} {s.provenance.costUsd !== undefined && ( )}
) : null} {s.present && !s.fresh && (

A regeneration would produce{" "} {target.appId}/{target.model} v{target.promptVersion} {target.promptVariant ? ` (${target.promptVariant})` : ""} .

)}
))}
); } function Row({ label, value }: { label: string; value: string }) { return ( <>
{label}
{value}
); } function Chapters({ chapters }: { chapters: DigestChapter[] }) { if (chapters.length === 0) { return (

No chapters.

); } return (

Chapters ({chapters.length})

    {chapters.map((c) => (
  1. {c.clock} {c.title} {c.decidedBy === "human" && ( edited )}
  2. ))}
); } function Tags({ tags }: { tags: EffectiveDigest["tags"] }) { if (tags.length === 0) return null; return (

Tags ({tags.length})

    {tags.map((t) => (
  • {t.tag} {t.decidedBy === "human" && ✎}
  • ))}
); } // Corrections. Deliberately minimal: retitle a chapter, or suppress it. Those // are the two things a reviewer actually does, and both are id-keyed so a // regeneration that re-emits the same segmentation keeps honoring them. function OverridesEditor({ slug, videoId, chapters, note, hasOverrides, }: { slug: string; videoId: string; chapters: DigestChapter[]; note: string; hasOverrides: boolean; }) { const [rows, setRows] = useState(() => chapters.map((c) => ({ id: c.id, title: c.title, enabled: true })), ); const [noteText, setNoteText] = useState(note); const [pending, startTransition] = useTransition(); const [result, setResult] = useState(null); // The panel re-renders from the server after a save (revalidatePath), so the // row list can go stale against a regeneration that happened in between. const chapterIds = chapters.map((c) => c.id).join(","); const rowIds = rows.map((r) => r.id).join(","); if (chapterIds !== rowIds) { setRows(chapters.map((c) => ({ id: c.id, title: c.title, enabled: true }))); } if (chapters.length === 0 && !hasOverrides) return null; const save = (): void => { setResult(null); startTransition(async () => { const edited = rows.filter((row, i) => { const original = chapters[i]; return ( !original || row.title !== original.title || row.enabled === false ); }); const res = await saveDigestOverridesAction(slug, videoId, { chapters: edited.map((row) => ({ id: row.id, // start/clock are patched in from the machine item by mergeItems — an // override authored as (id + title) must not blank them. start: chapters.find((c) => c.id === row.id)?.start ?? 0, clock: chapters.find((c) => c.id === row.id)?.clock ?? "00:00:00", title: row.title, decidedBy: "human" as const, ...(row.enabled ? {} : { enabled: false }), })), ...(noteText.trim() ? { note: noteText.trim() } : {}), }); setResult(res.ok ? "Saved." : res.error); }); }; return (
Correct this digest{hasOverrides ? " (edited)" : ""}
{rows.map((row, i) => (
setRows((prev) => prev.map((r, j) => j === i ? { ...r, enabled: e.target.checked } : r, ), ) } /> {chapters[i]?.clock} setRows((prev) => prev.map((r, j) => j === i ? { ...r, title: e.target.value } : r, ), ) } className="flex-1 rounded border border-border bg-card px-2 py-1 text-sm" />
))}
{result && ( {result} )}

Unchecking a chapter suppresses it without deleting it, so a regeneration that re-emits the same item does not resurrect something you rejected.

); }