KiWA001's picture
Add API key requirements to all endpoints with dashboard examples
48e7c93
<!DOCTYPE html>
<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 !important;
display: flex !important;
}
}
.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>