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('kpiTokens')}
${fmtTok(tot.tokens)}
-
${t('thPrompt')} ${fmtTok(tot.prompt_tokens)} · ${t('thCompl')} ${fmtTok(tot.completion_tokens)}
+
${t('thPrompt')} ${fmtTok(tot.prompt_tokens)}${t('thCompl')} ${fmtTok(tot.completion_tokens)}
+
${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() {
${t('cModel')} - +