| <!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; } |
| |
| |
| .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; } |
| |
| |
| .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); } |
| |
| |
| .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; } |
| |
| |
| #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' : ''}">▶</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>`; |
| } |
| } |
| |
| |
| 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> |
|
|