Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>K-AI API — Feel free to AI</title> | |
| <meta name="description" content="K-AI API — Free AI proxy API. No signup, no API keys. Feel free to AI."> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <!-- Chart.js --> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| :root { | |
| --bg-primary: #0a0a0f; | |
| --bg-secondary: #12121a; | |
| --bg-card: #16161f; | |
| --bg-card-hover: #1c1c28; | |
| --border: #2a2a3a; | |
| --text-primary: #f0f0f5; | |
| --text-secondary: #9898aa; | |
| --text-muted: #6b6b80; | |
| --accent: #6366f1; | |
| --accent-hover: #818cf8; | |
| --accent-glow: rgba(99, 102, 241, 0.15); | |
| --gradient-hero: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%); | |
| --radius-lg: 16px; | |
| --radius-sm: 8px; | |
| /* Ranking Colors */ | |
| --success: #22c55e; | |
| --error: #ef4444; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, sans-serif; | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| line-height: 1.6; | |
| } | |
| .container { | |
| max-width: 1000px; | |
| margin: 0 auto; | |
| padding: 0 24px; | |
| } | |
| /* ─── Hero ─── */ | |
| .hero { | |
| text-align: center; | |
| padding: 100px 0 80px; | |
| background: radial-gradient(circle at top center, rgba(99, 102, 241, 0.08) 0%, transparent 70%); | |
| } | |
| .hero h1 { | |
| font-family: 'Playfair Display', serif; | |
| font-size: 72px; | |
| font-weight: 400; | |
| font-style: italic; | |
| letter-spacing: -1px; | |
| background: var(--gradient-hero); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| margin-bottom: 8px; | |
| } | |
| .hero .motto { | |
| font-size: 24px; | |
| font-weight: 300; | |
| letter-spacing: 2px; | |
| color: var(--text-secondary); | |
| text-transform: uppercase; | |
| } | |
| .hero .desc { | |
| margin-top: 24px; | |
| font-size: 18px; | |
| color: var(--text-muted); | |
| max-width: 600px; | |
| margin-left: auto; | |
| margin-right: auto; | |
| } | |
| /* ─── Section ─── */ | |
| section { | |
| padding: 60px 0; | |
| border-top: 1px solid var(--bg-secondary); | |
| } | |
| .section-title { | |
| font-size: 28px; | |
| font-weight: 700; | |
| margin-bottom: 40px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .section-title .icon { | |
| width: 32px; | |
| height: 32px; | |
| background: var(--bg-card); | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 16px; | |
| } | |
| /* ─── Endpoint Block ─── */ | |
| .endpoint-block { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| overflow: hidden; | |
| margin-bottom: 40px; | |
| } | |
| .endpoint-header { | |
| padding: 24px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| } | |
| .endpoint-title h3 { | |
| font-size: 20px; | |
| font-weight: 700; | |
| margin-bottom: 4px; | |
| } | |
| .endpoint-title p { | |
| color: var(--text-secondary); | |
| font-size: 14px; | |
| } | |
| .method-badge { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 13px; | |
| font-weight: 700; | |
| padding: 4px 10px; | |
| border-radius: 6px; | |
| background: rgba(99, 102, 241, 0.2); | |
| color: #818cf8; | |
| } | |
| .method-badge.GET { | |
| background: rgba(34, 197, 94, 0.2); | |
| color: #4ade80; | |
| } | |
| .method-badge.LIVE { | |
| background: rgba(245, 158, 11, 0.2); | |
| color: #fbbf24; | |
| } | |
| .endpoint-body { | |
| display: grid; | |
| grid-template-columns: 1.2fr 0.8fr; | |
| } | |
| .endpoint-body.full-width { | |
| grid-template-columns: 1fr; | |
| } | |
| @media (max-width: 800px) { | |
| .endpoint-body, | |
| #analytics-body { | |
| flex-direction: column ; | |
| display: flex ; | |
| } | |
| } | |
| .endpoint-docs { | |
| padding: 24px; | |
| border-right: 1px solid var(--border); | |
| } | |
| .endpoint-demo { | |
| padding: 24px; | |
| background: var(--bg-secondary); | |
| } | |
| .param-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 14px; | |
| margin-top: 16px; | |
| } | |
| .param-table th { | |
| text-align: left; | |
| color: var(--text-muted); | |
| font-weight: 600; | |
| padding-bottom: 8px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .param-table td { | |
| padding: 12px 0; | |
| /* More padding for ranking */ | |
| border-bottom: 1px solid var(--border); | |
| color: var(--text-secondary); | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 13px; | |
| } | |
| .param-table tr:last-child td { | |
| border-bottom: none; | |
| } | |
| .param-name { | |
| font-family: 'JetBrains Mono', monospace; | |
| color: var(--accent-hover); | |
| } | |
| .param-desc { | |
| color: var(--text-secondary); | |
| } | |
| .param-req { | |
| color: var(--text-muted); | |
| font-size: 12px; | |
| margin-left: 6px; | |
| } | |
| /* ─── Interactive Demo ─── */ | |
| .demo-label { | |
| font-size: 11px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| color: var(--text-muted); | |
| margin-bottom: 12px; | |
| display: block; | |
| } | |
| .demo-input { | |
| width: 100%; | |
| background: var(--bg-input); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| padding: 10px; | |
| color: var(--text-primary); | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 13px; | |
| margin-bottom: 12px; | |
| } | |
| .demo-input:focus { | |
| outline: none; | |
| border-color: var(--accent); | |
| } | |
| .demo-select { | |
| width: 100%; | |
| background: var(--bg-card); | |
| color: var(--text-primary); | |
| padding: 10px; | |
| margin-bottom: 12px; | |
| border-radius: var(--radius-sm); | |
| border: 1px solid var(--border); | |
| /* Revert to native appearance to ensure visibility */ | |
| /* appearance: none; */ | |
| /* 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"); */ | |
| /* -webkit-appearance: none; */ | |
| /* background-repeat: no-repeat; */ | |
| /* background-position: right 12px top 50%; */ | |
| /* background-size: 12px auto; */ | |
| } | |
| .demo-select option { | |
| background: var(--bg-card); | |
| color: var(--text-primary); | |
| padding: 10px; | |
| } | |
| .demo-btn { | |
| width: 100%; | |
| padding: 8px 12px; | |
| /* Reduced padding */ | |
| background: var(--accent); | |
| color: white; | |
| border: none; | |
| border-radius: var(--radius-sm); | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .demo-btn:hover { | |
| background: var(--accent-hover); | |
| } | |
| .demo-btn:disabled { | |
| opacity: 0.5; | |
| cursor: wait; | |
| } | |
| .demo-response { | |
| margin-top: 16px; | |
| background: #0d0d14; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| padding: 12px; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| /* Reduced font size */ | |
| color: var(--text-secondary); | |
| white-space: pre-wrap; | |
| display: none; | |
| max-height: 500px; | |
| /* Scrollable */ | |
| /* Scrollable */ | |
| overflow-y: auto; | |
| } | |
| .demo-response pre { | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| margin: 0; | |
| font-family: inherit; | |
| } | |
| .demo-status { | |
| margin-top: 12px; | |
| font-weight: 600; | |
| font-size: 13px; | |
| display: none; | |
| } | |
| .demo-response.visible { | |
| display: block; | |
| } | |
| .demo-response.success { | |
| border-color: rgba(34, 197, 94, 0.3); | |
| color: #4ade80; | |
| } | |
| .demo-response.error { | |
| border-color: rgba(239, 68, 68, 0.3); | |
| color: #f87171; | |
| } | |
| /* ─── Ranking Badges & Styles ─── */ | |
| .rank-badge { | |
| display: inline-block; | |
| width: 24px; | |
| height: 24px; | |
| line-height: 24px; | |
| text-align: center; | |
| border-radius: 50%; | |
| background: var(--border); | |
| color: var(--text-muted); | |
| font-weight: bold; | |
| font-size: 11px; | |
| } | |
| tr:nth-child(1) .rank-badge { | |
| background: #Eab308; | |
| color: #000; | |
| } | |
| /* Gold */ | |
| tr:nth-child(2) .rank-badge { | |
| background: #94a3b8; | |
| color: #000; | |
| } | |
| /* Silver */ | |
| tr:nth-child(3) .rank-badge { | |
| background: #b45309; | |
| color: #fff; | |
| } | |
| /* Bronze */ | |
| .score-good { | |
| color: var(--success); | |
| } | |
| .score-bad { | |
| color: var(--error); | |
| } | |
| /* Charts */ | |
| canvas { | |
| max-height: 250px; | |
| width: 100%; | |
| } | |
| /* ─── Footer ─── */ | |
| footer { | |
| text-align: center; | |
| padding: 40px; | |
| color: var(--text-muted); | |
| font-size: 14px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="hero"> | |
| <div class="container"> | |
| <h1>K-AI API</h1> | |
| <p class="motto">Feel free to AI</p> | |
| <p class="desc">Leading AI models reverse engineered to a simple api call.</p> | |
| </div> | |
| </div> | |
| <div class="container"> | |
| <!-- POST /chat (Basic) --> | |
| <div class="endpoint-block"> | |
| <div class="endpoint-header"> | |
| <div class="endpoint-title"> | |
| <h3>Chat Completion</h3> | |
| <p>Send a message and get a response from the best available AI.</p> | |
| </div> | |
| <span class="method-badge">POST /v1/chat/completions</span> | |
| </div> | |
| <div class="endpoint-body"> | |
| <div class="endpoint-docs"> | |
| <span class="demo-label">Parameters</span> | |
| <table class="param-table"> | |
| <tr> | |
| <td><span class="param-name">message</span></td> | |
| <td><span class="param-desc">Your prompt</span><span class="param-req">(required)</span> | |
| </td> | |
| </tr> | |
| </table> | |
| <br> | |
| <span class="demo-label">Example Request</span> | |
| <div class="demo-response visible" style="color: #a78bfa;"> | |
| curl https://kiwa001-kai-api-gateway.hf.space/v1/chat/completions \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer YOUR_API_KEY" \ | |
| -d '{"model": "gemini-3-flash", "messages": [{"role": "user", "content": "Hello!"}]}' | |
| </div> | |
| <p style="color: var(--text-muted); font-size: 13px; margin-top: 10px;"> | |
| <strong>Note:</strong> Replace YOUR_API_KEY with your actual API key from the dashboard above. | |
| </p> | |
| </div> | |
| <div class="endpoint-demo"> | |
| <span class="demo-label">Try It Live</span> | |
| <textarea id="chat-basic-input" class="demo-input" rows="3" | |
| placeholder='{"message": "What is AI?"}'>What is the capital of France?</textarea> | |
| <select id="chat-basic-model" class="demo-select" style="margin-top:10px;"> | |
| <option value="gemini-3-flash" selected>Loading models...</option> | |
| </select> | |
| <button class="demo-btn" onclick="runDemo('chat-basic')">Run Request ▶</button> | |
| <div id="chat-basic-status" class="demo-status"></div> | |
| <div id="chat-basic-res" class="demo-response"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- GET /models --> | |
| <div class="endpoint-block"> | |
| <div class="endpoint-header"> | |
| <div class="endpoint-title"> | |
| <h3>List Models</h3> | |
| <p>Get all currently available AI models.</p> | |
| </div> | |
| <span class="method-badge GET">GET /models</span> | |
| </div> | |
| <div class="endpoint-body"> | |
| <div class="endpoint-docs"> | |
| <p style="color:var(--text-secondary); font-size:14px;">Returns a JSON list of all models supported | |
| by the API, ranked by quality.</p> | |
| <br> | |
| <span class="demo-label">Example Request</span> | |
| <div class="demo-response visible" style="color: #a78bfa;"> | |
| curl https://kiwa001-kai-api-gateway.hf.space/models \ | |
| -H "Authorization: Bearer YOUR_API_KEY" | |
| </div> | |
| </div> | |
| <div class="endpoint-demo"> | |
| <span class="demo-label">Try It Live</span> | |
| <button class="demo-btn" onclick="runDemo('models')">Fetch Models ▶</button> | |
| <div id="models-res" class="demo-response"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- POST /search & /deep_research --> | |
| <div class="endpoint-block"> | |
| <div class="endpoint-header"> | |
| <div class="endpoint-title"> | |
| <h3>Web Search & Research</h3> | |
| <p>Reverse-engineered web search and deep content gathering. Requires API key (free, no token deduction).</p> | |
| </div> | |
| <span class="method-badge">POST /search</span> | |
| </div> | |
| <div class="endpoint-body"> | |
| <div class="endpoint-docs"> | |
| <span class="demo-label">Endpoints</span> | |
| <ul style="color:var(--text-secondary); font-size:13px; margin-left:18px; margin-bottom:10px;"> | |
| <li><code>/search</code>: Standard web search (Links)</li> | |
| <li><code>/deep_research</code>: Deep content gathering (Scraper)</li> | |
| </ul> | |
| <span class="demo-label">Parameters</span> | |
| <table class="param-table"> | |
| <tr> | |
| <td><span class="param-name">query</span></td> | |
| <td><span class="param-desc">Search topic</span><span class="param-req">(required)</span> | |
| </td> | |
| </tr> | |
| </table> | |
| <br> | |
| <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;"> | |
| <span class="demo-label" style="margin-bottom:0;">Example Request</span> | |
| <div style="display:flex; gap:8px;"> | |
| <button onclick="toggleSearchEx('simple')" id="btn-ex-simple" | |
| style="padding:4px 8px; border-radius:4px; border:1px solid var(--accent); background:var(--accent); color:white; font-size:11px; cursor:pointer;">Simple</button> | |
| <button onclick="toggleSearchEx('deep')" id="btn-ex-deep" | |
| style="padding:4px 8px; border-radius:4px; border:1px solid var(--border); background:transparent; color:var(--text-muted); font-size:11px; cursor:pointer;">Deep | |
| Research</button> | |
| </div> | |
| </div> | |
| <div id="ex-code-simple" class="demo-response visible" style="color: #a78bfa;"> | |
| curl -X POST https://kiwa001-kai-api-gateway.hf.space/search \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer YOUR_API_KEY" \ | |
| -d '{"query": "Machine Learning", "limit": 5}' | |
| </div> | |
| <div id="ex-code-deep" class="demo-response" style="color: #a78bfa; display:none;"> | |
| curl -X POST https://kiwa001-kai-api-gateway.hf.space/deep_research \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer YOUR_API_KEY" \ | |
| -d '{"query": "Future of AI", "limit": 2}' | |
| </div> | |
| <p style="color: var(--text-muted); font-size: 13px; margin-top: 10px;"> | |
| <strong>Note:</strong> Web searches require an API key but are <strong>free</strong> (no token deduction). | |
| </p> | |
| </div> | |
| <div class="endpoint-demo"> | |
| <span class="demo-label">Try It Live</span> | |
| <input id="search-query" class="demo-input" placeholder="Query" value="When was Python released?"> | |
| <select id="search-mode" class="demo-select"> | |
| <option value="search">Simple Search (Links)</option> | |
| <option value="deep">Deep Research (Content Gathering)</option> | |
| </select> | |
| <button class="demo-btn" onclick="runSearch()">Execute Search ▶</button> | |
| <div id="search-res" class="demo-response"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Live Ranking Container --> | |
| <div class="endpoint-block"> | |
| <div class="endpoint-header"> | |
| <div class="endpoint-title"> | |
| <h3>🏆 Live Model Ranking (Time-Weighted)</h3> | |
| <p>Real-time performance tracked by the engine (Speed & Reliability).</p> | |
| </div> | |
| <span class="method-badge LIVE">LIVE UPDATES</span> | |
| </div> | |
| <div class="endpoint-body full-width"> | |
| <div style="padding: 24px; width: 100%; overflow-x: auto;"> | |
| <table class="param-table" id="rankings-table"> | |
| <thead> | |
| <tr> | |
| <th width="50">#</th> | |
| <th>Model ID</th> | |
| <th>Score</th> | |
| <th>Avg Time</th> | |
| <th>Success</th> | |
| <th>Fail</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td colspan="7" style="text-align:center; padding: 20px;">Loading live stats...</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Network Analytics (Graphs) --> | |
| <div class="endpoint-block"> | |
| <div class="endpoint-header"> | |
| <div class="endpoint-title"> | |
| <h3>Network Analytics</h3> | |
| <p>Live Latency vs. Reliability & Provider Distribution.</p> | |
| </div> | |
| </div> | |
| <div class="endpoint-body" id="analytics-body" | |
| style="display: flex; flex-direction: column; gap: 20px; padding: 20px;"> | |
| <div style="background: var(--bg-secondary); padding: 15px; border-radius: 8px; flex: 1;"> | |
| <h4 style="margin-bottom:10px; color:var(--text-secondary); font-size:12px; font-weight:700;">SPEED | |
| vs RELIABILITY</h4> | |
| <canvas id="scatterChart"></canvas> | |
| </div> | |
| <div style="background: var(--bg-secondary); padding: 15px; border-radius: 8px; flex: 1;"> | |
| <h4 style="margin-bottom:10px; color:var(--text-secondary); font-size:12px; font-weight:700;"> | |
| PROVIDER DISTRIBUTION</h4> | |
| <canvas id="pieChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <footer>K-AI API — Feel free to AI</footer> | |
| <script> | |
| // Global flag for Pie Chart | |
| window.pieChartRendered = false; | |
| // Auto-load models for dropdown | |
| async function loadModelsDropdown() { | |
| const select = document.getElementById('chat-basic-model'); | |
| if (!select) return; | |
| try { | |
| const res = await fetch('/models'); | |
| if (!res.ok) throw new Error("Failed to fetch models"); | |
| const data = await res.json(); | |
| const models = Array.isArray(data) ? data : (data.models || []); | |
| if (models.length === 0) throw new Error("No models returned"); | |
| select.innerHTML = ''; | |
| models.forEach(m => { | |
| const opt = document.createElement('option'); | |
| // G4F models usually have .model or .name, sometimes .id | |
| // Robust fallback: id -> model -> name | |
| const val = m.id || m.model || m.name; | |
| if (val) { | |
| opt.value = val; | |
| opt.textContent = val; | |
| if (val === 'gemini-3-flash') opt.selected = true; | |
| select.appendChild(opt); | |
| } | |
| }); | |
| } catch (e) { | |
| console.error("Model load error", e); | |
| // Fallback if empty or error | |
| if (select.options.length <= 1) { | |
| select.innerHTML = ` | |
| <option value="gemini-3-flash" selected>gemini-3-flash (Default)</option> | |
| <option value="gpt-4o">gpt-4o</option> | |
| <option value="gpt-4o-mini">gpt-4o-mini</option> | |
| <option value="glm-4">glm-4</option> | |
| <option value="mistral-large">mistral-large</option> | |
| `; | |
| } | |
| } | |
| } | |
| document.addEventListener('DOMContentLoaded', loadModelsDropdown); | |
| function toggleSearchEx(mode) { | |
| const simpleBtn = document.getElementById('btn-ex-simple'); | |
| const deepBtn = document.getElementById('btn-ex-deep'); | |
| const simpleCode = document.getElementById('ex-code-simple'); | |
| const deepCode = document.getElementById('ex-code-deep'); | |
| if (mode === 'simple') { | |
| simpleCode.style.display = 'block'; | |
| deepCode.style.display = 'none'; | |
| simpleBtn.style.background = 'var(--accent)'; | |
| simpleBtn.style.borderColor = 'var(--accent)'; | |
| simpleBtn.style.color = 'white'; | |
| deepBtn.style.background = 'transparent'; | |
| deepBtn.style.borderColor = 'var(--border)'; | |
| deepBtn.style.color = 'var(--text-muted)'; | |
| } else { | |
| simpleCode.style.display = 'none'; | |
| deepCode.style.display = 'block'; | |
| deepBtn.style.background = 'var(--accent)'; | |
| deepBtn.style.borderColor = 'var(--accent)'; | |
| deepBtn.style.color = 'white'; | |
| simpleBtn.style.background = 'transparent'; | |
| simpleBtn.style.borderColor = 'var(--border)'; | |
| simpleBtn.style.color = 'var(--text-muted)'; | |
| } | |
| } | |
| async function runDemo(type) { | |
| const resBox = document.getElementById(type + '-res'); | |
| const statusBox = document.getElementById(type + '-status'); | |
| resBox.className = 'demo-response visible'; | |
| resBox.style.display = 'none'; // Hide content while loading | |
| resBox.innerHTML = ''; | |
| if (statusBox) { | |
| statusBox.style.display = 'block'; | |
| statusBox.innerHTML = 'Sending Request... <span class="loading-spin"></span>'; | |
| statusBox.style.color = 'var(--text-muted)'; | |
| } | |
| const startTime = Date.now(); | |
| try { | |
| let url, body, method = 'POST'; | |
| let headers = { | |
| 'Content-Type': 'application/json' | |
| }; | |
| if (type === 'chat-basic') { | |
| // Simple Chat | |
| const inputVal = document.getElementById('chat-basic-input').value; | |
| const modelVal = document.getElementById('chat-basic-model').value || "gemini-3-flash"; | |
| if (!inputVal) { alert("Please enter a message"); return; } | |
| url = '/v1/chat/completions'; | |
| body = { | |
| model: modelVal, | |
| messages: [{ role: "user", content: inputVal }] | |
| }; | |
| } | |
| else if (type === 'chat-adv') { | |
| // Advanced Chat | |
| const model = document.getElementById('chat-adv-model').value || "gemini-3-flash"; | |
| const userMsg = document.getElementById('chat-adv-msg').value; | |
| if (!userMsg) { alert("Please enter a message"); return; } | |
| url = '/v1/chat/completions'; | |
| body = { | |
| model: model, | |
| messages: [ | |
| { role: "user", content: userMsg } | |
| ] | |
| }; | |
| } | |
| else if (type === 'models') { | |
| // List Models | |
| url = '/models'; // This is GET, no body | |
| method = 'GET'; | |
| body = undefined; | |
| // Keep models as public? Or require auth? | |
| // Usually /models is authenticated in OpenAI but let's keep it open for now or add auth. | |
| } | |
| const response = await fetch(url, { | |
| method: method, | |
| headers: headers, | |
| body: body ? JSON.stringify(body) : undefined | |
| }); | |
| const data = await response.json(); | |
| const duration = Date.now() - startTime; | |
| if (!response.ok) throw new Error(data.detail || 'Request failed'); | |
| if (!response.ok) throw new Error(data.detail || 'Request failed'); | |
| if (statusBox) { | |
| statusBox.innerHTML = `Success (${duration}ms)`; | |
| statusBox.style.color = 'var(--success)'; | |
| } | |
| resBox.style.display = 'block'; | |
| resBox.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`; | |
| } catch (err) { | |
| resBox.innerHTML = ` | |
| <div style="margin-bottom:5px; font-weight:bold; color:var(--error);"> | |
| Error | |
| </div> | |
| <pre>${err.message}</pre> | |
| `; | |
| } | |
| } | |
| async function runSearch() { | |
| const query = document.getElementById('search-query').value; | |
| const mode = document.getElementById('search-mode').value; | |
| const resBox = document.getElementById('search-res'); | |
| if (!query) { alert("Please enter a query"); return; } | |
| resBox.className = 'demo-response visible'; | |
| resBox.textContent = '⏳ Searching... (Deep Research may take 10s+)'; | |
| const endpoint = mode === 'deep' ? '/deep_research' : '/search'; | |
| try { | |
| const res = await fetch(endpoint, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ query: query }) | |
| }); | |
| const data = await res.json(); | |
| resBox.className = 'demo-response visible ' + (res.ok ? 'success' : 'error'); | |
| resBox.innerText = JSON.stringify(data, null, 2); | |
| } catch (e) { | |
| resBox.innerText = 'Error: ' + e.message; | |
| } | |
| } | |
| // --- Live Ranking Logic --- | |
| let availableModelsSet = new Set(); | |
| let scatterChart, pieChart; | |
| async function fetchRankingStats() { | |
| try { | |
| const t = new Date().getTime(); | |
| const [statsRes, modelsRes] = await Promise.all([ | |
| fetch(`/qaz/stats?t=${t}`), | |
| fetch(`/models?t=${t}`) | |
| ]); | |
| if (!statsRes.ok) { | |
| throw new Error(`Stats HTTP ${statsRes.status}`); | |
| } | |
| const stats = await statsRes.json(); | |
| availableModelsSet.clear(); | |
| if (modelsRes.ok) { | |
| const data = await modelsRes.json(); | |
| // Robust handling: Support both array and object wrapper | |
| const modelsList = Array.isArray(data) ? data : (data.models || []); | |
| if (Array.isArray(modelsList)) { | |
| modelsList.forEach(m => availableModelsSet.add(`${m.provider}/${m.model}`)); | |
| } else { | |
| console.error("Expected array but got:", data); | |
| } | |
| } | |
| renderDashboard(stats); | |
| } catch (e) { | |
| console.error("Failed to fetch ranking", e); | |
| const tbody = document.querySelector('#rankings-table tbody'); | |
| // Only replace if we haven't rendered data yet (or if it's the loading state) | |
| if (tbody.innerHTML.includes('Loading')) { | |
| tbody.innerHTML = `<tr><td colspan="7" style="color:#ef4444; text-align:center; padding: 20px;"> | |
| Failed to load stats: ${e.message}<br> | |
| <small style="opacity:0.7">If on Hugging Face, check "Logs" tab for backend errors.</small> | |
| </td></tr>`; | |
| } | |
| } | |
| } | |
| function calculateScore(s, f, timeMs, cf) { | |
| let base = s - (f * 2); | |
| let penalty = (timeMs || 0) / 1000.0; | |
| let score = base - penalty; | |
| if (cf >= 3) return score - 100000; | |
| return score; | |
| } | |
| function renderDashboard(data) { | |
| let rows = []; | |
| let providerCounts = {}; | |
| let scatterData = []; | |
| for (let [key, val] of Object.entries(data)) { | |
| let score = calculateScore(val.success, val.failure, val.avg_time_ms, val.consecutive_failures); | |
| rows.push({ id: key, ...val, score: score }); | |
| // Provider stats for Pie | |
| let prov = key.split('/')[0]; | |
| providerCounts[prov] = (providerCounts[prov] || 0) + val.success; | |
| // Scatter Data | |
| scatterData.push({ | |
| x: val.avg_time_ms || 0, | |
| y: score, | |
| id: key | |
| }); | |
| } | |
| rows.sort((a, b) => b.score - a.score); | |
| // Render Table | |
| const tbody = document.querySelector('#rankings-table tbody'); | |
| tbody.innerHTML = ''; | |
| if (rows.length === 0) { | |
| tbody.innerHTML = '<tr><td colspan="7" style="text-align:center; padding: 20px;">No stats available yet. Make a request!</td></tr>'; | |
| } else { | |
| rows.forEach((row, index) => { | |
| const tr = document.createElement('tr'); | |
| let scoreClass = row.score > 0 ? 'score-good' : 'score-bad'; | |
| let timeStr = row.avg_time_ms ? Math.round(row.avg_time_ms) + 'ms' : '-'; | |
| tr.innerHTML = ` | |
| <td><span class="rank-badge">${index + 1}</span></td> | |
| <td><b>${row.id}</b></td> | |
| <td class="${scoreClass}">${row.score.toFixed(2)}</td> | |
| <td>${timeStr}</td> | |
| <td>${row.success}</td> | |
| <td>${row.failure}</td> | |
| `; | |
| tbody.appendChild(tr); | |
| }); | |
| } | |
| // Render Charts | |
| updateScatterChart(scatterData); | |
| updatePieChart(providerCounts); | |
| } | |
| function updateScatterChart(data) { | |
| const ctx = document.getElementById('scatterChart').getContext('2d'); | |
| if (scatterChart) { | |
| scatterChart.data.datasets[0].data = data; | |
| scatterChart.update('none'); | |
| return; | |
| } | |
| const validData = data.filter(d => d.x > 0); | |
| scatterChart = new Chart(ctx, { | |
| type: 'scatter', | |
| data: { | |
| datasets: [{ | |
| label: 'Models', | |
| data: validData, | |
| backgroundColor: '#8b5cf6', | |
| borderColor: '#8b5cf6', | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| animation: { duration: 1000 }, | |
| scales: { | |
| x: { type: 'linear', position: 'bottom', title: { display: true, text: 'Time (ms)', color: '#6b6b80' }, grid: { color: '#2a2a3a' } }, | |
| y: { title: { display: true, text: 'Score', color: '#6b6b80' }, grid: { color: '#2a2a3a' } } | |
| }, | |
| plugins: { | |
| legend: { display: false }, | |
| tooltip: { callbacks: { label: (ctx) => ctx.raw.id + ': ' + ctx.raw.y.toFixed(2) } } | |
| } | |
| } | |
| }); | |
| } | |
| function updatePieChart(counts) { | |
| if (window.pieChartRendered) return; | |
| const ctx = document.getElementById('pieChart').getContext('2d'); | |
| window.pieChartRendered = true; | |
| pieChart = new Chart(ctx, { | |
| type: 'doughnut', | |
| data: { | |
| labels: Object.keys(counts), | |
| datasets: [{ | |
| data: Object.values(counts), | |
| backgroundColor: ['#8b5cf6', '#22c55e', '#f59e0b', '#ef4444', '#ec4899', '#3b82f6'], | |
| borderWidth: 0 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| animation: { duration: 1000 }, | |
| plugins: { | |
| legend: { position: 'right', labels: { color: '#9898aa', font: { size: 10 } } } | |
| } | |
| } | |
| }); | |
| } | |
| // Init Live Ranking | |
| fetchRankingStats(); | |
| setInterval(fetchRankingStats, 5000); | |
| // --- API Key Usage Lookup --- | |
| async function lookupTokenUsage() { | |
| const tokenInput = document.getElementById('lookup-token-input'); | |
| const token = tokenInput.value.trim(); | |
| const resultDiv = document.getElementById('lookup-result'); | |
| const errorDiv = document.getElementById('lookup-error'); | |
| if (!token) { | |
| errorDiv.style.display = 'block'; | |
| errorDiv.textContent = 'Please enter your API key'; | |
| resultDiv.style.display = 'none'; | |
| return; | |
| } | |
| if (!token.startsWith('sk-')) { | |
| errorDiv.style.display = 'block'; | |
| errorDiv.textContent = 'Invalid API key format. Should start with "sk-"'; | |
| resultDiv.style.display = 'none'; | |
| return; | |
| } | |
| // Show loading | |
| resultDiv.style.display = 'block'; | |
| errorDiv.style.display = 'none'; | |
| document.getElementById('lookup-name').textContent = 'Loading...'; | |
| try { | |
| const res = await fetch('/qaz/keys/lookup', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ token: token }) | |
| }); | |
| if (!res.ok) { | |
| let errorMessage = 'Key not found'; | |
| try { | |
| const error = await res.json(); | |
| errorMessage = error.detail || error.message || JSON.stringify(error) || 'Key not found'; | |
| } catch (parseError) { | |
| errorMessage = 'Key not found or server error'; | |
| } | |
| throw new Error(errorMessage); | |
| } | |
| const data = await res.json(); | |
| // Update display | |
| document.getElementById('lookup-name').textContent = data.name || 'Unnamed Key'; | |
| const statusEl = document.getElementById('lookup-status'); | |
| if (data.is_active) { | |
| statusEl.textContent = 'Active'; | |
| statusEl.style.color = 'var(--success)'; | |
| } else { | |
| statusEl.textContent = 'Inactive'; | |
| statusEl.style.color = 'var(--error)'; | |
| } | |
| const usagePercent = data.limit_tokens > 0 ? Math.round((data.usage_tokens / data.limit_tokens) * 100) : 0; | |
| document.getElementById('lookup-percentage').textContent = usagePercent + '%'; | |
| document.getElementById('lookup-used').textContent = `Used: ${data.usage_tokens.toLocaleString()}`; | |
| document.getElementById('lookup-remaining').textContent = `Remaining: ${data.remaining.toLocaleString()}`; | |
| const progressEl = document.getElementById('lookup-progress'); | |
| progressEl.style.width = Math.min(usagePercent, 100) + '%'; | |
| // Color coding | |
| if (usagePercent > 90) { | |
| progressEl.style.background = 'var(--error)'; | |
| document.getElementById('lookup-remaining').style.color = 'var(--error)'; | |
| } else if (usagePercent > 50) { | |
| progressEl.style.background = 'var(--warning)'; | |
| document.getElementById('lookup-remaining').style.color = 'var(--warning)'; | |
| } else { | |
| progressEl.style.background = 'var(--success)'; | |
| document.getElementById('lookup-remaining').style.color = 'var(--success)'; | |
| } | |
| } catch (e) { | |
| errorDiv.style.display = 'block'; | |
| const errorMsg = e.message || (typeof e === 'object' ? JSON.stringify(e) : String(e)); | |
| errorDiv.textContent = 'Error: ' + errorMsg; | |
| resultDiv.style.display = 'none'; | |
| } | |
| } | |
| // Allow Enter key to submit | |
| document.getElementById('lookup-token-input')?.addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter') { | |
| lookupTokenUsage(); | |
| } | |
| }); | |
| </script> | |
| <!-- API Key Usage Checker Section --> | |
| <section style="border-top: 1px solid var(--border); padding: 60px 0;"> | |
| <div class="container"> | |
| <h2 class="section-title">Check Your API Usage</h2> | |
| <p style="color: var(--text-muted); margin-bottom: 30px;">Enter your API key to see your current token usage and remaining quota.</p> | |
| <div style="background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 30px; max-width: 800px; margin: 0 auto;"> | |
| <div style="display: flex; gap: 15px; margin-bottom: 25px; flex-wrap: wrap;"> | |
| <input type="text" id="lookup-token-input" placeholder="Enter your API key (sk-...)" | |
| style="flex: 1; min-width: 300px; background: var(--bg-primary); border: 1px solid var(--border); color: var(--text-primary); | |
| padding: 14px 18px; border-radius: 8px; font-family: 'JetBrains Mono', monospace; font-size: 15px;"> | |
| <button onclick="lookupTokenUsage()" | |
| style="background: var(--accent); color: white; border: none; padding: 14px 28px; | |
| border-radius: 8px; font-weight: 600; cursor: pointer; font-size: 15px; white-space: nowrap;"> | |
| Check Usage | |
| </button> | |
| </div> | |
| <div id="lookup-error" style="display: none; margin-bottom: 20px; padding: 12px 16px; background: rgba(239, 68, 68, 0.1); | |
| border: 1px solid var(--error); border-radius: 8px; color: var(--error); font-size: 14px;"> | |
| </div> | |
| <div id="lookup-result" style="display: none;"> | |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 25px; margin-bottom: 25px;"> | |
| <div style="background: var(--bg-primary); padding: 20px; border-radius: 8px; border: 1px solid var(--border);"> | |
| <div style="color: var(--text-muted); font-size: 13px; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px;">Key Name</div> | |
| <div id="lookup-name" style="font-size: 20px; font-weight: 600; color: var(--text-primary);">-</div> | |
| </div> | |
| <div style="background: var(--bg-primary); padding: 20px; border-radius: 8px; border: 1px solid var(--border);"> | |
| <div style="color: var(--text-muted); font-size: 13px; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px;">Status</div> | |
| <div id="lookup-status" style="font-size: 20px; font-weight: 600;">-</div> | |
| </div> | |
| </div> | |
| <div style="background: var(--bg-primary); padding: 25px; border-radius: 8px; border: 1px solid var(--border);"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;"> | |
| <span style="color: var(--text-muted); font-size: 14px;">Token Usage</span> | |
| <span id="lookup-percentage" style="font-weight: 700; font-size: 18px; color: var(--text-primary);">0%</span> | |
| </div> | |
| <div style="width: 100%; height: 10px; background: var(--border); border-radius: 5px; overflow: hidden; margin-bottom: 15px;"> | |
| <div id="lookup-progress" style="height: 100%; width: 0%; background: var(--success); transition: all 0.3s ease;"></div> | |
| </div> | |
| <div style="display: flex; justify-content: space-between; font-family: 'JetBrains Mono', monospace; font-size: 14px;"> | |
| <span id="lookup-used" style="color: var(--text-muted);">Used: 0</span> | |
| <span id="lookup-remaining" style="color: var(--success); font-weight: 600;">Remaining: 1000</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </body> | |
| </html> |