Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 54a83b0075a6ac0a26352db98b745ee4c6797dfa
parent 3d9bc4d32183ef541f447c15f9b6ae7641815f31
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 19 Sep 2026 15:45:43 -0400

clip bench: the claim scrolls, the answer does not

At 1366x768 -- a laptop, which is where a walk is actually done -- the right
column overflowed its own scrollport by 68 px, and what fell off the bottom
was `correction`: the last of the six fields, and the entire "no" answer.
The verdict buttons were on screen, the box the verdict is explained in was
not.

The column was one scrolling stack, so the description and the sibling list
-- read once per clip -- pushed the fields that are used on every clip
underneath them. It is now the two things it always was: the CLAIM, which
takes the leftover height and scrolls inside it, and the ANSWER, which keeps
its own. That is the same rule the bench already follows one level up
("nothing outside a column scrolls"), applied one level down.

Measured against 839ec45 at 1366x768: before, the column scrolled 68 px and
`correction` sat below it; after, the column does not scroll at all and every
field is on screen, including in the taller `x` state where the "the note is
the answer" line appears. Unchanged at 1920x1080 (the claim simply gets the
extra 450 px) and below lg, which is still one scrolling column. No keyboard
behaviour is touched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Mumtool/components/projects/ClipBench.tsx | 419+++++++++++++++++++++++++++++++++++++++++--------------------------------------
1 file changed, 219 insertions(+), 200 deletions(-)

