supplymind / static /dashboard.html
Rishav
Add held-out improvement visuals
d211fba
Raw
History Blame Contribute Delete
24.7 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>SupplyMind Experiment Analysis</title>
<style>
:root {
--bg:#f4f7fb; --ink:#172033; --muted:#667085; --panel:#ffffff; --line:#d8e0ea;
--nav:#102236; --blue:#2563eb; --teal:#0f766e; --red:#c2410c; --amber:#b7791f;
--green:#047857; --grid:#e8edf3;
}
* { box-sizing:border-box; }
body { margin:0; font-family:Inter, Segoe UI, Arial, sans-serif; background:var(--bg); color:var(--ink); }
header { background:var(--nav); color:white; padding:18px 22px; display:flex; justify-content:space-between; align-items:center; gap:14px; flex-wrap:wrap; }
h1 { margin:0; font-size:22px; font-weight:750; letter-spacing:0; }
h2 { margin:0; font-size:15px; font-weight:700; }
main { max-width:1440px; margin:0 auto; padding:16px; display:grid; gap:14px; }
section { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:14px; }
button, a.button { height:34px; border-radius:6px; border:0; background:var(--blue); color:white; padding:8px 12px; cursor:pointer; text-decoration:none; font:inherit; display:inline-flex; align-items:center; }
a.link { color:#dbeafe; text-decoration:none; }
.muted { color:var(--muted); }
.subtle { color:#cbd5e1; font-size:13px; margin-top:4px; }
.status { display:flex; gap:9px; align-items:center; flex-wrap:wrap; }
.pill { border-radius:999px; border:1px solid #bfdbfe; background:#eff6ff; color:#1d4ed8; padding:4px 10px; font-size:12px; white-space:nowrap; }
.summary { display:grid; grid-template-columns:repeat(5, minmax(0, 1fr)); gap:10px; }
.metric { border:1px solid var(--line); border-radius:8px; padding:11px 12px; background:#fbfdff; min-height:82px; }
.metric span { display:block; color:var(--muted); font-size:12px; }
.metric strong { display:block; margin-top:8px; font-size:24px; line-height:1; }
.metric small { display:block; margin-top:7px; color:var(--muted); font-size:12px; }
.verdicts { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:10px; margin-bottom:12px; }
.verdict { border:1px solid var(--line); border-left:4px solid var(--muted); border-radius:8px; padding:10px 12px; background:#fbfdff; }
.verdict.good { border-left-color:var(--green); }
.verdict.bad { border-left-color:var(--red); }
.verdict.warn { border-left-color:var(--amber); }
.verdict strong { display:block; font-size:15px; }
.verdict span { display:block; color:var(--muted); margin-top:5px; font-size:12px; line-height:1.35; }
.chart-grid { display:grid; grid-template-columns:1fr 1fr; gap:14px; }
.wide { grid-column:1 / -1; }
.chart-head { display:flex; justify-content:space-between; gap:12px; align-items:flex-start; margin-bottom:8px; }
.legend { display:flex; gap:12px; flex-wrap:wrap; font-size:12px; color:var(--muted); justify-content:flex-end; }
.swatch { width:10px; height:10px; border-radius:2px; display:inline-block; margin-right:5px; }
canvas { display:block; width:100%; height:320px; }
.wide canvas { height:360px; }
table { width:100%; border-collapse:collapse; font-size:13px; }
th, td { padding:9px 8px; border-bottom:1px solid var(--line); text-align:left; vertical-align:top; }
th { background:#f8fafc; color:#42536a; font-weight:700; }
td.number { font-variant-numeric:tabular-nums; }
.delta.good { color:var(--green); font-weight:700; }
.delta.bad { color:var(--red); font-weight:700; }
.empty { color:var(--muted); padding:24px; text-align:center; border:1px dashed var(--line); border-radius:8px; background:#fbfdff; }
@media (max-width:1100px) {
.summary { grid-template-columns:repeat(2, minmax(0, 1fr)); }
.chart-grid { grid-template-columns:1fr; }
.wide { grid-column:auto; }
.verdicts { grid-template-columns:1fr; }
}
@media (max-width:640px) {
header { align-items:flex-start; }
main { padding:10px; }
.summary { grid-template-columns:1fr; }
canvas, .wide canvas { height:280px; }
}
</style>
</head>
<body>
<header>
<div>
<h1>SupplyMind Experiment Analysis</h1>
<div class="subtle">Training curves, reward quality, and held-out adapter comparisons</div>
</div>
<div class="status">
<a class="link" href="/v2/ui">Episode UI</a>
<button id="refresh">Refresh</button>
<span id="loaded" class="pill">loading</span>
</div>
</header>
<main>
<section class="summary" id="summary"></section>
<div class="chart-grid">
<section class="wide">
<div class="chart-head">
<h2>Loss Over Step</h2>
<div class="legend" id="lossLegend"></div>
</div>
<canvas id="lossPlot" width="1200" height="360"></canvas>
</section>
<section>
<div class="chart-head">
<h2>Reward Over Step</h2>
<div class="legend" id="rewardLegend"></div>
</div>
<canvas id="rewardPlot" width="760" height="320"></canvas>
</section>
<section>
<div class="chart-head">
<h2>Invalid Payload / Action Batches</h2>
<div class="legend" id="invalidLegend"></div>
</div>
<canvas id="invalidPlot" width="760" height="320"></canvas>
</section>
<section>
<div class="chart-head">
<h2>Completion Length</h2>
<div class="legend" id="lengthLegend"></div>
</div>
<canvas id="lengthPlot" width="760" height="320"></canvas>
</section>
<section>
<div class="chart-head">
<h2>Clipped Ratio</h2>
<div class="legend" id="clipLegend"></div>
</div>
<canvas id="clipPlot" width="760" height="320"></canvas>
</section>
</div>
<section>
<div class="chart-head">
<h2>Held-out Comparison</h2>
<div class="muted" id="comparisonNote"></div>
</div>
<div class="verdicts" id="verdicts"></div>
<canvas id="heldoutPlot" width="1200" height="320"></canvas>
<table>
<thead>
<tr>
<th>Role</th>
<th>Variant</th>
<th>Base Global</th>
<th>Variant Global</th>
<th>Global Delta</th>
<th>Base Role</th>
<th>Variant Role</th>
<th>Role Delta</th>
<th>Invalids</th>
<th>Raw Reward Delta</th>
</tr>
</thead>
<tbody id="heldout"></tbody>
</table>
</section>
</main>
<script>
const $ = id => document.getElementById(id);
const trainingDefs = [
{ key:"center_sft", role:"center", phase:"SFT", label:"center SFT", color:"#2563eb" },
{ key:"center_grpo", role:"center", phase:"GRPO", label:"center GRPO", color:"#7c3aed" },
{ key:"warehouse_sft", role:"warehouse", phase:"SFT", label:"warehouse SFT", color:"#0f766e" },
{ key:"warehouse_grpo", role:"warehouse", phase:"GRPO", label:"warehouse GRPO", color:"#047857" },
{ key:"center", role:"center", phase:null, label:"center", color:"#1d4ed8" },
{ key:"warehouse", role:"warehouse", phase:null, label:"warehouse", color:"#0f766e" },
];
const compareRoles = [
{ key:"center", label:"center" },
{ key:"warehouse", label:"warehouse" },
];
const fmt = (value, digits = 3) => {
const n = Number(value);
if (!Number.isFinite(n)) return "pending";
if (Math.abs(n) >= 100) return n.toFixed(1);
if (Math.abs(n) >= 10) return n.toFixed(2);
return n.toFixed(digits);
};
const roleScoreKey = role => role === "center" ? "mean_center_role_score" : "mean_warehouse_role_score";
const val = (row, keys) => {
for (const key of keys) {
if (row && row[key] !== undefined && row[key] !== null && Number.isFinite(Number(row[key]))) return Number(row[key]);
}
return null;
};
async function load() {
try {
const res = await fetch("/v2/training-results?ts=" + Date.now());
if (!res.ok) throw new Error("HTTP " + res.status);
const data = await res.json();
$("loaded").textContent = data.updated_at || data.status || "loaded";
render(data);
} catch (error) {
$("loaded").textContent = "failed";
$("summary").innerHTML = `<div class="empty" style="grid-column:1/-1">Could not load /v2/training-results: ${escapeHtml(error.message)}</div>`;
}
}
function render(data) {
const series = data.training_series || {};
const defs = activeTrainingDefs(series);
renderSummary(data, series);
renderLegend("lossLegend", defs);
renderLegend("rewardLegend", defs);
renderLegend("invalidLegend", [
...defs.map(def => ({ label:def.label, color:def.color })),
{ label:"payload/action", color:"#c2410c" },
]);
renderLegend("lengthLegend", defs);
renderLegend("clipLegend", defs);
drawLineChart("lossPlot", lineSeries(series, defs, ["loss"]), "loss");
drawLineChart("rewardPlot", lineSeries(series, defs, ["reward", "rewards/reward_completions/mean"]), "reward");
drawInvalidChart("invalidPlot", invalidSeries(series, defs));
drawLineChart("lengthPlot", lineSeries(series, defs, ["completions/mean_length", "completion_length", "mean_completion_length"]), "tokens");
drawLineChart("clipPlot", lineSeries(series, defs, ["completions/clipped_ratio", "clipped_ratio"]), "ratio", { minY:0, maxY:1 });
renderHeldout(data.comparisons || []);
}
function renderSummary(data, series) {
const summary = data.training_summary || {};
const defs = activeTrainingDefs(series);
const allSteps = defs.flatMap(def => (series[def.key]?.steps || []).map(step => ({ key:def.key, step })));
const lastRows = defs.map(def => {
const steps = series[def.key]?.steps || [];
return { key:def.key, row:steps[steps.length - 1], summary:summary[def.key] || {} };
});
const bestReward = Math.max(...allSteps.map(item => val(item.step, ["reward", "rewards/reward_completions/mean"]) ?? -Infinity));
const meanClip = avg(lastRows.map(item => item.summary.mean_clipped_ratio));
const meanLength = avg(lastRows.map(item => item.summary.mean_completion_length));
const totalInvalidPayloads = defs.reduce((sum, def) => sum + (series[def.key]?.reward_batches || []).reduce((s, b) => s + Number(b.invalid_payloads || 0), 0), 0);
const totalInvalidActions = defs.reduce((sum, def) => sum + (series[def.key]?.reward_batches || []).reduce((s, b) => s + Number(b.invalid_actions || 0), 0), 0);
$("summary").innerHTML = [
metric("Status", data.status || "pending", data.summary || ""),
metric("Steps", String(Math.max(0, ...lastRows.map(item => Number(item.summary.steps || item.row?.step || 0)))), data.hardware || ""),
metric("Best reward", bestReward === -Infinity ? "pending" : fmt(bestReward), "highest training step reward"),
metric("Mean length", fmt(meanLength, 1), "completion tokens"),
metric("Invalid batches", `${totalInvalidPayloads} / ${totalInvalidActions}`, "payloads / actions"),
metric("Mean clipped", fmt(meanClip, 3), "across role summaries"),
].join("");
}
function metric(label, value, note) {
return `<div class="metric"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong><small>${escapeHtml(note || "")}</small></div>`;
}
function activeTrainingDefs(trainingSeries) {
const defs = trainingDefs.filter(def => trainingSeries[def.key]);
return defs.length ? defs : trainingDefs.filter(def => !def.phase);
}
function renderLegend(id, items) {
$(id).innerHTML = items.length ? items.map(item =>
`<span><span class="swatch" style="background:${escapeHtml(item.color)}"></span>${escapeHtml(item.label)}</span>`
).join("") : "";
}
function lineSeries(trainingSeries, defs, keys) {
return defs.map(def => ({
label: def.label,
color: def.color,
points: (trainingSeries[def.key]?.steps || [])
.map(row => ({ x:val(row, ["step", "global_step"]), y:val(row, keys) }))
.filter(point => point.x !== null && point.y !== null),
}));
}
function invalidSeries(trainingSeries, defs) {
const out = [];
for (const def of defs) {
for (const kind of [
{ key:"invalid_payloads", label:"payloads", alpha:0.86 },
{ key:"invalid_actions", label:"actions", alpha:0.45 },
]) {
out.push({
label: `${def.label} ${kind.label}`,
color: def.color,
alpha: kind.alpha,
points: (trainingSeries[def.key]?.reward_batches || [])
.map(row => ({ x:val(row, ["call", "step"]), y:val(row, [kind.key]) }))
.filter(point => point.x !== null && point.y !== null),
});
}
}
return out;
}
function drawLineChart(id, series, yLabel, options = {}) {
const canvas = $(id);
const ctx = prepCanvas(canvas);
const bounds = chartBounds(canvas);
const points = series.flatMap(s => s.points);
if (!points.length) return drawEmpty(ctx, canvas, "No training series found");
const minX = Math.min(...points.map(p => p.x));
const maxX = Math.max(...points.map(p => p.x));
let minY = options.minY ?? Math.min(...points.map(p => p.y));
let maxY = options.maxY ?? Math.max(...points.map(p => p.y));
if (minY === maxY) { minY -= 1; maxY += 1; }
drawAxes(ctx, bounds, minX, maxX, minY, maxY, yLabel);
for (const item of series) {
drawPath(ctx, bounds, item.points, minX, maxX, minY, maxY, item.color);
}
}
function drawInvalidChart(id, series) {
const canvas = $(id);
const ctx = prepCanvas(canvas);
const bounds = chartBounds(canvas);
const points = series.flatMap(s => s.points);
if (!points.length) return drawEmpty(ctx, canvas, "No reward batch diagnostics found");
const minX = Math.min(...points.map(p => p.x));
const maxX = Math.max(...points.map(p => p.x));
const maxY = Math.max(1, ...points.map(p => p.y));
drawAxes(ctx, bounds, minX, maxX, 0, maxY, "count");
series.forEach((item, index) => {
const offset = (index - (series.length - 1) / 2) * 5;
drawBars(ctx, bounds, item.points, minX, maxX, 0, maxY, item.color, offset, item.alpha);
});
}
function prepCanvas(canvas) {
const ratio = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = Math.max(320, Math.round(rect.width * ratio));
canvas.height = Math.max(220, Math.round(rect.height * ratio));
const ctx = canvas.getContext("2d");
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
ctx.clearRect(0, 0, rect.width, rect.height);
ctx.font = "12px Segoe UI, Arial";
ctx.lineCap = "round";
ctx.lineJoin = "round";
return ctx;
}
function chartBounds(canvas) {
const rect = canvas.getBoundingClientRect();
return { left:52, top:18, right:rect.width - 18, bottom:rect.height - 34, width:rect.width, height:rect.height };
}
function drawAxes(ctx, b, minX, maxX, minY, maxY, yLabel) {
ctx.strokeStyle = "#d8e0ea";
ctx.lineWidth = 1;
ctx.fillStyle = "#667085";
for (let i = 0; i <= 4; i++) {
const y = b.bottom - ((b.bottom - b.top) * i / 4);
const value = minY + ((maxY - minY) * i / 4);
ctx.beginPath();
ctx.moveTo(b.left, y);
ctx.lineTo(b.right, y);
ctx.stroke();
ctx.fillText(fmt(value), 8, y + 4);
}
ctx.fillText(yLabel, b.left, 11);
ctx.fillText(String(minX), b.left, b.height - 12);
ctx.fillText(String(maxX), b.right - 22, b.height - 12);
}
function drawPath(ctx, b, points, minX, maxX, minY, maxY, color) {
if (!points.length) return;
ctx.strokeStyle = color;
ctx.lineWidth = 2.4;
ctx.beginPath();
points.forEach((point, index) => {
const x = scale(point.x, minX, maxX, b.left, b.right);
const y = scale(point.y, minY, maxY, b.bottom, b.top);
if (index === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
ctx.fillStyle = color;
points.forEach(point => {
const x = scale(point.x, minX, maxX, b.left, b.right);
const y = scale(point.y, minY, maxY, b.bottom, b.top);
ctx.beginPath();
ctx.arc(x, y, 2.7, 0, Math.PI * 2);
ctx.fill();
});
}
function drawBars(ctx, b, points, minX, maxX, minY, maxY, color, offset, alpha = 0.82) {
ctx.fillStyle = color;
const width = Math.max(3, Math.min(10, (b.right - b.left) / Math.max(12, points.length * 4)));
for (const point of points) {
const x = scale(point.x, minX, maxX, b.left, b.right) + offset;
const y = scale(point.y, minY, maxY, b.bottom, b.top);
ctx.globalAlpha = alpha;
ctx.fillRect(x - width / 2, y, width, b.bottom - y);
ctx.globalAlpha = 1;
}
}
function renderHeldout(comparisons) {
const rows = [];
for (const role of compareRoles) {
const base = comparisons.find(row => row.role === role.key && row.label === "base");
const variants = comparisons.filter(row => row.role === role.key && row.label !== "base");
for (const variant of variants) rows.push({ role:role.key, base, variant });
}
$("comparisonNote").textContent = rows.length ? "base vs SFT / GRPO variants" : "no held-out eval rows";
renderVerdicts(comparisons);
drawHeldoutBars("heldoutPlot", comparisons);
$("heldout").innerHTML = rows.length ? rows.map(row => heldoutRow(row)).join("") : `<tr><td colspan="10"><div class="empty">No held-out comparisons available.</div></td></tr>`;
}
function renderVerdicts(comparisons) {
const cards = compareRoles.map(role => {
const base = comparisons.find(row => row.role === role.key && row.label === "base");
const sft = comparisons.find(row => row.role === role.key && row.label === "sft");
const grpo = comparisons.find(row => row.role === role.key && row.label === "grpo");
if (!base) return verdictCard(role.label, "pending", "No held-out base run yet.", "warn");
const roleKey = roleScoreKey(role.key);
const sftDelta = sft ? Number(sft[roleKey]) - Number(base[roleKey]) : null;
const grpoDelta = grpo && sft ? Number(grpo[roleKey]) - Number(sft[roleKey]) : null;
const invalidNote = [sft, grpo].filter(Boolean).map(row => `${row.label}: ${Number(row.invalid_payloads || 0)} invalid payloads`).join(" · ");
if (sftDelta === null) return verdictCard(role.label, "waiting for SFT eval", invalidNote || "SFT result not available yet.", "warn");
if (sftDelta > 0.01 && (grpoDelta === null || grpoDelta >= -0.005)) {
return verdictCard(role.label, "format/scaffold improved", `SFT role score ${signed(sftDelta)} vs base. ${invalidNote}`, "good");
}
if (grpoDelta !== null && grpoDelta > 0.01) {
return verdictCard(role.label, "RL improved after SFT", `GRPO role score ${signed(grpoDelta)} vs SFT. ${invalidNote}`, "good");
}
if (sftDelta <= 0 && (grpoDelta === null || grpoDelta <= 0)) {
return verdictCard(role.label, "no held-out lift yet", `SFT ${signed(sftDelta)} vs base${grpoDelta === null ? "" : `, GRPO ${signed(grpoDelta)} vs SFT`}. ${invalidNote}`, "bad");
}
return verdictCard(role.label, "mixed result", `SFT ${signed(sftDelta)} vs base${grpoDelta === null ? "" : `, GRPO ${signed(grpoDelta)} vs SFT`}. ${invalidNote}`, "warn");
});
$("verdicts").innerHTML = cards.join("");
}
function verdictCard(role, title, note, status) {
return `<div class="verdict ${escapeHtml(status)}"><strong>${escapeHtml(role)}: ${escapeHtml(title)}</strong><span>${escapeHtml(note)}</span></div>`;
}
function drawHeldoutBars(id, comparisons) {
const canvas = $(id);
const ctx = prepCanvas(canvas);
const b = chartBounds(canvas);
const groups = [];
for (const role of compareRoles) {
for (const metric of [
{ key:"mean_global_score", label:"global" },
{ key:roleScoreKey(role.key), label:"role" },
]) {
const rows = ["base", "sft", "grpo"].map(label => comparisons.find(row => row.role === role.key && row.label === label));
if (rows.some(Boolean)) groups.push({ role:role.key, metric:metric.label, rows, key:metric.key });
}
}
if (!groups.length) return drawEmpty(ctx, canvas, "No held-out comparison rows yet");
drawAxes(ctx, b, 0, groups.length, 0, 1, "score");
const colors = { base:"#64748b", sft:"#2563eb", grpo:"#7c3aed" };
const labels = ["base", "sft", "grpo"];
const groupWidth = (b.right - b.left) / groups.length;
groups.forEach((group, groupIndex) => {
labels.forEach((label, labelIndex) => {
const row = group.rows[labelIndex];
if (!row) return;
const value = Number(row[group.key]);
if (!Number.isFinite(value)) return;
const barW = Math.min(18, groupWidth / 5);
const x = b.left + groupIndex * groupWidth + groupWidth / 2 + (labelIndex - 1) * (barW + 3);
const y = scale(Math.max(0, Math.min(1, value)), 0, 1, b.bottom, b.top);
ctx.fillStyle = colors[label];
ctx.fillRect(x - barW / 2, y, barW, b.bottom - y);
});
ctx.fillStyle = "#667085";
ctx.textAlign = "center";
ctx.fillText(`${group.role} ${group.metric}`, b.left + groupIndex * groupWidth + groupWidth / 2, b.height - 12);
ctx.textAlign = "left";
});
renderLegend("comparisonNote", [
{ label:"base", color:colors.base },
{ label:"SFT", color:colors.sft },
{ label:"GRPO", color:colors.grpo },
]);
}
function heldoutRow({ role, base, variant }) {
const roleKey = roleScoreKey(role);
const globalDelta = Number(variant?.mean_global_score) - Number(base?.mean_global_score);
const roleDelta = Number(variant?.[roleKey]) - Number(base?.[roleKey]);
const rawDelta = Number(variant?.mean_raw_reward) - Number(base?.mean_raw_reward);
const invalids = `${Number(variant?.invalid_payloads ?? 0)} / ${Number(variant?.invalid_actions ?? 0)}`;
return `
<tr>
<td>${escapeHtml(role)}</td>
<td>${escapeHtml(variant?.label || "variant")}</td>
<td class="number">${fmt(base?.mean_global_score)}</td>
<td class="number">${fmt(variant?.mean_global_score)}</td>
<td class="number ${deltaClass(globalDelta)}">${signed(globalDelta)}</td>
<td class="number">${fmt(base?.[roleKey])}</td>
<td class="number">${fmt(variant?.[roleKey])}</td>
<td class="number ${deltaClass(roleDelta)}">${signed(roleDelta)}</td>
<td class="number">${invalids}</td>
<td class="number ${deltaClass(rawDelta)}">${signed(rawDelta, 2)}</td>
</tr>`;
}
function deltaClass(value) {
if (!Number.isFinite(value) || value === 0) return "";
return value > 0 ? "delta good" : "delta bad";
}
function signed(value, digits = 3) {
if (!Number.isFinite(value)) return "pending";
return (value >= 0 ? "+" : "") + fmt(value, digits);
}
function scale(value, minIn, maxIn, minOut, maxOut) {
if (minIn === maxIn) return (minOut + maxOut) / 2;
return minOut + ((value - minIn) / (maxIn - minIn)) * (maxOut - minOut);
}
function avg(values) {
const nums = values.map(Number).filter(Number.isFinite);
return nums.length ? nums.reduce((sum, n) => sum + n, 0) / nums.length : null;
}
function drawEmpty(ctx, canvas, message) {
const rect = canvas.getBoundingClientRect();
ctx.fillStyle = "#667085";
ctx.textAlign = "center";
ctx.fillText(message, rect.width / 2, rect.height / 2);
ctx.textAlign = "left";
}
function escapeHtml(value) {
return String(value ?? "").replace(/[&<>"']/g, char => ({
"&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;", "'":"&#39;"
})[char]);
}
$("refresh").onclick = load;
window.addEventListener("resize", () => load());
load();
</script>
</body>
</html>