agentlytics 0.1.1 → 0.1.3
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/package.json +1 -1
- package/ui/src/App.jsx +5 -5
- package/ui/src/pages/DeepAnalysis.jsx +238 -64
- package/ui/src/pages/ProjectDetail.jsx +160 -84
- package/ui/src/pages/Projects.jsx +140 -77
- package/ui/src/pages/RelayDashboard.jsx +460 -254
- package/ui/src/pages/RelayUserDetail.jsx +373 -109
- package/ui/src/pages/Sessions.jsx +161 -43
|
@@ -1,21 +1,148 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react'
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
2
2
|
import { useParams, useNavigate } from 'react-router-dom'
|
|
3
|
-
import { ArrowLeft, MessageSquare, FolderOpen } from 'lucide-react'
|
|
3
|
+
import { ArrowLeft, MessageSquare, FolderOpen, ChevronDown, ChevronRight, Zap, Clock, Hash } from 'lucide-react'
|
|
4
|
+
import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement } from 'chart.js'
|
|
5
|
+
import { Doughnut, Bar } from 'react-chartjs-2'
|
|
4
6
|
import KpiCard from '../components/KpiCard'
|
|
5
|
-
import
|
|
7
|
+
import EditorIcon from '../components/EditorIcon'
|
|
6
8
|
import SectionTitle from '../components/SectionTitle'
|
|
7
|
-
import EditorBreakdown from '../components/EditorBreakdown'
|
|
8
|
-
import ModelBreakdown from '../components/ModelBreakdown'
|
|
9
9
|
import ChatSidebar from '../components/ChatSidebar'
|
|
10
10
|
import LiveFeed from '../components/LiveFeed'
|
|
11
|
-
import { formatNumber, formatDate } from '../lib/constants'
|
|
11
|
+
import { editorColor, editorLabel, formatNumber, formatDate } from '../lib/constants'
|
|
12
12
|
import { fetchRelayUserActivity, fetchRelaySession } from '../lib/api'
|
|
13
|
+
import { useTheme } from '../lib/theme'
|
|
14
|
+
|
|
15
|
+
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
|
|
16
|
+
|
|
17
|
+
const MONO = 'JetBrains Mono, monospace'
|
|
18
|
+
const MODEL_COLORS = ['#6366f1', '#a78bfa', '#818cf8', '#c084fc', '#e879f9', '#f472b6', '#fb7185', '#f87171', '#fbbf24', '#34d399']
|
|
19
|
+
|
|
20
|
+
function ProportionBar({ segments, height = 6 }) {
|
|
21
|
+
const total = segments.reduce((s, seg) => s + seg.value, 0)
|
|
22
|
+
if (total === 0) return null
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex w-full rounded-full overflow-hidden" style={{ height }}>
|
|
25
|
+
{segments.filter(s => s.value > 0).map((seg, i) => (
|
|
26
|
+
<div
|
|
27
|
+
key={i}
|
|
28
|
+
title={`${seg.label}: ${formatNumber(seg.value)}`}
|
|
29
|
+
className="h-full transition-all"
|
|
30
|
+
style={{ width: `${(seg.value / total * 100).toFixed(1)}%`, background: seg.color }}
|
|
31
|
+
/>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Left sidebar: sessions grouped by project (folder-tree)
|
|
38
|
+
function SessionSidebar({ sessions, projects, selectedChat, onSelectChat }) {
|
|
39
|
+
const [collapsed, setCollapsed] = useState(new Set())
|
|
40
|
+
const [filter, setFilter] = useState(null) // null = all, or project path
|
|
41
|
+
|
|
42
|
+
const toggle = (key) => {
|
|
43
|
+
setCollapsed(prev => {
|
|
44
|
+
const next = new Set(prev)
|
|
45
|
+
next.has(key) ? next.delete(key) : next.add(key)
|
|
46
|
+
return next
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Group sessions by project
|
|
51
|
+
const grouped = useMemo(() => {
|
|
52
|
+
const map = {}
|
|
53
|
+
const noProject = []
|
|
54
|
+
for (const s of sessions) {
|
|
55
|
+
if (s.folder) {
|
|
56
|
+
if (!map[s.folder]) map[s.folder] = []
|
|
57
|
+
map[s.folder].push(s)
|
|
58
|
+
} else {
|
|
59
|
+
noProject.push(s)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const sorted = Object.entries(map).sort((a, b) => b[1].length - a[1].length)
|
|
63
|
+
return { sorted, noProject }
|
|
64
|
+
}, [sessions])
|
|
65
|
+
|
|
66
|
+
const SessionItem = ({ s }) => (
|
|
67
|
+
<div
|
|
68
|
+
className="flex items-center gap-1.5 px-2 py-1.5 cursor-pointer transition rounded-sm"
|
|
69
|
+
style={{ background: selectedChat === s.id ? 'rgba(99,102,241,0.12)' : 'transparent' }}
|
|
70
|
+
onMouseEnter={e => { if (selectedChat !== s.id) e.currentTarget.style.background = 'var(--c-bg3)' }}
|
|
71
|
+
onMouseLeave={e => { if (selectedChat !== s.id) e.currentTarget.style.background = 'transparent' }}
|
|
72
|
+
onClick={() => onSelectChat(s.id)}
|
|
73
|
+
>
|
|
74
|
+
<EditorIcon source={s.source} size={10} />
|
|
75
|
+
<div className="flex-1 min-w-0">
|
|
76
|
+
<div className="text-[10px] font-medium truncate" style={{ color: 'var(--c-white)' }}>{s.name || 'Untitled'}</div>
|
|
77
|
+
<div className="flex items-center gap-1 text-[8px]" style={{ color: 'var(--c-text3)' }}>
|
|
78
|
+
{s.totalMessages > 0 && <span>{s.totalMessages}m</span>}
|
|
79
|
+
{s.totalMessages > 0 && s.lastUpdatedAt && <span>·</span>}
|
|
80
|
+
{s.lastUpdatedAt && <span>{formatDate(s.lastUpdatedAt)}</span>}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className="flex flex-col h-full">
|
|
88
|
+
<div className="flex items-center gap-2 px-3 py-2.5 shrink-0" style={{ borderBottom: '1px solid var(--c-border)' }}>
|
|
89
|
+
<Hash size={12} style={{ color: 'var(--c-accent)' }} />
|
|
90
|
+
<span className="text-[11px] font-medium uppercase tracking-wider" style={{ color: 'var(--c-text2)' }}>Sessions</span>
|
|
91
|
+
<span className="text-[9px] ml-auto" style={{ color: 'var(--c-text3)' }}>{sessions.length}</span>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div className="flex-1 overflow-y-auto scrollbar-thin py-1">
|
|
95
|
+
{grouped.sorted.map(([folder, list]) => {
|
|
96
|
+
const isCollapsed = collapsed.has(folder)
|
|
97
|
+
const folderName = folder.split('/').pop()
|
|
98
|
+
return (
|
|
99
|
+
<div key={folder}>
|
|
100
|
+
<div
|
|
101
|
+
className="flex items-center gap-1.5 px-2 py-1.5 cursor-pointer transition"
|
|
102
|
+
onMouseEnter={e => e.currentTarget.style.background = 'var(--c-bg3)'}
|
|
103
|
+
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
|
104
|
+
onClick={() => toggle(folder)}
|
|
105
|
+
>
|
|
106
|
+
{isCollapsed ? <ChevronRight size={10} style={{ color: 'var(--c-text3)' }} /> : <ChevronDown size={10} style={{ color: 'var(--c-text3)' }} />}
|
|
107
|
+
<FolderOpen size={10} style={{ color: '#818cf8' }} />
|
|
108
|
+
<span className="text-[10px] font-medium truncate flex-1" style={{ color: 'var(--c-text2)' }} title={folder}>{folderName}</span>
|
|
109
|
+
<span className="text-[8px]" style={{ color: 'var(--c-text3)' }}>{list.length}</span>
|
|
110
|
+
</div>
|
|
111
|
+
{!isCollapsed && (
|
|
112
|
+
<div className="pl-3">
|
|
113
|
+
{list.map(s => <SessionItem key={s.id} s={s} />)}
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
})}
|
|
119
|
+
|
|
120
|
+
{grouped.noProject.length > 0 && (
|
|
121
|
+
<div>
|
|
122
|
+
{grouped.sorted.length > 0 && (
|
|
123
|
+
<div className="px-2 py-1.5 text-[9px] uppercase tracking-wider" style={{ color: 'var(--c-text3)' }}>no project</div>
|
|
124
|
+
)}
|
|
125
|
+
<div className={grouped.sorted.length > 0 ? 'pl-1' : ''}>
|
|
126
|
+
{grouped.noProject.map(s => <SessionItem key={s.id} s={s} />)}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
13
134
|
|
|
14
135
|
export default function RelayUserDetail() {
|
|
15
136
|
const { username } = useParams()
|
|
16
137
|
const navigate = useNavigate()
|
|
17
138
|
const [sessions, setSessions] = useState(null)
|
|
18
139
|
const [selectedChat, setSelectedChat] = useState(null)
|
|
140
|
+
const { dark } = useTheme()
|
|
141
|
+
|
|
142
|
+
const legendColor = dark ? '#888' : '#555'
|
|
143
|
+
const gridColor = dark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.06)'
|
|
144
|
+
const txtDim = dark ? '#555' : '#999'
|
|
145
|
+
const txtColor = dark ? '#888' : '#555'
|
|
19
146
|
|
|
20
147
|
useEffect(() => {
|
|
21
148
|
if (username) {
|
|
@@ -35,6 +162,9 @@ export default function RelayUserDetail() {
|
|
|
35
162
|
const totalMessages = sessions.reduce((s, c) => s + (c.totalMessages || 0), 0)
|
|
36
163
|
const totalInputTokens = sessions.reduce((s, c) => s + (c.totalInputTokens || 0), 0)
|
|
37
164
|
const totalOutputTokens = sessions.reduce((s, c) => s + (c.totalOutputTokens || 0), 0)
|
|
165
|
+
const totalTokens = totalInputTokens + totalOutputTokens
|
|
166
|
+
const msgsPerSession = totalSessions > 0 ? (totalMessages / totalSessions).toFixed(1) : 0
|
|
167
|
+
const tokPerSession = totalSessions > 0 ? Math.round(totalTokens / totalSessions) : 0
|
|
38
168
|
|
|
39
169
|
// Editor breakdown
|
|
40
170
|
const editorMap = {}
|
|
@@ -47,12 +177,15 @@ export default function RelayUserDetail() {
|
|
|
47
177
|
const projectMap = {}
|
|
48
178
|
for (const s of sessions) {
|
|
49
179
|
if (s.folder) {
|
|
50
|
-
if (!projectMap[s.folder]) projectMap[s.folder] = { count: 0, lastActive: 0 }
|
|
180
|
+
if (!projectMap[s.folder]) projectMap[s.folder] = { count: 0, lastActive: 0, messages: 0, tokens: 0 }
|
|
51
181
|
projectMap[s.folder].count++
|
|
182
|
+
projectMap[s.folder].messages += s.totalMessages || 0
|
|
183
|
+
projectMap[s.folder].tokens += (s.totalInputTokens || 0) + (s.totalOutputTokens || 0)
|
|
52
184
|
if (s.lastUpdatedAt > projectMap[s.folder].lastActive) projectMap[s.folder].lastActive = s.lastUpdatedAt
|
|
53
185
|
}
|
|
54
186
|
}
|
|
55
187
|
const projects = Object.entries(projectMap).sort((a, b) => b[1].count - a[1].count)
|
|
188
|
+
const maxProjectSessions = projects.length > 0 ? Math.max(...projects.map(([, p]) => p.count)) : 1
|
|
56
189
|
|
|
57
190
|
// Model breakdown
|
|
58
191
|
const modelMap = {}
|
|
@@ -63,131 +196,262 @@ export default function RelayUserDetail() {
|
|
|
63
196
|
}
|
|
64
197
|
const models = Object.entries(modelMap).sort((a, b) => b[1] - a[1]).slice(0, 10)
|
|
65
198
|
|
|
66
|
-
|
|
199
|
+
// Tool breakdown
|
|
200
|
+
const toolMap = {}
|
|
201
|
+
for (const s of sessions) {
|
|
202
|
+
if (s.toolCalls) {
|
|
203
|
+
for (const t of s.toolCalls) toolMap[t] = (toolMap[t] || 0) + 1
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const tools = Object.entries(toolMap).sort((a, b) => b[1] - a[1]).slice(0, 15)
|
|
207
|
+
|
|
208
|
+
// Daily activity (sessions per day)
|
|
209
|
+
const dayMap = {}
|
|
210
|
+
for (const s of sessions) {
|
|
211
|
+
const d = new Date(s.lastUpdatedAt || s.createdAt).toISOString().slice(0, 10)
|
|
212
|
+
dayMap[d] = (dayMap[d] || 0) + 1
|
|
213
|
+
}
|
|
214
|
+
const days = Object.entries(dayMap).sort((a, b) => a[0].localeCompare(b[0])).slice(-14)
|
|
215
|
+
|
|
216
|
+
const handleFeedClick = (chatId) => {
|
|
67
217
|
setSelectedChat(chatId)
|
|
68
218
|
}
|
|
69
219
|
|
|
220
|
+
const sidebarH = 'calc(100vh - 42px)'
|
|
221
|
+
|
|
70
222
|
return (
|
|
71
|
-
<div className="fade-in flex
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
className="p-1.5 transition hover:bg-[var(--c-card)]"
|
|
79
|
-
style={{ border: '1px solid var(--c-border)', color: 'var(--c-text2)' }}
|
|
80
|
-
>
|
|
81
|
-
<ArrowLeft size={14} />
|
|
82
|
-
</button>
|
|
83
|
-
<div className="flex items-center gap-2">
|
|
84
|
-
<div className="w-8 h-8 flex items-center justify-center text-[13px] font-bold" style={{ background: 'rgba(99,102,241,0.15)', color: '#818cf8' }}>
|
|
85
|
-
{username.charAt(0).toUpperCase()}
|
|
86
|
-
</div>
|
|
87
|
-
<div>
|
|
88
|
-
<div className="text-[13px] font-bold" style={{ color: 'var(--c-white)' }}>{username}</div>
|
|
89
|
-
<div className="text-[9px]" style={{ color: 'var(--c-text3)' }}>Team Member</div>
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
223
|
+
<div className="fade-in flex" style={{ height: sidebarH }}>
|
|
224
|
+
{/* ── Left sidebar: Sessions tree ── */}
|
|
225
|
+
<div
|
|
226
|
+
className="hidden lg:flex flex-col w-[250px] shrink-0 sticky top-[42px] self-start"
|
|
227
|
+
style={{ height: sidebarH, borderRight: '1px solid var(--c-border)', background: 'var(--c-bg)' }}
|
|
228
|
+
>
|
|
229
|
+
<SessionSidebar sessions={sessions} projects={projects} selectedChat={selectedChat} onSelectChat={setSelectedChat} />
|
|
92
230
|
</div>
|
|
93
231
|
|
|
94
|
-
{/*
|
|
95
|
-
<div className="
|
|
96
|
-
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
232
|
+
{/* ── Center: scrollable content ── */}
|
|
233
|
+
<div className="flex-1 min-w-0 overflow-y-auto p-4 space-y-4">
|
|
234
|
+
{/* Header */}
|
|
235
|
+
<div className="flex items-center gap-3">
|
|
236
|
+
<button
|
|
237
|
+
onClick={() => navigate('/')}
|
|
238
|
+
className="p-1.5 transition hover:bg-[var(--c-card)] rounded-sm"
|
|
239
|
+
style={{ border: '1px solid var(--c-border)', color: 'var(--c-text2)' }}
|
|
240
|
+
>
|
|
241
|
+
<ArrowLeft size={14} />
|
|
242
|
+
</button>
|
|
243
|
+
<div className="flex items-center gap-2.5">
|
|
244
|
+
<div className="w-9 h-9 flex items-center justify-center text-[14px] font-bold rounded-sm" style={{ background: 'rgba(99,102,241,0.15)', color: '#818cf8' }}>
|
|
245
|
+
{username.charAt(0).toUpperCase()}
|
|
246
|
+
</div>
|
|
247
|
+
<div>
|
|
248
|
+
<div className="text-[14px] font-bold" style={{ color: 'var(--c-white)' }}>{username}</div>
|
|
249
|
+
<div className="flex items-center gap-2 mt-0.5">
|
|
250
|
+
{editors.map(([src]) => (
|
|
251
|
+
<EditorIcon key={src} source={src} size={11} />
|
|
252
|
+
))}
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
103
257
|
|
|
104
|
-
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
<
|
|
108
|
-
<
|
|
258
|
+
{/* KPIs */}
|
|
259
|
+
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))' }}>
|
|
260
|
+
<KpiCard label="sessions" value={totalSessions} />
|
|
261
|
+
<KpiCard label="messages" value={formatNumber(totalMessages)} sub={`${msgsPerSession}/session`} />
|
|
262
|
+
<KpiCard label="editors" value={editors.length} />
|
|
263
|
+
<KpiCard label="projects" value={projects.length} />
|
|
264
|
+
<KpiCard label="tokens" value={formatNumber(totalTokens)} sub={`${formatNumber(tokPerSession)}/session`} />
|
|
265
|
+
<KpiCard label="models" value={models.length} />
|
|
109
266
|
</div>
|
|
110
267
|
|
|
111
|
-
{/*
|
|
112
|
-
|
|
113
|
-
<
|
|
114
|
-
|
|
268
|
+
{/* Token overview */}
|
|
269
|
+
{totalTokens > 0 && (
|
|
270
|
+
<div className="card p-3">
|
|
271
|
+
<div className="flex items-center justify-between mb-2">
|
|
272
|
+
<SectionTitle>token usage</SectionTitle>
|
|
273
|
+
<span className="text-[10px] font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(totalTokens)} total</span>
|
|
274
|
+
</div>
|
|
275
|
+
<ProportionBar height={10} segments={[
|
|
276
|
+
{ label: 'Input', value: totalInputTokens, color: '#6366f1' },
|
|
277
|
+
{ label: 'Output', value: totalOutputTokens, color: '#a78bfa' },
|
|
278
|
+
]} />
|
|
279
|
+
<div className="flex items-center gap-4 mt-1.5 text-[9px]">
|
|
280
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#6366f1' }} /> input {formatNumber(totalInputTokens)}</span>
|
|
281
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#a78bfa' }} /> output {formatNumber(totalOutputTokens)}</span>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
|
|
286
|
+
{/* Charts: editors + models */}
|
|
287
|
+
<div className="grid grid-cols-1 xl:grid-cols-2 gap-3">
|
|
288
|
+
{editors.length > 0 && (
|
|
289
|
+
<div className="card p-3">
|
|
290
|
+
<SectionTitle>editors</SectionTitle>
|
|
291
|
+
<div style={{ height: 160 }}>
|
|
292
|
+
<Doughnut
|
|
293
|
+
data={{
|
|
294
|
+
labels: editors.map(([src]) => editorLabel(src)),
|
|
295
|
+
datasets: [{ data: editors.map(([, c]) => c), backgroundColor: editors.map(([src]) => editorColor(src)), borderWidth: 0 }],
|
|
296
|
+
}}
|
|
297
|
+
options={{
|
|
298
|
+
responsive: true, maintainAspectRatio: false, cutout: '60%',
|
|
299
|
+
plugins: {
|
|
300
|
+
legend: { position: 'right', labels: { color: legendColor, font: { size: 9, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
|
|
301
|
+
tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
|
|
302
|
+
},
|
|
303
|
+
}}
|
|
304
|
+
/>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
{models.length > 0 && (
|
|
309
|
+
<div className="card p-3">
|
|
310
|
+
<SectionTitle>models</SectionTitle>
|
|
311
|
+
<div style={{ height: 160 }}>
|
|
312
|
+
<Doughnut
|
|
313
|
+
data={{
|
|
314
|
+
labels: models.map(([name]) => name),
|
|
315
|
+
datasets: [{ data: models.map(([, c]) => c), backgroundColor: MODEL_COLORS, borderWidth: 0 }],
|
|
316
|
+
}}
|
|
317
|
+
options={{
|
|
318
|
+
responsive: true, maintainAspectRatio: false, cutout: '60%',
|
|
319
|
+
plugins: {
|
|
320
|
+
legend: { position: 'right', labels: { color: legendColor, font: { size: 8, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
|
|
321
|
+
tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
|
|
322
|
+
},
|
|
323
|
+
}}
|
|
324
|
+
/>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
115
328
|
</div>
|
|
116
|
-
</div>
|
|
117
329
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
330
|
+
{/* Daily activity bar */}
|
|
331
|
+
{days.length > 1 && (
|
|
332
|
+
<div className="card p-3">
|
|
333
|
+
<SectionTitle>daily activity (last 14 days)</SectionTitle>
|
|
334
|
+
<div style={{ height: 120 }}>
|
|
335
|
+
<Bar
|
|
336
|
+
data={{
|
|
337
|
+
labels: days.map(([d]) => d.slice(5)),
|
|
338
|
+
datasets: [{
|
|
339
|
+
data: days.map(([, c]) => c),
|
|
340
|
+
backgroundColor: '#6366f1',
|
|
341
|
+
borderRadius: 2,
|
|
342
|
+
}],
|
|
343
|
+
}}
|
|
344
|
+
options={{
|
|
345
|
+
responsive: true, maintainAspectRatio: false,
|
|
346
|
+
plugins: { legend: { display: false }, tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } } },
|
|
347
|
+
scales: {
|
|
348
|
+
x: { grid: { display: false }, ticks: { color: txtDim, font: { size: 8, family: MONO } } },
|
|
349
|
+
y: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 8, family: MONO }, stepSize: 1 }, beginAtZero: true },
|
|
350
|
+
},
|
|
351
|
+
}}
|
|
352
|
+
/>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
{/* Projects */}
|
|
358
|
+
{projects.length > 0 && (
|
|
359
|
+
<div>
|
|
360
|
+
<SectionTitle>projects</SectionTitle>
|
|
361
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-2 mt-2">
|
|
362
|
+
{projects.map(([folder, info]) => (
|
|
363
|
+
<div key={folder} className="card px-3 py-2.5">
|
|
364
|
+
<div className="flex items-center gap-1.5 mb-1.5">
|
|
365
|
+
<FolderOpen size={11} style={{ color: '#818cf8' }} />
|
|
366
|
+
<span className="text-[11px] font-medium truncate" style={{ color: 'var(--c-white)' }}>{folder.split('/').pop()}</span>
|
|
367
|
+
</div>
|
|
368
|
+
<div className="text-[8px] truncate mb-2" style={{ color: 'var(--c-text3)' }}>{folder}</div>
|
|
369
|
+
<div className="grid grid-cols-3 gap-1 text-center mb-1.5">
|
|
370
|
+
<div className="p-1 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
371
|
+
<div className="text-[10px] font-bold" style={{ color: 'var(--c-white)' }}>{info.count}</div>
|
|
372
|
+
<div className="text-[7px]" style={{ color: 'var(--c-text3)' }}>sessions</div>
|
|
373
|
+
</div>
|
|
374
|
+
<div className="p-1 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
375
|
+
<div className="text-[10px] font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(info.messages)}</div>
|
|
376
|
+
<div className="text-[7px]" style={{ color: 'var(--c-text3)' }}>messages</div>
|
|
377
|
+
</div>
|
|
378
|
+
<div className="p-1 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
379
|
+
<div className="text-[10px] font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(info.tokens)}</div>
|
|
380
|
+
<div className="text-[7px]" style={{ color: 'var(--c-text3)' }}>tokens</div>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
<div className="h-1.5 rounded-full overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
|
|
384
|
+
<div className="h-full rounded-full" style={{ width: `${(info.count / maxProjectSessions * 100).toFixed(0)}%`, background: '#6366f1' }} />
|
|
385
|
+
</div>
|
|
386
|
+
<div className="text-[8px] mt-1" style={{ color: 'var(--c-text3)' }}>{formatDate(info.lastActive)}</div>
|
|
133
387
|
</div>
|
|
134
|
-
|
|
135
|
-
|
|
388
|
+
))}
|
|
389
|
+
</div>
|
|
136
390
|
</div>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
className="card px-3 py-2 cursor-pointer hover:border-[var(--c-card-hover)] transition"
|
|
148
|
-
onClick={() => setSelectedChat(s.id)}
|
|
149
|
-
>
|
|
150
|
-
<div className="flex items-center gap-2">
|
|
151
|
-
<EditorDot source={s.source} size={6} />
|
|
152
|
-
<span className="text-[10px] font-medium truncate flex-1" style={{ color: 'var(--c-white)' }}>
|
|
153
|
-
{s.name || 'Untitled'}
|
|
391
|
+
)}
|
|
392
|
+
|
|
393
|
+
{/* Top tools */}
|
|
394
|
+
{tools.length > 0 && (
|
|
395
|
+
<div className="card p-3">
|
|
396
|
+
<SectionTitle>top tools</SectionTitle>
|
|
397
|
+
<div className="flex flex-wrap gap-1.5 mt-1">
|
|
398
|
+
{tools.map(([name, count]) => (
|
|
399
|
+
<span key={name} className="text-[9px] px-2 py-1 rounded-sm" style={{ background: 'var(--c-code-bg)', color: 'var(--c-text2)' }}>
|
|
400
|
+
{name} <span style={{ color: 'var(--c-text3)' }}>×{count}</span>
|
|
154
401
|
</span>
|
|
155
|
-
|
|
156
|
-
<span className="text-[8px] px-1.5 py-0.5" style={{ background: 'rgba(168,85,247,0.1)', color: '#a855f7' }}>{s.mode}</span>
|
|
157
|
-
)}
|
|
158
|
-
<span className="text-[9px]" style={{ color: 'var(--c-text3)' }}>{formatDate(s.lastUpdatedAt)}</span>
|
|
159
|
-
</div>
|
|
160
|
-
<div className="flex items-center gap-3 mt-1">
|
|
161
|
-
{s.totalMessages > 0 && (
|
|
162
|
-
<span className="flex items-center gap-1 text-[9px]" style={{ color: 'var(--c-text3)' }}>
|
|
163
|
-
<MessageSquare size={8} /> {s.totalMessages}
|
|
164
|
-
</span>
|
|
165
|
-
)}
|
|
166
|
-
{s.folder && (
|
|
167
|
-
<span className="flex items-center gap-1 text-[9px] truncate" style={{ color: 'var(--c-text3)' }}>
|
|
168
|
-
<FolderOpen size={8} /> {s.folder.split('/').pop()}
|
|
169
|
-
</span>
|
|
170
|
-
)}
|
|
171
|
-
{s.models && s.models.length > 0 && (
|
|
172
|
-
<span className="text-[8px] px-1" style={{ color: '#818cf8' }}>{[...new Set(s.models)][0]}</span>
|
|
173
|
-
)}
|
|
174
|
-
</div>
|
|
402
|
+
))}
|
|
175
403
|
</div>
|
|
176
|
-
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
|
|
407
|
+
{/* Recent sessions table */}
|
|
408
|
+
<div className="card p-3">
|
|
409
|
+
<SectionTitle>recent sessions</SectionTitle>
|
|
410
|
+
<div className="max-h-[400px] overflow-y-auto scrollbar-thin">
|
|
411
|
+
<table className="w-full text-[11px]">
|
|
412
|
+
<tbody>
|
|
413
|
+
{sessions.slice(0, 50).map(s => (
|
|
414
|
+
<tr
|
|
415
|
+
key={s.id}
|
|
416
|
+
className="cursor-pointer transition"
|
|
417
|
+
style={{ borderBottom: '1px solid var(--c-border)' }}
|
|
418
|
+
onMouseEnter={e => e.currentTarget.style.background = 'var(--c-bg3)'}
|
|
419
|
+
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
|
420
|
+
onClick={() => setSelectedChat(s.id)}
|
|
421
|
+
>
|
|
422
|
+
<td className="py-2 px-2 w-[24px]"><EditorIcon source={s.source} size={11} /></td>
|
|
423
|
+
<td className="py-2 px-2">
|
|
424
|
+
<div className="text-[10px] font-medium truncate" style={{ color: 'var(--c-white)' }}>{s.name || 'Untitled'}</div>
|
|
425
|
+
<div className="text-[9px] truncate" style={{ color: 'var(--c-text3)' }}>{s.folder ? s.folder.split('/').pop() : ''}</div>
|
|
426
|
+
</td>
|
|
427
|
+
<td className="py-2 px-2 text-[9px] whitespace-nowrap" style={{ color: 'var(--c-text3)' }}>
|
|
428
|
+
{s.totalMessages > 0 && <span>{s.totalMessages}m</span>}
|
|
429
|
+
</td>
|
|
430
|
+
<td className="py-2 px-2">
|
|
431
|
+
{s.mode && (
|
|
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
|
+
)}
|
|
434
|
+
</td>
|
|
435
|
+
<td className="py-2 px-2 text-[9px] whitespace-nowrap text-right" style={{ color: 'var(--c-text3)' }}>
|
|
436
|
+
{formatDate(s.lastUpdatedAt)}
|
|
437
|
+
</td>
|
|
438
|
+
</tr>
|
|
439
|
+
))}
|
|
440
|
+
</tbody>
|
|
441
|
+
</table>
|
|
442
|
+
</div>
|
|
177
443
|
</div>
|
|
178
444
|
</div>
|
|
179
445
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
{/* ── Live Feed (right column) ── */}
|
|
446
|
+
{/* ── Right sidebar: Live Feed ── */}
|
|
183
447
|
<div
|
|
184
|
-
className="hidden xl:
|
|
185
|
-
style={{ height: '
|
|
448
|
+
className="hidden xl:flex flex-col w-[300px] shrink-0 sticky top-[42px] self-start"
|
|
449
|
+
style={{ height: sidebarH, borderLeft: '1px solid var(--c-border)', background: 'var(--c-bg)' }}
|
|
186
450
|
>
|
|
187
451
|
<LiveFeed onSessionClick={handleFeedClick} />
|
|
188
452
|
</div>
|
|
189
453
|
|
|
190
|
-
{/* Session sidebar
|
|
454
|
+
{/* Session sidebar */}
|
|
191
455
|
<ChatSidebar
|
|
192
456
|
chatId={selectedChat}
|
|
193
457
|
onClose={() => setSelectedChat(null)}
|