agentlytics 0.1.15 → 0.1.16

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.
@@ -0,0 +1,413 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { CreditCard, RefreshCw, Clock, Zap, Shield, ChevronDown, ChevronUp } from 'lucide-react'
3
+ import { fetchUsage } from '../lib/api'
4
+ import { editorLabel, editorColor } from '../lib/constants'
5
+ import EditorIcon from '../components/EditorIcon'
6
+
7
+ function UsageBar({ value, max = 100, color, label }) {
8
+ const pct = max > 0 ? Math.min((value / max) * 100, 100) : 0
9
+ return (
10
+ <div>
11
+ {label && <div className="flex items-center justify-between mb-1">
12
+ <span className="text-[10px]" style={{ color: 'var(--c-text2)' }}>{label}</span>
13
+ <span className="text-[10px] font-mono" style={{ color: 'var(--c-white)' }}>{typeof value === 'number' ? value.toFixed(1) : value}%</span>
14
+ </div>}
15
+ <div className="h-1.5 w-full" style={{ background: 'var(--c-bg3)' }}>
16
+ <div className="h-full transition-all duration-500" style={{ width: `${pct}%`, background: color || '#6366f1' }} />
17
+ </div>
18
+ </div>
19
+ )
20
+ }
21
+
22
+ function TimeUntil({ date }) {
23
+ if (!date) return null
24
+ const d = new Date(date)
25
+ const now = new Date()
26
+ const diff = d - now
27
+ if (diff <= 0) return <span className="text-[10px]" style={{ color: 'var(--c-text3)' }}>expired</span>
28
+ const hours = Math.floor(diff / 3600000)
29
+ const mins = Math.floor((diff % 3600000) / 60000)
30
+ if (hours > 24) {
31
+ const days = Math.floor(hours / 24)
32
+ return <span className="text-[10px]" style={{ color: 'var(--c-text3)' }}>resets in {days}d {hours % 24}h</span>
33
+ }
34
+ return <span className="text-[10px]" style={{ color: 'var(--c-text3)' }}>resets in {hours}h {mins}m</span>
35
+ }
36
+
37
+ function PlanBadge({ name }) {
38
+ if (!name) return null
39
+ const n = (name || '').toLowerCase()
40
+ let bg = 'rgba(99,102,241,0.12)'
41
+ let fg = '#818cf8'
42
+ if (n.includes('pro')) { bg = 'rgba(168,85,247,0.12)'; fg = '#a855f7' }
43
+ if (n.includes('max') || n.includes('ultra')) { bg = 'rgba(234,179,8,0.12)'; fg = '#eab308' }
44
+ if (n.includes('plus')) { bg = 'rgba(34,197,94,0.12)'; fg = '#22c55e' }
45
+ if (n.includes('free')) { bg = 'rgba(107,114,128,0.12)'; fg = '#6b7280' }
46
+ return (
47
+ <span className="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5" style={{ background: bg, color: fg }}>
48
+ {name}
49
+ </span>
50
+ )
51
+ }
52
+
53
+ function FeaturePill({ label, enabled }) {
54
+ return (
55
+ <span
56
+ className="text-[10px] px-1.5 py-0.5"
57
+ style={{
58
+ background: enabled ? 'rgba(34,197,94,0.08)' : 'rgba(107,114,128,0.06)',
59
+ color: enabled ? '#22c55e' : 'var(--c-text3)',
60
+ border: `1px solid ${enabled ? 'rgba(34,197,94,0.15)' : 'var(--c-border)'}`,
61
+ }}
62
+ >
63
+ {label}
64
+ </span>
65
+ )
66
+ }
67
+
68
+ // ── Card renderers per source type ──
69
+
70
+ function CursorCard({ data }) {
71
+ const usage = data.usage || {}
72
+ const models = Object.keys(usage)
73
+ return (
74
+ <div className="space-y-3">
75
+ <div className="flex items-center gap-2 flex-wrap">
76
+ <PlanBadge name={data.plan?.name} />
77
+ {data.plan?.status && <span className="text-[10px]" style={{ color: data.plan.status === 'active' ? '#22c55e' : 'var(--c-text3)' }}>● {data.plan.status}</span>}
78
+ {data.plan?.isYearlyPlan && <span className="text-[10px]" style={{ color: 'var(--c-text3)' }}>yearly</span>}
79
+ </div>
80
+ {data.startOfMonth && (
81
+ <div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>
82
+ <Clock size={9} className="inline mr-1" />
83
+ billing started {new Date(data.startOfMonth).toLocaleDateString()}
84
+ </div>
85
+ )}
86
+ {models.length > 0 && (
87
+ <div className="space-y-1.5">
88
+ <div className="text-[10px] font-medium" style={{ color: 'var(--c-text2)' }}>model usage</div>
89
+ {models.map(m => {
90
+ const u = usage[m]
91
+ const pct = u.maxRequestUsage ? Math.round((u.numRequests / u.maxRequestUsage) * 100) : null
92
+ return (
93
+ <div key={m} className="flex items-center justify-between text-[11px]">
94
+ <span className="font-mono" style={{ color: 'var(--c-white)' }}>{m}</span>
95
+ <span style={{ color: 'var(--c-text2)' }}>
96
+ {u.numRequests}{u.maxRequestUsage ? ` / ${u.maxRequestUsage}` : ''} requests
97
+ </span>
98
+ </div>
99
+ )
100
+ })}
101
+ </div>
102
+ )}
103
+ </div>
104
+ )
105
+ }
106
+
107
+ function WindsurfCard({ data }) {
108
+ const u = data.usage || {}
109
+ const billing = data.billingCycle || {}
110
+ return (
111
+ <div className="space-y-3">
112
+ <div className="flex items-center gap-2 flex-wrap">
113
+ <PlanBadge name={data.plan?.name} />
114
+ {data.user?.name && <span className="text-[10px]" style={{ color: 'var(--c-text3)' }}>{data.user.name}</span>}
115
+ </div>
116
+
117
+ {/* Credits */}
118
+ <div className="space-y-2">
119
+ {u.promptCredits && (
120
+ <div>
121
+ <div className="flex justify-between text-[10px] mb-0.5">
122
+ <span style={{ color: 'var(--c-text2)' }}>prompt credits</span>
123
+ <span className="font-mono" style={{ color: 'var(--c-white)' }}>{u.promptCredits.remaining} / {u.promptCredits.allocated}</span>
124
+ </div>
125
+ <UsageBar value={u.promptCredits.allocated > 0 ? ((u.promptCredits.remaining / u.promptCredits.allocated) * 100) : 0} color="#06b6d4" />
126
+ </div>
127
+ )}
128
+ {u.flexCredits && u.flexCredits.allocated > 0 && (
129
+ <div>
130
+ <div className="flex justify-between text-[10px] mb-0.5">
131
+ <span style={{ color: 'var(--c-text2)' }}>flex credits</span>
132
+ <span className="font-mono" style={{ color: 'var(--c-white)' }}>{u.flexCredits.remaining} / {u.flexCredits.allocated}</span>
133
+ </div>
134
+ <UsageBar value={u.flexCredits.allocated > 0 ? ((u.flexCredits.remaining / u.flexCredits.allocated) * 100) : 0} color="#a78bfa" />
135
+ </div>
136
+ )}
137
+ {u.totalRemainingCredits != null && (
138
+ <div className="flex items-center justify-between pt-1" style={{ borderTop: '1px solid var(--c-border)' }}>
139
+ <span className="text-[10px] font-medium" style={{ color: 'var(--c-text2)' }}>total remaining</span>
140
+ <span className="text-[13px] font-bold font-mono" style={{ color: 'var(--c-white)' }}>{u.totalRemainingCredits}</span>
141
+ </div>
142
+ )}
143
+ </div>
144
+
145
+ {/* Billing */}
146
+ {(billing.start || billing.end) && (
147
+ <div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>
148
+ <Clock size={9} className="inline mr-1" />
149
+ {billing.start && new Date(billing.start).toLocaleDateString()} → {billing.end && new Date(billing.end).toLocaleDateString()}
150
+ </div>
151
+ )}
152
+
153
+ {/* Features */}
154
+ {data.features && (
155
+ <div className="flex flex-wrap gap-1">
156
+ {Object.entries(data.features).map(([k, v]) => (
157
+ <FeaturePill key={k} label={k.replace(/([A-Z])/g, ' $1').trim()} enabled={v} />
158
+ ))}
159
+ </div>
160
+ )}
161
+ </div>
162
+ )
163
+ }
164
+
165
+ function ClaudeCodeCard({ data }) {
166
+ const u = data.usage || {}
167
+ return (
168
+ <div className="space-y-3">
169
+ <PlanBadge name={data.plan?.name} />
170
+
171
+ <div className="space-y-2">
172
+ {u.fiveHour && (
173
+ <div>
174
+ <UsageBar value={u.fiveHour.utilization} color={u.fiveHour.utilization > 80 ? '#ef4444' : '#f97316'} label="5-hour limit" />
175
+ <TimeUntil date={u.fiveHour.resetsAt} />
176
+ </div>
177
+ )}
178
+ {u.sevenDay && (
179
+ <div>
180
+ <UsageBar value={u.sevenDay.utilization} color={u.sevenDay.utilization > 80 ? '#ef4444' : '#f97316'} label="7-day limit" />
181
+ <TimeUntil date={u.sevenDay.resetsAt} />
182
+ </div>
183
+ )}
184
+ {u.sevenDaySonnet && (
185
+ <div>
186
+ <UsageBar value={u.sevenDaySonnet.utilization} color="#a78bfa" label="7-day Sonnet" />
187
+ <TimeUntil date={u.sevenDaySonnet.resetsAt} />
188
+ </div>
189
+ )}
190
+ {u.sevenDayOpus && (
191
+ <div>
192
+ <UsageBar value={u.sevenDayOpus.utilization} color="#c084fc" label="7-day Opus" />
193
+ <TimeUntil date={u.sevenDayOpus.resetsAt} />
194
+ </div>
195
+ )}
196
+ </div>
197
+
198
+ {data.extraUsage && (
199
+ <div className="text-[10px] flex items-center gap-1.5" style={{ color: 'var(--c-text3)' }}>
200
+ <Zap size={9} />
201
+ extra usage: {data.extraUsage.isEnabled ? (
202
+ <span style={{ color: '#22c55e' }}>enabled{data.extraUsage.utilization != null ? ` (${data.extraUsage.utilization}% used)` : ''}</span>
203
+ ) : (
204
+ <span>disabled</span>
205
+ )}
206
+ </div>
207
+ )}
208
+ </div>
209
+ )
210
+ }
211
+
212
+ function CopilotCard({ data }) {
213
+ return (
214
+ <div className="space-y-3">
215
+ <div className="flex items-center gap-2 flex-wrap">
216
+ <PlanBadge name={data.plan?.name} />
217
+ {data.plan?.individual && <span className="text-[10px]" style={{ color: 'var(--c-text3)' }}>individual</span>}
218
+ </div>
219
+
220
+ {data.user?.login && (
221
+ <div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>
222
+ @{data.user.login}
223
+ </div>
224
+ )}
225
+
226
+ {data.features && (
227
+ <div className="flex flex-wrap gap-1">
228
+ {Object.entries(data.features).map(([k, v]) => (
229
+ <FeaturePill key={k} label={k.replace(/([A-Z])/g, ' $1').trim()} enabled={v} />
230
+ ))}
231
+ </div>
232
+ )}
233
+
234
+ {data.limits?.quotas && (
235
+ <div className="text-[10px]" style={{ color: 'var(--c-text2)' }}>
236
+ <Shield size={9} className="inline mr-1" />
237
+ quotas: {JSON.stringify(data.limits.quotas)}
238
+ {data.limits.resetDate && <span> (resets {new Date(data.limits.resetDate).toLocaleDateString()})</span>}
239
+ </div>
240
+ )}
241
+ </div>
242
+ )
243
+ }
244
+
245
+ function CodexCard({ data }) {
246
+ const sub = data.plan || {}
247
+ return (
248
+ <div className="space-y-3">
249
+ <div className="flex items-center gap-2 flex-wrap">
250
+ <PlanBadge name={sub.name} />
251
+ {data.authMode && <span className="text-[10px]" style={{ color: 'var(--c-text3)' }}>via {data.authMode}</span>}
252
+ </div>
253
+
254
+ {data.user?.email && (
255
+ <div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>
256
+ {data.user.email}
257
+ </div>
258
+ )}
259
+
260
+ {(sub.subscriptionStart || sub.subscriptionEnd) && (
261
+ <div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>
262
+ <Clock size={9} className="inline mr-1" />
263
+ {sub.subscriptionStart && new Date(sub.subscriptionStart).toLocaleDateString()} → {sub.subscriptionEnd && new Date(sub.subscriptionEnd).toLocaleDateString()}
264
+ </div>
265
+ )}
266
+ </div>
267
+ )
268
+ }
269
+
270
+ function GenericCard({ data }) {
271
+ return (
272
+ <div className="space-y-2">
273
+ {data.plan?.name && <PlanBadge name={data.plan.name} />}
274
+ {data.user && (
275
+ <div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>
276
+ {data.user.email || data.user.login || data.user.name || data.user.id || ''}
277
+ </div>
278
+ )}
279
+ <pre className="text-[10px] overflow-auto p-2" style={{ background: 'var(--c-bg3)', color: 'var(--c-text2)', maxHeight: 120 }}>
280
+ {JSON.stringify(data, null, 2)}
281
+ </pre>
282
+ </div>
283
+ )
284
+ }
285
+
286
+ function EditorCard({ data }) {
287
+ const [expanded, setExpanded] = useState(false)
288
+ const source = data.source
289
+ const color = editorColor(source)
290
+ const label = editorLabel(source)
291
+
292
+ const cardRenderer = () => {
293
+ switch (source) {
294
+ case 'cursor': return <CursorCard data={data} />
295
+ case 'windsurf': case 'windsurf-next': case 'antigravity': return <WindsurfCard data={data} />
296
+ case 'claude-code': return <ClaudeCodeCard data={data} />
297
+ case 'vscode': case 'vscode-insiders': return <CopilotCard data={data} />
298
+ case 'copilot-cli': return <CopilotCard data={data} />
299
+ case 'codex': return <CodexCard data={data} />
300
+ default: return <GenericCard data={data} />
301
+ }
302
+ }
303
+
304
+ // Models list (Windsurf variants)
305
+ const models = data.models || []
306
+
307
+ return (
308
+ <div className="card p-4 space-y-3">
309
+ <div className="flex items-center justify-between">
310
+ <div className="flex items-center gap-2">
311
+ <EditorIcon source={source} size={20} />
312
+ <span className="text-[13px] font-bold" style={{ color: 'var(--c-white)' }}>{label}</span>
313
+ </div>
314
+ <div className="w-1.5 h-1.5 rounded-full" style={{ background: color }} />
315
+ </div>
316
+
317
+ {cardRenderer()}
318
+
319
+ {/* Expandable models list */}
320
+ {models.length > 0 && (
321
+ <div>
322
+ <button
323
+ onClick={() => setExpanded(!expanded)}
324
+ className="flex items-center gap-1 text-[10px] transition hover:opacity-80"
325
+ style={{ color: 'var(--c-text2)' }}
326
+ >
327
+ {expanded ? <ChevronUp size={10} /> : <ChevronDown size={10} />}
328
+ {models.length} models
329
+ </button>
330
+ {expanded && (
331
+ <div className="mt-2 space-y-1">
332
+ {models.map((m, i) => (
333
+ <div key={i} className="flex items-center justify-between text-[10px] py-0.5" style={{ borderBottom: '1px solid var(--c-border)' }}>
334
+ <span style={{ color: 'var(--c-white)' }}>{m.label || m.model}</span>
335
+ {m.remainingFraction != null && (
336
+ <span className="font-mono" style={{ color: m.remainingFraction > 0.5 ? '#22c55e' : m.remainingFraction > 0.2 ? '#eab308' : '#ef4444' }}>
337
+ {(m.remainingFraction * 100).toFixed(0)}%
338
+ </span>
339
+ )}
340
+ </div>
341
+ ))}
342
+ </div>
343
+ )}
344
+ </div>
345
+ )}
346
+ </div>
347
+ )
348
+ }
349
+
350
+ export default function Subscriptions() {
351
+ const [data, setData] = useState(null)
352
+ const [loading, setLoading] = useState(true)
353
+ const [error, setError] = useState(null)
354
+
355
+ const load = () => {
356
+ setLoading(true)
357
+ setError(null)
358
+ fetchUsage()
359
+ .then(d => { setData(d); setLoading(false) })
360
+ .catch(e => { setError(e.message); setLoading(false) })
361
+ }
362
+
363
+ useEffect(() => { load() }, [])
364
+
365
+ return (
366
+ <div className="fade-in space-y-4">
367
+ <div className="flex items-center gap-3">
368
+ <div className="flex items-center gap-1.5 text-[13px] font-bold" style={{ color: 'var(--c-white)' }}>
369
+ <CreditCard size={14} style={{ color: '#6366f1' }} />
370
+ Subscriptions
371
+ </div>
372
+ <button
373
+ onClick={load}
374
+ disabled={loading}
375
+ className="flex items-center gap-1 px-2 py-0.5 text-[11px] transition hover:bg-[var(--c-card)]"
376
+ style={{ color: 'var(--c-text2)', border: '1px solid var(--c-border)' }}
377
+ >
378
+ <RefreshCw size={10} className={loading ? 'animate-spin' : ''} />
379
+ Refresh
380
+ </button>
381
+ {data && (
382
+ <span className="text-[11px] ml-auto" style={{ color: 'var(--c-text3)' }}>
383
+ {data.length} subscription{data.length !== 1 ? 's' : ''} detected
384
+ </span>
385
+ )}
386
+ </div>
387
+
388
+ {loading && !data && (
389
+ <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text2)' }}>loading subscriptions...</div>
390
+ )}
391
+
392
+ {error && (
393
+ <div className="text-[12px] px-3 py-2" style={{ background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.15)', color: '#ef4444' }}>
394
+ {error}
395
+ </div>
396
+ )}
397
+
398
+ {data && data.length === 0 && (
399
+ <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text3)' }}>
400
+ No subscriptions detected. Make sure your editors are installed and logged in.
401
+ </div>
402
+ )}
403
+
404
+ {data && data.length > 0 && (
405
+ <div className="grid gap-3" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))' }}>
406
+ {data.map((item, i) => (
407
+ <EditorCard key={item.source + '-' + i} data={item} />
408
+ ))}
409
+ </div>
410
+ )}
411
+ </div>
412
+ )
413
+ }