KiWA001 commited on
Commit
ea74b06
·
1 Parent(s): a4eba37

Refactor UI: Upgrade Dashboard & Simplify Admin

Browse files
Files changed (2) hide show
  1. static/admin.html +3 -310
  2. static/docs.html +140 -35
static/admin.html CHANGED
@@ -206,70 +206,7 @@
206
  </div>
207
  </div>
208
 
209
- <div class="grid">
210
- <div class="card">
211
- <h2>Speed vs Reliability</h2>
212
- <canvas id="scatterChart"></canvas>
213
- </div>
214
- <div class="card">
215
- <h2>Provider Distribution</h2>
216
- <canvas id="pieChart"></canvas>
217
- </div>
218
- </div>
219
-
220
- <!-- NEW: Search & Research Block -->
221
- <div class="card" style="margin-bottom: 20px;">
222
- <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;">
223
- <h2>Web Search & Deep Research</h2>
224
- <span class="status">NEW</span>
225
- </div>
226
- <div style="display: grid; grid-template-columns: 1fr auto; gap: 10px;">
227
- <input id="search-query" type="text" placeholder="Enter search query..." style="
228
- background: var(--bg); border: 1px solid var(--border); color: white; padding: 10px;
229
- border-radius: 6px; font-family: 'Inter', sans-serif; width: 100%;">
230
-
231
- <select id="search-mode" style="
232
- background: var(--surface); border: 1px solid var(--border); color: white; padding: 10px;
233
- border-radius: 6px;">
234
- <option value="search">Simple Search</option>
235
- <option value="deep">Deep Research</option>
236
- </select>
237
-
238
- <button onclick="runSearch()" style="
239
- background: var(--accent); color: white; border: none; padding: 10px 20px;
240
- border-radius: 6px; font-weight: 600; cursor: pointer;">
241
- Go
242
- </button>
243
- </div>
244
- <div id="search-res"
245
- style="
246
- margin-top: 15px; background: var(--bg); border: 1px solid var(--border);
247
- border-radius: 6px; padding: 15px; font-family: 'JetBrains Mono', monospace;
248
- font-size: 12px; color: var(--text-muted); white-space: pre-wrap; display: none; max-height: 300px; overflow-y: auto;">
249
- </div>
250
- </div>
251
-
252
- <div class="card">
253
- <h2>Live Model Ranking (Time-Weighted)</h2>
254
- <div style="overflow-x: auto;">
255
- <table id="rankings-table">
256
- <thead>
257
- <tr>
258
- <th width="50">#</th>
259
- <th>Model ID</th>
260
- <th>Score</th>
261
- <th>Avg Time</th>
262
- <th>Success</th>
263
- <th>Failure</th>
264
- <th>Consec. Fail</th>
265
- </tr>
266
- </thead>
267
- <tbody>
268
- <!-- JS will populate -->
269
- </tbody>
270
- </table>
271
- </div>
272
- </div>
273
 
274
  <div id="error-console"
275
  style="display:none; background:#ef4444; color:white; padding:10px; margin-bottom:20px; border-radius:8px; font-family:monospace;">
@@ -292,243 +229,7 @@
292
  </script>
293
 
294
  <script>
