HanningChen commited on
Commit
8cd1ab1
·
1 Parent(s): 95e24be
Files changed (2) hide show
  1. webui/static/main.js +268 -280
  2. webui/templates/index.html +1 -1
webui/static/main.js CHANGED
@@ -1,325 +1,313 @@
1
- // ====== Error logger (best-effort) ======
2
- window.addEventListener("error", (e) => {
3
- const log = document.getElementById("statusLog");
4
- if (log) {
5
- log.textContent =
6
- "JS error: " + (e.message || String(e.error)) + "\n" + log.textContent;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  }
8
- });
9
-
10
- // ====== Helpers ======
11
- const $ = (id) => document.getElementById(id);
12
- const qs = (sel) => document.querySelector(sel);
13
- const qsa = (sel) => Array.from(document.querySelectorAll(sel));
14
-
15
- // ====== Elements ======
16
- const form = $("runForm");
17
- const runBtn = $("runBtn");
18
- const runBtnText = $("runBtnText");
19
- const spinner = $("spinner");
20
- const resetBtn = $("resetBtn");
21
-
22
- const fileInput = $("fileInput");
23
- const dropzone = $("dropzone");
24
- const fileMeta = $("fileMeta");
25
- const previewWrap = $("previewWrap");
26
- const previewImg = $("previewImg");
27
-
28
- const statusDot = $("statusDot");
29
- const statusText = $("statusText");
30
- const statusLog = $("statusLog");
31
- const jobPill = $("jobPill");
32
-
33
- const imgInput = $("imgInput");
34
- const imgYolo = $("imgYolo");
35
- const imgSelected = $("imgSelected");
36
- const meta = $("meta");
37
-
38
- const dlInput = $("dlInput");
39
- const dlYolo = $("dlYolo");
40
- const dlSelected = $("dlSelected");
41
- const copyMetaBtn = $("copyMetaBtn");
42
-
43
- // Guard: if your HTML hasn’t loaded / ids mismatch
44
- if (!form || !fileInput || !dropzone) {
45
- console.error("Missing required DOM elements. Check your element ids.");
46
- }
47
-
48
- // ====== UI helpers ======
49
- function setStatus(kind, text, logLine) {
50
- // kind: idle | run | ok | bad
51
- if (statusDot) statusDot.className = `dot ${kind}`;
52
- if (statusText) statusText.textContent = text;
53
- if (statusLog && logLine) statusLog.textContent = logLine + "\n" + statusLog.textContent;
54
- }
55
-
56
- function setRunning(isRunning) {
57
- if (!runBtn || !spinner || !runBtnText) return;
58
- runBtn.disabled = isRunning;
59
- spinner.classList.toggle("hidden", !isRunning);
60
- runBtnText.textContent = isRunning ? "Running…" : "Run";
61
- }
62
-
63
- function clearResults() {
64
- if (imgInput) imgInput.removeAttribute("src");
65
- if (imgYolo) imgYolo.removeAttribute("src");
66
- if (imgSelected) imgSelected.removeAttribute("src");
67
- if (meta) meta.textContent = "";
68
- if (jobPill) jobPill.textContent = "job: —";
69
-
70
- [dlInput, dlYolo, dlSelected].forEach((a) => {
71
- if (!a) return;
72
- a.classList.add("hidden");
73
- a.removeAttribute("href");
74
- });
75
- }
76
-
77
- function setPreview(file) {
78
- if (!previewWrap || !previewImg || !fileMeta) return;
79
 
80
- if (!file) {
81
- previewWrap.classList.add("hidden");
82
- previewImg.removeAttribute("src");
83
- fileMeta.textContent = "No file selected";
84
- return;
85
  }
86
 
87
- fileMeta.textContent = `${file.name} • ${(file.size / 1024).toFixed(1)} KB`;
88
- const url = URL.createObjectURL(file);
89
- previewImg.src = url;
90
- previewWrap.classList.remove("hidden");
91
- }
92
-
93
- function setFileToInput(file) {
94
- // Ensures FormData(form) sees the file even for drag/drop
95
- const dt = new DataTransfer();
96
- dt.items.add(file);
97
- fileInput.files = dt.files;
98
- }
99
-
100
- // ====== Noise panel logic ======
101
- (function initNoisePanels() {
102
- const noiseType = $("noiseType");
103
- if (!noiseType) return;
104
-
105
- const panels = {
106
- none: $("noisePanelNone"),
107
- gaussian: $("noisePanelGaussian"),
108
- linear: $("noisePanelLinear"),
109
- adv: $("noisePanelAdv"),
110
- };
111
-
112
- const sliders = {
113
- gaussian: $("noiseGaussian"),
114
- linear: $("noiseLinear"),
115
- adv: $("noiseAdv"),
116
- };
117
-
118
- const labels = {
119
- gaussian: $("noiseValGaussian"),
120
- linear: $("noiseValLinear"),
121
- adv: $("noiseValAdv"),
122
- };
123
-
124
- const hiddenStrength = $("noiseStrength");
125
- if (!hiddenStrength) return;
126
 
127
- function noiseFamily(val) {
128
- const t = String(val || "none").toLowerCase();
 
 
 
 
129
 
130
- if (t === "none" || t === "off") return "none";
131
- if (t === "gaussian" || t.includes("gauss")) return "gaussian";
132
- if (t === "linear" || t.includes("linear")) return "linear";
 
 
133
 
134
- // adversarial variants
135
- if (
136
- t === "adv" ||
137
- t.startsWith("adv") ||
138
- t.includes("adversarial") ||
139
- t.includes("attack") ||
140
- t.includes("fgsm") ||
141
- t.includes("pgd")
142
- ) return "adv";
143
 
144
- return "none";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  }
146
 
147
- function updateHiddenStrength() {
148
- const fam = noiseFamily(noiseType.value);
149
- if (fam === "none") {
150
- hiddenStrength.value = "0";
151
- return;
152
- }
153
- hiddenStrength.value = String(sliders[fam]?.value ?? "0");
 
154
  }
155
 
156
- function showPanelForCurrentSelection() {
 
157
  const fam = noiseFamily(noiseType.value);
158
 
159
- Object.values(panels).forEach((p) => p && p.classList.add("hidden"));
160
- (panels[fam] || panels.none)?.classList.remove("hidden");
 
 
161
 
162
- updateHiddenStrength();
163
  }
164
 
165
- Object.keys(sliders).forEach((k) => {
166
- const s = sliders[k];
167
- if (!s) return;
168
- s.addEventListener("input", () => {
169
- if (labels[k]) labels[k].textContent = s.value;
170
- if (noiseFamily(noiseType.value) === k) updateHiddenStrength();
171
  });
172
- });
173
 
174
- noiseType.addEventListener("change", showPanelForCurrentSelection);
175
- showPanelForCurrentSelection();
176
- })();
 
