diff --git a/frontend/src/app.ts b/frontend/src/app.ts
index e5941da..5914405 100644
--- a/frontend/src/app.ts
+++ b/frontend/src/app.ts
@@ -417,6 +417,37 @@ function formatNumber(value: number | null, digits = 3): string {
return value.toFixed(digits);
}
+type StatusMetricItem = {
+ label: string;
+ value: string;
+};
+
+function formatGridValue(width: number | null | undefined, height: number | null | undefined, fallback = "pending"): string {
+ if (!width || !height) {
+ return fallback;
+ }
+ return `${width} x ${height}`;
+}
+
+function formatPhaseValue(phaseX: number | null | undefined, phaseY: number | null | undefined, fallback = "pending"): string {
+ if (phaseX === null || phaseX === undefined || phaseY === null || phaseY === undefined) {
+ return fallback;
+ }
+ return `${formatNumber(phaseX, 2)}, ${formatNumber(phaseY, 2)}`;
+}
+
+function renderStatusMetricItems(items: StatusMetricItem[]): void {
+ statusMetrics.innerHTML = "";
+ statusMetrics.style.setProperty("--status-metric-count", String(Math.max(items.length, 1)));
+ for (const { label, value } of items) {
+ const node = document.createElement("div");
+ node.className = "status-metric";
+ node.title = `${label}: ${value}`;
+ node.innerHTML = `${label}${value}`;
+ statusMetrics.appendChild(node);
+ }
+}
+
function getSelectedFrame(): FramePayload | null {
if (state.frames.length === 0) {
return null;
@@ -460,87 +491,85 @@ function renderInference(): void {
}
function renderStatusMetrics(): void {
- statusMetrics.innerHTML = "";
- statusMetrics.classList.toggle("status-metrics-fixed", state.stageKey === "solver");
- const items: Array<[string, string]> = [];
+ const items: StatusMetricItem[] = [];
const frame = getSelectedFrame();
if (state.stageKey === "inference") {
if (state.latticeSearch) {
- items.push(["Sizes", `${state.latticeSearch.completedCandidates} / ${state.latticeSearch.candidateCount}`]);
- items.push(["Phase samples", String(state.latticeSearch.phaseSampleCount)]);
- if (state.latticeSearch.currentTargetWidth && state.latticeSearch.currentTargetHeight) {
- items.push(["Current grid", `${state.latticeSearch.currentTargetWidth} x ${state.latticeSearch.currentTargetHeight}`]);
- }
- if (state.latticeSearch.device) {
- items.push(["Device", state.latticeSearch.device]);
- }
- if (state.latticeSearch.bestScore !== null) {
- items.push(["Best score", formatNumber(state.latticeSearch.bestScore, 3)]);
- }
+ items.push({ label: "Sizes", value: `${state.latticeSearch.completedCandidates} / ${state.latticeSearch.candidateCount}` });
+ items.push({ label: "Phases", value: String(state.latticeSearch.phaseSampleCount) });
+ items.push({
+ label: "Grid",
+ value: formatGridValue(state.latticeSearch.currentTargetWidth, state.latticeSearch.currentTargetHeight),
+ });
+ items.push({ label: "Device", value: state.latticeSearch.device ?? "pending" });
+ items.push({ label: "Best", value: formatNumber(state.latticeSearch.bestScore, 3) });
}
} else if (state.stageKey === "analysis" || state.stageKey === "selection") {
if (state.inference) {
- items.push(["Mode", state.inferenceMode === "fixed" ? "pinned" : state.inferenceMode ?? "search"]);
- items.push(["Grid", `${state.inference.target_width} x ${state.inference.target_height}`]);
- items.push(["Phase", `${formatNumber(state.inference.phase_x, 2)}, ${formatNumber(state.inference.phase_y, 2)}`]);
- items.push(["Confidence", formatNumber(state.inference.confidence, 3)]);
+ items.push({ label: "Mode", value: state.inferenceMode === "fixed" ? "pinned" : state.inferenceMode ?? "search" });
+ items.push({ label: "Grid", value: `${state.inference.target_width} x ${state.inference.target_height}` });
+ items.push({ label: "Phase", value: `${formatNumber(state.inference.phase_x, 2)}, ${formatNumber(state.inference.phase_y, 2)}` });
+ items.push({ label: "Confidence", value: formatNumber(state.inference.confidence, 3) });
}
} else if (state.stageKey === "rerank") {
if (state.phaseRerank) {
- items.push(["Candidates", `${state.phaseRerank.completedCandidates} / ${state.phaseRerank.candidateCount}`]);
- if (state.phaseRerank.currentCandidateIndex > 0) {
- items.push(["Active candidate", `${state.phaseRerank.currentCandidateIndex} / ${state.phaseRerank.candidateCount}`]);
- }
- items.push([
- "Preview step",
- state.phaseRerank.currentTotalSteps > 0
- ? `${state.phaseRerank.currentStep} / ${state.phaseRerank.currentTotalSteps}`
- : String(state.phaseRerank.previewSteps),
- ]);
- if (state.phaseRerank.currentTargetWidth && state.phaseRerank.currentTargetHeight) {
- items.push(["Grid", `${state.phaseRerank.currentTargetWidth} x ${state.phaseRerank.currentTargetHeight}`]);
- }
- if (state.phaseRerank.currentPhaseX !== null && state.phaseRerank.currentPhaseY !== null) {
- items.push(["Phase", `${formatNumber(state.phaseRerank.currentPhaseX, 2)}, ${formatNumber(state.phaseRerank.currentPhaseY, 2)}`]);
- }
- if (state.phaseRerank.currentLoss !== null) {
- items.push(["Loss", formatNumber(state.phaseRerank.currentLoss, 4)]);
- }
- items.push(["Confidence", formatNumber(state.phaseRerank.confidence, 3)]);
+ items.push({
+ label: "Done",
+ value: `${state.phaseRerank.completedCandidates} / ${state.phaseRerank.candidateCount}`,
+ });
+ items.push({
+ label: "Candidate",
+ value:
+ state.phaseRerank.currentCandidateIndex > 0
+ ? `${state.phaseRerank.currentCandidateIndex} / ${state.phaseRerank.candidateCount}`
+ : `0 / ${state.phaseRerank.candidateCount}`,
+ });
+ items.push({
+ label: "Preview",
+ value:
+ state.phaseRerank.currentTotalSteps > 0
+ ? `${state.phaseRerank.currentStep} / ${state.phaseRerank.currentTotalSteps}`
+ : state.phaseRerank.previewSteps > 0
+ ? `0 / ${state.phaseRerank.previewSteps}`
+ : "skipped",
+ });
+ items.push({
+ label: "Grid",
+ value: formatGridValue(state.phaseRerank.currentTargetWidth, state.phaseRerank.currentTargetHeight),
+ });
+ items.push({
+ label: "Phase",
+ value: formatPhaseValue(state.phaseRerank.currentPhaseX, state.phaseRerank.currentPhaseY),
+ });
+ items.push({ label: "Loss", value: formatNumber(state.phaseRerank.currentLoss, 4) });
+ items.push({ label: "Confidence", value: formatNumber(state.phaseRerank.confidence, 3) });
}
} else if (state.stageKey === "solver") {
- const solverTerms: Array<[string, number | null]> = [
- ["Coherence", frame?.terms.local_coherence ?? null],
- ["Edge", frame?.terms.local_edge ?? null],
- ["Smoothness", frame?.terms.smoothness ?? null],
- ["Collapse", frame?.terms.collapse ?? null],
- ["Magnitude", frame?.terms.magnitude ?? null],
+ const solverTerms: StatusMetricItem[] = [
+ { label: "Coherence", value: formatNumber(frame?.terms.local_coherence ?? null, 4) },
+ { label: "Edge", value: formatNumber(frame?.terms.local_edge ?? null, 4) },
+ { label: "Smoothness", value: formatNumber(frame?.terms.smoothness ?? null, 4) },
+ { label: "Collapse", value: formatNumber(frame?.terms.collapse ?? null, 4) },
+ { label: "Magnitude", value: formatNumber(frame?.terms.magnitude ?? null, 4) },
];
- items.push([
- "Grid",
- state.phaseFieldPrep ? `${state.phaseFieldPrep.targetWidth} x ${state.phaseFieldPrep.targetHeight}` : "pending",
- ]);
- items.push([
- "Step",
- frame ? `${frame.step} / ${frame.totalSteps}` : `0 / ${Math.max(0, state.solverStepBudget)}`,
- ]);
- items.push([
- "Cell pitch",
- state.phaseFieldPrep
- ? `${formatNumber(state.phaseFieldPrep.cellX, 1)} x ${formatNumber(state.phaseFieldPrep.cellY, 1)} px`
+ items.push({
+ label: "Grid",
+ value: state.phaseFieldPrep ? `${state.phaseFieldPrep.targetWidth} x ${state.phaseFieldPrep.targetHeight}` : "pending",
+ });
+ items.push({
+ label: "Step",
+ value: frame ? `${frame.step} / ${frame.totalSteps}` : `0 / ${Math.max(0, state.solverStepBudget)}`,
+ });
+ items.push({
+ label: "Cell pitch",
+ value: state.phaseFieldPrep
+ ? `${formatNumber(state.phaseFieldPrep.cellX, 1)}x${formatNumber(state.phaseFieldPrep.cellY, 1)} px`
: "pending",
- ]);
- items.push(["Loss", frame?.loss === null || frame?.loss === undefined ? "n/a" : formatNumber(frame.loss, 4)]);
- for (const [label, value] of solverTerms) {
- items.push([label, formatNumber(value, 4)]);
- }
- }
- for (const [label, value] of items) {
- const node = document.createElement("div");
- node.className = "status-metric";
- node.innerHTML = `${label}${value}`;
- statusMetrics.appendChild(node);
+ });
+ items.push({ label: "Loss", value: frame?.loss === null || frame?.loss === undefined ? "n/a" : formatNumber(frame.loss, 4) });
+ items.push(...solverTerms);
}
+ renderStatusMetricItems(items);
}
function renderSummary(): void {
diff --git a/src/repixelizer/gui_static/app.js b/src/repixelizer/gui_static/app.js
index 45312b8..e1facc7 100644
--- a/src/repixelizer/gui_static/app.js
+++ b/src/repixelizer/gui_static/app.js
@@ -282,6 +282,29 @@ function formatNumber(value, digits = 3) {
}
return value.toFixed(digits);
}
+function formatGridValue(width, height, fallback = "pending") {
+ if (!width || !height) {
+ return fallback;
+ }
+ return `${width} x ${height}`;
+}
+function formatPhaseValue(phaseX, phaseY, fallback = "pending") {
+ if (phaseX === null || phaseX === undefined || phaseY === null || phaseY === undefined) {
+ return fallback;
+ }
+ return `${formatNumber(phaseX, 2)}, ${formatNumber(phaseY, 2)}`;
+}
+function renderStatusMetricItems(items) {
+ statusMetrics.innerHTML = "";
+ statusMetrics.style.setProperty("--status-metric-count", String(Math.max(items.length, 1)));
+ for (const { label, value } of items) {
+ const node = document.createElement("div");
+ node.className = "status-metric";
+ node.title = `${label}: ${value}`;
+ node.innerHTML = `${label}${value}`;
+ statusMetrics.appendChild(node);
+ }
+}
function getSelectedFrame() {
if (state.frames.length === 0) {
return null;
@@ -323,90 +346,86 @@ function renderInference() {
}
}
function renderStatusMetrics() {
- statusMetrics.innerHTML = "";
- statusMetrics.classList.toggle("status-metrics-fixed", state.stageKey === "solver");
const items = [];
const frame = getSelectedFrame();
if (state.stageKey === "inference") {
if (state.latticeSearch) {
- items.push(["Sizes", `${state.latticeSearch.completedCandidates} / ${state.latticeSearch.candidateCount}`]);
- items.push(["Phase samples", String(state.latticeSearch.phaseSampleCount)]);
- if (state.latticeSearch.currentTargetWidth && state.latticeSearch.currentTargetHeight) {
- items.push(["Current grid", `${state.latticeSearch.currentTargetWidth} x ${state.latticeSearch.currentTargetHeight}`]);
- }
- if (state.latticeSearch.device) {
- items.push(["Device", state.latticeSearch.device]);
- }
- if (state.latticeSearch.bestScore !== null) {
- items.push(["Best score", formatNumber(state.latticeSearch.bestScore, 3)]);
- }
+ items.push({ label: "Sizes", value: `${state.latticeSearch.completedCandidates} / ${state.latticeSearch.candidateCount}` });
+ items.push({ label: "Phases", value: String(state.latticeSearch.phaseSampleCount) });
+ items.push({
+ label: "Grid",
+ value: formatGridValue(state.latticeSearch.currentTargetWidth, state.latticeSearch.currentTargetHeight),
+ });
+ items.push({ label: "Device", value: state.latticeSearch.device ?? "pending" });
+ items.push({ label: "Best", value: formatNumber(state.latticeSearch.bestScore, 3) });
}
}
else if (state.stageKey === "analysis" || state.stageKey === "selection") {
if (state.inference) {
- items.push(["Mode", state.inferenceMode === "fixed" ? "pinned" : state.inferenceMode ?? "search"]);
- items.push(["Grid", `${state.inference.target_width} x ${state.inference.target_height}`]);
- items.push(["Phase", `${formatNumber(state.inference.phase_x, 2)}, ${formatNumber(state.inference.phase_y, 2)}`]);
- items.push(["Confidence", formatNumber(state.inference.confidence, 3)]);
+ items.push({ label: "Mode", value: state.inferenceMode === "fixed" ? "pinned" : state.inferenceMode ?? "search" });
+ items.push({ label: "Grid", value: `${state.inference.target_width} x ${state.inference.target_height}` });
+ items.push({ label: "Phase", value: `${formatNumber(state.inference.phase_x, 2)}, ${formatNumber(state.inference.phase_y, 2)}` });
+ items.push({ label: "Confidence", value: formatNumber(state.inference.confidence, 3) });
}
}
else if (state.stageKey === "rerank") {
if (state.phaseRerank) {
- items.push(["Candidates", `${state.phaseRerank.completedCandidates} / ${state.phaseRerank.candidateCount}`]);
- if (state.phaseRerank.currentCandidateIndex > 0) {
- items.push(["Active candidate", `${state.phaseRerank.currentCandidateIndex} / ${state.phaseRerank.candidateCount}`]);
- }
- items.push([
- "Preview step",
- state.phaseRerank.currentTotalSteps > 0
+ items.push({
+ label: "Done",
+ value: `${state.phaseRerank.completedCandidates} / ${state.phaseRerank.candidateCount}`,
+ });
+ items.push({
+ label: "Candidate",
+ value: state.phaseRerank.currentCandidateIndex > 0
+ ? `${state.phaseRerank.currentCandidateIndex} / ${state.phaseRerank.candidateCount}`
+ : `0 / ${state.phaseRerank.candidateCount}`,
+ });
+ items.push({
+ label: "Preview",
+ value: state.phaseRerank.currentTotalSteps > 0
? `${state.phaseRerank.currentStep} / ${state.phaseRerank.currentTotalSteps}`
- : String(state.phaseRerank.previewSteps),
- ]);
- if (state.phaseRerank.currentTargetWidth && state.phaseRerank.currentTargetHeight) {
- items.push(["Grid", `${state.phaseRerank.currentTargetWidth} x ${state.phaseRerank.currentTargetHeight}`]);
- }
- if (state.phaseRerank.currentPhaseX !== null && state.phaseRerank.currentPhaseY !== null) {
- items.push(["Phase", `${formatNumber(state.phaseRerank.currentPhaseX, 2)}, ${formatNumber(state.phaseRerank.currentPhaseY, 2)}`]);
- }
- if (state.phaseRerank.currentLoss !== null) {
- items.push(["Loss", formatNumber(state.phaseRerank.currentLoss, 4)]);
- }
- items.push(["Confidence", formatNumber(state.phaseRerank.confidence, 3)]);
+ : state.phaseRerank.previewSteps > 0
+ ? `0 / ${state.phaseRerank.previewSteps}`
+ : "skipped",
+ });
+ items.push({
+ label: "Grid",
+ value: formatGridValue(state.phaseRerank.currentTargetWidth, state.phaseRerank.currentTargetHeight),
+ });
+ items.push({
+ label: "Phase",
+ value: formatPhaseValue(state.phaseRerank.currentPhaseX, state.phaseRerank.currentPhaseY),
+ });
+ items.push({ label: "Loss", value: formatNumber(state.phaseRerank.currentLoss, 4) });
+ items.push({ label: "Confidence", value: formatNumber(state.phaseRerank.confidence, 3) });
}
}
else if (state.stageKey === "solver") {
const solverTerms = [
- ["Coherence", frame?.terms.local_coherence ?? null],
- ["Edge", frame?.terms.local_edge ?? null],
- ["Smoothness", frame?.terms.smoothness ?? null],
- ["Collapse", frame?.terms.collapse ?? null],
- ["Magnitude", frame?.terms.magnitude ?? null],
+ { label: "Coherence", value: formatNumber(frame?.terms.local_coherence ?? null, 4) },
+ { label: "Edge", value: formatNumber(frame?.terms.local_edge ?? null, 4) },
+ { label: "Smoothness", value: formatNumber(frame?.terms.smoothness ?? null, 4) },
+ { label: "Collapse", value: formatNumber(frame?.terms.collapse ?? null, 4) },
+ { label: "Magnitude", value: formatNumber(frame?.terms.magnitude ?? null, 4) },
];
- items.push([
- "Grid",
- state.phaseFieldPrep ? `${state.phaseFieldPrep.targetWidth} x ${state.phaseFieldPrep.targetHeight}` : "pending",
- ]);
- items.push([
- "Step",
- frame ? `${frame.step} / ${frame.totalSteps}` : `0 / ${Math.max(0, state.solverStepBudget)}`,
- ]);
- items.push([
- "Cell pitch",
- state.phaseFieldPrep
- ? `${formatNumber(state.phaseFieldPrep.cellX, 1)} x ${formatNumber(state.phaseFieldPrep.cellY, 1)} px`
+ items.push({
+ label: "Grid",
+ value: state.phaseFieldPrep ? `${state.phaseFieldPrep.targetWidth} x ${state.phaseFieldPrep.targetHeight}` : "pending",
+ });
+ items.push({
+ label: "Step",
+ value: frame ? `${frame.step} / ${frame.totalSteps}` : `0 / ${Math.max(0, state.solverStepBudget)}`,
+ });
+ items.push({
+ label: "Cell pitch",
+ value: state.phaseFieldPrep
+ ? `${formatNumber(state.phaseFieldPrep.cellX, 1)}x${formatNumber(state.phaseFieldPrep.cellY, 1)} px`
: "pending",
- ]);
- items.push(["Loss", frame?.loss === null || frame?.loss === undefined ? "n/a" : formatNumber(frame.loss, 4)]);
- for (const [label, value] of solverTerms) {
- items.push([label, formatNumber(value, 4)]);
- }
- }
- for (const [label, value] of items) {
- const node = document.createElement("div");
- node.className = "status-metric";
- node.innerHTML = `${label}${value}`;
- statusMetrics.appendChild(node);
+ });
+ items.push({ label: "Loss", value: frame?.loss === null || frame?.loss === undefined ? "n/a" : formatNumber(frame.loss, 4) });
+ items.push(...solverTerms);
}
+ renderStatusMetricItems(items);
}
function renderSummary() {
summaryPanel.innerHTML = "";
diff --git a/src/repixelizer/gui_static/styles.css b/src/repixelizer/gui_static/styles.css
index 3b0d617..492cecb 100644
--- a/src/repixelizer/gui_static/styles.css
+++ b/src/repixelizer/gui_static/styles.css
@@ -732,22 +732,27 @@ p {
}
.status-metrics {
+ --status-metric-count: 1;
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
+ grid-template-columns: repeat(var(--status-metric-count), minmax(96px, 1fr));
+ grid-auto-rows: minmax(0, 1fr);
gap: 10px;
-}
-
-.status-metrics.status-metrics-fixed {
- grid-template-columns: repeat(3, minmax(0, 1fr));
+ overflow-x: auto;
+ overscroll-behavior-x: contain;
+ scrollbar-width: thin;
}
.status-metric {
min-width: 0;
width: 100%;
+ min-height: 72px;
box-sizing: border-box;
padding: 8px 10px;
border: 2px solid rgba(8, 36, 62, 0.88);
background: linear-gradient(180deg, rgba(9, 27, 44, 0.96), rgba(5, 16, 28, 0.95));
+ display: grid;
+ grid-template-rows: auto minmax(0, 1fr);
+ align-content: start;
}
.status-metric strong {
@@ -756,6 +761,9 @@ p {
font-size: 0.58rem;
color: var(--ui-accent);
text-transform: uppercase;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.status-metric span {
@@ -763,8 +771,10 @@ p {
font-size: 1rem;
line-height: 1.12;
color: var(--ui-text);
- overflow-wrap: anywhere;
- word-break: break-word;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-variant-numeric: tabular-nums;
}
@keyframes status-sweep {
@@ -950,10 +960,6 @@ select:focus-visible {
grid-template-columns: 1fr;
}
- .status-metrics.status-metrics-fixed {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
-
.card-head {
flex-wrap: wrap;
}
@@ -1003,7 +1009,7 @@ select:focus-visible {
width: 100%;
}
- .status-metrics.status-metrics-fixed {
- grid-template-columns: 1fr;
+ .status-metrics {
+ grid-template-columns: repeat(var(--status-metric-count), minmax(88px, 1fr));
}
}