295
-
296
-
297
- function calculateScore(s, f, timeMs, cf) {
298
- // Replicate python logic
299
- // Score = (Success - Failure * 2) - (AvgTime / 1000)
300
- // Circuit Breaker: cf >= 3 => -100000
301
-
302
- let base = s - (f * 2);
303
- let penalty = (timeMs || 0) / 1000.0;
304
- let score = base - penalty;
305
-
306
- if (cf >= 3) return score - 100000;
307
- return score;
308
- }
309
-
310
- function renderDashboard(data) {
311
- // 1. Process Data for Ranking
312
- let rows = [];
313
- let providerCounts = {};
314
- let scatterData = [];
315
-
316
- // data is dict: { "provider/model": {success, failure, avg_time_ms...} }
317
-
318
- for (let [key, val] of Object.entries(data)) {
319
- let score = calculateScore(val.success, val.failure, val.avg_time_ms, val.consecutive_failures);
320
- rows.push({
321
- id: key,
322
- ...val,
323
- score: score
324
- });
325
-
326
- // Provider count
327
- let prov = key.split('/')[0];
328
- providerCounts[prov] = (providerCounts[prov] || 0) + val.success;
329
-
330
- // Scatter data (Time vs Score)
331
- scatterData.push({
332
- x: val.avg_time_ms || 0,
333
- y: score,
334
- id: key
335
- });
336
- }
337
-
338
- // Sort by score desc
339
- rows.sort((a, b) => b.score - a.score);
340
-
341
- // 2. Render Table
342
- const tbody = document.querySelector('#rankings-table tbody');
343
- tbody.innerHTML = '';
344
-
345
- if (rows.length === 0) {
346
- tbody.innerHTML = `
347
- <tr>
348
- <td colspan="7" style="text-align:center; padding: 30px; color: var(--text-muted);">
349
- No data available.<br><br>
350
- <button onclick="testAllAI()" style="
351
- background: var(--accent); color: white; border: none; padding: 8px 16px;
352
- border-radius: 6px; font-weight: 600; cursor: pointer; font-size: 13px;">
353
- Run Initial Test
354
- </button>
355
- </td>
356
- </tr>
357
- `;
358
- } else {
359
- rows.forEach((row, index) => {
360
- const tr = document.createElement('tr');
361
- let scoreClass = row.score > 0 ? 'score-good' : 'score-bad';
362
- let timeStr = row.avg_time_ms ? Math.round(row.avg_time_ms) + 'ms' : '-';
363
-
364
- tr.innerHTML = `
365
- <td><span class="rank-badge">${index + 1}</span></td>
366
- <td><b>${row.id}</b></td>
367
- <td class="${scoreClass}">${row.score.toFixed(2)}</td>
368
- <td>${timeStr}</td>
369
- <td>${row.success}</td>
370
- <td>${row.failure}</td>
371
- <td>${row.consecutive_failures > 0 ? '<span style="color:red">⚠️ ' + row.consecutive_failures + '</span>' : '0'}</td>
372
- `;
373
- tbody.appendChild(tr);
374
- });
375
- }
376
-
377
- // 3. Render Scatter Chart
378
- updateScatterChart(scatterData);
379
-
380
- // 4. Render Pie Chart
381
- updatePieChart(providerCounts);
382
- }
383
-
384
- // --- Charts ---
385
- let scatterChart, pieChart;
386
-
387
- function updateScatterChart(data) {
388
- const ctx = document.getElementById('scatterChart').getContext('2d');
389
-
390
- if (scatterChart) {
391
- // Update existing chart without full re-render to avoid animation
392
- scatterChart.data.datasets[0].data = data;
393
- scatterChart.update('none'); // 'none' mode disables animation
394
- return;
395
- }
396
-
397
- // Filter out 0 time (data without stats)
398
- const validData = data.filter(d => d.x > 0);
399
-
400
- scatterChart = new Chart(ctx, {
401
- type: 'scatter',
402
- data: {
403
- datasets: [{
404
- label: 'Models',
405
- data: validData,
406
- backgroundColor: '#8b5cf6',
407
- borderColor: '#8b5cf6',
408
- }]
409
- },
410
- options: {
411
- responsive: true,
412
- animation: { duration: 1000 }, // Initial animation
413
- scales: {
414
- x: {
415
- type: 'linear',
416
- position: 'bottom',
417
- title: { display: true, text: 'Avg Response Time (ms)', color: '#a1a1aa' },
418
- grid: { color: '#27272a' }
419
- },
420
- y: {
421
- title: { display: true, text: 'Score', color: '#a1a1aa' },
422
- grid: { color: '#27272a' }
423
- }
424
- },
425
- plugins: {
426
- tooltip: {
427
- callbacks: {
428
- label: function (context) {
429
- return context.raw.id + ': ' + context.raw.y.toFixed(2);
430
- }
431
- }
432
- }
433
- }
434
- }
435
- });
436
- }
437
-
438
- function updatePieChart(counts) {
439
- // STOP updating if already rendered once (User Request)
440
- if (window.pieChartRendered) return;
441
-
442
- const ctx = document.getElementById('pieChart').getContext('2d');
443
-
444
- // Mark as rendered so we never touch it again until reload
445
- window.pieChartRendered = true;
446
-
447
- pieChart = new Chart(ctx, {
448
- type: 'doughnut',
449
- data: {
450
- labels: Object.keys(counts),
451
- datasets: [{
452
- data: Object.values(counts),
453
- backgroundColor: ['#8b5cf6', '#22c55e', '#f59e0b', '#ef4444'],
454
- borderWidth: 0
455
- }]
456
- },
457
- options: {
458
- responsive: true,
459
- animation: { duration: 1000 },
460
- plugins: {
461
- legend: { position: 'right', labels: { color: '#e4e4e7' } }
462
- }
463
- }
464
- });
465
- }
466
-
467
- // Initialize
468
- async function init() {
469
- await fetchStats();
470
- // Poll every 5 seconds
471
- setInterval(fetchStats, 5000);
472
- }
473
-
474
- // Make globally available for buttons
475
- window.fetchStats = async function () {
476
- try {
477
- const res = await fetch('/admin/stats');
478
- if (!res.ok) throw new Error("HTTP " + res.status);
479
- const stats = await res.json();
480
- renderDashboard(stats);
481
- } catch (e) {
482
- console.error("Failed to fetch stats", e);
483
- const tbody = document.querySelector('#rankings-table tbody');
484
- // Only show error if table is empty/loading
485
- if (!tbody.hasChildNodes() || tbody.innerHTML.trim() === '' || tbody.innerText.includes('Loading')) {
486
- tbody.innerHTML = `<tr><td colspan="7" style="color:var(--error); text-align:center; padding:20px;">Failed to fetch stats: ${e.message}</td></tr>`;
487
- }
488
- }
489
- };
490
-
491
- // internal helper
492
- const fetchStats = window.fetchStats;
493
-
494
- // --- Search Logic ---
495
- async function runSearch() {
496
- const query = document.getElementById('search-query').value;
497
- const mode = document.getElementById('search-mode').value;
498
- const resBox = document.getElementById('search-res');
499
-
500
- if (!query) { alert("Please enter a query"); return; }
501
-
502
- resBox.style.display = 'block';
503
- resBox.textContent = '⏳ Searching... (Deep Research may take 10s+)';
504
- resBox.style.color = 'var(--text-muted)';
505
- resBox.style.borderColor = 'var(--border)';
506
-
507
- const endpoint = mode === 'deep' ? '/deep_research' : '/search';
508
-
509
- try {
510
- const res = await fetch(endpoint, {
511
- method: 'POST',
512
- headers: { 'Content-Type': 'application/json' },
513
- body: JSON.stringify({ query: query })
514
- });
515
- const data = await res.json();
516
-
517
- if (res.ok) {
518
- resBox.style.borderColor = 'var(--success)';
519
- resBox.style.color = 'var(--text)';
520
- resBox.innerText = JSON.stringify(data, null, 2);
521
- } else {
522
- throw new Error(data.error || "Unknown error");
523
- }
524
- } catch (e) {
525
- resBox.style.borderColor = 'var(--error)';
526
- resBox.style.color = 'var(--error)';
527
- resBox.innerText = 'Error: ' + e.message;
528
- }
529
- }
530
-
531
- // --- New Actions ---
532
 
