clauddy 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -0
- package/auth.js +167 -0
- package/bin/clauddy.js +12 -0
- package/config.json +8 -0
- package/main.js +256 -0
- package/package.json +87 -0
- package/preload.js +18 -0
- package/renderer/index.html +162 -0
- package/renderer/pet.js +614 -0
- package/renderer/style.css +1195 -0
- package/usage.js +219 -0
package/renderer/pet.js
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
const SVGNS = 'http://www.w3.org/2000/svg'
|
|
2
|
+
const el = (id) => document.getElementById(id)
|
|
3
|
+
|
|
4
|
+
// Claude pixel-art sprite
|
|
5
|
+
const SPRITE = [
|
|
6
|
+
'.########.',
|
|
7
|
+
'.########.',
|
|
8
|
+
'##########',
|
|
9
|
+
'###o##o###',
|
|
10
|
+
'##########',
|
|
11
|
+
'.########.',
|
|
12
|
+
'.########.',
|
|
13
|
+
'.#.#..#.#.',
|
|
14
|
+
'.#.#..#.#.',
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
;(function buildPixel() {
|
|
18
|
+
const body = el('body')
|
|
19
|
+
const eyes = el('eyes')
|
|
20
|
+
const C = 10
|
|
21
|
+
SPRITE.forEach((row, r) => {
|
|
22
|
+
for (let c = 0; c < row.length; c++) {
|
|
23
|
+
const ch = row[c]
|
|
24
|
+
if (ch === '.') continue
|
|
25
|
+
const rect = document.createElementNS(SVGNS, 'rect')
|
|
26
|
+
rect.setAttribute('x', c * C)
|
|
27
|
+
rect.setAttribute('y', r * C)
|
|
28
|
+
rect.setAttribute('width', C)
|
|
29
|
+
rect.setAttribute('height', C)
|
|
30
|
+
body.appendChild(rect)
|
|
31
|
+
if (ch === 'o') {
|
|
32
|
+
const eye = document.createElementNS(SVGNS, 'rect')
|
|
33
|
+
eye.setAttribute('x', c * C)
|
|
34
|
+
eye.setAttribute('y', r * C)
|
|
35
|
+
eye.setAttribute('width', C)
|
|
36
|
+
eye.setAttribute('height', C)
|
|
37
|
+
eyes.appendChild(eye)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
})()
|
|
42
|
+
|
|
43
|
+
// night scene backdrop (clouds, crescent moon, stars, dotted ground/sky)
|
|
44
|
+
;(function buildScene() {
|
|
45
|
+
const s = el('scene')
|
|
46
|
+
if (!s) return
|
|
47
|
+
const add = (tag, attrs) => {
|
|
48
|
+
const e = document.createElementNS(SVGNS, tag)
|
|
49
|
+
for (const k in attrs) e.setAttribute(k, attrs[k])
|
|
50
|
+
s.appendChild(e)
|
|
51
|
+
}
|
|
52
|
+
// dotted top + bottom (sky + ground)
|
|
53
|
+
for (let x = 4; x <= 212; x += 9) {
|
|
54
|
+
add('circle', { cx: x, cy: 6, r: 1.3, fill: '#fff', 'fill-opacity': 0.85 })
|
|
55
|
+
add('circle', { cx: x, cy: 130, r: 1.3, fill: '#fff', 'fill-opacity': 0.85 })
|
|
56
|
+
}
|
|
57
|
+
// clouds (blocky, dark gray)
|
|
58
|
+
const cloud = (x, y, b, m, t) => {
|
|
59
|
+
add('rect', { x: x + 12, y: y, width: t, height: 9, fill: '#3a3a3a' })
|
|
60
|
+
add('rect', { x: x + 5, y: y + 7, width: m, height: 9, fill: '#3a3a3a' })
|
|
61
|
+
add('rect', { x: x, y: y + 14, width: b, height: 10, fill: '#3a3a3a' })
|
|
62
|
+
}
|
|
63
|
+
cloud(16, 10, 58, 42, 22)
|
|
64
|
+
cloud(120, 50, 40, 28, 15)
|
|
65
|
+
// stars (small plus)
|
|
66
|
+
const star = (x, y) => {
|
|
67
|
+
add('rect', { x: x - 3, y: y - 0.7, width: 6, height: 1.4, fill: '#d8d8d8' })
|
|
68
|
+
add('rect', { x: x - 0.7, y: y - 3, width: 1.4, height: 6, fill: '#d8d8d8' })
|
|
69
|
+
}
|
|
70
|
+
;[
|
|
71
|
+
[100, 16],
|
|
72
|
+
[60, 22],
|
|
73
|
+
[150, 100],
|
|
74
|
+
[196, 56],
|
|
75
|
+
[205, 98],
|
|
76
|
+
[128, 26],
|
|
77
|
+
[182, 22],
|
|
78
|
+
[202, 14],
|
|
79
|
+
].forEach(([x, y]) => {
|
|
80
|
+
star(x, y)
|
|
81
|
+
})
|
|
82
|
+
})()
|
|
83
|
+
|
|
84
|
+
// helpers
|
|
85
|
+
function fmtTokens(t) {
|
|
86
|
+
t = t || 0
|
|
87
|
+
if (t >= 1e9) return `${(t / 1e9).toFixed(2)}B`
|
|
88
|
+
if (t >= 1e6) return `${(t / 1e6).toFixed(1)}M`
|
|
89
|
+
if (t >= 1e3) return `${(t / 1e3).toFixed(1)}k`
|
|
90
|
+
return String(t)
|
|
91
|
+
}
|
|
92
|
+
function fmtReset(ms) {
|
|
93
|
+
if (!ms || ms <= 0) return 'now'
|
|
94
|
+
const h = Math.floor(ms / 3600000)
|
|
95
|
+
const m = Math.floor((ms % 3600000) / 60000)
|
|
96
|
+
return h > 0 ? `${h}h ${m}m` : `${m}m`
|
|
97
|
+
}
|
|
98
|
+
function setState(name) {
|
|
99
|
+
const b = document.body
|
|
100
|
+
;[...b.classList].forEach((c) => {
|
|
101
|
+
if (c.startsWith('state-')) b.classList.remove(c)
|
|
102
|
+
})
|
|
103
|
+
b.classList.add(`state-${name}`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// coins (Claude "eating" tokens) — arc in, spin, get gulped with a crumb pop
|
|
107
|
+
const W = 86
|
|
108
|
+
const H = 77
|
|
109
|
+
const MOUTH_X = 43
|
|
110
|
+
const MOUTH_Y = 46
|
|
111
|
+
|
|
112
|
+
function spawnCoin() {
|
|
113
|
+
const zone = el('dropzone')
|
|
114
|
+
const coin = document.createElement('div')
|
|
115
|
+
coin.className = 'coin'
|
|
116
|
+
const sz = 6 + Math.random() * 3
|
|
117
|
+
coin.style.width = coin.style.height = `${sz.toFixed(1)}px`
|
|
118
|
+
|
|
119
|
+
const side = Math.floor(Math.random() * 4)
|
|
120
|
+
let x, y
|
|
121
|
+
if (side === 0) {
|
|
122
|
+
x = Math.random() * W
|
|
123
|
+
y = -10
|
|
124
|
+
} else if (side === 1) {
|
|
125
|
+
x = W + 10
|
|
126
|
+
y = Math.random() * H * 0.7
|
|
127
|
+
} else if (side === 2) {
|
|
128
|
+
x = Math.random() * W
|
|
129
|
+
y = H + 10
|
|
130
|
+
} else {
|
|
131
|
+
x = -10
|
|
132
|
+
y = Math.random() * H * 0.7
|
|
133
|
+
}
|
|
134
|
+
coin.style.left = `${x}px`
|
|
135
|
+
coin.style.top = `${y}px`
|
|
136
|
+
zone.appendChild(coin)
|
|
137
|
+
|
|
138
|
+
const dx = MOUTH_X - x
|
|
139
|
+
const dy = MOUTH_Y - y
|
|
140
|
+
// perpendicular offset -> curved arc toward the mouth
|
|
141
|
+
const mxo = dx * 0.5 - dy * 0.18
|
|
142
|
+
const myo = dy * 0.5 + dx * 0.18
|
|
143
|
+
const rot = (Math.random() * 2 - 1) * 320
|
|
144
|
+
const anim = coin.animate(
|
|
145
|
+
[
|
|
146
|
+
{ transform: 'translate(0,0) scale(0.7) rotate(0deg)', opacity: 0.95 },
|
|
147
|
+
{
|
|
148
|
+
transform: `translate(${mxo}px,${myo}px) scale(1) rotate(${(rot * 0.6).toFixed(0)}deg)`,
|
|
149
|
+
opacity: 1,
|
|
150
|
+
offset: 0.55,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
transform: `translate(${dx}px,${dy}px) scale(0.2) rotate(${rot.toFixed(0)}deg)`,
|
|
154
|
+
opacity: 0,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
{ duration: 780 + Math.random() * 320, easing: 'cubic-bezier(0.45,0,0.55,1)' },
|
|
158
|
+
)
|
|
159
|
+
anim.onfinish = () => {
|
|
160
|
+
coin.remove()
|
|
161
|
+
popCrumbs()
|
|
162
|
+
chomp() // mouth opens to eat it
|
|
163
|
+
nibble() // tiny gulp reaction
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// the mouth opens and snaps shut on each token
|
|
168
|
+
function chomp() {
|
|
169
|
+
el('mouth').animate(
|
|
170
|
+
[
|
|
171
|
+
{ transform: 'scaleY(0.1)' },
|
|
172
|
+
{ transform: 'scaleY(1)', offset: 0.4 },
|
|
173
|
+
{ transform: 'scaleY(0.1)' },
|
|
174
|
+
],
|
|
175
|
+
{ duration: 240, easing: 'ease-in-out' },
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// quick squash of the whole pet on each gulp (doesn't fight the hop on #claude)
|
|
180
|
+
function nibble() {
|
|
181
|
+
el('pet').animate(
|
|
182
|
+
[
|
|
183
|
+
{ transform: 'scale(1, 1)' },
|
|
184
|
+
{ transform: 'scale(1.06, 0.94)', offset: 0.5 },
|
|
185
|
+
{ transform: 'scale(1, 1)' },
|
|
186
|
+
],
|
|
187
|
+
{ duration: 200, easing: 'ease' },
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// poke reaction: bouncy squish + little hearts floating up
|
|
192
|
+
function popHearts() {
|
|
193
|
+
const zone = el('dropzone')
|
|
194
|
+
const n = 5
|
|
195
|
+
for (let i = 0; i < n; i++) {
|
|
196
|
+
const h = document.createElement('div')
|
|
197
|
+
h.className = 'heart'
|
|
198
|
+
h.textContent = '♥'
|
|
199
|
+
// spread across lanes along the width + a little jitter
|
|
200
|
+
const baseX = 14 + i * 15 + (Math.random() * 6 - 3)
|
|
201
|
+
h.style.left = `${baseX.toFixed(0)}px`
|
|
202
|
+
h.style.top = `${(14 + Math.random() * 12).toFixed(0)}px`
|
|
203
|
+
zone.appendChild(h)
|
|
204
|
+
// fan outward from the center
|
|
205
|
+
const dx = (baseX - 43) * 0.55 + (Math.random() * 8 - 4)
|
|
206
|
+
const a = h.animate(
|
|
207
|
+
[
|
|
208
|
+
{ transform: 'translate(0, 8px) scale(0.4)', opacity: 0 },
|
|
209
|
+
{
|
|
210
|
+
transform: `translate(${(dx * 0.5).toFixed(1)}px, -12px) scale(1.3)`,
|
|
211
|
+
opacity: 1,
|
|
212
|
+
offset: 0.3,
|
|
213
|
+
},
|
|
214
|
+
{ transform: `translate(${dx.toFixed(1)}px, -48px) scale(0.85)`, opacity: 0 },
|
|
215
|
+
],
|
|
216
|
+
{ duration: 1100 + Math.random() * 350, easing: 'ease-out', delay: i * 120 },
|
|
217
|
+
)
|
|
218
|
+
a.onfinish = () => h.remove()
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function pokePet() {
|
|
222
|
+
oneShot('poke', 850)
|
|
223
|
+
popHearts()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function popCrumbs() {
|
|
227
|
+
const zone = el('dropzone')
|
|
228
|
+
for (let i = 0; i < 3; i++) {
|
|
229
|
+
const c = document.createElement('div')
|
|
230
|
+
c.className = 'crumb'
|
|
231
|
+
c.style.left = `${MOUTH_X}px`
|
|
232
|
+
c.style.top = `${MOUTH_Y}px`
|
|
233
|
+
zone.appendChild(c)
|
|
234
|
+
const ang = Math.random() * Math.PI * 2
|
|
235
|
+
const d = 5 + Math.random() * 8
|
|
236
|
+
const a = c.animate(
|
|
237
|
+
[
|
|
238
|
+
{ transform: 'translate(0,0) scale(1)', opacity: 0.9 },
|
|
239
|
+
{
|
|
240
|
+
transform: `translate(${(Math.cos(ang) * d).toFixed(1)}px, ${(Math.sin(ang) * d - 4).toFixed(1)}px) scale(0.2)`,
|
|
241
|
+
opacity: 0,
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
{ duration: 240 + Math.random() * 160, easing: 'ease-out' },
|
|
245
|
+
)
|
|
246
|
+
a.onfinish = () => c.remove()
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// continuous stream while working; faster when burning more tokens/min
|
|
251
|
+
let eatTimer = null
|
|
252
|
+
let eating = false
|
|
253
|
+
let currentRate = 0
|
|
254
|
+
function eatInterval() {
|
|
255
|
+
return Math.max(750, 1700 - Math.min(850, currentRate / 2600))
|
|
256
|
+
}
|
|
257
|
+
function startEating() {
|
|
258
|
+
stopEating()
|
|
259
|
+
const loop = () => {
|
|
260
|
+
spawnCoin()
|
|
261
|
+
eatTimer = setTimeout(loop, eatInterval())
|
|
262
|
+
}
|
|
263
|
+
loop()
|
|
264
|
+
}
|
|
265
|
+
function stopEating() {
|
|
266
|
+
if (eatTimer) clearTimeout(eatTimer)
|
|
267
|
+
eatTimer = null
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 30-day map
|
|
271
|
+
function renderHeat(days) {
|
|
272
|
+
const row = el('heat-row')
|
|
273
|
+
row.innerHTML = ''
|
|
274
|
+
const max = Math.max(1, ...days)
|
|
275
|
+
days.forEach((v, i) => {
|
|
276
|
+
const sq = document.createElement('div')
|
|
277
|
+
let lv = 0
|
|
278
|
+
if (v > 0) {
|
|
279
|
+
const r = v / max
|
|
280
|
+
lv = r < 0.3 ? 1 : r < 0.6 ? 2 : r < 0.85 ? 3 : 4
|
|
281
|
+
}
|
|
282
|
+
sq.className = `sq${lv ? ` lv${lv}` : ''}`
|
|
283
|
+
const daysAgo = days.length - 1 - i
|
|
284
|
+
sq.title = `${daysAgo === 0 ? 'today' : `${daysAgo}d ago`} · ${fmtTokens(v)} tokens`
|
|
285
|
+
row.appendChild(sq)
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// by model (7 days)
|
|
290
|
+
function renderModels(list) {
|
|
291
|
+
const box = el('bymodel-list')
|
|
292
|
+
box.innerHTML = ''
|
|
293
|
+
const top = list.slice(0, 4)
|
|
294
|
+
const max = Math.max(1, ...top.map((m) => m.tokens))
|
|
295
|
+
for (const m of top) {
|
|
296
|
+
const row = document.createElement('div')
|
|
297
|
+
row.className = 'mrow'
|
|
298
|
+
row.innerHTML =
|
|
299
|
+
`<span class="mname">${m.label}</span>` +
|
|
300
|
+
`<span class="mbar"><i style="width:${(m.tokens / max) * 100}%"></i></span>` +
|
|
301
|
+
`<span class="mval">${fmtTokens(m.tokens)}</span>`
|
|
302
|
+
box.appendChild(row)
|
|
303
|
+
}
|
|
304
|
+
if (!top.length) {
|
|
305
|
+
box.innerHTML = '<div class="mrow" style="opacity:.5">no activity</div>'
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// one-shot reaction (adds a class, removes after ms)
|
|
310
|
+
function oneShot(cls, ms) {
|
|
311
|
+
document.body.classList.add(cls)
|
|
312
|
+
setTimeout(() => document.body.classList.remove(cls), ms)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// session-reset celebration: jump + a colorful confetti burst
|
|
316
|
+
const CONFETTI_COLORS = ['#ffd23f', '#ff5d86', '#7ec77d', '#6db3f2', '#e0805a', '#c89bff']
|
|
317
|
+
function spawnConfetti(i) {
|
|
318
|
+
const zone = el('dropzone')
|
|
319
|
+
const p = document.createElement('div')
|
|
320
|
+
p.className = 'confetti'
|
|
321
|
+
p.style.background = CONFETTI_COLORS[i % CONFETTI_COLORS.length]
|
|
322
|
+
const w = 4 + Math.random() * 4
|
|
323
|
+
p.style.width = `${w.toFixed(1)}px`
|
|
324
|
+
p.style.height = `${(w + 2 + Math.random() * 4).toFixed(1)}px`
|
|
325
|
+
p.style.left = '43px'
|
|
326
|
+
p.style.top = '38px'
|
|
327
|
+
zone.appendChild(p)
|
|
328
|
+
// launch up + outward, then fall back down with a tumble
|
|
329
|
+
const ang = -Math.PI / 2 + (Math.random() * 2 - 1) * 1.15
|
|
330
|
+
const speed = 34 + Math.random() * 36
|
|
331
|
+
const ux = Math.cos(ang) * speed
|
|
332
|
+
const uy = Math.sin(ang) * speed // negative = upward
|
|
333
|
+
const fallY = 50 + Math.random() * 40
|
|
334
|
+
const rot = (Math.random() * 2 - 1) * 600
|
|
335
|
+
const a = p.animate(
|
|
336
|
+
[
|
|
337
|
+
{ transform: 'translate(0,0) rotate(0) scale(0.5)', opacity: 1 },
|
|
338
|
+
{
|
|
339
|
+
transform: `translate(${ux.toFixed(0)}px, ${uy.toFixed(0)}px) rotate(${(rot * 0.4).toFixed(0)}deg) scale(1)`,
|
|
340
|
+
opacity: 1,
|
|
341
|
+
offset: 0.4,
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
transform: `translate(${(ux * 1.4).toFixed(0)}px, ${fallY.toFixed(0)}px) rotate(${rot.toFixed(0)}deg) scale(0.9)`,
|
|
345
|
+
opacity: 0,
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
{ duration: 1150 + Math.random() * 550, easing: 'cubic-bezier(0.25, 0.7, 0.4, 1)' },
|
|
349
|
+
)
|
|
350
|
+
a.onfinish = () => p.remove()
|
|
351
|
+
}
|
|
352
|
+
function celebrate() {
|
|
353
|
+
oneShot('celebrate', 1300)
|
|
354
|
+
for (let i = 0; i < 24; i++) spawnConfetti(i)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// main render
|
|
358
|
+
let prevState = null
|
|
359
|
+
let prevPct = null
|
|
360
|
+
let lastData = null
|
|
361
|
+
function render(d) {
|
|
362
|
+
lastData = d
|
|
363
|
+
// % comes only from the connected account — no estimates
|
|
364
|
+
const liveOn = !!realUsage
|
|
365
|
+
document.body.classList.toggle('live', liveOn)
|
|
366
|
+
const sessPct = liveOn ? realUsage.session.pct : 0
|
|
367
|
+
const sessReset = liveOn ? realUsage.session.resetMs : null
|
|
368
|
+
const sessActive = liveOn && sessReset != null
|
|
369
|
+
const wkPct = liveOn ? realUsage.week.pct : 0
|
|
370
|
+
const wkReset = liveOn ? realUsage.week.resetMs : null
|
|
371
|
+
|
|
372
|
+
let st =
|
|
373
|
+
liveOn && sessPct >= 100
|
|
374
|
+
? 'tired'
|
|
375
|
+
: d.active
|
|
376
|
+
? 'working'
|
|
377
|
+
: liveOn && sessPct >= 90
|
|
378
|
+
? 'stressed'
|
|
379
|
+
: d.sleeping
|
|
380
|
+
? 'sleeping'
|
|
381
|
+
: 'idle'
|
|
382
|
+
// dev override from `./pet <state>`
|
|
383
|
+
if (debugState) {
|
|
384
|
+
const map = {
|
|
385
|
+
working: 'working',
|
|
386
|
+
sleeping: 'sleeping',
|
|
387
|
+
fire: 'stressed',
|
|
388
|
+
tired: 'tired',
|
|
389
|
+
idle: 'idle',
|
|
390
|
+
}
|
|
391
|
+
if (map[debugState]) st = map[debugState]
|
|
392
|
+
}
|
|
393
|
+
setState(st)
|
|
394
|
+
if (prevState && prevState !== st) {
|
|
395
|
+
if (st === 'sleeping') oneShot('drowse', 700)
|
|
396
|
+
else if (prevState === 'sleeping') oneShot('wake', 700)
|
|
397
|
+
}
|
|
398
|
+
prevState = st
|
|
399
|
+
|
|
400
|
+
const word =
|
|
401
|
+
st === 'working'
|
|
402
|
+
? 'working'
|
|
403
|
+
: st === 'sleeping'
|
|
404
|
+
? 'sleeping'
|
|
405
|
+
: st === 'stressed'
|
|
406
|
+
? 'on fire'
|
|
407
|
+
: st === 'tired'
|
|
408
|
+
? 'maxed out'
|
|
409
|
+
: 'idle'
|
|
410
|
+
el('status-text').textContent = word
|
|
411
|
+
el('mini-text').textContent = word
|
|
412
|
+
el('rate').textContent =
|
|
413
|
+
d.active && d.tokensPerMin > 0
|
|
414
|
+
? `${fmtTokens(d.tokensPerMin)} tok/min`
|
|
415
|
+
: `${fmtTokens(d.today.tokens)} tokens today`
|
|
416
|
+
|
|
417
|
+
if (liveOn && prevPct != null && sessActive && prevPct - sessPct > 25) celebrate()
|
|
418
|
+
prevPct = sessPct
|
|
419
|
+
el('session-pct').textContent = `${Math.round(sessPct)}%`
|
|
420
|
+
const mini = el('mini-pct')
|
|
421
|
+
mini.textContent = liveOn ? `${Math.round(sessPct)}%` : '—'
|
|
422
|
+
mini.classList.toggle('high', liveOn && sessPct >= 80)
|
|
423
|
+
const sf = el('session-fill')
|
|
424
|
+
sf.style.width = `${sessPct}%`
|
|
425
|
+
sf.classList.toggle('high', sessPct >= 80)
|
|
426
|
+
el('session-sub').textContent = sessActive
|
|
427
|
+
? `resets in ${fmtReset(sessReset)} · ${fmtTokens(d.session.tokens)} tokens`
|
|
428
|
+
: 'no active session'
|
|
429
|
+
|
|
430
|
+
el('week-pct').textContent = `${Math.round(wkPct)}%`
|
|
431
|
+
const wf = el('week-fill')
|
|
432
|
+
wf.style.width = `${wkPct}%`
|
|
433
|
+
wf.classList.toggle('high', wkPct >= 80)
|
|
434
|
+
el('week-sub').textContent =
|
|
435
|
+
wkReset != null
|
|
436
|
+
? `resets in ${fmtReset(wkReset)} · ${fmtTokens(d.week.tokens)} tokens`
|
|
437
|
+
: `${fmtTokens(d.week.tokens)} tokens · last 7 days`
|
|
438
|
+
|
|
439
|
+
renderModels(d.byModel || [])
|
|
440
|
+
renderHeat(d.days30 || [])
|
|
441
|
+
el('month-total').textContent = `${fmtTokens(d.monthTokens)} tokens`
|
|
442
|
+
|
|
443
|
+
currentRate = d.tokensPerMin || 0
|
|
444
|
+
if (st === 'working') {
|
|
445
|
+
if (!eating) {
|
|
446
|
+
eating = true
|
|
447
|
+
startEating()
|
|
448
|
+
}
|
|
449
|
+
} else if (eating) {
|
|
450
|
+
eating = false
|
|
451
|
+
stopEating()
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
fitSize()
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// fit the window to the content (no leftover border)
|
|
458
|
+
let lastH = 0
|
|
459
|
+
let lastW = 0
|
|
460
|
+
function fitSize() {
|
|
461
|
+
requestAnimationFrame(() => {
|
|
462
|
+
const collapsed = document.body.classList.contains('collapsed')
|
|
463
|
+
const w = collapsed ? 140 : 276
|
|
464
|
+
const h = el('card').offsetHeight + 24 // 12px margin top + bottom
|
|
465
|
+
if (Math.abs(h - lastH) > 2 || w !== lastW) {
|
|
466
|
+
lastH = h
|
|
467
|
+
lastW = w
|
|
468
|
+
window.api.resize(w, h)
|
|
469
|
+
}
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
let currentConfig = {}
|
|
474
|
+
let realUsage = null
|
|
475
|
+
let debugState = null
|
|
476
|
+
window.api.onDebugState((o) => {
|
|
477
|
+
const s = o?.state
|
|
478
|
+
if (s === 'poke') return pokePet()
|
|
479
|
+
if (s === 'celebrate') return celebrate()
|
|
480
|
+
debugState = s === 'auto' || s === 'clear' || !s ? null : s
|
|
481
|
+
if (lastData) render(lastData)
|
|
482
|
+
})
|
|
483
|
+
window.api.onUsage(render)
|
|
484
|
+
window.api.onError((msg) => {
|
|
485
|
+
el('status-text').textContent = 'error'
|
|
486
|
+
console.error(msg)
|
|
487
|
+
})
|
|
488
|
+
window.api.onConfig((cfg) => {
|
|
489
|
+
currentConfig = cfg || {}
|
|
490
|
+
})
|
|
491
|
+
window.api.onRealUsage((u) => {
|
|
492
|
+
realUsage = u || null
|
|
493
|
+
if (lastData) render(lastData)
|
|
494
|
+
})
|
|
495
|
+
window.api.onAuthState((s) => {
|
|
496
|
+
const on = !!s?.connected
|
|
497
|
+
document.body.classList.toggle('auth-on', on)
|
|
498
|
+
if (!on) {
|
|
499
|
+
realUsage = null
|
|
500
|
+
document.body.classList.remove('live')
|
|
501
|
+
el('acc-paste').classList.remove('show')
|
|
502
|
+
}
|
|
503
|
+
if (document.body.classList.contains('settings-open')) fitSize()
|
|
504
|
+
})
|
|
505
|
+
window.api.onAuthResult((r) => {
|
|
506
|
+
if (r?.ok) {
|
|
507
|
+
el('acc-msg').textContent = ''
|
|
508
|
+
el('acc-code').value = ''
|
|
509
|
+
el('acc-paste').classList.remove('show')
|
|
510
|
+
} else {
|
|
511
|
+
const e = r?.error || ''
|
|
512
|
+
el('acc-msg').textContent = /429|rate_limit/i.test(e)
|
|
513
|
+
? 'Rate limited by Anthropic — wait a few minutes, then try once with a fresh code.'
|
|
514
|
+
: `Failed: ${e || 'check the code and try again'}`
|
|
515
|
+
}
|
|
516
|
+
fitSize()
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
el('close').addEventListener('click', () => window.api.quit())
|
|
520
|
+
el('usage').addEventListener('click', () => window.api.openUsage())
|
|
521
|
+
|
|
522
|
+
// account login (browser flow)
|
|
523
|
+
el('acc-connect').addEventListener('click', () => {
|
|
524
|
+
window.api.authStart() // opens the browser to log in
|
|
525
|
+
el('acc-paste').classList.add('show') // reveal the code field
|
|
526
|
+
fitSize()
|
|
527
|
+
})
|
|
528
|
+
el('acc-confirm').addEventListener('click', () => {
|
|
529
|
+
const code = el('acc-code').value.trim()
|
|
530
|
+
if (!code) return
|
|
531
|
+
el('acc-msg').textContent = 'Checking…'
|
|
532
|
+
window.api.authCode(code)
|
|
533
|
+
const b = el('acc-confirm')
|
|
534
|
+
b.disabled = true
|
|
535
|
+
setTimeout(() => (b.disabled = false), 5000) // avoid hammering the rate-limited endpoint
|
|
536
|
+
})
|
|
537
|
+
el('acc-logout').addEventListener('click', () => window.api.authLogout())
|
|
538
|
+
|
|
539
|
+
// settings panel
|
|
540
|
+
function populateSettings() {
|
|
541
|
+
const c = currentConfig || {}
|
|
542
|
+
el('set-alerts').checked = c.alerts !== false
|
|
543
|
+
const th = c.alertThresholds || [80, 95]
|
|
544
|
+
el('set-t1').value = th[0] != null ? th[0] : 80
|
|
545
|
+
el('set-t2').value = th[1] != null ? th[1] : 95
|
|
546
|
+
}
|
|
547
|
+
function openSettings() {
|
|
548
|
+
document.body.classList.remove('collapsed')
|
|
549
|
+
populateSettings()
|
|
550
|
+
document.body.classList.add('settings-open')
|
|
551
|
+
fitSize()
|
|
552
|
+
}
|
|
553
|
+
el('gear').addEventListener('click', openSettings)
|
|
554
|
+
// the "connect" placeholder jumps straight to settings
|
|
555
|
+
el('limits-connect').addEventListener('click', openSettings)
|
|
556
|
+
// custom number steppers (▲ / ▼)
|
|
557
|
+
for (const b of document.querySelectorAll('.num-btn')) {
|
|
558
|
+
b.addEventListener('click', () => {
|
|
559
|
+
const input = el(b.dataset.for)
|
|
560
|
+
const min = Number(input.min) || 1
|
|
561
|
+
const max = Number(input.max) || 100
|
|
562
|
+
const next = (Number.parseInt(input.value, 10) || 0) + Number(b.dataset.step)
|
|
563
|
+
input.value = Math.min(max, Math.max(min, next))
|
|
564
|
+
})
|
|
565
|
+
}
|
|
566
|
+
el('set-cancel').addEventListener('click', () => {
|
|
567
|
+
document.body.classList.remove('settings-open')
|
|
568
|
+
fitSize()
|
|
569
|
+
})
|
|
570
|
+
el('set-save').addEventListener('click', () => {
|
|
571
|
+
const num = (id) => parseFloat(el(id).value)
|
|
572
|
+
window.api.saveConfig({
|
|
573
|
+
alerts: el('set-alerts').checked,
|
|
574
|
+
alertThresholds: [num('set-t1'), num('set-t2')]
|
|
575
|
+
.filter((n) => n >= 1 && n <= 100)
|
|
576
|
+
.sort((a, b) => a - b),
|
|
577
|
+
})
|
|
578
|
+
document.body.classList.remove('settings-open')
|
|
579
|
+
fitSize()
|
|
580
|
+
})
|
|
581
|
+
el('min').addEventListener('click', () => {
|
|
582
|
+
document.body.classList.toggle('collapsed')
|
|
583
|
+
fitSize()
|
|
584
|
+
})
|
|
585
|
+
// double-click the pet to collapse / expand
|
|
586
|
+
el('pet').addEventListener('dblclick', () => {
|
|
587
|
+
document.body.classList.toggle('collapsed')
|
|
588
|
+
fitSize()
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
// poke the pet -> bouncy squish + hearts
|
|
592
|
+
el('pet').addEventListener('click', () => pokePet())
|
|
593
|
+
|
|
594
|
+
// eyes follow the cursor
|
|
595
|
+
const eyesG = el('eyes')
|
|
596
|
+
window.addEventListener('mousemove', (e) => {
|
|
597
|
+
const b = document.body.classList
|
|
598
|
+
if (b.contains('state-sleeping') || b.contains('state-tired')) return
|
|
599
|
+
const r = el('pet').getBoundingClientRect()
|
|
600
|
+
const dx = e.clientX - (r.left + r.width / 2)
|
|
601
|
+
const dy = e.clientY - (r.top + r.height / 2)
|
|
602
|
+
const len = Math.hypot(dx, dy) || 1
|
|
603
|
+
eyesG.setAttribute(
|
|
604
|
+
'transform',
|
|
605
|
+
`translate(${((dx / len) * 3).toFixed(2)} ${((dy / len) * 2).toFixed(2)})`,
|
|
606
|
+
)
|
|
607
|
+
})
|
|
608
|
+
window.addEventListener('mouseout', (e) => {
|
|
609
|
+
if (!e.relatedTarget) eyesG.setAttribute('transform', 'translate(0 0)')
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
// welcome wave
|
|
613
|
+
document.body.classList.add('greet')
|
|
614
|
+
setTimeout(() => document.body.classList.remove('greet'), 1200)
|