NOT-OMEGA commited on
Commit
e14fce4
Β·
verified Β·
1 Parent(s): 06581d3

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +209 -176
index.html CHANGED
@@ -4,149 +4,196 @@
4
  <meta charset="UTF-8"/>
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
  <title>KVInfer Studio</title>
 
 
7
  <style>
8
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
  :root {
10
- --bg0: #0c0e13; --bg1: #12151c; --bg2: #191d27; --bg3: #212738;
11
- --accent: #5b8dee; --accent2: #3de68f; --warn: #f5a623; --danger: #e85d75;
12
- --text0: #e8ecf4; --text1: #a3adc4; --text2: #5a6278;
13
- --radius: 12px; --sidebar: 310px; --font: 'Inter', system-ui, sans-serif;
 
 
 
14
  }
15
- html,body { height:100%; background:var(--bg0); color:var(--text0); font-family:var(--font); font-size:14px; }
16
- .app { display:flex; height:100vh; overflow:hidden; }
17
 
18
  /* ── Sidebar ── */
19
- .sidebar { width:var(--sidebar); min-width:var(--sidebar); background:var(--bg1);
20
- border-right:1px solid var(--bg3); display:flex; flex-direction:column; overflow:hidden; }
21
- .sb-head { padding:14px 16px; border-bottom:1px solid var(--bg3); background:var(--bg0); }
22
- .sb-head h2 { font-size:13px; font-weight:700; letter-spacing:.07em;
23
- color:var(--text1); text-transform:uppercase; }
24
- .sb-head p { font-size:11px; color:var(--text2); margin-top:2px; }
25
- .sb-body { flex:1; overflow-y:auto; padding:10px;
26
- scrollbar-width:thin; scrollbar-color:var(--bg3) transparent; }
 
 
 
 
27
 
28
  /* Cards */
29
- .card { background:var(--bg2); border:1px solid var(--bg3); border-radius:var(--radius);
30
- padding:11px 13px; margin-bottom:9px; }
31
- .card-title { font-size:10.5px; font-weight:700; color:var(--text2);
32
- text-transform:uppercase; letter-spacing:.06em; margin-bottom:8px; }
 
33
 
34
  /* Stats */
35
- .srow { display:flex; justify-content:space-between; align-items:center; padding:3px 0; }
36
- .slabel { color:var(--text1); font-size:12px; }
37
- .sval { font-size:13px; font-weight:700; font-variant-numeric:tabular-nums; }
38
- .green { color:var(--accent2); } .blue { color:var(--accent); }
39
- .yellow { color:var(--warn); } .red { color:var(--danger); }
 
 
40
 
41
  /* Live dot */
42
- .dot { display:inline-block; width:7px; height:7px; border-radius:50%;
43
- background:var(--accent2); margin-right:5px; }
44
- .dot.idle { background:var(--text2); }
45
- .dot.live { animation:pulse 1.4s infinite; }
46
  @keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.3;transform:scale(.7)} }
47
 
48
  /* Sparkline */
49
- canvas#spark { width:100%; height:50px; display:block; border-radius:6px; }
 
50
 
51
  /* System prompt */
52
- .sysprompt { width:100%; background:var(--bg0); border:1px solid var(--bg3);
53
- border-radius:8px; color:var(--text0); font-size:12px; padding:8px;
54
- resize:vertical; min-height:56px; font-family:var(--font); line-height:1.5; }
55
- .sysprompt:focus { outline:none; border-color:var(--accent); }
56
 
57
  /* Sliders */
58
- .pgroup { display:flex; flex-direction:column; gap:2px; }
59
- .pgroup label { font-size:10px; color:var(--text2); text-transform:uppercase;
60
- letter-spacing:.06em; display:flex; justify-content:space-between; }
61
- .pgroup label span { color:var(--accent); font-weight:700; }
62
- .pgroup input[type=range] { width:100%; accent-color:var(--accent); }
 
 
 
 
 
 
 
63
 
64
  /* Buttons */
