diff --git a/internal/gateway/ui/index.html b/internal/gateway/ui/index.html
index b59f1d0..b5db78d 100644
--- a/internal/gateway/ui/index.html
+++ b/internal/gateway/ui/index.html
@@ -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() {
${t('kpiLat')}
@@ -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) =>
+ `
+ ${esc(m.name)}${fmtTok(m.tokens)}`).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 => `
`).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(`
`);
+ }));
+ } else {
+ optList = ['AUTO'].concat((s.models || []).map(m => `
`));
+ }
+ const finalOpts = optList.join('');
$('#tab-chat').innerHTML = `
@@ -1039,7 +1051,7 @@ async function renderChat() {