diff --git a/umtool/components/projects/ClipBench.tsx b/umtool/components/projects/ClipBench.tsx @@ -1703,217 +1703,236 @@ export default function ClipBench({ data }: { data: ClipBenchData }) { {/* ============ what this clip IS, and what the header says ============ */} <div className="flex min-h-0 flex-col gap-2 overflow-y-auto text-[12px]"> - {/* ---- the description, and the one question ---- - A clip is a claim: the report said somebody said this, here. The - walk is somebody checking that claim against the audio, and the - only two answers are "yes" and a note saying what is wrong. */} - <section - data-clip-description="" - className="space-y-1 rounded border border-[var(--color-line)] bg-[var(--color-panel)] px-2 py-1.5" - > - <div className="micro">this clip is supposed to be</div> - <p data-clip-quote="" className="leading-snug text-[var(--color-text)]"> - {draft.quote.trim() ? `“${draft.quote.trim()}”` : "— no quote on this entry —"} - </p> - <div className="micro">why it is in the cut</div> - <p data-clip-note="" className="text-[11px] leading-snug text-[var(--color-dim)]"> - {clip.note?.trim() || "no note"} - </p> - </section> - - {/* ---- what else the cut takes from this recording ---- - "Should this clip be wider, or is what it is missing already in - the cut?" is the question you cannot answer from inside one clip, - and it decides whether a widened window is context or a - duplicate. */} - <section - data-siblings={siblings.length} - className="space-y-1 rounded border border-[var(--color-line)] px-2 py-1.5 text-[11px]" + {/* ---- WHAT IT CLAIMS: read, then scrolled past ---- + This column is a claim on top and an answer underneath, and the + two have different jobs. The claim is read once per clip; the + answer -- the verdict and the six fields -- is used on every one + of them, and at 1366x768 (a laptop, which is what a walk is + actually done on) the claim's own height pushed `correction`, the + LAST field, below the column's scrollport: the one box the "no" + answer is written in, reachable only by scrolling. + So the claim gets the leftover height and scrolls inside it, + and the answer keeps its own -- the same rule the whole bench + follows, one level down. */} + <div + data-clip-context="" + className="flex flex-col gap-2 lg:min-h-[6rem] lg:flex-1 lg:overflow-y-auto" > - <div className="micro">also from this video</div> - {siblings.length === 0 ? ( - <p className="text-[var(--color-dim)]">no other clip in the cut uses this video</p> - ) : ( - <> - {nearest && ( - <p data-sibling-near={nearest.id} className="text-[var(--color-dirty)]"> - {nearest.side === "after" - ? `the next ${nearest.gap!.toFixed(0)} s are not in the cut; ${nearest.id} starts at ${clock(nearest.start)}` - : `the ${nearest.gap!.toFixed(0)} s before this are not in the cut; ${nearest.id} ends at ${clock(nearest.end)}`} - </p> - )} - <ul className="space-y-0.5"> - {siblings.map((sb) => ( - <li key={sb.id} data-sibling={sb.id} className="leading-snug"> - <Link - href={`/browse/${data.project}/clip/${sb.id}`} - className="font-mono text-[var(--color-sel)] hover:underline" - > - {sb.id} - </Link>{" "} - <span className="num text-[var(--color-dim)]"> - {clock(sb.start)}–{clock(sb.end)} - </span>{" "} - <span className="text-[var(--color-dim)]"> - ·{" "} - {sb.side === "overlap" - ? "overlaps this clip" - : sb.side === "after" - ? `${sb.gap!.toFixed(0)} s after this clip ends` - : `${sb.gap!.toFixed(0)} s before this clip starts`}{" "} - · {sb.where} - </span> - {sb.quote && ( - <span className="block text-[var(--color-dim)]"> - &ldquo;{sb.quote}&rdquo; - </span> - )} - </li> - ))} - </ul> - </> - )} - </section> - - <div className="flex flex-wrap items-center gap-2"> - <span className="text-[var(--color-text)]">Is this accurate?</span> - <button - type="button" - data-verdict-yes="" - className={buttonVariants({ variant: "primary", size: "sm" })} - disabled={!!busy} - onClick={() => void confirmClip()} - > - Yes, confirmed <kbd>y</kbd> - </button> - <button - type="button" - data-verdict-no="" - className={buttonVariants({ size: "sm" })} - onClick={rejectClip} + {/* ---- the description, and the one question ---- + A clip is a claim: the report said somebody said this, here. The + walk is somebody checking that claim against the audio, and the + only two answers are "yes" and a note saying what is wrong. */} + <section + data-clip-description="" + className="space-y-1 rounded border border-[var(--color-line)] bg-[var(--color-panel)] px-2 py-1.5" > - No, note what is wrong <kbd>x</kbd> - </button> - <span - data-verdict-state="" - className={ - verdict === "confirmed" - ? "text-[var(--color-good)]" - : verdict === "incorrect" - ? "text-[var(--color-dirty)]" - : "text-[var(--color-dim)]" - } + <div className="micro">this clip is supposed to be</div> + <p data-clip-quote="" className="leading-snug text-[var(--color-text)]"> + {draft.quote.trim() ? `“${draft.quote.trim()}”` : "— no quote on this entry —"} + </p> + <div className="micro">why it is in the cut</div> + <p data-clip-note="" className="text-[11px] leading-snug text-[var(--color-dim)]"> + {clip.note?.trim() || "no note"} + </p> + </section> + + {/* ---- what else the cut takes from this recording ---- + "Should this clip be wider, or is what it is missing already in + the cut?" is the question you cannot answer from inside one clip, + and it decides whether a widened window is context or a + duplicate. */} + <section + data-siblings={siblings.length} + className="space-y-1 rounded border border-[var(--color-line)] px-2 py-1.5 text-[11px]" > - {verdictLabel(clip)} - </span> - {needNote && !draft.correction.trim() && ( - <span className="text-[11px] text-[var(--color-dirty)]"> - the note is the answer — type it below, then blur or <kbd>Enter</kbd>. Saving - stays here, so you can read it back against the clip. - </span> - )} + <div className="micro">also from this video</div> + {siblings.length === 0 ? ( + <p className="text-[var(--color-dim)]">no other clip in the cut uses this video</p> + ) : ( + <> + {nearest && ( + <p data-sibling-near={nearest.id} className="text-[var(--color-dirty)]"> + {nearest.side === "after" + ? `the next ${nearest.gap!.toFixed(0)} s are not in the cut; ${nearest.id} starts at ${clock(nearest.start)}` + : `the ${nearest.gap!.toFixed(0)} s before this are not in the cut; ${nearest.id} ends at ${clock(nearest.end)}`} + </p> + )} + <ul className="space-y-0.5"> + {siblings.map((sb) => ( + <li key={sb.id} data-sibling={sb.id} className="leading-snug"> + <Link + href={`/browse/${data.project}/clip/${sb.id}`} + className="font-mono text-[var(--color-sel)] hover:underline" + > + {sb.id} + </Link>{" "} + <span className="num text-[var(--color-dim)]"> + {clock(sb.start)}–{clock(sb.end)} + </span>{" "} + <span className="text-[var(--color-dim)]"> + ·{" "} + {sb.side === "overlap" + ? "overlaps this clip" + : sb.side === "after" + ? `${sb.gap!.toFixed(0)} s after this clip ends` + : `${sb.gap!.toFixed(0)} s before this clip starts`}{" "} + · {sb.where} + </span> + {sb.quote && ( + <span className="block text-[var(--color-dim)]"> + &ldquo;{sb.quote}&rdquo; + </span> + )} + </li> + ))} + </ul> + </> + )} + </section> </div> - {data.sourceTitle == null && ( - <div className="text-[11px] leading-snug text-[var(--color-dirty)]"> - No cue file was found for this source, so the archived record&rsquo;s own title and - upload date cannot be read here — set <code className="font-mono">title</code> and{" "} - <code className="font-mono">date</code> by hand, or the build will fail before it - draws the header. + {/* ---- THE ANSWER: on screen for every clip, never scrolled to ---- */} + <div data-clip-answer="" className="flex flex-col gap-2 lg:flex-none"> + <div className="flex flex-wrap items-center gap-2"> + <span className="text-[var(--color-text)]">Is this accurate?</span> + <button + type="button" + data-verdict-yes="" + className={buttonVariants({ variant: "primary", size: "sm" })} + disabled={!!busy} + onClick={() => void confirmClip()} + > + Yes, confirmed <kbd>y</kbd> + </button> + <button + type="button" + data-verdict-no="" + className={buttonVariants({ size: "sm" })} + onClick={rejectClip} + > + No, note what is wrong <kbd>x</kbd> + </button> + <span + data-verdict-state="" + className={ + verdict === "confirmed" + ? "text-[var(--color-good)]" + : verdict === "incorrect" + ? "text-[var(--color-dirty)]" + : "text-[var(--color-dim)]" + } + > + {verdictLabel(clip)} + </span> + {needNote && !draft.correction.trim() && ( + <span className="text-[11px] text-[var(--color-dirty)]"> + the note is the answer — type it below, then blur or <kbd>Enter</kbd>. Saving + stays here, so you can read it back against the clip. + </span> + )} </div> - )} - <div className="micro"> - archive: <span className="text-[var(--color-text)]">{data.sourceTitle || "—"}</span> ·{" "} - <span className="text-[var(--color-text)]">{data.uploadDate || "—"}</span> - </div> + {data.sourceTitle == null && ( + <div className="text-[11px] leading-snug text-[var(--color-dirty)]"> + No cue file was found for this source, so the archived record&rsquo;s own title and + upload date cannot be read here — set <code className="font-mono">title</code> and{" "} + <code className="font-mono">date</code> by hand, or the build will fail before it + draws the header. + </div> + )} - {/* ---- the six fields that decide the header ---- - Each field's reason is a tooltip now rather than a paragraph: the - explanations are read once and the boxes are used every clip. */} - <div className="space-y-1.5"> - {ATTRIB.map(([k, label, why]) => { - const changed = draft[k] !== attribValue(clip, k); - return ( - <label key={k} className="block" title={why}> - <span className="font-mono text-[11px] text-[var(--color-text)]">{label}</span> - {k === "correction" && needNote && !draft.correction.trim() && ( - <span className="ml-2 text-[11px] text-[var(--color-dirty)]">required</span> - )} - {changed && ( - <span className="ml-2 text-[11px] text-[var(--color-dirty)]"> - unsaved — was {attribValue(clip, k) || "(empty)"} - </span> - )} - {k === "quote" || k === "correction" ? ( - <textarea - data-attrib-field={k} - name={k} - ref={k === "correction" ? correctionBox : undefined} - rows={3} - value={draft[k]} - placeholder={ - k === "correction" - ? "what the report got wrong: speaker, addressee, date, wording…" - : "" - } - onChange={(e) => setDraft((d) => ({ ...d, [k]: e.target.value }))} - onBlur={() => commit(k)} - className={`mt-0.5 w-full rounded border bg-[var(--color-panel-2)] px-2 py-1 text-[12px] ${ - k === "correction" && needNote && !draft.correction.trim() - ? "border-[var(--color-dirty)]" - : "border-[var(--color-line)]" - }`} - /> - ) : ( - <input - data-attrib-field={k} - name={k} - type="text" - value={draft[k]} - placeholder={ - k === "date" - ? data.uploadDate - ? `${data.uploadDate.slice(0, 4)}-${data.uploadDate.slice(4, 6)}-${data.uploadDate.slice(6, 8)}` - : "YYYY-MM-DD" - : k === "cite" - ? String(clip.start) - : k === "title" - ? (data.sourceTitle ?? "") - : "" - } - onChange={(e) => setDraft((d) => ({ ...d, [k]: e.target.value }))} - onBlur={() => commit(k)} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - commit(k); + <div className="micro"> + archive: <span className="text-[var(--color-text)]">{data.sourceTitle || "—"}</span> ·{" "} + <span className="text-[var(--color-text)]">{data.uploadDate || "—"}</span> + </div> + + {/* ---- the six fields that decide the header ---- + Each field's reason is a tooltip now rather than a paragraph: the + explanations are read once and the boxes are used every clip. */} + <div className="space-y-1.5"> + {ATTRIB.map(([k, label, why]) => { + const changed = draft[k] !== attribValue(clip, k); + return ( + <label key={k} className="block" title={why}> + <span className="font-mono text-[11px] text-[var(--color-text)]">{label}</span> + {k === "correction" && needNote && !draft.correction.trim() && ( + <span className="ml-2 text-[11px] text-[var(--color-dirty)]">required</span> + )} + {changed && ( + <span className="ml-2 text-[11px] text-[var(--color-dirty)]"> + unsaved — was {attribValue(clip, k) || "(empty)"} + </span> + )} + {k === "quote" || k === "correction" ? ( + <textarea + data-attrib-field={k} + name={k} + ref={k === "correction" ? correctionBox : undefined} + rows={3} + value={draft[k]} + placeholder={ + k === "correction" + ? "what the report got wrong: speaker, addressee, date, wording…" + : "" } - }} - className="mt-0.5 w-full rounded border border-[var(--color-line)] bg-[var(--color-panel-2)] px-2 py-1 font-mono text-[12px]" - /> - )} - </label> - ); - })} - </div> + onChange={(e) => setDraft((d) => ({ ...d, [k]: e.target.value }))} + onBlur={() => commit(k)} + className={`mt-0.5 w-full rounded border bg-[var(--color-panel-2)] px-2 py-1 text-[12px] ${ + k === "correction" && needNote && !draft.correction.trim() + ? "border-[var(--color-dirty)]" + : "border-[var(--color-line)]" + }`} + /> + ) : ( + <input + data-attrib-field={k} + name={k} + type="text" + value={draft[k]} + placeholder={ + k === "date" + ? data.uploadDate + ? `${data.uploadDate.slice(0, 4)}-${data.uploadDate.slice(4, 6)}-${data.uploadDate.slice(6, 8)}` + : "YYYY-MM-DD" + : k === "cite" + ? String(clip.start) + : k === "title" + ? (data.sourceTitle ?? "") + : "" + } + onChange={(e) => setDraft((d) => ({ ...d, [k]: e.target.value }))} + onBlur={() => commit(k)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + commit(k); + } + }} + className="mt-0.5 w-full rounded border border-[var(--color-line)] bg-[var(--color-panel-2)] px-2 py-1 font-mono text-[12px]" + /> + )} + </label> + ); + })} + </div> - <details className="rounded border border-[var(--color-line)] px-2 py-1 text-[11px] text-[var(--color-dim)]"> - <summary className="cursor-pointer">what these fields are for</summary> - <div className="mt-1 space-y-1 leading-snug"> - <p> - Each saves when it loses focus or on <kbd>Enter</kbd>; an empty one goes back to the - archive&rsquo;s own answer. The date the archive carries is the date the{" "} - <em>copy</em> was uploaded — a VOD mirror is often years after the stream, which is - what <code className="font-mono">date</code> is for. - </p> - {ATTRIB.map(([k, label, why]) => ( - <p key={k}> - <span className="font-mono text-[var(--color-text)]">{label}</span> — {why} + <details className="rounded border border-[var(--color-line)] px-2 py-1 text-[11px] text-[var(--color-dim)]"> + <summary className="cursor-pointer">what these fields are for</summary> + <div className="mt-1 space-y-1 leading-snug"> + <p> + Each saves when it loses focus or on <kbd>Enter</kbd>; an empty one goes back to the + archive&rsquo;s own answer. The date the archive carries is the date the{" "} + <em>copy</em> was uploaded — a VOD mirror is often years after the stream, which is + what <code className="font-mono">date</code> is for. </p> - ))} - </div> - </details> + {ATTRIB.map(([k, label, why]) => ( + <p key={k}> + <span className="font-mono text-[var(--color-text)]">{label}</span> — {why} + </p> + ))} + </div> + </details> + </div> </div> </div> </div>