mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 00:48:00 +00:00
fix(ui): status-code donut chart with hover tooltips + model usage % = share of total; plan.md P4-9/P4-10
- paintStatusTable: replace plain table with SVG ring (conic slices per status code, center total, clickable legend with title=value/reqs/percent/ok/err) - paintModelTable: percentage now = model share of total requests (was relative-to-max, so the top model always showed 100%/97%); bar stays max-relative - plan.md: record P4-9 (visualization) and P4-10 (claude 400 'extra usage' = Anthropic account quota, not fingerprint; qijiar+openai serves claude fine, no adapter disguise needed) - deployed to /usr/local/bin/llmsproxy (bak .bak.20260811i), server active
This commit is contained in:
@ -126,6 +126,16 @@ td .errc { color:var(--err); font-weight:600; }
|
||||
.bar i { display:block; height:100%; background:var(--accent); border-radius:3px; }
|
||||
.bar.ok i { background:var(--ok); }
|
||||
.bar.err i { background:var(--err); }
|
||||
.donut-wrap { display:flex; align-items:center; gap:22px; padding:6px 2px; flex-wrap:wrap; }
|
||||
.donut { position:relative; width:150px; height:150px; flex:0 0 auto; }
|
||||
.donut svg { transform:rotate(-90deg); }
|
||||
.donut .center { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; }
|
||||
.donut .center b { font-size:24px; font-weight:800; }
|
||||
.donut .center span { font-size:11px; color:var(--muted); }
|
||||
.donut-legend { display:flex; flex-direction:column; gap:7px; min-width:130px; }
|
||||
.donut-legend .dl { display:flex; align-items:center; gap:8px; font-size:12.5px; cursor:help; }
|
||||
.donut-legend .dl i { width:11px; height:11px; border-radius:3px; flex:0 0 auto; }
|
||||
.donut-legend .dl b { margin-left:auto; font-variant-numeric:tabular-nums; }
|
||||
table th { white-space:nowrap; }
|
||||
.recs-scroll { max-height:420px; overflow:auto; }
|
||||
.recs-scroll table th { position:sticky; top:0; background:var(--card); z-index:1; }
|
||||
@ -763,13 +773,15 @@ async function paintStats() {
|
||||
function paintModelTable(rows) {
|
||||
const el = $('#tb-model'); if (!el) return;
|
||||
if (!rows.length) { el.innerHTML = `<div class="muted">${t('noUsage')}</div>`; return; }
|
||||
const totalReq = rows.reduce((m, x) => m + x.reqs, 0);
|
||||
const maxReq = rows.reduce((m, x) => Math.max(m, x.reqs), 0);
|
||||
el.innerHTML = `<div class="tbl-wrap"><table><tr><th>${t('thModel')}</th><th class="num">${t('thReqs')}</th><th class="num">${t('thOk')}</th><th class="num">${t('thErr')}</th>
|
||||
<th class="num">${t('thPrompt')}</th><th class="num">${t('thCompl')}</th><th class="num">${t('thAvgLat')}</th><th class="num">${t('thMaxLat')}</th></tr>` +
|
||||
rows.map(r => {
|
||||
const maxReq = rows.reduce((m, x) => Math.max(m, x.reqs), 0);
|
||||
const sharePct = totalReq ? (r.reqs * 100 / totalReq).toFixed(1) : '0.0';
|
||||
const w = maxReq ? Math.max(6, r.reqs * 100 / maxReq) : 0;
|
||||
return `<tr><td>${esc(r.name)}</td>
|
||||
<td class="num"><span class="mini">${w.toFixed(0)}%</span><div class="bar"><i style="width:${w}%"></i></div>${fmtN(r.reqs)}</td>
|
||||
<td class="num"><span class="mini" title="${r.reqs} of ${totalReq} requests">${sharePct}%</span><div class="bar"><i style="width:${w}%"></i></div>${fmtN(r.reqs)}</td>
|
||||
<td class="num okc">${fmtN(r.ok)}</td><td class="num errc">${fmtN(r.err)}</td>
|
||||
<td class="num">${fmtTok(r.prompt_tokens)}</td><td class="num">${fmtTok(r.completion_tokens)}</td>
|
||||
<td class="num">${fmtMs(fmtLat(r.latency_sum_ms, r.reqs))}</td><td class="num">${fmtMs(r.latency_max_ms)}</td></tr>`;
|
||||
@ -788,9 +800,34 @@ function paintSrcTable(rows) {
|
||||
function paintStatusTable(rows) {
|
||||
const el = $('#tb-status'); if (!el) return;
|
||||
if (!rows.length) { el.innerHTML = `<div class="muted">${t('noUsage')}</div>`; return; }
|
||||
el.innerHTML = `<div class="tbl-wrap"><table><tr><th>${t('thCode')}</th><th class="num">${t('thReqs')}</th><th class="num">${t('thOk')}</th><th class="num">${t('thErr')}</th></tr>` +
|
||||
rows.map(r => `<tr><td><b class="${+r.name >= 400 ? 'errc' : 'okc'}">${esc(r.name)}</b></td>
|
||||
<td class="num">${fmtN(r.reqs)}</td><td class="num okc">${fmtN(r.ok)}</td><td class="num errc">${fmtN(r.err)}</td></tr>`).join('') + '</table></div>';
|
||||
const total = rows.reduce((m, x) => m + x.reqs, 0) || 1;
|
||||
// stable 2xx/3xx -> green, 4xx/5xx -> red-ish, but keep each code its own slice.
|
||||
const palette = ['#3fb27f', '#4fa3d8', '#f0a45c', '#e06c6c', '#b06ce0', '#5cc8d0', '#c9cc4f', '#e08c5c', '#8a9bb5', '#7fbf6f'];
|
||||
let acc = 0;
|
||||
const slices = rows.map((r, i) => {
|
||||
const frac = r.reqs / total;
|
||||
const start = acc;
|
||||
acc += frac * 100;
|
||||
return { ...r, color: palette[i % palette.length], start, end: acc };
|
||||
});
|
||||
const R = 66, C = 2 * Math.PI * R;
|
||||
const arcs = slices.map(s => {
|
||||
const len = (s.end - s.start) / 100 * C;
|
||||
const dash = `${Math.max(len, 0.5)} ${C - Math.max(len, 0.5)}`;
|
||||
const off = -s.start / 100 * C;
|
||||
return `<circle cx="70" cy="70" r="${R}" fill="none" stroke="${s.color}" stroke-width="22"
|
||||
stroke-dasharray="${dash}" stroke-dashoffset="${off}" stroke-linecap="butt">
|
||||
<title>${esc(s.name)}: ${fmtN(s.reqs)} (${(s.reqs / total * 100).toFixed(1)}%) · ${t('thOk')} ${fmtN(s.ok)} · ${t('thErr')} ${fmtN(s.err)}</title></circle>`;
|
||||
}).join('');
|
||||
const legend = slices.map(s => `<div class="dl" title="${esc(s.name)} ${t('thReqs')} ${fmtN(s.reqs)} (${(s.reqs / total * 100).toFixed(1)}%) · ${t('thOk')} ${fmtN(s.ok)} · ${t('thErr')} ${fmtN(s.err)}">
|
||||
<i style="background:${s.color}"></i><span>${esc(s.name)}</span><b>${fmtN(s.reqs)}</b></div>`).join('');
|
||||
el.innerHTML = `<div class="donut-wrap">
|
||||
<div class="donut">
|
||||
<svg width="140" height="140" viewBox="0 0 140 140">${arcs}</svg>
|
||||
<div class="center"><b>${fmtN(total)}</b><span>${t('thReqs')}</span></div>
|
||||
</div>
|
||||
<div class="donut-legend">${legend}</div>
|
||||
</div>`;
|
||||
}
|
||||
function paintKeyTable(rows, keyNames) {
|
||||
const el = $('#tb-key'); if (!el) return;
|
||||
|
||||
Reference in New Issue
Block a user