KiWA001 commited on
Commit
92812fd
·
1 Parent(s): 6d752be

feat: obscure admin routes (/qaz), split public/private docs, and refine dashboard UI

Browse files
admin_router.py CHANGED
@@ -6,7 +6,7 @@ import uuid
6
 
7
  from db import get_supabase
8
 
9
- router = APIRouter(prefix="/admin", tags=["Admin"])
10
 
11
  # --- Models ---
12
 
 
6
 
7
  from db import get_supabase
8
 
9
+ router = APIRouter(prefix="/qaz", tags=["Admin"])
10
 
11
  # --- Models ---
12
 
main.py CHANGED
@@ -116,30 +116,30 @@ app.include_router(admin_router)
116
  @app.get("/qazmlp", include_in_schema=False)
117
  async def admin_page():
118
  """Serve the Secret Admin Dashboard."""
119
- return FileResponse("static/admin.html")
120
 
121
 
122
- @app.get("/admin/stats", include_in_schema=False)
123
  async def admin_stats():
124
  """Return raw model stats for dashboard."""
125
  return JSONResponse(engine.get_stats())
126
 
127
 
128
- @app.post("/admin/test_all", include_in_schema=False)
129
  async def admin_test_all():
130
  """Trigger parallel testing of all models."""
131
  results = await engine.test_all_models()
132
  return JSONResponse(results)
133
 
134
 
135
- @app.post("/admin/clear_stats", include_in_schema=False)
136
  async def admin_clear_stats():
137
  """Clear all stats."""
138
  engine.clear_stats()
139
  return JSONResponse({"status": "cleared"})
140
 
141
 
142
- @app.get("/admin/debug_g4f", include_in_schema=False)
143
  async def admin_debug_g4f():
144
  """
145
  Verbose Debug for G4F Provider on Server.
@@ -203,18 +203,60 @@ async def admin_debug_g4f():
203
  return HTMLResponse(f"<pre>{output}</pre>")
204
 
205
 
 
206
  # ---------- Custom Swagger UI ----------
207
  @app.get("/docs", include_in_schema=False)
208
- async def custom_swagger_ui_html():
209
- """Serve custom dark-themed Swagger UI."""
210
  return get_swagger_ui_html(
211
- openapi_url=app.openapi_url,
212
- title=f"{API_TITLE} - Swagger UI",
213
  swagger_css_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css",
214
  swagger_js_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js",
215
  swagger_favicon_url="https://fastapi.tiangolo.com/img/favicon.png",
216
  )
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
  # ---------- Search Routes ----------
220
  @app.post("/search")
@@ -274,10 +316,7 @@ async def deep_research_endpoint(request: Request):
274
  # ---------- Routes ----------
275
 
276
 
277
- @app.get("/qazmlpdocs", include_in_schema=False)
278
- async def qazmlp_docs():
279
- """Serve the Secured Dashboard."""
280
- return FileResponse("static/qazmlpdocs.html")
281
 
282
  @app.get("/docs/public", include_in_schema=False)
283
  async def public_docs_page():
 
116
  @app.get("/qazmlp", include_in_schema=False)
117
  async def admin_page():
118
  """Serve the Secret Admin Dashboard."""
119
+ return FileResponse("static/qaz.html")
120
 
121
 
122
+ @app.get("/qaz/stats", include_in_schema=False)
123
  async def admin_stats():
124
  """Return raw model stats for dashboard."""
125
  return JSONResponse(engine.get_stats())
126
 
127
 
128
+ @app.post("/qaz/test_all", include_in_schema=False)
129
  async def admin_test_all():
130
  """Trigger parallel testing of all models."""
131
  results = await engine.test_all_models()
132
  return JSONResponse(results)
133
 
134
 
135
+ @app.post("/qaz/clear_stats", include_in_schema=False)
136
  async def admin_clear_stats():
137
  """Clear all stats."""
138
  engine.clear_stats()
139
  return JSONResponse({"status": "cleared"})
140
 
141
 
142
+ @app.get("/qaz/debug_g4f", include_in_schema=False)
143
  async def admin_debug_g4f():
144
  """
145
  Verbose Debug for G4F Provider on Server.
 
203
  return HTMLResponse(f"<pre>{output}</pre>")
204
 
205
 
206
+ # ---------- Custom Swagger UI ----------
207
  # ---------- Custom Swagger UI ----------
208
  @app.get("/docs", include_in_schema=False)
209
+ async def public_swagger_ui():
210
+ """Serve Public Swagger UI (No Admin)."""
211
  return get_swagger_ui_html(
212
+ openapi_url="/openapi_public.json",
213
+ title=f"{API_TITLE} - Public Docs",
214
  swagger_css_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css",
215
  swagger_js_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js",
216
  swagger_favicon_url="https://fastapi.tiangolo.com/img/favicon.png",
217
  )
218
 
219
+ @app.get("/qazmlpdocs", include_in_schema=False)
220
+ async def admin_swagger_ui():
221
+ """Serve Admin Swagger UI (Full)."""
222
+ return get_swagger_ui_html(
223
+ openapi_url=app.openapi_url, # Default includes Admin
224
+ title=f"{API_TITLE} - Admin Docs",
225
+ swagger_css_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css",
226
+ swagger_js_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js",
227
+ swagger_favicon_url="https://fastapi.tiangolo.com/img/favicon.png",
228
+ )
229
+
230
+ @app.get("/openapi_public.json", include_in_schema=False)
231
+ async def get_public_openapi():
232
+ """Generate OpenAPI schema without Admin routes."""
233
+ if app.openapi_schema:
234
+ schema = app.openapi_schema.copy()
235
+ else:
236
+ schema = app.openapi()
237
+
238
+ # Deep copy to avoid modifying the cached schema
239
+ import copy
240
+ public_schema = copy.deepcopy(schema)
241
+
242
+ # Filter paths
243
+ paths_to_remove = []
244
+ for path, methods in public_schema.get("paths", {}).items():
245
+ # Check if any Method in this path has "Admin" tag
246
+ is_admin = False
247
+ for method, details in methods.items():
248
+ if "tags" in details and "Admin" in details["tags"]:
249
+ is_admin = True
250
+ break
251
+
252
+ if is_admin or path.startswith("/qaz") or path.startswith("/admin"):
253
+ paths_to_remove.append(path)
254
+
255
+ for p in paths_to_remove:
256
+ del public_schema["paths"][p]
257
+
258
+ return JSONResponse(public_schema)
259
+
260
 
