agentlytics 0.1.3 → 0.1.6
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 +1 -1
- package/cache.js +420 -10
- package/editors/cursor.js +28 -6
- package/editors/vscode.js +6 -0
- package/index.js +78 -11
- package/package.json +2 -1
- package/server.js +27 -0
- package/ui/package-lock.json +60 -375
- package/ui/package.json +1 -1
- package/ui/src/App.jsx +22 -17
- package/ui/src/components/ActivityHeatmap.jsx +3 -3
- package/ui/src/components/AnimatedLogo.jsx +96 -0
- package/ui/src/components/ChatSidebar.jsx +7 -7
- package/ui/src/components/DateRangePicker.jsx +5 -5
- package/ui/src/components/EditorBreakdown.jsx +2 -2
- package/ui/src/components/EditorDot.jsx +1 -1
- package/ui/src/components/KpiCard.jsx +2 -2
- package/ui/src/components/LiveFeed.jsx +8 -8
- package/ui/src/components/LoginScreen.jsx +8 -6
- package/ui/src/components/MessageRenderer.jsx +5 -5
- package/ui/src/components/ModelBreakdown.jsx +3 -3
- package/ui/src/components/SectionTitle.jsx +1 -1
- package/ui/src/index.css +1 -1
- package/ui/src/lib/api.js +20 -0
- package/ui/src/lib/constants.js +8 -0
- package/ui/src/pages/ChatDetail.jsx +5 -2
- package/ui/src/pages/Compare.jsx +18 -18
- package/ui/src/pages/CostAnalysis.jsx +356 -0
- package/ui/src/pages/Dashboard.jsx +39 -21
- package/ui/src/pages/DeepAnalysis.jsx +38 -31
- package/ui/src/pages/ProjectDetail.jsx +23 -15
- package/ui/src/pages/Projects.jsx +14 -8
- package/ui/src/pages/RelayDashboard.jsx +29 -29
- package/ui/src/pages/RelaySessionDetail.jsx +1 -1
- package/ui/src/pages/RelayUserDetail.jsx +18 -18
- package/ui/src/pages/Sessions.jsx +24 -20
- package/ui/src/pages/SqlViewer.jsx +14 -14
|
@@ -3,8 +3,8 @@ import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearSca
|
|
|
3
3
|
import { Doughnut, Bar } from 'react-chartjs-2'
|
|
4
4
|
import { Search, MessageSquare, Wrench, Cpu, FolderOpen, Calendar, ArrowRight } from 'lucide-react'
|
|
5
5
|
import { useNavigate } from 'react-router-dom'
|
|
6
|
-
import { fetchProjects } from '../lib/api'
|
|
7
|
-
import { editorColor, editorLabel, formatNumber, formatDate, dateRangeToApiParams } from '../lib/constants'
|
|
6
|
+
import { fetchProjects, fetchCosts } from '../lib/api'
|
|
7
|
+
import { editorColor, editorLabel, formatNumber, formatCost, formatDate, dateRangeToApiParams } from '../lib/constants'
|
|
8
8
|
import { useTheme } from '../lib/theme'
|
|
9
9
|
import KpiCard from '../components/KpiCard'
|
|
10
10
|
import EditorIcon from '../components/EditorIcon'
|
|
@@ -25,11 +25,16 @@ export default function Projects({ overview }) {
|
|
|
25
25
|
const [search, setSearch] = useState('')
|
|
26
26
|
const [editorFilter, setEditorFilter] = useState('')
|
|
27
27
|
const [dateRange, setDateRange] = useState(null)
|
|
28
|
+
const [costs, setCosts] = useState(null)
|
|
28
29
|
const navigate = useNavigate()
|
|
29
30
|
const editors = overview?.editors || []
|
|
30
31
|
|
|
31
32
|
useEffect(() => {
|
|
32
|
-
|
|
33
|
+
const dateParams = dateRangeToApiParams(dateRange)
|
|
34
|
+
Promise.all([
|
|
35
|
+
fetchProjects(dateParams),
|
|
36
|
+
fetchCosts(dateParams),
|
|
37
|
+
]).then(([p, c]) => { setProjects(p); setCosts(c) })
|
|
33
38
|
}, [dateRange])
|
|
34
39
|
|
|
35
40
|
if (!projects) return <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text2)' }}>loading projects...</div>
|
|
@@ -64,6 +69,7 @@ export default function Projects({ overview }) {
|
|
|
64
69
|
<KpiCard label="sessions" value={formatNumber(totalSessions)} onClick={() => navigate('/sessions')} />
|
|
65
70
|
<KpiCard label="messages" value={formatNumber(totalMessages)} />
|
|
66
71
|
<KpiCard label="tokens" value={formatNumber(totalTokens)} />
|
|
72
|
+
<KpiCard label="est. cost" value={costs && costs.totalCost > 0 ? formatCost(costs.totalCost) : '\u2014'} />
|
|
67
73
|
</div>
|
|
68
74
|
|
|
69
75
|
{/* Charts row */}
|
|
@@ -136,7 +142,7 @@ export default function Projects({ overview }) {
|
|
|
136
142
|
<select
|
|
137
143
|
value={editorFilter}
|
|
138
144
|
onChange={e => setEditorFilter(e.target.value)}
|
|
139
|
-
className="px-2 py-1.5 text-[
|
|
145
|
+
className="px-2 py-1.5 text-[12px] outline-none rounded-sm"
|
|
140
146
|
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
141
147
|
>
|
|
142
148
|
<option value="">All Editors</option>
|
|
@@ -151,7 +157,7 @@ export default function Projects({ overview }) {
|
|
|
151
157
|
placeholder="search projects..."
|
|
152
158
|
value={search}
|
|
153
159
|
onChange={e => setSearch(e.target.value)}
|
|
154
|
-
className="w-full pl-8 pr-3 py-1.5 text-[
|
|
160
|
+
className="w-full pl-8 pr-3 py-1.5 text-[12px] outline-none rounded-sm"
|
|
155
161
|
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
156
162
|
/>
|
|
157
163
|
</div>
|
|
@@ -176,7 +182,7 @@ export default function Projects({ overview }) {
|
|
|
176
182
|
<FolderOpen size={14} className="flex-shrink-0 mt-0.5" style={{ color: 'var(--c-accent)' }} />
|
|
177
183
|
<div className="flex-1 min-w-0">
|
|
178
184
|
<div className="text-[12px] font-bold truncate" style={{ color: 'var(--c-white)' }}>{p.name}</div>
|
|
179
|
-
<div className="text-[
|
|
185
|
+
<div className="text-[10px] truncate" style={{ color: 'var(--c-text3)' }}>{p.folder}</div>
|
|
180
186
|
</div>
|
|
181
187
|
<div className="flex items-center gap-1 flex-shrink-0">
|
|
182
188
|
{editorEntries.slice(0, 4).map(([e]) => (
|
|
@@ -191,7 +197,7 @@ export default function Projects({ overview }) {
|
|
|
191
197
|
</div>
|
|
192
198
|
|
|
193
199
|
{/* Stats grid */}
|
|
194
|
-
<div className="grid grid-cols-2 gap-x-3 gap-y-1 text-[
|
|
200
|
+
<div className="grid grid-cols-2 gap-x-3 gap-y-1 text-[11px]">
|
|
195
201
|
<div className="flex items-center gap-1">
|
|
196
202
|
<MessageSquare size={9} style={{ color: 'var(--c-text3)' }} />
|
|
197
203
|
<span style={{ color: 'var(--c-text2)' }}>{p.totalSessions} sessions</span>
|
|
@@ -213,7 +219,7 @@ export default function Projects({ overview }) {
|
|
|
213
219
|
</div>
|
|
214
220
|
|
|
215
221
|
{/* Footer: editors breakdown + date */}
|
|
216
|
-
<div className="flex items-center gap-2 pt-1 text-[
|
|
222
|
+
<div className="flex items-center gap-2 pt-1 text-[10px]" style={{ borderTop: '1px solid var(--c-border)' }}>
|
|
217
223
|
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
|
218
224
|
{editorEntries.map(([e, c]) => (
|
|
219
225
|
<span key={e} className="inline-flex items-center gap-0.5 truncate">
|
|
@@ -88,7 +88,7 @@ function TeamSidebar({ userList, userColorMap, onUserClick, selectedUser }) {
|
|
|
88
88
|
{u.username.charAt(0).toUpperCase()}
|
|
89
89
|
</div>
|
|
90
90
|
<div className="flex-1 min-w-0">
|
|
91
|
-
<div className="text-[
|
|
91
|
+
<div className="text-[11px] font-medium truncate" style={{ color: 'var(--c-white)' }}>{u.username}</div>
|
|
92
92
|
<div className="flex items-center gap-1 text-[8px]" style={{ color: 'var(--c-text3)' }}>
|
|
93
93
|
<span>{u.sessions}s</span>
|
|
94
94
|
<span>·</span>
|
|
@@ -110,8 +110,8 @@ function TeamSidebar({ userList, userColorMap, onUserClick, selectedUser }) {
|
|
|
110
110
|
<div className="flex flex-col h-full">
|
|
111
111
|
<div className="flex items-center gap-2 px-3 py-2.5 shrink-0" style={{ borderBottom: '1px solid var(--c-border)' }}>
|
|
112
112
|
<Users size={12} style={{ color: 'var(--c-accent)' }} />
|
|
113
|
-
<span className="text-[
|
|
114
|
-
<span className="text-[
|
|
113
|
+
<span className="text-[12px] font-medium uppercase tracking-wider" style={{ color: 'var(--c-text2)' }}>Team</span>
|
|
114
|
+
<span className="text-[10px] ml-auto" style={{ color: 'var(--c-text3)' }}>{userList.length}</span>
|
|
115
115
|
</div>
|
|
116
116
|
|
|
117
117
|
<div className="flex-1 overflow-y-auto scrollbar-thin py-1">
|
|
@@ -129,7 +129,7 @@ function TeamSidebar({ userList, userColorMap, onUserClick, selectedUser }) {
|
|
|
129
129
|
>
|
|
130
130
|
{isCollapsed ? <ChevronRight size={10} style={{ color: 'var(--c-text3)' }} /> : <ChevronDown size={10} style={{ color: 'var(--c-text3)' }} />}
|
|
131
131
|
<FolderOpen size={10} style={{ color: '#818cf8' }} />
|
|
132
|
-
<span className="text-[
|
|
132
|
+
<span className="text-[11px] font-medium truncate flex-1" style={{ color: 'var(--c-text2)' }} title={proj}>{projName}</span>
|
|
133
133
|
<span className="text-[8px]" style={{ color: 'var(--c-text3)' }}>{users.length}</span>
|
|
134
134
|
</div>
|
|
135
135
|
{!isCollapsed && (
|
|
@@ -145,7 +145,7 @@ function TeamSidebar({ userList, userColorMap, onUserClick, selectedUser }) {
|
|
|
145
145
|
{ungrouped.length > 0 && (
|
|
146
146
|
<div>
|
|
147
147
|
{projectGroups.length > 0 && (
|
|
148
|
-
<div className="px-2 py-1.5 text-[
|
|
148
|
+
<div className="px-2 py-1.5 text-[10px] uppercase tracking-wider" style={{ color: 'var(--c-text3)' }}>
|
|
149
149
|
unassigned
|
|
150
150
|
</div>
|
|
151
151
|
)}
|
|
@@ -156,7 +156,7 @@ function TeamSidebar({ userList, userColorMap, onUserClick, selectedUser }) {
|
|
|
156
156
|
)}
|
|
157
157
|
|
|
158
158
|
{userList.length === 0 && (
|
|
159
|
-
<div className="text-[
|
|
159
|
+
<div className="text-[11px] py-6 text-center" style={{ color: 'var(--c-text3)' }}>
|
|
160
160
|
No team members yet
|
|
161
161
|
</div>
|
|
162
162
|
)}
|
|
@@ -180,7 +180,7 @@ function MergeSection({ userList }) {
|
|
|
180
180
|
return (
|
|
181
181
|
<div className="shrink-0 px-2 py-2" style={{ borderTop: '1px solid var(--c-border)' }}>
|
|
182
182
|
<div
|
|
183
|
-
className="flex items-center gap-1.5 cursor-pointer text-[
|
|
183
|
+
className="flex items-center gap-1.5 cursor-pointer text-[10px] uppercase tracking-wider"
|
|
184
184
|
style={{ color: 'var(--c-text3)' }}
|
|
185
185
|
onClick={() => setOpen(!open)}
|
|
186
186
|
>
|
|
@@ -193,7 +193,7 @@ function MergeSection({ userList }) {
|
|
|
193
193
|
<select
|
|
194
194
|
value={mergeFrom}
|
|
195
195
|
onChange={e => setMergeFrom(e.target.value)}
|
|
196
|
-
className="w-full text-[
|
|
196
|
+
className="w-full text-[11px] px-1.5 py-1 outline-none rounded-sm"
|
|
197
197
|
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
198
198
|
>
|
|
199
199
|
<option value="">from...</option>
|
|
@@ -204,7 +204,7 @@ function MergeSection({ userList }) {
|
|
|
204
204
|
<select
|
|
205
205
|
value={mergeTo}
|
|
206
206
|
onChange={e => setMergeTo(e.target.value)}
|
|
207
|
-
className="w-full text-[
|
|
207
|
+
className="w-full text-[11px] px-1.5 py-1 outline-none rounded-sm"
|
|
208
208
|
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
209
209
|
>
|
|
210
210
|
<option value="">into...</option>
|
|
@@ -228,7 +228,7 @@ function MergeSection({ userList }) {
|
|
|
228
228
|
}
|
|
229
229
|
setMerging(false)
|
|
230
230
|
}}
|
|
231
|
-
className="w-full text-[
|
|
231
|
+
className="w-full text-[11px] px-2 py-1 font-medium transition rounded-sm"
|
|
232
232
|
style={{
|
|
233
233
|
background: mergeFrom && mergeTo ? 'rgba(239,68,68,0.15)' : 'var(--c-bg3)',
|
|
234
234
|
color: mergeFrom && mergeTo ? '#ef4444' : 'var(--c-text3)',
|
|
@@ -240,7 +240,7 @@ function MergeSection({ userList }) {
|
|
|
240
240
|
{merging ? 'Merging...' : 'Merge'}
|
|
241
241
|
</button>
|
|
242
242
|
{mergeResult && (
|
|
243
|
-
<div className="text-[
|
|
243
|
+
<div className="text-[10px]" style={{ color: mergeResult.error ? '#ef4444' : '#22c55e' }}>
|
|
244
244
|
{mergeResult.error ? `Error: ${mergeResult.error}` : 'Merged!'}
|
|
245
245
|
</div>
|
|
246
246
|
)}
|
|
@@ -334,25 +334,25 @@ export default function RelayDashboard() {
|
|
|
334
334
|
<div className="card p-3">
|
|
335
335
|
<div className="flex items-center justify-between mb-2">
|
|
336
336
|
<SectionTitle>team token usage</SectionTitle>
|
|
337
|
-
<span className="text-[
|
|
337
|
+
<span className="text-[11px] font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(totalTok)} total</span>
|
|
338
338
|
</div>
|
|
339
339
|
<ProportionBar height={10} segments={[
|
|
340
340
|
{ label: 'Input', value: stats.totalInputTokens, color: '#6366f1' },
|
|
341
341
|
{ label: 'Output', value: stats.totalOutputTokens, color: '#a78bfa' },
|
|
342
342
|
]} />
|
|
343
|
-
<div className="flex items-center gap-4 mt-1.5 text-[
|
|
343
|
+
<div className="flex items-center gap-4 mt-1.5 text-[10px]">
|
|
344
344
|
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#6366f1' }} /> input {formatNumber(stats.totalInputTokens)}</span>
|
|
345
345
|
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#a78bfa' }} /> output {formatNumber(stats.totalOutputTokens)}</span>
|
|
346
346
|
</div>
|
|
347
347
|
{userList.length > 1 && (
|
|
348
348
|
<div className="mt-3 pt-2" style={{ borderTop: '1px solid var(--c-border)' }}>
|
|
349
|
-
<div className="text-[
|
|
349
|
+
<div className="text-[10px] mb-1.5" style={{ color: 'var(--c-text3)' }}>per-user contribution</div>
|
|
350
350
|
<ProportionBar height={8} segments={userList.map(u => ({
|
|
351
351
|
label: u.username,
|
|
352
352
|
value: u.totalInputTokens + u.totalOutputTokens,
|
|
353
353
|
color: userColorMap[u.username],
|
|
354
354
|
}))} />
|
|
355
|
-
<div className="flex flex-wrap gap-3 mt-1 text-[
|
|
355
|
+
<div className="flex flex-wrap gap-3 mt-1 text-[10px]">
|
|
356
356
|
{userList.map(u => (
|
|
357
357
|
<span key={u.username} className="flex items-center gap-1" style={{ color: 'var(--c-text3)' }}>
|
|
358
358
|
<span className="w-2 h-2 rounded-sm" style={{ background: userColorMap[u.username] }} />
|
|
@@ -445,14 +445,14 @@ export default function RelayDashboard() {
|
|
|
445
445
|
value={search}
|
|
446
446
|
onChange={e => setSearch(e.target.value)}
|
|
447
447
|
placeholder="Search messages, files, topics across all users..."
|
|
448
|
-
className="w-full pl-7 pr-3 py-1.5 text-[
|
|
448
|
+
className="w-full pl-7 pr-3 py-1.5 text-[12px] outline-none rounded-sm"
|
|
449
449
|
style={{ background: 'var(--c-bg3)', color: 'var(--c-white)', border: '1px solid var(--c-border)' }}
|
|
450
450
|
/>
|
|
451
451
|
</div>
|
|
452
452
|
<button
|
|
453
453
|
type="submit"
|
|
454
454
|
disabled={searching}
|
|
455
|
-
className="px-3 py-1.5 text-[
|
|
455
|
+
className="px-3 py-1.5 text-[11px] font-medium transition rounded-sm"
|
|
456
456
|
style={{ background: 'var(--c-card)', border: '1px solid var(--c-border)', color: 'var(--c-white)' }}
|
|
457
457
|
>
|
|
458
458
|
{searching ? 'Searching...' : 'Search'}
|
|
@@ -461,9 +461,9 @@ export default function RelayDashboard() {
|
|
|
461
461
|
{searchResults && (
|
|
462
462
|
<div className="mt-3 max-h-[300px] overflow-y-auto scrollbar-thin">
|
|
463
463
|
{searchResults.length === 0 ? (
|
|
464
|
-
<div className="text-[
|
|
464
|
+
<div className="text-[12px] py-2" style={{ color: 'var(--c-text3)' }}>No results found</div>
|
|
465
465
|
) : (
|
|
466
|
-
<table className="w-full text-[
|
|
466
|
+
<table className="w-full text-[12px]">
|
|
467
467
|
<tbody>
|
|
468
468
|
{searchResults.map((r, i) => (
|
|
469
469
|
<tr
|
|
@@ -475,14 +475,14 @@ export default function RelayDashboard() {
|
|
|
475
475
|
onClick={() => { setSelectedChat(r.chatId); setSelectedUsername(r.username) }}
|
|
476
476
|
>
|
|
477
477
|
<td className="py-2 px-2 w-[80px]">
|
|
478
|
-
<span className="text-[
|
|
478
|
+
<span className="text-[10px] font-medium px-1.5 py-0.5" style={{ background: 'rgba(99,102,241,0.15)', color: '#818cf8' }}>{r.username}</span>
|
|
479
479
|
</td>
|
|
480
480
|
<td className="py-2 px-2 w-[24px]"><EditorIcon source={r.source} size={11} /></td>
|
|
481
481
|
<td className="py-2 px-2">
|
|
482
|
-
<div className="text-[
|
|
483
|
-
<div className="text-[
|
|
482
|
+
<div className="text-[11px] truncate" style={{ color: 'var(--c-text3)' }}>{r.chatName}</div>
|
|
483
|
+
<div className="text-[11px] line-clamp-1 mt-0.5" style={{ color: 'var(--c-text)' }}>{r.content}</div>
|
|
484
484
|
</td>
|
|
485
|
-
<td className="py-2 px-2 text-[
|
|
485
|
+
<td className="py-2 px-2 text-[10px] whitespace-nowrap" style={{ color: 'var(--c-text3)' }}>{r.role}</td>
|
|
486
486
|
</tr>
|
|
487
487
|
))}
|
|
488
488
|
</tbody>
|
|
@@ -506,12 +506,12 @@ export default function RelayDashboard() {
|
|
|
506
506
|
onClick={() => navigate(`/relay/user/${u.username}`)}
|
|
507
507
|
>
|
|
508
508
|
<div className="flex items-center gap-2 mb-2">
|
|
509
|
-
<div className="w-7 h-7 flex items-center justify-center text-[
|
|
509
|
+
<div className="w-7 h-7 flex items-center justify-center text-[12px] font-bold rounded-sm flex-shrink-0" style={{ background: `${color}20`, color }}>
|
|
510
510
|
{u.username.charAt(0).toUpperCase()}
|
|
511
511
|
</div>
|
|
512
512
|
<div className="flex-1 min-w-0">
|
|
513
|
-
<div className="text-[
|
|
514
|
-
<div className="text-[
|
|
513
|
+
<div className="text-[12px] font-bold truncate" style={{ color: 'var(--c-white)' }}>{u.username}</div>
|
|
514
|
+
<div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>
|
|
515
515
|
{u.lastActive ? formatDate(u.lastActive) : 'No activity'}
|
|
516
516
|
</div>
|
|
517
517
|
</div>
|
|
@@ -529,7 +529,7 @@ export default function RelayDashboard() {
|
|
|
529
529
|
[formatNumber(uTok), 'tokens'],
|
|
530
530
|
].map(([v, l]) => (
|
|
531
531
|
<div key={l} className="p-1 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
532
|
-
<div className="text-[
|
|
532
|
+
<div className="text-[11px] font-bold" style={{ color: 'var(--c-white)' }}>{v}</div>
|
|
533
533
|
<div className="text-[7px]" style={{ color: 'var(--c-text3)' }}>{l}</div>
|
|
534
534
|
</div>
|
|
535
535
|
))}
|
|
@@ -554,7 +554,7 @@ export default function RelayDashboard() {
|
|
|
554
554
|
<div className="card p-8 text-center">
|
|
555
555
|
<Users size={32} className="mx-auto mb-3" style={{ color: 'var(--c-text3)' }} />
|
|
556
556
|
<div className="text-[12px] font-medium mb-1" style={{ color: 'var(--c-white)' }}>No team members yet</div>
|
|
557
|
-
<div className="text-[
|
|
557
|
+
<div className="text-[11px]" style={{ color: 'var(--c-text3)' }}>Share the join command with your team to start collecting data</div>
|
|
558
558
|
</div>
|
|
559
559
|
)}
|
|
560
560
|
</div>
|
|
@@ -575,7 +575,7 @@ export default function RelayDashboard() {
|
|
|
575
575
|
username={selectedUsername}
|
|
576
576
|
extraHeader={
|
|
577
577
|
selectedUsername ? (
|
|
578
|
-
<span className="text-[
|
|
578
|
+
<span className="text-[11px] font-medium px-1.5 py-0.5 shrink-0" style={{ background: 'rgba(99,102,241,0.15)', color: '#818cf8' }}>
|
|
579
579
|
{selectedUsername}
|
|
580
580
|
</span>
|
|
581
581
|
) : null
|
|
@@ -15,7 +15,7 @@ export default function RelaySessionDetail() {
|
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
const extraHeader = username ? (
|
|
18
|
-
<span className="text-[
|
|
18
|
+
<span className="text-[11px] font-medium px-1.5 py-0.5 shrink-0" style={{ background: 'rgba(99,102,241,0.15)', color: '#818cf8' }}>
|
|
19
19
|
{username}
|
|
20
20
|
</span>
|
|
21
21
|
) : null
|
|
@@ -73,7 +73,7 @@ function SessionSidebar({ sessions, projects, selectedChat, onSelectChat }) {
|
|
|
73
73
|
>
|
|
74
74
|
<EditorIcon source={s.source} size={10} />
|
|
75
75
|
<div className="flex-1 min-w-0">
|
|
76
|
-
<div className="text-[
|
|
76
|
+
<div className="text-[11px] font-medium truncate" style={{ color: 'var(--c-white)' }}>{s.name || 'Untitled'}</div>
|
|
77
77
|
<div className="flex items-center gap-1 text-[8px]" style={{ color: 'var(--c-text3)' }}>
|
|
78
78
|
{s.totalMessages > 0 && <span>{s.totalMessages}m</span>}
|
|
79
79
|
{s.totalMessages > 0 && s.lastUpdatedAt && <span>·</span>}
|
|
@@ -87,8 +87,8 @@ function SessionSidebar({ sessions, projects, selectedChat, onSelectChat }) {
|
|
|
87
87
|
<div className="flex flex-col h-full">
|
|
88
88
|
<div className="flex items-center gap-2 px-3 py-2.5 shrink-0" style={{ borderBottom: '1px solid var(--c-border)' }}>
|
|
89
89
|
<Hash size={12} style={{ color: 'var(--c-accent)' }} />
|
|
90
|
-
<span className="text-[
|
|
91
|
-
<span className="text-[
|
|
90
|
+
<span className="text-[12px] font-medium uppercase tracking-wider" style={{ color: 'var(--c-text2)' }}>Sessions</span>
|
|
91
|
+
<span className="text-[10px] ml-auto" style={{ color: 'var(--c-text3)' }}>{sessions.length}</span>
|
|
92
92
|
</div>
|
|
93
93
|
|
|
94
94
|
<div className="flex-1 overflow-y-auto scrollbar-thin py-1">
|
|
@@ -105,7 +105,7 @@ function SessionSidebar({ sessions, projects, selectedChat, onSelectChat }) {
|
|
|
105
105
|
>
|
|
106
106
|
{isCollapsed ? <ChevronRight size={10} style={{ color: 'var(--c-text3)' }} /> : <ChevronDown size={10} style={{ color: 'var(--c-text3)' }} />}
|
|
107
107
|
<FolderOpen size={10} style={{ color: '#818cf8' }} />
|
|
108
|
-
<span className="text-[
|
|
108
|
+
<span className="text-[11px] font-medium truncate flex-1" style={{ color: 'var(--c-text2)' }} title={folder}>{folderName}</span>
|
|
109
109
|
<span className="text-[8px]" style={{ color: 'var(--c-text3)' }}>{list.length}</span>
|
|
110
110
|
</div>
|
|
111
111
|
{!isCollapsed && (
|
|
@@ -120,7 +120,7 @@ function SessionSidebar({ sessions, projects, selectedChat, onSelectChat }) {
|
|
|
120
120
|
{grouped.noProject.length > 0 && (
|
|
121
121
|
<div>
|
|
122
122
|
{grouped.sorted.length > 0 && (
|
|
123
|
-
<div className="px-2 py-1.5 text-[
|
|
123
|
+
<div className="px-2 py-1.5 text-[10px] uppercase tracking-wider" style={{ color: 'var(--c-text3)' }}>no project</div>
|
|
124
124
|
)}
|
|
125
125
|
<div className={grouped.sorted.length > 0 ? 'pl-1' : ''}>
|
|
126
126
|
{grouped.noProject.map(s => <SessionItem key={s.id} s={s} />)}
|
|
@@ -270,13 +270,13 @@ export default function RelayUserDetail() {
|
|
|
270
270
|
<div className="card p-3">
|
|
271
271
|
<div className="flex items-center justify-between mb-2">
|
|
272
272
|
<SectionTitle>token usage</SectionTitle>
|
|
273
|
-
<span className="text-[
|
|
273
|
+
<span className="text-[11px] font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(totalTokens)} total</span>
|
|
274
274
|
</div>
|
|
275
275
|
<ProportionBar height={10} segments={[
|
|
276
276
|
{ label: 'Input', value: totalInputTokens, color: '#6366f1' },
|
|
277
277
|
{ label: 'Output', value: totalOutputTokens, color: '#a78bfa' },
|
|
278
278
|
]} />
|
|
279
|
-
<div className="flex items-center gap-4 mt-1.5 text-[
|
|
279
|
+
<div className="flex items-center gap-4 mt-1.5 text-[10px]">
|
|
280
280
|
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#6366f1' }} /> input {formatNumber(totalInputTokens)}</span>
|
|
281
281
|
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#a78bfa' }} /> output {formatNumber(totalOutputTokens)}</span>
|
|
282
282
|
</div>
|
|
@@ -363,20 +363,20 @@ export default function RelayUserDetail() {
|
|
|
363
363
|
<div key={folder} className="card px-3 py-2.5">
|
|
364
364
|
<div className="flex items-center gap-1.5 mb-1.5">
|
|
365
365
|
<FolderOpen size={11} style={{ color: '#818cf8' }} />
|
|
366
|
-
<span className="text-[
|
|
366
|
+
<span className="text-[12px] font-medium truncate" style={{ color: 'var(--c-white)' }}>{folder.split('/').pop()}</span>
|
|
367
367
|
</div>
|
|
368
368
|
<div className="text-[8px] truncate mb-2" style={{ color: 'var(--c-text3)' }}>{folder}</div>
|
|
369
369
|
<div className="grid grid-cols-3 gap-1 text-center mb-1.5">
|
|
370
370
|
<div className="p-1 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
371
|
-
<div className="text-[
|
|
371
|
+
<div className="text-[11px] font-bold" style={{ color: 'var(--c-white)' }}>{info.count}</div>
|
|
372
372
|
<div className="text-[7px]" style={{ color: 'var(--c-text3)' }}>sessions</div>
|
|
373
373
|
</div>
|
|
374
374
|
<div className="p-1 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
375
|
-
<div className="text-[
|
|
375
|
+
<div className="text-[11px] font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(info.messages)}</div>
|
|
376
376
|
<div className="text-[7px]" style={{ color: 'var(--c-text3)' }}>messages</div>
|
|
377
377
|
</div>
|
|
378
378
|
<div className="p-1 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
379
|
-
<div className="text-[
|
|
379
|
+
<div className="text-[11px] font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(info.tokens)}</div>
|
|
380
380
|
<div className="text-[7px]" style={{ color: 'var(--c-text3)' }}>tokens</div>
|
|
381
381
|
</div>
|
|
382
382
|
</div>
|
|
@@ -396,7 +396,7 @@ export default function RelayUserDetail() {
|
|
|
396
396
|
<SectionTitle>top tools</SectionTitle>
|
|
397
397
|
<div className="flex flex-wrap gap-1.5 mt-1">
|
|
398
398
|
{tools.map(([name, count]) => (
|
|
399
|
-
<span key={name} className="text-[
|
|
399
|
+
<span key={name} className="text-[10px] px-2 py-1 rounded-sm" style={{ background: 'var(--c-code-bg)', color: 'var(--c-text2)' }}>
|
|
400
400
|
{name} <span style={{ color: 'var(--c-text3)' }}>×{count}</span>
|
|
401
401
|
</span>
|
|
402
402
|
))}
|
|
@@ -408,7 +408,7 @@ export default function RelayUserDetail() {
|
|
|
408
408
|
<div className="card p-3">
|
|
409
409
|
<SectionTitle>recent sessions</SectionTitle>
|
|
410
410
|
<div className="max-h-[400px] overflow-y-auto scrollbar-thin">
|
|
411
|
-
<table className="w-full text-[
|
|
411
|
+
<table className="w-full text-[12px]">
|
|
412
412
|
<tbody>
|
|
413
413
|
{sessions.slice(0, 50).map(s => (
|
|
414
414
|
<tr
|
|
@@ -421,10 +421,10 @@ export default function RelayUserDetail() {
|
|
|
421
421
|
>
|
|
422
422
|
<td className="py-2 px-2 w-[24px]"><EditorIcon source={s.source} size={11} /></td>
|
|
423
423
|
<td className="py-2 px-2">
|
|
424
|
-
<div className="text-[
|
|
425
|
-
<div className="text-[
|
|
424
|
+
<div className="text-[11px] font-medium truncate" style={{ color: 'var(--c-white)' }}>{s.name || 'Untitled'}</div>
|
|
425
|
+
<div className="text-[10px] truncate" style={{ color: 'var(--c-text3)' }}>{s.folder ? s.folder.split('/').pop() : ''}</div>
|
|
426
426
|
</td>
|
|
427
|
-
<td className="py-2 px-2 text-[
|
|
427
|
+
<td className="py-2 px-2 text-[10px] whitespace-nowrap" style={{ color: 'var(--c-text3)' }}>
|
|
428
428
|
{s.totalMessages > 0 && <span>{s.totalMessages}m</span>}
|
|
429
429
|
</td>
|
|
430
430
|
<td className="py-2 px-2">
|
|
@@ -432,7 +432,7 @@ export default function RelayUserDetail() {
|
|
|
432
432
|
<span className="text-[8px] px-1.5 py-0.5 rounded-sm" style={{ background: 'rgba(168,85,247,0.1)', color: '#a855f7' }}>{s.mode}</span>
|
|
433
433
|
)}
|
|
434
434
|
</td>
|
|
435
|
-
<td className="py-2 px-2 text-[
|
|
435
|
+
<td className="py-2 px-2 text-[10px] whitespace-nowrap text-right" style={{ color: 'var(--c-text3)' }}>
|
|
436
436
|
{formatDate(s.lastUpdatedAt)}
|
|
437
437
|
</td>
|
|
438
438
|
</tr>
|
|
@@ -458,7 +458,7 @@ export default function RelayUserDetail() {
|
|
|
458
458
|
fetchFn={fetchFn}
|
|
459
459
|
username={username}
|
|
460
460
|
extraHeader={
|
|
461
|
-
<span className="text-[
|
|
461
|
+
<span className="text-[11px] font-medium px-1.5 py-0.5 shrink-0" style={{ background: 'rgba(99,102,241,0.15)', color: '#818cf8' }}>
|
|
462
462
|
{username}
|
|
463
463
|
</span>
|
|
464
464
|
}
|
|
@@ -4,7 +4,7 @@ import { Search, Filter, List, FolderOpen, ChevronDown, ChevronRight, X, AlertTr
|
|
|
4
4
|
import { Chart as ChartJS, ArcElement, CategoryScale, LinearScale, PointElement, LineElement, BarElement, Tooltip, Legend, Filler } from 'chart.js'
|
|
5
5
|
import { Line, Doughnut, Bar } from 'react-chartjs-2'
|
|
6
6
|
import { fetchChats } from '../lib/api'
|
|
7
|
-
import { editorColor, editorLabel, formatNumber, formatDate, dateRangeToApiParams } from '../lib/constants'
|
|
7
|
+
import { editorColor, editorLabel, formatNumber, formatCost, formatDate, dateRangeToApiParams } from '../lib/constants'
|
|
8
8
|
import { useTheme } from '../lib/theme'
|
|
9
9
|
import KpiCard from '../components/KpiCard'
|
|
10
10
|
import EditorIcon from '../components/EditorIcon'
|
|
@@ -250,15 +250,15 @@ export default function Sessions({ overview }) {
|
|
|
250
250
|
<td className="py-2 px-3">
|
|
251
251
|
<span className="inline-flex items-center gap-1.5">
|
|
252
252
|
<EditorIcon source={c.source} size={12} />
|
|
253
|
-
<span className="text-[
|
|
253
|
+
<span className="text-[12px]" style={{ color: 'var(--c-text2)' }}>{editorLabel(c.source)}</span>
|
|
254
254
|
</span>
|
|
255
255
|
</td>
|
|
256
|
-
<td className="py-2 px-3 font-medium truncate max-w-[280px] text-[
|
|
256
|
+
<td className="py-2 px-3 font-medium truncate max-w-[280px] text-[12px]" style={{ color: 'var(--c-white)' }}>
|
|
257
257
|
{c.name || <span style={{ color: 'var(--c-text3)' }}>Untitled</span>}
|
|
258
|
-
{c.encrypted && <span className="ml-1.5 text-[
|
|
258
|
+
{c.encrypted && <span className="ml-1.5 text-[10px] text-yellow-500/60">locked</span>}
|
|
259
259
|
</td>
|
|
260
260
|
{!groupByProject && (
|
|
261
|
-
<td className="py-2 px-3 truncate max-w-[160px] text-[
|
|
261
|
+
<td className="py-2 px-3 truncate max-w-[160px] text-[12px]" style={{ color: 'var(--c-text2)' }} title={c.folder}>
|
|
262
262
|
{c.folder ? (
|
|
263
263
|
<span
|
|
264
264
|
className="cursor-pointer hover:underline"
|
|
@@ -268,11 +268,11 @@ export default function Sessions({ overview }) {
|
|
|
268
268
|
) : ''}
|
|
269
269
|
</td>
|
|
270
270
|
)}
|
|
271
|
-
<td className="py-2 px-3 text-[
|
|
272
|
-
<td className="py-2 px-3 text-[
|
|
271
|
+
<td className="py-2 px-3 text-[12px]" style={{ color: 'var(--c-text2)' }}>{c.mode || ''}</td>
|
|
272
|
+
<td className="py-2 px-3 text-[12px] font-mono truncate max-w-[150px]" style={{ color: 'var(--c-text2)' }} title={c.topModel || ''}>
|
|
273
273
|
{c.topModel || ''}
|
|
274
274
|
</td>
|
|
275
|
-
<td className="py-2 px-3 text-[
|
|
275
|
+
<td className="py-2 px-3 text-[12px]">
|
|
276
276
|
{c.bubbleCount >= 500 ? (
|
|
277
277
|
<span className="inline-flex items-center gap-0.5 font-bold" style={{ color: '#ef4444' }}>
|
|
278
278
|
<AlertTriangle size={9} />{c.bubbleCount}
|
|
@@ -285,7 +285,10 @@ export default function Sessions({ overview }) {
|
|
|
285
285
|
<span style={{ color: 'var(--c-text3)' }}>{c.bubbleCount || 0}</span>
|
|
286
286
|
)}
|
|
287
287
|
</td>
|
|
288
|
-
<td className="py-2 px-3 text-[
|
|
288
|
+
<td className="py-2 px-3 text-[12px] font-mono text-right" style={{ color: c.cost > 0 ? 'var(--c-text2)' : 'var(--c-text3)' }}>
|
|
289
|
+
{c.cost > 0 ? formatCost(c.cost) : ''}
|
|
290
|
+
</td>
|
|
291
|
+
<td className="py-2 px-3 text-[12px] whitespace-nowrap" style={{ color: 'var(--c-text3)' }}>
|
|
289
292
|
{formatDate(c.lastUpdatedAt || c.createdAt)}
|
|
290
293
|
</td>
|
|
291
294
|
</tr>
|
|
@@ -342,7 +345,7 @@ export default function Sessions({ overview }) {
|
|
|
342
345
|
},
|
|
343
346
|
}}
|
|
344
347
|
/>
|
|
345
|
-
) : <div className="text-[
|
|
348
|
+
) : <div className="text-[11px] py-8 text-center" style={{ color: 'var(--c-text3)' }}>no model data</div>}
|
|
346
349
|
</div>
|
|
347
350
|
</div>
|
|
348
351
|
<div className="card p-3">
|
|
@@ -367,7 +370,7 @@ export default function Sessions({ overview }) {
|
|
|
367
370
|
plugins: { legend: { display: false }, tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } } },
|
|
368
371
|
}}
|
|
369
372
|
/>
|
|
370
|
-
) : <div className="text-[
|
|
373
|
+
) : <div className="text-[11px] py-8 text-center" style={{ color: 'var(--c-text3)' }}>no mode data</div>}
|
|
371
374
|
</div>
|
|
372
375
|
</div>
|
|
373
376
|
</div>
|
|
@@ -379,12 +382,12 @@ export default function Sessions({ overview }) {
|
|
|
379
382
|
<div className="flex items-center justify-between mb-2">
|
|
380
383
|
<SectionTitle>
|
|
381
384
|
session timeline
|
|
382
|
-
<span className="ml-2 font-normal text-[
|
|
385
|
+
<span className="ml-2 font-normal text-[10px]" style={{ color: 'var(--c-text3)' }}>(drag to select range)</span>
|
|
383
386
|
</SectionTitle>
|
|
384
387
|
{dateRange && (
|
|
385
388
|
<button
|
|
386
389
|
onClick={() => setDateRange(null)}
|
|
387
|
-
className="flex items-center gap-1 text-[
|
|
390
|
+
className="flex items-center gap-1 text-[11px] px-2 py-0.5 transition"
|
|
388
391
|
style={{ color: 'var(--c-accent)', border: '1px solid var(--c-border)' }}
|
|
389
392
|
>
|
|
390
393
|
<X size={10} />
|
|
@@ -424,7 +427,7 @@ export default function Sessions({ overview }) {
|
|
|
424
427
|
<select
|
|
425
428
|
value={editor}
|
|
426
429
|
onChange={e => setEditor(e.target.value)}
|
|
427
|
-
className="pl-8 pr-3 py-1 text-[
|
|
430
|
+
className="pl-8 pr-3 py-1 text-[12px] outline-none appearance-none cursor-pointer"
|
|
428
431
|
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
429
432
|
>
|
|
430
433
|
<option value="">All Editors</option>
|
|
@@ -440,13 +443,13 @@ export default function Sessions({ overview }) {
|
|
|
440
443
|
placeholder="search sessions..."
|
|
441
444
|
value={search}
|
|
442
445
|
onChange={e => setSearch(e.target.value)}
|
|
443
|
-
className="w-full pl-8 pr-3 py-1 text-[
|
|
446
|
+
className="w-full pl-8 pr-3 py-1 text-[12px] outline-none"
|
|
444
447
|
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
445
448
|
/>
|
|
446
449
|
</div>
|
|
447
450
|
<button
|
|
448
451
|
onClick={() => setGroupByProject(!groupByProject)}
|
|
449
|
-
className="flex items-center gap-1.5 px-3 py-1 text-[
|
|
452
|
+
className="flex items-center gap-1.5 px-3 py-1 text-[12px] transition"
|
|
450
453
|
style={{
|
|
451
454
|
border: groupByProject ? '1px solid var(--c-accent)' : '1px solid var(--c-border)',
|
|
452
455
|
color: groupByProject ? 'var(--c-accent)' : 'var(--c-text2)',
|
|
@@ -456,7 +459,7 @@ export default function Sessions({ overview }) {
|
|
|
456
459
|
{groupByProject ? <FolderOpen size={13} /> : <List size={13} />}
|
|
457
460
|
{groupByProject ? 'grouped' : 'flat'}
|
|
458
461
|
</button>
|
|
459
|
-
<span className="text-[
|
|
462
|
+
<span className="text-[11px]" style={{ color: 'var(--c-text3)' }}>
|
|
460
463
|
{loading ? 'loading...' : `${filtered.length} of ${total}`}
|
|
461
464
|
</span>
|
|
462
465
|
{/* Server-side date range filter */}
|
|
@@ -468,13 +471,14 @@ export default function Sessions({ overview }) {
|
|
|
468
471
|
<div className="card overflow-hidden">
|
|
469
472
|
<table className="w-full text-sm">
|
|
470
473
|
<thead>
|
|
471
|
-
<tr className="text-[
|
|
474
|
+
<tr className="text-[10px] uppercase tracking-wider" style={{ borderBottom: '1px solid var(--c-border)', color: 'var(--c-text3)' }}>
|
|
472
475
|
<th className="text-left py-2 px-3 font-medium">editor</th>
|
|
473
476
|
<th className="text-left py-2 px-3 font-medium">name</th>
|
|
474
477
|
<th className="text-left py-2 px-3 font-medium">project</th>
|
|
475
478
|
<th className="text-left py-2 px-3 font-medium">mode</th>
|
|
476
479
|
<th className="text-left py-2 px-3 font-medium">model</th>
|
|
477
480
|
<th className="text-left py-2 px-3 font-medium">context</th>
|
|
481
|
+
<th className="text-right py-2 px-3 font-medium">est. cost</th>
|
|
478
482
|
<th className="text-left py-2 px-3 font-medium">updated</th>
|
|
479
483
|
</tr>
|
|
480
484
|
</thead>
|
|
@@ -500,12 +504,12 @@ export default function Sessions({ overview }) {
|
|
|
500
504
|
{isCollapsed ? <ChevronRight size={13} style={{ color: 'var(--c-text3)' }} /> : <ChevronDown size={13} style={{ color: 'var(--c-text3)' }} />}
|
|
501
505
|
<FolderOpen size={13} style={{ color: 'var(--c-text3)' }} />
|
|
502
506
|
<span className="text-xs font-medium" style={{ color: 'var(--c-white)' }}>{g.name}</span>
|
|
503
|
-
<span className="text-[
|
|
507
|
+
<span className="text-[11px] truncate max-w-[300px]" style={{ color: 'var(--c-text3)' }}>{g.folder !== g.name && g.folder !== '(no project)' ? g.folder : ''}</span>
|
|
504
508
|
<span className="ml-auto flex items-center gap-2">
|
|
505
509
|
{editorSet.slice(0, 5).map(e => (
|
|
506
510
|
<span key={e} className="w-2 h-2 rounded-full" style={{ background: editorColor(e) }} title={editorLabel(e)} />
|
|
507
511
|
))}
|
|
508
|
-
<span className="text-[
|
|
512
|
+
<span className="text-[11px]" style={{ color: 'var(--c-text3)' }}>{g.items.length}</span>
|
|
509
513
|
</span>
|
|
510
514
|
</div>
|
|
511
515
|
{!isCollapsed && (
|