Simplify output editor header

This commit is contained in:
Meta Vens 2026-04-26 04:36:09 +01:00
commit e45b915604
4 changed files with 38 additions and 123 deletions

View file

@ -140,7 +140,6 @@ type AppState = {
cleanupImage: ImageAsset | null;
heatmapImage: ImageAsset | null;
finalOutputImage: ImageAsset | null;
runSummary: JsonRecord | null;
runtimeConfig: RuntimeConfig | null;
queueSummary: QueueSummary | null;
queuePosition: number | null;
@ -201,8 +200,7 @@ const gridToggle = byId<HTMLInputElement>("gridToggle");
const editorCanvas = byId<HTMLCanvasElement>("editorCanvas");
const editorGridCanvas = byId<HTMLCanvasElement>("editorGridCanvas");
const editorSurface = byId<HTMLDivElement>("editorSurface");
const editorMeta = byId<HTMLDivElement>("editorMeta");
const summaryPanel = byId<HTMLDivElement>("summaryPanel");
const editorCanvasSizeValue = byId<HTMLSpanElement>("editorCanvasSizeValue");
const resetEditorButton = byId<HTMLButtonElement>("resetEditorButton");
const downloadButton = byId<HTMLButtonElement>("downloadButton");
@ -277,7 +275,6 @@ const state: AppState = {
cleanupImage: null,
heatmapImage: null,
finalOutputImage: null,
runSummary: null,
runtimeConfig: null,
queueSummary: null,
queuePosition: null,
@ -524,7 +521,6 @@ function resetRunArtifacts(options: { preserveSourceImage?: boolean } = {}): voi
state.cleanupImage = null;
state.heatmapImage = null;
state.finalOutputImage = null;
state.runSummary = null;
state.queuePosition = null;
state.queueDepth = 0;
state.waitingCount = 0;
@ -670,17 +666,6 @@ async function fileToImageAsset(file: File): Promise<ImageAsset> {
};
}
function readNestedNumber(root: JsonRecord | null, path: string[]): number | null {
let current: unknown = root;
for (const key of path) {
if (!current || typeof current !== "object" || !(key in current)) {
return null;
}
current = (current as JsonRecord)[key];
}
return typeof current === "number" ? current : null;
}
function formatNumber(value: number | null, digits = 3): string {
if (value === null || !Number.isFinite(value)) {
return "n/a";
@ -858,27 +843,6 @@ function renderStatusMetrics(): void {
renderStatusMetricItems(items);
}
function renderSummary(): void {
summaryPanel.innerHTML = "";
if (!state.runSummary) {
summaryPanel.innerHTML = `<p class="muted">Final metrics show up here after the run finishes.</p>`;
return;
}
const entries: Array<[string, number | null]> = [
["Structure score", readNestedNumber(state.runSummary, ["source_structure", "score"])],
["Edge F1", readNestedNumber(state.runSummary, ["source_structure", "edge_f1"])],
["Exact match", readNestedNumber(state.runSummary, ["source_structure", "exact_match_ratio"])],
["Final fidelity", readNestedNumber(state.runSummary, ["source_fidelity", "final_output", "score"])],
["Mean displacement", readNestedNumber(state.runSummary, ["optimizer_displacement", "final_output", "mean_magnitude_px"])],
];
for (const [label, value] of entries) {
const card = document.createElement("div");
card.className = "info-card summary-card";
card.innerHTML = `<strong>${label}</strong><span>${formatNumber(value, 3)}</span>`;
summaryPanel.appendChild(card);
}
}
function resolveViewerPanels(): Record<ViewerPanelKey, ViewerPanelState> {
const frame = getSelectedFrame();
const debugViewerStage = new Set(["inference", "analysis", "selection", "rerank", "solver", "cleanup"]);
@ -1199,7 +1163,7 @@ function renderEditor(): void {
editorCanvas.height = 1;
editorGridCanvas.width = 1;
editorGridCanvas.height = 1;
editorMeta.innerHTML = `<p class="muted">Final output lands here once the solver finishes its little pilgrimage.</p>`;
editorCanvasSizeValue.textContent = "n/a";
return;
}
const width = offscreenCanvas.width * state.zoom;
@ -1233,17 +1197,7 @@ function renderEditor(): void {
gridContext.stroke();
}
}
editorMeta.innerHTML = `
<div class="info-card summary-card">
<strong>Canvas</strong>
<span>${offscreenCanvas.width} x ${offscreenCanvas.height}</span>
</div>
<div class="info-card summary-card">
<strong>State</strong>
<span>${state.editorDirty ? "Edited" : "Matches solver output"}</span>
</div>
`;
editorCanvasSizeValue.textContent = `${offscreenCanvas.width} x ${offscreenCanvas.height}`;
}
function setZoom(nextZoom: number, anchor: ViewportAnchor | null = null): void {
@ -1460,9 +1414,6 @@ function applyJobSnapshot(snapshot: Partial<JobStateSnapshot>): void {
if (typeof snapshot.status === "string") {
setJobState(snapshot.status);
}
if (snapshot.runSummary && typeof snapshot.runSummary === "object") {
state.runSummary = snapshot.runSummary;
}
}
async function sendHeartbeat(): Promise<void> {
@ -1571,7 +1522,6 @@ async function renderEverything(): Promise<void> {
renderInference();
renderStatusMetrics();
renderLossChart();
renderSummary();
await renderViewer();
}
@ -1813,7 +1763,6 @@ async function handleEvent(eventName: string, payload: JsonRecord): Promise<void
break;
case "pipeline_completed":
state.finalOutputImage = payload.outputImage as ImageAsset;
state.runSummary = (payload.runSummary as JsonRecord | null) ?? null;
stopHeartbeat();
setJobState("completed");
setStage("completed", "Run complete", "The output is ready. If the machine still did something stupid, fix it pixel by pixel.");

View file

@ -33,8 +33,7 @@ const gridToggle = byId("gridToggle");
const editorCanvas = byId("editorCanvas");
const editorGridCanvas = byId("editorGridCanvas");
const editorSurface = byId("editorSurface");
const editorMeta = byId("editorMeta");
const summaryPanel = byId("summaryPanel");
const editorCanvasSizeValue = byId("editorCanvasSizeValue");
const resetEditorButton = byId("resetEditorButton");
const downloadButton = byId("downloadButton");
const targetSizeInput = byId("targetSizeInput");
@ -105,7 +104,6 @@ const state = {
cleanupImage: null,
heatmapImage: null,
finalOutputImage: null,
runSummary: null,
runtimeConfig: null,
queueSummary: null,
queuePosition: null,
@ -336,7 +334,6 @@ function resetRunArtifacts(options = {}) {
state.cleanupImage = null;
state.heatmapImage = null;
state.finalOutputImage = null;
state.runSummary = null;
state.queuePosition = null;
state.queueDepth = 0;
state.waitingCount = 0;
@ -459,16 +456,6 @@ async function fileToImageAsset(file) {
height: image.naturalHeight || 1,
};
}
function readNestedNumber(root, path) {
let current = root;
for (const key of path) {
if (!current || typeof current !== "object" || !(key in current)) {
return null;
}
current = current[key];
}
return typeof current === "number" ? current : null;
}
function formatNumber(value, digits = 3) {
if (value === null || !Number.isFinite(value)) {
return "n/a";
@ -635,26 +622,6 @@ function renderStatusMetrics() {
}
renderStatusMetricItems(items);
}
function renderSummary() {
summaryPanel.innerHTML = "";
if (!state.runSummary) {
summaryPanel.innerHTML = `<p class="muted">Final metrics show up here after the run finishes.</p>`;
return;
}
const entries = [
["Structure score", readNestedNumber(state.runSummary, ["source_structure", "score"])],
["Edge F1", readNestedNumber(state.runSummary, ["source_structure", "edge_f1"])],
["Exact match", readNestedNumber(state.runSummary, ["source_structure", "exact_match_ratio"])],
["Final fidelity", readNestedNumber(state.runSummary, ["source_fidelity", "final_output", "score"])],
["Mean displacement", readNestedNumber(state.runSummary, ["optimizer_displacement", "final_output", "mean_magnitude_px"])],
];
for (const [label, value] of entries) {
const card = document.createElement("div");
card.className = "info-card summary-card";
card.innerHTML = `<strong>${label}</strong><span>${formatNumber(value, 3)}</span>`;
summaryPanel.appendChild(card);
}
}
function resolveViewerPanels() {
const frame = getSelectedFrame();
const debugViewerStage = new Set(["inference", "analysis", "selection", "rerank", "solver", "cleanup"]);
@ -949,7 +916,7 @@ function renderEditor() {
editorCanvas.height = 1;
editorGridCanvas.width = 1;
editorGridCanvas.height = 1;
editorMeta.innerHTML = `<p class="muted">Final output lands here once the solver finishes its little pilgrimage.</p>`;
editorCanvasSizeValue.textContent = "n/a";
return;
}
const width = offscreenCanvas.width * state.zoom;
@ -982,16 +949,7 @@ function renderEditor() {
gridContext.stroke();
}
}
editorMeta.innerHTML = `
<div class="info-card summary-card">
<strong>Canvas</strong>
<span>${offscreenCanvas.width} x ${offscreenCanvas.height}</span>
</div>
<div class="info-card summary-card">
<strong>State</strong>
<span>${state.editorDirty ? "Edited" : "Matches solver output"}</span>
</div>
`;
editorCanvasSizeValue.textContent = `${offscreenCanvas.width} x ${offscreenCanvas.height}`;
}
function setZoom(nextZoom, anchor = null) {
const clampedZoom = clampZoom(nextZoom);
@ -1189,9 +1147,6 @@ function applyJobSnapshot(snapshot) {
if (typeof snapshot.status === "string") {
setJobState(snapshot.status);
}
if (snapshot.runSummary && typeof snapshot.runSummary === "object") {
state.runSummary = snapshot.runSummary;
}
}
async function sendHeartbeat() {
if (!state.jobId || !hasActiveJob()) {
@ -1295,7 +1250,6 @@ async function renderEverything() {
renderInference();
renderStatusMetrics();
renderLossChart();
renderSummary();
await renderViewer();
}
async function handleEvent(eventName, payload) {
@ -1498,7 +1452,6 @@ async function handleEvent(eventName, payload) {
break;
case "pipeline_completed":
state.finalOutputImage = payload.outputImage;
state.runSummary = payload.runSummary ?? null;
stopHeartbeat();
setJobState("completed");
setStage("completed", "Run complete", "The output is ready. If the machine still did something stupid, fix it pixel by pixel.");

View file

@ -97,8 +97,14 @@
<p class="copy-tag">Pencil the stragglers. Wheel zooms to cursor, middle mouse pans, Alt steals color.</p>
</div>
<div class="editor-actions">
<button id="resetEditorButton" class="button button-small button-secondary" type="button">Reset Output</button>
<button id="downloadButton" class="button button-small" type="button">Export PNG</button>
<div id="editorCanvasSizeCard" class="info-card editor-canvas-card">
<strong>Canvas</strong>
<span id="editorCanvasSizeValue">n/a</span>
</div>
<div class="editor-button-group">
<button id="resetEditorButton" class="button button-small button-secondary" type="button">Reset Output</button>
<button id="downloadButton" class="button button-small" type="button">Export PNG</button>
</div>
</div>
</div>
@ -140,12 +146,6 @@
<canvas id="editorCanvas"></canvas>
<canvas id="editorGridCanvas"></canvas>
</div>
<div class="editor-meta">
<div id="editorMeta" class="info-stack">
<p class="muted">Final output lands here once the solver finishes its little pilgrimage.</p>
</div>
<div id="summaryPanel" class="summary-panel"></div>
</div>
</div>
</section>
</main>

View file

@ -191,8 +191,6 @@ p {
.sidebar,
.workspace,
.sidebar-section,
.editor-meta,
.summary-panel,
.candidate-list,
.event-log,
.info-stack {
@ -207,7 +205,6 @@ p {
.sidebar,
.sidebar-section,
.summary-panel,
.candidate-list,
.event-log,
.info-stack,
@ -865,6 +862,25 @@ select.field-control optgroup {
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
margin-left: auto;
}
.editor-button-group {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
.editor-canvas-card {
min-width: 148px;
padding: 10px 12px;
}
.editor-canvas-card span {
color: var(--ui-text-bright);
font-variant-numeric: tabular-nums;
}
.editor-toolbar {
@ -968,10 +984,7 @@ select.field-control optgroup {
}
.editor-stage {
display: grid;
grid-template-columns: minmax(0, 1fr) 290px;
gap: 12px;
align-items: start;
display: block;
}
.checkerboard {
@ -1011,10 +1024,6 @@ select.field-control optgroup {
pointer-events: none;
}
.summary-panel {
align-content: start;
}
input[type="range"] {
width: 100%;
accent-color: var(--ui-accent);
@ -1095,6 +1104,10 @@ select:focus-visible {
width: 100%;
}
.editor-button-group {
width: 100%;
}
.status-metrics {
grid-template-columns: repeat(var(--status-metric-count), minmax(88px, 1fr));
}