everydaytok commited on
Commit
78f28cf
Β·
verified Β·
1 Parent(s): 86fab25

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +284 -347
index.html CHANGED
@@ -11,25 +11,33 @@
11
  .glass { background:rgba(10,18,35,0.98); border:1px solid #1e2d40; }
12
  #drawer { transition:transform .3s ease; z-index:200; }
13
  .drawer-closed { transform:translateY(100%); }
14
- .tog { transition:all .15s; }
15
- .tog.on { color:#fff; }
16
- .tog.off { background:#1e293b !important; color:#64748b; }
17
  .lbtn {
18
- width:22px; height:22px; background:#1e293b; border:1px solid #334155;
19
- border-radius:4px; color:#94a3b8; font-weight:bold; font-size:14px;
20
  cursor:pointer; display:inline-flex; align-items:center; justify-content:center;
21
  }
22
  .lbtn:hover { background:#334155; color:#fff; }
 
 
 
 
 
 
 
23
  </style>
24
  </head>
25
  <body class="flex flex-col h-screen overflow-hidden">
26
 
27
  <!-- ── HEADER ──────────────────────────────────────────────────────────────── -->
28
- <header class="glass flex-shrink-0 px-2 py-1.5 flex justify-between items-center">
29
- <div class="flex flex-wrap gap-1 text-[8px] font-bold items-center">
30
- <span id="b-mode" class="px-1.5 py-0.5 rounded bg-yellow-900/60 text-yellow-300 border border-yellow-800/60">TRAIN</span>
31
- <span id="b-arch" class="px-1.5 py-0.5 rounded bg-blue-900/60 text-blue-300 border border-blue-800/60">ADDIT</span>
32
- <span id="b-alpha" class="px-1.5 py-0.5 rounded bg-orange-900/60 text-orange-300 border border-orange-800/60">Ξ±:0.45</span>
 
 
 
 
33
  <span class="px-1.5 py-0.5 rounded bg-green-900/60 text-green-300 border border-green-800/60 flex items-center gap-0.5">
34
  <span class="text-orange-400 mr-0.5">U</span>
35
  <button class="lbtn" onclick="quickL('upper',-1)">βˆ’</button>
@@ -40,412 +48,279 @@
40
  <span id="b-lower" class="w-3 text-center text-white">3</span>
41
  <button class="lbtn" onclick="quickL('lower',+1)">+</button>
42
  </span>
43
- <span id="b-data" class="px-1.5 py-0.5 rounded bg-pink-900/60 text-pink-300 border border-pink-800/60">HOUSNG</span>
44
  </div>
45
- <div class="flex items-center gap-2 ml-1">
46
- <span id="q-lbl" class="text-[8px] text-slate-600">Q:0</span>
47
- <div id="run-dot" class="w-2 h-2 rounded-full bg-slate-700"></div>
48
- <button onclick="openDrawer()" class="text-[10px] bg-blue-700 hover:bg-blue-600 px-2 py-1 rounded font-bold">βš™ DIALS</button>
 
 
 
 
 
 
49
  </div>
50
  </header>
51
 
52
  <!-- ── PLOTS ────────────────────────────────────────────────────────────────── -->
53
  <div class="flex-grow flex flex-col min-h-0 overflow-hidden">
54
  <div id="mesh-plot" class="flex-grow min-h-0"></div>
55
- <div id="err-plot" style="height:58px" class="flex-shrink-0 border-t border-slate-900"></div>
56
  </div>
57
 
58
  <!-- ── BOTTOM PANEL ─────────────────��───────────────────────────────────────── -->
59
- <div class="glass flex-shrink-0 border-t border-slate-800" style="height:174px">
60
  <div class="flex border-b border-slate-800 text-[10px]">
61
- <button onclick="tab('nodes')" id="tab-nodes" class="flex-1 py-1.5 bg-blue-900/40 text-blue-300 font-bold">NODES</button>
62
- <button onclick="tab('springs')" id="tab-springs" class="flex-1 py-1.5 text-slate-500">SPRINGS</button>
63
- <button onclick="tab('logs')" id="tab-logs" class="flex-1 py-1.5 text-slate-500">LOGS</button>
 
64
  </div>
65
  <div class="flex h-full overflow-hidden">
 
66
  <div class="w-20 flex-shrink-0 border-r border-slate-800 flex flex-col items-center justify-center p-1 gap-0.5">
67
- <div class="text-[7px] text-slate-600 uppercase tracking-widest">Tension</div>
68
  <div id="err-big" class="text-xl font-bold text-red-400 leading-none">0.00</div>
69
- <div id="pred-val" class="text-[8px] text-slate-500">P:β€”</div>
70
  <div id="iter-lbl" class="text-[8px] text-slate-700">IT:0</div>
 
71
  </div>
 
72
  <div class="flex-grow overflow-y-auto p-1.5 text-[10px]">
73
  <div id="pane-nodes"></div>
74
- <div id="pane-springs" class="hidden"></div>
75
- <div id="pane-logs" class="hidden text-[9px] text-slate-500"></div>
 
76
  </div>
77
  </div>
78
  </div>
79
 
80
  <!-- ── DRAWER ───────────────────────────────────────────────────────────────── -->
81
- <aside id="drawer" class="drawer-closed fixed inset-x-0 bottom-0 glass border-t border-slate-700 p-4 flex flex-col gap-3 max-h-[93vh] overflow-y-auto">
82
  <div class="flex justify-between items-center">
83
- <span class="text-orange-400 font-bold text-sm">ELASTIC MESH LABORATORY</span>
84
  <button onclick="closeDrawer()" class="text-slate-400 text-2xl leading-none">βœ•</button>
85
  </div>
86
 
87
- <div class="grid grid-cols-2 gap-2">
88
-
89
- <!-- MODE -->
90
- <div class="col-span-2 bg-slate-900 rounded p-3 border border-yellow-900/50">
91
- <div class="text-yellow-400 text-[9px] font-bold mb-2">EXECUTION MODE</div>
92
- <div class="flex gap-2">
93
- <button class="tog on flex-1 py-2 rounded text-xs font-bold bg-yellow-700"
94
- onclick="pick('mode','training',this,'bg-yellow-700')">TRAINING</button>
95
- <button class="tog off flex-1 py-2 rounded text-xs font-bold"
96
- onclick="pick('mode','inference',this,'bg-yellow-700')">INFERENCE</button>
97
- </div>
98
- </div>
99
-
100
- <!-- ARCH -->
101
- <div class="bg-slate-900 rounded p-3 border border-blue-900/50">
102
- <div class="text-blue-400 text-[9px] font-bold mb-2">NODE ACTIVATION</div>
103
- <button class="tog on w-full mb-1 py-2 rounded text-[10px] font-bold bg-blue-700"
104
- onclick="pick('architecture','additive',this,'bg-blue-700')">ADDITIVE Ξ£</button>
105
- <button class="tog off w-full py-2 rounded text-[10px] font-bold"
106
- onclick="pick('architecture','multiplicative',this,'bg-blue-700')">MULTIPLICATIVE Ξ </button>
107
- </div>
108
-
109
- <!-- ALPHA -->
110
- <div class="bg-slate-900 rounded p-3 border border-orange-900/50">
111
- <div class="text-orange-400 text-[9px] font-bold mb-1">BACK-TENSION Ξ±</div>
112
- <input id="alpha-sl" type="range" min="0" max="100" value="45" step="5"
113
- class="w-full accent-orange-500 mt-1"
114
- oninput="document.getElementById('alpha-val').innerText=(this.value/100).toFixed(2)">
115
- <div class="text-center text-orange-300 font-bold text-xl mt-0.5" id="alpha-val">0.45</div>
116
- <div class="text-[8px] text-slate-600 text-center mt-0.5">elastic coupling A↕B</div>
117
  </div>
 
118
 
119
- <!-- TOPOLOGY -->
120
- <div class="col-span-2 bg-slate-900 rounded p-3 border border-green-900/50">
121
- <div class="text-green-400 text-[9px] font-bold mb-3">
122
- HOURGLASS TOPOLOGY
123
- <span class="text-slate-500 font-normal ml-1">A β†’ [upper] β†’ C ← [lower] ← B</span>
124
- </div>
125
- <div class="grid grid-cols-2 gap-3">
126
- <div class="bg-slate-800 rounded p-3 text-center border border-orange-900/40">
127
- <div class="text-orange-400 text-[8px] mb-1">UPPER BULGE (A-side)</div>
128
- <div class="flex items-center justify-center gap-3">
129
- <button class="lbtn text-base" onclick="chL('upper',-1)">βˆ’</button>
130
- <span id="dial-upper" class="text-orange-300 font-bold text-3xl w-8 text-center">3</span>
131
- <button class="lbtn text-base" onclick="chL('upper',+1)">+</button>
132
- </div>
133
- <div class="text-[8px] text-slate-600 mt-1">U nodes</div>
134
  </div>
135
- <div class="bg-slate-800 rounded p-3 text-center border border-cyan-900/40">
136
- <div class="text-cyan-400 text-[8px] mb-1">LOWER BULGE (B-side)</div>
137
- <div class="flex items-center justify-center gap-3">
138
- <button class="lbtn text-base" onclick="chL('lower',-1)">βˆ’</button>
139
- <span id="dial-lower" class="text-cyan-300 font-bold text-3xl w-8 text-center">3</span>
140
- <button class="lbtn text-base" onclick="chL('lower',+1)">+</button>
141
- </div>
142
- <div class="text-[8px] text-slate-600 mt-1">L nodes</div>
143
  </div>
144
  </div>
145
- <div id="spring-count" class="text-center text-[8px] text-slate-600 mt-2"></div>
146
  </div>
 
147
  </div>
148
 
149
- <button onclick="applyConfig()"
150
- class="w-full bg-white text-black py-3 rounded font-bold text-sm hover:bg-slate-200">
151
- APPLY CONFIG &amp; RESET MESH
152
- </button>
153
-
154
- <!-- DATASET -->
155
- <div class="bg-slate-900 rounded p-3 border border-pink-900/50">
156
- <div class="text-pink-400 text-[9px] font-bold mb-2">DATASET
157
- <span class="text-slate-600 font-normal ml-1">(hidden from mesh)</span>
158
  </div>
159
- <select id="ds-sel" onchange="refreshDS()"
160
- class="w-full bg-black border border-slate-700 p-2 text-white text-xs rounded mb-2">
161
- <option value="housing">Housing β€” AΓ—2.5 + BΓ—1.2</option>
162
- <option value="subtraction">Subtraction β€” A βˆ’ B</option>
163
- <option value="multiplication">Multiplication β€” A Γ— B</option>
164
- <option value="quadratic">Quadratic β€” AΒ² + B</option>
165
- </select>
166
- <p id="ds-hint" class="text-[9px] text-slate-500 italic mb-2"></p>
167
- <div class="flex gap-2">
168
- <input id="batch-n" type="number" value="30" min="5" max="200"
169
- class="w-16 bg-black border border-slate-700 p-2 text-white text-sm text-center rounded">
170
- <button onclick="startBatch()"
171
- class="flex-1 bg-pink-800 hover:bg-pink-700 py-2 text-xs font-bold rounded">START BATCH</button>
172
  </div>
173
  </div>
174
 
175
- <!-- CUSTOM -->
176
- <div class="bg-slate-900 rounded p-3 border border-cyan-900/50">
177
- <div class="text-cyan-400 text-[9px] font-bold mb-1">CUSTOM INPUT</div>
178
- <div id="ds-examples" class="text-[9px] text-slate-500 mb-2"></div>
179
- <div class="grid grid-cols-3 gap-2 mb-2">
180
- <div>
181
- <div class="text-[8px] text-slate-600 mb-1">A (top)</div>
182
- <input id="ca" type="number" value="5" step="0.1"
183
- class="w-full bg-black border border-slate-700 p-2 text-white text-sm text-center rounded"
184
- oninput="updateExpected()">
185
- </div>
186
- <div>
187
- <div class="text-[8px] text-slate-600 mb-1">B (bottom)</div>
188
- <input id="cb" type="number" value="3" step="0.1"
189
- class="w-full bg-black border border-slate-700 p-2 text-white text-sm text-center rounded"
190
- oninput="updateExpected()">
191
- </div>
192
- <div>
193
- <div class="text-[8px] text-slate-600 mb-1">C target</div>
194
- <input id="cc" type="number" placeholder="auto" step="0.1"
195
- class="w-full bg-black border border-slate-700 p-2 text-white text-sm text-center rounded">
196
- </div>
197
  </div>
198
- <div id="expected-lbl" class="text-[9px] text-yellow-400 mb-2 min-h-[14px]"></div>
199
- <button onclick="runCustom()"
200
- class="w-full bg-cyan-800 hover:bg-cyan-700 py-2 text-xs font-bold rounded">RUN CUSTOM</button>
201
  </div>
202
 
203
- <button onclick="halt()"
204
- class="w-full border border-red-700 text-red-500 py-2 rounded text-xs font-bold">HALT ENGINE</button>
 
 
 
 
205
  </aside>
206
 
207
  <script>
208
- // ── STATE ──────────────────────────────────────────────────────────────────────
209
- const cfg = { mode: 'training', architecture: 'additive' };
210
  const topo = { upper: 3, lower: 3 };
211
 
212
- function springCount() {
213
- const { upper: u, lower: l } = topo;
214
- return `Springs: ${u} (A→U) + ${u} (U→C) + ${l} (B→L) + ${l} (L→C) = ${2*u + 2*l} total`;
215
- }
216
- function updateSpringCount() {
217
- document.getElementById('spring-count').innerText = springCount();
218
- }
219
-
220
- function chL(w, d) {
221
- topo[w] = Math.max(1, Math.min(8, topo[w] + d));
222
- document.getElementById(`dial-${w}`).innerText = topo[w];
223
- updateSpringCount();
224
- }
225
-
226
- async function quickL(w, d) {
227
- const res = await fetch('/set_layer', {
228
- method: 'POST', headers: {'Content-Type':'application/json'},
229
- body: JSON.stringify({ layer: w, delta: d })
230
- });
231
- const data = await res.json();
232
- topo.upper = data.n_upper; topo.lower = data.n_lower;
233
- syncTopoUI();
234
- meshPlotted = false;
235
- }
236
-
237
- function syncTopoUI() {
238
  ['upper','lower'].forEach(k => {
239
  document.getElementById(`b-${k}`).innerText = topo[k];
240
  document.getElementById(`dial-${k}`).innerText = topo[k];
241
  });
242
- updateSpringCount();
 
 
 
243
  }
244
 
245
- function pick(key, val, btn, cls) {
246
- cfg[key] = val;
247
- btn.parentElement.querySelectorAll('.tog').forEach(b => {
248
- b.classList.add('off'); b.classList.remove('on', cls);
249
  });
250
- btn.classList.remove('off'); btn.classList.add('on', cls);
 
 
251
  }
252
 
253
- function openDrawer() { document.getElementById('drawer').classList.remove('drawer-closed'); refreshDS(); }
254
  function closeDrawer() { document.getElementById('drawer').classList.add('drawer-closed'); }
255
 
256
- async function applyConfig() {
257
- const ds = document.getElementById('ds-sel').value;
258
- const alpha = parseFloat(document.getElementById('alpha-sl').value) / 100;
259
- await fetch('/config', {
260
  method:'POST', headers:{'Content-Type':'application/json'},
261
- body: JSON.stringify({
262
- ...cfg, dataset: ds, back_alpha: alpha,
263
- n_upper: topo.upper, n_lower: topo.lower
264
- })
265
  });
266
- document.getElementById('b-mode').innerText = cfg.mode === 'training' ? 'TRAIN' : 'INFER';
267
- document.getElementById('b-arch').innerText = cfg.architecture.slice(0,5).toUpperCase();
268
- document.getElementById('b-alpha').innerText = `Ξ±:${alpha.toFixed(2)}`;
269
- document.getElementById('b-data').innerText = ds.slice(0,6).toUpperCase();
270
- meshPlotted = false;
271
- closeDrawer();
272
  }
273
-
274
- async function startBatch() {
275
- await fetch('/generate', {
276
- method:'POST', headers:{'Content-Type':'application/json'},
277
- body: JSON.stringify({ count: parseInt(document.getElementById('batch-n').value) })
278
- });
279
- closeDrawer();
280
  }
281
-
282
- async function runCustom() {
283
- const a = document.getElementById('ca').value;
284
- const b = document.getElementById('cb').value;
285
- const c = document.getElementById('cc').value;
286
- await fetch('/run_custom', {
287
  method:'POST', headers:{'Content-Type':'application/json'},
288
- body: JSON.stringify({ a, b, c: c !== '' ? c : null })
289
  });
290
  closeDrawer();
291
  }
292
-
293
- async function halt() {
294
- await fetch('/halt', { method:'POST' });
295
- closeDrawer();
296
- }
297
-
298
- // ── DATASET META ──────────────────────────────────────────────────────────────
299
- const DS = {
300
- housing: { hint:'AΓ—2.5+BΓ—1.2', fn:(a,b)=>(a*2.5+b*1.2).toFixed(3), ex:[{a:4,b:2},{a:6,b:3}] },
301
- subtraction: { hint:'Aβˆ’B', fn:(a,b)=>(a-b).toFixed(3), ex:[{a:8,b:3},{a:5,b:5}] },
302
- multiplication:{ hint:'AΓ—B', fn:(a,b)=>(a*b).toFixed(3), ex:[{a:6,b:7},{a:3,b:9}] },
303
- quadratic: { hint:'AΒ²+B', fn:(a,b)=>(a*a+b).toFixed(3), ex:[{a:3,b:2},{a:4,b:5}] },
304
- };
305
-
306
- function refreshDS() {
307
- const ds = document.getElementById('ds-sel').value, m = DS[ds];
308
- document.getElementById('ds-hint').innerText = m.hint;
309
- document.getElementById('ds-examples').innerHTML = 'e.g. ' +
310
- m.ex.map(e => `<span class="cursor-pointer text-cyan-500 underline"
311
- onclick="fillC(${e.a},${e.b})">A=${e.a} B=${e.b}β†’${m.fn(e.a,e.b)}</span>`).join(' ');
312
- updateExpected();
313
- }
314
-
315
- function fillC(a, b) {
316
- document.getElementById('ca').value = a;
317
- document.getElementById('cb').value = b;
318
- document.getElementById('cc').value = '';
319
- updateExpected();
320
- }
321
-
322
- function updateExpected() {
323
- const a = document.getElementById('ca').value, b = document.getElementById('cb').value;
324
- const ds = document.getElementById('ds-sel').value;
325
- if (a && b && DS[ds])
326
- document.getElementById('expected-lbl').innerText =
327
- `Ground truth: ${DS[ds].fn(parseFloat(a), parseFloat(b))}`;
328
- }
329
-
330
- refreshDS();
331
- updateSpringCount();
332
 
333
  // ── TABS ──────────────────────────────────────────────────────────────────────
334
  function tab(name) {
335
- ['nodes','springs','logs'].forEach(t => {
336
  document.getElementById(`pane-${t}`).classList.toggle('hidden', t !== name);
337
  document.getElementById(`tab-${t}`).className = t === name
338
- ? 'flex-1 py-1.5 bg-blue-900/40 text-blue-300 font-bold text-[10px]'
339
- : 'flex-1 py-1.5 text-slate-500 text-[10px]';
340
  });
341
  }
342
 
343
  // ── VISUALIZATION ─────────────────────────────────────────────────────────────
344
- //
345
- // Vertical bilateral hourglass β€” exact sketch match:
346
- //
347
- // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” ← A bus (top rectangle, orange)
348
- // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
349
- // / U1 U2 U3 \ ← upper bulge (fan-out from A)
350
- // / \
351
- // └──────Cβ”€β”€β”€β”€β”€β”€β”˜ ← C waist (center rectangle, cyan)
352
- // \ /
353
- // \ L1 L2 L3 / ← lower bulge (fan-in to B)
354
- // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” ← B bus (bottom rectangle, purple)
355
- // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
356
- //
357
- // Y axis: A=+4.4, upper bulge=+2.0, C=0, lower bulge=-2.0, B=-4.4
358
-
359
- const Y = { A: 4.4, U: 2.1, C: 0.0, L: -2.1, B: -4.4 };
360
-
361
- function layerX(ids, spread) {
362
- const n = ids.length;
363
- return ids.map((_, i) => n === 1 ? 0 : spread * (2*i/(n-1) - 1));
364
- }
365
-
366
- function buildPos(layers) {
367
- // layers = [['A'], ['U1'..'Un'], ['C'], ['L1'..'Ln'], ['B']]
368
  const pos = {};
369
- const spreads = [0, 3.8, 0, 3.8, 0];
370
- const ys = [Y.A, Y.U, Y.C, Y.L, Y.B];
371
  layers.forEach((layer, li) => {
372
- const xs = layerX(layer, spreads[li]);
373
- layer.forEach((id, i) => { pos[id] = [xs[i], ys[li]]; });
 
 
 
374
  });
375
  return pos;
376
  }
377
 
378
- // Bus rectangle shapes
379
  function busShapes(pos) {
380
  const sh = [];
381
- const rect = (xc, yc, hw, hh, fill, stroke) => sh.push({
382
- type: 'rect', xref: 'x', yref: 'y',
383
- x0: xc-hw, x1: xc+hw, y0: yc-hh, y1: yc+hh,
384
- fillcolor: fill, line: { color: stroke, width: 2 }
385
  });
386
- rect(0, Y.A, 2.2, 0.35, 'rgba(251,146,60,0.10)', 'rgba(251,146,60,0.65)'); // A bus
387
- rect(0, Y.C, 1.8, 0.32, 'rgba(56,189,248,0.10)', 'rgba(56,189,248,0.70)'); // C waist
388
- rect(0, Y.B, 2.2, 0.35, 'rgba(192,132,252,0.10)','rgba(192,132,252,0.65)'); // B bus
389
  return sh;
390
  }
391
 
392
- // Spring color: positive = amber warmth, negative = cool blue
393
- function springColor(k) {
394
- const t = Math.min(Math.abs(k) / 6, 1);
395
- if (k >= 0) return [`rgb(${Math.round(180+t*70)},${Math.round(100+t*80)},30)`, 1.0 + t*3.5];
396
- return [`rgb(30,${Math.round(80+t*100)},${Math.round(140+t*115)})`, 1.0 + t*3.5];
 
397
  }
398
 
399
  function buildTraces(nodes, springs, layers) {
400
- const pos = buildPos(layers);
401
  const traces = [];
402
 
403
- // Edges
404
- for (const [key, k] of Object.entries(springs)) {
405
  const [u, v] = key.split('β†’');
406
  if (!pos[u] || !pos[v]) continue;
407
- const [col, wd] = springColor(k);
408
  traces.push({
409
  type:'scatter', mode:'lines',
410
  x:[pos[u][0], pos[v][0]], y:[pos[u][1], pos[v][1]],
411
- line:{ color: col, width: wd },
412
- hoverinfo:'none', showlegend:false
413
  });
414
  }
415
 
416
- // Nodes
417
  const allN = layers.flat();
418
- const nodeCol = id => {
419
- if (id === 'A') return '#fb923c';
420
- if (id === 'B') return '#c084fc';
421
- if (id === 'C') return '#38bdf8';
422
- if (id.startsWith('U')) return '#4ade80';
423
- return '#67e8f9';
424
- };
425
-
426
  traces.push({
427
  type:'scatter', mode:'markers+text',
428
  x: allN.map(id => pos[id][0]),
429
  y: allN.map(id => pos[id][1]),
430
  text: allN.map(id => {
431
- const xv = Number(nodes[id]?.x ?? 0).toFixed(2);
432
- return `${id}\n${xv}`;
433
  }),
434
- textposition: allN.map(id => {
435
- if (id === 'A') return 'top center';
436
- if (id === 'B') return 'bottom center';
437
- return 'top center';
438
- }),
439
- textfont:{ size:9, color: allN.map(id => nodeCol(id)) },
440
  marker:{
441
  size: allN.map(id => {
442
- const v = Math.abs(nodes[id]?.vel ?? 0);
443
  const base = ['A','B','C'].includes(id) ? 20 : 13;
444
- return base + Math.min(v * 30, 9);
445
  }),
446
- color: allN.map(id => nodeCol(id)),
447
- opacity: allN.map(id => 0.75 + Math.min(Math.abs(nodes[id]?.vel ?? 0) * 1.8, 0.25)),
448
- line:{ width:2.5, color: allN.map(id => nodes[id]?.anchored ? '#ef4444' : '#22c55e') }
 
 
 
449
  },
450
  hoverinfo:'none', showlegend:false
451
  });
@@ -454,25 +329,36 @@ function buildTraces(nodes, springs, layers) {
454
  }
455
 
456
  function meshLayout(layers) {
457
- const pos = buildPos(layers);
458
  return {
459
- margin:{ l:8, r:8, t:8, b:8 },
460
  paper_bgcolor:'transparent', plot_bgcolor:'transparent',
461
- xaxis:{ visible:false, range:[-5.5, 5.5] },
462
- yaxis:{ visible:false, range:[-5.5, 5.5] },
463
  showlegend:false,
464
- shapes: busShapes(pos),
465
  };
466
  }
467
 
468
  const ERR_LAYOUT = {
469
- margin:{ l:30, r:6, t:3, b:12 },
470
  paper_bgcolor:'transparent', plot_bgcolor:'transparent',
471
- xaxis:{ visible:false },
472
- yaxis:{ color:'#334155', gridcolor:'#0f172a', zeroline:true, zerolinecolor:'#22c55e', zerolinewidth:1 },
 
 
 
473
  showlegend:false,
474
  };
475
 
 
 
 
 
 
 
 
 
 
476
  let meshPlotted = false, errPlotted = false, lastLayerKey = '';
477
 
478
  // ── POLL ──────────────────────────────────────────────────────────────────────
@@ -482,78 +368,129 @@ setInterval(async () => {
482
  const d = await r.json();
483
 
484
  // Sync topo
485
- topo.upper = d.n_upper; topo.lower = d.n_lower;
486
- syncTopoUI();
487
- document.getElementById('b-alpha').innerText = `Ξ±:${d.back_alpha.toFixed(2)}`;
488
-
489
- // Header
 
 
 
 
 
 
 
 
 
490
  document.getElementById('run-dot').className =
491
  `w-2 h-2 rounded-full ${d.running ? 'bg-green-400' : 'bg-slate-700'}`;
492
- document.getElementById('q-lbl').innerText = `Q:${d.queue_size}`;
493
- document.getElementById('iter-lbl').innerText = `IT:${d.iter}`;
494
 
495
- // Tension
496
- const e = Math.abs(d.error);
497
- const col = e < 0.05 ? 'text-green-400' : e < 2 ? 'text-yellow-400' : 'text-red-400';
498
  document.getElementById('err-big').className = `text-xl font-bold ${col} leading-none`;
499
- document.getElementById('err-big').innerText = d.error.toFixed(4);
500
- document.getElementById('pred-val').innerText = `P:${d.prediction.toFixed(3)}`;
 
 
501
 
502
- // Nodes pane — ordered top→bottom
503
- const order = d.layers.flat();
504
  let nh = '';
505
- order.forEach(id => {
506
  const n = d.nodes[id]; if (!n) return;
507
  const icon = n.anchored ? '<span class="text-red-400">⊠</span>' : '<span class="text-green-500">β—Ž</span>';
508
- const vel = `<span class="text-slate-700 text-[8px] ml-1">v:${(n.vel||0).toFixed(3)}</span>`;
509
- nh += `<div class="flex justify-between items-center py-0.5 border-b border-slate-900">
510
- ${icon}<span class="ml-1 text-sky-300">${id}</span>
511
- <span class="text-white font-bold">${Number(n.x).toFixed(4)}</span>${vel}</div>`;
 
 
 
 
 
 
512
  });
513
  document.getElementById('pane-nodes').innerHTML = nh;
514
 
515
- // Springs pane
516
  let sh = '';
517
- for (const [key, k] of Object.entries(d.springs)) {
518
- const kc = k < 0 ? 'text-blue-300' : k > 4 ? 'text-yellow-200' : 'text-purple-300';
519
- sh += `<div class="flex justify-between py-0.5 border-b border-slate-900">
 
520
  <span class="text-slate-500 text-[9px]">${key}</span>
521
- <span class="${kc} font-bold text-[10px]">${k.toFixed(4)}</span></div>`;
 
 
 
522
  }
523
  document.getElementById('pane-springs').innerHTML = sh;
524
 
525
- // Logs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  document.getElementById('pane-logs').innerHTML =
527
- d.logs.map(l => `<div class="py-0.5 border-b border-slate-900/50">${l}</div>`).join('');
528
 
529
- // Mesh plot
530
  const layerKey = JSON.stringify(d.layers);
531
  const traces = buildTraces(d.nodes, d.springs, d.layers);
532
  const layout = meshLayout(d.layers);
533
  if (!meshPlotted || layerKey !== lastLayerKey) {
534
- Plotly.newPlot('mesh-plot', traces, layout, { displayModeBar:false, responsive:true });
535
- meshPlotted = true; lastLayerKey = layerKey;
 
536
  } else {
537
  Plotly.react('mesh-plot', traces, layout);
538
  }
539
 
540
- // Error chart
541
- const hist = d.history;
542
  const eTrace = {
543
  type:'scatter', mode:'lines',
544
- x: hist.map((_,i)=>i), y: hist,
545
- line:{ color:'#f97316', width:1.5 },
546
  fill:'tozeroy', fillcolor:'rgba(249,115,22,0.07)'
547
  };
548
  if (!errPlotted) {
549
- Plotly.newPlot('err-plot', [eTrace], ERR_LAYOUT, { displayModeBar:false, responsive:true });
550
  errPlotted = true;
551
  } else {
552
  Plotly.react('err-plot', [eTrace], ERR_LAYOUT);
553
  }
554
 
555
  } catch(e) { /* silent */ }
556
- }, 200);
557
  </script>
558
  </body>
559
  </html>
 
11
  .glass { background:rgba(10,18,35,0.98); border:1px solid #1e2d40; }
12
  #drawer { transition:transform .3s ease; z-index:200; }
13
  .drawer-closed { transform:translateY(100%); }
 
 
 
14
  .lbtn {
15
+ width:20px; height:20px; background:#1e293b; border:1px solid #334155;
16
+ border-radius:3px; color:#94a3b8; font-weight:bold; font-size:13px;
17
  cursor:pointer; display:inline-flex; align-items:center; justify-content:center;
18
  }
19
  .lbtn:hover { background:#334155; color:#fff; }
20
+ .ptype { display:inline-block; padding:1px 5px; border-radius:3px; font-size:9px; font-weight:bold; }
21
+ .pt-box_proj { background:#1e3a2f; color:#4ade80; }
22
+ .pt-halfspace { background:#1e2d4a; color:#60a5fa; }
23
+ .pt-sphere { background:#3a1e3a; color:#e879f9; }
24
+ .pt-simplex { background:#3a2a1e; color:#fb923c; }
25
+ .pt-elastic { background:#3a1e1e; color:#f87171; }
26
+ .pt-unknown { background:#1e1e1e; color:#64748b; }
27
  </style>
28
  </head>
29
  <body class="flex flex-col h-screen overflow-hidden">
30
 
31
  <!-- ── HEADER ──────────────────────────────────────────────────────────────── -->
32
+ <header class="glass flex-shrink-0 px-2 py-1.5 flex justify-between items-center gap-1">
33
+
34
+ <!-- Mode + topology badges -->
35
+ <div class="flex flex-wrap gap-1 items-center text-[8px] font-bold">
36
+ <span id="b-mode" class="px-1.5 py-0.5 rounded bg-yellow-900/60 text-yellow-300 border border-yellow-800/60">IDLE</span>
37
+ <span id="b-type" class="px-1.5 py-0.5 rounded bg-slate-800 text-slate-400 border border-slate-700">β€”</span>
38
+ <span id="b-dim" class="px-1.5 py-0.5 rounded bg-slate-900 text-slate-600 border border-slate-800">D:32</span>
39
+
40
+ <!-- Inline topology U / L -->
41
  <span class="px-1.5 py-0.5 rounded bg-green-900/60 text-green-300 border border-green-800/60 flex items-center gap-0.5">
42
  <span class="text-orange-400 mr-0.5">U</span>
43
  <button class="lbtn" onclick="quickL('upper',-1)">βˆ’</button>
 
48
  <span id="b-lower" class="w-3 text-center text-white">3</span>
49
  <button class="lbtn" onclick="quickL('lower',+1)">+</button>
50
  </span>
 
51
  </div>
52
+
53
+ <!-- Controls -->
54
+ <div class="flex items-center gap-1 flex-shrink-0">
55
+ <span id="q-lbl" class="text-[8px] text-slate-600">Q:0</span>
56
+ <div id="run-dot" class="w-2 h-2 rounded-full bg-slate-700"></div>
57
+ <button onclick="doTrainOffline()" class="text-[9px] bg-purple-800 hover:bg-purple-700 px-2 py-1 rounded font-bold">⚑ TRAIN</button>
58
+ <button onclick="doTrainVisual()" class="text-[9px] bg-blue-800 hover:bg-blue-700 px-2 py-1 rounded font-bold">πŸ‘ VIZ</button>
59
+ <button onclick="doInfer()" class="text-[9px] bg-green-800 hover:bg-green-700 px-2 py-1 rounded font-bold">β–Ά INFER</button>
60
+ <button onclick="doHalt()" class="text-[9px] bg-red-900 hover:bg-red-800 px-2 py-1 rounded font-bold">β– </button>
61
+ <button onclick="openDrawer()" class="text-[9px] bg-slate-700 hover:bg-slate-600 px-2 py-1 rounded font-bold">βš™</button>
62
  </div>
63
  </header>
64
 
65
  <!-- ── PLOTS ────────────────────────────────────────────────────────────────── -->
66
  <div class="flex-grow flex flex-col min-h-0 overflow-hidden">
67
  <div id="mesh-plot" class="flex-grow min-h-0"></div>
68
+ <div id="err-plot" style="height:56px" class="flex-shrink-0 border-t border-slate-900"></div>
69
  </div>
70
 
71
  <!-- ── BOTTOM PANEL ─────────────────��───────────────────────────────────────── -->
72
+ <div class="glass flex-shrink-0 border-t border-slate-800" style="height:180px">
73
  <div class="flex border-b border-slate-800 text-[10px]">
74
+ <button onclick="tab('nodes')" id="tab-nodes" class="flex-1 py-1 bg-blue-900/40 text-blue-300 font-bold">NODES</button>
75
+ <button onclick="tab('springs')" id="tab-springs" class="flex-1 py-1 text-slate-500">SPRINGS</button>
76
+ <button onclick="tab('accuracy')" id="tab-accuracy" class="flex-1 py-1 text-slate-500">ACCURACY</button>
77
+ <button onclick="tab('logs')" id="tab-logs" class="flex-1 py-1 text-slate-500">LOGS</button>
78
  </div>
79
  <div class="flex h-full overflow-hidden">
80
+ <!-- Tension meter -->
81
  <div class="w-20 flex-shrink-0 border-r border-slate-800 flex flex-col items-center justify-center p-1 gap-0.5">
82
+ <div class="text-[7px] text-slate-600 uppercase tracking-widest">β€–errorβ€–</div>
83
  <div id="err-big" class="text-xl font-bold text-red-400 leading-none">0.00</div>
84
+ <div id="pred-val" class="text-[8px] text-slate-500">β€–Cβ€–:β€”</div>
85
  <div id="iter-lbl" class="text-[8px] text-slate-700">IT:0</div>
86
+ <div id="step-lbl" class="text-[7px] text-slate-800">S:0</div>
87
  </div>
88
+ <!-- Panes -->
89
  <div class="flex-grow overflow-y-auto p-1.5 text-[10px]">
90
  <div id="pane-nodes"></div>
91
+ <div id="pane-springs" class="hidden"></div>
92
+ <div id="pane-accuracy" class="hidden"></div>
93
+ <div id="pane-logs" class="hidden text-[9px] text-slate-500"></div>
94
  </div>
95
  </div>
96
  </div>
97
 
98
  <!-- ── DRAWER ───────────────────────────────────────────────────────────────── -->
99
+ <aside id="drawer" class="drawer-closed fixed inset-x-0 bottom-0 glass border-t border-slate-700 p-4 flex flex-col gap-3 max-h-[90vh] overflow-y-auto">
100
  <div class="flex justify-between items-center">
101
+ <span class="text-orange-400 font-bold">ELASTIC MESH LABORATORY</span>
102
  <button onclick="closeDrawer()" class="text-slate-400 text-2xl leading-none">βœ•</button>
103
  </div>
104
 
105
+ <!-- Architecture info (read-only for now) -->
106
+ <div class="bg-slate-900 rounded p-3 border border-blue-900/50 text-[9px]">
107
+ <div class="text-blue-400 font-bold mb-2">ARCHITECTURE</div>
108
+ <div class="text-slate-400 space-y-1">
109
+ <div>β€’ Spring type: <span class="text-white">Matrix K ∈ ℝ^(DΓ—D)</span> per edge</div>
110
+ <div>β€’ Update rule: <span class="text-white">Matrix LMS β€” joint optimal step</span></div>
111
+ <div>β€’ Inference: <span class="text-white">EWC-regularised online adaptation</span></div>
112
+ <div>β€’ Convergence: <span class="text-white">β€–errorβ€– &lt; 0.08 or 400 steps</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  </div>
114
+ </div>
115
 
116
+ <!-- Topology controls (detailed) -->
117
+ <div class="bg-slate-900 rounded p-3 border border-green-900/50">
118
+ <div class="text-green-400 text-[9px] font-bold mb-2">TOPOLOGY A β†’ [U] β†’ C ← [L] ← B</div>
119
+ <div class="grid grid-cols-2 gap-2">
120
+ <div class="bg-slate-800 rounded p-2 text-center border border-orange-900/30">
121
+ <div class="text-orange-400 text-[8px] mb-1">UPPER (A-side)</div>
122
+ <div class="flex items-center justify-center gap-2">
123
+ <button class="lbtn text-base" onclick="quickL('upper',-1)">βˆ’</button>
124
+ <span id="dial-upper" class="text-orange-300 font-bold text-2xl">3</span>
125
+ <button class="lbtn text-base" onclick="quickL('upper',+1)">+</button>
 
 
 
 
 
126
  </div>
127
+ </div>
128
+ <div class="bg-slate-800 rounded p-2 text-center border border-cyan-900/30">
129
+ <div class="text-cyan-400 text-[8px] mb-1">LOWER (B-side)</div>
130
+ <div class="flex items-center justify-center gap-2">
131
+ <button class="lbtn text-base" onclick="quickL('lower',-1)">βˆ’</button>
132
+ <span id="dial-lower" class="text-cyan-300 font-bold text-2xl">3</span>
133
+ <button class="lbtn text-base" onclick="quickL('lower',+1)">+</button>
 
134
  </div>
135
  </div>
 
136
  </div>
137
+ <div id="spring-info" class="text-[8px] text-slate-600 mt-2 text-center"></div>
138
  </div>
139
 
140
+ <!-- Training controls -->
141
+ <div class="bg-slate-900 rounded p-3 border border-purple-900/50">
142
+ <div class="text-purple-400 text-[9px] font-bold mb-2">OFFLINE TRAINING (fast, no display)</div>
143
+ <div class="flex gap-2 items-center mb-2">
144
+ <label class="text-[9px] text-slate-500 w-16">Epochs</label>
145
+ <input id="ep-n" type="number" value="5" min="1" max="50"
146
+ class="w-16 bg-black border border-slate-700 p-1 text-white text-sm text-center rounded">
147
+ <button onclick="doTrainOffline()"
148
+ class="flex-1 bg-purple-800 hover:bg-purple-700 py-2 text-xs font-bold rounded">⚑ START</button>
149
  </div>
150
+ <div class="text-[8px] text-slate-600">
151
+ Runs full LMS epochs at CPU speed. Fisher info accumulates for EWC.
 
 
 
 
 
 
 
 
 
 
 
152
  </div>
153
  </div>
154
 
155
+ <!-- Inference controls -->
156
+ <div class="bg-slate-900 rounded p-3 border border-green-900/50">
157
+ <div class="text-green-400 text-[9px] font-bold mb-2">INFERENCE TEST</div>
158
+ <div class="flex gap-2 items-center mb-2">
159
+ <label class="text-[9px] text-slate-500 w-16">N samples</label>
160
+ <input id="inf-n" type="number" value="200" min="10" max="556"
161
+ class="w-16 bg-black border border-slate-700 p-1 text-white text-sm text-center rounded">
162
+ <button onclick="doInfer()"
163
+ class="flex-1 bg-green-800 hover:bg-green-700 py-2 text-xs font-bold rounded">β–Ά RUN</button>
164
+ </div>
165
+ <div class="text-[8px] text-slate-600">
166
+ Mesh sees A+B only. C_pred vs C_target β†’ accuracy per problem type.
 
 
 
 
 
 
 
 
 
 
167
  </div>
 
 
 
168
  </div>
169
 
170
+ <div class="flex gap-2">
171
+ <button onclick="doReset()"
172
+ class="flex-1 border border-slate-600 text-slate-400 py-2 rounded text-xs font-bold">RESET SPRINGS</button>
173
+ <button onclick="doHalt(); closeDrawer()"
174
+ class="flex-1 border border-red-700 text-red-500 py-2 rounded text-xs font-bold">HALT</button>
175
+ </div>
176
  </aside>
177
 
178
  <script>
179
+ // ── LOCAL STATE ───────────────────────────────────────────────────────────────
 
180
  const topo = { upper: 3, lower: 3 };
181
 
182
+ function syncTopoUI(nu, nl) {
183
+ topo.upper = nu; topo.lower = nl;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  ['upper','lower'].forEach(k => {
185
  document.getElementById(`b-${k}`).innerText = topo[k];
186
  document.getElementById(`dial-${k}`).innerText = topo[k];
187
  });
188
+ const d = 32; // from server dim
189
+ const n = nu + nl;
190
+ document.getElementById('spring-info').innerText =
191
+ `${2*n} spring matrices Γ— DΒ² = ${2*n}Γ—${d*d} = ${2*n*d*d} parameters`;
192
  }
193
 
194
+ async function quickL(w, d) {
195
+ const r = await fetch('/set_layer', {
196
+ method:'POST', headers:{'Content-Type':'application/json'},
197
+ body: JSON.stringify({ layer: w, delta: d })
198
  });
199
+ const data = await r.json();
200
+ syncTopoUI(data.n_upper, data.n_lower);
201
+ meshPlotted = false;
202
  }
203
 
204
+ function openDrawer() { document.getElementById('drawer').classList.remove('drawer-closed'); }
205
  function closeDrawer() { document.getElementById('drawer').classList.add('drawer-closed'); }
206
 
207
+ // ── ACTIONS ───────────────────────────────────────────────────────────────────
208
+ async function doTrainOffline() {
209
+ const ep = parseInt(document.getElementById('ep-n').value);
210
+ await fetch('/train_offline', {
211
  method:'POST', headers:{'Content-Type':'application/json'},
212
+ body: JSON.stringify({ epochs: ep })
 
 
 
213
  });
 
 
 
 
 
 
214
  }
215
+ async function doTrainVisual() {
216
+ await fetch('/train_visual', { method:'POST', headers:{'Content-Type':'application/json'}, body:'{}' });
 
 
 
 
 
217
  }
218
+ async function doInfer() {
219
+ const n = parseInt(document.getElementById('inf-n').value);
220
+ await fetch('/infer', {
 
 
 
221
  method:'POST', headers:{'Content-Type':'application/json'},
222
+ body: JSON.stringify({ n })
223
  });
224
  closeDrawer();
225
  }
226
+ async function doHalt() { await fetch('/halt', { method:'POST' }); }
227
+ async function doReset() { await fetch('/reset', { method:'POST' }); meshPlotted = false; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
  // ── TABS ──────────────────────────────────────────────────────────────────────
230
  function tab(name) {
231
+ ['nodes','springs','accuracy','logs'].forEach(t => {
232
  document.getElementById(`pane-${t}`).classList.toggle('hidden', t !== name);
233
  document.getElementById(`tab-${t}`).className = t === name
234
+ ? 'flex-1 py-1 bg-blue-900/40 text-blue-300 font-bold text-[10px]'
235
+ : 'flex-1 py-1 text-slate-500 text-[10px]';
236
  });
237
  }
238
 
239
  // ── VISUALIZATION ─────────────────────────────────────────────────────────────
240
+ // Vertical bilateral hourglass:
241
+ // A (top bus) β†’ U1..Un (upper bulge) β†’ C (center waist)
242
+ // B (bot bus) β†’ L1..Ln (lower bulge) β†’ C
243
+ const Y = { A:4.3, U:2.0, C:0.0, L:-2.0, B:-4.3 };
244
+ const SPREAD = { A:0, U:3.6, C:0, L:3.6, B:0 };
245
+ const NCOL = id =>
246
+ id==='A' ? '#fb923c' : id==='B' ? '#c084fc' : id==='C' ? '#38bdf8' :
247
+ id.startsWith('U') ? '#4ade80' : '#67e8f9';
248
+
249
+ function nodePos(layers) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  const pos = {};
251
+ const ymap = [Y.A, Y.U, Y.C, Y.L, Y.B];
252
+ const smap = [SPREAD.A, SPREAD.U, SPREAD.C, SPREAD.L, SPREAD.B];
253
  layers.forEach((layer, li) => {
254
+ const n = layer.length;
255
+ const s = smap[li] || 0;
256
+ layer.forEach((id, i) => {
257
+ pos[id] = [n===1 ? 0 : s*(2*i/(n-1)-1), ymap[li]];
258
+ });
259
  });
260
  return pos;
261
  }
262
 
 
263
  function busShapes(pos) {
264
  const sh = [];
265
+ const rect = (xc,yc,hw,hh,fill,stroke) => sh.push({
266
+ type:'rect', xref:'x', yref:'y',
267
+ x0:xc-hw, x1:xc+hw, y0:yc-hh, y1:yc+hh,
268
+ fillcolor:fill, line:{color:stroke, width:2}
269
  });
270
+ rect(0, Y.A, 2.0, 0.32, 'rgba(251,146,60,0.09)', 'rgba(251,146,60,0.60)');
271
+ rect(0, Y.C, 1.6, 0.30, 'rgba(56,189,248,0.09)', 'rgba(56,189,248,0.65)');
272
+ rect(0, Y.B, 2.0, 0.32, 'rgba(192,132,252,0.09)','rgba(192,132,252,0.60)');
273
  return sh;
274
  }
275
 
276
+ function springColor(frob, mean) {
277
+ const t = Math.min(frob / 4, 1);
278
+ const r = mean >= 0
279
+ ? [Math.round(160+t*90), Math.round(90+t*80), 30]
280
+ : [30, Math.round(80+t*100), Math.round(120+t*135)];
281
+ return [`rgb(${r[0]},${r[1]},${r[2]})`, 0.8 + t*3.2];
282
  }
283
 
284
  function buildTraces(nodes, springs, layers) {
285
+ const pos = nodePos(layers);
286
  const traces = [];
287
 
288
+ // Edge traces
289
+ for (const [key, s] of Object.entries(springs)) {
290
  const [u, v] = key.split('β†’');
291
  if (!pos[u] || !pos[v]) continue;
292
+ const [col, wd] = springColor(s.frob, s.mean);
293
  traces.push({
294
  type:'scatter', mode:'lines',
295
  x:[pos[u][0], pos[v][0]], y:[pos[u][1], pos[v][1]],
296
+ line:{color:col, width:wd}, hoverinfo:'none', showlegend:false
 
297
  });
298
  }
299
 
300
+ // Node trace
301
  const allN = layers.flat();
 
 
 
 
 
 
 
 
302
  traces.push({
303
  type:'scatter', mode:'markers+text',
304
  x: allN.map(id => pos[id][0]),
305
  y: allN.map(id => pos[id][1]),
306
  text: allN.map(id => {
307
+ const n = nodes[id];
308
+ return `${id}\nβ€–${n?.norm?.toFixed(2) ?? '?'}β€–`;
309
  }),
310
+ textposition: allN.map(id => id==='B' ? 'bottom center' : 'top center'),
311
+ textfont:{ size:9, color: allN.map(id => NCOL(id)) },
 
 
 
 
312
  marker:{
313
  size: allN.map(id => {
314
+ const vn = nodes[id]?.vel_norm ?? 0;
315
  const base = ['A','B','C'].includes(id) ? 20 : 13;
316
+ return base + Math.min(vn*20, 8);
317
  }),
318
+ color: allN.map(id => NCOL(id)),
319
+ opacity: allN.map(id => 0.75 + Math.min((nodes[id]?.vel_norm??0)*1.5, 0.25)),
320
+ line:{
321
+ width:2.5,
322
+ color: allN.map(id => nodes[id]?.anchored ? '#ef4444' : '#22c55e')
323
+ }
324
  },
325
  hoverinfo:'none', showlegend:false
326
  });
 
329
  }
330
 
331
  function meshLayout(layers) {
 
332
  return {
333
+ margin:{l:6,r:6,t:6,b:6},
334
  paper_bgcolor:'transparent', plot_bgcolor:'transparent',
335
+ xaxis:{visible:false, range:[-5.5, 5.5]},
336
+ yaxis:{visible:false, range:[-5.5, 5.5]},
337
  showlegend:false,
338
+ shapes: busShapes(nodePos(layers)),
339
  };
340
  }
341
 
342
  const ERR_LAYOUT = {
343
+ margin:{l:28,r:5,t:3,b:10},
344
  paper_bgcolor:'transparent', plot_bgcolor:'transparent',
345
+ xaxis:{visible:false},
346
+ yaxis:{
347
+ color:'#334155', gridcolor:'#0f172a',
348
+ zeroline:true, zerolinecolor:'#22c55e', zerolinewidth:1
349
+ },
350
  showlegend:false,
351
  };
352
 
353
+ // Problem type color mapping
354
+ const TYPE_COL = {
355
+ box_proj: '#4ade80',
356
+ halfspace: '#60a5fa',
357
+ sphere: '#e879f9',
358
+ simplex: '#fb923c',
359
+ elastic: '#f87171',
360
+ };
361
+
362
  let meshPlotted = false, errPlotted = false, lastLayerKey = '';
363
 
364
  // ── POLL ──────────────────────────────────────────────────────────────────────
 
368
  const d = await r.json();
369
 
370
  // Sync topo
371
+ syncTopoUI(d.n_upper, d.n_lower);
372
+
373
+ // Header badges
374
+ const modeColor = {
375
+ training: 'bg-yellow-700 text-yellow-100',
376
+ inference: 'bg-green-800 text-green-100',
377
+ idle: 'bg-slate-800 text-slate-400',
378
+ }[d.mode] || 'bg-slate-800 text-slate-400';
379
+ document.getElementById('b-mode').className = `px-1.5 py-0.5 rounded text-[8px] font-bold ${modeColor}`;
380
+ document.getElementById('b-mode').innerText = d.mode.toUpperCase();
381
+ document.getElementById('b-type').innerText = d.current_type || 'β€”';
382
+ document.getElementById('b-type').className =
383
+ `ptype pt-${d.current_type || 'unknown'}`;
384
+ document.getElementById('b-dim').innerText = `D:${d.dim}`;
385
  document.getElementById('run-dot').className =
386
  `w-2 h-2 rounded-full ${d.running ? 'bg-green-400' : 'bg-slate-700'}`;
387
+ document.getElementById('q-lbl').innerText = `Q:${d.queue_size}`;
 
388
 
389
+ // Tension meter
390
+ const e = d.error;
391
+ const col = e < 0.05 ? 'text-green-400' : e < 0.5 ? 'text-yellow-400' : 'text-red-400';
392
  document.getElementById('err-big').className = `text-xl font-bold ${col} leading-none`;
393
+ document.getElementById('err-big').innerText = e.toFixed(4);
394
+ document.getElementById('pred-val').innerText = `β€–Cβ€–:${d.pred_norm.toFixed(2)}`;
395
+ document.getElementById('iter-lbl').innerText = `IT:${d.iter}`;
396
+ document.getElementById('step-lbl').innerText = `S:${d.step_count}`;
397
 
398
+ // ── Nodes pane ───��─────────────────────────────────────────────────────
 
399
  let nh = '';
400
+ (d.layers || []).flat().forEach(id => {
401
  const n = d.nodes[id]; if (!n) return;
402
  const icon = n.anchored ? '<span class="text-red-400">⊠</span>' : '<span class="text-green-500">β—Ž</span>';
403
+ const vcol = n.anchored ? 'text-orange-300' : 'text-sky-300';
404
+ const head = (n.x_head || []).map(v => v.toFixed(2)).join(' ');
405
+ nh += `<div class="py-0.5 border-b border-slate-900/60">
406
+ <div class="flex justify-between">
407
+ ${icon}<span class="${vcol} ml-1 font-bold">${id}</span>
408
+ <span class="text-white">β€–xβ€–=${n.norm}</span>
409
+ <span class="text-slate-600 text-[8px]">v=${n.vel_norm.toFixed(3)}</span>
410
+ </div>
411
+ <div class="text-[8px] text-slate-700 mt-0.5 font-mono">[${head}…]</div>
412
+ </div>`;
413
  });
414
  document.getElementById('pane-nodes').innerHTML = nh;
415
 
416
+ // ── Springs pane ───────────────────────────────────────────────────────
417
  let sh = '';
418
+ for (const [key, s] of Object.entries(d.springs || {})) {
419
+ const fc = s.frob > 3 ? 'text-yellow-200' : s.mean < 0 ? 'text-blue-300' : 'text-purple-300';
420
+ const fi = s.fish > 0.01 ? 'text-orange-400' : 'text-slate-700';
421
+ sh += `<div class="flex justify-between py-0.5 border-b border-slate-900/60 items-center">
422
  <span class="text-slate-500 text-[9px]">${key}</span>
423
+ <span class="${fc} font-bold text-[10px]">β€–Kβ€–=${s.frob}</span>
424
+ <span class="text-slate-600 text-[8px]">ΞΌ=${s.mean}</span>
425
+ <span class="${fi} text-[8px]">F=${s.fish}</span>
426
+ </div>`;
427
  }
428
  document.getElementById('pane-springs').innerHTML = sh;
429
 
430
+ // ── Accuracy pane ──────────────────────────────────────────────────────
431
+ const acc = d.type_acc || {};
432
+ const done = d.n_test_done || 0;
433
+ const total= d.test_size || 0;
434
+ let ah = `<div class="text-[9px] text-slate-500 mb-2">
435
+ Test progress: <span class="text-white font-bold">${done}</span> / ${total}
436
+ </div>`;
437
+
438
+ if (Object.keys(acc).length > 0) {
439
+ ah += `<table class="w-full text-[9px] border-collapse">
440
+ <tr class="text-slate-600 border-b border-slate-800">
441
+ <th class="text-left py-0.5">Type</th>
442
+ <th class="text-right py-0.5">N</th>
443
+ <th class="text-right py-0.5">Acc%</th>
444
+ <th class="text-right py-0.5">Avgβ€–eβ€–</th>
445
+ </tr>`;
446
+ for (const [t, v] of Object.entries(acc)) {
447
+ const aCol = v.acc >= 80 ? 'text-green-400' : v.acc >= 50 ? 'text-yellow-400' : 'text-red-400';
448
+ ah += `<tr class="border-b border-slate-900/60">
449
+ <td class="py-0.5"><span class="ptype pt-${t}">${t}</span></td>
450
+ <td class="text-right text-slate-400">${v.n}</td>
451
+ <td class="text-right ${aCol} font-bold">${v.acc}%</td>
452
+ <td class="text-right text-slate-400">${v.avg_err}</td>
453
+ </tr>`;
454
+ }
455
+ ah += '</table>';
456
+ } else {
457
+ ah += '<div class="text-slate-700 text-[9px]">Run inference to see per-type accuracy.</div>';
458
+ }
459
+ document.getElementById('pane-accuracy').innerHTML = ah;
460
+
461
+ // ── Logs pane ──────────────────────────────────────────────────────────
462
  document.getElementById('pane-logs').innerHTML =
463
+ d.logs.map(l => `<div class="py-0.5 border-b border-slate-900/40 font-mono">${l}</div>`).join('');
464
 
465
+ // ── Mesh plot ──────────────────────────────────────────────────────────
466
  const layerKey = JSON.stringify(d.layers);
467
  const traces = buildTraces(d.nodes, d.springs, d.layers);
468
  const layout = meshLayout(d.layers);
469
  if (!meshPlotted || layerKey !== lastLayerKey) {
470
+ Plotly.newPlot('mesh-plot', traces, layout, {displayModeBar:false, responsive:true});
471
+ meshPlotted = true;
472
+ lastLayerKey = layerKey;
473
  } else {
474
  Plotly.react('mesh-plot', traces, layout);
475
  }
476
 
477
+ // ── Error history chart ────────────────────────────────────────────────
478
+ const hist = d.history || [];
479
  const eTrace = {
480
  type:'scatter', mode:'lines',
481
+ x: hist.map((_,i) => i), y: hist,
482
+ line:{color:'#f97316', width:1.5},
483
  fill:'tozeroy', fillcolor:'rgba(249,115,22,0.07)'
484
  };
485
  if (!errPlotted) {
486
+ Plotly.newPlot('err-plot',[eTrace], ERR_LAYOUT, {displayModeBar:false, responsive:true});
487
  errPlotted = true;
488
  } else {
489
  Plotly.react('err-plot', [eTrace], ERR_LAYOUT);
490
  }
491
 
492
  } catch(e) { /* silent */ }
493
+ }, 220);
494
  </script>
495
  </body>
496
  </html>