101 lines
3.2 KiB
TypeScript
101 lines
3.2 KiB
TypeScript
import { BAND_VAR } from '../bands'
|
||
import type { Run } from '../types'
|
||
|
||
/**
|
||
* The batch in one line: how much runs unattended, how much needs a person.
|
||
*
|
||
* This is the number the whole pipeline exists to produce. Reading it off a list
|
||
* of cards means counting, so it is stated once at the top — and stated as
|
||
* documents, not fields, because the decision it supports ("what do I have to
|
||
* look at today") is made per document.
|
||
*/
|
||
export function StatsBar({ runs }: { runs: Run[] }) {
|
||
const done = runs.filter((r) => r.status === 'done')
|
||
const clean = done.filter((r) => flagged(r) === 0)
|
||
const needs = done.filter((r) => flagged(r) > 0)
|
||
const busy = runs.filter((r) => r.status === 'queued' || r.status === 'running')
|
||
const failed = runs.filter((r) => r.status === 'failed')
|
||
const fields = done.reduce((n, r) => n + flagged(r), 0)
|
||
|
||
if (runs.length === 0) return null
|
||
|
||
return (
|
||
<div className="sb-runbar">
|
||
<Stat n={clean.length} label="run unattended" tone="accent" />
|
||
<Stat n={needs.length} label="need a reviewer" tone="amber" />
|
||
{fields > 0 && (
|
||
<Stat n={fields} label="fields flagged" tone="slate" />
|
||
)}
|
||
{busy.length > 0 && <Stat n={busy.length} label="extracting" tone="blue" />}
|
||
{failed.length > 0 && <Stat n={failed.length} label="failed" tone="red" />}
|
||
|
||
<BandLegend />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function flagged(run: Run): number {
|
||
if (run.review) return run.review.total
|
||
const s = run.summary
|
||
return s ? s.amber + s.red + s.unscored + s.blank_critical : 0
|
||
}
|
||
|
||
function Stat({
|
||
n,
|
||
label,
|
||
tone,
|
||
}: {
|
||
n: number
|
||
label: string
|
||
// "accent" ties to the brand ramp (this stat means "went straight through,"
|
||
// the same semantic as the confirm/approve action elsewhere); the rest are
|
||
// theme tokens for the state they report.
|
||
tone: 'accent' | 'amber' | 'slate' | 'blue' | 'red'
|
||
}) {
|
||
// `slate` stays on the runbar's own ink — a stat with no state to report
|
||
// should not borrow a colour that means one.
|
||
const colour = {
|
||
accent: 'var(--accent-1)',
|
||
amber: 'var(--verify)',
|
||
slate: '',
|
||
blue: 'var(--accent-2)',
|
||
red: 'var(--review)',
|
||
}[tone]
|
||
return (
|
||
<span>
|
||
<b style={colour ? { color: colour } : undefined}>{n}</b> {label}
|
||
</span>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* What the colours mean, on the page rather than in a document nobody opens.
|
||
* The caveat is part of the legend on purpose: the bands rate how legibly the
|
||
* source was printed, and a reviewer who reads them as "verified" will trust a
|
||
* confident misread.
|
||
*/
|
||
export function BandLegend() {
|
||
return (
|
||
<div className="sb-keys">
|
||
<Swatch band="green" label="≥0.95 clean" />
|
||
<Swatch band="amber" label="0.90–0.95 sample" />
|
||
<Swatch band="red" label="<0.90 review" />
|
||
<span
|
||
className="cursor-help text-muted"
|
||
title="The score rates how legibly the source was printed, not whether the value is correct. A cleanly printed value can still be misread."
|
||
>
|
||
print clarity, not correctness ⓘ
|
||
</span>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function Swatch({ band, label }: { band: 'green' | 'amber' | 'red'; label: string }) {
|
||
return (
|
||
<span className="flex items-center text-ink-2">
|
||
<i className="sb-swatch" style={{ backgroundColor: BAND_VAR[band] }} />
|
||
{label}
|
||
</span>
|
||
)
|
||
}
|