Files

56 lines
4.8 KiB
JavaScript

// Step 4, Results: the runs that finished, with their loss graphs.
import { composer as c } from "../../composer/composer.js";
import { clock, fmt, monitor, seconds, stepTimes } from "./train.js";
const STATES = { done: "Finished", error: "Failed", stopped: "Stopped", training: "Running" };
export function resultsStep({ app, ui }) {
const runs = ui.runs.filter((r) => !(ui.run.active && r.run === ui.run.runId)); // the run going on is shown above, not a second time from its old record
const live = c.region.stack({ gap: "md", children: [monitor(ui)].filter(Boolean) }); // the run going on now, redrawn on its own
ui.monitorBox = live;
if (!runs.length) return ui.run.shown ? live : c.emptyState.default({ title: "No runs yet", hint: "A run you start in the Train step is kept here, with its loss graph." });
const chosen = runs.find((r) => r.run === ui.chosenRun) || runs[0];
const rows = runs.map((r) => ({ id: r.run, label: `${r.name || "LoRA"} · ${STATES[r.state] || r.state}`, hint: `${r.set} · ${r.steps || 0} steps${r.final_loss != null ? ` · loss ${fmt(r.final_loss)}` : ""}` }));
const list = c.gallery.rows({ items: rows, selection: [chosen.run], onActivate: (item) => { ui.chosenRun = item.id; ui.loadRecord(item.id); ui.redraw(); } });
const rec = ui.record && ui.record.run === chosen.run ? ui.record : null;
const chart = c.chart.line({ series: rec && rec.loss ? [{ name: "loss", values: rec.loss, tone: "accent" }] : [], xLabel: "step", yLabel: "loss", height: "md", empty: rec ? "No loss was recorded" : "Loading" });
const facts = rec ? [
`${STATES[rec.state] || rec.state}`, `${rec.steps || 0} of ${rec.total || 0} steps`, rec.seconds ? `${clock(rec.seconds)} in all` : null, rec.step_seconds ? `${seconds(rec.step_seconds)} per step` : null,
rec.final_loss != null ? `final loss ${fmt(rec.final_loss)}` : null, rec.set ? `from "${rec.set}"` : null,
].filter(Boolean).join(" · ") : "";
const s = (rec && rec.settings) || {};
const settings = rec ? ["algorithm", "rank", "learning_rate", "optimizer", "loss", "resolution", "batch_size", "use"].filter((k) => s[k] !== undefined).map((k) => `${k.replace("_", " ")} ${s[k]}`).join(" · ") : "";
return c.region.stack({ gap: "lg", children: [
live,
list,
c.group.default({ label: chosen.name || "LoRA", rows: [c.region.stack({ gap: "lg", children: [
c.hint.default({ text: facts }),
rec && rec.lora ? c.banner.info({ text: `File: ${rec.lora}. Pick it in a LoRA loader to use it.` }) : null,
rec && rec.error ? c.inlineError.default({ text: rec.error }) : null,
rec && rec.resumable && ["error", "stopped"].includes(rec.state) ? c.banner.info({ text: `The state at step ${rec.steps || 0} or earlier is kept. Restore and continue picks the training up from it with the Train step's current settings.` }) : null,
chart, stepTimes(rec && rec.times), c.hint.default({ text: settings }),
c.toolbar.default({ items: [...(rec ? downloads(app, ui, rec) : []), ...(rec && rec.resumable && !ui.run.active && !["training", "queued", "previewing", "paused"].includes(ui.run.state) ? [c.button.sm({ label: "Restore and continue", tone: "primary", title: "Carry on from the saved state", onClick: async () => { await ui.restore(chosen.run); } })] : [])], trailing: [c.button.sm({ label: "Delete this record", tone: "ghost", title: "Removes the record only, not the LoRA file", onClick: async () => { await app.api.trainForgetRun(chosen.run); ui.chosenRun = ""; await ui.loadRuns(); ui.redraw(); } })] }),
].filter(Boolean) })] }),
] });
}
const nameOf = (file, rec) => file === rec.lora ? "Finished LoRA" : /_preview(_\d+)?\.safetensors$/.test(file) ? "Last preview" : `Step ${(/_step(\d+)/.exec(file) || [])[1]}`;
/** Saves a LoRA of the run through the browser, to wherever the person keeps files: an instance's own disk is gone when it is. */
function downloads(app, ui, rec) {
const files = rec.files || (rec.lora ? [rec.lora] : []);
if (!files.length) return [];
ui.fileChoice = ui.fileChoice || {};
const chosen = () => (files.includes(ui.fileChoice[rec.run]) ? ui.fileChoice[rec.run] : files[0]);
const file = chosen();
const pick = files.length > 1 ? [c.select.sm({ label: "File to download", options: files.map((f) => ({ value: f, label: nameOf(f, rec) })), value: file, onChange: (v) => { ui.fileChoice[rec.run] = v; } })] : [];
return [...pick, c.button.sm({ label: "Download LoRA", tone: "primary", title: `Save ${file} to this computer`, onClick: async () => {
const file = chosen();
try {
const res = await fetch(app.api.trainLoraUrl(rec.run, file === rec.lora ? "" : file));
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).why || `The server refused (${res.status}).`);
app.dom.saveBlob(await res.blob(), file);
} catch (err) { c.toast.warn({ text: err.message }); }
} })];
}