@krtclcdy/tersio 2.22.0 → 2.23.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.
Files changed (47) hide show
  1. package/README.md +38 -30
  2. package/cli/common.js +43 -39
  3. package/cli/currency.js +2 -2
  4. package/cli/dashboard.js +60 -116
  5. package/cli/doctor-fix.js +35 -29
  6. package/cli/doctor.js +26 -15
  7. package/cli/install.js +15 -34
  8. package/cli/profile.js +2 -1
  9. package/cli/settings.js +6 -6
  10. package/cli/update.js +12 -11
  11. package/cli/usage.js +6 -1
  12. package/dashboard/dist/index.html +65 -0
  13. package/extensions/ai-addons-updater/index.js +7 -3
  14. package/extensions/ai-addons-updater/index.ts +15 -3
  15. package/extensions/caveman-session/index.js +17 -12
  16. package/extensions/caveman-session/index.ts +17 -12
  17. package/extensions/caveman-session/rule.md +90 -0
  18. package/extensions/combo-toggle/index.js +22 -2
  19. package/extensions/combo-toggle/index.ts +26 -2
  20. package/extensions/lib/utils.js +20 -4
  21. package/extensions/lib/utils.ts +18 -4
  22. package/extensions/rtk-session/index.js +11 -10
  23. package/extensions/rtk-session/index.ts +10 -11
  24. package/extensions/shared/plugin-settings.js +37 -3
  25. package/extensions/shared/plugin-settings.ts +34 -3
  26. package/extensions/shared/rtk-gain.js +2 -2
  27. package/extensions/shared/rtk-gain.ts +3 -3
  28. package/extensions/shared/session-state.js +5 -3
  29. package/extensions/shared/session-state.ts +4 -3
  30. package/extensions/shared/types.ts +5 -0
  31. package/extensions/shared/usage-ledger.js +114 -4
  32. package/extensions/shared/usage-ledger.ts +122 -4
  33. package/extensions/tersio-commands/index.js +10 -13
  34. package/extensions/tersio-commands/index.ts +10 -13
  35. package/package.json +21 -12
  36. package/tersio.js +10 -10
  37. package/dashboard/charts.js +0 -1208
  38. package/dashboard/core.js +0 -357
  39. package/dashboard/settings.js +0 -166
  40. package/dashboard/share.js +0 -218
  41. package/dashboard/styles.css +0 -379
  42. package/dashboard/template.html +0 -515
  43. package/extensions/shared/mode-reinforcement.ts +0 -29
  44. package/gain/dist/brand.webp +0 -0
  45. package/gain/dist/index.html +0 -65
  46. package/gain/dist/vite.svg +0 -1
  47. /package/dashboard/{brand.webp → dist/brand.webp} +0 -0
