agentlytics 0.1.1 → 0.1.2
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/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/Sessions.jsx +161 -43
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { useState, useEffect, useMemo } from 'react'
|
|
2
2
|
import { useSearchParams, useNavigate } from 'react-router-dom'
|
|
3
|
-
import { ArrowLeft, Search } from 'lucide-react'
|
|
3
|
+
import { ArrowLeft, Search, FolderOpen, Calendar, MessageSquare, Wrench, Cpu, Zap, AlertTriangle } from 'lucide-react'
|
|
4
4
|
import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement } from 'chart.js'
|
|
5
5
|
import { Doughnut, Bar } from 'react-chartjs-2'
|
|
6
6
|
import { fetchProjects, fetchChats } from '../lib/api'
|
|
7
7
|
import { editorColor, editorLabel, formatNumber, formatDate } from '../lib/constants'
|
|
8
8
|
import { useTheme } from '../lib/theme'
|
|
9
9
|
import KpiCard from '../components/KpiCard'
|
|
10
|
-
import
|
|
10
|
+
import EditorIcon from '../components/EditorIcon'
|
|
11
|
+
import SectionTitle from '../components/SectionTitle'
|
|
11
12
|
import ChatSidebar from '../components/ChatSidebar'
|
|
12
13
|
|
|
13
14
|
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
|
|
14
15
|
|
|
15
16
|
const MONO = 'JetBrains Mono, monospace'
|
|
16
17
|
const MODEL_COLORS = ['#6366f1', '#a78bfa', '#818cf8', '#c084fc', '#e879f9', '#f472b6', '#fb7185', '#f87171', '#fbbf24', '#34d399']
|
|
18
|
+
const TOOL_COLORS = ['#10b981', '#34d399', '#6ee7b7', '#a7f3d0', '#d1fae5', '#ecfdf5', '#b8f0d8', '#7ce0b8', '#4ade80', '#22c55e']
|
|
17
19
|
|
|
18
20
|
export default function ProjectDetail() {
|
|
19
21
|
const [searchParams] = useSearchParams()
|
|
@@ -29,6 +31,7 @@ export default function ProjectDetail() {
|
|
|
29
31
|
const [loading, setLoading] = useState(true)
|
|
30
32
|
const [chatSearch, setChatSearch] = useState('')
|
|
31
33
|
const [selectedChatId, setSelectedChatId] = useState(null)
|
|
34
|
+
const [enabledEditors, setEnabledEditors] = useState(null)
|
|
32
35
|
|
|
33
36
|
useEffect(() => {
|
|
34
37
|
if (!folder) return
|
|
@@ -40,19 +43,34 @@ export default function ProjectDetail() {
|
|
|
40
43
|
const match = projects.find(p => p.folder === folder)
|
|
41
44
|
setProject(match || null)
|
|
42
45
|
setChats(chatData.chats || [])
|
|
46
|
+
if (match) setEnabledEditors(new Set(Object.keys(match.editors)))
|
|
43
47
|
setLoading(false)
|
|
44
48
|
})
|
|
45
49
|
}, [folder])
|
|
46
50
|
|
|
51
|
+
const editorFilteredChats = useMemo(() => {
|
|
52
|
+
if (!enabledEditors) return chats
|
|
53
|
+
return chats.filter(c => enabledEditors.has(c.source))
|
|
54
|
+
}, [chats, enabledEditors])
|
|
55
|
+
|
|
47
56
|
const filteredChats = useMemo(() => {
|
|
48
|
-
if (!chatSearch) return
|
|
57
|
+
if (!chatSearch) return editorFilteredChats
|
|
49
58
|
const q = chatSearch.toLowerCase()
|
|
50
|
-
return
|
|
59
|
+
return editorFilteredChats.filter(c =>
|
|
51
60
|
(c.name && c.name.toLowerCase().includes(q)) ||
|
|
52
61
|
(c.topModel && c.topModel.toLowerCase().includes(q)) ||
|
|
53
62
|
(c.source && c.source.toLowerCase().includes(q))
|
|
54
63
|
)
|
|
55
|
-
}, [
|
|
64
|
+
}, [editorFilteredChats, chatSearch])
|
|
65
|
+
|
|
66
|
+
const toggleEditor = (editorId) => {
|
|
67
|
+
setEnabledEditors(prev => {
|
|
68
|
+
const next = new Set(prev)
|
|
69
|
+
if (next.has(editorId)) next.delete(editorId)
|
|
70
|
+
else next.add(editorId)
|
|
71
|
+
return next
|
|
72
|
+
})
|
|
73
|
+
}
|
|
56
74
|
|
|
57
75
|
if (!folder) return <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text3)' }}>no project specified</div>
|
|
58
76
|
if (loading) return <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text2)' }}>loading project...</div>
|
|
@@ -60,65 +78,120 @@ export default function ProjectDetail() {
|
|
|
60
78
|
|
|
61
79
|
const editorEntries = Object.entries(project.editors).sort((a, b) => b[1] - a[1])
|
|
62
80
|
const maxEditorCount = editorEntries.length > 0 ? editorEntries[0][1] : 1
|
|
81
|
+
const allEnabled = !enabledEditors || enabledEditors.size === editorEntries.length
|
|
82
|
+
|
|
83
|
+
// Derive stats from editor-filtered chats
|
|
84
|
+
const fSessionCount = editorFilteredChats.length
|
|
85
|
+
const fEditorCounts = {}
|
|
86
|
+
const fModelCounts = {}
|
|
87
|
+
for (const c of editorFilteredChats) {
|
|
88
|
+
fEditorCounts[c.source] = (fEditorCounts[c.source] || 0) + 1
|
|
89
|
+
if (c.topModel) fModelCounts[c.topModel] = (fModelCounts[c.topModel] || 0) + 1
|
|
90
|
+
}
|
|
91
|
+
const fTopModels = Object.entries(fModelCounts).sort((a, b) => b[1] - a[1]).slice(0, 10)
|
|
92
|
+
const fMaxEditorCount = Math.max(...Object.values(fEditorCounts), 1)
|
|
93
|
+
|
|
94
|
+
// Use project-level stats when all editors enabled, otherwise show filtered session count
|
|
95
|
+
const totalTok = project.totalInputTokens + project.totalOutputTokens
|
|
96
|
+
const outputRatio = project.totalInputTokens > 0 ? (project.totalOutputTokens / project.totalInputTokens).toFixed(1) : '0'
|
|
97
|
+
const displaySessions = allEnabled ? project.totalSessions : fSessionCount
|
|
98
|
+
const avgMsgs = allEnabled && project.totalSessions > 0 ? (project.totalMessages / project.totalSessions).toFixed(1) : 0
|
|
63
99
|
|
|
64
100
|
return (
|
|
65
|
-
<div className="fade-in space-y-
|
|
66
|
-
{/*
|
|
67
|
-
<div className="
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
<div className="
|
|
101
|
+
<div className="fade-in space-y-4">
|
|
102
|
+
{/* Header card */}
|
|
103
|
+
<div className="card p-4">
|
|
104
|
+
<div className="flex items-start gap-3">
|
|
105
|
+
<button
|
|
106
|
+
onClick={() => navigate('/projects')}
|
|
107
|
+
className="flex items-center gap-1 text-[11px] transition mt-0.5 flex-shrink-0"
|
|
108
|
+
style={{ color: 'var(--c-text3)' }}
|
|
109
|
+
>
|
|
110
|
+
<ArrowLeft size={12} />
|
|
111
|
+
</button>
|
|
112
|
+
<FolderOpen size={18} className="flex-shrink-0 mt-0.5" style={{ color: 'var(--c-accent)' }} />
|
|
113
|
+
<div className="flex-1 min-w-0">
|
|
114
|
+
<h1 className="text-sm font-bold truncate" style={{ color: 'var(--c-white)' }}>{project.name}</h1>
|
|
115
|
+
<div className="text-[10px] truncate" style={{ color: 'var(--c-text3)' }}>{project.folder}</div>
|
|
116
|
+
</div>
|
|
117
|
+
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
118
|
+
{editorEntries.map(([e]) => (
|
|
119
|
+
<EditorIcon key={e} source={e} size={14} />
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div className="flex items-center gap-4 mt-3 pt-3 text-[10px]" style={{ borderTop: '1px solid var(--c-border)' }}>
|
|
124
|
+
<div className="flex items-center gap-1" style={{ color: 'var(--c-text3)' }}>
|
|
125
|
+
<Calendar size={9} />
|
|
126
|
+
<span>{formatDate(project.firstSeen)}</span>
|
|
127
|
+
<span>→</span>
|
|
128
|
+
<span>{formatDate(project.lastSeen)}</span>
|
|
129
|
+
</div>
|
|
130
|
+
<div className="flex items-center gap-1.5 ml-auto">
|
|
131
|
+
{editorEntries.map(([e, c]) => (
|
|
132
|
+
<span key={e} className="inline-flex items-center gap-0.5">
|
|
133
|
+
<span className="w-1.5 h-1.5 rounded-full" style={{ background: editorColor(e) }} />
|
|
134
|
+
<span style={{ color: 'var(--c-text3)' }}>{editorLabel(e)}</span>
|
|
135
|
+
<span className="font-bold" style={{ color: 'var(--c-text2)' }}>{c}</span>
|
|
136
|
+
</span>
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
78
139
|
</div>
|
|
79
140
|
</div>
|
|
80
141
|
|
|
81
142
|
{/* KPIs */}
|
|
82
|
-
<div className="grid
|
|
83
|
-
<KpiCard label="sessions" value={
|
|
84
|
-
<KpiCard label="messages" value={formatNumber(project.totalMessages)} />
|
|
85
|
-
<KpiCard label="tool calls" value={formatNumber(project.totalToolCalls)} />
|
|
86
|
-
<KpiCard label="
|
|
87
|
-
|
|
88
|
-
|
|
143
|
+
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))' }}>
|
|
144
|
+
<KpiCard label="sessions" value={displaySessions} sub={!allEnabled ? 'filtered' : ''} />
|
|
145
|
+
<KpiCard label="messages" value={formatNumber(project.totalMessages)} sub={`${avgMsgs} avg/session`} />
|
|
146
|
+
<KpiCard label="tool calls" value={formatNumber(project.totalToolCalls)} sub={<span className="flex items-center gap-0.5"><Wrench size={8} /> invocations</span>} />
|
|
147
|
+
<KpiCard label="tokens" value={formatNumber(totalTok)} sub={`${outputRatio}× out/in`} />
|
|
148
|
+
{project.totalCacheRead > 0 && (
|
|
149
|
+
<KpiCard label="cache read" value={formatNumber(project.totalCacheRead)} sub={`write: ${formatNumber(project.totalCacheWrite)}`} />
|
|
150
|
+
)}
|
|
151
|
+
<KpiCard label="you wrote" value={formatNumber(project.totalUserChars)} sub={`AI: ${formatNumber(project.totalAssistantChars)}`} />
|
|
89
152
|
</div>
|
|
90
153
|
|
|
91
154
|
{/* Charts row */}
|
|
92
|
-
<div className="grid grid-cols-1 lg:grid-cols-3 gap-
|
|
93
|
-
{/* Editors */}
|
|
94
|
-
<div className="card p-
|
|
95
|
-
<
|
|
96
|
-
<div className="space-y-
|
|
97
|
-
{editorEntries.map(([e, c]) =>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
<div className="
|
|
102
|
-
<
|
|
155
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
|
|
156
|
+
{/* Editors bar with checkboxes */}
|
|
157
|
+
<div className="card p-3">
|
|
158
|
+
<SectionTitle>editors</SectionTitle>
|
|
159
|
+
<div className="space-y-1.5 mt-1">
|
|
160
|
+
{editorEntries.map(([e, c]) => {
|
|
161
|
+
const checked = enabledEditors ? enabledEditors.has(e) : true
|
|
162
|
+
const count = fEditorCounts[e] || 0
|
|
163
|
+
return (
|
|
164
|
+
<div key={e} className="flex items-center gap-2">
|
|
165
|
+
<input
|
|
166
|
+
type="checkbox"
|
|
167
|
+
checked={checked}
|
|
168
|
+
onChange={() => toggleEditor(e)}
|
|
169
|
+
className="accent-[var(--c-accent)] w-3 h-3 flex-shrink-0 cursor-pointer"
|
|
170
|
+
/>
|
|
171
|
+
<EditorIcon source={e} size={11} />
|
|
172
|
+
<span className="text-[10px] truncate flex-1 cursor-pointer select-none" onClick={() => toggleEditor(e)} style={{ color: checked ? 'var(--c-text2)' : 'var(--c-text3)', opacity: checked ? 1 : 0.4 }}>{editorLabel(e)}</span>
|
|
173
|
+
<div className="w-16 h-3 rounded-sm overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
|
|
174
|
+
<div className="h-full rounded-sm transition-all" style={{ width: `${(count / fMaxEditorCount * 100).toFixed(0)}%`, background: checked ? editorColor(e) : 'var(--c-text3)', opacity: checked ? 1 : 0.2 }} />
|
|
175
|
+
</div>
|
|
176
|
+
<span className="text-[10px] w-6 text-right font-bold" style={{ color: checked ? 'var(--c-white)' : 'var(--c-text3)', opacity: checked ? 1 : 0.4 }}>{count}</span>
|
|
103
177
|
</div>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
))}
|
|
178
|
+
)
|
|
179
|
+
})}
|
|
107
180
|
</div>
|
|
108
181
|
</div>
|
|
109
182
|
|
|
110
|
-
{/* Models
|
|
111
|
-
<div className="card p-
|
|
112
|
-
<
|
|
113
|
-
{
|
|
114
|
-
<div style={{ height:
|
|
183
|
+
{/* Models doughnut */}
|
|
184
|
+
<div className="card p-3">
|
|
185
|
+
<SectionTitle>models</SectionTitle>
|
|
186
|
+
{fTopModels.length > 0 ? (
|
|
187
|
+
<div style={{ height: 160 }}>
|
|
115
188
|
<Doughnut
|
|
116
189
|
data={{
|
|
117
|
-
labels:
|
|
118
|
-
datasets: [{ data:
|
|
190
|
+
labels: fTopModels.map(m => m[0]),
|
|
191
|
+
datasets: [{ data: fTopModels.map(m => m[1]), backgroundColor: MODEL_COLORS, borderWidth: 0 }],
|
|
119
192
|
}}
|
|
120
193
|
options={{
|
|
121
|
-
responsive: true, maintainAspectRatio: false, cutout: '
|
|
194
|
+
responsive: true, maintainAspectRatio: false, cutout: '60%',
|
|
122
195
|
plugins: {
|
|
123
196
|
legend: { position: 'right', labels: { color: txtColor, font: { size: 8, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
|
|
124
197
|
tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
|
|
@@ -129,17 +202,17 @@ export default function ProjectDetail() {
|
|
|
129
202
|
) : <div className="text-[10px] py-8 text-center" style={{ color: 'var(--c-text3)' }}>no model data</div>}
|
|
130
203
|
</div>
|
|
131
204
|
|
|
132
|
-
{/* Top tools */}
|
|
133
|
-
<div className="card p-
|
|
134
|
-
<
|
|
205
|
+
{/* Top tools horizontal bar */}
|
|
206
|
+
<div className="card p-3">
|
|
207
|
+
<SectionTitle>top tools <span style={{ color: 'var(--c-text3)' }}>({formatNumber(project.totalToolCalls)})</span></SectionTitle>
|
|
135
208
|
{project.topTools.length > 0 ? (
|
|
136
|
-
<div style={{ height:
|
|
209
|
+
<div style={{ height: 160 }}>
|
|
137
210
|
<Bar
|
|
138
211
|
data={{
|
|
139
212
|
labels: project.topTools.map(t => t.name),
|
|
140
213
|
datasets: [{
|
|
141
214
|
data: project.topTools.map(t => t.count),
|
|
142
|
-
backgroundColor:
|
|
215
|
+
backgroundColor: TOOL_COLORS.slice(0, project.topTools.length),
|
|
143
216
|
borderRadius: 2,
|
|
144
217
|
}],
|
|
145
218
|
}}
|
|
@@ -157,18 +230,10 @@ export default function ProjectDetail() {
|
|
|
157
230
|
</div>
|
|
158
231
|
</div>
|
|
159
232
|
|
|
160
|
-
{/*
|
|
161
|
-
{(project.totalCacheRead > 0 || project.totalCacheWrite > 0) && (
|
|
162
|
-
<div className="flex gap-4 text-[10px]" style={{ color: 'var(--c-text3)' }}>
|
|
163
|
-
<span>cache read: {formatNumber(project.totalCacheRead)}</span>
|
|
164
|
-
<span>cache write: {formatNumber(project.totalCacheWrite)}</span>
|
|
165
|
-
</div>
|
|
166
|
-
)}
|
|
167
|
-
|
|
168
|
-
{/* Sessions list */}
|
|
233
|
+
{/* Sessions */}
|
|
169
234
|
<div>
|
|
170
|
-
<div className="flex items-center gap-3 mb-
|
|
171
|
-
<
|
|
235
|
+
<div className="flex items-center gap-3 mb-2">
|
|
236
|
+
<SectionTitle>sessions <span style={{ color: 'var(--c-text3)' }}>({filteredChats.length}{!allEnabled ? ` of ${chats.length}` : ''})</span></SectionTitle>
|
|
172
237
|
<div className="relative max-w-xs flex-1">
|
|
173
238
|
<Search size={11} className="absolute left-2.5 top-1/2 -translate-y-1/2" style={{ color: 'var(--c-text3)' }} />
|
|
174
239
|
<input
|
|
@@ -176,21 +241,22 @@ export default function ProjectDetail() {
|
|
|
176
241
|
placeholder="filter sessions..."
|
|
177
242
|
value={chatSearch}
|
|
178
243
|
onChange={e => setChatSearch(e.target.value)}
|
|
179
|
-
className="w-full pl-7 pr-3 py-1 text-[11px] outline-none"
|
|
244
|
+
className="w-full pl-7 pr-3 py-1 text-[11px] outline-none rounded-sm"
|
|
180
245
|
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
181
246
|
/>
|
|
182
247
|
</div>
|
|
183
248
|
</div>
|
|
184
249
|
|
|
185
250
|
<div className="card overflow-hidden">
|
|
186
|
-
<table className="w-full text-
|
|
251
|
+
<table className="w-full text-[11px]">
|
|
187
252
|
<thead>
|
|
188
|
-
<tr className="text-[
|
|
189
|
-
<th className="text-left py-2
|
|
190
|
-
<th className="text-left py-2
|
|
191
|
-
<th className="text-left py-2
|
|
192
|
-
<th className="text-left py-2
|
|
193
|
-
<th className="text-left py-2
|
|
253
|
+
<tr className="text-[9px] uppercase tracking-wider" style={{ borderBottom: '1px solid var(--c-border)', color: 'var(--c-text3)' }}>
|
|
254
|
+
<th className="text-left py-2 px-3 font-medium">editor</th>
|
|
255
|
+
<th className="text-left py-2 px-3 font-medium">name</th>
|
|
256
|
+
<th className="text-left py-2 px-3 font-medium">mode</th>
|
|
257
|
+
<th className="text-left py-2 px-3 font-medium">model</th>
|
|
258
|
+
<th className="text-left py-2 px-3 font-medium">context</th>
|
|
259
|
+
<th className="text-left py-2 px-3 font-medium">updated</th>
|
|
194
260
|
</tr>
|
|
195
261
|
</thead>
|
|
196
262
|
<tbody>
|
|
@@ -203,22 +269,32 @@ export default function ProjectDetail() {
|
|
|
203
269
|
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
|
204
270
|
onClick={() => setSelectedChatId(c.id)}
|
|
205
271
|
>
|
|
206
|
-
<td className="py-2
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
{c.encrypted && <span className="ml-2 text-[10px] text-yellow-500/60">locked</span>}
|
|
212
|
-
</td>
|
|
213
|
-
<td className="py-2.5 px-4">
|
|
214
|
-
<span className="text-xs" style={{ color: 'var(--c-text2)' }}>{c.mode}</span>
|
|
272
|
+
<td className="py-2 px-3">
|
|
273
|
+
<span className="inline-flex items-center gap-1.5">
|
|
274
|
+
<EditorIcon source={c.source} size={12} />
|
|
275
|
+
<span style={{ color: 'var(--c-text2)' }}>{editorLabel(c.source)}</span>
|
|
276
|
+
</span>
|
|
215
277
|
</td>
|
|
216
|
-
<td className="py-2
|
|
217
|
-
{c.
|
|
278
|
+
<td className="py-2 px-3 font-medium truncate max-w-[280px]" style={{ color: 'var(--c-white)' }}>
|
|
279
|
+
{c.name || <span style={{ color: 'var(--c-text3)' }}>Untitled</span>}
|
|
280
|
+
{c.encrypted && <span className="ml-1.5 text-[9px] text-yellow-500/60">locked</span>}
|
|
218
281
|
</td>
|
|
219
|
-
<td className="py-2
|
|
220
|
-
|
|
282
|
+
<td className="py-2 px-3" style={{ color: 'var(--c-text2)' }}>{c.mode || ''}</td>
|
|
283
|
+
<td className="py-2 px-3 font-mono truncate max-w-[150px]" style={{ color: 'var(--c-text2)' }} title={c.topModel || ''}>{c.topModel || ''}</td>
|
|
284
|
+
<td className="py-2 px-3">
|
|
285
|
+
{c.bubbleCount >= 500 ? (
|
|
286
|
+
<span className="inline-flex items-center gap-0.5 font-bold" style={{ color: '#ef4444' }}>
|
|
287
|
+
<AlertTriangle size={9} />{c.bubbleCount} msgs
|
|
288
|
+
</span>
|
|
289
|
+
) : c.bubbleCount >= 100 ? (
|
|
290
|
+
<span className="inline-flex items-center gap-0.5 font-bold" style={{ color: '#f59e0b' }}>
|
|
291
|
+
<AlertTriangle size={9} />{c.bubbleCount} msgs
|
|
292
|
+
</span>
|
|
293
|
+
) : (
|
|
294
|
+
<span style={{ color: 'var(--c-text3)' }}>{c.bubbleCount || 0} msgs</span>
|
|
295
|
+
)}
|
|
221
296
|
</td>
|
|
297
|
+
<td className="py-2 px-3 whitespace-nowrap" style={{ color: 'var(--c-text3)' }}>{formatDate(c.lastUpdatedAt || c.createdAt)}</td>
|
|
222
298
|
</tr>
|
|
223
299
|
))}
|
|
224
300
|
</tbody>
|