@krtclcdy/tersio 2.15.0 → 2.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -18
- package/cli/common.js +17 -10
- package/cli/dashboard.js +4 -0
- package/cli/doctor.js +3 -0
- package/cli/install.js +109 -111
- package/cli/rtk-wiring.js +66 -8
- package/cli/update.js +54 -42
- package/cli/usage.js +3 -0
- package/dashboard/app.js +220 -55
- package/dashboard/styles.css +15 -4
- package/dashboard/template.html +29 -9
- package/extensions/shared/usage-ledger.js +17 -5
- package/package.json +1 -1
package/dashboard/app.js
CHANGED
|
@@ -164,7 +164,9 @@
|
|
|
164
164
|
});
|
|
165
165
|
})();
|
|
166
166
|
|
|
167
|
+
var OPENAI_SVG = '<svg viewBox="0 0 24 24" fill="#000" aria-hidden="true"><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>';
|
|
167
168
|
var PROVIDERS = [
|
|
169
|
+
[/openai|codex|gpt-|o1/i, 'OpenAI', 'openai', '#fff', OPENAI_SVG],
|
|
168
170
|
[/muse/i, 'Meta', 'meta', '#0082fb'],
|
|
169
171
|
[/deepseek/i, 'DeepSeek', 'deepseek', '#4d6bfe'],
|
|
170
172
|
[/qwen|qwq/i, 'Qwen', 'qwen', '#6950EF'],
|
|
@@ -175,21 +177,27 @@
|
|
|
175
177
|
[/nemotron|nvidia/i, 'NVIDIA', 'nvidia', '#76b900'],
|
|
176
178
|
[/mistral/i, 'Mistral', 'mistralai', '#ff7000'],
|
|
177
179
|
[/claude|anthropic/i, 'Anthropic', 'anthropic', '#d97757'],
|
|
178
|
-
[/gemini|google/i, 'Google', 'googlegemini', '#
|
|
180
|
+
[/gemini|google/i, 'Google', 'googlegemini', '#8E75B2'],
|
|
179
181
|
];
|
|
180
182
|
function vendorOf(model) {
|
|
181
183
|
for (var i = 0; i < PROVIDERS.length; i++) {
|
|
182
|
-
if (PROVIDERS[i][0].test(model)) return { name: PROVIDERS[i][1], slug: PROVIDERS[i][2], color: PROVIDERS[i][3] };
|
|
184
|
+
if (PROVIDERS[i][0].test(model)) return { name: PROVIDERS[i][1], slug: PROVIDERS[i][2], color: PROVIDERS[i][3], svg: PROVIDERS[i][4] };
|
|
183
185
|
}
|
|
184
186
|
return { name: 'Other', slug: '', color: '#71717a' };
|
|
185
187
|
}
|
|
186
|
-
// Brand glyph
|
|
187
|
-
//
|
|
188
|
+
// Brand glyph: inline SVG when the provider ships one (OpenAI), else the
|
|
189
|
+
// Simple Icons CDN; unknown / Other providers render a clean Lucide
|
|
190
|
+
// sparkles icon with no solid background as a placeholder.
|
|
188
191
|
function brandHTML(v, small) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
if (v.svg) {
|
|
193
|
+
return '<span class="brandmark' + (small ? ' sm' : '') + '" style="background:' + v.color + '" title="' + v.name + '">' + v.svg + '</span>';
|
|
194
|
+
}
|
|
195
|
+
if (!v.slug) {
|
|
196
|
+
return '<span class="brandmark placeholder' + (small ? ' sm' : '') + '" title="' + v.name + '">' +
|
|
197
|
+
'<i data-lucide="sparkles" class="fb" style="display:grid"></i></span>';
|
|
198
|
+
}
|
|
199
|
+
var icon = '<img src="https://cdn.simpleicons.org/' + v.slug + '/white" alt="" loading="lazy" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'grid\';">';
|
|
200
|
+
var fb = '<i data-lucide="sparkles" class="fb"></i>';
|
|
193
201
|
return '<span class="brandmark' + (small ? ' sm' : '') + '" style="background:' + v.color + '" title="' + v.name + '">' + icon + fb + '</span>';
|
|
194
202
|
}
|
|
195
203
|
var PALETTE = ['#34d399', '#818cf8', '#22d3ee', '#fbbf24', '#f472b6', '#a78bfa', '#fb923c', '#2dd4bf'];
|
|
@@ -204,6 +212,24 @@
|
|
|
204
212
|
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
|
|
205
213
|
return Math.floor(s / 86400) + 'd ago';
|
|
206
214
|
}
|
|
215
|
+
// Per-request generation time + output throughput from the message-level
|
|
216
|
+
// `duration` (ms). Missing → '–'. Throughput uses output tokens since
|
|
217
|
+
// that is what streams to the user during the measured window.
|
|
218
|
+
function fmtDur(ms) {
|
|
219
|
+
if (ms === undefined) return '–';
|
|
220
|
+
var s = ms / 1000;
|
|
221
|
+
return (s < 10 ? s.toFixed(1) : Math.round(s)) + 's';
|
|
222
|
+
}
|
|
223
|
+
function speedText(r) {
|
|
224
|
+
if (r.d === undefined) return '–';
|
|
225
|
+
var tps = r.d > 0 ? r.o / (r.d / 1000) : 0;
|
|
226
|
+
return fmtDur(r.d) + ' ⚡' + (tps < 10 ? tps.toFixed(1) : Math.round(tps)) + '/s';
|
|
227
|
+
}
|
|
228
|
+
function speedTitle(r) {
|
|
229
|
+
if (r.d === undefined) return 'no duration recorded';
|
|
230
|
+
var tps = r.d > 0 ? r.o / (r.d / 1000) : 0;
|
|
231
|
+
return '⏱ ' + (r.d / 1000).toFixed(1) + 's elapsed with the model, ⚡ ' + r.o + ' output tokens (' + tps.toFixed(1) + ' tok/s)';
|
|
232
|
+
}
|
|
207
233
|
function modelTotal(byModel, m) {
|
|
208
234
|
var b = byModel[m] || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
209
235
|
return b.input + b.output + b.cacheRead + b.cacheWrite;
|
|
@@ -234,6 +260,7 @@
|
|
|
234
260
|
function showTip(html, x, y) {
|
|
235
261
|
var t = tip();
|
|
236
262
|
t.innerHTML = html;
|
|
263
|
+
if (window.lucide) lucide.createIcons();
|
|
237
264
|
t.classList.add('show');
|
|
238
265
|
moveTip(x, y);
|
|
239
266
|
}
|
|
@@ -265,19 +292,80 @@
|
|
|
265
292
|
var denom = b.input + b.cacheRead;
|
|
266
293
|
var hit = denom ? (b.cacheRead / denom * 100).toFixed(1) + '%' : '-';
|
|
267
294
|
var total = b.input + b.output + b.cacheRead + b.cacheWrite;
|
|
268
|
-
var
|
|
269
|
-
h
|
|
270
|
-
h += '<div class="tr"><span class="sw" style="background:var(--
|
|
295
|
+
var cb = ((DATA.byModelBucketUsd || {})[m]) || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
296
|
+
var h = '<div class="tt">' + shortName(m) + '</div><div class="tv">' + fmtShort(total) + ' total (' + fxMoney(cb.input + cb.output + cb.cacheRead + cb.cacheWrite) + ')</div>';
|
|
297
|
+
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">input</span><span class="tvr">' + fmt(b.input) + ' (' + fxMoney(cb.input) + ')</span></div>';
|
|
298
|
+
h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">output</span><span class="tvr">' + fmt(b.output) + ' (' + fxMoney(cb.output) + ')</span></div>';
|
|
299
|
+
if (b.cacheRead > 0) h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">cache read</span><span class="tvr">' + fmt(b.cacheRead) + ' (' + fxMoney(cb.cacheRead) + ')</span></div>';
|
|
300
|
+
if (b.cacheWrite > 0) h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">cache write</span><span class="tvr">' + fmt(b.cacheWrite) + ' (' + fxMoney(cb.cacheWrite) + ')</span></div>';
|
|
271
301
|
h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">requests</span><span class="tvr">' + fmt(req) + '</span></div>';
|
|
272
302
|
h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">cache hit</span><span class="tvr">' + hit + '</span></div>';
|
|
273
303
|
return h;
|
|
274
304
|
}
|
|
305
|
+
function stampLocal(ts) {
|
|
306
|
+
var d = new Date(ts);
|
|
307
|
+
function p(n) { return String(n).padStart(2, '0'); }
|
|
308
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
|
|
309
|
+
}
|
|
310
|
+
function tipRecentHTML(r) {
|
|
311
|
+
var tps = r.d !== undefined && r.d > 0 ? r.o / (r.d / 1000) : 0;
|
|
312
|
+
var h = '<div class="tt">' + shortName(r.m) + '</div><div class="tv">' + stampLocal(r.t) + '</div>';
|
|
313
|
+
h += '<div class="tr"><span class="sw" style="background:#fb923c"></span><span class="tn">input</span><span class="tvr">' + fmt(r.i) + '</span></div>';
|
|
314
|
+
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">output</span><span class="tvr">' + fmt(r.o) + '</span></div>';
|
|
315
|
+
if ((r.cr || 0) > 0) h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">cache</span><span class="tvr">' + fmtShort(r.cr) + '</span></div>';
|
|
316
|
+
if ((r.cw || 0) > 0) h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">cache write</span><span class="tvr">' + fmtShort(r.cw) + '</span></div>';
|
|
317
|
+
h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">elapsed</span><span class="tvr">' + fmtDur(r.d) + '</span></div>';
|
|
318
|
+
h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">speed</span><span class="tvr">' + (r.d !== undefined ? (tps < 10 ? tps.toFixed(1) : Math.round(tps)) + ' tok/s' : '–') + '</span></div>';
|
|
319
|
+
return h;
|
|
320
|
+
}
|
|
321
|
+
// Zone indicators: Lucide face per savings-bento metric. Coarse by design;
|
|
322
|
+
// captions say est. where modeled. Icons paint via showTip/render createIcons.
|
|
323
|
+
function zoneIcon(glyph, cls) { return '<i data-lucide="' + glyph + '" class="zface ' + cls + '"></i>'; }
|
|
324
|
+
function co2Zone(g) {
|
|
325
|
+
if (!(g > 0)) return ['minus', 'no data', ''];
|
|
326
|
+
if (g <= 50) return ['sprout', 'light', 'good'];
|
|
327
|
+
if (g <= 500) return ['smile', 'moderate', 'good'];
|
|
328
|
+
if (g <= 2000) return ['meh', 'heavy', 'warn'];
|
|
329
|
+
return ['flame', 'very high', 'bad'];
|
|
330
|
+
}
|
|
331
|
+
function levZone(x) {
|
|
332
|
+
if (!(x > 0)) return ['minus', 'no savings yet', ''];
|
|
333
|
+
if (x >= 3) return ['rocket', 'high leverage', 'good'];
|
|
334
|
+
if (x >= 1) return ['smile', 'solid', 'good'];
|
|
335
|
+
return ['meh', 'light', 'warn'];
|
|
336
|
+
}
|
|
337
|
+
function shareZone(p) {
|
|
338
|
+
if (!(p > 0)) return ['minus', 'uncached', ''];
|
|
339
|
+
if (p >= 80) return ['rocket', 'high', 'good'];
|
|
340
|
+
if (p >= 50) return ['smile', 'good', 'good'];
|
|
341
|
+
return ['meh', 'low', 'warn'];
|
|
342
|
+
}
|
|
275
343
|
function tipCo2HTML() {
|
|
344
|
+
var z = co2Zone(DATA.co2g || 0);
|
|
276
345
|
var h = '<div class="tt">CO2</div><div class="tv">~' + (DATA.co2g || 0).toFixed(1) + 'g est.</div>';
|
|
346
|
+
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">zone</span><span class="tvr">' + zoneIcon(z[0], z[2]) + ' ' + z[1] + '</span></div>';
|
|
277
347
|
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">energy</span><span class="tvr">~' + (DATA.energyWh || 0).toFixed(1) + ' Wh</span></div>';
|
|
278
|
-
h
|
|
279
|
-
|
|
280
|
-
|
|
348
|
+
return h;
|
|
349
|
+
}
|
|
350
|
+
function tipSavedHTML() {
|
|
351
|
+
var usd = DATA.usd || 0, saved = DATA.savedUsd || 0;
|
|
352
|
+
var lev = usd ? saved / usd : 0;
|
|
353
|
+
var z = levZone(lev);
|
|
354
|
+
var t = DATA.tokens || { cacheRead: 0 };
|
|
355
|
+
var h = '<div class="tt">Saved by cache</div><div class="tv">' + fxMoney(saved) + ' est.</div>';
|
|
356
|
+
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">zone</span><span class="tvr">' + zoneIcon(z[0], z[2]) + ' ' + z[1] + '</span></div>';
|
|
357
|
+
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">leverage</span><span class="tvr">x' + lev.toFixed(1) + ' per $1</span></div>';
|
|
358
|
+
h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">cache read</span><span class="tvr">' + fmtShort(t.cacheRead || 0) + '</span></div>';
|
|
359
|
+
return h;
|
|
360
|
+
}
|
|
361
|
+
function tipShareHTML(share, pp) {
|
|
362
|
+
var z = shareZone(share);
|
|
363
|
+
var t = DATA.tokens || { cacheRead: 0, cacheWrite: 0 };
|
|
364
|
+
var h = '<div class="tt">Cache share</div><div class="tv">' + share + '%</div>';
|
|
365
|
+
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">zone</span><span class="tvr">' + zoneIcon(z[0], z[2]) + ' ' + z[1] + '</span></div>';
|
|
366
|
+
h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">read</span><span class="tvr">' + fmtShort(t.cacheRead || 0) + '</span></div>';
|
|
367
|
+
if ((t.cacheWrite || 0) > 0) h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">write</span><span class="tvr">' + fmtShort(t.cacheWrite) + '</span></div>';
|
|
368
|
+
h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">7d trend</span><span class="tvr">' + (pp >= 0 ? '+' : '') + pp.toFixed(1) + 'pp</span></div>';
|
|
281
369
|
return h;
|
|
282
370
|
}
|
|
283
371
|
function hoverModel(el, m) {
|
|
@@ -285,6 +373,11 @@
|
|
|
285
373
|
el.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
|
|
286
374
|
el.addEventListener('mouseleave', hideTip);
|
|
287
375
|
}
|
|
376
|
+
function hoverRecent(el, r) {
|
|
377
|
+
el.addEventListener('mouseenter', function(ev) { showTip(tipRecentHTML(r), ev.clientX, ev.clientY); });
|
|
378
|
+
el.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
|
|
379
|
+
el.addEventListener('mouseleave', hideTip);
|
|
380
|
+
}
|
|
288
381
|
function dayModelRows(key) {
|
|
289
382
|
var per = (DATA.byDayModel || {})[key] || {};
|
|
290
383
|
var tops = topModels(DATA.byModel || {}, 8), rows = [], other = 0;
|
|
@@ -455,9 +548,10 @@
|
|
|
455
548
|
var p = Math.round((cur - prev) / prev * 100);
|
|
456
549
|
return (p >= 0 ? '+' : '') + p + '%';
|
|
457
550
|
}
|
|
551
|
+
var modelPage = 1, modelPer = 10, recentPage = 1, recentPer = 15;
|
|
458
552
|
function renderModels() {
|
|
459
553
|
var byModel = DATA.byModel || {};
|
|
460
|
-
var tops = topModels(byModel,
|
|
554
|
+
var tops = topModels(byModel, Object.keys(byModel).length).filter(function(m) { return modelTotal(byModel, m) > 0; });
|
|
461
555
|
var cards = document.getElementById('modelCards');
|
|
462
556
|
cards.innerHTML = '';
|
|
463
557
|
if (!tops.length) cards.innerHTML = '<div class="empty md:col-span-3">' + emptyState('boxes', 'No models yet', 'Model token totals will appear here once sessions report tokens.') + '</div>';
|
|
@@ -465,12 +559,23 @@
|
|
|
465
559
|
var v = vendorOf(m), d = wow(m);
|
|
466
560
|
var card = document.createElement('div');
|
|
467
561
|
card.className = 'modelcard relative p-4 rounded-xl overflow-hidden';
|
|
468
|
-
if (v.
|
|
562
|
+
if (v.svg) {
|
|
563
|
+
var ghostSvg = document.createElement('span');
|
|
564
|
+
ghostSvg.className = 'ghostimg'; ghostSvg.setAttribute('aria-hidden', 'true');
|
|
565
|
+
ghostSvg.innerHTML = v.svg;
|
|
566
|
+
card.appendChild(ghostSvg);
|
|
567
|
+
} else if (v.slug) {
|
|
469
568
|
var ghost = document.createElement('img');
|
|
470
569
|
ghost.className = 'ghostimg'; ghost.alt = '';
|
|
471
570
|
ghost.src = 'https://cdn.simpleicons.org/' + v.slug + '/white';
|
|
472
571
|
ghost.setAttribute('aria-hidden', 'true');
|
|
473
572
|
card.appendChild(ghost);
|
|
573
|
+
} else {
|
|
574
|
+
var ghostSparkle = document.createElement('i');
|
|
575
|
+
ghostSparkle.setAttribute('data-lucide', 'sparkles');
|
|
576
|
+
ghostSparkle.className = 'ghosticon';
|
|
577
|
+
ghostSparkle.setAttribute('aria-hidden', 'true');
|
|
578
|
+
card.appendChild(ghostSparkle);
|
|
474
579
|
}
|
|
475
580
|
card.style.cssText = 'border: 1px solid var(--line); background: var(--panel)';
|
|
476
581
|
var head = document.createElement('p');
|
|
@@ -502,10 +607,13 @@
|
|
|
502
607
|
ol.innerHTML = '';
|
|
503
608
|
var max = 1;
|
|
504
609
|
tops.forEach(function(m) { max = Math.max(max, modelTotal(byModel, m)); });
|
|
505
|
-
|
|
610
|
+
var modelPages = Math.max(1, Math.ceil(tops.length / modelPer));
|
|
611
|
+
if (modelPage > modelPages) modelPage = modelPages;
|
|
612
|
+
var modelStart = (modelPage - 1) * modelPer;
|
|
613
|
+
tops.slice(modelStart, modelStart + modelPer).forEach(function(m, i) {
|
|
506
614
|
var v = vendorOf(m);
|
|
507
615
|
var li = document.createElement('li');
|
|
508
|
-
li.className = 'mrow flex items-center gap-3 px-4 py-3' + (i < tops.length - 1 ? ' rowline' : '');
|
|
616
|
+
li.className = 'mrow flex items-center gap-3 px-4 py-3' + (i < Math.min(tops.length - modelStart, modelPer) - 1 ? ' rowline' : '');
|
|
509
617
|
var badgeWrap = document.createElement('span');
|
|
510
618
|
badgeWrap.innerHTML = brandHTML(v, true);
|
|
511
619
|
var badge = badgeWrap.firstChild;
|
|
@@ -527,6 +635,7 @@
|
|
|
527
635
|
ol.appendChild(li);
|
|
528
636
|
});
|
|
529
637
|
document.getElementById('modelsCount').textContent = tops.length ? tops.length + ' models' : '';
|
|
638
|
+
paintPager('modelPages', 'modelRange', modelPage, modelPer, tops.length, function(p) { modelPage = p; renderModels(); });
|
|
530
639
|
ol.style.display = tops.length ? '' : 'none';
|
|
531
640
|
document.getElementById('emptyModels').classList.toggle('hidden', tops.length > 0);
|
|
532
641
|
if (tops.length && 'IntersectionObserver' in window && !reduce) {
|
|
@@ -616,26 +725,31 @@
|
|
|
616
725
|
g.commands ? fmt(tools) + ' tools · ' + fmt(g.commands) + ' commands · ' + fmtShort(g.saved) + ' saved' : 'tool calls in sessions';
|
|
617
726
|
paintCmdPager(pages);
|
|
618
727
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
var
|
|
728
|
+
// Shared page-number painter: prev/next plus windowed numbers with
|
|
729
|
+
// ellipsis. onPick(page) rerenders the owning card. Same controls as the
|
|
730
|
+
// Command tools pager; Models and Recent reuse it verbatim.
|
|
731
|
+
function paintPager(wrapId, rangeId, page, per, total, onPick) {
|
|
732
|
+
var start = total ? (page - 1) * per + 1 : 0;
|
|
733
|
+
document.getElementById(rangeId).textContent =
|
|
734
|
+
'Showing ' + start + '–' + Math.min(total, page * per) + ' of ' + total;
|
|
735
|
+
var wrap = document.getElementById(wrapId);
|
|
624
736
|
wrap.innerHTML = '';
|
|
625
|
-
|
|
737
|
+
var pages = Math.max(1, Math.ceil(total / per));
|
|
738
|
+
if (page > pages) onPick(pages);
|
|
739
|
+
function btn(label, next, opts) {
|
|
626
740
|
var b = document.createElement('button');
|
|
627
741
|
b.type = 'button'; b.className = 'pgbtn mono' + (opts && opts.on ? ' on' : '');
|
|
628
742
|
b.textContent = label;
|
|
629
743
|
if (opts && opts.dim) b.setAttribute('aria-label', opts.dim);
|
|
630
744
|
if (opts && opts.off) b.disabled = true;
|
|
631
|
-
else b.addEventListener('click', function() {
|
|
745
|
+
else b.addEventListener('click', function() { onPick(next); });
|
|
632
746
|
wrap.appendChild(b);
|
|
633
747
|
return b;
|
|
634
748
|
}
|
|
635
|
-
btn('‹',
|
|
749
|
+
btn('‹', page - 1, { dim: 'Previous page', off: page <= 1 });
|
|
636
750
|
var nums = [];
|
|
637
751
|
for (var p = 1; p <= pages; p++) {
|
|
638
|
-
if (p === 1 || p === pages || Math.abs(p -
|
|
752
|
+
if (p === 1 || p === pages || Math.abs(p - page) <= 1) nums.push(p);
|
|
639
753
|
else if (nums[nums.length - 1] !== '…') nums.push('…');
|
|
640
754
|
}
|
|
641
755
|
nums.forEach(function(p) {
|
|
@@ -643,9 +757,27 @@
|
|
|
643
757
|
var s = document.createElement('span');
|
|
644
758
|
s.textContent = '…'; s.style.padding = '0 2px';
|
|
645
759
|
wrap.appendChild(s);
|
|
646
|
-
} else btn(String(p), p, { on: p ===
|
|
760
|
+
} else btn(String(p), p, { on: p === page });
|
|
761
|
+
});
|
|
762
|
+
btn('›', page + 1, { dim: 'Next page', off: page >= pages });
|
|
763
|
+
}
|
|
764
|
+
function paintCmdPager(pages) {
|
|
765
|
+
var total = cmdRows.length;
|
|
766
|
+
if (cmdPage > pages) cmdPage = pages;
|
|
767
|
+
paintPager('cmdPages', 'cmdRange', cmdPage, cmdPer, total, function(p) { cmdPage = p; renderCmd(); });
|
|
768
|
+
}
|
|
769
|
+
function bindPerPage(sel, get, set, render) {
|
|
770
|
+
Array.prototype.forEach.call(document.querySelectorAll(sel + ' [data-pp]'), function(btn) {
|
|
771
|
+
if (btn.dataset.bound) return;
|
|
772
|
+
btn.dataset.bound = '1';
|
|
773
|
+
btn.addEventListener('click', function() {
|
|
774
|
+
if (Number(btn.dataset.pp) === get()) return;
|
|
775
|
+
Array.prototype.forEach.call(document.querySelectorAll(sel + ' [data-pp]'), function(b) { b.classList.remove('on'); });
|
|
776
|
+
btn.classList.add('on');
|
|
777
|
+
set(Number(btn.dataset.pp));
|
|
778
|
+
render();
|
|
779
|
+
});
|
|
647
780
|
});
|
|
648
|
-
btn('›', cmdPage + 1, { dim: 'Next page', off: cmdPage >= pages });
|
|
649
781
|
}
|
|
650
782
|
function bindCmdTable() {
|
|
651
783
|
if (bindCmdTable.done) return;
|
|
@@ -659,17 +791,10 @@
|
|
|
659
791
|
renderCmd();
|
|
660
792
|
});
|
|
661
793
|
});
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
btn.classList.add('on');
|
|
666
|
-
cmdPer = Number(btn.dataset.pp);
|
|
667
|
-
cmdPage = 1;
|
|
668
|
-
renderCmd();
|
|
669
|
-
});
|
|
670
|
-
});
|
|
794
|
+
bindPerPage('#cmdPager', function() { return cmdPer; }, function(n) { cmdPer = n; cmdPage = 1; }, renderCmd);
|
|
795
|
+
bindPerPage('section[aria-label="All models"]', function() { return modelPer; }, function(n) { modelPer = n; modelPage = 1; }, renderModels);
|
|
796
|
+
bindPerPage('section[aria-label="Recent requests"]', function() { return recentPer; }, function(n) { recentPer = n; recentPage = 1; }, renderRecent);
|
|
671
797
|
}
|
|
672
|
-
|
|
673
798
|
function fmtMs(ms) {
|
|
674
799
|
if (ms < 1000) return Math.round(ms) + 'ms';
|
|
675
800
|
return (ms / 1000).toFixed(1) + 's';
|
|
@@ -783,7 +908,19 @@
|
|
|
783
908
|
if (pel && pair[1]) { pel.textContent = pair[1]; pel.title = pair[1]; }
|
|
784
909
|
});
|
|
785
910
|
countUp(document.getElementById('saved'), d.savedUsd || 0, true);
|
|
786
|
-
|
|
911
|
+
(function() {
|
|
912
|
+
var lev = (d.usd || 0) ? (d.savedUsd || 0) / d.usd : 0;
|
|
913
|
+
var z = levZone(lev);
|
|
914
|
+
var note = document.getElementById('usdNote');
|
|
915
|
+
note.innerHTML = ((d.priced ? 'per-model price table' : 'incl. default pricing') + (fx.live ? ' · fx live' : ' · fx snapshot') + ' · ') + zoneIcon(z[0], z[2]) + ' ' + z[1];
|
|
916
|
+
note.title = 'Cache leverage x' + lev.toFixed(1) + ' — cache savings per $1 spent, vs full input price';
|
|
917
|
+
})();
|
|
918
|
+
(function() {
|
|
919
|
+
var z = co2Zone(d.co2g || 0);
|
|
920
|
+
var el = document.getElementById('co2');
|
|
921
|
+
el.innerHTML = '~' + (d.co2g || 0).toFixed(1) + 'g ' + zoneIcon(z[0], z[2]);
|
|
922
|
+
el.title = z[1] + ' footprint (est.)';
|
|
923
|
+
})();
|
|
787
924
|
(function() {
|
|
788
925
|
var el = document.getElementById('co2');
|
|
789
926
|
var card = el ? el.closest('div.rounded-xl') : null;
|
|
@@ -793,16 +930,39 @@
|
|
|
793
930
|
card.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
|
|
794
931
|
card.addEventListener('mouseleave', hideTip);
|
|
795
932
|
})();
|
|
933
|
+
(function() {
|
|
934
|
+
var el = document.getElementById('saved');
|
|
935
|
+
var card = el ? el.closest('div.rounded-xl') : null;
|
|
936
|
+
if (!card || card.dataset.savetip) return;
|
|
937
|
+
card.dataset.savetip = '1';
|
|
938
|
+
card.addEventListener('mouseenter', function(ev) { showTip(tipSavedHTML(), ev.clientX, ev.clientY); });
|
|
939
|
+
card.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
|
|
940
|
+
card.addEventListener('mouseleave', hideTip);
|
|
941
|
+
})();
|
|
796
942
|
var share = total ? Math.round((t.cacheRead + t.cacheWrite) / total * 100) : 0;
|
|
797
|
-
|
|
943
|
+
var shareZ = shareZone(share);
|
|
944
|
+
document.getElementById('cacheShare').innerHTML = share + '% ' + zoneIcon(shareZ[0], shareZ[2]);
|
|
945
|
+
document.getElementById('cacheShare').title = shareZ[1] + ' cache share';
|
|
798
946
|
var shareOf = function(b) { var s = dayTotal(b); return s ? ((b.cacheRead || 0) + (b.cacheWrite || 0)) / s * 100 : 0; };
|
|
799
947
|
var keys = Object.keys(byDay).sort(), cPct = 0, pPct = 0, cn = 0, pn = 0;
|
|
800
948
|
keys.slice(-7).forEach(function(k) { cPct += shareOf(byDay[k]); cn++; });
|
|
801
949
|
keys.slice(-14, -7).forEach(function(k) { pPct += shareOf(byDay[k]); pn++; });
|
|
802
950
|
var pp = (cn ? cPct / cn : 0) - (pn ? pPct / pn : 0);
|
|
803
951
|
var cd = document.getElementById('cacheDelta');
|
|
804
|
-
|
|
805
|
-
|
|
952
|
+
var cs = document.getElementById('cacheShare');
|
|
953
|
+
cs.dataset.share = String(share);
|
|
954
|
+
cs.dataset.pp = pp.toFixed(1);
|
|
955
|
+
(function() {
|
|
956
|
+
var el = document.getElementById('cacheShare');
|
|
957
|
+
var card = el ? el.closest('div.rounded-xl') : null;
|
|
958
|
+
if (!card || card.dataset.sharetip) return;
|
|
959
|
+
card.dataset.sharetip = '1';
|
|
960
|
+
card.addEventListener('mouseenter', function(ev) {
|
|
961
|
+
showTip(tipShareHTML(Number(el.dataset.share || 0), Number(el.dataset.pp || 0)), ev.clientX, ev.clientY);
|
|
962
|
+
});
|
|
963
|
+
card.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
|
|
964
|
+
card.addEventListener('mouseleave', hideTip);
|
|
965
|
+
})();
|
|
806
966
|
|
|
807
967
|
(function() {
|
|
808
968
|
var el = document.getElementById('ticker');
|
|
@@ -833,33 +993,38 @@
|
|
|
833
993
|
}
|
|
834
994
|
observe();
|
|
835
995
|
}
|
|
836
|
-
|
|
837
996
|
function renderRecent() {
|
|
838
997
|
var body = document.getElementById('recent');
|
|
839
998
|
body.innerHTML = '';
|
|
840
|
-
var
|
|
999
|
+
var all = DATA.recent || [];
|
|
1000
|
+
var recentPages = Math.max(1, Math.ceil(all.length / recentPer));
|
|
1001
|
+
if (recentPage > recentPages) recentPage = recentPages;
|
|
1002
|
+
var recentStart = (recentPage - 1) * recentPer;
|
|
1003
|
+
var rows = all.slice(recentStart, recentStart + recentPer);
|
|
841
1004
|
rows.forEach(function(r, i) {
|
|
842
1005
|
var v = vendorOf(r.m);
|
|
843
1006
|
var tr = document.createElement('tr');
|
|
844
|
-
tr.className = 'rrow' + (i <
|
|
1007
|
+
tr.className = 'rrow' + (i < Math.min(all.length - recentStart, recentPer) - 1 ? ' rowline' : '');
|
|
845
1008
|
var dot = '<span style="display:inline-block;width:8px;height:8px;border-radius:99px;background:' + v.color + ';margin-right:8px"></span>';
|
|
846
|
-
tr.innerHTML = '<td class="py-2.5 pr-3 truncate" style="
|
|
1009
|
+
tr.innerHTML = '<td class="py-2.5 pr-3 truncate" style="min-width: 0"></td>' +
|
|
1010
|
+
'<td class="text-right py-2.5 pr-3 whitespace-nowrap"></td>' +
|
|
847
1011
|
'<td class="text-right py-2.5 pr-3 whitespace-nowrap"></td>' +
|
|
848
1012
|
'<td class="text-right py-2.5 whitespace-nowrap" style="color: var(--dim)"></td>';
|
|
849
1013
|
var tds = tr.children;
|
|
850
1014
|
tds[0].innerHTML = dot + '<span></span>';
|
|
851
1015
|
tds[0].querySelector('span:last-child').textContent = r.m;
|
|
852
|
-
tds[0].title = r.m;
|
|
853
1016
|
tds[1].innerHTML = '<span style="color:#fb923c"></span> <span style="color:var(--accent)"></span>';
|
|
854
|
-
tds[1].children[0].textContent = fmt(r.i) + '
|
|
855
|
-
tds[1].children[1].textContent = fmt(r.o) + '
|
|
856
|
-
tds[
|
|
857
|
-
tds[
|
|
1017
|
+
tds[1].children[0].textContent = fmt(r.i) + '↑';
|
|
1018
|
+
tds[1].children[1].textContent = fmt(r.o) + '↓';
|
|
1019
|
+
tds[2].textContent = speedText(r);
|
|
1020
|
+
tds[3].textContent = relTime(r.t);
|
|
1021
|
+
hoverRecent(tr, r);
|
|
858
1022
|
body.appendChild(tr);
|
|
859
1023
|
});
|
|
860
|
-
document.getElementById('recentCount').textContent =
|
|
861
|
-
|
|
862
|
-
document.getElementById('
|
|
1024
|
+
document.getElementById('recentCount').textContent = all.length ? all.length + ' requests' : '';
|
|
1025
|
+
paintPager('recentPages', 'recentRange', recentPage, recentPer, all.length, function(p) { recentPage = p; renderRecent(); });
|
|
1026
|
+
document.getElementById('recentTable').style.display = all.length ? '' : 'none';
|
|
1027
|
+
document.getElementById('emptyRecent').classList.toggle('hidden', all.length > 0);
|
|
863
1028
|
}
|
|
864
1029
|
|
|
865
1030
|
var seen = new WeakSet();
|
package/dashboard/styles.css
CHANGED
|
@@ -58,7 +58,11 @@
|
|
|
58
58
|
.pill { display: inline-flex; align-items: center; gap: 4px; border-radius: 999px; padding: 3px 10px; font-size: 11px; white-space: nowrap; }
|
|
59
59
|
.pill.good { background: var(--accent-soft); color: var(--accent); }
|
|
60
60
|
.pill.bad { background: rgba(248, 113, 113, .12); color: #f87171; }
|
|
61
|
-
.
|
|
61
|
+
.zface { display: inline-block; vertical-align: -2px; width: 14px; height: 14px; }
|
|
62
|
+
.zface.good { color: var(--accent); }
|
|
63
|
+
.zface.warn { color: #fbbf24; }
|
|
64
|
+
.zface.bad { color: #f87171; }
|
|
65
|
+
#tip .tvr .zface { vertical-align: -3px; }
|
|
62
66
|
.fxbtn { background: var(--track); color: var(--ink); border: 0; cursor: pointer; gap: 6px; }
|
|
63
67
|
.fxbtn:focus-visible { outline: 1px solid var(--accent); }
|
|
64
68
|
.fxbtn svg { transition: transform .15s; }
|
|
@@ -117,25 +121,32 @@
|
|
|
117
121
|
.cell.l1 { background: var(--cell-1); } .cell.l2 { background: var(--cell-2); }
|
|
118
122
|
.cell.l3 { background: var(--cell-3); } .cell.l4 { background: var(--cell-4); }
|
|
119
123
|
.brandmark { width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; flex-shrink: 0; color: #fff; }
|
|
124
|
+
.brandmark.placeholder { background: transparent !important; border: 1px solid var(--line); color: var(--dim); }
|
|
120
125
|
.brandmark img { width: 18px; height: 18px; }
|
|
126
|
+
.brandmark svg { width: 18px; height: 18px; display: block; }
|
|
121
127
|
.brandmark.sm { width: 28px; height: 28px; border-radius: 8px; }
|
|
122
128
|
.brandmark.sm img { width: 14px; height: 14px; }
|
|
129
|
+
.brandmark.sm svg { width: 14px; height: 14px; }
|
|
123
130
|
.brandmark .fb { display: none; width: 18px; height: 18px; }
|
|
124
131
|
.brandmark.sm .fb { width: 14px; height: 14px; }
|
|
125
132
|
.ghostimg { position: absolute; right: -6px; bottom: -10px; width: 88px; height: 88px;
|
|
126
133
|
opacity: .08; pointer-events: none; user-select: none; }
|
|
134
|
+
span.ghostimg { display: grid; place-items: center; }
|
|
135
|
+
span.ghostimg svg { width: 72px; height: 72px; display: block; }
|
|
136
|
+
.ghosticon { position: absolute; right: 4px; bottom: -6px; width: 72px; height: 72px;
|
|
137
|
+
opacity: .06; pointer-events: none; user-select: none; color: var(--dim); }
|
|
127
138
|
@media (prefers-color-scheme: light) {
|
|
128
139
|
:root:not([data-theme="dark"]) .ghostimg { opacity: .06; filter: brightness(0); }
|
|
129
140
|
}
|
|
130
141
|
html[data-theme="light"] .ghostimg { opacity: .06; filter: brightness(0); }
|
|
131
|
-
#tip { position: fixed; z-index: 100; pointer-events: none; min-width:
|
|
142
|
+
#tip { position: fixed; z-index: 100; pointer-events: none; min-width: 250px; max-width: 370px;
|
|
132
143
|
background: var(--panel);
|
|
133
144
|
border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px;
|
|
134
145
|
box-shadow: var(--shadow-xs); opacity: 0; transition: opacity .15s; }
|
|
135
146
|
#tip.show { opacity: 1; }
|
|
136
147
|
#tip .tt { font-size: 11px; text-transform: uppercase; letter-spacing: .14em; font-weight: 700; }
|
|
137
148
|
#tip .tv { font-size: 13px; margin: 2px 0 8px; }
|
|
138
|
-
#tip .tr { display: flex; align-items: center; gap:
|
|
149
|
+
#tip .tr { display: flex; align-items: center; gap: 10px; font-size: 12px; padding: 3px 0; }
|
|
139
150
|
#tip .sw { width: 9px; height: 9px; border-radius: 2.5px; flex-shrink: 0; }
|
|
140
151
|
#tip .tn { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
141
152
|
.scrollarea { overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
|
|
@@ -145,7 +156,7 @@
|
|
|
145
156
|
.rrow { transition: background .2s; cursor: pointer; }
|
|
146
157
|
.rrow:hover { box-shadow: var(--shadow-xs); }
|
|
147
158
|
#recentTable thead th { position: sticky; top: 0; background: var(--panel); z-index: 1; }
|
|
148
|
-
#tip .tvr { flex-shrink: 0; }
|
|
159
|
+
#tip .tvr { flex-shrink: 0; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
149
160
|
.modelcard { transition: background .25s; }
|
|
150
161
|
.modelcard:hover { box-shadow: var(--shadow-xs); }
|
|
151
162
|
|
package/dashboard/template.html
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
<div class="dlg-head">
|
|
47
47
|
<div class="min-w-0">
|
|
48
48
|
<p class="dlg-title" id="settingsTitle">Settings</p>
|
|
49
|
-
<p class="dlg-desc">Appearance, data, and where statistics live
|
|
49
|
+
<p class="dlg-desc">Appearance, data, and where statistics live. <span id="verChip" class="mono">tersio</span></p>
|
|
50
50
|
</div>
|
|
51
51
|
<button id="settingsClose" class="btn-push flex shrink-0 items-center p-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Close settings">
|
|
52
52
|
<i data-lucide="x" class="size-4"></i>
|
|
@@ -206,34 +206,45 @@
|
|
|
206
206
|
|
|
207
207
|
<!-- Models + recent: twin cards -->
|
|
208
208
|
<div class="rise grid grid-cols-12 grid-flow-dense gap-3 mt-8">
|
|
209
|
-
<section class="col-span-12 lg:col-span-
|
|
209
|
+
<section class="col-span-12 lg:col-span-6 rounded-xl p-5 overflow-hidden flex flex-col" style="border: 1px solid var(--line); background: var(--panel)" aria-label="All models">
|
|
210
210
|
<div class="flex items-center gap-2 mb-1">
|
|
211
211
|
<i data-lucide="layers" class="size-4" style="color: var(--accent)"></i>
|
|
212
212
|
<h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Models</h2>
|
|
213
213
|
<span class="mono text-xs ml-auto truncate shrink-0" style="color: var(--dim)" id="modelsCount"></span>
|
|
214
214
|
</div>
|
|
215
|
-
<p class="mono text-xs mb-4" style="color: var(--dim)">ranked by tokens / top
|
|
216
|
-
<div class="scrollarea"
|
|
215
|
+
<p class="mono text-xs mb-4" style="color: var(--dim)">ranked by tokens / top 10</p>
|
|
216
|
+
<div class="scrollarea"><ol id="models" class="overflow-hidden"></ol></div>
|
|
217
|
+
<div class="mono text-xs mt-4 pt-4 flex flex-wrap items-center gap-x-4 gap-y-3 mt-auto" style="color: var(--dim); border-top: 1px solid var(--line)">
|
|
218
|
+
<span id="modelRange">Showing 0 of 0</span>
|
|
219
|
+
<span class="seg flex items-center gap-1 p-1 rounded-[10px]" style="border: 1px solid var(--line)" role="group" aria-label="Models per page">
|
|
220
|
+
<button data-pp="10" class="on px-2.5 py-1" role="button">10</button>
|
|
221
|
+
<button data-pp="15" class="px-2.5 py-1" role="button">15</button>
|
|
222
|
+
<button data-pp="25" class="px-2.5 py-1" role="button">25</button>
|
|
223
|
+
<button data-pp="50" class="px-2.5 py-1" role="button">50</button>
|
|
224
|
+
</span>
|
|
225
|
+
<span class="ml-auto flex items-center gap-1" id="modelPages" role="navigation" aria-label="Model pages"></span>
|
|
226
|
+
</div>
|
|
217
227
|
<div id="emptyModels" class="empty hidden">
|
|
218
228
|
<span class="empty-icon"><i data-lucide="boxes" class="size-5"></i></span>
|
|
219
229
|
<p class="empty-title">No models yet</p>
|
|
220
230
|
<p class="empty-desc">Model token totals will appear here once sessions report tokens.</p>
|
|
221
231
|
</div>
|
|
222
232
|
</section>
|
|
223
|
-
<section class="col-span-12 lg:col-span-
|
|
233
|
+
<section class="col-span-12 lg:col-span-6 rounded-xl p-5 overflow-hidden flex flex-col" style="border: 1px solid var(--line); background: var(--panel)" aria-label="Recent requests">
|
|
224
234
|
<div class="flex items-center gap-2 mb-1">
|
|
225
235
|
<i data-lucide="history" class="size-4" style="color: var(--accent)"></i>
|
|
226
236
|
<h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Recent requests</h2>
|
|
227
237
|
<span class="mono text-xs ml-auto truncate shrink-0" style="color: var(--dim)" id="recentCount"></span>
|
|
228
238
|
</div>
|
|
229
|
-
<p class="mono text-xs mb-4 truncate" style="color: var(--dim)">latest assistant messages</p>
|
|
230
|
-
<div class="scrollarea overflow-x-auto"
|
|
239
|
+
<p class="mono text-xs mb-4 truncate" style="color: var(--dim)" title="Time = elapsed time with the model; Speed = output tokens per second">latest assistant messages · ⏱ elapsed · ⚡ tok/s</p>
|
|
240
|
+
<div class="scrollarea overflow-x-auto">
|
|
231
241
|
<table class="w-full mono text-[13px]" id="recentTable">
|
|
232
|
-
<colgroup><col><col style="width:
|
|
242
|
+
<colgroup><col><col style="width:120px"><col style="width:110px"><col style="width:80px"></colgroup>
|
|
233
243
|
<thead>
|
|
234
244
|
<tr class="text-left text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">
|
|
235
245
|
<th class="font-normal py-2 pr-3">Model</th>
|
|
236
246
|
<th class="font-normal text-right py-2 pr-3">In / Out</th>
|
|
247
|
+
<th class="font-normal text-right py-2 pr-3" title="Elapsed time with the model / output token speed">Time</th>
|
|
237
248
|
<th class="font-normal text-right py-2">When</th>
|
|
238
249
|
</tr>
|
|
239
250
|
</thead>
|
|
@@ -245,6 +256,16 @@
|
|
|
245
256
|
<p class="empty-title">No requests yet</p>
|
|
246
257
|
<p class="empty-desc">Recent assistant messages will show here once sessions report tokens.</p>
|
|
247
258
|
</div>
|
|
259
|
+
<div class="mono text-xs mt-4 pt-4 flex flex-wrap items-center gap-x-4 gap-y-3 mt-auto" style="color: var(--dim); border-top: 1px solid var(--line)">
|
|
260
|
+
<span id="recentRange">Showing 0 of 0</span>
|
|
261
|
+
<span class="seg flex items-center gap-1 p-1 rounded-[10px]" style="border: 1px solid var(--line)" role="group" aria-label="Requests per page">
|
|
262
|
+
<button data-pp="10" class="px-2.5 py-1" role="button">10</button>
|
|
263
|
+
<button data-pp="15" class="on px-2.5 py-1" role="button">15</button>
|
|
264
|
+
<button data-pp="25" class="px-2.5 py-1" role="button">25</button>
|
|
265
|
+
<button data-pp="50" class="px-2.5 py-1" role="button">50</button>
|
|
266
|
+
</span>
|
|
267
|
+
<span class="ml-auto flex items-center gap-1" id="recentPages" role="navigation" aria-label="Recent pages"></span>
|
|
268
|
+
</div>
|
|
248
269
|
</section>
|
|
249
270
|
</div>
|
|
250
271
|
|
|
@@ -294,7 +315,6 @@
|
|
|
294
315
|
<footer class="mono text-xs mt-12 pt-4" style="color: var(--dim); border-top: 1px solid var(--line)">
|
|
295
316
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-3">
|
|
296
317
|
<span>© 2026 Tersio. KurutoDenzeru. All rights reserved.</span>
|
|
297
|
-
<span class="pill flat" id="verChip">tersio</span>
|
|
298
318
|
<span class="ml-auto flex items-center gap-1" style="color: var(--ink)">
|
|
299
319
|
<a href="https://github.com/KurutoDenzeru/tersio" target="_blank" rel="noopener" aria-label="GitHub" class="btn-push social grid size-8 place-items-center rounded-lg"><img height="16" width="16" src="https://cdn.jsdelivr.net/npm/simple-icons@v16/icons/github.svg" alt="GitHub"></a>
|
|
300
320
|
<a href="https://linkedin.com/in/kurtcalacday/" target="_blank" rel="noopener" aria-label="LinkedIn" class="btn-push social grid size-8 place-items-center rounded-lg"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg></a>
|