533
  async function testAllAI() {
534
  const btn = document.getElementById('btn-test-all');
@@ -546,10 +247,7 @@
546
 
547
  console.log("Test Results:", results);
548
 
549
- // Refresh dashboard immediately to show new stats
550
- await window.fetchStats();
551
-
552
- // Show simple alert summary? Or just let the dashboard speak for itself.
553
  let passed = results.filter(r => r.status === 'PASS').length;
554
  let total = results.length;
555
  alert(`Test Complete.\nPassed: ${passed}/${total}`);
@@ -574,7 +272,6 @@
574
 
575
  try {
576
  await fetch('/admin/clear_stats', { method: 'POST' });
577
- await window.fetchStats(); // Refresh (should be empty)
578
  alert("Stats cleared successfully.");
579
  } catch (e) {
580
  alert("Clear failed: " + e.message);
@@ -583,10 +280,6 @@
583
  btn.disabled = false;
584
  }
585
  }
586
-
587
- // Start
588
- init();
589
-
590
  </script>
591
  </body>
592
 
 
206
  </div>
207
  </div>
208
 
209
+ <!-- Removed Charts & Tables (Moved to Main Page) -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
 
211
  <div id="error-console"
212
  style="display:none; background:#ef4444; color:white; padding:10px; margin-bottom:20px; border-radius:8px; font-family:monospace;">
 