65
- .btn { display:inline-flex; align-items:center; gap:5px; padding:6px 13px;
66
- border-radius:8px; border:none; cursor:pointer; font-size:12px; font-weight:600;
67
- font-family:var(--font); transition:all .13s; }
68
- .btn-p { background:var(--accent); color:#fff; }
69
- .btn-p:hover { background:#4879d4; }
70
- .btn-s { background:var(--bg3); color:var(--text0); }
71
- .btn-s:hover { background:#2b3149; }
72
- .btn-sm { padding:5px 9px; font-size:11px; border-radius:6px; }
73
- .btn-full { width:100%; justify-content:center; }
74
- .btn:disabled { opacity:.35; cursor:not-allowed; }
 
75
 
76
  /* ── Chat Main ── */
77
- .chat { flex:1; display:flex; flex-direction:column; overflow:hidden; background:var(--bg0); }
78
- .chat-hdr { height:54px; display:flex; align-items:center; padding:0 20px;
79
- border-bottom:1px solid var(--bg3); background:var(--bg1); gap:10px; }
80
- .badge { background:var(--bg3); border-radius:6px; padding:3px 9px;
81
- font-size:11px; font-weight:700; color:var(--accent); letter-spacing:.04em; }
82
- .chat-hdr h1 { font-size:15px; font-weight:700; }
83
- .chat-hdr .spc { flex:1; }
84
- .statpill { display:flex; align-items:center; gap:5px; font-size:11px; color:var(--text1); }
 
 
 
85
 
86
  /* Messages */
87
- .msgs { flex:1; overflow-y:auto; padding:18px 20px;
88
- display:flex; flex-direction:column; gap:14px; scroll-behavior:smooth;
89
- scrollbar-width:thin; scrollbar-color:var(--bg3) transparent; }
90
- .mg { display:flex; flex-direction:column; gap:3px; max-width:700px; }
91
- .mg.user { align-self:flex-end; align-items:flex-end; }
92
- .mg.asst { align-self:flex-start; align-items:flex-start; }
93
- .mlabel { font-size:10.5px; color:var(--text2); padding:0 4px; }
94
- .bubble { padding:10px 14px; border-radius:14px; line-height:1.65;
95
- white-space:pre-wrap; word-break:break-word; max-width:560px; }
96
- .mg.user .bubble { background:var(--accent); color:#fff; border-bottom-right-radius:4px; }
97
- .mg.asst .bubble { background:var(--bg2); color:var(--text0);
98
- border:1px solid var(--bg3); border-bottom-left-radius:4px; }
99
- .cursor2 { display:inline-block; width:2px; height:.9em;
100
- background:var(--accent2); margin-left:2px;
101
- animation:blink .55s infinite; vertical-align:text-bottom; }
 
 
 
 
 
 
 
102
  @keyframes blink { 0%,49%{opacity:1} 50%,100%{opacity:0} }
103
- .bmeta { font-size:10px; color:var(--text2); padding:0 4px; display:flex; gap:10px; }
104
- .bmeta b { color:var(--accent2); }
 
105
 
106
  /* Welcome */
107
- .welcome { flex:1; display:flex; flex-direction:column;
108
- align-items:center; justify-content:center; gap:8px;
109
- color:var(--text2); text-align:center; padding:40px; }
110
- .welcome .logo { font-size:44px; margin-bottom:6px; }
111
- .welcome h2 { font-size:19px; color:var(--text1); font-weight:600; }
112
- .welcome p { max-width:340px; line-height:1.6; font-size:13px; }
 
 
 
 
 
 
 
113
 
114
  /* Input */
115
- .inputbar { padding:12px 18px; border-top:1px solid var(--bg3);
116
- background:var(--bg1); display:flex; gap:9px; align-items:flex-end; }
117
- .inputwrap { flex:1; background:var(--bg2); border:1px solid var(--bg3);
118
- border-radius:11px; display:flex; align-items:flex-end;
119
- padding:3px 3px 3px 13px; gap:5px; transition:border-color .13s; }
120
- .inputwrap:focus-within { border-color:var(--accent); }
121
- #inp { flex:1; background:none; border:none; outline:none;
122
- color:var(--text0); font-size:14px; font-family:var(--font);
123
- resize:none; line-height:1.5; max-height:120px; padding:7px 0; }
124
- #inp::placeholder { color:var(--text2); }
125
- .sbtn { background:var(--accent); border:none; cursor:pointer;
126
- width:34px; height:34px; border-radius:8px; display:flex;
127
- align-items:center; justify-content:center; flex-shrink:0; transition:.13s; }
128
- .sbtn:hover { background:#4879d4; }
129
- .sbtn:disabled { opacity:.3; cursor:not-allowed; }
130
- .sbtn svg { fill:white; }
 
 
131
 
132
  /* Benchmark modal */
133
- #bov { display:none; position:fixed; inset:0; background:rgba(0,0,0,.72);
134
- z-index:100; align-items:center; justify-content:center; }
135
- #bov.on { display:flex; }
136
- .bmod { background:var(--bg1); border:1px solid var(--bg3);
137
- border-radius:16px; padding:22px; width:510px; max-height:80vh; overflow-y:auto; }
138
- .bmod h3 { font-size:15px; margin-bottom:14px; }
139
- .btbl { width:100%; border-collapse:collapse; font-size:12px; }
140
- .btbl th,.btbl td { padding:6px 9px; text-align:left; border-bottom:1px solid var(--bg3); }
141
- .btbl th { color:var(--text2); font-weight:600; }
142
- .btbl td.good { color:var(--accent2); } .btbl td.mid { color:var(--warn); }
143
- .btbl td.bad { color:var(--danger); }
144
-
145
- /* Perf badge row */
146
- .perf-badges { display:flex; gap:8px; flex-wrap:wrap; margin-top:6px; }
147
- .pb { background:var(--bg3); border-radius:6px; padding:4px 9px;
148
- font-size:11px; display:flex; gap:5px; align-items:center; }
149
- .pb .pbl { color:var(--text2); } .pb .pbv { font-weight:700; }
 
 
 
 
 
 
 
150
  </style>
151
  </head>
152
  <body>
@@ -155,7 +202,7 @@ canvas#spark { width:100%; height:50px; display:block; border-radius:6px; }
155
  <!-- ── Sidebar ── -->
156
  <aside class="sidebar">
157
  <div class="sb-head">
158
- <h2>⚑ KVInfer Studio</h2>
159
  <p>152M Β· GPT-2 Β· AVX2 + OpenMP Β· KV-Cache</p>
160
  </div>
161
  <div class="sb-body">
@@ -179,7 +226,7 @@ canvas#spark { width:100%; height:50px; display:block; border-radius:6px; }
179
  <div class="card">
180
  <div class="card-title">Throughput History (tok/s)</div>
181
  <canvas id="spark"></canvas>
182
- <div style="margin-top:6px">
183
  <div class="srow"><span class="slabel">Session avg</span>
184
  <span class="sval green" id="s-avg">β€”</span></div>
185
  <div class="srow"><span class="slabel">Session peak</span>
@@ -198,9 +245,9 @@ canvas#spark { width:100%; height:50px; display:block; border-radius:6px; }
198
  <span class="sval blue" id="s-engcache">β€”</span></div>
199
  <div class="srow"><span class="slabel">Server RAM</span>
200
  <span class="sval" id="s-ram">β€”</span></div>
201
- <div style="display:flex;gap:6px;margin-top:9px">
202
- <button class="btn btn-s btn-sm btn-full" onclick="clearChat()">πŸ—‘ Clear</button>
203
- <button class="btn btn-s btn-sm btn-full" onclick="openBench()">πŸ“Š Benchmark</button>
204
  </div>
205
  </div>
206
 
@@ -214,7 +261,7 @@ canvas#spark { width:100%; height:50px; display:block; border-radius:6px; }
214
  <!-- Params -->
215
  <div class="card">
216
  <div class="card-title">Generation</div>
217
- <div style="display:flex;flex-direction:column;gap:9px;margin-top:2px">
218
  <div class="pgroup">
219
  <label>Temperature <span id="v-temp">0.70</span></label>
220
  <input type="range" id="p-temp" min="0.1" max="2.0" step="0.05" value="0.7"
@@ -250,21 +297,28 @@ canvas#spark { width:100%; height:50px; display:block; border-radius:6px; }
250
 
251
  <div class="msgs" id="msgs">
252
  <div class="welcome" id="welcome">
253
- <div class="logo">πŸ€–</div>
254
  <h2>KVInfer Studio</h2>
255
  <p>152M Β· GPT-2 Decoder-Only Β· Custom C++ inference engine with AVX2 SIMD, OpenMP parallelism &amp; persistent session KV-cache.</p>
256
- <p style="margin-top:8px;color:var(--text2)">Type a message below to begin.</p>
 
 
 
 
 
 
257
  </div>
258
  </div>
259
 
260
  <div class="inputbar">
261
  <div class="inputwrap">
262
- <textarea id="inp" rows="1" placeholder="Send a message…"
263
  onkeydown="handleKey(event)"></textarea>
264
  <button class="sbtn" id="sbtn" onclick="send()">
265
  <svg width="15" height="15" viewBox="0 0 24 24"><path d="M2 21l21-9L2 3v7l15 2-15 2v7z"/></svg>
266
  </button>
267
  </div>
 
268
  </div>
269
  </main>
270
 
@@ -273,13 +327,13 @@ canvas#spark { width:100%; height:50px; display:block; border-radius:6px; }
273
  <!-- ── Benchmark modal ── -->
274
  <div id="bov">
275
  <div class="bmod">
276
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
277
  <h3>πŸ“Š Quick Benchmark</h3>
278
  <button class="btn btn-s btn-sm" onclick="closeBench()">βœ•</button>
279
  </div>
280
  <div id="bcontent">
281
- <p style="color:var(--text1);font-size:13px">Runs 5 built-in prompts and measures throughput, TTFT, and per-token latency.</p>
282
- <button class="btn btn-p btn-full" style="margin-top:13px" id="btnbench" onclick="runBench()">β–Ά Run Benchmark</button>
283
  </div>
284
  </div>
285
  </div>
@@ -296,7 +350,6 @@ let totalToks = 0;
296
  let tpsHist = [];
297
  let peakTps = 0;
298
  let engCache = 0;
299
-
300
  // ─────────────────────────────────────────
301
  // Textarea auto-resize
302
  // ─────────────────────────────────────────
@@ -308,7 +361,6 @@ inp.addEventListener('input', () => {
308
  function handleKey(e) {
309
  if (e.key==='Enter' && !e.shiftKey) { e.preventDefault(); send(); }
310
  }
311
-
312
  // ─────────────────────────────────────────
313
  // UI helpers
314
  // ─────────────────────────────────────────
@@ -319,44 +371,40 @@ function setBusy(v) {
319
  const d = document.getElementById(id);
320
  d.className = 'dot' + (v ? ' live' : ' idle');
321
  });
322
- document.getElementById('hstatus').textContent = v ? 'Generating…' : 'Idle';
323
  }
324
-
325
  function scrollBot() {
326
  const el = document.getElementById('msgs');
327
  el.scrollTop = el.scrollHeight;
328
  }
329
-
330
  function hideWelcome() {
331
  const w = document.getElementById('welcome');
332
  if (w) w.remove();
333
  }
334
-
335
  function esc(s) {
336
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>');
337
  }
338
-
339
  function addUserMsg(text) {
340
  hideWelcome();
341
  const g = document.createElement('div');
342
  g.className = 'mg user';
343
- g.innerHTML = `<div class="mlabel">You</div><div class="bubble">${esc(text)}</div>`;
344
  document.getElementById('msgs').appendChild(g);
345
  scrollBot();
346
  }
347
-
348
  function createAssistantSlot() {
349
  const g = document.createElement('div');
350
  g.className = 'mg asst';
351
  g.innerHTML = `
352
- <div class="mlabel">Assistant</div>
353
- <div class="bubble" id="bubble"><span class="cursor2"></span></div>
354
- <div class="bmeta" id="bmeta"></div>`;
 
 
355
  document.getElementById('msgs').appendChild(g);
356
  scrollBot();
357
  return document.getElementById('bubble');
358
  }
359
-
360
  // ─────────────────────────────────────────
361
  // FIX #5 β€” TTFT: use explicit null, not falsy check
362
  // ─────────────────────────────────────────
@@ -367,13 +415,11 @@ async function send() {
367
  inp.value = ''; inp.style.height = 'auto';
368
  addUserMsg(text);
369
  setBusy(true);
370
-
371
  const bubble = createAssistantSlot();
372
  let content = '';
373
  let t0 = Date.now();
374
  let firstTokT = null; // ← FIX: explicit null, not undefined
375
  let tokCount = 0;
376
-
377
  const payload = {
378
  message: text,
379
  session_id: sessId,
@@ -382,7 +428,6 @@ async function send() {
382
  temperature: parseFloat(document.getElementById('p-temp').value),
383
  top_k: parseInt(document.getElementById('p-topk').value),
384
  };
385
-
386
  try {
387
  const resp = await fetch(`${API}/chat`, {
388
  method: 'POST',
@@ -390,11 +435,9 @@ async function send() {
390
  body: JSON.stringify(payload),
391
  });
392
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
393
-
394
  const reader = resp.body.getReader();
395
  const decoder = new TextDecoder();
396
  let buf = '';
397
-
398
  while (true) {
399
  const {done, value} = await reader.read();
400
  if (done) break;
@@ -408,9 +451,7 @@ async function send() {
408
  if (raw === '[DONE]') break;
409
  let chunk;
410
  try { chunk = JSON.parse(raw); } catch { continue; }
411
-
412
  const now = Date.now();
413
-
414
  if (chunk.type === 'token') {
415
  // FIX #5 β€” correct null check
416
  if (firstTokT === null) {
@@ -424,29 +465,24 @@ async function send() {
424
  document.getElementById('s-ttft').textContent = (firstTokT - t0) + ' ms';
425
  bubble.innerHTML = esc(content) + '<span class="cursor2"></span>';
426
  scrollBot();
427
-
428
  } else if (chunk.type === 'done') {
429
  bubble.innerHTML = esc(content);
430
  const ttft = firstTokT !== null ? (firstTokT - t0) : 0;
431
  const tps = chunk.tps;
432
  const ms = chunk.total_ms;
433
-
434
  // Update meta line
435
  document.getElementById('bmeta').innerHTML =
436
  `<b>${tps}</b> tok/s Β· <b>TTFT</b> ${ttft}ms Β· ` +
437
  `<b>${tokCount}</b> tokens Β· <b>${ms.toFixed(0)}ms</b> total`;
438
-
439
  // Update sidebar stats
440
  document.getElementById('s-tps').textContent = tps + ' tok/s';
441
  document.getElementById('s-lat').textContent = ms.toFixed(0) + ' ms';
442
-
443
  tpsHist.push(tps);
444
  if (tpsHist.length > 30) tpsHist.shift();
445
  if (tps > peakTps) peakTps = tps;
446
  const avg = (tpsHist.reduce((a,b)=>a+b,0)/tpsHist.length).toFixed(1);
447
  document.getElementById('s-avg').textContent = avg + ' tok/s';
448
  document.getElementById('s-peak').textContent = peakTps.toFixed(1) + ' tok/s';
449
-
450
  // FIX #2 indicator β€” show how many tokens are cached in engine
451
  if (chunk.session_id) {
452
  fetch(`${API}/chat/history?session_id=${chunk.session_id}`)
@@ -456,18 +492,16 @@ async function send() {
456
  document.getElementById('s-engcache').textContent = engCache + ' tok';
457
  }).catch(()=>{});
458
  }
459
-
460
  turnCount++;
461
  document.getElementById('s-turns').textContent = turnCount;
462
  drawSpark();
463
-
464
  } else if (chunk.type === 'error') {
465
- bubble.innerHTML = `<span style="color:var(--danger)">Error: ${esc(chunk.message)}</span>`;
466
  }
467
  }
468
  }
469
  } catch (err) {
470
- bubble.innerHTML = `<span style="color:var(--danger)">Connection error: ${esc(err.message)}</span>`;
471
  } finally {
472
  const cur = bubble.querySelector('.cursor2');
473
  if (cur) cur.remove();
@@ -475,7 +509,6 @@ async function send() {
475
  scrollBot();
476
  }
477
  }
478
-
479
  // ─────────────────────────────────────────
480
  // Sparkline
481
  // ─────────────────────────────────────────
@@ -493,18 +526,17 @@ function drawSpark() {
493
  const mx = Math.max(...d) * 1.15 || 1;
494
  const step = W / (d.length-1);
495
  const grad = ctx.createLinearGradient(0,0,0,H);
496
- grad.addColorStop(0, 'rgba(61,230,143,.3)');
497
- grad.addColorStop(1, 'rgba(61,230,143,.02)');
498
  ctx.beginPath();
499
  d.forEach((v,i) => {
500
  const x=i*step, y=H-(v/mx)*(H-4)-2;
501
  i===0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y);
502
  });
503
- ctx.strokeStyle='#3de68f'; ctx.lineWidth=2; ctx.stroke();
504
  ctx.lineTo((d.length-1)*step,H); ctx.lineTo(0,H); ctx.closePath();
505
  ctx.fillStyle=grad; ctx.fill();
506
  }
507
-
508
  // ─────────────────────────────────────────
509
  // Clear chat
510
  // ─────────────────────────────────────────
@@ -518,25 +550,29 @@ async function clearChat() {
518
  turnCount = 0; totalToks = 0; tpsHist = []; peakTps = 0; engCache = 0;
519
  document.getElementById('msgs').innerHTML = `
520
  <div class="welcome" id="welcome">
521
- <div class="logo">πŸ€–</div><h2>KVInfer Studio</h2>
 
522
  <p>152M Β· GPT-2 Decoder-Only Β· C++ AVX2 + OpenMP Β· Persistent session KV-cache.</p>
523
- <p style="margin-top:8px;color:var(--text2)">Type a message below to begin.</p>
 
 
 
 
 
524
  </div>`;
525
  ['s-turns','s-totok'].forEach(id => document.getElementById(id).textContent = '0');
526
  ['s-tps','s-ttft','s-lat','s-avg','s-peak','s-toks','s-engcache'].forEach(
527
  id => document.getElementById(id).textContent = 'β€”');
528
  drawSpark();
529
  }
530
-
531
  // ─────────────────────────────────────────
532
  // Benchmark modal
533
  // ─────────────────────────────────────────
534
  function openBench() { document.getElementById('bov').classList.add('on'); }
535
  function closeBench() { document.getElementById('bov').classList.remove('on'); }
536
-
537
  async function runBench() {
538
  const btn = document.getElementById('btnbench');
539
- btn.disabled = true; btn.textContent = '⏳ Running…';
540
  try {
541
  const r = await fetch(`${API}/benchmark/run`);
542
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
@@ -551,34 +587,31 @@ async function runBench() {
551
  <td>${x.tokens_out}</td></tr>`;
552
  }).join('');
553
  document.getElementById('bcontent').innerHTML = `
554
- <div style="display:flex;gap:10px;margin-bottom:14px">
555
- <div class="card" style="flex:1;text-align:center">
556
- <div class="card-title">Avg Throughput</div>
557
- <div class="sval green" style="font-size:22px">${d.summary.avg_tps}</div>
558
- <div style="color:var(--text2);font-size:11px">tok/s</div>
559
  </div>
560
- <div class="card" style="flex:1;text-align:center">
561
- <div class="card-title">Avg TTFT</div>
562
- <div class="sval blue" style="font-size:22px">${d.summary.avg_ttft_ms}</div>
563
- <div style="color:var(--text2);font-size:11px">ms</div>
564
  </div>
565
  </div>
566
  <table class="btbl">
567
  <thead><tr><th>Prompt</th><th>tok/s</th><th>TTFT</th><th>Total</th><th>Toks</th></tr></thead>
568
  <tbody>${rows}</tbody>
569
  </table>
570
- <div style="margin-top:12px;display:flex;gap:7px">
571
  <button class="btn btn-p btn-sm" onclick="runBench()">β†Ί Rerun</button>
572
  <button class="btn btn-s btn-sm" onclick="closeBench()">Close</button>
573
  </div>`;
574
  } catch(e) {
575
  document.getElementById('bcontent').innerHTML =
576
- `<p style="color:var(--danger)">Error: ${e.message}</p>
577
- <button class="btn btn-s btn-sm" style="margin-top:10px" onclick="runBench()">Retry</button>`;
578
  }
579
  btn.disabled = false;
580
  }
581
-
582
  // ─────────────────────────────────────────
583
  // Poll server metrics every 8s
584
  // ─────────────────────────────────────────
@@ -596,4 +629,4 @@ pollMetrics();
596
  setInterval(pollMetrics, 8000);
597
  </script>
598
  </body>
599
- </html>
 
4
  <meta charset="UTF-8"/>
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
  <title>KVInfer Studio</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet">
9
  <style>
10
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
11
  :root {
12
+ --bg0: #0f0e0c; --bg1: #1a1916; --bg2: #252420; --bg3: #302e28;
13
+ --amber: #e8a030; --amber2: #f5c060;
14
+ --green: #5dbd7a; --red: #d95f52; --blue: #5b8dee;
15
+ --text0: #ffffff; --text1: #e8e4d8; --text2: #c8c4b4; --text3: #8a8478;
16
+ --radius: 6px; --sidebar: 290px;
17
+ --mono: 'Space Mono', monospace;
18
+ --sans: 'Syne', sans-serif;
19
  }
20
+ html, body { height: 100%; background: var(--bg0); color: var(--text0); font-family: var(--mono); font-size: 13px; }
21
+ .app { display: flex; height: 100vh; overflow: hidden; }
22
 
23
  /* ── Sidebar ── */
24
+ .sidebar { width: var(--sidebar); min-width: var(--sidebar); background: var(--bg1);
25
+ border-right: 1px solid var(--bg3); display: flex; flex-direction: column; overflow: hidden; }
26
+
27
+ .sb-head { padding: 16px 18px 14px; border-bottom: 1px solid var(--bg3); background: var(--bg0); }
28
+ .sb-head h2 { font-family: var(--sans); font-size: 20px; font-weight: 800;
29
+ letter-spacing: -0.03em; color: var(--text0); }
30
+ .sb-head h2 span { color: var(--amber); }
31
+ .sb-head p { font-size: 10px; color: var(--text2); margin-top: 3px;
32
+ letter-spacing: 0.08em; text-transform: uppercase; }
33
+
34
+ .sb-body { flex: 1; overflow-y: auto; padding: 10px 12px;
35
+ scrollbar-width: thin; scrollbar-color: var(--bg3) transparent; }
36
 
37
  /* Cards */
38
+ .card { background: var(--bg2); border: 1px solid var(--bg3); border-radius: var(--radius);
39
+ padding: 11px 13px; margin-bottom: 8px; }
40
+ .card-title { font-size: 9px; font-weight: 700; color: var(--text2);
41
+ text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 9px;
42
+ display: flex; align-items: center; gap: 6px; }
43
 
44
  /* Stats */
45
+ .srow { display: flex; justify-content: space-between; align-items: center; padding: 3.5px 0; }
46
+ .slabel { color: var(--text2); font-size: 11px; }
47
+ .sval { font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text0); }
48
+ .green { color: var(--green); }
49
+ .blue { color: var(--blue); }
50
+ .yellow { color: var(--amber); }
51
+ .red { color: var(--red); }
52
 
53
  /* Live dot */
54
+ .dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
55
+ .dot.idle { background: var(--text3); }
56
+ .dot.live { background: var(--green); animation: pulse 1.4s infinite; }
 
57
  @keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.3;transform:scale(.7)} }
58
 
59
  /* Sparkline */
60
+ canvas#spark { width: 100%; height: 48px; display: block; border-radius: var(--radius);
61
+ background: var(--bg0); }
62
 
63
  /* System prompt */
64
+ .sysprompt { width: 100%; background: var(--bg0); border: 1px solid var(--bg3);
65
+ border-radius: var(--radius); color: var(--text0); font-size: 11px; padding: 8px 10px;
66
+ resize: vertical; min-height: 56px; font-family: var(--mono); line-height: 1.6; }
67
+ .sysprompt:focus { outline: none; border-color: var(--amber); }
68
 
69
  /* Sliders */
70
+ .pgroup { display: flex; flex-direction: column; gap: 4px; }
71
+ .pgroup label { font-size: 9px; color: var(--text2); text-transform: uppercase;
72
+ letter-spacing: 0.08em; display: flex; justify-content: space-between; }
73
+ .pgroup label span { color: var(--amber); font-weight: 700; }
74
+ .pgroup input[type=range] {
75
+ width: 100%; -webkit-appearance: none; height: 2px;
76
+ background: var(--bg3); border-radius: 1px; outline: none;
77
+ }
78
+ .pgroup input[type=range]::-webkit-slider-thumb {
79
+ -webkit-appearance: none; width: 11px; height: 11px; border-radius: 50%;
80
+ background: var(--amber); cursor: pointer; border: 2px solid var(--bg1);
81
+ }
82
 
83
  /* Buttons */
84
+ .btn { display: inline-flex; align-items: center; gap: 5px; padding: 6px 12px;
85
+ border-radius: var(--radius); border: 1px solid var(--bg3); cursor: pointer;
86
+ font-size: 10px; font-weight: 700; font-family: var(--mono);
87
+ text-transform: uppercase; letter-spacing: 0.05em; transition: all .13s; }
88
+ .btn-p { background: var(--amber); color: var(--bg0); border-color: var(--amber); }
89
+ .btn-p:hover { background: var(--amber2); border-color: var(--amber2); }
90
+ .btn-s { background: var(--bg3); color: var(--text1); }
91
+ .btn-s:hover { background: #3a3830; color: var(--text0); border-color: var(--text3); }
92
+ .btn-sm { padding: 5px 9px; font-size: 10px; }
93
+ .btn-full { width: 100%; justify-content: center; }
94
+ .btn:disabled { opacity: .35; cursor: not-allowed; }
95
 
96
  /* ── Chat Main ── */
97
+ .chat { flex: 1; display: flex; flex-direction: column; overflow: hidden; background: var(--bg0); }
98
+ .chat-hdr { height: 52px; display: flex; align-items: center; padding: 0 22px;
99
+ border-bottom: 1px solid var(--bg3); background: var(--bg1); gap: 10px; }
100
+ .badge { background: rgba(232,160,48,0.12); border: 1px solid rgba(232,160,48,0.3);
101
+ border-radius: var(--radius); padding: 3px 10px;
102
+ font-size: 11px; font-weight: 700; color: var(--amber); letter-spacing: 0.05em;
103
+ font-family: var(--sans); }
104
+ .chat-hdr h1 { font-family: var(--sans); font-size: 14px; font-weight: 600; color: var(--text0); }
105
+ .chat-hdr .spc { flex: 1; }
106
+ .statpill { display: flex; align-items: center; gap: 6px;
107
+ font-size: 10px; color: var(--text2); letter-spacing: 0.06em; text-transform: uppercase; }
108
 
109
  /* Messages */
110
+ .msgs { flex: 1; overflow-y: auto; padding: 0;
111
+ display: flex; flex-direction: column; scroll-behavior: smooth;
112
+ scrollbar-width: thin; scrollbar-color: var(--bg3) transparent; }
113
+
114
+ .mg { display: flex; gap: 0; padding: 16px 24px;
115
+ border-bottom: 1px solid var(--bg3); animation: fadeup 0.18s ease; }
116
+ .mg:last-child { border-bottom: none; }
117
+ @keyframes fadeup { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
118
+
119
+ .mg-role { width: 64px; flex-shrink: 0; padding-top: 1px; }
120
+ .mlabel { font-size: 9px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; }
121
+ .mg.user .mlabel { color: var(--amber); }
122
+ .mg.asst .mlabel { color: var(--green); }
123
+ .mg-body { flex: 1; min-width: 0; }
124
+
125
+ .bubble { font-family: var(--mono); font-size: 13px; line-height: 1.75;
126
+ color: var(--text1); white-space: pre-wrap; word-break: break-word; max-width: 680px; }
127
+ .mg.user .bubble { color: var(--text2); }
128
+
129
+ .cursor2 { display: inline-block; width: 8px; height: 2px;
130
+ background: var(--amber); margin-left: 3px;
131
+ vertical-align: middle; animation: blink .6s infinite; }
132
  @keyframes blink { 0%,49%{opacity:1} 50%,100%{opacity:0} }
133
+
134
+ .bmeta { font-size: 10px; color: var(--text3); margin-top: 6px; display: flex; gap: 12px; }
135
+ .bmeta b { color: var(--amber); }
136
 
137
  /* Welcome */
138
+ .welcome { flex: 1; display: flex; flex-direction: column;
139
+ align-items: center; justify-content: center; gap: 14px;
140
+ text-align: center; padding: 40px; animation: fadeup 0.4s ease; }
141
+ .welcome .logo { font-family: var(--sans); font-size: 80px; font-weight: 800;
142
+ color: var(--amber); letter-spacing: -0.06em; line-height: 1;
143
+ text-shadow: 0 0 80px rgba(232,160,48,0.2); }
144
+ .welcome h2 { font-family: var(--sans); font-size: 26px; font-weight: 700;
145
+ color: var(--text0); letter-spacing: -0.02em; }
146
+ .welcome p { max-width: 420px; line-height: 1.8; font-size: 13px; color: var(--text2); }
147
+ .spec-chips { display: flex; gap: 7px; flex-wrap: wrap; justify-content: center; margin-top: 4px; }
148
+ .chip { font-size: 10px; padding: 4px 12px; border: 1px solid var(--bg3);
149
+ border-radius: 20px; color: var(--text2); letter-spacing: 0.07em;
150
+ text-transform: uppercase; background: var(--bg2); }
151
 
152
  /* Input */
153
+ .inputbar { padding: 14px 20px; border-top: 1px solid var(--bg3); background: var(--bg1); }
154
+ .inputwrap { background: var(--bg2); border: 1px solid var(--bg3);
155
+ border-radius: var(--radius); display: flex; align-items: flex-end;
156
+ padding: 3px 3px 3px 14px; gap: 5px; transition: border-color .13s; }
157
+ .inputwrap:focus-within { border-color: var(--amber); }
158
+ #inp { flex: 1; background: none; border: none; outline: none;
159
+ color: var(--text0); font-size: 13px; font-family: var(--mono);
160
+ resize: none; line-height: 1.6; max-height: 120px; padding: 8px 0; }
161
+ #inp::placeholder { color: var(--text3); }
162
+ .sbtn { background: var(--amber); border: none; cursor: pointer;
163
+ width: 34px; height: 34px; border-radius: var(--radius); display: flex;
164
+ align-items: center; justify-content: center; flex-shrink: 0;
165
+ transition: .13s; align-self: flex-end; margin-bottom: 3px; }
166
+ .sbtn:hover { background: var(--amber2); }
167
+ .sbtn:disabled { opacity: .3; cursor: not-allowed; }
168
+ .sbtn svg { fill: var(--bg0); }
169
+ .input-hint { margin-top: 6px; font-size: 10px; color: var(--text3);
170
+ text-align: right; letter-spacing: 0.04em; }
171
 
172
  /* Benchmark modal */
173
+ #bov { display: none; position: fixed; inset: 0; background: rgba(10,9,8,.85);
174
+ z-index: 100; align-items: center; justify-content: center; }
175
+ #bov.on { display: flex; }
176
+ .bmod { background: var(--bg1); border: 1px solid var(--bg3);
177
+ border-radius: 8px; padding: 24px; width: 520px; max-height: 80vh; overflow-y: auto;
178
+ animation: fadeup 0.18s ease; }
179
+ .bmod h3 { font-family: var(--sans); font-size: 16px; font-weight: 700;
180
+ margin-bottom: 16px; color: var(--text0); }
181
+ .btbl { width: 100%; border-collapse: collapse; font-size: 11px; font-family: var(--mono); }
182
+ .btbl th, .btbl td { padding: 7px 10px; text-align: left; border-bottom: 1px solid var(--bg3); }
183
+ .btbl th { color: var(--text3); font-weight: 400; font-size: 9px;
184
+ text-transform: uppercase; letter-spacing: 0.1em; }
185
+ .btbl td { color: var(--text2); }
186
+ .btbl td.good { color: var(--green); }
187
+ .btbl td.mid { color: var(--amber); }
188
+ .btbl td.bad { color: var(--red); }
189
+ .bench-summary { display: grid; grid-template-columns: 1fr 1fr; gap: 1px;
190
+ background: var(--bg3); border: 1px solid var(--bg3); border-radius: var(--radius);
191
+ overflow: hidden; margin-bottom: 16px; }
192
+ .bench-stat { background: var(--bg2); padding: 14px 16px; text-align: center; }
193
+ .bench-stat .bval { font-family: var(--sans); font-size: 26px; font-weight: 800;
194
+ color: var(--amber); letter-spacing: -0.03em; }
195
+ .bench-stat .blbl { font-size: 9px; color: var(--text3);
196
+ text-transform: uppercase; letter-spacing: 0.1em; margin-top: 2px; }
197
  </style>
198
  </head>
199
  <body>
 
202
  <!-- ── Sidebar ── -->
203
  <aside class="sidebar">
204
  <div class="sb-head">
205
+ <h2><span>KV</span>Infer</h2>
206
  <p>152M Β· GPT-2 Β· AVX2 + OpenMP Β· KV-Cache</p>
207
  </div>
208
  <div class="sb-body">
 
226
  <div class="card">
227
  <div class="card-title">Throughput History (tok/s)</div>
228
  <canvas id="spark"></canvas>
229
+ <div style="margin-top:7px">
230
  <div class="srow"><span class="slabel">Session avg</span>
231
  <span class="sval green" id="s-avg">β€”</span></div>
232
  <div class="srow"><span class="slabel">Session peak</span>
 
245
  <span class="sval blue" id="s-engcache">β€”</span></div>
246
  <div class="srow"><span class="slabel">Server RAM</span>
247
  <span class="sval" id="s-ram">β€”</span></div>
248
+ <div style="display:flex;gap:6px;margin-top:10px">
249
+ <button class="btn btn-s btn-sm btn-full" onclick="clearChat()">β†Ί Clear</button>
250
+ <button class="btn btn-p btn-sm btn-full" onclick="openBench()">⊞ Benchmark</button>
251
  </div>
252
  </div>
253
 
 
261
  <!-- Params -->
262
  <div class="card">
263
  <div class="card-title">Generation</div>
264
+ <div style="display:flex;flex-direction:column;gap:10px;margin-top:2px">
265
  <div class="pgroup">
266
  <label>Temperature <span id="v-temp">0.70</span></label>
267
  <input type="range" id="p-temp" min="0.1" max="2.0" step="0.05" value="0.7"
 
297
 
298
  <div class="msgs" id="msgs">
299
  <div class="welcome" id="welcome">
300
+ <div class="logo">KV</div>
301
  <h2>KVInfer Studio</h2>
302
  <p>152M Β· GPT-2 Decoder-Only Β· Custom C++ inference engine with AVX2 SIMD, OpenMP parallelism &amp; persistent session KV-cache.</p>
303
+ <div class="spec-chips">
304
+ <span class="chip">152M params</span>
305
+ <span class="chip">AVX2 SIMD</span>
306
+ <span class="chip">OpenMP</span>
307
+ <span class="chip">KV Cache</span>
308
+ <span class="chip">Streaming</span>
309
+ </div>
310
  </div>
311
  </div>
312
 
313
  <div class="inputbar">
314
  <div class="inputwrap">
315
+ <textarea id="inp" rows="1" placeholder="Send a message..."
316
  onkeydown="handleKey(event)"></textarea>
317
  <button class="sbtn" id="sbtn" onclick="send()">
318
  <svg width="15" height="15" viewBox="0 0 24 24"><path d="M2 21l21-9L2 3v7l15 2-15 2v7z"/></svg>
319
  </button>
320
  </div>
321
+ <div class="input-hint">Enter to send &nbsp;Β·&nbsp; Shift+Enter for newline</div>
322
  </div>
323
  </main>
324
 
 
327
  <!-- ── Benchmark modal ── -->
328
  <div id="bov">
329
  <div class="bmod">
330
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
331
  <h3>πŸ“Š Quick Benchmark</h3>
332
  <button class="btn btn-s btn-sm" onclick="closeBench()">βœ•</button>
333
  </div>
334
  <div id="bcontent">
335
+ <p style="color:var(--text2);font-size:12px;line-height:1.7;margin-bottom:14px">Runs 5 built-in prompts and measures throughput, TTFT, and per-token latency.</p>
336
+ <button class="btn btn-p btn-full" style="margin-top:4px" id="btnbench" onclick="runBench()">β–Ά Run Benchmark</button>
337
  </div>
338
  </div>
339
  </div>
 
350
  let tpsHist = [];
351
  let peakTps = 0;
352
  let engCache = 0;
 
353
  // ─────────────────────────────────────────
354
  // Textarea auto-resize
355
  // ─────────────────────────────────────────
 
361
  function handleKey(e) {
362
  if (e.key==='Enter' && !e.shiftKey) { e.preventDefault(); send(); }
363
  }
 
364
  // ─────────────────────────────────────────
365
  // UI helpers
366
  // ─────────────────────────────────────────
 
371
  const d = document.getElementById(id);
372
  d.className = 'dot' + (v ? ' live' : ' idle');
373
  });
374
+ document.getElementById('hstatus').textContent = v ? 'Generating...' : 'Idle';
375
  }
 
376
  function scrollBot() {
377
  const el = document.getElementById('msgs');
378
  el.scrollTop = el.scrollHeight;
379
  }
 
380
  function hideWelcome() {
381
  const w = document.getElementById('welcome');
382
  if (w) w.remove();
383
  }
 
384
  function esc(s) {
385
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>');
386
  }
 
387
  function addUserMsg(text) {
388
  hideWelcome();
389
  const g = document.createElement('div');
390
  g.className = 'mg user';
391
+ g.innerHTML = `<div class="mg-role"><div class="mlabel">You</div></div><div class="mg-body"><div class="bubble">${esc(text)}</div></div>`;
392
  document.getElementById('msgs').appendChild(g);
393
  scrollBot();
394
  }
 
395
  function createAssistantSlot() {
396
  const g = document.createElement('div');
397
  g.className = 'mg asst';
398
  g.innerHTML = `
399
+ <div class="mg-role"><div class="mlabel">Model</div></div>
400
+ <div class="mg-body">
401
+ <div class="bubble" id="bubble"><span class="cursor2"></span></div>
402
+ <div class="bmeta" id="bmeta"></div>
403
+ </div>`;
404
  document.getElementById('msgs').appendChild(g);
405
  scrollBot();
406
  return document.getElementById('bubble');
407
  }
 
408
  // ─────────────────────────────────────────
409
  // FIX #5 β€” TTFT: use explicit null, not falsy check
410
  // ─────────────────────────────────────────
 
415
  inp.value = ''; inp.style.height = 'auto';
416
  addUserMsg(text);
417
  setBusy(true);
 
418
  const bubble = createAssistantSlot();
419
  let content = '';
420
  let t0 = Date.now();
421
  let firstTokT = null; // ← FIX: explicit null, not undefined
422
  let tokCount = 0;
 
423
  const payload = {
424
  message: text,
425
  session_id: sessId,
 
428
  temperature: parseFloat(document.getElementById('p-temp').value),
429
  top_k: parseInt(document.getElementById('p-topk').value),
430
  };
 
431
  try {
432
  const resp = await fetch(`${API}/chat`, {
433
  method: 'POST',
 
435
  body: JSON.stringify(payload),
436
  });
437
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
 
438
  const reader = resp.body.getReader();
439
  const decoder = new TextDecoder();
440
  let buf = '';
 
441
  while (true) {
442
  const {done, value} = await reader.read();
443
  if (done) break;
 
451
  if (raw === '[DONE]') break;
452
  let chunk;
453
  try { chunk = JSON.parse(raw); } catch { continue; }
 
454
  const now = Date.now();
 
455
  if (chunk.type === 'token') {
456
  // FIX #5 β€” correct null check
457
  if (firstTokT === null) {
 
465
  document.getElementById('s-ttft').textContent = (firstTokT - t0) + ' ms';
466
  bubble.innerHTML = esc(content) + '<span class="cursor2"></span>';
467
  scrollBot();
 
468
  } else if (chunk.type === 'done') {
469
  bubble.innerHTML = esc(content);
470
  const ttft = firstTokT !== null ? (firstTokT - t0) : 0;
471
  const tps = chunk.tps;
472
  const ms = chunk.total_ms;
 
473
  // Update meta line
474
  document.getElementById('bmeta').innerHTML =
475
  `<b>${tps}</b> tok/s Β· <b>TTFT</b> ${ttft}ms Β· ` +
476
  `<b>${tokCount}</b> tokens Β· <b>${ms.toFixed(0)}ms</b> total`;
 
477
  // Update sidebar stats
478
  document.getElementById('s-tps').textContent = tps + ' tok/s';
479
  document.getElementById('s-lat').textContent = ms.toFixed(0) + ' ms';
 
480
  tpsHist.push(tps);
481
  if (tpsHist.length > 30) tpsHist.shift();
482
  if (tps > peakTps) peakTps = tps;
483
  const avg = (tpsHist.reduce((a,b)=>a+b,0)/tpsHist.length).toFixed(1);
484
  document.getElementById('s-avg').textContent = avg + ' tok/s';
485
  document.getElementById('s-peak').textContent = peakTps.toFixed(1) + ' tok/s';
 
486
  // FIX #2 indicator β€” show how many tokens are cached in engine
487
  if (chunk.session_id) {
488
  fetch(`${API}/chat/history?session_id=${chunk.session_id}`)
 
492
  document.getElementById('s-engcache').textContent = engCache + ' tok';
493
  }).catch(()=>{});
494
  }
 
495
  turnCount++;
496
  document.getElementById('s-turns').textContent = turnCount;
497
  drawSpark();
 
498
  } else if (chunk.type === 'error') {
499
+ bubble.innerHTML = `<span style="color:var(--red)">Error: ${esc(chunk.message)}</span>`;
500
  }
501
  }
502
  }
503
  } catch (err) {
504
+ bubble.innerHTML = `<span style="color:var(--red)">Connection error: ${esc(err.message)}</span>`;
505
  } finally {
506
  const cur = bubble.querySelector('.cursor2');
507
  if (cur) cur.remove();
 
509
  scrollBot();
510
  }
511
  }
 
512
  // ─────────────────────────────────────────
513
  // Sparkline
514
  // ─────────────────────────────────────────
 
526
  const mx = Math.max(...d) * 1.15 || 1;
527
  const step = W / (d.length-1);
528
  const grad = ctx.createLinearGradient(0,0,0,H);
529
+ grad.addColorStop(0, 'rgba(232,160,48,.28)');
530
+ grad.addColorStop(1, 'rgba(232,160,48,.02)');
531
  ctx.beginPath();
532
  d.forEach((v,i) => {
533
  const x=i*step, y=H-(v/mx)*(H-4)-2;
534
  i===0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y);
535
  });
536
+ ctx.strokeStyle='#e8a030'; ctx.lineWidth=1.5; ctx.stroke();
537
  ctx.lineTo((d.length-1)*step,H); ctx.lineTo(0,H); ctx.closePath();
538
  ctx.fillStyle=grad; ctx.fill();
539
  }
 
540
  // ─────────────────────────────────────────
541
  // Clear chat
542
  // ─────────────────────────────────────────
 
550
  turnCount = 0; totalToks = 0; tpsHist = []; peakTps = 0; engCache = 0;
551
  document.getElementById('msgs').innerHTML = `
552
  <div class="welcome" id="welcome">
553
+ <div class="logo">KV</div>
554
+ <h2>KVInfer Studio</h2>
555
  <p>152M Β· GPT-2 Decoder-Only Β· C++ AVX2 + OpenMP Β· Persistent session KV-cache.</p>
556
+ <div class="spec-chips">
557
+ <span class="chip">152M params</span>
558
+ <span class="chip">AVX2 SIMD</span>
559
+ <span class="chip">OpenMP</span>
560
+ <span class="chip">KV Cache</span>
561
+ </div>
562
  </div>`;
563
  ['s-turns','s-totok'].forEach(id => document.getElementById(id).textContent = '0');
564
  ['s-tps','s-ttft','s-lat','s-avg','s-peak','s-toks','s-engcache'].forEach(
565
  id => document.getElementById(id).textContent = 'β€”');
566
  drawSpark();
567
  }
 
568
  // ─────────────────────────────────────────
569
  // Benchmark modal
570
  // ─────────────────────────────────────────
571
  function openBench() { document.getElementById('bov').classList.add('on'); }
572
  function closeBench() { document.getElementById('bov').classList.remove('on'); }
 
573
  async function runBench() {
574
  const btn = document.getElementById('btnbench');
575
+ btn.disabled = true; btn.textContent = '⏳ Running...';
576
  try {
577
  const r = await fetch(`${API}/benchmark/run`);
578
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
 
587
  <td>${x.tokens_out}</td></tr>`;
588
  }).join('');
589
  document.getElementById('bcontent').innerHTML = `
590
+ <div class="bench-summary">
591
+ <div class="bench-stat">
592
+ <div class="bval">${d.summary.avg_tps}</div>
593
+ <div class="blbl">Avg Throughput (tok/s)</div>
 
594
  </div>
595
+ <div class="bench-stat">
596
+ <div class="bval">${d.summary.avg_ttft_ms}</div>
597
+ <div class="blbl">Avg TTFT (ms)</div>
 
598
  </div>
599
  </div>
600
  <table class="btbl">
601
  <thead><tr><th>Prompt</th><th>tok/s</th><th>TTFT</th><th>Total</th><th>Toks</th></tr></thead>
602
  <tbody>${rows}</tbody>
603
  </table>
604
+ <div style="margin-top:14px;display:flex;gap:8px">
605
  <button class="btn btn-p btn-sm" onclick="runBench()">β†Ί Rerun</button>
606
  <button class="btn btn-s btn-sm" onclick="closeBench()">Close</button>
607
  </div>`;
608
  } catch(e) {
609
  document.getElementById('bcontent').innerHTML =
610
+ `<p style="color:var(--red);margin-bottom:12px">Error: ${e.message}</p>
611
+ <button class="btn btn-s btn-sm" onclick="runBench()">Retry</button>`;
612
  }
613
  btn.disabled = false;
614
  }
 
615
  // ─────────────────────────────────────────
616
  // Poll server metrics every 8s
617
  // ─────────────────────────────────────────
 
629
  setInterval(pollMetrics, 8000);
630
  </script>
631
  </body>
632
+ </html>