177
 
178
- // ====== Tabs + HW noise labels ======
179
- (function initTabsAndHwNoise() {
180
- const tabBtns = qsa(".tabBtn");
181
- const tabPanels = qsa(".tabPanel");
182
 
183
- tabBtns.forEach((b) => {
184
- b.addEventListener("click", () => {
185
- tabBtns.forEach((x) => x.classList.remove("active"));
186
- tabPanels.forEach((p) => p.classList.add("hidden"));
187
 
188
- b.classList.add("active");
189
- const id = b.getAttribute("data-tab");
190
- $(id)?.classList.remove("hidden");
 
 
 
 
 
 
 
 
191
  });
192
- });
193
-
194
- const w = $("hwNoiseWidth");
195
- const s = $("hwNoiseStrength");
196
- const wv = $("hwNoiseWidthVal");
197
- const sv = $("hwNoiseStrengthVal");
198
- if (w && wv) w.addEventListener("input", () => (wv.textContent = w.value));
199
- if (s && sv) s.addEventListener("input", () => (sv.textContent = s.value));
200
- })();
201
 
202
- // ====== Upload / Dropzone ======
203
- (function initUpload() {
204
- if (!fileInput || !dropzone) return;
205
 
206
- // Click to open picker
207
- dropzone.addEventListener("click", () => fileInput.click());
208
- dropzone.addEventListener("keydown", (e) => {
209
- if (e.key === "Enter" || e.key === " ") {
210
- e.preventDefault();
211
- fileInput.click();
212
  }
213
- });
 
 
 
214
 
215
- // File picked
216
- fileInput.addEventListener("change", () => {
217
- const file = fileInput.files && fileInput.files[0];
218
- setPreview(file);
219
- });
 
 
 
 
 
 
 
 
 
 
 
220
 
221
- // Drag styling
222
- ["dragenter", "dragover"].forEach((evt) => {
223
- dropzone.addEventListener(evt, (e) => {
224
  e.preventDefault();
225
- e.stopPropagation();
226
  dropzone.classList.add("drag");
227
  });
228
- });
229
 
230
- ["dragleave", "drop"].forEach((evt) => {
231
- dropzone.addEventListener(evt, (e) => {
232
  e.preventDefault();
233
- e.stopPropagation();
234
  dropzone.classList.remove("drag");
 
 
 
 
235
  });
236
- });
237
-
238
- // Drop file
239
- dropzone.addEventListener("drop", (e) => {
240
- const file = e.dataTransfer?.files?.[0];
241
- if (!file) return;
242
- setFileToInput(file);
243
- setPreview(file);
244
- });
245
- })();
246
-
247
- // ====== Reset + copy meta ======
248
- if (resetBtn) {
249
- resetBtn.addEventListener("click", () => {
250
- if (form) form.reset();
251
- if (fileInput) fileInput.value = "";
252
- setPreview(null);
253
- clearResults();
254
- if (statusLog) statusLog.textContent = "Waiting for input…";
255
- setStatus("idle", "Idle", "Reset UI.");
256
- });
257
- }
258
-
259
- if (copyMetaBtn) {
260
- copyMetaBtn.addEventListener("click", async () => {
261
- const text = meta?.textContent || "";
262
- if (!text) return;
263
- await navigator.clipboard.writeText(text);
264
- setStatus("ok", "Done", "Copied metadata to clipboard.");
265
- });
266
- }
267
-
268
- // ====== Submit ======
269
- if (form) {
270
- form.addEventListener("submit", async (e) => {
271
- e.preventDefault();
272
 
273
- const file = fileInput?.files?.[0];
274
- if (!file) {
275
- setStatus("bad", "Error", "No file selected.");
276
- return;
277
- }
 
 
 
 
 
 
 
 
 
 
 
 
278
 
279
- setRunning(true);
280
- setStatus("run", "Running", "Submitting request to /api/run …");
 
 
 
 
 
 
 
 
281
 
282
- const fd = new FormData(form);
 
 
283
 
284
- try {
285
- const resp = await fetch("/api/run", { method: "POST", body: fd });
286
- const data = await resp.json();
287
 
288
- if (!resp.ok || !data.ok) {
289
- throw new Error(data?.error || `HTTP ${resp.status}`);
 
 
290
  }
291
 
292
- if (jobPill) jobPill.textContent = `job: ${data.job_id}`;
293
- setStatus("ok", "Done", `Inference finished. job_id=${data.job_id}`);
294
-
295
- // Update images (cache-bust)
296
- const bust = `t=${Date.now()}`;
297
- if (imgInput) imgInput.src = `${data.image_urls.input}?${bust}`;
298
- if (imgYolo) imgYolo.src = `${data.image_urls.yolo}?${bust}`;
299
- if (imgSelected) imgSelected.src = `${data.image_urls.selected}?${bust}`;
300
-
301
- // Open links
302
- if (dlInput) { dlInput.href = data.image_urls.input; dlInput.classList.remove("hidden"); }
303
- if (dlYolo) { dlYolo.href = data.image_urls.yolo; dlYolo.classList.remove("hidden"); }
304
- if (dlSelected) { dlSelected.href = data.image_urls.selected; dlSelected.classList.remove("hidden"); }
305
-
306
- // Meta
307
- if (meta) {
308
- meta.textContent = JSON.stringify(
309
- {
310
- task_id: data.task_id,
311
- task_name: data.task_name,
312
- selected_indices: data.selected_indices,
313
- job_id: data.job_id,
314
- },
315
- null,
316
- 2
317
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  }
319
- } catch (err) {
320
- setStatus("bad", "Error", String(err));
321
- } finally {
322
- setRunning(false);
 
 
 
 
 
 
323
  }
 
 
 
 
 
 
 
 
324
  });
325
- }
 
1
+ // static/main.js
2
+ (() => {
3
+ // Helpers
4
+ const $ = (id) => document.getElementById(id);
5
+ const qsa = (sel) => Array.from(document.querySelectorAll(sel));
6
+
7
+ // Elements
8
+ const form = $("runForm");
9
+ const runBtn = $("runBtn");
10
+ const runBtnText = $("runBtnText");
11
+ const spinner = $("spinner");
12
+ const resetBtn = $("resetBtn");
13
+
14
+ const fileInput = $("fileInput");
15
+ const dropzone = $("dropzone");
16
+ const fileMeta = $("fileMeta");
17
+ const previewWrap = $("previewWrap");
18
+ const previewImg = $("previewImg");
19
+
20
+ const statusDot = $("statusDot");
21
+ const statusText = $("statusText");
22
+ const statusLog = $("statusLog");
23
+ const jobPill = $("jobPill");
24
+
25
+ const imgInput = $("imgInput");
26
+ const imgYolo = $("imgYolo");
27
+ const imgSelected = $("imgSelected");
28
+ const meta = $("meta");
29
+
30
+ const dlInput = $("dlInput");
31
+ const dlYolo = $("dlYolo");
32
+ const dlSelected = $("dlSelected");
33
+ const copyMetaBtn = $("copyMetaBtn");
34
+
35
+ // Noise elements
36
+ const noiseType = $("noiseType");
37
+ const noiseStrengthHidden = $("noiseStrength");
38
+ const noisePanelNone = $("noisePanelNone");
39
+ const noisePanelGaussian = $("noisePanelGaussian");
40
+ const noisePanelLinear = $("noisePanelLinear");
41
+ const noisePanelAdv = $("noisePanelAdv");
42
+ const noiseGaussian = $("noiseGaussian");
43
+ const noiseLinear = $("noiseLinear");
44
+ const noiseAdv = $("noiseAdv");
45
+ const noiseValGaussian = $("noiseValGaussian");
46
+ const noiseValLinear = $("noiseValLinear");
47
+ const noiseValAdv = $("noiseValAdv");
48
+
49
+ // HW noise elements (UI only; backend currently doesn't accept them unless you add to api_run)
50
+ const hwNoiseWidth = $("hwNoiseWidth");
51
+ const hwNoiseStrength = $("hwNoiseStrength");
52
+ const hwNoiseWidthVal = $("hwNoiseWidthVal");
53
+ const hwNoiseStrengthVal = $("hwNoiseStrengthVal");
54
+
55
+ function setStatus(kind, text, logLine) {
56
+ if (statusDot) statusDot.className = `dot ${kind}`;
57
+ if (statusText) statusText.textContent = text;
58
+ if (statusLog && logLine) statusLog.textContent = logLine;
59
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
+ function setRunning(isRunning) {
62
+ if (!runBtn) return;
63
+ runBtn.disabled = isRunning;
64
+ if (spinner) spinner.classList.toggle("hidden", !isRunning);
65
+ if (runBtnText) runBtnText.textContent = isRunning ? "Running…" : "Run";
66
  }
67
 
68
+ function setPreview(file) {
69
+ if (!fileMeta || !previewWrap || !previewImg) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
+ if (!file) {
72
+ fileMeta.textContent = "No file selected";
73
+ previewWrap.classList.add("hidden");
74
+ previewImg.removeAttribute("src");
75
+ return;
76
+ }
77
 
78
+ fileMeta.textContent = `${file.name} ${(file.size / 1024).toFixed(1)} KB`;
79
+ const url = URL.createObjectURL(file);
80
+ previewImg.src = url;
81
+ previewWrap.classList.remove("hidden");
82
+ }
83
 
84
+ function setFileToInput(file) {
85
+ const dt = new DataTransfer();
86
+ dt.items.add(file);
87
+ fileInput.files = dt.files;
88
+ }
 
 
 
 
89
 
90
+ // Tabs
91
+ function initTabs() {
92
+ const tabBtns = qsa(".tabBtn");
93
+ const tabPanels = qsa(".tabPanel");
94
+
95
+ tabBtns.forEach((btn) => {
96
+ btn.addEventListener("click", () => {
97
+ tabBtns.forEach((b) => b.classList.remove("active"));
98
+ tabPanels.forEach((p) => p.classList.add("hidden"));
99
+
100
+ btn.classList.add("active");
101
+ const id = btn.getAttribute("data-tab");
102
+ const panel = $(id);
103
+ if (panel) panel.classList.remove("hidden");
104
+ });
105
+ });
106
  }
107
 
108
+ // Noise panel switching
109
+ function noiseFamily(val) {
110
+ const t = String(val || "none").toLowerCase();
111
+ if (t === "none" || t === "default" || t === "off") return "none";
112
+ if (t === "gaussian") return "gaussian";
113
+ if (t === "linear") return "linear";
114
+ if (t.startsWith("adv")) return "adv";
115
+ return "none";
116
  }
117
 
118
+ function updateNoiseHidden() {
119
+ if (!noiseType || !noiseStrengthHidden) return;
120
  const fam = noiseFamily(noiseType.value);
121
 
122
+ let v = 0;
123
+ if (fam === "gaussian" && noiseGaussian) v = Number(noiseGaussian.value || 0);
124
+ if (fam === "linear" && noiseLinear) v = Number(noiseLinear.value || 0);
125
+ if (fam === "adv" && noiseAdv) v = Number(noiseAdv.value || 0);
126
 
127
+ noiseStrengthHidden.value = String(v);
128
  }
129
 
130
+ function showNoisePanel() {
131
+ if (!noiseType) return;
132
+ const fam = noiseFamily(noiseType.value);
133
+
134
+ [noisePanelNone, noisePanelGaussian, noisePanelLinear, noisePanelAdv].forEach((p) => {
135
+ if (p) p.classList.add("hidden");
136
  });
 
137
 
138
+ if (fam === "none" && noisePanelNone) noisePanelNone.classList.remove("hidden");
139
+ if (fam === "gaussian" && noisePanelGaussian) noisePanelGaussian.classList.remove("hidden");
140
+ if (fam === "linear" && noisePanelLinear) noisePanelLinear.classList.remove("hidden");
141
+ if (fam === "adv" && noisePanelAdv) noisePanelAdv.classList.remove("hidden");
142
 
143
+ updateNoiseHidden();
144
+ }
 
 
145
 
146
+ function initNoise() {
147
+ if (!noiseType) return;
148
+ noiseType.addEventListener("change", showNoisePanel);
 
149
 
150
+ if (noiseGaussian) noiseGaussian.addEventListener("input", () => {
151
+ if (noiseValGaussian) noiseValGaussian.textContent = noiseGaussian.value;
152
+ updateNoiseHidden();
153
+ });
154
+ if (noiseLinear) noiseLinear.addEventListener("input", () => {
155
+ if (noiseValLinear) noiseValLinear.textContent = noiseLinear.value;
156
+ updateNoiseHidden();
157
+ });
158
+ if (noiseAdv) noiseAdv.addEventListener("input", () => {
159
+ if (noiseValAdv) noiseValAdv.textContent = noiseAdv.value;
160
+ updateNoiseHidden();
161
  });
 
 
 
 
 
 
 
 
 
162
 
163
+ showNoisePanel();
164
+ }
 
165
 
166
+ function initHwNoiseLabels() {
167
+ if (hwNoiseWidth && hwNoiseWidthVal) {
168
+ hwNoiseWidth.addEventListener("input", () => (hwNoiseWidthVal.textContent = hwNoiseWidth.value));
 
 
 
169
  }
170
+ if (hwNoiseStrength && hwNoiseStrengthVal) {
171
+ hwNoiseStrength.addEventListener("input", () => (hwNoiseStrengthVal.textContent = hwNoiseStrength.value));
172
+ }
173
+ }
174
 
175
+ // Upload handlers
176
+ function initUpload() {
177
+ if (!fileInput || !dropzone) return;
178
+
179
+ dropzone.addEventListener("click", () => fileInput.click());
180
+ dropzone.addEventListener("keydown", (e) => {
181
+ if (e.key === "Enter" || e.key === " ") {
182
+ e.preventDefault();
183
+ fileInput.click();
184
+ }
185
+ });
186
+
187
+ fileInput.addEventListener("change", () => {
188
+ const f = fileInput.files && fileInput.files[0];
189
+ setPreview(f);
190
+ });
191
 
192
+ dropzone.addEventListener("dragover", (e) => {
 
 
193
  e.preventDefault();
 
194
  dropzone.classList.add("drag");
195
  });
196
+ dropzone.addEventListener("dragleave", () => dropzone.classList.remove("drag"));
197
 
198
+ dropzone.addEventListener("drop", (e) => {
 
199
  e.preventDefault();
 
200
  dropzone.classList.remove("drag");
201
+ const f = e.dataTransfer?.files?.[0];
202
+ if (!f) return;
203
+ setFileToInput(f);
204
+ setPreview(f);
205
  });
206
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
+ // Reset
209
+ function initReset() {
210
+ if (!resetBtn || !form) return;
211
+ resetBtn.addEventListener("click", () => {
212
+ form.reset();
213
+ if (fileInput) fileInput.value = "";
214
+ setPreview(null);
215
+ if (jobPill) jobPill.textContent = "job: —";
216
+ if (imgInput) imgInput.removeAttribute("src");
217
+ if (imgYolo) imgYolo.removeAttribute("src");
218
+ if (imgSelected) imgSelected.removeAttribute("src");
219
+ if (meta) meta.textContent = "";
220
+ setStatus("idle", "Idle", "Waiting for input…");
221
+ setRunning(false);
222
+ showNoisePanel();
223
+ });
224
+ }
225
 
226
+ // Copy metadata
227
+ function initCopyMeta() {
228
+ if (!copyMetaBtn || !meta) return;
229
+ copyMetaBtn.addEventListener("click", async () => {
230
+ const text = meta.textContent || "";
231
+ if (!text) return;
232
+ await navigator.clipboard.writeText(text);
233
+ setStatus("ok", "Done", "Copied metadata to clipboard.");
234
+ });
235
+ }
236
 
237
+ // Submit
238
+ function initSubmit() {
239
+ if (!form) return;
240
 
241
+ form.addEventListener("submit", async (e) => {
242
+ e.preventDefault();
 
243
 
244
+ const file = fileInput?.files?.[0];
245
+ if (!file) {
246
+ setStatus("bad", "Error", "No file selected.");
247
+ return;
248
  }
249
 
250
+ updateNoiseHidden(); // ensure hidden noise_strength matches slider
251
+ setRunning(true);
252
+ setStatus("run", "Running", "Submitting request…");
253
+
254
+ try {
255
+ const fd = new FormData(form);
256
+
257
+ // NOTE: backend currently ignores HW noise + hdc_bits unless you add them to api_run.
258
+ const resp = await fetch("/api/run", { method: "POST", body: fd });
259
+ const data = await resp.json();
260
+
261
+ if (!resp.ok || !data.ok) throw new Error(data?.error || `HTTP ${resp.status}`);
262
+
263
+ if (jobPill) jobPill.textContent = `job: ${data.job_id}`;
264
+
265
+ const bust = `t=${Date.now()}`;
266
+ if (imgInput) imgInput.src = `${data.image_urls.input}?${bust}`;
267
+ if (imgYolo) imgYolo.src = `${data.image_urls.yolo}?${bust}`;
268
+ if (imgSelected) imgSelected.src = `${data.image_urls.selected}?${bust}`;
269
+
270
+ if (dlInput) { dlInput.href = data.image_urls.input; dlInput.classList.remove("hidden"); }
271
+ if (dlYolo) { dlYolo.href = data.image_urls.yolo; dlYolo.classList.remove("hidden"); }
272
+ if (dlSelected) { dlSelected.href = data.image_urls.selected; dlSelected.classList.remove("hidden"); }
273
+
274
+ if (meta) {
275
+ meta.textContent = JSON.stringify(
276
+ {
277
+ job_id: data.job_id,
278
+ task_id: data.task_id,
279
+ task_name: data.task_name,
280
+ selected_indices: data.selected_indices,
281
+ },
282
+ null,
283
+ 2
284
+ );
285
+ }
286
+
287
+ setStatus("ok", "Done", "Finished.");
288
+ } catch (err) {
289
+ setStatus("bad", "Error", String(err));
290
+ } finally {
291
+ setRunning(false);
292
  }
293
+ });
294
+ }
295
+
296
+ // Boot
297
+ document.addEventListener("DOMContentLoaded", () => {
298
+ // Basic sanity: show you in logs if ids are missing
299
+ if (!form || !fileInput || !dropzone) {
300
+ console.error("DOM ids mismatch: runForm/fileInput/dropzone not found.");
301
+ if (statusLog) statusLog.textContent = "JS init failed: missing DOM elements (check ids).";
302
+ return;
303
  }
304
+ initTabs();
305
+ initNoise();
306
+ initHwNoiseLabels();
307
+ initUpload();
308
+ initReset();
309
+ initCopyMeta();
310
+ initSubmit();
311
+ setStatus("idle", "Idle", "Waiting for input…");
312
  });
313
+ })();
webui/templates/index.html CHANGED
@@ -308,6 +308,6 @@
308
  </main>
309
  </div>
310
 
311
- <script src="/static/main.js"></script>
312
  </body>
313
  </html>
 
308
  </main>
309
  </div>
310
 
311
+ <script src="/static/main.js?v=20260125_1"></script>
312
  </body>
313
  </html>