@krtclcdy/tersio 2.21.0 → 2.22.1

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/dashboard/app.js DELETED
@@ -1,1495 +0,0 @@
1
- (function() {
2
- var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
3
- var hasGsap = !reduce && typeof window.gsap !== 'undefined';
4
- var hasST = hasGsap && typeof window.ScrollTrigger !== 'undefined';
5
- if (hasST) gsap.registerPlugin(ScrollTrigger);
6
-
7
- function fmt(n) { return Number(n).toLocaleString('en-US'); }
8
- function fmtShort(n) {
9
- if (n >= 1e12) return (n / 1e12).toFixed(1) + 'T';
10
- if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
11
- if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
12
- if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
13
- return String(Math.round(n));
14
- }
15
-
16
- var CURS = {
17
- USD: { s: '$', d: 2 }, PHP: { s: '₱', d: 2 }, EUR: { s: '€', d: 2 }, GBP: { s: '£', d: 2 },
18
- JPY: { s: '¥', d: 0 }, KRW: { s: '₩', d: 0 }, SGD: { s: 'S$', d: 2 }, AUD: { s: 'A$', d: 2 },
19
- CAD: { s: 'C$', d: 2 }, INR: { s: '₹', d: 2 }
20
- };
21
- var FLAGS = { USD: '🇺🇸', PHP: '🇵🇭', EUR: '🇪🇺', GBP: '🇬🇧', JPY: '🇯🇵', KRW: '🇰🇷', SGD: '🇸🇬', AUD: '🇦🇺', CAD: '🇨🇦', INR: '🇮🇳' };
22
- // ponytail: static snapshot; live rates from frankfurter replace it when reachable
23
- var fx = { cur: 'USD', rates: { USD: 1, PHP: 58.7, EUR: 0.92, GBP: 0.79, JPY: 149.8, KRW: 1385, SGD: 1.34, AUD: 1.52, CAD: 1.37, INR: 88.2 }, live: false };
24
- try {
25
- var savedCur = localStorage.getItem('tersio-fx-cur');
26
- if (savedCur && CURS[savedCur]) fx.cur = savedCur;
27
- var savedFx = JSON.parse(localStorage.getItem('tersio-fx') || 'null');
28
- if (savedFx && savedFx.rates && savedFx.rates.USD === 1) fx.rates = savedFx.rates;
29
- } catch (e) { }
30
- // Magnitude-aware decimals. Model prices keep falling, so a flat 2dp rounds
31
- // real spend down to "$0.00"; small amounts keep enough digits to stay
32
- // readable (0.000187, not 0). Larger amounts use the currency's own scale.
33
- function moneyDecimals(v, base) {
34
- var a = Math.abs(v);
35
- if (a >= 0.1 || a === 0) return base;
36
- if (a >= 0.001) return Math.max(base, 4);
37
- if (a >= 0.00001) return Math.max(base, 6);
38
- return Math.max(base, 8);
39
- }
40
- function fxMoney(v) {
41
- var c = CURS[fx.cur] || CURS.USD, r = fx.rates[fx.cur] || 1, amt = v * r;
42
- var d = moneyDecimals(amt, c.d);
43
- return c.s + amt.toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d });
44
- }
45
- // Tooltips build innerHTML, and error notes come from providers, so they are
46
- // never interpolated raw.
47
- function esc(s) {
48
- return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
49
- }
50
-
51
- function countUp(el, target, money) {
52
- function text(v) { return money ? fxMoney(v) : fmt(Math.round(v)); }
53
- if (!hasGsap || target <= 0) { el.textContent = text(target); return; }
54
- var o = { v: 0 };
55
- gsap.to(o, { v: target, duration: 1.1, ease: 'power3.out', onUpdate: function() { el.textContent = text(o.v); } });
56
- }
57
-
58
- function tick() {
59
- var c = document.getElementById('clock');
60
- if (c) c.textContent = new Date().toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
61
- }
62
- tick(); setInterval(tick, 1000);
63
- function currentTheme() {
64
- try { return localStorage.getItem('tersio-theme') || 'system'; } catch (e) { return 'system'; }
65
- }
66
- function paintThemeTabs() {
67
- var v = currentTheme();
68
- Array.prototype.forEach.call(document.querySelectorAll('[data-theme-val]'), function(b) {
69
- b.classList.toggle('on', b.getAttribute('data-theme-val') === v);
70
- });
71
- }
72
- function applyTheme(v) {
73
- if (v === 'system') document.documentElement.removeAttribute('data-theme');
74
- else document.documentElement.setAttribute('data-theme', v);
75
- try { localStorage.setItem('tersio-theme', v); } catch (e) { }
76
- paintThemeTabs();
77
- }
78
- applyTheme(currentTheme());
79
- (function initRail() {
80
- var rail = document.getElementById('rail');
81
- if (!rail || initRail.done) return;
82
- initRail.done = true;
83
- var N = 24, ticks = [];
84
- for (var i = 0; i < N; i++) {
85
- (function(i) {
86
- var b = document.createElement('button');
87
- b.type = 'button'; b.tabIndex = -1; b.setAttribute('aria-hidden', 'true');
88
- b.addEventListener('click', function() {
89
- var max = document.documentElement.scrollHeight - window.innerHeight;
90
- window.scrollTo({ top: max * (i / (N - 1)), behavior: 'smooth' });
91
- });
92
- rail.appendChild(b); ticks.push(b);
93
- })(i);
94
- }
95
- var queued = false;
96
- function paint() {
97
- queued = false;
98
- var max = document.documentElement.scrollHeight - window.innerHeight;
99
- var f = max > 0 ? window.scrollY / max : 0;
100
- ticks.forEach(function(b, j) { b.classList.toggle('lit', j / (N - 1) <= f); });
101
- }
102
- window.addEventListener('scroll', function() {
103
- if (!queued) { queued = true; requestAnimationFrame(paint); }
104
- }, { passive: true });
105
- paint();
106
- })();
107
- (function initFx() {
108
- var btn = document.getElementById('fxBtn'), panel = document.getElementById('fxPanel'), cur = document.getElementById('fxCur');
109
- var keys = Object.keys(CURS), active = -1;
110
- function paint() {
111
- cur.textContent = FLAGS[fx.cur] + ' ' + fx.cur;
112
- Array.prototype.forEach.call(panel.children, function(o) {
113
- o.setAttribute('aria-selected', o.dataset.cur === fx.cur ? 'true' : 'false');
114
- });
115
- }
116
- function setCur(k) {
117
- fx.cur = k; active = keys.indexOf(k);
118
- try { localStorage.setItem('tersio-fx-cur', fx.cur); } catch (e) { }
119
- // Served mode: also persist server-side so close → reopen keeps the
120
- // choice. Every `tersio gain` run is a fresh ephemeral port (a new
121
- // origin), so localStorage alone cannot survive a restart.
122
- if (window.location.protocol !== 'file:' && typeof fetch === 'function') {
123
- try {
124
- fetch('currency', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ currency: k }) }).catch(function() { });
125
- } catch (e) { }
126
- }
127
- paint();
128
- if (DATA) render(DATA);
129
- }
130
- function open(show) {
131
- var willOpen = show === undefined ? panel.classList.contains('hidden') : show;
132
- panel.classList.toggle('hidden', !willOpen);
133
- btn.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
134
- if (willOpen) mark(keys.indexOf(fx.cur));
135
- }
136
- function mark(i) {
137
- active = (i + keys.length) % keys.length;
138
- Array.prototype.forEach.call(panel.children, function(o, j) {
139
- o.classList.toggle('active', j === active);
140
- });
141
- var el = panel.children[active];
142
- if (el) el.focus();
143
- }
144
- keys.forEach(function(k) {
145
- var o = document.createElement('button');
146
- o.type = 'button'; o.className = 'fxopt mono'; o.dataset.cur = k;
147
- o.setAttribute('role', 'option');
148
- o.innerHTML = '<span>' + FLAGS[k] + ' ' + k + '</span><svg class="tick" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
149
- o.addEventListener('click', function() { setCur(k); open(false); btn.focus(); });
150
- o.addEventListener('mousemove', function() { mark(keys.indexOf(k)); });
151
- panel.appendChild(o);
152
- });
153
- paint();
154
- btn.addEventListener('click', function() { open(); });
155
- btn.addEventListener('keydown', function(ev) {
156
- if (ev.key === 'ArrowDown' || ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); open(true); }
157
- });
158
- panel.addEventListener('keydown', function(ev) {
159
- if (ev.key === 'Escape') { open(false); btn.focus(); }
160
- else if (ev.key === 'ArrowDown') { ev.preventDefault(); mark(active + 1); }
161
- else if (ev.key === 'ArrowUp') { ev.preventDefault(); mark(active - 1); }
162
- else if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); setCur(keys[active]); open(false); btn.focus(); }
163
- else if (ev.key === 'Tab') open(false);
164
- });
165
- document.addEventListener('click', function(ev) {
166
- if (!panel.classList.contains('hidden') && !btn.contains(ev.target) && !panel.contains(ev.target)) open(false);
167
- });
168
- if (typeof fetch !== 'function' || typeof AbortController === 'undefined') return;
169
- var ctl = new AbortController();
170
- var to = setTimeout(function() { ctl.abort(); }, 4000);
171
- fetch('https://api.frankfurter.app/latest?from=USD', { signal: ctl.signal }).then(function(r) { return r.json(); }).then(function(j) {
172
- clearTimeout(to);
173
- if (!j || !j.rates) return;
174
- var ok = Object.keys(CURS).every(function(k) { return k === 'USD' || typeof j.rates[k] === 'number'; });
175
- if (!ok) return;
176
- fx.rates = Object.assign({ USD: 1 }, j.rates); fx.live = true;
177
- try { localStorage.setItem('tersio-fx', JSON.stringify({ rates: fx.rates, ts: Date.now() })); } catch (e) { }
178
- if (DATA) render(DATA);
179
- }).catch(function() { clearTimeout(to); });
180
- })();
181
- Array.prototype.forEach.call(document.querySelectorAll('[data-theme-val]'), function(b) {
182
- b.addEventListener('click', function() { applyTheme(b.getAttribute('data-theme-val')); });
183
- });
184
- (function() {
185
- var sysMedia = window.matchMedia('(prefers-color-scheme: dark)');
186
- if (sysMedia && typeof sysMedia.addEventListener === 'function') sysMedia.addEventListener('change', function() {
187
- if (currentTheme() === 'system') applyTheme('system');
188
- });
189
- })();
190
-
191
- 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>';
192
- var PROVIDERS = [
193
- [/openai|codex|gpt-|o1/i, 'OpenAI', 'openai', '#fff', OPENAI_SVG],
194
- [/muse/i, 'Meta', 'meta', '#0082fb'],
195
- [/deepseek/i, 'DeepSeek', 'deepseek', '#4d6bfe'],
196
- [/qwen|qwq/i, 'Qwen', 'qwen', '#6950EF'],
197
- [/glm|z-ai|zhipu/i, 'Z.ai', 'zdotai', '#2D2D2D'],
198
- [/mimo/i, 'Xiaomi', 'xiaomi', '#ff6900'],
199
- [/kimi|moonshot/i, 'Moonshot', 'kimi', '#a855f7'],
200
- [/minimax/i, 'MiniMax', 'minimax', '#e11d48'],
201
- [/nemotron|nvidia/i, 'NVIDIA', 'nvidia', '#76b900'],
202
- [/mistral/i, 'Mistral', 'mistralai', '#ff7000'],
203
- [/claude|anthropic/i, 'Anthropic', 'anthropic', '#d97757'],
204
- [/gemini|google|gemma/i, 'Google', 'google', '#4285F4'],
205
- ];
206
- function vendorOf(model) {
207
- for (var i = 0; i < PROVIDERS.length; i++) {
208
- if (PROVIDERS[i][0].test(model)) return { name: PROVIDERS[i][1], slug: PROVIDERS[i][2], color: PROVIDERS[i][3], svg: PROVIDERS[i][4] };
209
- }
210
- return { name: 'Other', slug: '', color: '#71717a' };
211
- }
212
- // Brand glyph: inline SVG when the provider ships one (OpenAI), else the
213
- // Simple Icons CDN; unknown / Other providers render a clean Lucide
214
- // sparkles icon with no solid background as a placeholder.
215
- function brandHTML(v, small) {
216
- if (v.svg) {
217
- return '<span class="brandmark' + (small ? ' sm' : '') + '" style="background:' + v.color + '" title="' + v.name + '">' + v.svg + '</span>';
218
- }
219
- if (!v.slug) {
220
- return '<span class="brandmark placeholder' + (small ? ' sm' : '') + '" title="' + v.name + '">' +
221
- '<i data-lucide="sparkles" class="fb" style="display:grid"></i></span>';
222
- }
223
- var icon = '<img src="https://cdn.simpleicons.org/' + v.slug + '/white" alt="" loading="lazy" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'grid\';">';
224
- var fb = '<i data-lucide="sparkles" class="fb"></i>';
225
- return '<span class="brandmark' + (small ? ' sm' : '') + '" style="background:' + v.color + '" title="' + v.name + '">' + icon + fb + '</span>';
226
- }
227
- var PALETTE = ['#34d399', '#818cf8', '#22d3ee', '#fbbf24', '#f472b6', '#a78bfa', '#fb923c', '#2dd4bf'];
228
-
229
- function dayKey(d) {
230
- return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
231
- }
232
- // Absolute local stamp for the When column: "Sep 18, 11:14:08 PM". The
233
- // column is sortable, and a relative label made that ordering unreadable.
234
- function whenStamp(ts) {
235
- var d = new Date(ts);
236
- return d.toLocaleString('en-US', { month: 'short', day: 'numeric' }) + ', ' +
237
- d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', second: '2-digit' });
238
- }
239
- // Per-request generation time + output throughput from the message-level
240
- // `duration` (ms). Missing → '–'. Throughput uses output tokens since
241
- // that is what streams to the user during the measured window.
242
- function fmtDur(ms) {
243
- if (ms === undefined) return '–';
244
- var s = ms / 1000;
245
- return (s < 10 ? s.toFixed(1) : Math.round(s)) + 's';
246
- }
247
- function speedText(r) {
248
- if (r.d === undefined) return '–';
249
- var tps = r.d > 0 ? r.o / (r.d / 1000) : 0;
250
- return fmtDur(r.d) + ' ⚡' + (tps < 10 ? tps.toFixed(1) : Math.round(tps)) + '/s';
251
- }
252
- function speedTitle(r) {
253
- if (r.d === undefined) return 'no duration recorded';
254
- var tps = r.d > 0 ? r.o / (r.d / 1000) : 0;
255
- return '⏱ ' + (r.d / 1000).toFixed(1) + 's elapsed with the model, ⚡ ' + r.o + ' output tokens (' + tps.toFixed(1) + ' tok/s)';
256
- }
257
- function modelTotal(byModel, m) {
258
- var b = byModel[m] || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
259
- return b.input + b.output + b.cacheRead + b.cacheWrite;
260
- }
261
- var DATA = null, cardsShown = false;
262
- function topModels(byModel, n) {
263
- return Object.keys(byModel).sort(function(a, b) { return modelTotal(byModel, b) - modelTotal(byModel, a); }).slice(0, n);
264
- }
265
- // Display form for folded keys: namespace stays lowercase, model segments
266
- // title-case with version dots kept (`deepseek-v4.1-flash` →
267
- // `Deepseek-V4.1-Flash`). Keys arrive folded, so this prettifies only.
268
- function displayModel(m) {
269
- var bare = String(m).replace(/(?::free|-free)$/i, '');
270
- function cap(s) {
271
- var low = s.toLowerCase();
272
- if (low === 'openai') return 'OpenAI';
273
- if (low === 'ai') return 'AI';
274
- if (/^\d+[a-z]+$/.test(low)) return low.toUpperCase();
275
- if (s.length <= 2) return s.toUpperCase();
276
- return s[0].toUpperCase() + s.slice(1).toLowerCase();
277
- }
278
- function seg(s) { return s.split('.').map(cap).join('.'); }
279
- function words(s) { return s.split(/[-_:]+/).filter(Boolean).map(seg).join('-'); }
280
- var slash = bare.indexOf('/');
281
- if (slash >= 0) return bare.slice(0, slash).toLowerCase() + '/' + words(bare.slice(slash + 1));
282
- return words(bare);
283
- }
284
- function allDays() {
285
- var set = {};
286
- Object.keys(DATA.byDay || {}).forEach(function(d) { set[d] = 1; });
287
- Object.keys(DATA.byDayModel || {}).forEach(function(d) { set[d] = 1; });
288
- return Object.keys(set).sort();
289
- }
290
-
291
- var tipEl = null;
292
- function tip() {
293
- if (!tipEl) {
294
- tipEl = document.createElement('div');
295
- tipEl.id = 'tip'; tipEl.className = 'mono'; tipEl.setAttribute('role', 'tooltip');
296
- document.body.appendChild(tipEl);
297
- }
298
- return tipEl;
299
- }
300
- function showTip(html, x, y) {
301
- var t = tip();
302
- t.innerHTML = html;
303
- if (window.lucide) lucide.createIcons();
304
- t.classList.add('show');
305
- moveTip(x, y);
306
- }
307
- function moveTip(x, y) {
308
- if (!tipEl) return;
309
- var w = 300, h = tipEl.offsetHeight || 220;
310
- var lx = x + 16 > window.innerWidth - w ? x - w - 12 : x + 16;
311
- var ly = y + 16 > window.innerHeight - h ? y - h - 12 : y + 16;
312
- tipEl.style.left = Math.max(8, lx) + 'px';
313
- tipEl.style.top = Math.max(8, ly) + 'px';
314
- }
315
- function hideTip() { if (tipEl) tipEl.classList.remove('show'); }
316
- function dateHead(key) {
317
- var dt = new Date(key + 'T12:00:00');
318
- return isNaN(dt) ? key : dt.toLocaleString('en-US', { month: 'short', day: 'numeric' }).toUpperCase();
319
- }
320
- function shortName(m) { var d = displayModel(m); return d.length > 22 ? d.slice(0, 21) + '...' : d; }
321
- function tipDayHTML(key, total, rows) {
322
- var h = '<div class="tt">' + dateHead(key) + '</div><div class="tv">' + fmtShort(total) + ' total</div>';
323
- rows.forEach(function(r) {
324
- h += '<div class="tr"><span class="sw" style="background:' + r[2] + '"></span>' +
325
- '<span class="tn">' + shortName(r[0]) + '</span><span class="tvr">' + fmtShort(r[1]) + '</span></div>';
326
- });
327
- return h;
328
- }
329
- function tipModelHTML(m) {
330
- var b = (DATA.byModel || {})[m] || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
331
- var req = ((DATA.byModelMessages || {})[m]) || 0;
332
- var denom = b.input + b.cacheRead;
333
- var hit = denom ? (b.cacheRead / denom * 100).toFixed(1) + '%' : '-';
334
- var total = b.input + b.output + b.cacheRead + b.cacheWrite;
335
- var cb = ((DATA.byModelBucketUsd || {})[m]) || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
336
- var h = '<div class="tt">' + shortName(m) + '</div><div class="tv">' + fmtShort(total) + ' total (' + fxMoney(cb.input + cb.output + cb.cacheRead + cb.cacheWrite) + ')</div>';
337
- 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>';
338
- 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>';
339
- 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>';
340
- 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>';
341
- h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">requests</span><span class="tvr">' + fmt(req) + '</span></div>';
342
- h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">cache hit</span><span class="tvr">' + hit + '</span></div>';
343
- return h;
344
- }
345
- function stampLocal(ts) {
346
- var d = new Date(ts);
347
- function p(n) { return String(n).padStart(2, '0'); }
348
- return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
349
- }
350
- // Per-request outcome. OMP records stopReason on every assistant turn and,
351
- // on failure, the HTTP errorStatus + errorMessage; toolUse turns are
352
- // ordinary completed turns, so only error and aborted stand apart.
353
- var STATUS_RANK = { error: 0, aborted: 1, completed: 2 };
354
- function statusRank(r) { var k = STATUS_RANK[r.st]; return k === undefined ? 2 : k; }
355
- function statusLabel(r) {
356
- if (r.st === 'error') return 'error' + (r.code ? ' ' + r.code : '');
357
- return r.st === 'aborted' ? 'aborted' : 'completed';
358
- }
359
- function statusColor(r) { return r.st === 'error' ? '#f87171' : r.st === 'aborted' ? '#fbbf24' : 'var(--accent)'; }
360
- // Cost precedence. A recorded charge > 0 is authoritative and shown bare.
361
- // A recording of exactly 0 usually means a free/local provider (every row on
362
- // a local model reports 0) and would blank the column, so those fall back to
363
- // the modeled figure carrying the dashboard's usual "~" estimate marker.
364
- // costRecorded() still reports the raw 0 so the tooltip can show both.
365
- function costRecorded(r) { return typeof r.usd === 'number'; }
366
- function costIsMeasured(r) { return costRecorded(r) && r.usd > 0; }
367
- function displayCost(r) { return costIsMeasured(r) ? r.usd : (r.est || 0); }
368
-
369
- function tipRecentHTML(r) {
370
- var tps = r.d !== undefined && r.d > 0 ? r.o / (r.d / 1000) : 0;
371
- var isEst = !costIsMeasured(r);
372
- var h = '<div class="tt">' + esc(shortName(r.m)) + '</div><div class="tv">' + stampLocal(r.t) + '</div>';
373
- h += '<div class="tr"><span class="sw" style="background:#fb923c"></span><span class="tn">input</span><span class="tvr">' + fmt(r.i) + '</span></div>';
374
- 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>';
375
- if ((r.cr || 0) > 0) h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">cache read</span><span class="tvr">' + fmt(r.cr) + '</span></div>';
376
- 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">' + fmt(r.cw) + '</span></div>';
377
- 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>';
378
- 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>';
379
- h += '<div class="tr"><span class="sw" style="background:' + statusColor(r) + '"></span><span class="tn">status</span><span class="tvr">' + statusLabel(r) + '</span></div>';
380
- h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">cost ' + (isEst ? '(est.)' : '(measured)') + '</span><span class="tvr">' + (isEst ? '~' : '') + fxMoney(displayCost(r)) + '</span></div>';
381
- if (costRecorded(r)) h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">charged</span><span class="tvr">' + fxMoney(r.usd) + '</span></div>';
382
- if (r.note) h += '<div class="tnote">' + esc(r.note) + '</div>';
383
- return h;
384
- }
385
- // Zone indicators: Lucide face per savings-bento metric. Coarse by design;
386
- // captions say est. where modeled. Icons paint via showTip/render createIcons.
387
- function zoneIcon(glyph, cls) { return '<i data-lucide="' + glyph + '" class="zface ' + cls + '"></i>'; }
388
- function co2Zone(g) {
389
- if (!(g > 0)) return ['minus', 'no data', ''];
390
- if (g <= 50) return ['sprout', 'light', 'good'];
391
- if (g <= 500) return ['smile', 'moderate', 'good'];
392
- if (g <= 2000) return ['meh', 'heavy', 'warn'];
393
- return ['flame', 'very high', 'bad'];
394
- }
395
- function levZone(x) {
396
- if (!(x > 0)) return ['minus', 'no savings yet', ''];
397
- if (x >= 3) return ['rocket', 'high leverage', 'good'];
398
- if (x >= 1) return ['smile', 'solid', 'good'];
399
- return ['meh', 'light', 'warn'];
400
- }
401
- function shareZone(p) {
402
- if (!(p > 0)) return ['minus', 'uncached', ''];
403
- if (p >= 80) return ['rocket', 'high', 'good'];
404
- if (p >= 50) return ['smile', 'good', 'good'];
405
- return ['meh', 'low', 'warn'];
406
- }
407
- function tipCo2HTML() {
408
- var z = co2Zone(DATA.co2g || 0);
409
- var h = '<div class="tt">CO2</div><div class="tv">~' + (DATA.co2g || 0).toFixed(1) + 'g est.</div>';
410
- 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>';
411
- 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>';
412
- return h;
413
- }
414
- function tipSavedHTML() {
415
- var usd = DATA.usd || 0, saved = DATA.savedUsd || 0;
416
- var lev = usd ? saved / usd : 0;
417
- var z = levZone(lev);
418
- var t = DATA.tokens || { cacheRead: 0 };
419
- var h = '<div class="tt">Saved by cache</div><div class="tv">' + fxMoney(saved) + ' est.</div>';
420
- 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>';
421
- 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>';
422
- 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>';
423
- return h;
424
- }
425
- function tipShareHTML(share, pp) {
426
- var z = shareZone(share);
427
- var t = DATA.tokens || { cacheRead: 0, cacheWrite: 0 };
428
- var h = '<div class="tt">Cache share</div><div class="tv">' + share + '%</div>';
429
- 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>';
430
- 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>';
431
- 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>';
432
- 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>';
433
- return h;
434
- }
435
- function hoverModel(el, m) {
436
- el.addEventListener('mouseenter', function(ev) { showTip(tipModelHTML(m), ev.clientX, ev.clientY); });
437
- el.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
438
- el.addEventListener('mouseleave', hideTip);
439
- }
440
- function hoverRecent(el, r) {
441
- el.addEventListener('mouseenter', function(ev) { showTip(tipRecentHTML(r), ev.clientX, ev.clientY); });
442
- el.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
443
- el.addEventListener('mouseleave', hideTip);
444
- }
445
- // Model detail dialog: shadcn chart language over daily model volume.
446
- // byDayModel is keyed by DAY (Record<day, Record<model, tokens>>); the old
447
- // mdSeries read it keyed by model and rendered all-zero charts. Area chart
448
- // with gradient + hover crosshair, downsampled rounded bars, token-mix
449
- // donut, KPI strip, 30/90/ALL range tabs.
450
- var mdModel = null, mdSpan = 'all', mdHover = { pts: [], days: [] };
451
- function mdSeries(m) {
452
- var days = allDays();
453
- var per = DATA.byDayModel || {};
454
- return days.map(function(d) { return { day: d, v: ((per[d] || {})[m] || 0) }; });
455
- }
456
- function mdSlice(series) {
457
- if (mdSpan === 'all' || series.length <= mdSpan) return series;
458
- return series.slice(series.length - mdSpan);
459
- }
460
- function mdMonthTicks(days) {
461
- if (!days.length) return [];
462
- var seen = {}, out = [];
463
- days.forEach(function(d) {
464
- var t = d.slice(0, 7);
465
- if (!seen[t]) { seen[t] = 1; out.push({ day: d, label: new Date(d + 'T12:00:00').toLocaleString('en-US', { month: 'short' }) }); }
466
- });
467
- return out.slice(-6);
468
- }
469
- function mdShortDay(d) { return new Date(d + 'T12:00:00').toLocaleString('en-US', { month: 'short', day: 'numeric' }); }
470
- function mdAreaChart(svg, pts, days) {
471
- var W = 560, H = 190, max = 1, i, x, y;
472
- pts.forEach(function(p) { max = Math.max(max, p); });
473
- var step = pts.length > 1 ? (W - 16) / (pts.length - 1) : 0;
474
- function X(i) { return 8 + i * step; }
475
- function Y(p) { return 14 + (1 - p / max) * (H - 36); }
476
- var grid = '';
477
- for (i = 1; i <= 3; i++) { y = 14 + (H - 36) * i / 3; grid += '<line class="grid" x1="8" y1="' + y.toFixed(1) + '" x2="' + (W - 8) + '" y2="' + y.toFixed(1) + '"/>'; }
478
- var d = pts.map(function(p, i) { return (i ? 'L' : 'M') + X(i).toFixed(1) + ' ' + Y(p).toFixed(1); }).join(' ');
479
- var last = pts.length ? { x: X(pts.length - 1), y: Y(pts[pts.length - 1]) } : { x: 8, y: H - 22 };
480
- svg.innerHTML = '<defs><linearGradient id="mdGrad" x1="0" y1="0" x2="0" y2="1">' +
481
- '<stop offset="0" style="stop-color:var(--accent)" stop-opacity="0.35"/>' +
482
- '<stop offset="1" style="stop-color:var(--accent)" stop-opacity="0"/></linearGradient></defs>' +
483
- grid +
484
- (pts.length ? '<path class="area" d="' + d + ' L' + last.x.toFixed(1) + ' ' + (H - 22) + ' L8 ' + (H - 22) + ' Z"/>' : '') +
485
- (pts.length ? '<path class="line" d="' + d + '"/>' : '') +
486
- (pts.length ? '<circle class="dot" cx="' + last.x.toFixed(1) + '" cy="' + last.y.toFixed(1) + '" r="4"/>' : '') +
487
- (pts.length ? '<circle class="hoverdot" id="mdHoverDot" cx="-10" cy="-10" r="4" style="display:none"/>' : '');
488
- mdHover = { pts: pts, days: days };
489
- svg.onmousemove = function(ev) {
490
- var tip = document.getElementById('mdTip');
491
- if (!tip || !mdHover.pts.length) return;
492
- var r = svg.getBoundingClientRect();
493
- var fx = (ev.clientX - r.left) / r.width * W;
494
- var idx = Math.max(0, Math.min(mdHover.pts.length - 1, Math.round((fx - 8) / (step || 1))));
495
- var dot = document.getElementById('mdHoverDot');
496
- if (dot) { dot.setAttribute('cx', X(idx).toFixed(1)); dot.setAttribute('cy', Y(mdHover.pts[idx]).toFixed(1)); dot.style.display = ''; }
497
- tip.textContent = mdShortDay(mdHover.days[idx]) + ' · ' + fmtShort(mdHover.pts[idx]);
498
- tip.style.display = 'block';
499
- // Raw % positioning overflows past the dialog edge on the first/last
500
- // points (the dialog clips it). Measure and pin inside the card.
501
- var chart = svg.closest ? svg.closest('.md-chart') : null;
502
- var px = svg.offsetLeft + (X(idx) / W) * svg.clientWidth;
503
- if (chart) {
504
- var half = tip.offsetWidth / 2;
505
- px = Math.max(half + 4, Math.min(chart.clientWidth - half - 4, px));
506
- }
507
- tip.style.left = px + 'px';
508
- tip.style.top = '8px';
509
- };
510
- svg.onmouseleave = function() {
511
- var tip = document.getElementById('mdTip');
512
- if (tip) tip.style.display = 'none';
513
- var dot = document.getElementById('mdHoverDot');
514
- if (dot) dot.style.display = 'none';
515
- };
516
- }
517
- function mdBars(el, vals, days) {
518
- el.innerHTML = '';
519
- var max = 1, i, total = 0, active = 0, peak = 0, peakI = 0;
520
- vals.forEach(function(v, k) { max = Math.max(max, v); total += v; if (v > 0) { active++; if (v > peak) { peak = v; peakI = k; } } });
521
- var n = vals.length, bucket = 1;
522
- if (n > 90) bucket = Math.ceil(n / 90);
523
- for (i = 0; i < n; i += bucket) {
524
- var sum = 0, c = 0;
525
- for (var j = i; j < Math.min(n, i + bucket); j++) { sum += vals[j]; c++; }
526
- var avg = sum / c;
527
- var s = document.createElement('span');
528
- if (!avg) s.className = 'zero';
529
- else if (avg === max) s.className = 'top';
530
- s.style.height = Math.max(avg ? 5 : 2, Math.round(avg / max * 100)) + '%';
531
- s.title = mdShortDay(days[i]) + ' · ' + fmtShort(Math.round(sum));
532
- el.appendChild(s);
533
- }
534
- // Sparse models leave the bars mostly flat: name the peak day, its share
535
- // of the period, and the per-active-day average instead of empty space.
536
- var note = document.getElementById('mdBarsNote');
537
- if (note) {
538
- if (!active) note.textContent = 'no activity in range';
539
- else note.textContent = 'Peak ' + mdShortDay(days[peakI]) + ' · ' + fmtShort(peak) + ' (' + (total ? Math.round(peak / total * 100) : 0) + '% of period) · avg ' + fmtShort(Math.round(total / active)) + ' / active day';
540
- }
541
- }
542
- function mdDonut(svg, parts) {
543
- var r = 54, C = 2 * Math.PI * r, off = 0;
544
- var total = parts.reduce(function(a, p) { return a + p.v; }, 0) || 1;
545
- var html = '<circle class="tk" cx="70" cy="70" r="' + r + '"/>';
546
- parts.forEach(function(p) {
547
- var len = p.v / total * C;
548
- if (len <= 0) return;
549
- html += '<circle cx="70" cy="70" r="' + r + '" style="stroke:' + p.color + '" stroke-dasharray="' + len.toFixed(1) + ' ' + (C - len).toFixed(1) + '" stroke-dashoffset="' + (-off).toFixed(1) + '" stroke-linecap="butt"/>';
550
- off += len;
551
- });
552
- svg.innerHTML = html;
553
- document.getElementById('mdMixTotal').textContent = fmtShort(total);
554
- document.getElementById('mdMixLegend').innerHTML = parts.filter(function(p) { return p.v > 0; }).map(function(p) {
555
- return '<div class="row"><span class="dot" style="background:' + p.color + '"></span><span>' + p.label + '</span><span class="pct">' + (p.v / total * 100).toFixed(1) + '% · ' + fmtShort(p.v) + '</span></div>';
556
- }).join('');
557
- }
558
- function mdMonths(el, days) {
559
- el.innerHTML = '';
560
- mdMonthTicks(days).forEach(function(t) {
561
- var s = document.createElement('span');
562
- s.textContent = t.label;
563
- el.appendChild(s);
564
- });
565
- }
566
- function mdKpi(k, v, d, up) {
567
- return '<div class="md-kpi"><p class="k mono">' + k + '</p><p class="v">' + v + '</p><p class="d mono ' + (up ? 'up' : 'flat') + '">' + d + '</p></div>';
568
- }
569
- function mdStat(k, v, s) {
570
- return '<div class="md-stat"><p class="k mono">' + k + '</p><p class="v mono">' + v + '</p>' + (s ? '<p class="s mono">' + s + '</p>' : '') + '</div>';
571
- }
572
- function mdRange(days) {
573
- if (!days.length) return '–';
574
- return mdShortDay(days[0]).toUpperCase() + ' → ' + mdShortDay(days[days.length - 1]).toUpperCase();
575
- }
576
- function mdTrendText(vals) {
577
- var max = 0, i;
578
- vals.forEach(function(x) { max = Math.max(max, x); });
579
- if (vals.length < 14) return max ? { t: 'peak ' + fmtShort(max) + ' in a day', up: true } : { t: 'no activity yet', up: false };
580
- var a = 0, b = 0;
581
- for (i = vals.length - 14; i < vals.length; i++) a += vals[i];
582
- for (i = Math.max(0, vals.length - 28); i < vals.length - 14; i++) b += vals[i];
583
- if (!b) return { t: fmtShort(a) + ' / 2 wks', up: a > 0 };
584
- var pct = (a - b) / b * 100;
585
- return { t: (pct >= 0 ? '+' : '') + pct.toFixed(0) + '% vs prior 2 wks', up: pct >= 0 };
586
- }
587
- function renderModelDialog(m) {
588
- var byModel = DATA.byModel || {};
589
- var tops = topModels(byModel, Object.keys(byModel).length);
590
- var rank = tops.indexOf(m) + 1;
591
- var b = byModel[m] || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
592
- var total = modelTotal(byModel, m);
593
- var grand = 1;
594
- tops.forEach(function(k) { grand += modelTotal(byModel, k); });
595
- var v = vendorOf(m);
596
- var req = ((DATA.byModelMessages || {})[m]) || 0;
597
- var usd = ((DATA.byModelUsd || {})[m]) || 0;
598
- var denom = b.input + b.cacheRead;
599
- var hit = denom ? (b.cacheRead / denom * 100) : 0;
600
- var series = mdSlice(mdSeries(m));
601
- var days = series.map(function(p) { return p.day; });
602
- var vals = series.map(function(p) { return p.v; });
603
- var active = vals.filter(function(x) { return x > 0; }).length;
604
- var tr = mdTrendText(mdSeries(m).map(function(p) { return p.v; }));
605
- document.getElementById('mdBadge').innerHTML = brandHTML(v, false);
606
- var title = document.getElementById('mdTitle');
607
- title.textContent = displayModel(m);
608
- title.title = m;
609
- document.getElementById('mdSub').textContent = v.name + ' · ' + fmt(req) + ' requests · ' + (total / grand * 100).toFixed(1) + '% of volume';
610
- document.getElementById('mdRank').textContent = rank ? '#' + String(rank).padStart(2, '0') : '#–';
611
- var trend = document.getElementById('mdTrend');
612
- trend.textContent = tr.t;
613
- trend.className = 'md-trend mono' + (tr.up ? ' up' : '');
614
- document.getElementById('mdKpis').innerHTML =
615
- mdKpi('Tokens', fmtShort(total), mdRange(days), true) +
616
- mdKpi('Spend', fxMoney(usd), req ? fxMoney(usd / req) + ' / req' : 'no requests', usd > 0) +
617
- mdKpi('Cache hit', hit.toFixed(0) + '%', fmtShort(b.cacheRead) + ' cached', hit >= 50) +
618
- mdKpi('Requests', fmt(req), active + ' active days', active > 0);
619
- document.getElementById('mdMomRange').textContent = mdRange(days);
620
- mdAreaChart(document.getElementById('mdSpark'), vals, days);
621
- mdMonths(document.getElementById('mdMonths'), days);
622
- var act = document.getElementById('mdActive');
623
- act.textContent = active + ' / ' + days.length + ' days';
624
- act.className = 'md-trend mono' + (active ? ' up' : '');
625
- mdBars(document.getElementById('mdBars'), vals, days);
626
- mdMonths(document.getElementById('mdBarsMonths'), days);
627
- mdDonut(document.getElementById('mdMix'), [
628
- { label: 'Input', v: b.input, color: 'var(--accent)' },
629
- { label: 'Output', v: b.output, color: '#818cf8' },
630
- { label: 'Cache read', v: b.cacheRead, color: '#22d3ee' },
631
- { label: 'Cache write', v: b.cacheWrite, color: 'var(--dim)' },
632
- ]);
633
- document.getElementById('mdStats').innerHTML =
634
- mdStat('Input', fmtShort(b.input), null) +
635
- mdStat('Output', fmtShort(b.output), null) +
636
- mdStat('Cache read', fmtShort(b.cacheRead), hit.toFixed(0) + '% hit') +
637
- mdStat('Cache write', fmtShort(b.cacheWrite), null);
638
- if (window.lucide) lucide.createIcons();
639
- if (hasGsap && !reduce) {
640
- gsap.fromTo('#modelDialog .md-kpi', { y: 12, opacity: 0.2 }, { y: 0, opacity: 1, duration: 0.4, ease: 'power2.out', stagger: 0.05, overwrite: true });
641
- gsap.fromTo('#modelDialog .md-chart', { y: 14, opacity: 0.2 }, { y: 0, opacity: 1, duration: 0.45, ease: 'power2.out', stagger: 0.07, overwrite: true });
642
- gsap.fromTo('#modelDialog .md-stat', { scale: 0.94, opacity: 0.2 }, { scale: 1, opacity: 1, duration: 0.4, ease: 'power2.out', stagger: 0.04, overwrite: true });
643
- }
644
- }
645
- function openModelDialog(m) {
646
- var dlg = document.getElementById('modelDialog');
647
- if (!dlg || typeof dlg.showModal !== 'function') return;
648
- mdModel = m;
649
- Array.prototype.forEach.call(document.querySelectorAll('#modelDialog [data-mdspan]'), function(btn) {
650
- var on = btn.getAttribute('data-mdspan') === String(mdSpan);
651
- btn.classList.toggle('on', on);
652
- btn.onclick = function() {
653
- mdSpan = btn.getAttribute('data-mdspan') === 'all' ? 'all' : Number(btn.getAttribute('data-mdspan'));
654
- Array.prototype.forEach.call(document.querySelectorAll('#modelDialog [data-mdspan]'), function(o) { o.classList.toggle('on', o === btn); });
655
- renderModelDialog(mdModel);
656
- };
657
- });
658
- renderModelDialog(m);
659
- if (!dlg.open) dlg.showModal();
660
- }
661
- function dayModelRows(key) {
662
- var per = (DATA.byDayModel || {})[key] || {};
663
- var tops = topModels(DATA.byModel || {}, 8), rows = [], other = 0;
664
- tops.forEach(function(m, i) {
665
- var v = per[m] || 0;
666
- if (v) rows.push([m, v, PALETTE[i % PALETTE.length]]);
667
- });
668
- Object.keys(per).forEach(function(m) { if (tops.indexOf(m) < 0) other += per[m]; });
669
- if (other) rows.push(['Other', other, 'var(--dim)']);
670
- return rows;
671
- }
672
-
673
- var GMODE = 'daily';
674
- function mondayOf(d) {
675
- var m = new Date(d);
676
- m.setDate(d.getDate() - ((d.getDay() + 6) % 7));
677
- m.setHours(0, 0, 0, 0);
678
- return m;
679
- }
680
- function weekTipHTML(monKey, weekTotal, weekPer) {
681
- var mon = new Date(monKey + 'T12:00:00'), sun = new Date(mon);
682
- sun.setDate(mon.getDate() + 6);
683
- var head = isNaN(mon) ? monKey : mon.toLocaleString('en-US', { month: 'short', day: 'numeric' }).toUpperCase() +
684
- ' - ' + (isNaN(sun) ? '' : sun.toLocaleString('en-US', { month: 'short', day: 'numeric' }).toUpperCase());
685
- var tops = topModels(DATA.byModel || {}, 8), rows = [], other = 0;
686
- tops.forEach(function(m, i) {
687
- var v = weekPer[m] || 0;
688
- if (v) rows.push([m, v, PALETTE[i % PALETTE.length]]);
689
- });
690
- Object.keys(weekPer).forEach(function(m) { if (tops.indexOf(m) < 0) other += weekPer[m]; });
691
- if (other) rows.push(['Other', other, 'var(--dim)']);
692
- return tipDayHTML(head, weekTotal, rows.length ? rows : [['no activity', 0, 'var(--dim)']]);
693
- }
694
- // Shared empty state (shadcn pattern): icon chip + title + hint. Callers
695
- // inject the HTML; render()'s trailing lucide.createIcons() paints icons.
696
- function emptyState(icon, title, desc) {
697
- return '<span class="empty-icon"><i data-lucide="' + icon + '" class="size-5"></i></span>' +
698
- '<p class="empty-title">' + title + '</p>' +
699
- '<p class="empty-desc">' + desc + '</p>';
700
- }
701
- function renderGraph(byDay) {
702
- var g = document.getElementById('graph');
703
- g.innerHTML = '';
704
- var today = new Date(); today.setHours(0, 0, 0, 0);
705
- var start = new Date(today); start.setDate(start.getDate() - (52 * 7) - today.getDay());
706
- var sums = {};
707
- Object.keys(byDay).forEach(function(day) {
708
- var b = byDay[day];
709
- sums[day] = b.input + b.output + b.cacheRead + b.cacheWrite;
710
- });
711
- var hasData = Object.keys(sums).some(function(k) { return sums[k] > 0; });
712
- document.getElementById('emptyGraph').classList.toggle('hidden', hasData);
713
- document.getElementById('graph').style.display = hasData ? '' : 'none';
714
- document.getElementById('graphMonths').style.display = hasData ? '' : 'none';
715
- if (!hasData) {
716
- document.getElementById('graphRange').textContent = '';
717
- document.getElementById('graphCap').textContent = 'no data in trailing 12 months';
718
- return;
719
- }
720
- // per-day values under the active mode
721
- var dayKeys = [], wk;
722
- for (wk = 0; wk < 53; wk++) for (var di = 0; di < 7; di++) {
723
- var dd = new Date(start); dd.setDate(dd.getDate() + wk * 7 + di);
724
- dayKeys.push(dayKey(dd));
725
- }
726
- var weekPer = {}, weekTotal = {}, run = 0, cellVal = {};
727
- dayKeys.forEach(function(k) {
728
- var dt = new Date(k + 'T12:00:00');
729
- if (GMODE === 'weekly' && !isNaN(dt)) {
730
- var mk = dayKey(mondayOf(dt));
731
- weekTotal[mk] = (weekTotal[mk] || 0) + (sums[k] || 0);
732
- var per = (DATA.byDayModel || {})[k] || {};
733
- if (!weekPer[mk]) weekPer[mk] = {};
734
- Object.keys(per).forEach(function(m) { weekPer[mk][m] = (weekPer[mk][m] || 0) + per[m]; });
735
- }
736
- if (GMODE === 'cumulative') { run += sums[k] || 0; cellVal[k] = run; }
737
- });
738
- var max = 1;
739
- if (GMODE === 'weekly') {
740
- Object.keys(weekTotal).forEach(function(k) { max = Math.max(max, weekTotal[k]); });
741
- } else if (GMODE === 'cumulative') {
742
- max = Math.max(1, run);
743
- } else {
744
- Object.keys(sums).forEach(function(k) { max = Math.max(max, sums[k]); });
745
- }
746
- var months = document.getElementById('graphMonths');
747
- months.innerHTML = '';
748
- var lastMonth = '';
749
- for (var w = 0; w < 53; w++) {
750
- var col = document.createElement('div');
751
- col.style.cssText = 'display:flex;flex-direction:column;gap:3px;min-width:0;';
752
- var ml = document.createElement('span');
753
- ml.className = 'heatmonth w-full';
754
- for (var i = 0; i < 7; i++) {
755
- var cdt = new Date(start); cdt.setDate(cdt.getDate() + w * 7 + i);
756
- var key = dayKey(cdt), v, tipFn;
757
- if (GMODE === 'weekly') {
758
- var mk2 = dayKey(mondayOf(cdt));
759
- v = weekTotal[mk2] || 0;
760
- tipFn = (function(mk3, vv) {
761
- return function() { return weekTipHTML(mk3, vv, weekPer[mk3] || {}); };
762
- })(mk2, v);
763
- } else if (GMODE === 'cumulative') {
764
- v = cellVal[key] || 0;
765
- tipFn = (function(kk, vv) {
766
- return function() {
767
- var rows = dayModelRows(kk);
768
- return tipDayHTML('THROUGH ' + dateHead(kk), vv, rows.length ? rows : [['no activity', 0, 'var(--dim)']]);
769
- };
770
- })(key, v);
771
- } else {
772
- v = sums[key] || 0;
773
- tipFn = (function(kk, vv) {
774
- return function() {
775
- var rows = dayModelRows(kk);
776
- return tipDayHTML(kk, vv, rows.length ? rows : [['no activity', 0, 'var(--dim)']]);
777
- };
778
- })(key, v);
779
- }
780
- var lvl = 0;
781
- if (v > 0) lvl = Math.min(4, 1 + Math.floor((v / max) * 3.99));
782
- var cell = document.createElement('span');
783
- cell.className = 'cell' + (lvl ? ' l' + lvl : '');
784
- (function(fn) {
785
- cell.addEventListener('mouseenter', function(ev) { showTip(fn(), ev.clientX, ev.clientY); });
786
- cell.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
787
- cell.addEventListener('mouseleave', hideTip);
788
- })(tipFn);
789
- col.appendChild(cell);
790
- if (i === 0) {
791
- var mm = cdt.toLocaleString('en-US', { month: 'short' });
792
- ml.textContent = mm !== lastMonth ? mm : '';
793
- lastMonth = mm;
794
- }
795
- }
796
- g.appendChild(col);
797
- months.appendChild(ml);
798
- }
799
- Array.prototype.forEach.call(months.children, function(m) { m.style.minWidth = '0'; });
800
- document.getElementById('graphRange').textContent =
801
- dayKey(start) + ' to ' + dayKey(today);
802
- document.getElementById('graphCap').textContent =
803
- GMODE === 'weekly' ? 'weekly totals / trailing 12 months' :
804
- GMODE === 'cumulative' ? 'running total / trailing 12 months' :
805
- 'daily values / trailing 12 months';
806
- }
807
- function bindGraphTabs() {
808
- if (bindGraphTabs.done) return;
809
- bindGraphTabs.done = true;
810
- Array.prototype.forEach.call(document.querySelectorAll('#activity [data-gmode]'), function(btn) {
811
- btn.addEventListener('click', function() {
812
- Array.prototype.forEach.call(document.querySelectorAll('#activity [data-gmode]'), function(b) { b.classList.remove('on'); });
813
- btn.classList.add('on');
814
- GMODE = btn.getAttribute('data-gmode');
815
- if (DATA) renderGraph(DATA.byDay || {});
816
- });
817
- });
818
- }
819
-
820
- var modelPage = 1, modelPer = 10, recentPage = 1, recentPer = 25;
821
- // Recent requests default to newest-first, which is how the transcript
822
- // arrives; every column is sortable like the command table.
823
- var recentSort = { key: 'when', dir: -1 }, recentRows = [];
824
- function recentVal(r, key) {
825
- if (key === 'model') return r.m.toLowerCase();
826
- if (key === 'input') return r.i;
827
- if (key === 'output') return r.o;
828
- if (key === 'time') return r.d === undefined ? null : r.d;
829
- if (key === 'status') return statusRank(r);
830
- if (key === 'cost') return displayCost(r);
831
- return r.t;
832
- }
833
- function sortRecent() {
834
- var k = recentSort.key, d = recentSort.dir;
835
- recentRows.sort(function(a, b) {
836
- var av = recentVal(a, k), bv = recentVal(b, k);
837
- if (av === null || av === undefined) return 1;
838
- if (bv === null || bv === undefined) return -1;
839
- if (av < bv) return -d;
840
- if (av > bv) return d;
841
- return b.t - a.t;
842
- });
843
- Array.prototype.forEach.call(document.querySelectorAll('#recentTable .thsort'), function(btn) {
844
- var on = btn.dataset.sort === k;
845
- btn.querySelector('.arr').textContent = on ? (d === 1 ? '↑' : '↓') : '⇅';
846
- btn.closest('th').setAttribute('aria-sort', on ? (d === 1 ? 'ascending' : 'descending') : 'none');
847
- });
848
- }
849
- function renderModels() {
850
- var byModel = DATA.byModel || {};
851
- var tops = topModels(byModel, Object.keys(byModel).length).filter(function(m) { return modelTotal(byModel, m) > 0; });
852
- var cards = document.getElementById('modelCards');
853
- cards.innerHTML = '';
854
- 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>';
855
- tops.slice(0, 3).forEach(function(m, i) {
856
- var v = vendorOf(m);
857
- var card = document.createElement('div');
858
- card.className = 'modelcard relative p-4 rounded-xl overflow-hidden';
859
- if (v.svg) {
860
- var ghostSvg = document.createElement('span');
861
- ghostSvg.className = 'ghostimg'; ghostSvg.setAttribute('aria-hidden', 'true');
862
- ghostSvg.innerHTML = v.svg;
863
- card.appendChild(ghostSvg);
864
- } else if (v.slug) {
865
- var ghost = document.createElement('img');
866
- ghost.className = 'ghostimg'; ghost.alt = '';
867
- ghost.src = 'https://cdn.simpleicons.org/' + v.slug + '/white';
868
- ghost.setAttribute('aria-hidden', 'true');
869
- card.appendChild(ghost);
870
- } else {
871
- var ghostSparkle = document.createElement('i');
872
- ghostSparkle.setAttribute('data-lucide', 'sparkles');
873
- ghostSparkle.className = 'ghosticon';
874
- ghostSparkle.setAttribute('aria-hidden', 'true');
875
- card.appendChild(ghostSparkle);
876
- }
877
- card.style.cssText = 'border: 1px solid var(--line); background: var(--panel); cursor: pointer';
878
- card.setAttribute('role', 'button');
879
- var head = document.createElement('p');
880
- head.className = 'mono text-xs mb-3'; head.style.color = 'var(--dim)';
881
- head.textContent = '0' + (i + 1);
882
- var row = document.createElement('div');
883
- row.className = 'flex items-center gap-3';
884
- var badgeWrap = document.createElement('span');
885
- badgeWrap.innerHTML = brandHTML(v, false);
886
- var badge = badgeWrap.firstChild;
887
- var mid = document.createElement('div'); mid.className = 'min-w-0 flex-1';
888
- var nm = document.createElement('p'); nm.className = 'mono text-sm font-bold truncate'; nm.textContent = displayModel(m); nm.title = m;
889
- var vn = document.createElement('p'); vn.className = 'mono text-xs truncate'; vn.style.color = 'var(--dim)'; vn.textContent = v.name;
890
- mid.appendChild(nm); mid.appendChild(vn);
891
- var tot = document.createElement('p'); tot.className = 'mono font-bold text-lg shrink-0'; tot.textContent = fmtShort(modelTotal(byModel, m));
892
- row.appendChild(badge); row.appendChild(mid); row.appendChild(tot);
893
- card.appendChild(head); card.appendChild(row);
894
- hoverModel(card, m);
895
- (function(model) { card.addEventListener('click', function() { openModelDialog(model); }); })(m);
896
- cards.appendChild(card);
897
- });
898
- var ol = document.getElementById('models');
899
- ol.innerHTML = '';
900
- var max = 1;
901
- tops.forEach(function(m) { max = Math.max(max, modelTotal(byModel, m)); });
902
- var modelPages = Math.max(1, Math.ceil(tops.length / modelPer));
903
- if (modelPage > modelPages) modelPage = modelPages;
904
- var modelStart = (modelPage - 1) * modelPer;
905
- tops.slice(modelStart, modelStart + modelPer).forEach(function(m, i) {
906
- var v = vendorOf(m);
907
- var li = document.createElement('li');
908
- li.className = 'mrow flex items-center gap-3 px-4 py-3' + (i < Math.min(tops.length - modelStart, modelPer) - 1 ? ' rowline' : '');
909
- var badgeWrap = document.createElement('span');
910
- badgeWrap.innerHTML = brandHTML(v, true);
911
- var badge = badgeWrap.firstChild;
912
- var mid = document.createElement('div'); mid.className = 'min-w-0 flex-1';
913
- var nm = document.createElement('p'); nm.className = 'mono text-sm truncate'; nm.textContent = displayModel(m); nm.title = m;
914
- var track = document.createElement('div'); track.className = 'bar-track mt-1.5 h-1.5 overflow-hidden';
915
- var fill = document.createElement('div'); fill.className = 'bar-fill h-full';
916
- var mv = modelTotal(byModel, m);
917
- fill.dataset.w = Math.round(mv / max * 100) + '%';
918
- fill.style.width = '0';
919
- track.appendChild(fill); mid.appendChild(nm); mid.appendChild(track);
920
- var right = document.createElement('div'); right.className = 'shrink-0 text-right';
921
- var n = document.createElement('p'); n.className = 'mono font-bold'; n.textContent = fmt(modelTotal(byModel, m));
922
- var cost = document.createElement('p'); cost.className = 'mono text-xs'; cost.style.color = 'var(--dim)';
923
- cost.textContent = fxMoney((DATA.byModelUsd || {})[m] || 0);
924
- right.appendChild(n); right.appendChild(cost);
925
- li.appendChild(badge); li.appendChild(mid); li.appendChild(right);
926
- hoverModel(li, m);
927
- (function(model) { li.addEventListener('click', function() { openModelDialog(model); }); })(m);
928
- ol.appendChild(li);
929
- });
930
- document.getElementById('modelsCount').textContent = tops.length ? tops.length + ' models' : '';
931
- paintPager('modelPages', 'modelRange', modelPage, modelPer, tops.length, function(p) { modelPage = p; renderModels(); });
932
- ol.style.display = tops.length ? '' : 'none';
933
- document.getElementById('emptyModels').classList.toggle('hidden', tops.length > 0);
934
- if (tops.length && 'IntersectionObserver' in window && !reduce) {
935
- var fio = new IntersectionObserver(function(entries) {
936
- entries.forEach(function(e) {
937
- if (e.isIntersecting) {
938
- e.target.querySelectorAll('.bar-fill').forEach(function(f) { f.style.width = f.dataset.w || '0'; });
939
- fio.disconnect();
940
- }
941
- });
942
- }, { threshold: 0.2 });
943
- fio.observe(ol);
944
- } else if (tops.length) {
945
- ol.querySelectorAll('.bar-fill').forEach(function(f) { f.style.width = f.dataset.w || '0'; });
946
- }
947
- if (hasGsap && hasST && !cardsShown && tops.length) {
948
- cardsShown = true;
949
- gsap.from('#modelCards > div', {
950
- scale: 0.92, opacity: 0.2, duration: 0.6, ease: 'power2.out', stagger: 0.08, overwrite: true,
951
- scrollTrigger: { trigger: '#modelCards', start: 'top 88%' }
952
- });
953
- }
954
- }
955
-
956
- // Union of session tool calls + RTK-metered commands, sortable + paged.
957
- // Session tools were never metered per command: saved / avg / time show – and sort last.
958
- // Rows group by command alone, machine-wide: same command in any repo is one row.
959
- var cmdSort = { key: 'count', dir: -1 }, cmdPage = 1, cmdPer = 15, cmdRows = [];
960
- function cmdVal(r, key) {
961
- if (key === 'name') return r.name.toLowerCase();
962
- return r[key];
963
- }
964
- function sortCmd() {
965
- var k = cmdSort.key, d = cmdSort.dir;
966
- cmdRows.sort(function(a, b) {
967
- var av = cmdVal(a, k), bv = cmdVal(b, k);
968
- if (av === null || av === undefined) return 1;
969
- if (bv === null || bv === undefined) return -1;
970
- if (av < bv) return -d;
971
- if (av > bv) return d;
972
- return b.count - a.count;
973
- });
974
- Array.prototype.forEach.call(document.querySelectorAll('#tools .thsort'), function(btn) {
975
- var on = btn.dataset.sort === k;
976
- btn.querySelector('.arr').textContent = on ? (d === 1 ? '↑' : '↓') : '⇅';
977
- btn.closest('th').setAttribute('aria-sort', on ? (d === 1 ? 'ascending' : 'descending') : 'none');
978
- });
979
- }
980
- function renderCmd() {
981
- var g = (DATA.rtkGain || {});
982
- var byName = {};
983
- function add(name, count, saved, avgPct, avgMs) {
984
- var r = byName[name];
985
- if (!r) { byName[name] = { name: name, count: count, saved: saved, avgPct: avgPct, avgMs: avgMs }; return; }
986
- r.count += count;
987
- if (saved !== null && saved !== undefined) r.saved = (r.saved || 0) + saved;
988
- }
989
- (DATA.byTool || []).forEach(function(r) { add(r[0], r[1], null, null, null); });
990
- (g.byCommand || []).forEach(function(r) { add(r.command, r.count, r.saved, r.avgPct, r.avgMs); });
991
- cmdRows = Object.keys(byName).map(function(k) { return byName[k]; });
992
- sortCmd();
993
- var pages = Math.max(1, Math.ceil(cmdRows.length / cmdPer));
994
- if (cmdPage > pages) cmdPage = pages;
995
- var body = document.getElementById('cmdBody');
996
- body.innerHTML = '';
997
- var max = 1;
998
- cmdRows.forEach(function(r) { max = Math.max(max, r.count); });
999
- var start = (cmdPage - 1) * cmdPer;
1000
- cmdRows.slice(start, start + cmdPer).forEach(function(r, i) {
1001
- var n = start + i;
1002
- var tr = document.createElement('tr');
1003
- tr.className = 'mrow' + (i < Math.min(cmdRows.length - start, cmdPer) - 1 ? ' rowline' : '');
1004
- tr.innerHTML = '<td class="text-right pr-3 py-2.5 mono text-xs w-10" style="color: var(--dim)"></td>' +
1005
- '<td class="py-2.5 pr-3 truncate" style="max-width: 280px"></td>' +
1006
- '<td class="text-right py-2.5 pr-3 font-bold"></td>' +
1007
- '<td class="text-right py-2.5 pr-3 font-bold"></td>' +
1008
- '<td class="text-right py-2.5 pr-3" style="color: var(--accent)"></td>' +
1009
- '<td class="text-right py-2.5 pr-3" style="color: var(--dim)"></td>' +
1010
- '<td class="py-2.5 min-w-32"><div class="bar-track h-1.5 overflow-hidden"><div class="bar-fill h-full"></div></div></td>';
1011
- var tds = tr.children;
1012
- tds[0].textContent = String(n + 1).padStart(2, '0');
1013
- tds[1].textContent = r.name;
1014
- tds[1].title = r.name;
1015
- tds[2].textContent = fmt(r.count);
1016
- tds[3].textContent = r.saved === null ? '–' : fmtShort(r.saved);
1017
- tds[4].textContent = r.avgPct === null ? '–' : r.avgPct.toFixed(1) + '%';
1018
- tds[5].textContent = r.avgMs === null || r.avgMs === undefined ? '–' : fmtMs(r.avgMs);
1019
- tds[6].firstChild.firstChild.style.width = r.count ? Math.round(r.count / max * 100) + '%' : '0';
1020
- body.appendChild(tr);
1021
- });
1022
- document.getElementById('cmdTable').style.display = cmdRows.length ? '' : 'none';
1023
- document.getElementById('emptyCmd').classList.toggle('hidden', cmdRows.length > 0);
1024
- document.getElementById('toolsScope').textContent =
1025
- g.commands ? fmt(cmdRows.length) + ' commands · ' + fmt(g.commands) + ' runs · ' + fmtShort(g.saved) + ' saved' : 'tool calls in sessions';
1026
- paintCmdPager(pages);
1027
- }
1028
- // Shared page-number painter: prev/next plus windowed numbers with
1029
- // ellipsis. onPick(page) rerenders the owning card. Same controls as the
1030
- // Command tools pager; Models and Recent reuse it verbatim.
1031
- function paintPager(wrapId, rangeId, page, per, total, onPick) {
1032
- var start = total ? (page - 1) * per + 1 : 0;
1033
- document.getElementById(rangeId).textContent =
1034
- 'Showing ' + start + '–' + Math.min(total, page * per) + ' of ' + total;
1035
- var wrap = document.getElementById(wrapId);
1036
- wrap.innerHTML = '';
1037
- var pages = Math.max(1, Math.ceil(total / per));
1038
- if (page > pages) onPick(pages);
1039
- function btn(label, next, opts) {
1040
- var b = document.createElement('button');
1041
- b.type = 'button'; b.className = 'pgbtn mono' + (opts && opts.on ? ' on' : '');
1042
- b.textContent = label;
1043
- if (opts && opts.dim) b.setAttribute('aria-label', opts.dim);
1044
- if (opts && opts.off) b.disabled = true;
1045
- else b.addEventListener('click', function() { onPick(next); });
1046
- wrap.appendChild(b);
1047
- return b;
1048
- }
1049
- btn('‹', page - 1, { dim: 'Previous page', off: page <= 1 });
1050
- var nums = [];
1051
- for (var p = 1; p <= pages; p++) {
1052
- if (p === 1 || p === pages || Math.abs(p - page) <= 1) nums.push(p);
1053
- else if (nums[nums.length - 1] !== '…') nums.push('…');
1054
- }
1055
- nums.forEach(function(p) {
1056
- if (p === '…') {
1057
- var s = document.createElement('span');
1058
- s.textContent = '…'; s.style.padding = '0 2px';
1059
- wrap.appendChild(s);
1060
- } else btn(String(p), p, { on: p === page });
1061
- });
1062
- btn('›', page + 1, { dim: 'Next page', off: page >= pages });
1063
- }
1064
- function paintCmdPager(pages) {
1065
- var total = cmdRows.length;
1066
- if (cmdPage > pages) cmdPage = pages;
1067
- paintPager('cmdPages', 'cmdRange', cmdPage, cmdPer, total, function(p) { cmdPage = p; renderCmd(); });
1068
- }
1069
- function bindPerPage(sel, get, set, render) {
1070
- Array.prototype.forEach.call(document.querySelectorAll(sel + ' [data-pp]'), function(btn) {
1071
- if (btn.dataset.bound) return;
1072
- btn.dataset.bound = '1';
1073
- btn.addEventListener('click', function() {
1074
- if (Number(btn.dataset.pp) === get()) return;
1075
- Array.prototype.forEach.call(document.querySelectorAll(sel + ' [data-pp]'), function(b) { b.classList.remove('on'); });
1076
- btn.classList.add('on');
1077
- set(Number(btn.dataset.pp));
1078
- render();
1079
- });
1080
- });
1081
- }
1082
- function bindCmdTable() {
1083
- if (bindCmdTable.done) return;
1084
- bindCmdTable.done = true;
1085
- Array.prototype.forEach.call(document.querySelectorAll('#tools .thsort'), function(btn) {
1086
- btn.addEventListener('click', function() {
1087
- var k = btn.dataset.sort;
1088
- if (cmdSort.key === k) cmdSort.dir = -cmdSort.dir;
1089
- else { cmdSort.key = k; cmdSort.dir = k === 'name' ? 1 : -1; }
1090
- cmdPage = 1;
1091
- renderCmd();
1092
- });
1093
- });
1094
- bindPerPage('#cmdPager', function() { return cmdPer; }, function(n) { cmdPer = n; cmdPage = 1; }, renderCmd);
1095
- bindPerPage('section[aria-label="All models"]', function() { return modelPer; }, function(n) { modelPer = n; modelPage = 1; }, renderModels);
1096
- bindPerPage('section[aria-label="Recent requests"]', function() { return recentPer; }, function(n) { recentPer = n; recentPage = 1; }, renderRecent);
1097
- }
1098
- function bindRecentTable() {
1099
- if (bindRecentTable.done) return;
1100
- bindRecentTable.done = true;
1101
- Array.prototype.forEach.call(document.querySelectorAll('#recentTable .thsort'), function(btn) {
1102
- btn.addEventListener('click', function() {
1103
- var k = btn.dataset.sort;
1104
- if (recentSort.key === k) recentSort.dir = -recentSort.dir;
1105
- else { recentSort.key = k; recentSort.dir = k === 'model' || k === 'status' ? 1 : -1; }
1106
- recentPage = 1;
1107
- renderRecent();
1108
- });
1109
- });
1110
- }
1111
- function fmtMs(ms) {
1112
- if (ms < 1000) return Math.round(ms) + 'ms';
1113
- return (ms / 1000).toFixed(1) + 's';
1114
- }
1115
-
1116
- function renderStrip(t) {
1117
- var defs = [
1118
- ['input', t.input, 'arrow-down-to-line'],
1119
- ['output', t.output, 'arrow-up-from-line'],
1120
- ['cache read', t.cacheRead, 'hard-drive-download'],
1121
- ['cache write', t.cacheWrite, 'hard-drive-upload']
1122
- ].filter(function(b) { return b[1] > 0; });
1123
- var strip = document.getElementById('tokens');
1124
- strip.innerHTML = '';
1125
- defs.forEach(function(b, i) {
1126
- var cell = document.createElement('div');
1127
- cell.className = 'stripcell flex-1 px-4 py-4' + (i > 0 ? ' sm:border-l' : '');
1128
- cell.style.borderColor = 'var(--line)';
1129
- cell.innerHTML = '<div class="flex items-center gap-2 mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">' +
1130
- '<i data-lucide="' + b[2] + '" class="size-3.5"></i><span></span></div>' +
1131
- '<p class="mono font-bold text-3xl mt-1"></p>';
1132
- cell.querySelector('span').textContent = b[0];
1133
- cell.querySelector('p').textContent = fmt(b[1]);
1134
- strip.appendChild(cell);
1135
- });
1136
- }
1137
-
1138
- // 30-day volume sparkline + blended-rate daily stats for the USD card.
1139
- // Daily cost is a blended estimate (total USD / total tokens x day
1140
- // tokens), always rendered with ~ and never mixed with measured totals.
1141
- function renderSpark(byDay, usd, savedUsd, total) {
1142
- var el = document.getElementById('usdSpark');
1143
- el.innerHTML = '';
1144
- var keys = Object.keys(byDay).sort().slice(-30);
1145
- // Zero state: the spark strip is a fixed h-16, so a placeholder inside it
1146
- // overflows onto the big number. Hide the number, strip, and stats, and
1147
- // mount one placeholder in the card body instead. Restored below on data.
1148
- var mid = el.parentElement || null, card = mid && mid.parentElement ? mid.parentElement : null;
1149
- var usdEl = document.getElementById('usd'), noteEl = document.getElementById('usdNote');
1150
- var ex = document.getElementById('emptyUsd');
1151
- if (!keys.length && card && mid) {
1152
- if (!ex) {
1153
- ex = document.createElement('div');
1154
- ex.id = 'emptyUsd'; ex.className = 'empty'; ex.style.marginTop = '16px';
1155
- ex.innerHTML = emptyState('trending-up', 'No cost data yet', 'Daily spend will spark here once sessions report tokens.');
1156
- card.insertBefore(ex, mid);
1157
- }
1158
- el.style.display = 'none'; mid.style.display = 'none';
1159
- if (usdEl) usdEl.style.display = 'none';
1160
- if (noteEl) noteEl.style.display = 'none';
1161
- } else {
1162
- if (ex && ex.parentElement) ex.parentElement.removeChild(ex);
1163
- el.style.display = '';
1164
- if (mid) mid.style.display = '';
1165
- if (usdEl) usdEl.style.display = '';
1166
- if (noteEl) noteEl.style.display = '';
1167
- }
1168
- var rate = total ? usd / total : 0;
1169
- var max = 1, vols = [];
1170
- keys.forEach(function(k) {
1171
- var v = dayTotal(byDay[k] || {});
1172
- vols.push({ k: k, v: v });
1173
- max = Math.max(max, v);
1174
- });
1175
- var top = vols.slice().sort(function(a, b) { return b.v - a.v; })[0] || { k: '', v: 0 };
1176
- vols.forEach(function(r) {
1177
- var s = document.createElement('span');
1178
- if (r.k === top.k && r.v > 0) s.className = 'top';
1179
- s.style.height = r.v ? Math.max(4, Math.round(r.v / max * 100)) + '%' : '0';
1180
- s.dataset.usd = (rate * r.v).toFixed(2);
1181
- s.dataset.tok = r.v;
1182
- s.dataset.day = r.k;
1183
- s.addEventListener('mouseenter', function(ev) {
1184
- showTip('<div class="tt">' + dateHead(s.dataset.day) + '</div><div class="tv">~' + fxMoney(Number(s.dataset.usd)) + '</div>' +
1185
- '<div class="tr"><span class="sw" style="background:var(--accent)"></span>' +
1186
- '<span class="tn">tokens</span><span class="tvr">' + fmt(Number(s.dataset.tok)) + '</span></div>',
1187
- ev.clientX, ev.clientY);
1188
- });
1189
- s.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
1190
- s.addEventListener('mouseleave', hideTip);
1191
- el.appendChild(s);
1192
- });
1193
- var days = keys.length || 1;
1194
- document.getElementById('usdPerDay').textContent = fxMoney(usd / days);
1195
- document.getElementById('usdTopDay').textContent = top.k ? top.k.slice(5) + ' ~' + fxMoney(rate * top.v) : '-';
1196
- document.getElementById('usdLeverage').textContent = usd ? (savedUsd / usd).toFixed(1) : '-';
1197
- }
1198
-
1199
- function dayTotal(b) { return (b.input || 0) + (b.output || 0) + (b.cacheRead || 0) + (b.cacheWrite || 0); }
1200
-
1201
- function render(d) {
1202
- DATA = d;
1203
- // Server-provided default currency (tersio gain --currency); a saved
1204
- // picker choice in localStorage always wins.
1205
- if (!render.fxInit) {
1206
- render.fxInit = true;
1207
- var saved = null;
1208
- try { saved = localStorage.getItem('tersio-fx-cur'); } catch (e) { }
1209
- if (!saved && d.currency && CURS[d.currency]) {
1210
- fx.cur = d.currency;
1211
- var curEl = document.getElementById('fxCur');
1212
- if (curEl) curEl.textContent = FLAGS[fx.cur] + ' ' + fx.cur;
1213
- var panel = document.getElementById('fxPanel');
1214
- if (panel) Array.prototype.forEach.call(panel.children, function(o) {
1215
- o.setAttribute('aria-selected', o.dataset.cur === fx.cur ? 'true' : 'false');
1216
- });
1217
- }
1218
- }
1219
- var t = d.tokens || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
1220
- var total = t.input + t.output + t.cacheRead + t.cacheWrite;
1221
- var totalEl = document.getElementById('total');
1222
- if (hasGsap) {
1223
- var o = { v: 0 };
1224
- gsap.to(o, { v: total, duration: 1.2, ease: 'power3.out', onUpdate: function() { totalEl.textContent = fmt(o.v); } });
1225
- } else {
1226
- totalEl.textContent = fmt(total);
1227
- }
1228
- document.getElementById('costLabel').textContent = fx.cur.toLowerCase() + ' cost';
1229
- countUp(document.getElementById('usd'), d.usd || 0, true);
1230
- document.getElementById('usdNote').textContent = (d.priced ? 'per-model price table' : 'incl. default pricing') + (fx.live ? ' · fx live' : ' · fx snapshot');
1231
- var byDay = d.byDay || {};
1232
- renderSpark(byDay, d.usd || 0, d.savedUsd || 0, total);
1233
- document.getElementById('verChip').textContent = 'tersio v' + (d.version || '?');
1234
- var dp = d.paths || {};
1235
- [['pathLedger', dp.ledger], ['pathSessions', dp.sessions], ['pathUsageDb', dp.usageDb]].forEach(function(pair) {
1236
- var pel = document.getElementById(pair[0]);
1237
- if (pel && pair[1]) { pel.textContent = pair[1]; pel.title = pair[1]; }
1238
- });
1239
- countUp(document.getElementById('saved'), d.savedUsd || 0, true);
1240
- (function() {
1241
- var lev = (d.usd || 0) ? (d.savedUsd || 0) / d.usd : 0;
1242
- var z = levZone(lev);
1243
- var note = document.getElementById('usdNote');
1244
- note.innerHTML = ((d.priced ? 'per-model price table' : 'incl. default pricing') + (fx.live ? ' · fx live' : ' · fx snapshot') + ' · ') + zoneIcon(z[0], z[2]) + ' ' + z[1];
1245
- note.title = 'Cache leverage x' + lev.toFixed(1) + ' — cache savings per $1 spent, vs full input price';
1246
- })();
1247
- (function() {
1248
- var z = co2Zone(d.co2g || 0);
1249
- var el = document.getElementById('co2');
1250
- el.innerHTML = '~' + (d.co2g || 0).toFixed(1) + 'g ' + zoneIcon(z[0], z[2]);
1251
- el.title = z[1] + ' footprint (est.)';
1252
- })();
1253
- (function() {
1254
- var el = document.getElementById('co2');
1255
- var card = el ? el.closest('div.rounded-xl') : null;
1256
- if (!card || card.dataset.co2tip) return;
1257
- card.dataset.co2tip = '1';
1258
- card.addEventListener('mouseenter', function(ev) { showTip(tipCo2HTML(), ev.clientX, ev.clientY); });
1259
- card.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
1260
- card.addEventListener('mouseleave', hideTip);
1261
- })();
1262
- (function() {
1263
- var el = document.getElementById('saved');
1264
- var card = el ? el.closest('div.rounded-xl') : null;
1265
- if (!card || card.dataset.savetip) return;
1266
- card.dataset.savetip = '1';
1267
- card.addEventListener('mouseenter', function(ev) { showTip(tipSavedHTML(), ev.clientX, ev.clientY); });
1268
- card.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
1269
- card.addEventListener('mouseleave', hideTip);
1270
- })();
1271
- var share = total ? Math.round((t.cacheRead + t.cacheWrite) / total * 100) : 0;
1272
- var shareZ = shareZone(share);
1273
- document.getElementById('cacheShare').innerHTML = share + '% ' + zoneIcon(shareZ[0], shareZ[2]);
1274
- document.getElementById('cacheShare').title = shareZ[1] + ' cache share';
1275
- var shareOf = function(b) { var s = dayTotal(b); return s ? ((b.cacheRead || 0) + (b.cacheWrite || 0)) / s * 100 : 0; };
1276
- var keys = Object.keys(byDay).sort(), cPct = 0, pPct = 0, cn = 0, pn = 0;
1277
- keys.slice(-7).forEach(function(k) { cPct += shareOf(byDay[k]); cn++; });
1278
- keys.slice(-14, -7).forEach(function(k) { pPct += shareOf(byDay[k]); pn++; });
1279
- var pp = (cn ? cPct / cn : 0) - (pn ? pPct / pn : 0);
1280
- var cd = document.getElementById('cacheDelta');
1281
- var cs = document.getElementById('cacheShare');
1282
- cs.dataset.share = String(share);
1283
- cs.dataset.pp = pp.toFixed(1);
1284
- (function() {
1285
- var el = document.getElementById('cacheShare');
1286
- var card = el ? el.closest('div.rounded-xl') : null;
1287
- if (!card || card.dataset.sharetip) return;
1288
- card.dataset.sharetip = '1';
1289
- card.addEventListener('mouseenter', function(ev) {
1290
- showTip(tipShareHTML(Number(el.dataset.share || 0), Number(el.dataset.pp || 0)), ev.clientX, ev.clientY);
1291
- });
1292
- card.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
1293
- card.addEventListener('mouseleave', hideTip);
1294
- })();
1295
-
1296
- (function() {
1297
- var el = document.getElementById('ticker');
1298
- if (!el) return;
1299
- var tops = topModels(DATA.byModel || {}, 5);
1300
- var parts = tops.map(function(m) {
1301
- var b = (DATA.byModel || {})[m];
1302
- var v = b.input + b.output + b.cacheRead + b.cacheWrite;
1303
- var nm = m.toUpperCase().replace(/-(FREE|CONTRIBUTOR.*|NEXT|LATEST)$/, '').replace(/[-.]?\d[\d.]*/, '').replace(/-V(?=-|$)/, '');
1304
- return nm + ' ' + fmtShort(v);
1305
- });
1306
- if (!parts.length) return;
1307
- var half = parts.join(' \u25c6 ');
1308
- el.textContent = half + ' \u25c6 ' + half;
1309
- })();
1310
- renderStrip(t);
1311
- bindGraphTabs();
1312
- bindCmdTable();
1313
- bindRecentTable();
1314
- renderGraph(d.byDay || {});
1315
- renderModels();
1316
- renderRecent();
1317
- renderCmd();
1318
-
1319
- if (window.lucide) lucide.createIcons();
1320
- if (hasGsap && !render.introDone) {
1321
- render.introDone = true;
1322
- gsap.from('.hero-in', { y: 26, opacity: 0, duration: 0.8, ease: 'power3.out', stagger: 0.08 });
1323
- }
1324
- observe();
1325
- }
1326
- // shadcn/ui badge: default/secondary/destructive/outline carry the meaning,
1327
- // the label carries the detail. Built as DOM nodes so provider error notes
1328
- // never reach innerHTML.
1329
- function statusBadge(r) {
1330
- var b = document.createElement('span');
1331
- b.className = 'badge';
1332
- if (r.st === 'error') {
1333
- b.classList.add('destructive');
1334
- b.textContent = statusLabel(r);
1335
- b.title = 'error' + (r.code ? ' ' + r.code : '') + (r.note ? ' — ' + r.note : '');
1336
- } else if (r.st === 'aborted') {
1337
- b.classList.add('outline');
1338
- b.textContent = 'aborted';
1339
- b.title = r.note || 'Interrupted by user';
1340
- } else {
1341
- b.classList.add('secondary');
1342
- b.textContent = 'completed';
1343
- if (r.note) b.title = r.note;
1344
- }
1345
- return b;
1346
- }
1347
- function renderRecent() {
1348
- var body = document.getElementById('recent');
1349
- body.innerHTML = '';
1350
- var all = DATA.recent || [];
1351
- recentRows = all.slice();
1352
- sortRecent();
1353
- var recentPages = Math.max(1, Math.ceil(recentRows.length / recentPer));
1354
- if (recentPage > recentPages) recentPage = recentPages;
1355
- var recentStart = (recentPage - 1) * recentPer;
1356
- var rows = recentRows.slice(recentStart, recentStart + recentPer);
1357
- rows.forEach(function(r, i) {
1358
- var v = vendorOf(r.m);
1359
- var measured = costIsMeasured(r);
1360
- var tr = document.createElement('tr');
1361
- tr.className = 'rrow' + (i < Math.min(recentRows.length - recentStart, recentPer) - 1 ? ' rowline' : '');
1362
- tr.innerHTML = '<td class="py-2.5 pr-3 truncate" style="min-width: 0"></td>' +
1363
- '<td class="text-right py-2.5 pr-3 whitespace-nowrap" style="color:#fb923c"></td>' +
1364
- '<td class="text-right py-2.5 pr-3 whitespace-nowrap" style="color:var(--accent)"></td>' +
1365
- '<td class="text-right py-2.5 pr-3 whitespace-nowrap" style="color:var(--dim)">–</td>' +
1366
- '<td class="py-2.5 pr-3 whitespace-nowrap"></td>' +
1367
- '<td class="text-right py-2.5 pr-3 whitespace-nowrap"></td>' +
1368
- '<td class="text-right py-2.5 whitespace-nowrap" style="color: var(--dim)"></td>';
1369
- var tds = tr.children;
1370
- var dot = '<span style="display:inline-block;width:8px;height:8px;border-radius:99px;background:' + v.color + ';margin-right:8px"></span>';
1371
- tds[0].innerHTML = dot + '<span></span>';
1372
- tds[0].querySelector('span:last-child').textContent = displayModel(r.m);
1373
- tds[1].textContent = fmt(r.i);
1374
- tds[2].textContent = fmt(r.o);
1375
- tds[3].textContent = speedText(r);
1376
- tds[4].appendChild(statusBadge(r));
1377
- tds[5].textContent = (measured ? '' : '~') + fxMoney(displayCost(r));
1378
- tds[5].title = measured
1379
- ? 'measured — charged by the provider'
1380
- : (costRecorded(r)
1381
- ? 'est. — provider charged ' + fxMoney(r.usd) + ' (free or local), modeled from tokens'
1382
- : 'est. — modeled from tokens');
1383
- if (measured) tds[5].style.color = 'var(--ink)';
1384
- tds[6].textContent = whenStamp(r.t);
1385
- hoverRecent(tr, r);
1386
- body.appendChild(tr);
1387
- });
1388
- document.getElementById('recentCount').textContent = all.length ? recentRows.length + ' requests' : '';
1389
- paintPager('recentPages', 'recentRange', recentPage, recentPer, recentRows.length, function(p) { recentPage = p; renderRecent(); });
1390
- document.getElementById('recentTable').style.display = all.length ? '' : 'none';
1391
- document.getElementById('emptyRecent').classList.toggle('hidden', all.length > 0);
1392
- }
1393
-
1394
- var seen = new WeakSet();
1395
- function observe() {
1396
- var els = document.querySelectorAll('.rise');
1397
- if (!('IntersectionObserver' in window) || reduce) {
1398
- els.forEach(function(el) { el.classList.add('in'); });
1399
- return;
1400
- }
1401
- var io = new IntersectionObserver(function(entries) {
1402
- entries.forEach(function(e) {
1403
- if (e.isIntersecting && !seen.has(e.target)) {
1404
- seen.add(e.target); e.target.classList.add('in'); io.unobserve(e.target);
1405
- }
1406
- });
1407
- }, { threshold: 0.12 });
1408
- els.forEach(function(el) { if (!seen.has(el)) io.observe(el); });
1409
- }
1410
-
1411
- var lastJson = '';
1412
- function load() {
1413
- // Served mode only: file:// exports have no data.json endpoint, so
1414
- // polling there would just burn cycles on 404s.
1415
- if (window.location.protocol === 'file:') return;
1416
- // Skip background tabs: no render work while hidden, instant refresh on return.
1417
- if (document.hidden) return;
1418
- fetch('data.json').then(function(r) { return r.json(); }).then(function(d) {
1419
- var json = JSON.stringify(d);
1420
- if (json === lastJson) return;
1421
- lastJson = json;
1422
- render(d);
1423
- }).catch(function() {
1424
- var cards = document.getElementById('modelCards');
1425
- if (cards && !cards.children.length) cards.innerHTML = '<div class="empty md:col-span-3">' + emptyState('cloud-off', 'Could not load data', 'Serve with tersio gain instead of opening this file directly.') + '</div>';
1426
- if (window.lucide) lucide.createIcons();
1427
- observe();
1428
- });
1429
- }
1430
- // Live view: re-read data.json every 5s while served and visible; identical
1431
- // payloads skip render. The manual Reload button stays as an instant refresh.
1432
- setInterval(load, 5000);
1433
- document.addEventListener('visibilitychange', function() { if (!document.hidden) load(); });
1434
- function toast(title, desc, icon) {
1435
- var wrap = document.querySelector('.toaster');
1436
- if (!wrap) {
1437
- wrap = document.createElement('div');
1438
- wrap.className = 'toaster';
1439
- wrap.popover = 'manual';
1440
- document.body.appendChild(wrap);
1441
- if (wrap.showPopover) wrap.showPopover(); // top layer: paints above the native settings dialog
1442
- }
1443
- var el = document.createElement('div');
1444
- el.className = 'toast';
1445
- el.innerHTML = '<span class="toast-icon"><i data-lucide="' + (icon || 'check') + '" class="size-4"></i></span><div><p class="toast-title"></p><p class="toast-desc"></p></div>';
1446
- el.querySelector('.toast-title').textContent = title;
1447
- el.querySelector('.toast-desc').textContent = desc;
1448
- wrap.appendChild(el);
1449
- if (window.lucide) lucide.createIcons();
1450
- setTimeout(function() {
1451
- el.classList.add('out');
1452
- setTimeout(function() { el.remove(); }, 200);
1453
- }, 4000);
1454
- }
1455
- document.getElementById('reload').addEventListener('click', load);
1456
- (function() {
1457
- var dlg = document.getElementById('settings');
1458
- if (dlg && typeof dlg.showModal === 'function') {
1459
- document.getElementById('settingsBtn').addEventListener('click', function() { dlg.showModal(); });
1460
- document.getElementById('settingsClose').addEventListener('click', function() { dlg.close(); });
1461
- dlg.addEventListener('click', function(ev) { if (ev.target === dlg) dlg.close(); });
1462
- }
1463
- var md = document.getElementById('modelDialog');
1464
- if (md && typeof md.showModal === 'function') {
1465
- document.getElementById('mdClose').addEventListener('click', function() { md.close(); });
1466
- md.addEventListener('click', function(ev) { if (ev.target === md) md.close(); });
1467
- }
1468
- })();
1469
- (function() {
1470
- var resetBtn = document.getElementById('reset');
1471
- if (!resetBtn) return;
1472
- if (window.location.protocol === 'file:') { resetBtn.style.display = 'none'; return; }
1473
- var label = document.getElementById('resetLabel');
1474
- resetBtn.addEventListener('click', function() {
1475
- if (resetBtn.dataset.armed) {
1476
- delete resetBtn.dataset.armed;
1477
- label.textContent = 'Clearing…';
1478
- fetch('reset', { method: 'POST' }).then(function(r) { return r.json(); }).then(function() {
1479
- label.textContent = 'Reset';
1480
- load();
1481
- toast('Statistics reset', 'The usage statistics view now starts fresh. Transcripts and RTK history were never touched.', 'rotate-ccw');
1482
- }).catch(function() {
1483
- label.textContent = 'Reset';
1484
- toast('Reset failed', 'Could not reach the server. Try again.', 'circle-alert');
1485
- });
1486
- } else {
1487
- resetBtn.dataset.armed = '1';
1488
- label.textContent = 'Sure?';
1489
- setTimeout(function() { delete resetBtn.dataset.armed; if (label.textContent === 'Sure?') label.textContent = 'Reset'; }, 3000);
1490
- }
1491
- });
1492
- })();
1493
- load();
1494
- observe();
1495
- })();