Training Progress Viz

A controlled-render dashboard for a live (or replayed) training run. The user streams an array of { iter, loss, sample?, msPerIter? } entries plus an optional latest snapshot; the component draws a 520×200 loss curve with raw points, an EMA-smoothed accent trace, dashed semantic threshold flags (random guessing, …, Shakespeare-like), and a pulsing marker on the latest point while isTraining is true. Below the curve, the recent generated samples appear in an auto-scrolling feed colored from warning (high loss) to success (low loss). A progress bar, iter/ETA stats line, and Stop / Resume / Train-more buttons sit at the bottom.

0/500

Customize
Run
500
35ms
0.95

Installation

npx shadcn@latest add https://craftbits.dev/r/training-progress-viz.json

Usage

import { TrainingProgressViz } from "@craft-bits/viz/training-progress-viz";
 
<TrainingProgressViz
  history={history}
  latest={latest}
  totalIters={500}
  isTraining={isTraining}
  onStop={handleStop}
  onResume={handleResume}
  onTrainMore={handleTrainMore}
/>;

Stream new entries as the trainer produces them — the component is a pure function of history + latest + isTraining, so any shape of upstream state (Redux, Zustand, plain useState) works.

Custom thresholds for a non-language task:

<TrainingProgressViz
  history={history}
  yMax={2}
  thresholds={[
    { loss: 1.0, label: "baseline", color: "var(--cb-warning)" },
    { loss: 0.5, label: "target", color: "var(--cb-success)" },
  ]}
/>

Hide the thresholds entirely:

<TrainingProgressViz history={history} thresholds={[]} />

Understanding the component

  1. Loss curve. A 520×200 SVG. The x axis spans 0..max(totalIters, last iter) so the run always fills the canvas; the y axis spans 0..yMax. Dashed grid lines at every integer loss double as y-axis labels. All theme-able color comes from var(--cb-…) tokens.
  2. Threshold flags. Each entry in thresholds draws a dashed horizontal line, a small dot at the right edge, and a label. Defaults reflect the four ML-training milestones (random guessing → Shakespeare-like). Pass [] to suppress or your own list to retarget for a non-language model.
  3. Raw points + EMA trace. Every history entry renders as a low-opacity accent dot; the EMA-smoothed trace (default α = 0.95) is drawn over the top as a 2px accent line. Tune emaAlpha for noisier vs flatter curves.
  4. Latest point. When latest is present, a 4px accent circle is drawn at its (iter, loss). While isTraining is true the radius and opacity loop in a 1.2s breathe; otherwise it sits static.
  5. Sample feed. Any history entry whose sample is a non-empty string lands in the feed (capped to the most recent sampleWindow, default 5). Each card's background tint maps loss to warning (high) → success (low) via color-mix. New samples land with a spring entrance and auto-scroll the feed to the bottom.
  6. Progress + ETA. The bar fills proportional to currentIter / totalIters. The stats line shows iter/total, last measured ms/iter, and a remaining-seconds estimate.
  7. Controls. Stop is shown while training; Resume is shown when paused mid-run; Train-more is shown once currentIter >= totalIters. Each is a real <button type="button"> with focus-visible rings and a data-cb-viz-action attribute for telemetry hooks.
  8. Reduced motion. Under prefers-reduced-motion: reduce, the latest-point breathe, list entrance, sample-feed scroll, and progress-bar tween all collapse to instant state changes.

Props

PropTypeDefaultDescription
historyreadonly TrainingProgressVizHistoryEntry[]Completed iterations in order.
latestTrainingProgressVizLatestMost recent point — pulses while isTraining.
totalItersnumber500Target iteration count; drives x-axis + progress bar.
isTrainingbooleanfalseIf true, latest-point breathes and the Stop button is shown.
thresholdsreadonly TrainingProgressVizThreshold[]four ML thresholdsSemantic loss flags drawn dashed across the curve.
yMaxnumber5Y-axis ceiling. Values above clamp to the top edge.
emaAlphanumber0.95EMA smoothing factor for the trace line.
sampleWindownumber5Number of recent sample entries to keep on screen.
transitionTransitionSPRINGS.snapOverride the progress-bar / latest-point tween.
onStop() => voidFires when the user clicks Stop.
onResume() => voidFires when the user clicks Resume.
onTrainMore() => voidFires when the user clicks Train more.
classNamestringMerged onto the root via cn().

Accessibility

  • The SVG canvas is role="img" with an aria-label summarising the current iteration, target, and latest loss.
  • The progress bar is a real role="progressbar" with aria-valuemin, aria-valuemax, aria-valuenow, and a label.
  • The sample-feed wrapper carries an aria-label so screen readers can find it as a landmark.
  • Stop / Resume / Train-more are real <button type="button"> controls with focus-visible rings, tap-down scale, and data-cb-viz-action attributes.
  • Colour is never the only signal — every threshold has a text label, every sample card has a numeric loss n.nn label, and the progress bar reports its value to assistive tech.
  • Motion respects prefers-reduced-motion: reduce — the latest-point breathe, sample-feed entrance, auto-scroll, and progress-bar tween collapse to instant.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/viz/TrainingProgressViz.tsx). The source dropped the SvgLabel primitive and the lesson-only lessonId / runCount props, generalised the hard-coded four-flag threshold table into a thresholds prop with a TrainingProgressVizThreshold type, promoted yMax, emaAlpha, and sampleWindow to typed knobs, remapped the palette from --color-{ink,accent,warn,success}-* to var(--cb-*) semantic tokens, swapped the inline pulse spring for SPRINGS.snap, added a real role="progressbar" to the bar, and routed the latest-point breathe + sample-feed motion through prefers-reduced-motion.