/* ── Dataset + Train/Push + Model Registry panels ── Ported from the Hover design project, extended per the operator's scope-growth ask (hov-zaml.7): a full model registry (every trained version kept, push ANY of them, not just the newest) with per-model notes. */ const { useState } = React; // ───────────────────────────────────────────────────────────── // ZONE 4 — DATASET // ───────────────────────────────────────────────────────────── function ClassBalanceBar({ stats }) { const { counts, total, minorityFrac } = stats; const warn = total > 0 && stats.classes >= 2 && minorityFrac < 0.30; // the smallest class that actually has samples -> the one to nudge more of const present = SARG.classes.filter(c => (counts[c.label] || 0) > 0); const smallest = present.reduce((a, c) => ((counts[c.label] || 0) < (counts[a.label] || 0) ? c : a), present[0] || SARG.classes[0]); return (
{SARG.classes.map(c => ( {counts[c.label] || 0} {c.name} ))}
{total === 0 ? (
) : ( SARG.classes.map(c => { const w = ((counts[c.label] || 0) / total) * 100; return w > 0 ?
: null; }) )}
{warn && (
▲ Lopsided — add more {smallest.name} samples for a fair model.
)}
); } function SpanRow({ span, isNew, onDelete }) { const cls = SARG.byLabel(span.label) || SARG.classes[0]; const hasWall = span.t_end_wall > 0; let hh = '--', mm = '--', ss = '--'; if (hasWall) { const t = new Date(span.t_end_wall * 1000); hh = String(t.getHours()).padStart(2, '0'); mm = String(t.getMinutes()).padStart(2, '0'); ss = String(t.getSeconds()).padStart(2, '0'); } return (
{cls.short} {hh}:{mm}:{ss} {span.duration_sec}s {span.samples} smp 0..255 for the color border: '1px solid var(--hair-2)', justifySelf: 'center', }} />
); } function DatasetZone({ spans, stats, newSpanId, onDelete, onClear }) { const [confirmClear, setConfirmClear] = useState(false); const empty = spans.length === 0; return (
Accumulated dataset
{empty ? 'nothing recorded yet' : ( <>{stats.spans} spans · {stats.total} samples )}
{!empty && ( confirmClear ? (
Delete all {stats.spans} spans?
) : ( ) )}
{empty ? (
Label some readings to build a dataset
Hold the drifter inside a mat and tap IN-MAT, in clear water and tap OPEN WATER, and lift it into the air for OUT OF WATER. Each label appends a span here.
) : ( <>
LabelEndedDur SamplesAvg
{[...spans].reverse().map(s => ( ))}
↻ Train uses all {stats.spans} spans. It retrains from scratch on everything above — not just the last one you labeled.
)}
); } // ───────────────────────────────────────────────────────────── // ZONE 5 — TRAIN + PUSH (the model registry itself is its own zone -- see registry.jsx) // ───────────────────────────────────────────────────────────── function ResultPanel({ result, liveVersion, onPush }) { const good = result.tone === 'good'; const accent = good ? 'var(--sage)' : 'var(--amber)'; const bg = good ? 'rgba(122,166,146,0.08)' : 'rgba(179,135,61,0.09)'; const border = good ? 'rgba(122,166,146,0.3)' : 'rgba(179,135,61,0.35)'; const st = result.dataset; const alreadyPushed = liveVersion > 0 && result.version === liveVersion; return (
{/* val accuracy */}
Validation accuracy · v{result.version}
{result.val.toFixed(2)}
separability {result.separability.toFixed(2)}
{/* read */}
{good ? '✓' : '▲'} {result.headline}
{result.detail}
{SARG.classes.filter(c => ((st.counts || {})[c.label] || 0) > 0).map(c => ( {st.counts[c.label]} {c.short} ))} {st.spans} spans
{alreadyPushed ? ( Pushed to board as model v{liveVersion} ) : ( <> {good ? 'The board keeps running its current model until you push.' : 'You can still push, but this model is flagged — watch the live verdict closely.'} )}
{alreadyPushed && ( // Post-push loop-closer nudge (design carry-over, prompt-11): val accuracy is noisy on small field // datasets -- the live board verdict, watched by hand, is the real ground truth.
↻ Now move the drifter in and out of the mat, and watch the Board verdict up top — that live test is the real ground truth.
)}
); } function TrainPushZone({ stats, result, training, liveVersion, onTrain, onPush }) { const canTrain = stats.classes >= 2 && stats.total > 0; // at least 2 of the (up to 3) classes present const presentNames = SARG.classes.filter(c => (stats.counts[c.label] || 0) > 0).map(c => c.name); return (
Train & push
{canTrain ? ( <>Will train on {stats.spans} spans · {SARG.classes.filter(c => (stats.counts[c.label] || 0) > 0).map((c, i) => ( {i > 0 && ' / '}{stats.counts[c.label]} {c.short} ))} ) : stats.total === 0 ? ( Label some readings first — nothing to train on yet. ) : ( Need at least 2 classes. You only have {presentNames.join(', ')} — add another (open water, in-mat, or out of water). )}
{result && (
)}
); } Object.assign(window, { ClassBalanceBar, SpanRow, DatasetZone, ResultPanel, TrainPushZone });