@@ -1,1208 +0,0 @@
1
- (function() {
2
- var T = window.Tersio;
3
- var fmt = T.fmt, fmtShort = T.fmtShort, CURS = T.CURS, FLAGS = T.FLAGS, fx = T.fx,
4
- fxMoney = T.fxMoney, esc = T.esc, countUp = T.countUp,
5
- showTip = T.showTip, moveTip = T.moveTip, hideTip = T.hideTip,
6
- reduce = T.reduce, hasGsap = T.hasGsap, hasST = T.hasST;
7
- 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>';
8
- var PROVIDERS = [
9
- [/openai|codex|gpt-|o1/i, 'OpenAI', 'openai', '#fff', OPENAI_SVG],
10
- [/muse/i, 'Meta', 'meta', '#0082fb'],
11
- [/deepseek/i, 'DeepSeek', 'deepseek', '#4d6bfe'],
12
- [/qwen|qwq/i, 'Qwen', 'qwen', '#6950EF'],
13
- [/glm|z-ai|zhipu/i, 'Z.ai', 'zdotai', '#2D2D2D'],
14
- [/mimo/i, 'Xiaomi', 'xiaomi', '#ff6900'],
15
- [/kimi|moonshot/i, 'Moonshot', 'kimi', '#a855f7'],
16
- [/minimax/i, 'MiniMax', 'minimax', '#e11d48'],
17
- [/nemotron|nvidia/i, 'NVIDIA', 'nvidia', '#76b900'],
18
- [/mistral/i, 'Mistral', 'mistralai', '#ff7000'],
19
- [/claude|anthropic/i, 'Anthropic', 'anthropic', '#d97757'],
20
- [/gemini|google|gemma/i, 'Google', 'google', '#4285F4'],
21
- ];
22
- function vendorOf(model) {
23
- for (var i = 0; i < PROVIDERS.length; i++) {
24
- if (PROVIDERS[i][0].test(model)) return { name: PROVIDERS[i][1], slug: PROVIDERS[i][2], color: PROVIDERS[i][3], svg: PROVIDERS[i][4] };
25
- }
26
- return { name: 'Other', slug: '', color: '#71717a' };
27
- }
28
- // Brand glyph: inline SVG when the provider ships one (OpenAI), else the
29
- // Simple Icons CDN; unknown / Other providers render a clean Lucide
30
- // sparkles icon with no solid background as a placeholder.
31
- function brandHTML(v, small) {
32
- if (v.svg) {
33
- return '<span class="brandmark' + (small ? ' sm' : '') + '" style="background:' + v.color + '" title="' + v.name + '">' + v.svg + '</span>';
34
- }
35
- if (!v.slug) {
36
- return '<span class="brandmark placeholder' + (small ? ' sm' : '') + '" title="' + v.name + '">' +
37
- '<i data-lucide="sparkles" class="fb" style="display:grid"></i></span>';
38
- }
39
- var icon = '<img src="https://cdn.simpleicons.org/' + v.slug + '/white" alt="" loading="lazy" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'grid\';">';
40
- var fb = '<i data-lucide="sparkles" class="fb"></i>';
41
- return '<span class="brandmark' + (small ? ' sm' : '') + '" style="background:' + v.color + '" title="' + v.name + '">' + icon + fb + '</span>';
42
- }
43
- var PALETTE = ['#34d399', '#818cf8', '#22d3ee', '#fbbf24', '#f472b6', '#a78bfa', '#fb923c', '#2dd4bf'];
44
-
45
- function dayKey(d) {
46
- return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
47
- }
48
- // Absolute local stamp for the When column: "Sep 18, 11:14:08 PM". The
49
- // column is sortable, and a relative label made that ordering unreadable.
50
- function whenStamp(ts) {
51
- var d = new Date(ts);
52
- return d.toLocaleString('en-US', { month: 'short', day: 'numeric' }) + ', ' +
53
- d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', second: '2-digit' });
54
- }
55
- // Per-request generation time + output throughput from the message-level
56
- // `duration` (ms). Missing → '–'. Throughput uses output tokens since
57
- // that is what streams to the user during the measured window.
58
- function fmtDur(ms) {
59
- if (ms === undefined) return '–';
60
- var s = ms / 1000;
61
- return (s < 10 ? s.toFixed(1) : Math.round(s)) + 's';
62
- }
63
- function speedText(r) {
64
- if (r.d === undefined) return '–';
65
- var tps = r.d > 0 ? r.o / (r.d / 1000) : 0;
66
- return fmtDur(r.d) + ' ⚡' + (tps < 10 ? tps.toFixed(1) : Math.round(tps)) + '/s';
67
- }
68
- function speedTitle(r) {
69
- if (r.d === undefined) return 'no duration recorded';
70
- var tps = r.d > 0 ? r.o / (r.d / 1000) : 0;
71
- return '⏱ ' + (r.d / 1000).toFixed(1) + 's elapsed with the model, ⚡ ' + r.o + ' output tokens (' + tps.toFixed(1) + ' tok/s)';
72
- }
73
- function modelTotal(byModel, m) {
74
- var b = byModel[m] || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
75
- return b.input + b.output + b.cacheRead + b.cacheWrite;
76
- }
77
- var cardsShown = false;
78
- function topModels(byModel, n) {
79
- return Object.keys(byModel).sort(function(a, b) { return modelTotal(byModel, b) - modelTotal(byModel, a); }).slice(0, n);
80
- }
81
- // Display form for folded keys: namespace stays lowercase, model segments
82
- // title-case with version dots kept (`deepseek-v4.1-flash` →
83
- // `Deepseek-V4.1-Flash`). Keys arrive folded, so this prettifies only.
84
- function displayModel(m) {
85
- var bare = String(m).replace(/(?::free|-free)$/i, '');
86
- function cap(s) {
87
- var low = s.toLowerCase();
88
- if (low === 'openai') return 'OpenAI';
89
- if (low === 'ai') return 'AI';
90
- if (/^\d+[a-z]+$/.test(low)) return low.toUpperCase();
91
- if (s.length <= 2) return s.toUpperCase();
92
- return s[0].toUpperCase() + s.slice(1).toLowerCase();
93
- }
94
- function seg(s) { return s.split('.').map(cap).join('.'); }
95
- function words(s) { return s.split(/[-_:]+/).filter(Boolean).map(seg).join('-'); }
96
- var slash = bare.indexOf('/');
97
- if (slash >= 0) return bare.slice(0, slash).toLowerCase() + '/' + words(bare.slice(slash + 1));
98
- return words(bare);
99
- }
100
- function allDays() {
101
- var set = {};
102
- Object.keys(T.getData().byDay || {}).forEach(function(d) { set[d] = 1; });
103
- Object.keys(T.getData().byDayModel || {}).forEach(function(d) { set[d] = 1; });
104
- return Object.keys(set).sort();
105
- }
106
-
107
- function dateHead(key) {
108
- var dt = new Date(key + 'T12:00:00');
109
- return isNaN(dt) ? key : dt.toLocaleString('en-US', { month: 'short', day: 'numeric' }).toUpperCase();
110
- }
111
- function shortName(m) { var d = displayModel(m); return d.length > 22 ? d.slice(0, 21) + '...' : d; }
112
- function tipDayHTML(key, total, rows) {
113
- var h = '<div class="tt">' + dateHead(key) + '</div><div class="tv">' + fmtShort(total) + ' total</div>';
114
- rows.forEach(function(r) {
115
- h += '<div class="tr"><span class="sw" style="background:' + r[2] + '"></span>' +
116
- '<span class="tn">' + shortName(r[0]) + '</span><span class="tvr">' + fmtShort(r[1]) + '</span></div>';
117
- });
118
- return h;
119
- }
120
- function tipModelHTML(m) {
121
- var b = (T.getData().byModel || {})[m] || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
122
- var req = ((T.getData().byModelMessages || {})[m]) || 0;
123
- var denom = b.input + b.cacheRead;
124
- var hit = denom ? (b.cacheRead / denom * 100).toFixed(1) + '%' : '-';
125
- var total = b.input + b.output + b.cacheRead + b.cacheWrite;
126
- var cb = ((T.getData().byModelBucketUsd || {})[m]) || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
127
- var h = '<div class="tt">' + shortName(m) + '</div><div class="tv">' + fmtShort(total) + ' total (' + fxMoney(cb.input + cb.output + cb.cacheRead + cb.cacheWrite) + ')</div>';
128
- 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>';
129
- 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>';
130
- 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>';
131
- 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>';
132
- h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">requests</span><span class="tvr">' + fmt(req) + '</span></div>';
133
- h += '<div class="tr"><span class="sw" style="background:var(--dim)"></span><span class="tn">cache hit</span><span class="tvr">' + hit + '</span></div>';
134
- return h;
135
- }
136
- function stampLocal(ts) {
137
- var d = new Date(ts);
138
- function p(n) { return String(n).padStart(2, '0'); }
139
- return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
140
- }
141
- // Per-request outcome. OMP records stopReason on every assistant turn and,
142
- // on failure, the HTTP errorStatus + errorMessage; toolUse turns are
143
- // ordinary completed turns, so only error and aborted stand apart.
144
- var STATUS_RANK = { error: 0, aborted: 1, completed: 2 };
145
- function statusRank(r) { var k = STATUS_RANK[r.st]; return k === undefined ? 2 : k; }
146
- function statusLabel(r) {
147
- if (r.st === 'error') return 'error' + (r.code ? ' ' + r.code : '');
148
- return r.st === 'aborted' ? 'aborted' : 'completed';
149
- }
150
- function statusColor(r) { return r.st === 'error' ? '#f87171' : r.st === 'aborted' ? '#fbbf24' : 'var(--accent)'; }
151
- // Cost precedence. A recorded charge > 0 is authoritative and shown bare.
152
- // A recording of exactly 0 usually means a free/local provider (every row on
153
- // a local model reports 0) and would blank the column, so those fall back to
154
- // the modeled figure carrying the dashboard's usual "~" estimate marker.
155
- // costRecorded() still reports the raw 0 so the tooltip can show both.
156
- function costRecorded(r) { return typeof r.usd === 'number'; }
157
- function costIsMeasured(r) { return costRecorded(r) && r.usd > 0; }
158
- function displayCost(r) { return costIsMeasured(r) ? r.usd : (r.est || 0); }
159
-
160
- function tipRecentHTML(r) {
161
- var tps = r.d !== undefined && r.d > 0 ? r.o / (r.d / 1000) : 0;
162
- var isEst = !costIsMeasured(r);
163
- var h = '<div class="tt">' + esc(shortName(r.m)) + '</div><div class="tv">' + stampLocal(r.t) + '</div>';
164
- h += '<div class="tr"><span class="sw" style="background:#fb923c"></span><span class="tn">input</span><span class="tvr">' + fmt(r.i) + '</span></div>';
165
- 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>';
166
- 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>';
167
- 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>';
168
- 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>';
169
- 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>';
170
- h += '<div class="tr"><span class="sw" style="background:' + statusColor(r) + '"></span><span class="tn">status</span><span class="tvr">' + statusLabel(r) + '</span></div>';
171
- 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>';
172
- 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>';
173
- if (r.note) h += '<div class="tnote">' + esc(r.note) + '</div>';
174
- return h;
175
- }
176
- // Zone indicators: Lucide face per savings-bento metric. Coarse by design;
177
- // captions say est. where modeled. Icons paint via showTip/render createIcons.
178
- function zoneIcon(glyph, cls) { return '<i data-lucide="' + glyph + '" class="zface ' + cls + '"></i>'; }
179
- function co2Zone(g) {
180
- if (!(g > 0)) return ['minus', 'no data', ''];
181
- if (g <= 50) return ['sprout', 'light', 'good'];
182
- if (g <= 500) return ['smile', 'moderate', 'good'];
183
- if (g <= 2000) return ['meh', 'heavy', 'warn'];
184
- return ['flame', 'very high', 'bad'];
185
- }
186
- function levZone(x) {
187
- if (!(x > 0)) return ['minus', 'no savings yet', ''];
188
- if (x >= 3) return ['rocket', 'high leverage', 'good'];
189
- if (x >= 1) return ['smile', 'solid', 'good'];
190
- return ['meh', 'light', 'warn'];
191
- }
192
- function shareZone(p) {
193
- if (!(p > 0)) return ['minus', 'uncached', ''];
194
- if (p >= 80) return ['rocket', 'high', 'good'];
195
- if (p >= 50) return ['smile', 'good', 'good'];
196
- return ['meh', 'low', 'warn'];
197
- }
198
- function tipCo2HTML() {
199
- var z = co2Zone(T.getData().co2g || 0);
200
- var h = '<div class="tt">CO2</div><div class="tv">~' + (T.getData().co2g || 0).toFixed(1) + 'g est.</div>';
201
- 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>';
202
- h += '<div class="tr"><span class="sw" style="background:var(--accent)"></span><span class="tn">energy</span><span class="tvr">~' + (T.getData().energyWh || 0).toFixed(1) + ' Wh</span></div>';
203
- return h;
204
- }
205
- function tipSavedHTML() {
206
- var usd = T.getData().usd || 0, saved = T.getData().savedUsd || 0;
207
- var lev = usd ? saved / usd : 0;
208
- var z = levZone(lev);
209
- var t = T.getData().tokens || { cacheRead: 0 };
210
- var h = '<div class="tt">Saved by cache</div><div class="tv">' + fxMoney(saved) + ' est.</div>';
211
- 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>';
212
- 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>';
213
- 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>';
214
- return h;
215
- }
216
- function tipShareHTML(share, pp) {
217
- var z = shareZone(share);
218
- var t = T.getData().tokens || { cacheRead: 0, cacheWrite: 0 };
219
- var h = '<div class="tt">Cache share</div><div class="tv">' + share + '%</div>';
220
- 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>';
221
- 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>';
222
- 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>';
223
- 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>';
224
- return h;
225
- }
226
- function hoverModel(el, m) {
227
- el.addEventListener('mouseenter', function(ev) { showTip(tipModelHTML(m), ev.clientX, ev.clientY); });
228
- el.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
229
- el.addEventListener('mouseleave', hideTip);
230
- }
231
- function hoverRecent(el, r) {
232
- el.addEventListener('mouseenter', function(ev) { showTip(tipRecentHTML(r), ev.clientX, ev.clientY); });
233
- el.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
234
- el.addEventListener('mouseleave', hideTip);
235
- }
236
- // Model detail dialog: shadcn chart language over daily model volume.
237
- // byDayModel is keyed by DAY (Record<day, Record<model, tokens>>); the old
238
- // mdSeries read it keyed by model and rendered all-zero charts. Area chart
239
- // with gradient + hover crosshair, downsampled rounded bars, token-mix
240
- // donut, KPI strip, 30/90/ALL range tabs.
241
- var mdModel = null, mdSpan = 'all', mdHover = { pts: [], days: [] };
242
- function mdSeries(m) {
243
- var days = allDays();
244
- var per = T.getData().byDayModel || {};
245
- return days.map(function(d) { return { day: d, v: ((per[d] || {})[m] || 0) }; });
246
- }
247
- function mdSlice(series) {
248
- if (mdSpan === 'all' || series.length <= mdSpan) return series;
249
- return series.slice(series.length - mdSpan);
250
- }
251
- function mdMonthTicks(days) {
252
- if (!days.length) return [];
253
- var seen = {}, out = [];
254
- days.forEach(function(d) {
255
- var t = d.slice(0, 7);
256
- if (!seen[t]) { seen[t] = 1; out.push({ day: d, label: new Date(d + 'T12:00:00').toLocaleString('en-US', { month: 'short' }) }); }
257
- });
258
- return out.slice(-6);
259
- }
260
- function mdShortDay(d) { return new Date(d + 'T12:00:00').toLocaleString('en-US', { month: 'short', day: 'numeric' }); }
261
- function mdAreaChart(svg, pts, days) {
262
- var W = 560, H = 190, max = 1, i, x, y;
263
- pts.forEach(function(p) { max = Math.max(max, p); });
264
- var step = pts.length > 1 ? (W - 16) / (pts.length - 1) : 0;
265
- function X(i) { return 8 + i * step; }
266
- function Y(p) { return 14 + (1 - p / max) * (H - 36); }
267
- // shadcn CartesianGrid language: subtle dashed horizontals only.
268
- var grid = '';
269
- 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) + '" stroke-dasharray="3 4"/>'; }
270
- var d = pts.map(function(p, i) { return (i ? 'L' : 'M') + X(i).toFixed(1) + ' ' + Y(p).toFixed(1); }).join(' ');
271
- var last = pts.length ? { x: X(pts.length - 1), y: Y(pts[pts.length - 1]) } : { x: 8, y: H - 22 };
272
- svg.innerHTML = '<defs><linearGradient id="mdGrad" x1="0" y1="0" x2="0" y2="1">' +
273
- '<stop offset="0" style="stop-color:var(--accent)" stop-opacity="0.32"/>' +
274
- '<stop offset="1" style="stop-color:var(--accent)" stop-opacity="0.02"/></linearGradient></defs>' +
275
- grid +
276
- (pts.length ? '<path class="area" d="' + d + ' L' + last.x.toFixed(1) + ' ' + (H - 22) + ' L8 ' + (H - 22) + ' Z"/>' : '') +
277
- (pts.length ? '<path class="line" d="' + d + '" stroke-width="2.5"/>' : '') +
278
- (pts.length ? '<circle class="dot" cx="' + last.x.toFixed(1) + '" cy="' + last.y.toFixed(1) + '" r="5"/>' : '') +
279
- (pts.length ? '<circle class="hoverdot" id="mdHoverDot" cx="-10" cy="-10" r="5" style="display:none"/>' : '');
280
- mdHover = { pts: pts, days: days };
281
- svg.onmousemove = function(ev) {
282
- var tip = document.getElementById('mdTip');
283
- if (!tip || !mdHover.pts.length) return;
284
- var r = svg.getBoundingClientRect();
285
- var fx = (ev.clientX - r.left) / r.width * W;
286
- var idx = Math.max(0, Math.min(mdHover.pts.length - 1, Math.round((fx - 8) / (step || 1))));
287
- var dot = document.getElementById('mdHoverDot');
288
- if (dot) { dot.setAttribute('cx', X(idx).toFixed(1)); dot.setAttribute('cy', Y(mdHover.pts[idx]).toFixed(1)); dot.style.display = ''; }
289
- tip.innerHTML = '<div class="tt">' + mdShortDay(mdHover.days[idx]) + '</div><div class="tv">' + fmtShort(mdHover.pts[idx]) + ' tokens</div>';
290
- tip.style.display = 'block';
291
- // Raw % positioning overflows past the dialog edge on the first/last
292
- // points (the dialog clips it). Measure and pin inside the card.
293
- var chart = svg.closest ? svg.closest('.md-chart') : null;
294
- var px = svg.offsetLeft + (X(idx) / W) * svg.clientWidth;
295
- if (chart) {
296
- var half = tip.offsetWidth / 2;
297
- px = Math.max(half + 4, Math.min(chart.clientWidth - half - 4, px));
298
- }
299
- tip.style.left = px + 'px';
300
- tip.style.top = '8px';
301
- };
302
- svg.onmouseleave = function() {
303
- var tip = document.getElementById('mdTip');
304
- if (tip) tip.style.display = 'none';
305
- var dot = document.getElementById('mdHoverDot');
306
- if (dot) dot.style.display = 'none';
307
- };
308
- }
309
- function mdBars(el, vals, days) {
310
- el.innerHTML = '';
311
- var max = 1, i, total = 0, active = 0, peak = 0, peakI = 0;
312
- vals.forEach(function(v, k) { max = Math.max(max, v); total += v; if (v > 0) { active++; if (v > peak) { peak = v; peakI = k; } } });
313
- var n = vals.length, bucket = 1;
314
- if (n > 90) bucket = Math.ceil(n / 90);
315
- for (i = 0; i < n; i += bucket) {
316
- var sum = 0, c = 0;
317
- for (var j = i; j < Math.min(n, i + bucket); j++) { sum += vals[j]; c++; }
318
- var avg = sum / c;
319
- var s = document.createElement('span');
320
- if (!avg) s.className = 'zero';
321
- else if (avg === max) s.className = 'top';
322
- s.style.height = Math.max(avg ? 5 : 2, Math.round(avg / max * 100)) + '%';
323
- s.title = mdShortDay(days[i]) + ' · ' + fmtShort(Math.round(sum));
324
- (function(day, tokens) {
325
- s.addEventListener('mouseenter', function(ev) {
326
- showTip('<div class="tt">' + day + '</div><div class="tv">' + tokens + ' tokens</div>', ev.clientX, ev.clientY);
327
- });
328
- s.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
329
- s.addEventListener('mouseleave', hideTip);
330
- })(mdShortDay(days[i]), fmtShort(Math.round(sum)));
331
- el.appendChild(s);
332
- }
333
- // Sparse models leave the bars mostly flat: name the peak day, its share
334
- // of the period, and the per-active-day average instead of empty space.
335
- var note = document.getElementById('mdBarsNote');
336
- if (note) {
337
- if (!active) note.textContent = 'no activity in range';
338
- 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';
339
- }
340
- }
341
- function mdDonut(svg, parts) {
342
- var r = 54, C = 2 * Math.PI * r, off = 0;
343
- var total = parts.reduce(function(a, p) { return a + p.v; }, 0) || 1;
344
- var live = parts.filter(function(p) { return p.v > 0; });
345
- // shadcn pie paddingAngle: breathing room between segments.
346
- var gap = live.length > 1 ? 4 : 0;
347
- var html = '<circle class="tk" cx="70" cy="70" r="' + r + '"/>';
348
- parts.forEach(function(p) {
349
- var len = p.v / total * C;
350
- if (len <= 0) return;
351
- html += '<circle cx="70" cy="70" r="' + r + '" style="stroke:' + p.color + '" stroke-dasharray="' + Math.max(len - gap, 0.5).toFixed(1) + ' ' + (C - len + gap).toFixed(1) + '" stroke-dashoffset="' + (-off - gap / 2).toFixed(1) + '" stroke-linecap="butt"/>';
352
- off += len;
353
- });
354
- svg.innerHTML = html;
355
- document.getElementById('mdMixTotal').textContent = fmtShort(total);
356
- var legend = document.getElementById('mdMixLegend');
357
- legend.innerHTML = live.map(function(p) {
358
- return '<div class="row" data-mix="' + p.label + '"><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>';
359
- }).join('');
360
- Array.prototype.forEach.call(legend.children, function(rowEl) {
361
- var part = live.filter(function(p) { return p.label === rowEl.getAttribute('data-mix'); })[0];
362
- if (!part) return;
363
- rowEl.addEventListener('mouseenter', function(ev) {
364
- showTip('<div class="tt">' + part.label + '</div><div class="tv">' + fmtShort(part.v) + ' (' + (part.v / total * 100).toFixed(1) + '%)</div>', ev.clientX, ev.clientY);
365
- });
366
- rowEl.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
367
- rowEl.addEventListener('mouseleave', hideTip);
368
- });
369
- }
370
- function mdMonths(el, days) {
371
- el.innerHTML = '';
372
- mdMonthTicks(days).forEach(function(t) {
373
- var s = document.createElement('span');
374
- s.textContent = t.label;
375
- el.appendChild(s);
376
- });
377
- }
378
- function mdKpi(k, v, d, up) {
379
- 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>';
380
- }
381
- function mdStat(k, v, s) {
382
- 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>';
383
- }
384
- function mdRange(days) {
385
- if (!days.length) return '–';
386
- return mdShortDay(days[0]).toUpperCase() + ' → ' + mdShortDay(days[days.length - 1]).toUpperCase();
387
- }
388
- function mdTrendText(vals) {
389
- var max = 0, i;
390
- vals.forEach(function(x) { max = Math.max(max, x); });
391
- if (vals.length < 14) return max ? { t: 'peak ' + fmtShort(max) + ' in a day', up: true } : { t: 'no activity yet', up: false };
392
- var a = 0, b = 0;
393
- for (i = vals.length - 14; i < vals.length; i++) a += vals[i];
394
- for (i = Math.max(0, vals.length - 28); i < vals.length - 14; i++) b += vals[i];
395
- if (!b) return { t: fmtShort(a) + ' / 2 wks', up: a > 0 };
396
- var pct = (a - b) / b * 100;
397
- return { t: (pct >= 0 ? '+' : '') + pct.toFixed(0) + '% vs prior 2 wks', up: pct >= 0 };
398
- }
399
- function renderModelDialog(m) {
400
- var byModel = T.getData().byModel || {};
401
- var tops = topModels(byModel, Object.keys(byModel).length);
402
- var rank = tops.indexOf(m) + 1;
403
- var b = byModel[m] || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
404
- var total = modelTotal(byModel, m);
405
- var grand = 1;
406
- tops.forEach(function(k) { grand += modelTotal(byModel, k); });
407
- var v = vendorOf(m);
408
- var req = ((T.getData().byModelMessages || {})[m]) || 0;
409
- var usd = ((T.getData().byModelUsd || {})[m]) || 0;
410
- var denom = b.input + b.cacheRead;
411
- var hit = denom ? (b.cacheRead / denom * 100) : 0;
412
- var series = mdSlice(mdSeries(m));
413
- var days = series.map(function(p) { return p.day; });
414
- var vals = series.map(function(p) { return p.v; });
415
- var active = vals.filter(function(x) { return x > 0; }).length;
416
- var tr = mdTrendText(mdSeries(m).map(function(p) { return p.v; }));
417
- document.getElementById('mdBadge').innerHTML = brandHTML(v, false);
418
- var title = document.getElementById('mdTitle');
419
- title.textContent = displayModel(m);
420
- title.title = m;
421
- document.getElementById('mdSub').textContent = v.name + ' · ' + fmt(req) + ' requests · ' + (total / grand * 100).toFixed(1) + '% of volume';
422
- document.getElementById('mdRank').textContent = rank ? '#' + String(rank).padStart(2, '0') : '#–';
423
- var trend = document.getElementById('mdTrend');
424
- trend.textContent = tr.t;
425
- trend.className = 'md-trend mono' + (tr.up ? ' up' : '');
426
- document.getElementById('mdKpis').innerHTML =
427
- mdKpi('Tokens', fmtShort(total), mdRange(days), true) +
428
- mdKpi('Spend', fxMoney(usd), req ? fxMoney(usd / req) + ' / req' : 'no requests', usd > 0) +
429
- mdKpi('Cache hit', hit.toFixed(0) + '%', fmtShort(b.cacheRead) + ' cached', hit >= 50) +
430
- mdKpi('Requests', fmt(req), active + ' active days', active > 0);
431
- document.getElementById('mdMomRange').textContent = mdRange(days);
432
- mdAreaChart(document.getElementById('mdSpark'), vals, days);
433
- mdMonths(document.getElementById('mdMonths'), days);
434
- var act = document.getElementById('mdActive');
435
- act.textContent = active + ' / ' + days.length + ' days';
436
- act.className = 'md-trend mono' + (active ? ' up' : '');
437
- mdBars(document.getElementById('mdBars'), vals, days);
438
- mdMonths(document.getElementById('mdBarsMonths'), days);
439
- mdDonut(document.getElementById('mdMix'), [
440
- { label: 'Input', v: b.input, color: 'var(--accent)' },
441
- { label: 'Output', v: b.output, color: '#818cf8' },
442
- { label: 'Cache read', v: b.cacheRead, color: '#22d3ee' },
443
- { label: 'Cache write', v: b.cacheWrite, color: 'var(--dim)' },
444
- ]);
445
- document.getElementById('mdStats').innerHTML =
446
- mdStat('Input', fmtShort(b.input), null) +
447
- mdStat('Output', fmtShort(b.output), null) +
448
- mdStat('Cache read', fmtShort(b.cacheRead), hit.toFixed(0) + '% hit') +
449
- mdStat('Cache write', fmtShort(b.cacheWrite), null);
450
- if (window.lucide) lucide.createIcons();
451
- if (hasGsap && !reduce) {
452
- 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 });
453
- 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 });
454
- 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 });
455
- }
456
- }
457
- function openModelDialog(m) {
458
- var dlg = document.getElementById('modelDialog');
459
- if (!dlg || typeof dlg.showModal !== 'function') return;
460
- mdModel = m;
461
- Array.prototype.forEach.call(document.querySelectorAll('#modelDialog [data-mdspan]'), function(btn) {
462
- var on = btn.getAttribute('data-mdspan') === String(mdSpan);
463
- btn.classList.toggle('on', on);
464
- btn.onclick = function() {
465
- mdSpan = btn.getAttribute('data-mdspan') === 'all' ? 'all' : Number(btn.getAttribute('data-mdspan'));
466
- Array.prototype.forEach.call(document.querySelectorAll('#modelDialog [data-mdspan]'), function(o) { o.classList.toggle('on', o === btn); });
467
- renderModelDialog(mdModel);
468
- };
469
- });
470
- renderModelDialog(m);
471
- if (!dlg.open) dlg.showModal();
472
- }
473
- function dayModelRows(key) {
474
- var per = (T.getData().byDayModel || {})[key] || {};
475
- var tops = topModels(T.getData().byModel || {}, 8), rows = [], other = 0;
476
- tops.forEach(function(m, i) {
477
- var v = per[m] || 0;
478
- if (v) rows.push([m, v, PALETTE[i % PALETTE.length]]);
479
- });
480
- Object.keys(per).forEach(function(m) { if (tops.indexOf(m) < 0) other += per[m]; });
481
- if (other) rows.push(['Other', other, 'var(--dim)']);
482
- return rows;
483
- }
484
-
485
- var GMODE = 'daily';
486
- function mondayOf(d) {
487
- var m = new Date(d);
488
- m.setDate(d.getDate() - ((d.getDay() + 6) % 7));
489
- m.setHours(0, 0, 0, 0);
490
- return m;
491
- }
492
- function weekTipHTML(monKey, weekTotal, weekPer) {
493
- var mon = new Date(monKey + 'T12:00:00'), sun = new Date(mon);
494
- sun.setDate(mon.getDate() + 6);
495
- var head = isNaN(mon) ? monKey : mon.toLocaleString('en-US', { month: 'short', day: 'numeric' }).toUpperCase() +
496
- ' - ' + (isNaN(sun) ? '' : sun.toLocaleString('en-US', { month: 'short', day: 'numeric' }).toUpperCase());
497
- var tops = topModels(T.getData().byModel || {}, 8), rows = [], other = 0;
498
- tops.forEach(function(m, i) {
499
- var v = weekPer[m] || 0;
500
- if (v) rows.push([m, v, PALETTE[i % PALETTE.length]]);
501
- });
502
- Object.keys(weekPer).forEach(function(m) { if (tops.indexOf(m) < 0) other += weekPer[m]; });
503
- if (other) rows.push(['Other', other, 'var(--dim)']);
504
- return tipDayHTML(head, weekTotal, rows.length ? rows : [['no activity', 0, 'var(--dim)']]);
505
- }
506
- // Shared empty state (shadcn pattern): icon chip + title + hint. Callers
507
- // inject the HTML; render()'s trailing lucide.createIcons() paints icons.
508
- function emptyState(icon, title, desc) {
509
- return '<span class="empty-icon"><i data-lucide="' + icon + '" class="size-5"></i></span>' +
510
- '<p class="empty-title">' + title + '</p>' +
511
- '<p class="empty-desc">' + desc + '</p>';
512
- }
513
- function renderGraph(byDay) {
514
- var g = document.getElementById('graph');
515
- g.innerHTML = '';
516
- var today = new Date(); today.setHours(0, 0, 0, 0);
517
- var start = new Date(today); start.setDate(start.getDate() - (52 * 7) - today.getDay());
518
- var sums = {};
519
- Object.keys(byDay).forEach(function(day) {
520
- var b = byDay[day];
521
- sums[day] = b.input + b.output + b.cacheRead + b.cacheWrite;
522
- });
523
- var hasData = Object.keys(sums).some(function(k) { return sums[k] > 0; });
524
- document.getElementById('emptyGraph').classList.toggle('hidden', hasData);
525
- document.getElementById('graph').style.display = hasData ? '' : 'none';
526
- document.getElementById('graphMonths').style.display = hasData ? '' : 'none';
527
- if (!hasData) {
528
- document.getElementById('graphRange').textContent = '';
529
- document.getElementById('graphCap').textContent = 'no data in trailing 12 months';
530
- return;
531
- }
532
- // per-day values under the active mode
533
- var dayKeys = [], wk;
534
- for (wk = 0; wk < 53; wk++) for (var di = 0; di < 7; di++) {
535
- var dd = new Date(start); dd.setDate(dd.getDate() + wk * 7 + di);
536
- dayKeys.push(dayKey(dd));
537
- }
538
- var weekPer = {}, weekTotal = {}, run = 0, cellVal = {};
539
- dayKeys.forEach(function(k) {
540
- var dt = new Date(k + 'T12:00:00');
541
- if (GMODE === 'weekly' && !isNaN(dt)) {
542
- var mk = dayKey(mondayOf(dt));
543
- weekTotal[mk] = (weekTotal[mk] || 0) + (sums[k] || 0);
544
- var per = (T.getData().byDayModel || {})[k] || {};
545
- if (!weekPer[mk]) weekPer[mk] = {};
546
- Object.keys(per).forEach(function(m) { weekPer[mk][m] = (weekPer[mk][m] || 0) + per[m]; });
547
- }
548
- if (GMODE === 'cumulative') { run += sums[k] || 0; cellVal[k] = run; }
549
- });
550
- var max = 1;
551
- if (GMODE === 'weekly') {
552
- Object.keys(weekTotal).forEach(function(k) { max = Math.max(max, weekTotal[k]); });
553
- } else if (GMODE === 'cumulative') {
554
- max = Math.max(1, run);
555
- } else {
556
- Object.keys(sums).forEach(function(k) { max = Math.max(max, sums[k]); });
557
- }
558
- var months = document.getElementById('graphMonths');
559
- months.innerHTML = '';
560
- var lastMonth = '';
561
- for (var w = 0; w < 53; w++) {
562
- var col = document.createElement('div');
563
- col.style.cssText = 'display:flex;flex-direction:column;gap:3px;min-width:0;';
564
- var ml = document.createElement('span');
565
- ml.className = 'heatmonth w-full';
566
- for (var i = 0; i < 7; i++) {
567
- var cdt = new Date(start); cdt.setDate(cdt.getDate() + w * 7 + i);
568
- var key = dayKey(cdt), v, tipFn;
569
- if (GMODE === 'weekly') {
570
- var mk2 = dayKey(mondayOf(cdt));
571
- v = weekTotal[mk2] || 0;
572
- tipFn = (function(mk3, vv) {
573
- return function() { return weekTipHTML(mk3, vv, weekPer[mk3] || {}); };
574
- })(mk2, v);
575
- } else if (GMODE === 'cumulative') {
576
- v = cellVal[key] || 0;
577
- tipFn = (function(kk, vv) {
578
- return function() {
579
- var rows = dayModelRows(kk);
580
- return tipDayHTML('THROUGH ' + dateHead(kk), vv, rows.length ? rows : [['no activity', 0, 'var(--dim)']]);
581
- };
582
- })(key, v);
583
- } else {
584
- v = sums[key] || 0;
585
- tipFn = (function(kk, vv) {
586
- return function() {
587
- var rows = dayModelRows(kk);
588
- return tipDayHTML(kk, vv, rows.length ? rows : [['no activity', 0, 'var(--dim)']]);
589
- };
590
- })(key, v);
591
- }
592
- var lvl = 0;
593
- if (v > 0) lvl = Math.min(4, 1 + Math.floor((v / max) * 3.99));
594
- var cell = document.createElement('span');
595
- cell.className = 'cell' + (lvl ? ' l' + lvl : '');
596
- (function(fn) {
597
- cell.addEventListener('mouseenter', function(ev) { showTip(fn(), ev.clientX, ev.clientY); });
598
- cell.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
599
- cell.addEventListener('mouseleave', hideTip);
600
- })(tipFn);
601
- col.appendChild(cell);
602
- if (i === 0) {
603
- var mm = cdt.toLocaleString('en-US', { month: 'short' });
604
- ml.textContent = mm !== lastMonth ? mm : '';
605
- lastMonth = mm;
606
- }
607
- }
608
- g.appendChild(col);
609
- months.appendChild(ml);
610
- }
611
- Array.prototype.forEach.call(months.children, function(m) { m.style.minWidth = '0'; });
612
- document.getElementById('graphRange').textContent =
613
- dayKey(start) + ' to ' + dayKey(today);
614
- document.getElementById('graphCap').textContent =
615
- GMODE === 'weekly' ? 'weekly totals / trailing 12 months' :
616
- GMODE === 'cumulative' ? 'running total / trailing 12 months' :
617
- 'daily values / trailing 12 months';
618
- }
619
- function bindGraphTabs() {
620
- if (bindGraphTabs.done) return;
621
- bindGraphTabs.done = true;
622
- Array.prototype.forEach.call(document.querySelectorAll('#activity [data-gmode]'), function(btn) {
623
- btn.addEventListener('click', function() {
624
- Array.prototype.forEach.call(document.querySelectorAll('#activity [data-gmode]'), function(b) { b.classList.remove('on'); });
625
- btn.classList.add('on');
626
- GMODE = btn.getAttribute('data-gmode');
627
- if (T.getData()) renderGraph(T.getData().byDay || {});
628
- });
629
- });
630
- }
631
-
632
- var modelPage = 1, modelPer = 10, recentPage = 1, recentPer = 25;
633
- // Recent requests default to newest-first, which is how the transcript
634
- // arrives; every column is sortable like the command table.
635
- var recentSort = { key: 'when', dir: -1 }, recentRows = [];
636
- function recentVal(r, key) {
637
- if (key === 'model') return r.m.toLowerCase();
638
- if (key === 'input') return r.i;
639
- if (key === 'output') return r.o;
640
- if (key === 'time') return r.d === undefined ? null : r.d;
641
- if (key === 'status') return statusRank(r);
642
- if (key === 'cost') return displayCost(r);
643
- return r.t;
644
- }
645
- function sortRecent() {
646
- var k = recentSort.key, d = recentSort.dir;
647
- recentRows.sort(function(a, b) {
648
- var av = recentVal(a, k), bv = recentVal(b, k);
649
- if (av === null || av === undefined) return 1;
650
- if (bv === null || bv === undefined) return -1;
651
- if (av < bv) return -d;
652
- if (av > bv) return d;
653
- return b.t - a.t;
654
- });
655
- Array.prototype.forEach.call(document.querySelectorAll('#recentTable .thsort'), function(btn) {
656
- var on = btn.dataset.sort === k;
657
- btn.querySelector('.arr').textContent = on ? (d === 1 ? '↑' : '↓') : '⇅';
658
- btn.closest('th').setAttribute('aria-sort', on ? (d === 1 ? 'ascending' : 'descending') : 'none');
659
- });
660
- }
661
- function renderModels() {
662
- var byModel = T.getData().byModel || {};
663
- var tops = topModels(byModel, Object.keys(byModel).length).filter(function(m) { return modelTotal(byModel, m) > 0; });
664
- var cards = document.getElementById('modelCards');
665
- cards.innerHTML = '';
666
- 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>';
667
- tops.slice(0, 3).forEach(function(m, i) {
668
- var v = vendorOf(m);
669
- var card = document.createElement('div');
670
- card.className = 'modelcard relative p-4 rounded-xl overflow-hidden';
671
- if (v.svg) {
672
- var ghostSvg = document.createElement('span');
673
- ghostSvg.className = 'ghostimg'; ghostSvg.setAttribute('aria-hidden', 'true');
674
- ghostSvg.innerHTML = v.svg;
675
- card.appendChild(ghostSvg);
676
- } else if (v.slug) {
677
- var ghost = document.createElement('img');
678
- ghost.className = 'ghostimg'; ghost.alt = '';
679
- ghost.src = 'https://cdn.simpleicons.org/' + v.slug + '/white';
680
- ghost.setAttribute('aria-hidden', 'true');
681
- card.appendChild(ghost);
682
- } else {
683
- var ghostSparkle = document.createElement('i');
684
- ghostSparkle.setAttribute('data-lucide', 'sparkles');
685
- ghostSparkle.className = 'ghosticon';
686
- ghostSparkle.setAttribute('aria-hidden', 'true');
687
- card.appendChild(ghostSparkle);
688
- }
689
- card.style.cssText = 'border: 1px solid var(--line); background: var(--panel); cursor: pointer';
690
- card.setAttribute('role', 'button');
691
- var head = document.createElement('p');
692
- head.className = 'mono text-xs mb-3'; head.style.color = 'var(--dim)';
693
- head.textContent = '0' + (i + 1);
694
- var row = document.createElement('div');
695
- row.className = 'flex items-center gap-3';
696
- var badgeWrap = document.createElement('span');
697
- badgeWrap.innerHTML = brandHTML(v, false);
698
- var badge = badgeWrap.firstChild;
699
- var mid = document.createElement('div'); mid.className = 'min-w-0 flex-1';
700
- var nm = document.createElement('p'); nm.className = 'mono text-sm font-bold truncate'; nm.textContent = displayModel(m); nm.title = m;
701
- var vn = document.createElement('p'); vn.className = 'mono text-xs truncate'; vn.style.color = 'var(--dim)'; vn.textContent = v.name;
702
- mid.appendChild(nm); mid.appendChild(vn);
703
- var tot = document.createElement('p'); tot.className = 'mono font-bold text-lg shrink-0'; tot.textContent = fmtShort(modelTotal(byModel, m));
704
- row.appendChild(badge); row.appendChild(mid); row.appendChild(tot);
705
- card.appendChild(head); card.appendChild(row);
706
- hoverModel(card, m);
707
- (function(model) { card.addEventListener('click', function() { openModelDialog(model); }); })(m);
708
- cards.appendChild(card);
709
- });
710
- var ol = document.getElementById('models');
711
- ol.innerHTML = '';
712
- var max = 1;
713
- tops.forEach(function(m) { max = Math.max(max, modelTotal(byModel, m)); });
714
- var modelPages = Math.max(1, Math.ceil(tops.length / modelPer));
715
- if (modelPage > modelPages) modelPage = modelPages;
716
- var modelStart = (modelPage - 1) * modelPer;
717
- tops.slice(modelStart, modelStart + modelPer).forEach(function(m, i) {
718
- var v = vendorOf(m);
719
- var li = document.createElement('li');
720
- li.className = 'mrow flex items-center gap-3 px-4 py-3' + (i < Math.min(tops.length - modelStart, modelPer) - 1 ? ' rowline' : '');
721
- var badgeWrap = document.createElement('span');
722
- badgeWrap.innerHTML = brandHTML(v, true);
723
- var badge = badgeWrap.firstChild;
724
- var mid = document.createElement('div'); mid.className = 'min-w-0 flex-1';
725
- var nm = document.createElement('p'); nm.className = 'mono text-sm truncate'; nm.textContent = displayModel(m); nm.title = m;
726
- var track = document.createElement('div'); track.className = 'bar-track mt-1.5 h-1.5 overflow-hidden';
727
- var fill = document.createElement('div'); fill.className = 'bar-fill h-full';
728
- var mv = modelTotal(byModel, m);
729
- fill.dataset.w = Math.round(mv / max * 100) + '%';
730
- fill.style.width = '0';
731
- track.appendChild(fill); mid.appendChild(nm); mid.appendChild(track);
732
- var right = document.createElement('div'); right.className = 'shrink-0 text-right';
733
- var n = document.createElement('p'); n.className = 'mono font-bold'; n.textContent = fmt(modelTotal(byModel, m));
734
- var cost = document.createElement('p'); cost.className = 'mono text-xs'; cost.style.color = 'var(--dim)';
735
- cost.textContent = fxMoney((T.getData().byModelUsd || {})[m] || 0);
736
- right.appendChild(n); right.appendChild(cost);
737
- li.appendChild(badge); li.appendChild(mid); li.appendChild(right);
738
- hoverModel(li, m);
739
- (function(model) { li.addEventListener('click', function() { openModelDialog(model); }); })(m);
740
- ol.appendChild(li);
741
- });
742
- document.getElementById('modelsCount').textContent = tops.length ? tops.length + ' models' : '';
743
- paintPager('modelPages', 'modelRange', modelPage, modelPer, tops.length, function(p) { modelPage = p; renderModels(); });
744
- ol.style.display = tops.length ? '' : 'none';
745
- document.getElementById('emptyModels').classList.toggle('hidden', tops.length > 0);
746
- if (tops.length && 'IntersectionObserver' in window && !reduce) {
747
- var fio = new IntersectionObserver(function(entries) {
748
- entries.forEach(function(e) {
749
- if (e.isIntersecting) {
750
- e.target.querySelectorAll('.bar-fill').forEach(function(f) { f.style.width = f.dataset.w || '0'; });
751
- fio.disconnect();
752
- }
753
- });
754
- }, { threshold: 0.2 });
755
- fio.observe(ol);
756
- } else if (tops.length) {
757
- ol.querySelectorAll('.bar-fill').forEach(function(f) { f.style.width = f.dataset.w || '0'; });
758
- }
759
- if (hasGsap && hasST && !cardsShown && tops.length) {
760
- cardsShown = true;
761
- gsap.from('#modelCards > div', {
762
- scale: 0.92, opacity: 0.2, duration: 0.6, ease: 'power2.out', stagger: 0.08, overwrite: true,
763
- scrollTrigger: { trigger: '#modelCards', start: 'top 88%' }
764
- });
765
- }
766
- }
767
-
768
- // Union of session tool calls + RTK-metered commands, sortable + paged.
769
- // Session tools were never metered per command: saved / avg / time show – and sort last.
770
- // Rows group by command alone, machine-wide: same command in any repo is one row.
771
- var cmdSort = { key: 'count', dir: -1 }, cmdPage = 1, cmdPer = 15, cmdRows = [];
772
- function cmdVal(r, key) {
773
- if (key === 'name') return r.name.toLowerCase();
774
- return r[key];
775
- }
776
- function sortCmd() {
777
- var k = cmdSort.key, d = cmdSort.dir;
778
- cmdRows.sort(function(a, b) {
779
- var av = cmdVal(a, k), bv = cmdVal(b, k);
780
- if (av === null || av === undefined) return 1;
781
- if (bv === null || bv === undefined) return -1;
782
- if (av < bv) return -d;
783
- if (av > bv) return d;
784
- return b.count - a.count;
785
- });
786
- Array.prototype.forEach.call(document.querySelectorAll('#tools .thsort'), function(btn) {
787
- var on = btn.dataset.sort === k;
788
- btn.querySelector('.arr').textContent = on ? (d === 1 ? '↑' : '↓') : '⇅';
789
- btn.closest('th').setAttribute('aria-sort', on ? (d === 1 ? 'ascending' : 'descending') : 'none');
790
- });
791
- }
792
- function renderCmd() {
793
- var g = (T.getData().rtkGain || {});
794
- var byName = {};
795
- function add(name, count, saved, avgPct, avgMs) {
796
- var r = byName[name];
797
- if (!r) { byName[name] = { name: name, count: count, saved: saved, avgPct: avgPct, avgMs: avgMs }; return; }
798
- r.count += count;
799
- if (saved !== null && saved !== undefined) r.saved = (r.saved || 0) + saved;
800
- }
801
- (T.getData().byTool || []).forEach(function(r) { add(r[0], r[1], null, null, null); });
802
- (g.byCommand || []).forEach(function(r) { add(r.command, r.count, r.saved, r.avgPct, r.avgMs); });
803
- cmdRows = Object.keys(byName).map(function(k) { return byName[k]; });
804
- sortCmd();
805
- var pages = Math.max(1, Math.ceil(cmdRows.length / cmdPer));
806
- if (cmdPage > pages) cmdPage = pages;
807
- var body = document.getElementById('cmdBody');
808
- body.innerHTML = '';
809
- var max = 1;
810
- cmdRows.forEach(function(r) { max = Math.max(max, r.count); });
811
- var start = (cmdPage - 1) * cmdPer;
812
- cmdRows.slice(start, start + cmdPer).forEach(function(r, i) {
813
- var n = start + i;
814
- var tr = document.createElement('tr');
815
- tr.className = 'mrow' + (i < Math.min(cmdRows.length - start, cmdPer) - 1 ? ' rowline' : '');
816
- tr.innerHTML = '<td class="text-right pr-3 py-2.5 mono text-xs w-10" style="color: var(--dim)"></td>' +
817
- '<td class="py-2.5 pr-3 truncate" style="max-width: 280px"></td>' +
818
- '<td class="text-right py-2.5 pr-3 font-bold"></td>' +
819
- '<td class="text-right py-2.5 pr-3 font-bold"></td>' +
820
- '<td class="text-right py-2.5 pr-3" style="color: var(--accent)"></td>' +
821
- '<td class="text-right py-2.5 pr-3" style="color: var(--dim)"></td>' +
822
- '<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>';
823
- var tds = tr.children;
824
- tds[0].textContent = String(n + 1).padStart(2, '0');
825
- tds[1].textContent = r.name;
826
- tds[1].title = r.name;
827
- tds[2].textContent = fmt(r.count);
828
- tds[3].textContent = r.saved === null ? '–' : fmtShort(r.saved);
829
- tds[4].textContent = r.avgPct === null ? '–' : r.avgPct.toFixed(1) + '%';
830
- tds[5].textContent = r.avgMs === null || r.avgMs === undefined ? '–' : fmtMs(r.avgMs);
831
- tds[6].firstChild.firstChild.style.width = r.count ? Math.round(r.count / max * 100) + '%' : '0';
832
- body.appendChild(tr);
833
- });
834
- document.getElementById('cmdTable').style.display = cmdRows.length ? '' : 'none';
835
- document.getElementById('emptyCmd').classList.toggle('hidden', cmdRows.length > 0);
836
- document.getElementById('toolsScope').textContent =
837
- g.commands ? fmt(cmdRows.length) + ' commands · ' + fmt(g.commands) + ' runs · ' + fmtShort(g.saved) + ' saved' : 'tool calls in sessions';
838
- paintCmdPager(pages);
839
- }
840
- // Shared page-number painter: prev/next plus windowed numbers with
841
- // ellipsis. onPick(page) rerenders the owning card. Same controls as the
842
- // Command tools pager; Models and Recent reuse it verbatim.
843
- function paintPager(wrapId, rangeId, page, per, total, onPick) {
844
- var start = total ? (page - 1) * per + 1 : 0;
845
- document.getElementById(rangeId).textContent =
846
- 'Showing ' + start + '–' + Math.min(total, page * per) + ' of ' + total;
847
- var wrap = document.getElementById(wrapId);
848
- wrap.innerHTML = '';
849
- var pages = Math.max(1, Math.ceil(total / per));
850
- if (page > pages) onPick(pages);
851
- function btn(label, next, opts) {
852
- var b = document.createElement('button');
853
- b.type = 'button'; b.className = 'pgbtn mono' + (opts && opts.on ? ' on' : '');
854
- b.textContent = label;
855
- if (opts && opts.dim) b.setAttribute('aria-label', opts.dim);
856
- if (opts && opts.off) b.disabled = true;
857
- else b.addEventListener('click', function() { onPick(next); });
858
- wrap.appendChild(b);
859
- return b;
860
- }
861
- btn('‹', page - 1, { dim: 'Previous page', off: page <= 1 });
862
- var nums = [];
863
- for (var p = 1; p <= pages; p++) {
864
- if (p === 1 || p === pages || Math.abs(p - page) <= 1) nums.push(p);
865
- else if (nums[nums.length - 1] !== '…') nums.push('…');
866
- }
867
- nums.forEach(function(p) {
868
- if (p === '…') {
869
- var s = document.createElement('span');
870
- s.textContent = '…'; s.style.padding = '0 2px';
871
- wrap.appendChild(s);
872
- } else btn(String(p), p, { on: p === page });
873
- });
874
- btn('›', page + 1, { dim: 'Next page', off: page >= pages });
875
- }
876
- function paintCmdPager(pages) {
877
- var total = cmdRows.length;
878
- if (cmdPage > pages) cmdPage = pages;
879
- paintPager('cmdPages', 'cmdRange', cmdPage, cmdPer, total, function(p) { cmdPage = p; renderCmd(); });
880
- }
881
- function bindPerPage(sel, get, set, render) {
882
- Array.prototype.forEach.call(document.querySelectorAll(sel + ' [data-pp]'), function(btn) {
883
- if (btn.dataset.bound) return;
884
- btn.dataset.bound = '1';
885
- btn.addEventListener('click', function() {
886
- if (Number(btn.dataset.pp) === get()) return;
887
- Array.prototype.forEach.call(document.querySelectorAll(sel + ' [data-pp]'), function(b) { b.classList.remove('on'); });
888
- btn.classList.add('on');
889
- set(Number(btn.dataset.pp));
890
- render();
891
- });
892
- });
893
- }
894
- function bindCmdTable() {
895
- if (bindCmdTable.done) return;
896
- bindCmdTable.done = true;
897
- Array.prototype.forEach.call(document.querySelectorAll('#tools .thsort'), function(btn) {
898
- btn.addEventListener('click', function() {
899
- var k = btn.dataset.sort;
900
- if (cmdSort.key === k) cmdSort.dir = -cmdSort.dir;
901
- else { cmdSort.key = k; cmdSort.dir = k === 'name' ? 1 : -1; }
902
- cmdPage = 1;
903
- renderCmd();
904
- });
905
- });
906
- bindPerPage('#cmdPager', function() { return cmdPer; }, function(n) { cmdPer = n; cmdPage = 1; }, renderCmd);
907
- bindPerPage('section[aria-label="All models"]', function() { return modelPer; }, function(n) { modelPer = n; modelPage = 1; }, renderModels);
908
- bindPerPage('section[aria-label="Recent requests"]', function() { return recentPer; }, function(n) { recentPer = n; recentPage = 1; }, renderRecent);
909
- }
910
- function bindRecentTable() {
911
- if (bindRecentTable.done) return;
912
- bindRecentTable.done = true;
913
- Array.prototype.forEach.call(document.querySelectorAll('#recentTable .thsort'), function(btn) {
914
- btn.addEventListener('click', function() {
915
- var k = btn.dataset.sort;
916
- if (recentSort.key === k) recentSort.dir = -recentSort.dir;
917
- else { recentSort.key = k; recentSort.dir = k === 'model' || k === 'status' ? 1 : -1; }
918
- recentPage = 1;
919
- renderRecent();
920
- });
921
- });
922
- }
923
- function fmtMs(ms) {
924
- if (ms < 1000) return Math.round(ms) + 'ms';
925
- return (ms / 1000).toFixed(1) + 's';
926
- }
927
-
928
- function renderStrip(t) {
929
- var defs = [
930
- ['input', t.input, 'arrow-down-to-line'],
931
- ['output', t.output, 'arrow-up-from-line'],
932
- ['cache read', t.cacheRead, 'hard-drive-download'],
933
- ['cache write', t.cacheWrite, 'hard-drive-upload']
934
- ].filter(function(b) { return b[1] > 0; });
935
- var strip = document.getElementById('tokens');
936
- strip.innerHTML = '';
937
- defs.forEach(function(b, i) {
938
- var cell = document.createElement('div');
939
- cell.className = 'stripcell flex-1 px-4 py-4' + (i > 0 ? ' sm:border-l' : '');
940
- cell.style.borderColor = 'var(--line)';
941
- cell.innerHTML = '<div class="flex items-center gap-2 mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">' +
942
- '<i data-lucide="' + b[2] + '" class="size-3.5"></i><span></span></div>' +
943
- '<p class="mono font-bold text-3xl mt-1"></p>';
944
- cell.querySelector('span').textContent = b[0];
945
- cell.querySelector('p').textContent = fmt(b[1]);
946
- strip.appendChild(cell);
947
- });
948
- }
949
-
950
- // 30-day volume sparkline + blended-rate daily stats for the USD card.
951
- // Daily cost is a blended estimate (total USD / total tokens x day
952
- // tokens), always rendered with ~ and never mixed with measured totals.
953
- function renderSpark(byDay, usd, savedUsd, total) {
954
- var el = document.getElementById('usdSpark');
955
- el.innerHTML = '';
956
- var keys = Object.keys(byDay).sort().slice(-30);
957
- // Zero state: the spark strip is a fixed h-16, so a placeholder inside it
958
- // overflows onto the big number. Hide the number, strip, and stats, and
959
- // mount one placeholder in the card body instead. Restored below on data.
960
- var mid = el.parentElement || null, card = mid && mid.parentElement ? mid.parentElement : null;
961
- var usdEl = document.getElementById('usd'), noteEl = document.getElementById('usdNote');
962
- var ex = document.getElementById('emptyUsd');
963
- if (!keys.length && card && mid) {
964
- if (!ex) {
965
- ex = document.createElement('div');
966
- ex.id = 'emptyUsd'; ex.className = 'empty'; ex.style.marginTop = '16px';
967
- ex.innerHTML = emptyState('trending-up', 'No cost data yet', 'Daily spend will spark here once sessions report tokens.');
968
- card.insertBefore(ex, mid);
969
- }
970
- el.style.display = 'none'; mid.style.display = 'none';
971
- if (usdEl) usdEl.style.display = 'none';
972
- if (noteEl) noteEl.style.display = 'none';
973
- } else {
974
- if (ex && ex.parentElement) ex.parentElement.removeChild(ex);
975
- el.style.display = '';
976
- if (mid) mid.style.display = '';
977
- if (usdEl) usdEl.style.display = '';
978
- if (noteEl) noteEl.style.display = '';
979
- }
980
- var rate = total ? usd / total : 0;
981
- var max = 1, vols = [];
982
- keys.forEach(function(k) {
983
- var v = dayTotal(byDay[k] || {});
984
- vols.push({ k: k, v: v });
985
- max = Math.max(max, v);
986
- });
987
- var top = vols.slice().sort(function(a, b) { return b.v - a.v; })[0] || { k: '', v: 0 };
988
- vols.forEach(function(r) {
989
- var s = document.createElement('span');
990
- if (r.k === top.k && r.v > 0) s.className = 'top';
991
- s.style.height = r.v ? Math.max(4, Math.round(r.v / max * 100)) + '%' : '0';
992
- s.dataset.usd = (rate * r.v).toFixed(2);
993
- s.dataset.tok = r.v;
994
- s.dataset.day = r.k;
995
- s.addEventListener('mouseenter', function(ev) {
996
- showTip('<div class="tt">' + dateHead(s.dataset.day) + '</div><div class="tv">~' + fxMoney(Number(s.dataset.usd)) + '</div>' +
997
- '<div class="tr"><span class="sw" style="background:var(--accent)"></span>' +
998
- '<span class="tn">tokens</span><span class="tvr">' + fmt(Number(s.dataset.tok)) + '</span></div>',
999
- ev.clientX, ev.clientY);
1000
- });
1001
- s.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
1002
- s.addEventListener('mouseleave', hideTip);
1003
- el.appendChild(s);
1004
- });
1005
- var days = keys.length || 1;
1006
- document.getElementById('usdPerDay').textContent = fxMoney(usd / days);
1007
- document.getElementById('usdTopDay').textContent = top.k ? top.k.slice(5) + ' ~' + fxMoney(rate * top.v) : '-';
1008
- document.getElementById('usdLeverage').textContent = usd ? (savedUsd / usd).toFixed(1) : '-';
1009
- }
1010
-
1011
- function dayTotal(b) { return (b.input || 0) + (b.output || 0) + (b.cacheRead || 0) + (b.cacheWrite || 0); }
1012
-
1013
- function renderMain(d) {
1014
- // Server-provided default currency (tersio gain --currency); a saved
1015
- // picker choice in localStorage always wins.
1016
- if (!renderMain.fxInit) {
1017
- renderMain.fxInit = true;
1018
- var saved = null;
1019
- try { saved = localStorage.getItem('tersio-fx-cur'); } catch (e) { }
1020
- if (!saved && d.currency && CURS[d.currency]) {
1021
- fx.cur = d.currency;
1022
- var curEl = document.getElementById('fxCur');
1023
- if (curEl) curEl.textContent = FLAGS[fx.cur] + ' ' + fx.cur;
1024
- var panel = document.getElementById('fxPanel');
1025
- if (panel) Array.prototype.forEach.call(panel.children, function(o) {
1026
- o.setAttribute('aria-selected', o.dataset.cur === fx.cur ? 'true' : 'false');
1027
- });
1028
- }
1029
- }
1030
- var t = d.tokens || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
1031
- var total = t.input + t.output + t.cacheRead + t.cacheWrite;
1032
- var totalEl = document.getElementById('total');
1033
- if (hasGsap) {
1034
- var o = { v: 0 };
1035
- gsap.to(o, { v: total, duration: 1.2, ease: 'power3.out', onUpdate: function() { totalEl.textContent = fmt(o.v); } });
1036
- } else {
1037
- totalEl.textContent = fmt(total);
1038
- }
1039
- document.getElementById('costLabel').textContent = fx.cur.toLowerCase() + ' cost';
1040
- countUp(document.getElementById('usd'), d.usd || 0, true);
1041
- document.getElementById('usdNote').textContent = (d.priced ? 'per-model price table' : 'incl. default pricing') + (fx.live ? ' · fx live' : ' · fx snapshot');
1042
- var byDay = d.byDay || {};
1043
- renderSpark(byDay, d.usd || 0, d.savedUsd || 0, total);
1044
- document.getElementById('verChip').textContent = 'tersio v' + (d.version || '?');
1045
- var dp = d.paths || {};
1046
- [['pathLedger', dp.ledger], ['pathSessions', dp.sessions], ['pathUsageDb', dp.usageDb]].forEach(function(pair) {
1047
- var pel = document.getElementById(pair[0]);
1048
- if (pel && pair[1]) { pel.textContent = pair[1]; pel.title = pair[1]; }
1049
- });
1050
- countUp(document.getElementById('saved'), d.savedUsd || 0, true);
1051
- (function() {
1052
- var lev = (d.usd || 0) ? (d.savedUsd || 0) / d.usd : 0;
1053
- var z = levZone(lev);
1054
- var note = document.getElementById('usdNote');
1055
- note.innerHTML = ((d.priced ? 'per-model price table' : 'incl. default pricing') + (fx.live ? ' · fx live' : ' · fx snapshot') + ' · ') + zoneIcon(z[0], z[2]) + ' ' + z[1];
1056
- note.title = 'Cache leverage x' + lev.toFixed(1) + ' — cache savings per $1 spent, vs full input price';
1057
- })();
1058
- (function() {
1059
- var z = co2Zone(d.co2g || 0);
1060
- var el = document.getElementById('co2');
1061
- el.innerHTML = '~' + (d.co2g || 0).toFixed(1) + 'g ' + zoneIcon(z[0], z[2]);
1062
- el.title = z[1] + ' footprint (est.)';
1063
- })();
1064
- (function() {
1065
- var el = document.getElementById('co2');
1066
- var card = el ? el.closest('div.rounded-xl') : null;
1067
- if (!card || card.dataset.co2tip) return;
1068
- card.dataset.co2tip = '1';
1069
- card.addEventListener('mouseenter', function(ev) { showTip(tipCo2HTML(), ev.clientX, ev.clientY); });
1070
- card.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
1071
- card.addEventListener('mouseleave', hideTip);
1072
- })();
1073
- (function() {
1074
- var el = document.getElementById('saved');
1075
- var card = el ? el.closest('div.rounded-xl') : null;
1076
- if (!card || card.dataset.savetip) return;
1077
- card.dataset.savetip = '1';
1078
- card.addEventListener('mouseenter', function(ev) { showTip(tipSavedHTML(), ev.clientX, ev.clientY); });
1079
- card.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
1080
- card.addEventListener('mouseleave', hideTip);
1081
- })();
1082
- var share = total ? Math.round((t.cacheRead + t.cacheWrite) / total * 100) : 0;
1083
- var shareZ = shareZone(share);
1084
- document.getElementById('cacheShare').innerHTML = share + '% ' + zoneIcon(shareZ[0], shareZ[2]);
1085
- document.getElementById('cacheShare').title = shareZ[1] + ' cache share';
1086
- var shareOf = function(b) { var s = dayTotal(b); return s ? ((b.cacheRead || 0) + (b.cacheWrite || 0)) / s * 100 : 0; };
1087
- var keys = Object.keys(byDay).sort(), cPct = 0, pPct = 0, cn = 0, pn = 0;
1088
- keys.slice(-7).forEach(function(k) { cPct += shareOf(byDay[k]); cn++; });
1089
- keys.slice(-14, -7).forEach(function(k) { pPct += shareOf(byDay[k]); pn++; });
1090
- var pp = (cn ? cPct / cn : 0) - (pn ? pPct / pn : 0);
1091
- var cd = document.getElementById('cacheDelta');
1092
- var cs = document.getElementById('cacheShare');
1093
- cs.dataset.share = String(share);
1094
- cs.dataset.pp = pp.toFixed(1);
1095
- (function() {
1096
- var el = document.getElementById('cacheShare');
1097
- var card = el ? el.closest('div.rounded-xl') : null;
1098
- if (!card || card.dataset.sharetip) return;
1099
- card.dataset.sharetip = '1';
1100
- card.addEventListener('mouseenter', function(ev) {
1101
- showTip(tipShareHTML(Number(el.dataset.share || 0), Number(el.dataset.pp || 0)), ev.clientX, ev.clientY);
1102
- });
1103
- card.addEventListener('mousemove', function(ev) { moveTip(ev.clientX, ev.clientY); });
1104
- card.addEventListener('mouseleave', hideTip);
1105
- })();
1106
-
1107
- (function() {
1108
- var el = document.getElementById('ticker');
1109
- if (!el) return;
1110
- var tops = topModels(T.getData().byModel || {}, 5);
1111
- var parts = tops.map(function(m) {
1112
- var b = (T.getData().byModel || {})[m];
1113
- var v = b.input + b.output + b.cacheRead + b.cacheWrite;
1114
- var nm = m.toUpperCase().replace(/-(FREE|CONTRIBUTOR.*|NEXT|LATEST)$/, '').replace(/[-.]?\d[\d.]*/, '').replace(/-V(?=-|$)/, '');
1115
- return nm + ' ' + fmtShort(v);
1116
- });
1117
- if (!parts.length) return;
1118
- var half = parts.join(' \u25c6 ');
1119
- el.textContent = half + ' \u25c6 ' + half;
1120
- })();
1121
- renderStrip(t);
1122
- bindGraphTabs();
1123
- bindCmdTable();
1124
- bindRecentTable();
1125
- renderGraph(d.byDay || {});
1126
- renderModels();
1127
- renderRecent();
1128
- renderCmd();
1129
-
1130
- if (window.lucide) lucide.createIcons();
1131
- if (hasGsap && !renderMain.introDone) {
1132
- renderMain.introDone = true;
1133
- gsap.from('.hero-in', { y: 26, opacity: 0, duration: 0.8, ease: 'power3.out', stagger: 0.08 });
1134
- }
1135
- T.observe();
1136
- }
1137
- // shadcn/ui badge: default/secondary/destructive/outline carry the meaning,
1138
- // the label carries the detail. Built as DOM nodes so provider error notes
1139
- // never reach innerHTML.
1140
- function statusBadge(r) {
1141
- var b = document.createElement('span');
1142
- b.className = 'badge';
1143
- if (r.st === 'error') {
1144
- b.classList.add('destructive');
1145
- b.textContent = statusLabel(r);
1146
- b.title = 'error' + (r.code ? ' ' + r.code : '') + (r.note ? ' — ' + r.note : '');
1147
- } else if (r.st === 'aborted') {
1148
- b.classList.add('outline');
1149
- b.textContent = 'aborted';
1150
- b.title = r.note || 'Interrupted by user';
1151
- } else {
1152
- b.classList.add('secondary');
1153
- b.textContent = 'completed';
1154
- if (r.note) b.title = r.note;
1155
- }
1156
- return b;
1157
- }
1158
- function renderRecent() {
1159
- var body = document.getElementById('recent');
1160
- body.innerHTML = '';
1161
- var all = T.getData().recent || [];
1162
- recentRows = all.slice();
1163
- sortRecent();
1164
- var recentPages = Math.max(1, Math.ceil(recentRows.length / recentPer));
1165
- if (recentPage > recentPages) recentPage = recentPages;
1166
- var recentStart = (recentPage - 1) * recentPer;
1167
- var rows = recentRows.slice(recentStart, recentStart + recentPer);
1168
- rows.forEach(function(r, i) {
1169
- var v = vendorOf(r.m);
1170
- var measured = costIsMeasured(r);
1171
- var tr = document.createElement('tr');
1172
- tr.className = 'rrow' + (i < Math.min(recentRows.length - recentStart, recentPer) - 1 ? ' rowline' : '');
1173
- tr.innerHTML = '<td class="py-2.5 pr-3 truncate" style="min-width: 0"></td>' +
1174
- '<td class="text-right py-2.5 pr-3 whitespace-nowrap" style="color:#fb923c"></td>' +
1175
- '<td class="text-right py-2.5 pr-3 whitespace-nowrap" style="color:var(--accent)"></td>' +
1176
- '<td class="text-right py-2.5 pr-3 whitespace-nowrap" style="color:var(--dim)">–</td>' +
1177
- '<td class="py-2.5 pr-3 whitespace-nowrap"></td>' +
1178
- '<td class="text-right py-2.5 pr-3 whitespace-nowrap"></td>' +
1179
- '<td class="text-right py-2.5 whitespace-nowrap" style="color: var(--dim)"></td>';
1180
- var tds = tr.children;
1181
- var dot = '<span style="display:inline-block;width:8px;height:8px;border-radius:99px;background:' + v.color + ';margin-right:8px"></span>';
1182
- tds[0].innerHTML = dot + '<span></span>';
1183
- tds[0].querySelector('span:last-child').textContent = displayModel(r.m);
1184
- tds[1].textContent = fmt(r.i);
1185
- tds[2].textContent = fmt(r.o);
1186
- tds[3].textContent = speedText(r);
1187
- tds[4].appendChild(statusBadge(r));
1188
- tds[5].textContent = (measured ? '' : '~') + fxMoney(displayCost(r));
1189
- tds[5].title = measured
1190
- ? 'measured — charged by the provider'
1191
- : (costRecorded(r)
1192
- ? 'est. — provider charged ' + fxMoney(r.usd) + ' (free or local), modeled from tokens'
1193
- : 'est. — modeled from tokens');
1194
- if (measured) tds[5].style.color = 'var(--ink)';
1195
- tds[6].textContent = whenStamp(r.t);
1196
- hoverRecent(tr, r);
1197
- body.appendChild(tr);
1198
- });
1199
- document.getElementById('recentCount').textContent = all.length ? recentRows.length + ' requests' : '';
1200
- paintPager('recentPages', 'recentRange', recentPage, recentPer, recentRows.length, function(p) { recentPage = p; renderRecent(); });
1201
- document.getElementById('recentTable').style.display = all.length ? '' : 'none';
1202
- document.getElementById('emptyRecent').classList.toggle('hidden', all.length > 0);
1203
- }
1204
- T.renderMain = renderMain;
1205
- T.openModelDialog = openModelDialog;
1206
- T.dayTotal = dayTotal;
1207
- T.emptyState = emptyState;
1208
- })();