openajaj / static /providers.html
Jindrich3's picture
Super-squash branch 'main' using huggingface_hub
5eb8692
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenAjaj — Provider Reliability</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #0f0f1a;
color: #d0d0e8;
min-height: 100vh;
padding: 20px 24px;
}
a { color: #6fa8dc; text-decoration: none; }
a:hover { text-decoration: underline; }
.header { margin-bottom: 24px; }
.header h1 { font-size: 20px; font-weight: 700; margin-bottom: 6px; }
.header nav { font-size: 13px; color: #666680; display: flex; gap: 16px; }
/* ── Summary cards ── */
.cards { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 24px; }
.card {
background: #1a1a2e; border: 1px solid #2a2a48; border-radius: 10px;
padding: 12px 18px; min-width: 110px;
}
.card .v { font-size: 24px; font-weight: 800; }
.card .l { font-size: 10px; color: #666680; text-transform: uppercase; letter-spacing: .5px; margin-top: 2px; }
/* ── Provider rows ── */
.prov {
background: #1a1a2e; border: 1px solid #2a2a48; border-radius: 10px;
margin-bottom: 10px; overflow: hidden;
}
.prov-head {
display: grid; grid-template-columns: 130px 70px 1fr 200px 30px;
align-items: center; gap: 12px; padding: 14px 18px; cursor: pointer;
transition: background .15s;
}
.prov-head:hover { background: #22223a; }
.prov-name { font-weight: 700; font-size: 15px; }
.prov-rel { font-size: 22px; font-weight: 800; text-align: right; }
.prov-bar { background: #12122a; border-radius: 5px; height: 8px; overflow: hidden; }
.prov-bar-fill { height: 100%; border-radius: 5px; transition: width .5s; }
.prov-stats { font-size: 11px; color: #666680; text-align: right; white-space: nowrap; }
.prov-arrow { font-size: 12px; color: #444460; transition: transform .2s; text-align: center; }
.prov-arrow.open { transform: rotate(90deg); }
/* ── Model detail table ── */
.models { display: none; border-top: 1px solid #2a2a48; }
.models.open { display: block; }
.models table { width: 100%; border-collapse: collapse; font-size: 12px; }
.models th {
background: #141428; padding: 6px 14px; text-align: left;
color: #555570; font-size: 10px; text-transform: uppercase; letter-spacing: .4px;
}
.models td { padding: 8px 14px; border-bottom: 1px solid #1a1a30; }
.models tr:hover { background: #1e1e34; }
.tag {
display: inline-block; font-size: 9px; padding: 1px 6px; border-radius: 3px;
font-weight: 700; vertical-align: middle;
}
.tag-free { background: #0f2a1a; color: #4ec88a; border: 1px solid #1a4a2a; }
.tag-paid { background: #2a1a0f; color: #c88a4e; border: 1px solid #4a2a1a; }
.err-text { color: #d04040; font-size: 10px; margin-top: 2px; }
.speed { color: #4ec88a; font-weight: 600; }
.muted { color: #444460; }
.bar-sm { display: inline-block; width: 50px; height: 5px; background: #1a1a30; border-radius: 3px; overflow: hidden; vertical-align: middle; }
.bar-sm-fill { height: 100%; border-radius: 3px; }
.loading { text-align: center; padding: 60px; color: #555570; }
/* ── Tooltip ── */
#tip {
position: fixed; display: none; background: #1a1a30; color: #c0c0e0;
border: 1px solid #3a3a60; border-radius: 6px; padding: 6px 10px;
font-size: 11px; max-width: 300px; z-index: 999; pointer-events: none;
line-height: 1.4; box-shadow: 0 4px 20px rgba(0,0,0,.6);
}
</style>
</head>
<body>
<div class="header">
<h1>Provider Reliability</h1>
<nav>
<span>Live reliability from real API calls</span>
<a href="/results">Model IQ Table</a>
<a href="/">Chat</a>
</nav>
</div>
<div id="app"><div class="loading">Loading...</div></div>
<div id="tip"></div>
<script>
const $ = id => document.getElementById(id);
let data = [];
let open = new Set();
function rc(p) {
if (p == null) return '#444460';
return p >= 90 ? '#4ec88a' : p >= 70 ? '#d8b84e' : '#d04040';
}
function render() {
const totP = data.length;
const totA = data.reduce((s, p) => s + p.attempts, 0);
const totS = data.reduce((s, p) => s + p.successes, 0);
const totE = data.reduce((s, p) => s + p.errors, 0);
const avgR = totA ? Math.round(totS / totA * 100) : 0;
let h = `
<div class="cards">
<div class="card"><div class="v">${totP}</div><div class="l">Providers</div></div>
<div class="card"><div class="v">${totA}</div><div class="l">API Calls</div></div>
<div class="card"><div class="v" style="color:${totE ? '#d04040' : '#4ec88a'}">${totE}</div><div class="l">Errors</div></div>
<div class="card"><div class="v" style="color:${rc(avgR)}">${avgR}%</div><div class="l">Overall</div></div>
</div>`;
for (const p of data) {
const r = p.reliability;
const c = rc(r);
const isOpen = open.has(p.provider);
const errPct = p.attempts ? Math.round(p.errors / p.attempts * 100) : 0;
let statsArr = [`${p.successes}/${p.attempts} ok`];
if (p.errors) statsArr.push(`${p.errors} err (${errPct}%)`);
statsArr.push(`${p.models.length} model${p.models.length !== 1 ? 's' : ''}`);
if (p.avg_ttft != null) statsArr.push(`${p.avg_ttft.toFixed(1)}s TTFT`);
if (p.avg_tok_sec != null) statsArr.push(`${Math.round(p.avg_tok_sec)} tok/s`);
h += `
<div class="prov">
<div class="prov-head" onclick="tog('${p.provider}')">
<div class="prov-name">${p.provider}</div>
<div class="prov-rel" style="color:${c}">${r != null ? r + '%' : '—'}</div>
<div class="prov-bar"><div class="prov-bar-fill" style="width:${r || 0}%;background:${c}"></div></div>
<div class="prov-stats">${statsArr.join(' · ')}</div>
<div class="prov-arrow${isOpen ? ' open' : ''}">&#9654;</div>
</div>
<div class="models${isOpen ? ' open' : ''}">
<table>
<tr><th>Model</th><th>Tier</th><th>Reliability</th><th>Calls</th><th>Errors</th><th>TTFT</th><th>Tok/s</th><th></th></tr>
${p.models.map(m => {
const mc = rc(m.reliability);
const shortName = m.id.split('/').pop();
const fullId = m.id.includes('/') ? `<div style="color:#444460;font-size:10px">${m.id}</div>` : '';
const errLine = m.last_error_msg ? `<div class="err-text">${m.last_error_msg}</div>` : '';
return `<tr>
<td><div style="font-weight:600">${shortName}</div>${fullId}${errLine}</td>
<td>${m.free ? '<span class="tag tag-free">FREE</span>' : '<span class="tag tag-paid">PAID</span>'}</td>
<td style="font-weight:700;color:${mc}">${m.reliability != null ? m.reliability + '%' : '—'}</td>
<td class="muted">${m.attempts}</td>
<td style="color:${m.errors ? '#d04040' : '#444460'}">${m.errors}</td>
<td>${m.ttft != null ? `<span class="speed">${m.ttft.toFixed(1)}s</span>` : '<span class="muted">—</span>'}</td>
<td>${m.tok_sec != null ? `<span class="speed">${m.tok_sec.toFixed(0)}</span>` : '<span class="muted">—</span>'}</td>
<td><div class="bar-sm"><div class="bar-sm-fill" style="width:${m.reliability||0}%;background:${mc}"></div></div></td>
</tr>`;
}).join('')}
</table>
</div>
</div>`;
}
$('app').innerHTML = h;
}
function tog(id) {
open.has(id) ? open.delete(id) : open.add(id);
render();
}
async function load() {
try {
const resp = await fetch('/api/provider-reliability');
const json = await resp.json();
if (json.error) throw new Error(json.error);
data = json.providers || [];
render();
} catch (e) {
$('app').innerHTML = `<div class="loading" style="color:#d04040">Error: ${e.message}</div>`;
}
}
// Tooltips
const tip = $('tip');
document.addEventListener('mouseover', e => {
const el = e.target.closest('[data-tip]');
if (!el) { tip.style.display = 'none'; return; }
tip.textContent = el.dataset.tip;
tip.style.display = 'block';
});
document.addEventListener('mousemove', e => {
if (tip.style.display !== 'block') return;
let x = e.clientX + 12, y = e.clientY + 12;
if (x + 310 > innerWidth) x = e.clientX - 310;
if (y + 60 > innerHeight) y = e.clientY - 60;
tip.style.left = x + 'px'; tip.style.top = y + 'px';
});
document.addEventListener('mouseout', e => {
if (!e.target.closest('[data-tip]')) tip.style.display = 'none';
});
load();
</script>
</body>
</html>