Spaces:
Running
Running
refactor: remove legacy /chat endpoint and update dashboard
Browse files- main.py +0 -62
- static/docs.html +230 -198
main.py
CHANGED
|
@@ -255,68 +255,6 @@ async def root():
|
|
| 255 |
return FileResponse("static/docs.html")
|
| 256 |
|
| 257 |
|
| 258 |
-
@app.post(
|
| 259 |
-
"/chat",
|
| 260 |
-
response_model=ChatResponse,
|
| 261 |
-
responses={500: {"model": ErrorResponse}},
|
| 262 |
-
tags=["Chat"],
|
| 263 |
-
)
|
| 264 |
-
async def chat(request: ChatRequest):
|
| 265 |
-
"""
|
| 266 |
-
Send a message and get an AI response.
|
| 267 |
-
|
| 268 |
-
- **message**: Your prompt/question (required)
|
| 269 |
-
- **model**: Optional model name (defaults to best available)
|
| 270 |
-
- **provider**: "auto" (tries all), "g4f", or "pollinations"
|
| 271 |
-
- **system_prompt**: Optional system instructions for the AI
|
| 272 |
-
|
| 273 |
-
Each request is fully stateless — no conversation history has to be retained.
|
| 274 |
-
If a specific provider is chosen, tries all models on that provider
|
| 275 |
-
ranked by quality before falling back to other providers.
|
| 276 |
-
All users have unlimited access — no rate limiting.
|
| 277 |
-
"""
|
| 278 |
-
logger.info(
|
| 279 |
-
f"Chat request: model={request.model}, provider={request.provider}, "
|
| 280 |
-
f"message_length={len(request.message)}"
|
| 281 |
-
)
|
| 282 |
-
|
| 283 |
-
try:
|
| 284 |
-
result = await engine.chat(
|
| 285 |
-
prompt=request.message,
|
| 286 |
-
model=request.model,
|
| 287 |
-
provider=request.provider or "auto",
|
| 288 |
-
system_prompt=request.system_prompt,
|
| 289 |
-
)
|
| 290 |
-
|
| 291 |
-
return ChatResponse(
|
| 292 |
-
response=result["response"],
|
| 293 |
-
model=result["model"],
|
| 294 |
-
provider=result["provider"],
|
| 295 |
-
attempts=result.get("attempts", 1),
|
| 296 |
-
response_time_ms=result.get("response_time_ms"),
|
| 297 |
-
timestamp=datetime.utcnow().isoformat() + "Z",
|
| 298 |
-
)
|
| 299 |
-
|
| 300 |
-
except ValueError as e:
|
| 301 |
-
raise HTTPException(status_code=400, detail=str(e))
|
| 302 |
-
except RuntimeError as e:
|
| 303 |
-
logger.error(f"All models exhausted: {e}")
|
| 304 |
-
raise HTTPException(
|
| 305 |
-
status_code=503,
|
| 306 |
-
detail=(
|
| 307 |
-
"All AI models are currently unavailable. "
|
| 308 |
-
"Please contact the developer to fix this issue. "
|
| 309 |
-
f"Details: {str(e)}"
|
| 310 |
-
),
|
| 311 |
-
)
|
| 312 |
-
except Exception as e:
|
| 313 |
-
logger.error(f"Unexpected error: {e}")
|
| 314 |
-
raise HTTPException(
|
| 315 |
-
status_code=500,
|
| 316 |
-
detail=f"Internal server error: {str(e)}",
|
| 317 |
-
)
|
| 318 |
-
|
| 319 |
-
|
| 320 |
@app.get(
|
| 321 |
"/models",
|
| 322 |
response_model=ModelsResponse,
|
|
|
|
| 255 |
return FileResponse("static/docs.html")
|
| 256 |
|
| 257 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
@app.get(
|
| 259 |
"/models",
|
| 260 |
response_model=ModelsResponse,
|
static/docs.html
CHANGED
|
@@ -517,7 +517,7 @@
|
|
| 517 |
<h3>List Models</h3>
|
| 518 |
<p>Get all currently available AI models.</p>
|
| 519 |
</div>
|
| 520 |
-
<span class="method-badge GET">
|
| 521 |
</div>
|
| 522 |
<div class="endpoint-body">
|
| 523 |
<div class="endpoint-docs">
|
|
@@ -598,165 +598,197 @@
|
|
| 598 |
async function runDemo(type) {
|
| 599 |
const resBox = document.getElementById(type + '-res');
|
| 600 |
resBox.className = 'demo-response visible';
|
| 601 |
-
resBox.
|
|
|
|
| 602 |
|
| 603 |
-
|
| 604 |
-
let url = '/chat';
|
| 605 |
-
let method = 'POST';
|
| 606 |
-
let body = {};
|
| 607 |
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
body = {
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
model: document.getElementById('chat-adv-model').value
|
| 615 |
};
|
| 616 |
-
}
|
| 617 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 618 |
method = 'GET';
|
| 619 |
-
body =
|
|
|
|
|
|
|
| 620 |
}
|
| 621 |
|
| 622 |
-
const
|
| 623 |
method: method,
|
| 624 |
-
headers:
|
| 625 |
-
body: body ? JSON.stringify(body) :
|
| 626 |
});
|
| 627 |
|
| 628 |
-
const data = await
|
| 629 |
-
|
| 630 |
-
resBox.textContent = JSON.stringify(data, null, 2);
|
| 631 |
-
} catch (e) {
|
| 632 |
-
resBox.textContent = 'Error: ' + e.message;
|
| 633 |
-
}
|
| 634 |
-
}
|
| 635 |
-
|
| 636 |
-
async function runSearch() {
|
| 637 |
-
const query = document.getElementById('search-query').value;
|
| 638 |
-
const mode = document.getElementById('search-mode').value;
|
| 639 |
-
const resBox = document.getElementById('search-res');
|
| 640 |
-
|
| 641 |
-
if (!query) { alert("Please enter a query"); return; }
|
| 642 |
|
| 643 |
-
|
| 644 |
-
resBox.textContent = '⏳ Searching... (Deep Research may take 10s+)';
|
| 645 |
|
| 646 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 647 |
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
}
|
| 660 |
-
}
|
| 661 |
|
| 662 |
-
|
| 663 |
|
| 664 |
-
|
| 665 |
-
|
| 666 |
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
|
|
|
| 690 |
}
|
| 691 |
-
}
|
| 692 |
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
Failed to load stats: ${e.message}<br>
|
| 701 |
<small style="opacity:0.7">If on Hugging Face, check "Logs" tab for backend errors.</small>
|
| 702 |
</td></tr>`;
|
|
|
|
| 703 |
}
|
| 704 |
}
|
| 705 |
-
}
|
| 706 |
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
}
|
| 714 |
-
|
| 715 |
-
function renderDashboard(data) {
|
| 716 |
-
let rows = [];
|
| 717 |
-
let providerCounts = {};
|
| 718 |
-
let scatterData = [];
|
| 719 |
-
|
| 720 |
-
for (let [key, val] of Object.entries(data)) {
|
| 721 |
-
let score = calculateScore(val.success, val.failure, val.avg_time_ms, val.consecutive_failures);
|
| 722 |
-
rows.push({ id: key, ...val, score: score });
|
| 723 |
-
|
| 724 |
-
// Provider stats for Pie
|
| 725 |
-
let prov = key.split('/')[0];
|
| 726 |
-
providerCounts[prov] = (providerCounts[prov] || 0) + val.success;
|
| 727 |
-
|
| 728 |
-
// Scatter Data
|
| 729 |
-
scatterData.push({
|
| 730 |
-
x: val.avg_time_ms || 0,
|
| 731 |
-
y: score,
|
| 732 |
-
id: key
|
| 733 |
-
});
|
| 734 |
}
|
| 735 |
-
rows.sort((a, b) => b.score - a.score);
|
| 736 |
-
|
| 737 |
-
// Render Table
|
| 738 |
-
const tbody = document.querySelector('#rankings-table tbody');
|
| 739 |
-
tbody.innerHTML = '';
|
| 740 |
-
|
| 741 |
-
if (rows.length === 0) {
|
| 742 |
-
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center; padding: 20px;">No stats available yet. Make a request!</td></tr>';
|
| 743 |
-
} else {
|
| 744 |
-
rows.forEach((row, index) => {
|
| 745 |
-
const tr = document.createElement('tr');
|
| 746 |
-
let scoreClass = row.score > 0 ? 'score-good' : 'score-bad';
|
| 747 |
-
let timeStr = row.avg_time_ms ? Math.round(row.avg_time_ms) + 'ms' : '-';
|
| 748 |
-
|
| 749 |
-
let status = '';
|
| 750 |
-
if (!availableModelsSet.has(row.id)) {
|
| 751 |
-
status = '<span style="color:#9ca3af; font-size:11px; border:1px solid #374151; padding:2px 6px; border-radius:4px;">OFFLINE (Local Only)</span>';
|
| 752 |
-
tr.style.opacity = '0.6';
|
| 753 |
-
} else if (row.consecutive_failures >= 3) {
|
| 754 |
-
status = '<span style="color:#ef4444">⚠️ CRITICAL</span>';
|
| 755 |
-
} else {
|
| 756 |
-
status = '<span style="color:#22c55e">● Active</span>';
|
| 757 |
-
}
|
| 758 |
|
| 759 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 760 |
<td><span class="rank-badge">${index + 1}</span></td>
|
| 761 |
<td><b>${row.id}</b></td>
|
| 762 |
<td class="${scoreClass}">${row.score.toFixed(2)}</td>
|
|
@@ -765,77 +797,77 @@
|
|
| 765 |
<td>${row.failure}</td>
|
| 766 |
<td>${status}</td>
|
| 767 |
`;
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
// Render Charts
|
| 773 |
-
updateScatterChart(scatterData);
|
| 774 |
-
updatePieChart(providerCounts);
|
| 775 |
-
}
|
| 776 |
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
scatterChart.data.datasets[0].data = data;
|
| 781 |
-
scatterChart.update('none');
|
| 782 |
-
return;
|
| 783 |
}
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
datasets
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
},
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 806 |
}
|
| 807 |
-
}
|
| 808 |
-
}
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
}
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
}
|
| 832 |
-
}
|
| 833 |
-
}
|
| 834 |
-
}
|
| 835 |
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
|
| 840 |
</script>
|
| 841 |
|
|
|
|
| 517 |
<h3>List Models</h3>
|
| 518 |
<p>Get all currently available AI models.</p>
|
| 519 |
</div>
|
| 520 |
+
<span class="method-badge GET">POST /v1/chat/completions</span>
|
| 521 |
</div>
|
| 522 |
<div class="endpoint-body">
|
| 523 |
<div class="endpoint-docs">
|
|
|
|
| 598 |
async function runDemo(type) {
|
| 599 |
const resBox = document.getElementById(type + '-res');
|
| 600 |
resBox.className = 'demo-response visible';
|
| 601 |
+
resBox.style.display = 'block';
|
| 602 |
+
resBox.innerHTML = 'Sending Request... <span class="loading-spin"></span>';
|
| 603 |
|
| 604 |
+
const startTime = Date.now();
|
|
|
|
|
|
|
|
|
|
| 605 |
|
| 606 |
+
try {
|
| 607 |
+
let url, body, method = 'POST';
|
| 608 |
+
let headers = {
|
| 609 |
+
'Content-Type': 'application/json',
|
| 610 |
+
'Authorization': 'Bearer sk-kai-demo-public' // Use Demo Key for Dashboard
|
| 611 |
+
};
|
| 612 |
+
|
| 613 |
+
if (type === 'chat') {
|
| 614 |
+
// Simple Chat
|
| 615 |
+
url = '/v1/chat/completions';
|
| 616 |
body = {
|
| 617 |
+
model: "gemini-3-flash",
|
| 618 |
+
messages: [{ role: "user", content: "Hello! Who are you?" }]
|
|
|
|
| 619 |
};
|
| 620 |
+
}
|
| 621 |
+
else if (type === 'chat-adv') {
|
| 622 |
+
// Advanced Chat
|
| 623 |
+
const model = document.getElementById('chat-adv-model').value || "gemini-3-flash";
|
| 624 |
+
url = '/v1/chat/completions';
|
| 625 |
+
body = {
|
| 626 |
+
model: model,
|
| 627 |
+
messages: [
|
| 628 |
+
{ role: "system", content: "You are a helpful assistant." },
|
| 629 |
+
{ role: "user", content: "Tell me a fun fact." }
|
| 630 |
+
]
|
| 631 |
+
};
|
| 632 |
+
}
|
| 633 |
+
else if (type === 'models') {
|
| 634 |
+
// List Models
|
| 635 |
+
url = '/models'; // This is GET, no body
|
| 636 |
method = 'GET';
|
| 637 |
+
body = undefined;
|
| 638 |
+
// Keep models as public? Or require auth?
|
| 639 |
+
// Usually /models is authenticated in OpenAI but let's keep it open for now or add auth.
|
| 640 |
}
|
| 641 |
|
| 642 |
+
const response = await fetch(url, {
|
| 643 |
method: method,
|
| 644 |
+
headers: headers,
|
| 645 |
+
body: body ? JSON.stringify(body) : undefined
|
| 646 |
});
|
| 647 |
|
| 648 |
+
const data = await response.json();
|
| 649 |
+
const duration = Date.now() - startTime;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
|
| 651 |
+
if (!response.ok) throw new Error(data.detail || 'Request failed');
|
|
|
|
| 652 |
|
| 653 |
+
resBox.innerHTML = `
|
| 654 |
+
<div style="margin-bottom:5px; font-weight:bold; color:var(--success);">
|
| 655 |
+
Success (${duration}ms)
|
| 656 |
+
</div>
|
| 657 |
+
<pre>${JSON.stringify(data, null, 2)}</pre>
|
| 658 |
+
`;
|
| 659 |
|
| 660 |
+
} catch (err) {
|
| 661 |
+
resBox.innerHTML = `
|
| 662 |
+
<div style="margin-bottom:5px; font-weight:bold; color:var(--error);">
|
| 663 |
+
Error
|
| 664 |
+
</div>
|
| 665 |
+
<pre>${err.message}</pre>
|
| 666 |
+
`;
|
| 667 |
+
}
|
| 668 |
+
async function runSearch() {
|
| 669 |
+
const query = document.getElementById('search-query').value;
|
| 670 |
+
const mode = document.getElementById('search-mode').value;
|
| 671 |
+
const resBox = document.getElementById('search-res');
|
| 672 |
+
|
| 673 |
+
if (!query) { alert("Please enter a query"); return; }
|
| 674 |
+
|
| 675 |
+
resBox.className = 'demo-response visible';
|
| 676 |
+
resBox.textContent = '⏳ Searching... (Deep Research may take 10s+)';
|
| 677 |
+
|
| 678 |
+
const endpoint = mode === 'deep' ? '/deep_research' : '/search';
|
| 679 |
+
|
| 680 |
+
try {
|
| 681 |
+
const res = await fetch(endpoint, {
|
| 682 |
+
method: 'POST',
|
| 683 |
+
headers: { 'Content-Type': 'application/json' },
|
| 684 |
+
body: JSON.stringify({ query: query })
|
| 685 |
+
});
|
| 686 |
+
const data = await res.json();
|
| 687 |
+
resBox.className = 'demo-response visible ' + (res.ok ? 'success' : 'error');
|
| 688 |
+
resBox.innerText = JSON.stringify(data, null, 2);
|
| 689 |
+
} catch (e) {
|
| 690 |
+
resBox.innerText = 'Error: ' + e.message;
|
| 691 |
+
}
|
| 692 |
}
|
|
|
|
| 693 |
|
| 694 |
+
// --- Live Ranking Logic ---
|
| 695 |
|
| 696 |
+
let availableModelsSet = new Set();
|
| 697 |
+
let scatterChart, pieChart;
|
| 698 |
|
| 699 |
+
async function fetchRankingStats() {
|
| 700 |
+
try {
|
| 701 |
+
const t = new Date().getTime();
|
| 702 |
+
const [statsRes, modelsRes] = await Promise.all([
|
| 703 |
+
fetch(`/admin/stats?t=${t}`),
|
| 704 |
+
fetch(`/models?t=${t}`)
|
| 705 |
+
]);
|
| 706 |
+
|
| 707 |
+
if (!statsRes.ok) {
|
| 708 |
+
throw new Error(`Stats HTTP ${statsRes.status}`);
|
| 709 |
+
}
|
| 710 |
+
const stats = await statsRes.json();
|
| 711 |
+
|
| 712 |
+
availableModelsSet.clear();
|
| 713 |
+
if (modelsRes.ok) {
|
| 714 |
+
const data = await modelsRes.json();
|
| 715 |
+
// Robust handling: Support both array and object wrapper
|
| 716 |
+
const modelsList = Array.isArray(data) ? data : (data.models || []);
|
| 717 |
+
|
| 718 |
+
if (Array.isArray(modelsList)) {
|
| 719 |
+
modelsList.forEach(m => availableModelsSet.add(`${m.provider}/${m.model}`));
|
| 720 |
+
} else {
|
| 721 |
+
console.error("Expected array but got:", data);
|
| 722 |
+
}
|
| 723 |
}
|
|
|
|
| 724 |
|
| 725 |
+
renderDashboard(stats);
|
| 726 |
+
} catch (e) {
|
| 727 |
+
console.error("Failed to fetch ranking", e);
|
| 728 |
+
const tbody = document.querySelector('#rankings-table tbody');
|
| 729 |
+
// Only replace if we haven't rendered data yet (or if it's the loading state)
|
| 730 |
+
if (tbody.innerHTML.includes('Loading')) {
|
| 731 |
+
tbody.innerHTML = `<tr><td colspan="7" style="color:#ef4444; text-align:center; padding: 20px;">
|
| 732 |
Failed to load stats: ${e.message}<br>
|
| 733 |
<small style="opacity:0.7">If on Hugging Face, check "Logs" tab for backend errors.</small>
|
| 734 |
</td></tr>`;
|
| 735 |
+
}
|
| 736 |
}
|
| 737 |
}
|
|
|
|
| 738 |
|
| 739 |
+
function calculateScore(s, f, timeMs, cf) {
|
| 740 |
+
let base = s - (f * 2);
|
| 741 |
+
let penalty = (timeMs || 0) / 1000.0;
|
| 742 |
+
let score = base - penalty;
|
| 743 |
+
if (cf >= 3) return score - 100000;
|
| 744 |
+
return score;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 745 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 746 |
|
| 747 |
+
function renderDashboard(data) {
|
| 748 |
+
let rows = [];
|
| 749 |
+
let providerCounts = {};
|
| 750 |
+
let scatterData = [];
|
| 751 |
+
|
| 752 |
+
for (let [key, val] of Object.entries(data)) {
|
| 753 |
+
let score = calculateScore(val.success, val.failure, val.avg_time_ms, val.consecutive_failures);
|
| 754 |
+
rows.push({ id: key, ...val, score: score });
|
| 755 |
+
|
| 756 |
+
// Provider stats for Pie
|
| 757 |
+
let prov = key.split('/')[0];
|
| 758 |
+
providerCounts[prov] = (providerCounts[prov] || 0) + val.success;
|
| 759 |
+
|
| 760 |
+
// Scatter Data
|
| 761 |
+
scatterData.push({
|
| 762 |
+
x: val.avg_time_ms || 0,
|
| 763 |
+
y: score,
|
| 764 |
+
id: key
|
| 765 |
+
});
|
| 766 |
+
}
|
| 767 |
+
rows.sort((a, b) => b.score - a.score);
|
| 768 |
+
|
| 769 |
+
// Render Table
|
| 770 |
+
const tbody = document.querySelector('#rankings-table tbody');
|
| 771 |
+
tbody.innerHTML = '';
|
| 772 |
+
|
| 773 |
+
if (rows.length === 0) {
|
| 774 |
+
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center; padding: 20px;">No stats available yet. Make a request!</td></tr>';
|
| 775 |
+
} else {
|
| 776 |
+
rows.forEach((row, index) => {
|
| 777 |
+
const tr = document.createElement('tr');
|
| 778 |
+
let scoreClass = row.score > 0 ? 'score-good' : 'score-bad';
|
| 779 |
+
let timeStr = row.avg_time_ms ? Math.round(row.avg_time_ms) + 'ms' : '-';
|
| 780 |
+
|
| 781 |
+
let status = '';
|
| 782 |
+
if (!availableModelsSet.has(row.id)) {
|
| 783 |
+
status = '<span style="color:#9ca3af; font-size:11px; border:1px solid #374151; padding:2px 6px; border-radius:4px;">OFFLINE (Local Only)</span>';
|
| 784 |
+
tr.style.opacity = '0.6';
|
| 785 |
+
} else if (row.consecutive_failures >= 3) {
|
| 786 |
+
status = '<span style="color:#ef4444">⚠️ CRITICAL</span>';
|
| 787 |
+
} else {
|
| 788 |
+
status = '<span style="color:#22c55e">● Active</span>';
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
tr.innerHTML = `
|
| 792 |
<td><span class="rank-badge">${index + 1}</span></td>
|
| 793 |
<td><b>${row.id}</b></td>
|
| 794 |
<td class="${scoreClass}">${row.score.toFixed(2)}</td>
|
|
|
|
| 797 |
<td>${row.failure}</td>
|
| 798 |
<td>${status}</td>
|
| 799 |
`;
|
| 800 |
+
tbody.appendChild(tr);
|
| 801 |
+
});
|
| 802 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 803 |
|
| 804 |
+
// Render Charts
|
| 805 |
+
updateScatterChart(scatterData);
|
| 806 |
+
updatePieChart(providerCounts);
|
|
|
|
|
|
|
|
|
|
| 807 |
}
|
| 808 |
+
|
| 809 |
+
function updateScatterChart(data) {
|
| 810 |
+
const ctx = document.getElementById('scatterChart').getContext('2d');
|
| 811 |
+
if (scatterChart) {
|
| 812 |
+
scatterChart.data.datasets[0].data = data;
|
| 813 |
+
scatterChart.update('none');
|
| 814 |
+
return;
|
| 815 |
+
}
|
| 816 |
+
const validData = data.filter(d => d.x > 0);
|
| 817 |
+
scatterChart = new Chart(ctx, {
|
| 818 |
+
type: 'scatter',
|
| 819 |
+
data: {
|
| 820 |
+
datasets: [{
|
| 821 |
+
label: 'Models',
|
| 822 |
+
data: validData,
|
| 823 |
+
backgroundColor: '#8b5cf6',
|
| 824 |
+
borderColor: '#8b5cf6',
|
| 825 |
+
}]
|
| 826 |
},
|
| 827 |
+
options: {
|
| 828 |
+
responsive: true,
|
| 829 |
+
maintainAspectRatio: false,
|
| 830 |
+
animation: { duration: 1000 },
|
| 831 |
+
scales: {
|
| 832 |
+
x: { type: 'linear', position: 'bottom', title: { display: true, text: 'Time (ms)', color: '#6b6b80' }, grid: { color: '#2a2a3a' } },
|
| 833 |
+
y: { title: { display: true, text: 'Score', color: '#6b6b80' }, grid: { color: '#2a2a3a' } }
|
| 834 |
+
},
|
| 835 |
+
plugins: {
|
| 836 |
+
legend: { display: false },
|
| 837 |
+
tooltip: { callbacks: { label: (ctx) => ctx.raw.id + ': ' + ctx.raw.y.toFixed(2) } }
|
| 838 |
+
}
|
| 839 |
}
|
| 840 |
+
});
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
function updatePieChart(counts) {
|
| 844 |
+
if (window.pieChartRendered) return;
|
| 845 |
+
const ctx = document.getElementById('pieChart').getContext('2d');
|
| 846 |
+
window.pieChartRendered = true;
|
| 847 |
+
pieChart = new Chart(ctx, {
|
| 848 |
+
type: 'doughnut',
|
| 849 |
+
data: {
|
| 850 |
+
labels: Object.keys(counts),
|
| 851 |
+
datasets: [{
|
| 852 |
+
data: Object.values(counts),
|
| 853 |
+
backgroundColor: ['#8b5cf6', '#22c55e', '#f59e0b', '#ef4444', '#ec4899', '#3b82f6'],
|
| 854 |
+
borderWidth: 0
|
| 855 |
+
}]
|
| 856 |
+
},
|
| 857 |
+
options: {
|
| 858 |
+
responsive: true,
|
| 859 |
+
maintainAspectRatio: false,
|
| 860 |
+
animation: { duration: 1000 },
|
| 861 |
+
plugins: {
|
| 862 |
+
legend: { position: 'right', labels: { color: '#9898aa', font: { size: 10 } } }
|
| 863 |
+
}
|
| 864 |
}
|
| 865 |
+
});
|
| 866 |
+
}
|
|
|
|
| 867 |
|
| 868 |
+
// Init Live Ranking
|
| 869 |
+
fetchRankingStats();
|
| 870 |
+
setInterval(fetchRankingStats, 5000);
|
| 871 |
|
| 872 |
</script>
|
| 873 |
|