Training: steps instead of tabs, walked with Back and Next; no way on to training with an empty set; Results leads back to the set and its tags

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MpnF7GwNGMbngU11wjym7P
This commit is contained in:
Claude
2026-10-20 15:52:22 +00:00
parent 354ca29e6a
commit 1110194d22
3 changed files with 37 additions and 15 deletions
+16 -12
View File
@@ -9,9 +9,9 @@ import { makeSession, srcKey } from "./state.js";
const VIDEO = /\.(mp4|webm|mov|mkv)$/i;
const TABS = [{ value: "data", label: "1 Your media" }, { value: "tags", label: "2 Describe" }, { value: "train", label: "3 Train" }, { value: "results", label: "4 Results" }];
const TABS = [{ value: "data" }, { value: "tags" }, { value: "train" }, { value: "results" }]; // the steps, in order: Back and Next walk them
const STEPS = {
data: { title: "Pick what to learn from", sub: "Drop pictures and clips, choose from your Bin, or take results you rated. This step is optional: a set you made earlier is ready as it is." },
data: { title: "Pick what to learn from", sub: "Drop pictures and clips, choose from your Bin, or take results you rated. A set you filled earlier can go straight on; an empty one cannot be trained." },
tags: { title: "Describe each one", sub: "The words under each picture are what the LoRA learns to connect with it. Fix them by hand, or let the tagger suggest." },
train: { title: "Train the LoRA", sub: "Choose how thorough it should be and press Start. Progress and previews appear in Results." },
results: { title: "Results", sub: "Watch the training, look at the previews and download a LoRA, the final one or any step." },
@@ -74,19 +74,23 @@ export default {
train: () => trainStep({ app, session, go, ui }),
results: () => resultsStep({ app, ui }),
};
function go(tab) { ui.tab = tab; if (tab === "results") loadRuns(); draw(); }
function go(tab) { if (tab !== "data" && tab !== "results" && !((session.doc || {}).items || []).length) tab = "data"; ui.tab = tab; if (tab === "results") loadRuns(); draw(); }
function draw() {
if (!win) return;
const tabs = c.segmented.md({ label: "Steps", options: TABS, value: ui.tab, onChange: go });
const note = session.error ? c.inlineError.default({ text: session.error }) : null;
const info = STEPS[ui.tab];
body.set([tabs, c.label.section({ text: info.title }), c.hint.default({ text: info.sub }), note, steps[ui.tab]()].filter(Boolean));
const order = TABS.map((t) => t.value), at = order.indexOf(ui.tab);
const back = at ? c.button.sm({ label: ui.tab === "results" ? "Back to training" : "Back", tone: "ghost", onClick: () => go(order[at - 1]) }) : null;
const skip = ui.tab === "data" ? c.button.sm({ label: "Skip to training", tone: "ghost", onClick: () => go("train") }) : null;
const next = ui.tab === "data" ? c.button.sm({ label: "Next: tags", tone: "primary", disabled: !session.name, onClick: () => go("tags") })
: ui.tab === "tags" ? c.button.sm({ label: "Next: train", tone: "primary", onClick: () => go("train") }) : null;
foot = () => win && win.setFooter({ actions: [back, skip, next, ...(ui.tab === "train" ? ui.controls || [] : [])].filter(Boolean), note: ui.tab === "train" ? footNote(ui) : "" });
const info = STEPS[ui.tab], order = TABS.map((t) => t.value), at = order.indexOf(ui.tab);
const ready = ((session.doc || {}).items || []).length > 0; // nothing to learn from: no way on to training
body.set([c.label.section({ text: `${at + 1} / ${order.length} · ${info.title}` }), c.hint.default({ text: info.sub }), note, steps[ui.tab]()].filter(Boolean));
const results = (ui.tab === "data" || ui.tab === "train") ? c.button.sm({ label: "See results", tone: "ghost", title: "Earlier runs, the live graph and downloads", onClick: () => go("results") }) : null;
const back = ui.tab === "results" ? c.button.sm({ label: "Back to training", tone: "ghost", onClick: () => go("train") }) // a run carries on while another set is made or tags are fixed
: at ? c.button.sm({ label: "Back", tone: "ghost", onClick: () => go(order[at - 1]) }) : null;
const skip = ui.tab === "data" ? c.button.sm({ label: "Skip to training", tone: "ghost", disabled: !ready, title: ready ? "" : "Add pictures or clips to the set first", onClick: () => go("train") }) : null;
const next = ui.tab === "data" ? c.button.sm({ label: "Next: tags", tone: "primary", disabled: !ready, title: ready ? "" : "Add pictures or clips to the set first", onClick: () => go("tags") })
: ui.tab === "tags" ? c.button.sm({ label: "Next: train", tone: "primary", disabled: !ready, onClick: () => go("train") }) : null;
const elsewhere = ui.tab === "results" ? [c.button.sm({ label: "Prepare a set", tone: "ghost", title: "Make or fill a set; a training going on carries on", onClick: () => go("data") }),
c.button.sm({ label: "Fix tags", tone: "ghost", title: "Correct the captions; a training going on carries on", onClick: () => go("tags") })] : [];
const actions = [results, ...elsewhere, back, skip, next];
foot = () => win && win.setFooter({ actions: [...actions, ...(ui.tab === "train" ? ui.controls || [] : [])].filter(Boolean), note: ui.tab === "train" ? footNote(ui) : "" });
foot();
}
+1 -1
View File
@@ -126,7 +126,7 @@ export function notice(ui, go) {
const where = c.button.sm({ label: "Open Results", tone: "ghost", onClick: () => go("results") });
if (run.error) return c.region.stack({ gap: "md", children: [c.inlineError.default({ text: run.error }), where] });
const text = { done: "Training finished.", stopped: "Training stopped.", paused: `Training paused at step ${run.step || 0}.`, previewing: "Making a preview." }[run.state] || "Training started.";
return c.region.stack({ gap: "md", children: [c.banner.info({ text: `${aboutControls(run.message) ? `${run.message}. ` : ""}${text} Check graphs and previews in the Results tab.` }), where] });
return c.region.stack({ gap: "md", children: [c.banner.info({ text: `${aboutControls(run.message) ? `${run.message}. ` : ""}${text} Graphs and previews are on the Results step.` }), where] });
}
/** Progress, the message, and the loss graph. */
+20 -2
View File
@@ -47,7 +47,7 @@ const open = async (f) => {
return { host, button, off };
};
const text = () => document.body.textContent;
const tab = (label) => [...document.querySelectorAll("button, [role=tab]")].find((b) => b.textContent.trim().endsWith(label));
const tab = (label) => click(label === "Results" ? "See results" : label === "Train" ? "Back to training" : label);
const click = (label, root = document) => [...root.querySelectorAll("button")].find((b) => b.textContent.trim() === label);
test("captions: tags are trimmed and unique, counts are per caption, bulk edits only the chosen", () => {
@@ -202,7 +202,7 @@ test("a run in progress draws its loss, then reports the saved file; Stop asks t
await wait(20);
click("Start training").click();
await wait(1200);
assert.ok(text().includes("Check graphs and previews in the Results tab"), "Train only says where to look");
assert.ok(text().includes("Graphs and previews are on the Results step"), "Train only says where to look");
assert.ok(!document.querySelector(".cx-chart"), "the graph is not on the Train tab");
tab("Results").click();
await wait(20);
@@ -525,3 +525,21 @@ test("Data step: results whose rating may be from another video are marked, not
assert.ok(text().includes('1 marked "old rating?"'), "the count is shown, and nothing is hidden");
off();
});
test("Steps: no tabs; with no media the way on to training is closed, and Results leads back to the set and its tags", async () => {
const f = fake();
f.doc.items = [];
const { button, off } = await open(f);
button.click();
await wait(60);
assert.ok(!document.querySelector("[role=tab], [role=tablist]"), "no tab strip");
assert.ok(text().includes("1 / 4"), "the step says where it is");
assert.ok(click("Next: tags").disabled && click("Skip to training").disabled, "no media, no training");
click("See results").click();
await wait(40);
assert.ok(click("Prepare a set") && click("Fix tags"), "back to the set or its tags while a run goes on");
click("Fix tags").click();
await wait(40);
assert.ok(text().includes("1 / 4"), "an empty set sends you to add media first");
off();
});