diff --git a/internal/gateway/ui/index.html b/internal/gateway/ui/index.html index 831b7a4..aa76e9a 100644 --- a/internal/gateway/ui/index.html +++ b/internal/gateway/ui/index.html @@ -114,7 +114,7 @@ pre.configbox { background:var(--card2); border:1px solid var(--line); border-ra .tab-pane { animation:tabIn .25s ease; } .chat-log .chat-empty { animation:fadeIn .3s ease; } .kpis { display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:12px; margin-bottom:18px; } -.kpi { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:14px 16px; box-shadow:var(--shadow); display:flex; flex-direction:column; height:206px; } +.kpi { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:14px 16px; box-shadow:var(--shadow); display:flex; flex-direction:column; height:252px; } .kpi .k-hdr { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:6px; flex:0 0 auto; } .kpi .k-lab { font-size:12px; color:var(--muted); font-weight:600; white-space:nowrap; } .kpi .k-val { font-size:26px; font-weight:800; margin-top:4px; font-variant-numeric:tabular-nums; letter-spacing:.3px; flex:0 0 auto; } @@ -123,7 +123,7 @@ pre.configbox { background:var(--card2); border:1px solid var(--line); border-ra .kpi .k-sub.k2 { display:flex; flex-direction:column; align-items:flex-start; line-height:1.5; } .kpi .k-chart { position:relative; flex:1 1 auto; min-height:40px; margin-top:8px; } .kpi canvas { position:absolute; inset:0; width:100%; height:100%; display:block; } -.status-legend { display:flex; flex-wrap:wrap; gap:5px 9px; margin-top:6px; justify-content:center; } +.status-legend { display:flex; flex-wrap:wrap; gap:5px 9px; margin-top:6px; justify-content:center; overflow-y:auto; max-height:66px; scrollbar-width:thin; } .status-legend .lg { display:inline-flex; align-items:center; gap:5px; font-size:11px; color:var(--muted); white-space:nowrap; } .status-legend .lg i { width:9px; height:9px; border-radius:2px; flex:0 0 auto; } .status-legend .lg b { color:var(--fg); font-variant-numeric:tabular-nums; } @@ -522,7 +522,7 @@ const STR = { sortSource:'源', sortPrio:'优先级 %s', sortEmpty:'该源暂无模型', kpiActive:'活跃请求', kpiReqs:'总请求', kpiOk:'成功率', kpiTokens:'Tokens', kpiLat:'平均延迟', kpiMaxLat:'最大延迟', dashModel:'模型用量', dashSrc:'源用量与延迟', dashKey:'密钥用量', dashRecs:'请求记录', exportCsv:'导出 CSV', expWeek:'近一周', expMonth:'近一月', expYear:'近一年', expRange:'自定义范围', expStart:'开始日期', expEnd:'结束日期', expDownload:'下载', expKeysCsv:'导出密钥用量', exitKeyView:'退出该密钥视图', - dashStatus:'状态码分布', thCode:'状态码', statusTag:'状态码分类统计(含 402 欠费 / 400 schema 错误;两者不计入上游退避但单独计数)', + dashStatus:'状态码分布', thCode:'状态码', kOther:'其他', statusTag:'状态码分类统计(含 402 欠费 / 400 schema 错误;两者不计入上游退避但单独计数)', thModel:'模型', thSrc:'源', thKey:'密钥', thReqs:'请求', thOk:'成功', thErr:'失败', thPrompt:'输入 Tokens', thCompl:'输出 Tokens', thAvgLat:'平均延迟', thMaxLat:'最长延迟', thTime:'时间', thType:'类型', thStatus:'状态', thLatMs:'延迟', @@ -578,7 +578,7 @@ kMeTitle:'My key', kMeRole:'Role', kMeModels:'Models I can use', kMeHint:'Keys c sortSource:'source', sortPrio:'priority %s', sortEmpty:'no models in this source', kpiActive:'Active requests', kpiReqs:'Requests', kpiOk:'Success rate', kpiTokens:'Tokens', kpiLat:'Avg latency', kpiMaxLat:'Max latency', dashModel:'Model usage', dashSrc:'Source usage & latency', dashKey:'Key usage', dashRecs:'Request records', exportCsv:'Export CSV', expWeek:'Last week', expMonth:'Last month', expYear:'Last year', expRange:'Custom range', expStart:'Start date', expEnd:'End date', expDownload:'Download', expKeysCsv:'Export key usage', exitKeyView:'Exit this key view', - dashStatus:'Status codes', thCode:'Code', statusTag:'Per-status aggregates — 402 quota / 400 schema errors are counted here but never back off the provider', + dashStatus:'Status codes', thCode:'Code', kOther:'other', statusTag:'Per-status aggregates — 402 quota / 400 schema errors are counted here but never back off the provider', thModel:'Model', thSrc:'Source', thKey:'Key', thReqs:'Requests', thOk:'OK', thErr:'Err', thPrompt:'Prompt Tokens', thCompl:'Completion Tokens', thAvgLat:'Avg latency', thMaxLat:'Max latency', thTime:'Time', thType:'Type', thStatus:'Status', thLatMs:'Latency', @@ -962,18 +962,25 @@ function drawTokensChart(models) { const H = rect.height || 86; cv.width = rect.width * dpr; cv.height = H * dpr; ctx.scale(dpr, dpr); ctx.clearRect(0, 0, cv.width, cv.height); - const rows = (models||[]).slice().sort((a,b)=>b.tokens-a.tokens).slice(0,4); - const tot = rows.reduce((a,x)=>a+x.tokens,0) || 1; + // all non-zero models sorted desc; total uses the full set so shares are true + const all = (models||[]).filter(m => (m.tokens||0) > 0).sort((a,b)=>b.tokens-a.tokens); + if (!all.length) return; + const grand = all.reduce((a,x)=>a+x.tokens,0); + // keep the top 4 as visible slices; lump the rest into "other" + const top = all.slice(0,4); + const rest = all.slice(4).reduce((a,x)=>a+x.tokens,0); + const slices = rest > 0 ? top.concat([{ name: '… ' + t('kOther'), tokens: rest, _other: true }]) : top; + const tot = grand; let x = 0; - rows.forEach((m,i) => { + slices.forEach((m,i) => { const w = Math.max(2, m.tokens / tot * rect.width); ctx.fillStyle = tokPalette[i%tokPalette.length]; ctx.fillRect(x, 0, w, H); x += w; }); - // color-swatch legend instead of on-chart text + // legend: list each individual model (not aggregated) that has tokens const lg = document.getElementById('tb-tokens-legend'); - if (lg) lg.innerHTML = rows.map((m,i) => + if (lg) lg.innerHTML = all.map((m,i) => ` ${esc(m.name)}${fmtTok(m.tokens)}`).join(''); }