229
  </script>
230
 
231
  <script>
232
+ // --- Actions ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
  async function testAllAI() {
235
  const btn = document.getElementById('btn-test-all');
 
247
 
248
  console.log("Test Results:", results);
249
 
250
+ // Show simple alert summary
 
 
 
251
  let passed = results.filter(r => r.status === 'PASS').length;
252
  let total = results.length;
253
  alert(`Test Complete.\nPassed: ${passed}/${total}`);
 
272
 
273
  try {
274
  await fetch('/admin/clear_stats', { method: 'POST' });
 
275
  alert("Stats cleared successfully.");
276
  } catch (e) {
277
  alert("Clear failed: " + e.message);
 
280
  btn.disabled = false;
281
  }
282
  }
 
 
 
 
283
  </script>
284
  </body>
285
 
static/docs.html CHANGED
@@ -10,6 +10,8 @@
10
  <link
11
  href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap"
12
  rel="stylesheet">
 
 
13
  <style>
14
  :root {
15
  --bg-primary: #0a0a0f;
@@ -355,6 +357,12 @@
355
  color: var(--error);
356
  }
357
 
 
 
 
 
 
 
358
  /* ─── Footer ─── */
359
  footer {
360
  text-align: center;
@@ -526,7 +534,7 @@
526
  <div class="endpoint-block">
527
  <div class="endpoint-header">
528
  <div class="endpoint-title">
529
- <h3>🏆 Live Model Ranking</h3>
530
  <p>Real-time performance tracked by the engine (Speed & Reliability).</p>
531
  </div>
532
  <span class="method-badge LIVE">LIVE UPDATES</span>
@@ -538,7 +546,7 @@
538
  <tr>
539
  <th width="50">#</th>
540
  <th>Model ID</th>
541
- <th>Score (Speed Weighted)</th>
542
  <th>Avg Time</th>
543
  <th>Success</th>
544
  <th>Fail</th>
@@ -555,11 +563,36 @@
555
  </div>
556
  </div>
557
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
  </div>
559
 
560
  <footer>K-AI API — Feel free to AI</footer>
561
 
562
  <script>
 
 
 
563
  async function runDemo(type) {
564
  const resBox = document.getElementById(type + '-res');
565
  resBox.className = 'demo-response visible';
@@ -627,10 +660,10 @@
627
  // --- Live Ranking Logic ---
628
 
629
  let availableModelsSet = new Set();
 
630
 
631
  async function fetchRankingStats() {
632
  try {
633
- // Fetch stats AND available models in parallel
634
  const [statsRes, modelsRes] = await Promise.all([
635
  fetch('/admin/stats'),
636
  fetch('/models')
@@ -639,21 +672,19 @@
639
  if (!statsRes.ok) return;
640
  const stats = await statsRes.json();
641
 
642
- // Process available models
643
  availableModelsSet.clear();
644
  if (modelsRes.ok) {
645
  const models = await modelsRes.json();
646
  models.forEach(m => availableModelsSet.add(`${m.provider}/${m.model}`));
647
  }
648
 
649
- renderRankingTable(stats);
650
  } catch (e) {
651
  console.error("Failed to fetch ranking", e);
652
  }
653
  }
654
 
655
  function calculateScore(s, f, timeMs, cf) {
656
- // Score = (Success - Failure * 2) - (AvgTime / 1000)
657
  let base = s - (f * 2);
658
  let penalty = (timeMs || 0) / 1000.0;
659
  let score = base - penalty;
@@ -661,56 +692,130 @@
661
  return score;
662
  }
663
 
664
- function renderRankingTable(data) {
665
  let rows = [];
 
 
 
666
  for (let [key, val] of Object.entries(data)) {
667
  let score = calculateScore(val.success, val.failure, val.avg_time_ms, val.consecutive_failures);
668
  rows.push({ id: key, ...val, score: score });
 
 
 
 
 
 
 
 
 
 
 
669
  }
670
- // Sort by score desc
671
  rows.sort((a, b) => b.score - a.score);
672
 
 
673
  const tbody = document.querySelector('#rankings-table tbody');
674
  tbody.innerHTML = '';
675
 
676
  if (rows.length === 0) {
677
  tbody.innerHTML = '<tr><td colspan="7" style="text-align:center; padding: 20px;">No stats available yet. Make a request!</td></tr>';
678
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
679
  }
680
 
681
- rows.forEach((row, index) => {
682
- const tr = document.createElement('tr');
683
- let scoreClass = row.score > 0 ? 'score-good' : 'score-bad';
684
- let timeStr = row.avg_time_ms ? Math.round(row.avg_time_ms) + 'ms' : '-';
685
-
686
- // Determine Status
687
- let status = '';
688
- // Check if this specific provider/model combo is actually available on THIS server
689
- if (!availableModelsSet.has(row.id)) {
690
- status = '<span style="color:#9ca3af; font-size:11px; border:1px solid #374151; padding:2px 6px; border-radius:4px;">OFFLINE (Local Only)</span>';
691
- tr.style.opacity = '0.6';
692
- } else if (row.consecutive_failures >= 3) {
693
- status = '<span style="color:#ef4444">⚠️ CRITICAL</span>';
694
- } else {
695
- status = '<span style="color:#22c55e">● Active</span>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
  }
 
 
697
 
698
- tr.innerHTML = `
699
- <td><span class="rank-badge">${index + 1}</span></td>
700
- <td><b>${row.id}</b></td>
701
- <td class="${scoreClass}">${row.score.toFixed(2)}</td>
702
- <td>${timeStr}</td>
703
- <td>${row.success}</td>
704
- <td>${row.failure}</td>
705
- <td>${status}</td>
706
- `;
707
- tbody.appendChild(tr);
 
 
 
 
 
 
 
 
 
 
 
 
708
  });
709
  }
710
 
711
  // Init Live Ranking
712
  fetchRankingStats();
713
- setInterval(fetchRankingStats, 5000); // Poll every 5s
714
 
715
  </script>
716
 
 
10
  <link
11
  href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap"
12
  rel="stylesheet">
13
+ <!-- Chart.js -->
14
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
15
  <style>
16
  :root {
17
  --bg-primary: #0a0a0f;
 
357
  color: var(--error);
358
  }
359
 
360
+ /* Charts */
361
+ canvas {
362
+ max-height: 250px;
363
+ width: 100%;
364
+ }
365
+
366
  /* ─── Footer ─── */
367
  footer {
368
  text-align: center;
 
534
  <div class="endpoint-block">
535
  <div class="endpoint-header">
536
  <div class="endpoint-title">
537
+ <h3>🏆 Live Model Ranking (Time-Weighted)</h3>
538
  <p>Real-time performance tracked by the engine (Speed & Reliability).</p>
539
  </div>
540
  <span class="method-badge LIVE">LIVE UPDATES</span>
 
546
  <tr>
547
  <th width="50">#</th>
548
  <th>Model ID</th>
549
+ <th>Score</th>
550
  <th>Avg Time</th>
551
  <th>Success</th>
552
  <th>Fail</th>
 
563
  </div>
564
  </div>
565
 
566
+ <!-- Network Analytics (Graphs) -->
567
+ <div class="endpoint-block">
568
+ <div class="endpoint-header">
569
+ <div class="endpoint-title">
570
+ <h3>Network Analytics</h3>
571
+ <p>Live Latency vs. Reliability & Provider Distribution.</p>
572
+ </div>
573
+ </div>
574
+ <div class="endpoint-body" style="grid-template-columns: 1fr 1fr; gap: 20px; padding: 20px;">
575
+ <div style="background: var(--bg-secondary); padding: 15px; border-radius: 8px;">
576
+ <h4 style="margin-bottom:10px; color:var(--text-secondary); font-size:12px; font-weight:700;">SPEED
577
+ vs RELIABILITY</h4>
578
+ <canvas id="scatterChart"></canvas>
579
+ </div>
580
+ <div style="background: var(--bg-secondary); padding: 15px; border-radius: 8px;">
581
+ <h4 style="margin-bottom:10px; color:var(--text-secondary); font-size:12px; font-weight:700;">
582
+ PROVIDER DISTRIBUTION</h4>
583
+ <canvas id="pieChart"></canvas>
584
+ </div>
585
+ </div>
586
+ </div>
587
+
588
  </div>
589
 
590
  <footer>K-AI API — Feel free to AI</footer>
591
 
592
  <script>
593
+ // Global flag for Pie Chart
594
+ window.pieChartRendered = false;
595
+
596
  async function runDemo(type) {
597
  const resBox = document.getElementById(type + '-res');
598
  resBox.className = 'demo-response visible';
 
660
  // --- Live Ranking Logic ---
661
 
662
  let availableModelsSet = new Set();
663
+ let scatterChart, pieChart;
664
 
665
  async function fetchRankingStats() {
666
  try {
 
667
  const [statsRes, modelsRes] = await Promise.all([
668
  fetch('/admin/stats'),
669
  fetch('/models')
 
672
  if (!statsRes.ok) return;
673
  const stats = await statsRes.json();
674
 
 
675
  availableModelsSet.clear();
676
  if (modelsRes.ok) {
677
  const models = await modelsRes.json();
678
  models.forEach(m => availableModelsSet.add(`${m.provider}/${m.model}`));
679
  }
680
 
681
+ renderDashboard(stats);
682
  } catch (e) {
683
  console.error("Failed to fetch ranking", e);
684
  }
685
  }
686
 
687
  function calculateScore(s, f, timeMs, cf) {
 
688
  let base = s - (f * 2);
689
  let penalty = (timeMs || 0) / 1000.0;
690
  let score = base - penalty;
 
692
  return score;
693
  }
694
 
695
+ function renderDashboard(data) {
696
  let rows = [];
697
+ let providerCounts = {};
698
+ let scatterData = [];
699
+
700
  for (let [key, val] of Object.entries(data)) {
701
  let score = calculateScore(val.success, val.failure, val.avg_time_ms, val.consecutive_failures);
702
  rows.push({ id: key, ...val, score: score });
703
+
704
+ // Provider stats for Pie
705
+ let prov = key.split('/')[0];
706
+ providerCounts[prov] = (providerCounts[prov] || 0) + val.success;
707
+
708
+ // Scatter Data
709
+ scatterData.push({
710
+ x: val.avg_time_ms || 0,
711
+ y: score,
712
+ id: key
713
+ });
714
  }
 
715
  rows.sort((a, b) => b.score - a.score);
716
 
717
+ // Render Table
718
  const tbody = document.querySelector('#rankings-table tbody');
719
  tbody.innerHTML = '';
720
 
721
  if (rows.length === 0) {
722
  tbody.innerHTML = '<tr><td colspan="7" style="text-align:center; padding: 20px;">No stats available yet. Make a request!</td></tr>';
723
+ } else {
724
+ rows.forEach((row, index) => {
725
+ const tr = document.createElement('tr');
726
+ let scoreClass = row.score > 0 ? 'score-good' : 'score-bad';
727
+ let timeStr = row.avg_time_ms ? Math.round(row.avg_time_ms) + 'ms' : '-';
728
+
729
+ let status = '';
730
+ if (!availableModelsSet.has(row.id)) {
731
+ status = '<span style="color:#9ca3af; font-size:11px; border:1px solid #374151; padding:2px 6px; border-radius:4px;">OFFLINE (Local Only)</span>';
732
+ tr.style.opacity = '0.6';
733
+ } else if (row.consecutive_failures >= 3) {
734
+ status = '<span style="color:#ef4444">⚠️ CRITICAL</span>';
735
+ } else {
736
+ status = '<span style="color:#22c55e">● Active</span>';
737
+ }
738
+
739
+ tr.innerHTML = `
740
+ <td><span class="rank-badge">${index + 1}</span></td>
741
+ <td><b>${row.id}</b></td>
742
+ <td class="${scoreClass}">${row.score.toFixed(2)}</td>
743
+ <td>${timeStr}</td>
744
+ <td>${row.success}</td>
745
+ <td>${row.failure}</td>
746
+ <td>${status}</td>
747
+ `;
748
+ tbody.appendChild(tr);
749
+ });
750
  }
751
 
752
+ // Render Charts
753
+ updateScatterChart(scatterData);
754
+ updatePieChart(providerCounts);
755
+ }
756
+
757
+ function updateScatterChart(data) {
758
+ const ctx = document.getElementById('scatterChart').getContext('2d');
759
+ if (scatterChart) {
760
+ scatterChart.data.datasets[0].data = data;
761
+ scatterChart.update('none');
762
+ return;
763
+ }
764
+ const validData = data.filter(d => d.x > 0);
765
+ scatterChart = new Chart(ctx, {
766
+ type: 'scatter',
767
+ data: {
768
+ datasets: [{
769
+ label: 'Models',
770
+ data: validData,
771
+ backgroundColor: '#8b5cf6',
772
+ borderColor: '#8b5cf6',
773
+ }]
774
+ },
775
+ options: {
776
+ responsive: true,
777
+ maintainAspectRatio: false,
778
+ animation: { duration: 1000 },
779
+ scales: {
780
+ x: { type: 'linear', position: 'bottom', title: { display: true, text: 'Time (ms)', color: '#6b6b80' }, grid: { color: '#2a2a3a' } },
781
+ y: { title: { display: true, text: 'Score', color: '#6b6b80' }, grid: { color: '#2a2a3a' } }
782
+ },
783
+ plugins: {
784
+ legend: { display: false },
785
+ tooltip: { callbacks: { label: (ctx) => ctx.raw.id + ': ' + ctx.raw.y.toFixed(2) } }
786
+ }
787
  }
788
+ });
789
+ }
790
 
791
+ function updatePieChart(counts) {
792
+ if (window.pieChartRendered) return;
793
+ const ctx = document.getElementById('pieChart').getContext('2d');
794
+ window.pieChartRendered = true;
795
+ pieChart = new Chart(ctx, {
796
+ type: 'doughnut',
797
+ data: {
798
+ labels: Object.keys(counts),
799
+ datasets: [{
800
+ data: Object.values(counts),
801
+ backgroundColor: ['#8b5cf6', '#22c55e', '#f59e0b', '#ef4444', '#ec4899', '#3b82f6'],
802
+ borderWidth: 0
803
+ }]
804
+ },
805
+ options: {
806
+ responsive: true,
807
+ maintainAspectRatio: false,
808
+ animation: { duration: 1000 },
809
+ plugins: {
810
+ legend: { position: 'right', labels: { color: '#9898aa', font: { size: 10 } } }
811
+ }
812
+ }
813
  });
814
  }
815
 
816
  // Init Live Ranking
817
  fetchRankingStats();
818
+ setInterval(fetchRankingStats, 5000);
819
 
820
  </script>
821