mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 08:57:57 +00:00
fix(ui): chat model dropdown shows source (src:model pin), tokens card stacked bar + swatch legend, tokens in/out on two lines
- chat tab model select: list each model grouped by source as 'source · model' with value 'source:model' (the unambiguous pin syntax — all 18 real models contain '-' but none contain ':'), so same-named models on different sources are distinguishable in tests; falls back to flat model list for non-admin (sources not exposed) - tokens card: remove on-chart text, add color-swatch legend (#tb-tokens-legend) mapping bar color -> model; in/out tokens on separate lines (.k-sub.k2) - deployed (bak .bak.20260811r), server active
This commit is contained in:
@ -108,6 +108,7 @@ pre.configbox { background:var(--card2); border:1px solid var(--line); border-ra
|
||||
.kpi .k-val { font-size:26px; font-weight:800; margin-top:4px; font-variant-numeric:tabular-nums; letter-spacing:.3px; }
|
||||
.kpi .k-val .u { font-size:13px; font-weight:600; color:var(--muted); margin-left:3px; }
|
||||
.kpi .k-sub { font-size:11.5px; color:var(--muted); margin-top:4px; }
|
||||
.kpi .k-sub.k2 { display:flex; flex-direction:column; align-items:flex-start; line-height:1.5; }
|
||||
.kpi .k-chart { margin-top:8px; position:relative; height:86px; }
|
||||
.kpi canvas { width:100%; height:86px; display:block; }
|
||||
.kpi.kpi-status .k-chart-pie { height:110px; }
|
||||
@ -781,8 +782,9 @@ async function paintStats() {
|
||||
<div class="kpi">
|
||||
<div class="k-hdr"><span class="k-lab">${t('kpiTokens')}</span></div>
|
||||
<div class="k-val">${fmtTok(tot.tokens)}</div>
|
||||
<div class="k-sub">${t('thPrompt')} ${fmtTok(tot.prompt_tokens)} · ${t('thCompl')} ${fmtTok(tot.completion_tokens)}</div>
|
||||
<div class="k-sub k2"><span>${t('thPrompt')} ${fmtTok(tot.prompt_tokens)}</span><span>${t('thCompl')} ${fmtTok(tot.completion_tokens)}</span></div>
|
||||
<div class="k-chart"><canvas id="cv-tokens"></canvas></div>
|
||||
<div class="status-legend" id="tb-tokens-legend"></div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="k-hdr"><span class="k-lab">${t('kpiLat')}</span></div>
|
||||
@ -919,6 +921,7 @@ function drawReqsChart(st) {
|
||||
const counts = Array.from(byDay.values()).slice(-7);
|
||||
dailyBars(ctx, counts, rect.width, 86, '#3fb27f');
|
||||
}
|
||||
const tokPalette = ['#3fb27f','#4fa3d8','#f0a45c','#b06ce0'];
|
||||
function drawTokensChart(models) {
|
||||
const cv = document.getElementById('cv-tokens'); if (!cv) return;
|
||||
const ctx = cv.getContext('2d');
|
||||
@ -926,22 +929,20 @@ function drawTokensChart(models) {
|
||||
const parent = cv.parentElement, rect = parent.getBoundingClientRect();
|
||||
cv.width = rect.width * dpr; cv.height = 86 * dpr; ctx.scale(dpr, dpr);
|
||||
ctx.clearRect(0, 0, cv.width, cv.height);
|
||||
// top-4 models by tokens as a stacked bar (share ~= pie) + legend line
|
||||
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;
|
||||
const palette = ['#3fb27f','#4fa3d8','#f0a45c','#b06ce0'];
|
||||
let x = 0;
|
||||
ctx.fillRect;
|
||||
rows.forEach((m,i) => {
|
||||
const w = Math.max(2, m.tokens / tot * rect.width);
|
||||
ctx.fillStyle = palette[i%palette.length];
|
||||
ctx.fillStyle = tokPalette[i%tokPalette.length];
|
||||
ctx.fillRect(x, 0, w, 86);
|
||||
x += w;
|
||||
});
|
||||
ctx.fillStyle = 'rgba(226,232,240,.85)';
|
||||
ctx.font = '10px ui-monospace,Menlo,Consolas,monospace';
|
||||
ctx.textAlign = 'left'; ctx.textBaseline = 'bottom';
|
||||
ctx.fillText((rows[0]||{}).name ? '@' + rows[0].name + ' ' + fmtTok(rows[0].tokens) : '', 3, 54);
|
||||
// color-swatch legend instead of on-chart text
|
||||
const lg = document.getElementById('tb-tokens-legend');
|
||||
if (lg) lg.innerHTML = rows.map((m,i) =>
|
||||
`<span class="lg" title="${esc(m.name)}: ${fmtTok(m.tokens)} (${(m.tokens/tot*100).toFixed(1)}%)">
|
||||
<i style="background:${tokPalette[i%tokPalette.length]}"></i>${esc(m.name)}<b>${fmtTok(m.tokens)}</b></span>`).join('');
|
||||
}
|
||||
function drawLatencyChart(st) {
|
||||
const cv = document.getElementById('cv-lat'); if (!cv) return;
|
||||
@ -1025,7 +1026,18 @@ let chatHistory = [];
|
||||
let chatImages = [];
|
||||
async function renderChat() {
|
||||
const s = await api('/api/status');
|
||||
const opts = ['AUTO', ...(s.models || [])].map(m => `<option value="${escAttr(m)}">${esc(m)}</option>`).join('');
|
||||
// Prefer per-source options ("source:model" routing) when the caller sees
|
||||
// sources (admin); otherwise fall back to the flat model list (user scope).
|
||||
let optList;
|
||||
if (Array.isArray(s.sources) && s.sources.length) {
|
||||
optList = ['AUTO'];
|
||||
s.sources.forEach(src => (src.models || []).forEach(m => {
|
||||
optList.push(`<option value="${escAttr(`${src.name}:${m}`)}">${esc(`${src.name} · ${m}`)}</option>`);
|
||||
}));
|
||||
} else {
|
||||
optList = ['AUTO'].concat((s.models || []).map(m => `<option value="${escAttr(m)}">${esc(m)}</option>`));
|
||||
}
|
||||
const finalOpts = optList.join('');
|
||||
$('#tab-chat').innerHTML = `
|
||||
<div class="tab-chat">
|
||||
<div class="chat-wrap">
|
||||
@ -1039,7 +1051,7 @@ async function renderChat() {
|
||||
<div class="chat-composer">
|
||||
<div class="chat-tools">
|
||||
<span class="tl">${t('cModel')}</span>
|
||||
<select id="ct-model">${opts}</select>
|
||||
<select id="ct-model">${finalOpts}</select>
|
||||
<span class="grow"></span>
|
||||
<label class="chk"><input id="ct-thinking" type="checkbox" checked>${t('cThinking')}</label>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user