261
  # ---------- Search Routes ----------
262
  @app.post("/search")
 
316
  # ---------- Routes ----------
317
 
318
 
319
+
 
 
 
320
 
321
  @app.get("/docs/public", include_in_schema=False)
322
  async def public_docs_page():
static/docs.html CHANGED
@@ -269,6 +269,19 @@
269
  margin-bottom: 12px;
270
  border-radius: var(--radius-sm);
271
  border: 1px solid var(--border);
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  }
273
 
274
  .demo-btn {
@@ -432,7 +445,6 @@
432
  <div class="demo-response visible" style="color: #a78bfa;">
433
  curl https://kiwa001-kai-api-gateway.hf.space/v1/chat/completions \
434
  -H "Content-Type: application/json" \
435
- -H "Authorization: Bearer sk-kai-demo-public" \
436
  -d '{"model": "gemini-3-flash", "messages": [{"role": "user", "content": "Hello!"}]}'
437
  </div>
438
  </div>
@@ -442,23 +454,13 @@
442
  placeholder='{"message": "What is AI?"}'>What is the capital of France?</textarea>
443
 
444
  <select id="chat-basic-model" class="demo-select" style="margin-top:10px;">
445
- <option value="gemini-3-flash">gemini-3-flash (Default)</option>
446
- <option value="gpt-4o">gpt-4o</option>
447
- <option value="gpt-4o-mini">gpt-4o-mini</option>
448
- <option value="glm-4">glm-4</option>
449
- <option value="mistral-large">mistral-large</option>
450
  </select>
451
 
452
  <button class="demo-btn" onclick="runDemo('chat-basic')">Run Request ▶</button>
453
  <div id="chat-basic-status" class="demo-status"></div>
454
 
455
- <select id="chat-basic-model" class="demo-select" style="margin-top:10px;">
456
- <option value="gemini-3-flash">gemini-3-flash (Default)</option>
457
- <option value="gpt-4o">gpt-4o</option>
458
- <option value="gpt-4o-mini">gpt-4o-mini</option>
459
- <option value="glm-4">glm-4</option>
460
- <option value="mistral-large">mistral-large</option>
461
- </select>
462
 
463
  <div id="chat-basic-res" class="demo-response"></div>
464
  </div>
@@ -483,6 +485,11 @@
483
  <div class="endpoint-docs">
484
  <p style="color:var(--text-secondary); font-size:14px;">Returns a JSON list of all models supported
485
  by the API, ranked by quality.</p>
 
 
 
 
 
486
  </div>
487
  <div class="endpoint-demo">
488
  <span class="demo-label">Try It Live</span>
@@ -517,6 +524,29 @@
517
  </td>
518
  </tr>
519
  </table>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
520
  </div>
521
  <div class="endpoint-demo">
522
  <span class="demo-label">Try It Live</span>
@@ -594,6 +624,75 @@
594
  // Global flag for Pie Chart
595
  window.pieChartRendered = false;
596
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
  async function runDemo(type) {
598
  const resBox = document.getElementById(type + '-res');
599
  const statusBox = document.getElementById(type + '-status');
@@ -613,8 +712,7 @@
613
  try {
614
  let url, body, method = 'POST';
615
  let headers = {
616
- 'Content-Type': 'application/json',
617
- 'Authorization': 'Bearer sk-kai-demo-public' // Use Demo Key for Dashboard
618
  };
619
 
620
  if (type === 'chat-basic') {
@@ -720,7 +818,7 @@
720
  try {
721
  const t = new Date().getTime();
722
  const [statsRes, modelsRes] = await Promise.all([
723
- fetch(`/admin/stats?t=${t}`),
724
  fetch(`/models?t=${t}`)
725
  ]);
726
 
 
269
  margin-bottom: 12px;
270
  border-radius: var(--radius-sm);
271
  border: 1px solid var(--border);
272
+ appearance: none;
273
+ /* Remove default arrow to ensure styling applies */
274
+ -webkit-appearance: none;
275
+ background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%239898aa%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
276
+ background-repeat: no-repeat;
277
+ background-position: right 12px top 50%;
278
+ background-size: 12px auto;
279
+ }
280
+
281
+ .demo-select option {
282
+ background: var(--bg-card);
283
+ color: var(--text-primary);
284
+ padding: 10px;
285
  }
286
 
287
  .demo-btn {
 
445
  <div class="demo-response visible" style="color: #a78bfa;">
446
  curl https://kiwa001-kai-api-gateway.hf.space/v1/chat/completions \
447
  -H "Content-Type: application/json" \
 
448
  -d '{"model": "gemini-3-flash", "messages": [{"role": "user", "content": "Hello!"}]}'
449
  </div>
450
  </div>
 
454
  placeholder='{"message": "What is AI?"}'>What is the capital of France?</textarea>
455
 
456
  <select id="chat-basic-model" class="demo-select" style="margin-top:10px;">
457
+ <option value="gemini-3-flash" selected>Loading models...</option>
 
 
 
 
458
  </select>
459
 
460
  <button class="demo-btn" onclick="runDemo('chat-basic')">Run Request ▶</button>
461
  <div id="chat-basic-status" class="demo-status"></div>
462
 
463
+
 
 
 
 
 
 
464
 
465
  <div id="chat-basic-res" class="demo-response"></div>
466
  </div>
 
485
  <div class="endpoint-docs">
486
  <p style="color:var(--text-secondary); font-size:14px;">Returns a JSON list of all models supported
487
  by the API, ranked by quality.</p>
488
+ <br>
489
+ <span class="demo-label">Example Request</span>
490
+ <div class="demo-response visible" style="color: #a78bfa;">
491
+ curl https://kiwa001-kai-api-gateway.hf.space/models
492
+ </div>
493
  </div>
494
  <div class="endpoint-demo">
495
  <span class="demo-label">Try It Live</span>
 
524
  </td>
525
  </tr>
526
  </table>
527
+ <br>
528
+ <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;">
529
+ <span class="demo-label" style="margin-bottom:0;">Example Request</span>
530
+ <div style="display:flex; gap:8px;">
531
+ <button onclick="toggleSearchEx('simple')" id="btn-ex-simple"
532
+ style="padding:4px 8px; border-radius:4px; border:1px solid var(--accent); background:var(--accent); color:white; font-size:11px; cursor:pointer;">Simple</button>
533
+ <button onclick="toggleSearchEx('deep')" id="btn-ex-deep"
534
+ style="padding:4px 8px; border-radius:4px; border:1px solid var(--border); background:transparent; color:var(--text-muted); font-size:11px; cursor:pointer;">Deep
535
+ Research</button>
536
+ </div>
537
+ </div>
538
+
539
+ <div id="ex-code-simple" class="demo-response visible" style="color: #a78bfa;">
540
+ curl -X POST https://kiwa001-kai-api-gateway.hf.space/search \
541
+ -H "Content-Type: application/json" \
542
+ -d '{"query": "Machine Learning", "limit": 5}'
543
+ </div>
544
+
545
+ <div id="ex-code-deep" class="demo-response" style="color: #a78bfa; display:none;">
546
+ curl -X POST https://kiwa001-kai-api-gateway.hf.space/deep_research \
547
+ -H "Content-Type: application/json" \
548
+ -d '{"query": "Future of AI", "limit": 2}'
549
+ </div>
550
  </div>
551
  <div class="endpoint-demo">
552
  <span class="demo-label">Try It Live</span>
 
624
  // Global flag for Pie Chart
625
  window.pieChartRendered = false;
626
 
627
+ // Auto-load models for dropdown
628
+ async function loadModelsDropdown() {
629
+ const select = document.getElementById('chat-basic-model');
630
+ if (!select) return;
631
+
632
+ try {
633
+ const res = await fetch('/models');
634
+ if (!res.ok) throw new Error("Failed to fetch models");
635
+
636
+ const data = await res.json();
637
+ const models = Array.isArray(data) ? data : (data.models || []);
638
+
639
+ if (models.length === 0) throw new Error("No models returned");
640
+
641
+ select.innerHTML = '';
642
+ models.forEach(m => {
643
+ const opt = document.createElement('option');
644
+ opt.value = m.id;
645
+ opt.textContent = m.id;
646
+ if (m.id === 'gemini-3-flash') opt.selected = true;
647
+ select.appendChild(opt);
648
+ });
649
+ } catch (e) {
650
+ console.error("Model load error", e);
651
+ // Fallback if empty or error
652
+ if (select.options.length <= 1) {
653
+ select.innerHTML = `
654
+ <option value="gemini-3-flash" selected>gemini-3-flash (Default)</option>
655
+ <option value="gpt-4o">gpt-4o</option>
656
+ <option value="gpt-4o-mini">gpt-4o-mini</option>
657
+ <option value="glm-4">glm-4</option>
658
+ <option value="mistral-large">mistral-large</option>
659
+ `;
660
+ }
661
+ }
662
+ }
663
+ document.addEventListener('DOMContentLoaded', loadModelsDropdown);
664
+
665
+ function toggleSearchEx(mode) {
666
+ const simpleBtn = document.getElementById('btn-ex-simple');
667
+ const deepBtn = document.getElementById('btn-ex-deep');
668
+ const simpleCode = document.getElementById('ex-code-simple');
669
+ const deepCode = document.getElementById('ex-code-deep');
670
+
671
+ if (mode === 'simple') {
672
+ simpleCode.style.display = 'block';
673
+ deepCode.style.display = 'none';
674
+
675
+ simpleBtn.style.background = 'var(--accent)';
676
+ simpleBtn.style.borderColor = 'var(--accent)';
677
+ simpleBtn.style.color = 'white';
678
+
679
+ deepBtn.style.background = 'transparent';
680
+ deepBtn.style.borderColor = 'var(--border)';
681
+ deepBtn.style.color = 'var(--text-muted)';
682
+ } else {
683
+ simpleCode.style.display = 'none';
684
+ deepCode.style.display = 'block';
685
+
686
+ deepBtn.style.background = 'var(--accent)';
687
+ deepBtn.style.borderColor = 'var(--accent)';
688
+ deepBtn.style.color = 'white';
689
+
690
+ simpleBtn.style.background = 'transparent';
691
+ simpleBtn.style.borderColor = 'var(--border)';
692
+ simpleBtn.style.color = 'var(--text-muted)';
693
+ }
694
+ }
695
+
696
  async function runDemo(type) {
697
  const resBox = document.getElementById(type + '-res');
698
  const statusBox = document.getElementById(type + '-status');
 
712
  try {
713
  let url, body, method = 'POST';
714
  let headers = {
715
+ 'Content-Type': 'application/json'
 
716
  };
717
 
718
  if (type === 'chat-basic') {
 
818
  try {
819
  const t = new Date().getTime();
820
  const [statsRes, modelsRes] = await Promise.all([
821
+ fetch(`/qaz/stats?t=${t}`),
822
  fetch(`/models?t=${t}`)
823
  ]);
824
 
static/{admin.html → qaz.html} RENAMED
@@ -268,7 +268,7 @@
268
 
269
  try {
270
  // Call backend
271
- const res = await fetch('/admin/test_all', { method: 'POST' });
272
  const results = await res.json();
273
 
274
  console.log("Test Results:", results);
@@ -297,7 +297,7 @@
297
  btn.disabled = true;
298
 
299
  try {
300
- await fetch('/admin/clear_stats', { method: 'POST' });
301
  alert("Stats cleared successfully.");
302
  } catch (e) {
303
  alert("Clear failed: " + e.message);
@@ -308,7 +308,7 @@
308
  }
309
  async function loadKeys() {
310
  try {
311
- const res = await fetch('/admin/keys');
312
  const keys = await res.json();
313
 
314
  const tbody = document.getElementById('keys-list');
@@ -348,7 +348,7 @@
348
  if (!name) return;
349
 
350
  try {
351
- const res = await fetch('/admin/keys', {
352
  method: 'POST',
353
  headers: { 'Content-Type': 'application/json' },
354
  body: JSON.stringify({ name: name, limit_tokens: 1000000 })
@@ -369,7 +369,7 @@
369
  async function revokeKey(id) {
370
  if (!confirm("Are you sure you want to delete this key? Access will be immediately revoked.")) return;
371
  try {
372
- await fetch(`/admin/keys/${id}`, { method: 'DELETE' });
373
  loadKeys();
374
  } catch (e) {
375
  alert("Error: " + e.message);
 
268
 
269
  try {
270
  // Call backend
271
+ const res = await fetch('/qaz/test_all', { method: 'POST' });
272
  const results = await res.json();
273
 
274
  console.log("Test Results:", results);
 
297
  btn.disabled = true;
298
 
299
  try {
300
+ await fetch('/qaz/clear_stats', { method: 'POST' });
301
  alert("Stats cleared successfully.");
302
  } catch (e) {
303
  alert("Clear failed: " + e.message);
 
308
  }
309
  async function loadKeys() {
310
  try {
311
+ const res = await fetch('/qaz/keys');
312
  const keys = await res.json();
313
 
314
  const tbody = document.getElementById('keys-list');
 
348
  if (!name) return;
349
 
350
  try {
351
+ const res = await fetch('/qaz/keys', {
352
  method: 'POST',
353
  headers: { 'Content-Type': 'application/json' },
354
  body: JSON.stringify({ name: name, limit_tokens: 1000000 })
 
369
  async function revokeKey(id) {
370
  if (!confirm("Are you sure you want to delete this key? Access will be immediately revoked.")) return;
371
  try {
372
+ await fetch(`/qaz/keys/${id}`, { method: 'DELETE' });
373
  loadKeys();
374
  } catch (e) {
375
  alert("Error: " + e.message);
static/qazmlpdocs.html DELETED
@@ -1,885 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>K-AI API — Feel free to AI</title>
8
- <meta name="description" content="K-AI API — Free AI proxy API. No signup, no API keys. Feel free to AI.">
9
- <link rel="preconnect" href="https://fonts.googleapis.com">
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;
18
- --bg-secondary: #12121a;
19
- --bg-card: #16161f;
20
- --bg-card-hover: #1c1c28;
21
- --border: #2a2a3a;
22
- --text-primary: #f0f0f5;
23
- --text-secondary: #9898aa;
24
- --text-muted: #6b6b80;
25
- --accent: #6366f1;
26
- --accent-hover: #818cf8;
27
- --accent-glow: rgba(99, 102, 241, 0.15);
28
- --gradient-hero: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%);
29
- --radius-lg: 16px;
30
- --radius-sm: 8px;
31
-
32
- /* Ranking Colors */
33
- --success: #22c55e;
34
- --error: #ef4444;
35
- }
36
-
37
- * {
38
- margin: 0;
39
- padding: 0;
40
- box-sizing: border-box;
41
- }
42
-
43
- body {
44
- font-family: 'Inter', -apple-system, sans-serif;
45
- background: var(--bg-primary);
46
- color: var(--text-primary);
47
- line-height: 1.6;
48
- }
49
-
50
- .container {
51
- max-width: 1000px;
52
- margin: 0 auto;
53
- padding: 0 24px;
54
- }
55
-
56
- /* ─── Hero ─── */
57
- .hero {
58
- text-align: center;
59
- padding: 100px 0 80px;
60
- background: radial-gradient(circle at top center, rgba(99, 102, 241, 0.08) 0%, transparent 70%);
61
- }
62
-
63
- .hero h1 {
64
- font-size: 72px;
65
- font-weight: 900;
66
- letter-spacing: -3px;
67
- background: var(--gradient-hero);
68
- -webkit-background-clip: text;
69
- background-clip: text;
70
- -webkit-text-fill-color: transparent;
71
- margin-bottom: 8px;
72
- }
73
-
74
- .hero .motto {
75
- font-size: 24px;
76
- font-weight: 300;
77
- letter-spacing: 2px;
78
- color: var(--text-secondary);
79
- text-transform: uppercase;
80
- }
81
-
82
- .hero .desc {
83
- margin-top: 24px;
84
- font-size: 18px;
85
- color: var(--text-muted);
86
- max-width: 600px;
87
- margin-left: auto;
88
- margin-right: auto;
89
- }
90
-
91
- /* ─── Section ─── */
92
- section {
93
- padding: 60px 0;
94
- border-top: 1px solid var(--bg-secondary);
95
- }
96
-
97
- .section-title {
98
- font-size: 28px;
99
- font-weight: 700;
100
- margin-bottom: 40px;
101
- display: flex;
102
- align-items: center;
103
- gap: 12px;
104
- }
105
-
106
- .section-title .icon {
107
- width: 32px;
108
- height: 32px;
109
- background: var(--bg-card);
110
- border-radius: 8px;
111
- display: flex;
112
- align-items: center;
113
- justify-content: center;
114
- font-size: 16px;
115
- }
116
-
117
- /* ─── Endpoint Block ─── */
118
- .endpoint-block {
119
- background: var(--bg-card);
120
- border: 1px solid var(--border);
121
- border-radius: var(--radius-lg);
122
- overflow: hidden;
123
- margin-bottom: 40px;
124
- }
125
-
126
- .endpoint-header {
127
- padding: 24px;
128
- border-bottom: 1px solid var(--border);
129
- display: flex;
130
- justify-content: space-between;
131
- align-items: flex-start;
132
- }
133
-
134
- .endpoint-title h3 {
135
- font-size: 20px;
136
- font-weight: 700;
137
- margin-bottom: 4px;
138
- }
139
-
140
- .endpoint-title p {
141
- color: var(--text-secondary);
142
- font-size: 14px;
143
- }
144
-
145
- .method-badge {
146
- font-family: 'JetBrains Mono', monospace;
147
- font-size: 13px;
148
- font-weight: 700;
149
- padding: 4px 10px;
150
- border-radius: 6px;
151
- background: rgba(99, 102, 241, 0.2);
152
- color: #818cf8;
153
- }
154
-
155
- .method-badge.GET {
156
- background: rgba(34, 197, 94, 0.2);
157
- color: #4ade80;
158
- }
159
-
160
- .method-badge.LIVE {
161
- background: rgba(245, 158, 11, 0.2);
162
- color: #fbbf24;
163
- }
164
-
165
- .endpoint-body {
166
- display: grid;
167
- grid-template-columns: 1.2fr 0.8fr;
168
- }
169
-
170
- .endpoint-body.full-width {
171
- grid-template-columns: 1fr;
172
- }
173
-
174
- @media (max-width: 800px) {
175
-
176
- .endpoint-body,
177
- #analytics-body {
178
- flex-direction: column !important;
179
- display: flex !important;
180
- }
181
- }
182
-
183
- .endpoint-docs {
184
- padding: 24px;
185
- border-right: 1px solid var(--border);
186
- }
187
-
188
- .endpoint-demo {
189
- padding: 24px;
190
- background: var(--bg-secondary);
191
- }
192
-
193
- .param-table {
194
- width: 100%;
195
- border-collapse: collapse;
196
- font-size: 14px;
197
- margin-top: 16px;
198
- }
199
-
200
- .param-table th {
201
- text-align: left;
202
- color: var(--text-muted);
203
- font-weight: 600;
204
- padding-bottom: 8px;
205
- border-bottom: 1px solid var(--border);
206
- }
207
-
208
- .param-table td {
209
- padding: 12px 0;
210
- /* More padding for ranking */
211
- border-bottom: 1px solid var(--border);
212
- color: var(--text-secondary);
213
- font-family: 'JetBrains Mono', monospace;
214
- font-size: 13px;
215
- }
216
-
217
- .param-table tr:last-child td {
218
- border-bottom: none;
219
- }
220
-
221
- .param-name {
222
- font-family: 'JetBrains Mono', monospace;
223
- color: var(--accent-hover);
224
- }
225
-
226
- .param-desc {
227
- color: var(--text-secondary);
228
- }
229
-
230
- .param-req {
231
- color: var(--text-muted);
232
- font-size: 12px;
233
- margin-left: 6px;
234
- }
235
-
236
- /* ─── Interactive Demo ─── */
237
- .demo-label {
238
- font-size: 11px;
239
- font-weight: 700;
240
- text-transform: uppercase;
241
- letter-spacing: 1px;
242
- color: var(--text-muted);
243
- margin-bottom: 12px;
244
- display: block;
245
- }
246
-
247
- .demo-input {
248
- width: 100%;
249
- background: var(--bg-input);
250
- border: 1px solid var(--border);
251
- border-radius: var(--radius-sm);
252
- padding: 10px;
253
- color: var(--text-primary);
254
- font-family: 'JetBrains Mono', monospace;
255
- font-size: 13px;
256
- margin-bottom: 12px;
257
- }
258
-
259
- .demo-input:focus {
260
- outline: none;
261
- border-color: var(--accent);
262
- }
263
-
264
- .demo-select {
265
- width: 100%;
266
- background: var(--bg-card);
267
- color: var(--text-primary);
268
- padding: 10px;
269
- margin-bottom: 12px;
270
- border-radius: var(--radius-sm);
271
- border: 1px solid var(--border);
272
- }
273
-
274
- .demo-btn {
275
- width: 100%;
276
- padding: 8px 12px;
277
- /* Reduced padding */
278
- background: var(--accent);
279
- color: white;
280
- border: none;
281
- border-radius: var(--radius-sm);
282
- font-weight: 600;
283
- cursor: pointer;
284
- transition: all 0.2s;
285
- }
286
-
287
- .demo-btn:hover {
288
- background: var(--accent-hover);
289
- }
290
-
291
- .demo-btn:disabled {
292
- opacity: 0.5;
293
- cursor: wait;
294
- }
295
-
296
- .demo-response {
297
- margin-top: 16px;
298
- background: #0d0d14;
299
- border: 1px solid var(--border);
300
- border-radius: var(--radius-sm);
301
- padding: 12px;
302
- font-family: 'JetBrains Mono', monospace;
303
- font-size: 11px;
304
- /* Reduced font size */
305
- color: var(--text-secondary);
306
- white-space: pre-wrap;
307
- display: none;
308
- max-height: 500px;
309
- /* Scrollable */
310
- /* Scrollable */
311
- overflow-y: auto;
312
- }
313
-
314
- .demo-response pre {
315
- white-space: pre-wrap;
316
- word-wrap: break-word;
317
- margin: 0;
318
- font-family: inherit;
319
- }
320
-
321
- .demo-status {
322
- margin-top: 12px;
323
- font-weight: 600;
324
- font-size: 13px;
325
- display: none;
326
- }
327
-
328
- .demo-response.visible {
329
- display: block;
330
- }
331
-
332
- .demo-response.success {
333
- border-color: rgba(34, 197, 94, 0.3);
334
- color: #4ade80;
335
- }
336
-
337
- .demo-response.error {
338
- border-color: rgba(239, 68, 68, 0.3);
339
- color: #f87171;
340
- }
341
-
342
- /* ─── Ranking Badges & Styles ─── */
343
- .rank-badge {
344
- display: inline-block;
345
- width: 24px;
346
- height: 24px;
347
- line-height: 24px;
348
- text-align: center;
349
- border-radius: 50%;
350
- background: var(--border);
351
- color: var(--text-muted);
352
- font-weight: bold;
353
- font-size: 11px;
354
- }
355
-
356
- tr:nth-child(1) .rank-badge {
357
- background: #Eab308;
358
- color: #000;
359
- }
360
-
361
- /* Gold */
362
- tr:nth-child(2) .rank-badge {
363
- background: #94a3b8;
364
- color: #000;
365
- }
366
-
367
- /* Silver */
368
- tr:nth-child(3) .rank-badge {
369
- background: #b45309;
370
- color: #fff;
371
- }
372
-
373
- /* Bronze */
374
-
375
- .score-good {
376
- color: var(--success);
377
- }
378
-
379
- .score-bad {
380
- color: var(--error);
381
- }
382
-
383
- /* Charts */
384
- canvas {
385
- max-height: 250px;
386
- width: 100%;
387
- }
388
-
389
- /* ─── Footer ─── */
390
- footer {
391
- text-align: center;
392
- padding: 40px;
393
- color: var(--text-muted);
394
- font-size: 14px;
395
- }
396
- </style>
397
- </head>
398
-
399
- <body>
400
-
401
- <div class="hero">
402
- <div class="container">
403
- <h1>K-AI API</h1>
404
- <p class="motto">Feel free to AI</p>
405
- <p class="desc">The completely free AI proxy. No signup. No keys. Just code.</p>
406
- </div>
407
- </div>
408
-
409
- <div class="container">
410
-
411
- <!-- POST /chat (Basic) -->
412
- <div class="endpoint-block">
413
- <div class="endpoint-header">
414
- <div class="endpoint-title">
415
- <h3>Chat Completion</h3>
416
- <p>Send a message and get a response from the best available AI.</p>
417
- </div>
418
- <span class="method-badge">POST /v1/chat/completions</span>
419
- </div>
420
- <div class="endpoint-body">
421
- <div class="endpoint-docs">
422
- <span class="demo-label">Parameters</span>
423
- <table class="param-table">
424
- <tr>
425
- <td><span class="param-name">message</span></td>
426
- <td><span class="param-desc">Your prompt</span><span class="param-req">(required)</span>
427
- </td>
428
- </tr>
429
- </table>
430
- <br>
431
- <span class="demo-label">Example Request</span>
432
- <div class="demo-response visible" style="color: #a78bfa;">
433
- curl https://kiwa001-kai-api-gateway.hf.space/v1/chat/completions \
434
- -H "Content-Type: application/json" \
435
- -H "Authorization: Bearer sk-kai-demo-public" \
436
- -d '{"model": "gemini-3-flash", "messages": [{"role": "user", "content": "Hello!"}]}'
437
- </div>
438
- </div>
439
- <div class="endpoint-demo">
440
- <span class="demo-label">Try It Live</span>
441
- <textarea id="chat-basic-input" class="demo-input" rows="3"
442
- placeholder='{"message": "What is AI?"}'>What is the capital of France?</textarea>
443
-
444
- <select id="chat-basic-model" class="demo-select" style="margin-top:10px;">
445
- <option value="gemini-3-flash">gemini-3-flash (Default)</option>
446
- <option value="gpt-4o">gpt-4o</option>
447
- <option value="gpt-4o-mini">gpt-4o-mini</option>
448
- <option value="glm-4">glm-4</option>
449
- <option value="mistral-large">mistral-large</option>
450
- </select>
451
-
452
- <button class="demo-btn" onclick="runDemo('chat-basic')">Run Request ▶</button>
453
- <div id="chat-basic-status" class="demo-status"></div>
454
-
455
- <select id="chat-basic-model" class="demo-select" style="margin-top:10px;">
456
- <option value="gemini-3-flash">gemini-3-flash (Default)</option>
457
- <option value="gpt-4o">gpt-4o</option>
458
- <option value="gpt-4o-mini">gpt-4o-mini</option>
459
- <option value="glm-4">glm-4</option>
460
- <option value="mistral-large">mistral-large</option>
461
- </select>
462
-
463
- <div id="chat-basic-res" class="demo-response"></div>
464
- </div>
465
- </div>
466
- </div>
467
-
468
-
469
-
470
-
471
-
472
-
473
- <!-- GET /models -->
474
- <div class="endpoint-block">
475
- <div class="endpoint-header">
476
- <div class="endpoint-title">
477
- <h3>List Models</h3>
478
- <p>Get all currently available AI models.</p>
479
- </div>
480
- <span class="method-badge GET">GET /models</span>
481
- </div>
482
- <div class="endpoint-body">
483
- <div class="endpoint-docs">
484
- <p style="color:var(--text-secondary); font-size:14px;">Returns a JSON list of all models supported
485
- by the API, ranked by quality.</p>
486
- </div>
487
- <div class="endpoint-demo">
488
- <span class="demo-label">Try It Live</span>
489
- <button class="demo-btn" onclick="runDemo('models')">Fetch Models ▶</button>
490
- <div id="models-res" class="demo-response"></div>
491
- </div>
492
- </div>
493
- </div>
494
-
495
-
496
- <!-- POST /search & /deep_research -->
497
- <div class="endpoint-block">
498
- <div class="endpoint-header">
499
- <div class="endpoint-title">
500
- <h3>Web Search & Research</h3>
501
- <p>Reverse-engineered web search and deep content gathering. No API keys required.</p>
502
- </div>
503
- <span class="method-badge">POST /search</span>
504
- </div>
505
- <div class="endpoint-body">
506
- <div class="endpoint-docs">
507
- <span class="demo-label">Endpoints</span>
508
- <ul style="color:var(--text-secondary); font-size:13px; margin-left:18px; margin-bottom:10px;">
509
- <li><code>/search</code>: Standard web search (Links)</li>
510
- <li><code>/deep_research</code>: Deep content gathering (Scraper)</li>
511
- </ul>
512
- <span class="demo-label">Parameters</span>
513
- <table class="param-table">
514
- <tr>
515
- <td><span class="param-name">query</span></td>
516
- <td><span class="param-desc">Search topic</span><span class="param-req">(required)</span>
517
- </td>
518
- </tr>
519
- </table>
520
- </div>
521
- <div class="endpoint-demo">
522
- <span class="demo-label">Try It Live</span>
523
- <input id="search-query" class="demo-input" placeholder="Query" value="When was Python released?">
524
- <select id="search-mode" class="demo-select">
525
- <option value="search">Simple Search (Links)</option>
526
- <option value="deep">Deep Research (Content Gathering)</option>
527
- </select>
528
- <button class="demo-btn" onclick="runSearch()">Execute Search ▶</button>
529
- <div id="search-res" class="demo-response"></div>
530
- </div>
531
- </div>
532
- </div>
533
-
534
- <!-- Live Ranking Container -->
535
- <div class="endpoint-block">
536
- <div class="endpoint-header">
537
- <div class="endpoint-title">
538
- <h3>🏆 Live Model Ranking (Time-Weighted)</h3>
539
- <p>Real-time performance tracked by the engine (Speed & Reliability).</p>
540
- </div>
541
- <span class="method-badge LIVE">LIVE UPDATES</span>
542
- </div>
543
- <div class="endpoint-body full-width">
544
- <div style="padding: 24px; width: 100%; overflow-x: auto;">
545
- <table class="param-table" id="rankings-table">
546
- <thead>
547
- <tr>
548
- <th width="50">#</th>
549
- <th>Model ID</th>
550
- <th>Score</th>
551
- <th>Avg Time</th>
552
- <th>Success</th>
553
- <th>Fail</th>
554
- </tr>
555
- </thead>
556
- <tbody>
557
- <tr>
558
- <td colspan="7" style="text-align:center; padding: 20px;">Loading live stats...</td>
559
- </tr>
560
- </tbody>
561
- </table>
562
- </div>
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" id="analytics-body"
575
- style="display: flex; flex-direction: column; gap: 20px; padding: 20px;">
576
- <div style="background: var(--bg-secondary); padding: 15px; border-radius: 8px; flex: 1;">
577
- <h4 style="margin-bottom:10px; color:var(--text-secondary); font-size:12px; font-weight:700;">SPEED
578
- vs RELIABILITY</h4>
579
- <canvas id="scatterChart"></canvas>
580
- </div>
581
- <div style="background: var(--bg-secondary); padding: 15px; border-radius: 8px; flex: 1;">
582
- <h4 style="margin-bottom:10px; color:var(--text-secondary); font-size:12px; font-weight:700;">
583
- PROVIDER DISTRIBUTION</h4>
584
- <canvas id="pieChart"></canvas>
585
- </div>
586
- </div>
587
- </div>
588
-
589
- </div>
590
-
591
- <footer>K-AI API — Feel free to AI</footer>
592
-
593
- <script>
594
- // Global flag for Pie Chart
595
- window.pieChartRendered = false;
596
-
597
- async function runDemo(type) {
598
- const resBox = document.getElementById(type + '-res');
599
- const statusBox = document.getElementById(type + '-status');
600
-
601
- resBox.className = 'demo-response visible';
602
- resBox.style.display = 'none'; // Hide content while loading
603
- resBox.innerHTML = '';
604
-
605
- if (statusBox) {
606
- statusBox.style.display = 'block';
607
- statusBox.innerHTML = 'Sending Request... <span class="loading-spin"></span>';
608
- statusBox.style.color = 'var(--text-muted)';
609
- }
610
-
611
- const startTime = Date.now();
612
-
613
- try {
614
- let url, body, method = 'POST';
615
- let headers = {
616
- 'Content-Type': 'application/json',
617
- 'Authorization': 'Bearer sk-kai-demo-public' // Use Demo Key for Dashboard
618
- };
619
-
620
- if (type === 'chat-basic') {
621
- // Simple Chat
622
- const inputVal = document.getElementById('chat-basic-input').value;
623
- const modelVal = document.getElementById('chat-basic-model').value || "gemini-3-flash";
624
-
625
- if (!inputVal) { alert("Please enter a message"); return; }
626
-
627
- url = '/v1/chat/completions';
628
- body = {
629
- model: modelVal,
630
- messages: [{ role: "user", content: inputVal }]
631
- };
632
- }
633
- else if (type === 'chat-adv') {
634
- // Advanced Chat
635
- const model = document.getElementById('chat-adv-model').value || "gemini-3-flash";
636
- const userMsg = document.getElementById('chat-adv-msg').value;
637
-
638
- if (!userMsg) { alert("Please enter a message"); return; }
639
-
640
- url = '/v1/chat/completions';
641
- body = {
642
- model: model,
643
- messages: [
644
- { role: "user", content: userMsg }
645
- ]
646
- };
647
- }
648
- else if (type === 'models') {
649
- // List Models
650
- url = '/models'; // This is GET, no body
651
- method = 'GET';
652
- body = undefined;
653
- // Keep models as public? Or require auth?
654
- // Usually /models is authenticated in OpenAI but let's keep it open for now or add auth.
655
- }
656
-
657
- const response = await fetch(url, {
658
- method: method,
659
- headers: headers,
660
- body: body ? JSON.stringify(body) : undefined
661
- });
662
-
663
- const data = await response.json();
664
- const duration = Date.now() - startTime;
665
-
666
- if (!response.ok) throw new Error(data.detail || 'Request failed');
667
-
668
- if (!response.ok) throw new Error(data.detail || 'Request failed');
669
-
670
- if (statusBox) {
671
- statusBox.innerHTML = `Success (${duration}ms)`;
672
- statusBox.style.color = 'var(--success)';
673
- }
674
-
675
- resBox.style.display = 'block';
676
- resBox.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`;
677
-
678
- } catch (err) {
679
- resBox.innerHTML = `
680
- <div style="margin-bottom:5px; font-weight:bold; color:var(--error);">
681
- Error
682
- </div>
683
- <pre>${err.message}</pre>
684
- `;
685
- }
686
- }
687
-
688
- async function runSearch() {
689
- const query = document.getElementById('search-query').value;
690
- const mode = document.getElementById('search-mode').value;
691
- const resBox = document.getElementById('search-res');
692
-
693
- if (!query) { alert("Please enter a query"); return; }
694
-
695
- resBox.className = 'demo-response visible';
696
- resBox.textContent = '⏳ Searching... (Deep Research may take 10s+)';
697
-
698
- const endpoint = mode === 'deep' ? '/deep_research' : '/search';
699
-
700
- try {
701
- const res = await fetch(endpoint, {
702
- method: 'POST',
703
- headers: { 'Content-Type': 'application/json' },
704
- body: JSON.stringify({ query: query })
705
- });
706
- const data = await res.json();
707
- resBox.className = 'demo-response visible ' + (res.ok ? 'success' : 'error');
708
- resBox.innerText = JSON.stringify(data, null, 2);
709
- } catch (e) {
710
- resBox.innerText = 'Error: ' + e.message;
711
- }
712
- }
713
-
714
- // --- Live Ranking Logic ---
715
-
716
- let availableModelsSet = new Set();
717
- let scatterChart, pieChart;
718
-
719
- async function fetchRankingStats() {
720
- try {
721
- const t = new Date().getTime();
722
- const [statsRes, modelsRes] = await Promise.all([
723
- fetch(`/admin/stats?t=${t}`),
724
- fetch(`/models?t=${t}`)
725
- ]);
726
-
727
- if (!statsRes.ok) {
728
- throw new Error(`Stats HTTP ${statsRes.status}`);
729
- }
730
- const stats = await statsRes.json();
731
-
732
- availableModelsSet.clear();
733
- if (modelsRes.ok) {
734
- const data = await modelsRes.json();
735
- // Robust handling: Support both array and object wrapper
736
- const modelsList = Array.isArray(data) ? data : (data.models || []);
737
-
738
- if (Array.isArray(modelsList)) {
739
- modelsList.forEach(m => availableModelsSet.add(`${m.provider}/${m.model}`));
740
- } else {
741
- console.error("Expected array but got:", data);
742
- }
743
- }
744
-
745
- renderDashboard(stats);
746
- } catch (e) {
747
- console.error("Failed to fetch ranking", e);
748
- const tbody = document.querySelector('#rankings-table tbody');
749
- // Only replace if we haven't rendered data yet (or if it's the loading state)
750
- if (tbody.innerHTML.includes('Loading')) {
751
- tbody.innerHTML = `<tr><td colspan="7" style="color:#ef4444; text-align:center; padding: 20px;">
752
- Failed to load stats: ${e.message}<br>
753
- <small style="opacity:0.7">If on Hugging Face, check "Logs" tab for backend errors.</small>
754
- </td></tr>`;
755
- }
756
- }
757
- }
758
-
759
- function calculateScore(s, f, timeMs, cf) {
760
- let base = s - (f * 2);
761
- let penalty = (timeMs || 0) / 1000.0;
762
- let score = base - penalty;
763
- if (cf >= 3) return score - 100000;
764
- return score;
765
- }
766
-
767
- function renderDashboard(data) {
768
- let rows = [];
769
- let providerCounts = {};
770
- let scatterData = [];
771
-
772
- for (let [key, val] of Object.entries(data)) {
773
- let score = calculateScore(val.success, val.failure, val.avg_time_ms, val.consecutive_failures);
774
- rows.push({ id: key, ...val, score: score });
775
-
776
- // Provider stats for Pie
777
- let prov = key.split('/')[0];
778
- providerCounts[prov] = (providerCounts[prov] || 0) + val.success;
779
-
780
- // Scatter Data
781
- scatterData.push({
782
- x: val.avg_time_ms || 0,
783
- y: score,
784
- id: key
785
- });
786
- }
787
- rows.sort((a, b) => b.score - a.score);
788
-
789
- // Render Table
790
- const tbody = document.querySelector('#rankings-table tbody');
791
- tbody.innerHTML = '';
792
-
793
- if (rows.length === 0) {
794
- tbody.innerHTML = '<tr><td colspan="7" style="text-align:center; padding: 20px;">No stats available yet. Make a request!</td></tr>';
795
- } else {
796
- rows.forEach((row, index) => {
797
- const tr = document.createElement('tr');
798
- let scoreClass = row.score > 0 ? 'score-good' : 'score-bad';
799
- let timeStr = row.avg_time_ms ? Math.round(row.avg_time_ms) + 'ms' : '-';
800
-
801
- tr.innerHTML = `
802
- <td><span class="rank-badge">${index + 1}</span></td>
803
- <td><b>${row.id}</b></td>
804
- <td class="${scoreClass}">${row.score.toFixed(2)}</td>
805
- <td>${timeStr}</td>
806
- <td>${row.success}</td>
807
- <td>${row.failure}</td>
808
- `;
809
- tbody.appendChild(tr);
810
- });
811
- }
812
-
813
- // Render Charts
814
- updateScatterChart(scatterData);
815
- updatePieChart(providerCounts);
816
- }
817
-
818
- function updateScatterChart(data) {
819
- const ctx = document.getElementById('scatterChart').getContext('2d');
820
- if (scatterChart) {
821
- scatterChart.data.datasets[0].data = data;
822
- scatterChart.update('none');
823
- return;
824
- }
825
- const validData = data.filter(d => d.x > 0);
826
- scatterChart = new Chart(ctx, {
827
- type: 'scatter',
828
- data: {
829
- datasets: [{
830
- label: 'Models',
831
- data: validData,
832
- backgroundColor: '#8b5cf6',
833
- borderColor: '#8b5cf6',
834
- }]
835
- },
836
- options: {
837
- responsive: true,
838
- maintainAspectRatio: false,
839
- animation: { duration: 1000 },
840
- scales: {
841
- x: { type: 'linear', position: 'bottom', title: { display: true, text: 'Time (ms)', color: '#6b6b80' }, grid: { color: '#2a2a3a' } },
842
- y: { title: { display: true, text: 'Score', color: '#6b6b80' }, grid: { color: '#2a2a3a' } }
843
- },
844
- plugins: {
845
- legend: { display: false },
846
- tooltip: { callbacks: { label: (ctx) => ctx.raw.id + ': ' + ctx.raw.y.toFixed(2) } }
847
- }
848
- }
849
- });
850
- }
851
-
852
- function updatePieChart(counts) {
853
- if (window.pieChartRendered) return;
854
- const ctx = document.getElementById('pieChart').getContext('2d');
855
- window.pieChartRendered = true;
856
- pieChart = new Chart(ctx, {
857
- type: 'doughnut',
858
- data: {
859
- labels: Object.keys(counts),
860
- datasets: [{
861
- data: Object.values(counts),
862
- backgroundColor: ['#8b5cf6', '#22c55e', '#f59e0b', '#ef4444', '#ec4899', '#3b82f6'],
863
- borderWidth: 0
864
- }]
865
- },
866
- options: {
867
- responsive: true,
868
- maintainAspectRatio: false,
869
- animation: { duration: 1000 },
870
- plugins: {
871
- legend: { position: 'right', labels: { color: '#9898aa', font: { size: 10 } } }
872
- }
873
- }
874
- });
875
- }
876
-
877
- // Init Live Ranking
878
- fetchRankingStats();
879
- setInterval(fetchRankingStats, 5000);
880
-
881
- </script>
882
-
883
- </body>
884
-
885
- </html>