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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlytics",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Comprehensive analytics dashboard for AI coding agents — Cursor, Windsurf, Claude Code, VS Code Copilot, Zed, Antigravity, OpenCode, Command Code",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/ui/src/App.jsx
CHANGED
|
@@ -46,16 +46,16 @@ export default function App() {
|
|
|
46
46
|
}, [mode, authed])
|
|
47
47
|
|
|
48
48
|
const refreshOverview = useCallback(() => {
|
|
49
|
-
fetchOverview().then(setOverview)
|
|
49
|
+
fetchOverview().then(setOverview).catch(() => {})
|
|
50
50
|
}, [])
|
|
51
51
|
|
|
52
52
|
useEffect(() => {
|
|
53
|
-
refreshOverview()
|
|
54
|
-
}, [])
|
|
53
|
+
if (mode === 'local') refreshOverview()
|
|
54
|
+
}, [mode])
|
|
55
55
|
|
|
56
56
|
// Live mode: refetch overview every 60s
|
|
57
57
|
useEffect(() => {
|
|
58
|
-
if (live) {
|
|
58
|
+
if (live && mode === 'local') {
|
|
59
59
|
liveRef.current = setInterval(() => {
|
|
60
60
|
refreshOverview()
|
|
61
61
|
}, 60000)
|
|
@@ -182,7 +182,7 @@ export default function App() {
|
|
|
182
182
|
</div>
|
|
183
183
|
)}
|
|
184
184
|
|
|
185
|
-
<main className=
|
|
185
|
+
<main className={isRelay ? 'px-0' : 'p-4 max-w-[1400px] mx-auto'}>
|
|
186
186
|
{mode === null ? (
|
|
187
187
|
<div className="text-sm py-12 text-center" style={{ color: 'var(--c-text2)' }}>loading...</div>
|
|
188
188
|
) : isRelay ? (
|
|
@@ -1,19 +1,53 @@
|
|
|
1
|
-
import { useState, useEffect, useRef } from 'react'
|
|
1
|
+
import { useState, useEffect, useRef, useMemo } from 'react'
|
|
2
2
|
import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement } from 'chart.js'
|
|
3
3
|
import { Doughnut, Bar } from 'react-chartjs-2'
|
|
4
|
-
import { Loader2, X } from 'lucide-react'
|
|
4
|
+
import { Loader2, X, ArrowRight, Zap, MessageSquare, Wrench, Cpu, TrendingUp, BarChart3 } from 'lucide-react'
|
|
5
5
|
import { fetchDeepAnalytics, fetchToolCalls } from '../lib/api'
|
|
6
6
|
import { editorLabel, editorColor, formatNumber, formatDateTime, dateRangeToApiParams } from '../lib/constants'
|
|
7
7
|
import { useTheme } from '../lib/theme'
|
|
8
8
|
import KpiCard from '../components/KpiCard'
|
|
9
|
+
import EditorIcon from '../components/EditorIcon'
|
|
10
|
+
import SectionTitle from '../components/SectionTitle'
|
|
9
11
|
import DateRangePicker from '../components/DateRangePicker'
|
|
10
12
|
|
|
11
13
|
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
|
|
12
14
|
|
|
13
15
|
const MODEL_COLORS = ['#6366f1', '#a78bfa', '#818cf8', '#c084fc', '#e879f9', '#f472b6', '#fb7185', '#f87171', '#fbbf24', '#34d399']
|
|
16
|
+
const TOOL_COLORS = ['#10b981', '#34d399', '#6ee7b7', '#a7f3d0', '#d1fae5', '#ecfdf5', '#b8f0d8', '#7ce0b8', '#4ade80', '#22c55e']
|
|
14
17
|
const MONO = 'JetBrains Mono, monospace'
|
|
15
18
|
|
|
16
|
-
//
|
|
19
|
+
// Categorize tools into groups
|
|
20
|
+
const TOOL_CATEGORIES = {
|
|
21
|
+
'File Operations': ['read_file', 'write_to_file', 'edit', 'multi_edit', 'Read', 'Write', 'EditFile', 'edit_file', 'create_file', 'read_notebook', 'edit_notebook'],
|
|
22
|
+
'Search': ['grep_search', 'find_by_name', 'code_search', 'search', 'list_dir', 'Grep', 'Find', 'SearchFiles', 'ListDir'],
|
|
23
|
+
'Terminal': ['run_command', 'command_status', 'RunCommand', 'execute_command', 'Bash', 'bash'],
|
|
24
|
+
'Browser': ['browser_preview', 'read_url_content', 'view_content_chunk'],
|
|
25
|
+
'AI Tools': ['mcp0_', 'mcp1_', 'mcp5_', 'mcp6_', 'skill', 'trajectory_search'],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function categorizeTools(tools) {
|
|
29
|
+
const cats = {}
|
|
30
|
+
for (const t of tools) {
|
|
31
|
+
let found = false
|
|
32
|
+
for (const [cat, patterns] of Object.entries(TOOL_CATEGORIES)) {
|
|
33
|
+
if (patterns.some(p => t.name.startsWith(p) || t.name === p || t.name.toLowerCase().includes(p.toLowerCase()))) {
|
|
34
|
+
if (!cats[cat]) cats[cat] = { tools: [], total: 0 }
|
|
35
|
+
cats[cat].tools.push(t)
|
|
36
|
+
cats[cat].total += t.count
|
|
37
|
+
found = true
|
|
38
|
+
break
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!found) {
|
|
42
|
+
if (!cats['Other']) cats['Other'] = { tools: [], total: 0 }
|
|
43
|
+
cats['Other'].tools.push(t)
|
|
44
|
+
cats['Other'].total += t.count
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return Object.entries(cats).sort((a, b) => b[1].total - a[1].total)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Summarize args for display
|
|
17
51
|
function summarizeArgs(toolName, args) {
|
|
18
52
|
if (!args || typeof args !== 'object') return null
|
|
19
53
|
if (args.CommandLine || args.command) return args.CommandLine || args.command
|
|
@@ -27,7 +61,6 @@ function summarizeArgs(toolName, args) {
|
|
|
27
61
|
return vals.length > 0 ? vals[0].substring(0, 120) : null
|
|
28
62
|
}
|
|
29
63
|
|
|
30
|
-
// Detect if args contain a diff (old_string/new_string or similar)
|
|
31
64
|
function getDiff(args) {
|
|
32
65
|
if (!args || typeof args !== 'object') return null
|
|
33
66
|
const old = args.old_string || args.old_text || args.oldText || args.search || null
|
|
@@ -76,7 +109,7 @@ function ToolCallRow({ call, toolName, index }) {
|
|
|
76
109
|
return (
|
|
77
110
|
<div className="px-2 py-1 text-[10px]" style={{ background: index % 2 === 0 ? 'var(--c-code-bg)' : 'transparent' }}>
|
|
78
111
|
<div className="flex items-start gap-2 cursor-pointer" onClick={() => hasDetail && setExpanded(!expanded)}>
|
|
79
|
-
<
|
|
112
|
+
<EditorIcon source={call.source} size={10} />
|
|
80
113
|
<div className="flex-1 min-w-0">
|
|
81
114
|
{summary && (
|
|
82
115
|
<div className="font-mono truncate" style={{ color: 'var(--c-white)' }} title={summary}>{summary}</div>
|
|
@@ -85,7 +118,7 @@ function ToolCallRow({ call, toolName, index }) {
|
|
|
85
118
|
<span>{editorLabel(call.source)}</span>
|
|
86
119
|
{project && <span>· {project}</span>}
|
|
87
120
|
{call.timestamp && <span>· {new Date(call.timestamp).toLocaleDateString()}</span>}
|
|
88
|
-
{hasDetail && <span>{expanded ? '
|
|
121
|
+
{hasDetail && <span>{expanded ? '▾' : '▸'}</span>}
|
|
89
122
|
</div>
|
|
90
123
|
</div>
|
|
91
124
|
</div>
|
|
@@ -113,10 +146,11 @@ function ToolDrillDown({ toolName, folder, onClose }) {
|
|
|
113
146
|
<div className="card p-3 fade-in" style={{ borderColor: 'rgba(99,102,241,0.3)' }}>
|
|
114
147
|
<div className="flex items-center justify-between mb-2">
|
|
115
148
|
<div className="flex items-center gap-2">
|
|
149
|
+
<Wrench size={12} style={{ color: 'var(--c-accent)' }} />
|
|
116
150
|
<span className="text-xs font-bold" style={{ color: 'var(--c-white)' }}>{toolName}</span>
|
|
117
151
|
<span className="text-[10px]" style={{ color: 'var(--c-text2)' }}>
|
|
118
152
|
{calls ? `${calls.length} calls` : '...'}
|
|
119
|
-
{calls && projectName ? `
|
|
153
|
+
{calls && projectName ? ` in ${projectName}` : ''}
|
|
120
154
|
</span>
|
|
121
155
|
</div>
|
|
122
156
|
<button onClick={onClose} className="p-0.5" style={{ color: 'var(--c-text2)' }}><X size={12} /></button>
|
|
@@ -136,6 +170,24 @@ function ToolDrillDown({ toolName, folder, onClose }) {
|
|
|
136
170
|
)
|
|
137
171
|
}
|
|
138
172
|
|
|
173
|
+
// Proportional bar component
|
|
174
|
+
function ProportionBar({ segments, height = 6 }) {
|
|
175
|
+
const total = segments.reduce((s, seg) => s + seg.value, 0)
|
|
176
|
+
if (total === 0) return null
|
|
177
|
+
return (
|
|
178
|
+
<div className="flex w-full rounded-full overflow-hidden" style={{ height }}>
|
|
179
|
+
{segments.filter(s => s.value > 0).map((seg, i) => (
|
|
180
|
+
<div
|
|
181
|
+
key={i}
|
|
182
|
+
title={`${seg.label}: ${formatNumber(seg.value)}`}
|
|
183
|
+
className="h-full transition-all"
|
|
184
|
+
style={{ width: `${(seg.value / total * 100).toFixed(1)}%`, background: seg.color }}
|
|
185
|
+
/>
|
|
186
|
+
))}
|
|
187
|
+
</div>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
139
191
|
export default function DeepAnalysis({ overview }) {
|
|
140
192
|
const [editor, setEditor] = useState('')
|
|
141
193
|
const [folder, setFolder] = useState('')
|
|
@@ -165,6 +217,21 @@ export default function DeepAnalysis({ overview }) {
|
|
|
165
217
|
const tools = data?.topTools?.slice(0, 15) || []
|
|
166
218
|
const models = data?.topModels?.slice(0, 10) || []
|
|
167
219
|
|
|
220
|
+
// Computed insights
|
|
221
|
+
const insights = useMemo(() => {
|
|
222
|
+
if (!data) return null
|
|
223
|
+
const totalTok = data.totalInputTokens + data.totalOutputTokens
|
|
224
|
+
const msgsPerSession = data.analyzedChats > 0 ? (data.totalMessages / data.analyzedChats).toFixed(1) : 0
|
|
225
|
+
const toolsPerSession = data.analyzedChats > 0 ? (data.totalToolCalls / data.analyzedChats).toFixed(1) : 0
|
|
226
|
+
const tokPerMsg = data.totalMessages > 0 ? Math.round(totalTok / data.totalMessages) : 0
|
|
227
|
+
const cacheHitRate = data.totalInputTokens > 0 ? ((data.totalCacheRead / data.totalInputTokens) * 100).toFixed(1) : 0
|
|
228
|
+
const outputRatio = data.totalInputTokens > 0 ? (data.totalOutputTokens / data.totalInputTokens).toFixed(2) : 0
|
|
229
|
+
const aiVsHuman = data.totalUserChars > 0 ? (data.totalAssistantChars / data.totalUserChars).toFixed(1) : 0
|
|
230
|
+
return { totalTok, msgsPerSession, toolsPerSession, tokPerMsg, cacheHitRate, outputRatio, aiVsHuman }
|
|
231
|
+
}, [data])
|
|
232
|
+
|
|
233
|
+
const toolCategories = useMemo(() => tools.length > 0 ? categorizeTools(tools) : [], [tools])
|
|
234
|
+
|
|
168
235
|
function handleToolClick(evt, elements) {
|
|
169
236
|
if (elements.length > 0) {
|
|
170
237
|
const idx = elements[0].index
|
|
@@ -174,12 +241,13 @@ export default function DeepAnalysis({ overview }) {
|
|
|
174
241
|
}
|
|
175
242
|
|
|
176
243
|
return (
|
|
177
|
-
<div className="fade-in space-y-
|
|
244
|
+
<div className="fade-in space-y-4">
|
|
245
|
+
{/* Filters */}
|
|
178
246
|
<div className="flex items-center gap-2">
|
|
179
247
|
<select
|
|
180
248
|
value={editor}
|
|
181
249
|
onChange={e => setEditor(e.target.value)}
|
|
182
|
-
className="px-2 py-1 text-[11px] outline-none"
|
|
250
|
+
className="px-2 py-1.5 text-[11px] outline-none rounded-sm"
|
|
183
251
|
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
184
252
|
>
|
|
185
253
|
<option value="">All Editors</option>
|
|
@@ -190,7 +258,7 @@ export default function DeepAnalysis({ overview }) {
|
|
|
190
258
|
<select
|
|
191
259
|
value={folder}
|
|
192
260
|
onChange={e => setFolder(e.target.value)}
|
|
193
|
-
className="px-2 py-1 text-[11px] outline-none max-w-[200px] truncate"
|
|
261
|
+
className="px-2 py-1.5 text-[11px] outline-none max-w-[200px] truncate rounded-sm"
|
|
194
262
|
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
195
263
|
>
|
|
196
264
|
<option value="">All Projects</option>
|
|
@@ -198,35 +266,151 @@ export default function DeepAnalysis({ overview }) {
|
|
|
198
266
|
<option key={p.fullPath || p.name} value={p.fullPath}>{p.name}</option>
|
|
199
267
|
))}
|
|
200
268
|
</select>
|
|
201
|
-
{loading && (
|
|
202
|
-
|
|
203
|
-
)}
|
|
204
|
-
{data && <span className="text-[10px]" style={{ color: 'var(--c-text2)' }}>{data.analyzedChats} sessions</span>}
|
|
269
|
+
{loading && <Loader2 size={11} className="animate-spin" style={{ color: 'var(--c-text3)' }} />}
|
|
270
|
+
{data && <span className="text-[10px]" style={{ color: 'var(--c-text2)' }}>{data.analyzedChats} sessions analyzed</span>}
|
|
205
271
|
<div className="ml-auto"><DateRangePicker value={dateRange} onChange={setDateRange} /></div>
|
|
206
272
|
</div>
|
|
207
273
|
|
|
208
|
-
{data && (
|
|
274
|
+
{data && insights && (
|
|
209
275
|
<>
|
|
210
|
-
|
|
276
|
+
{/* KPIs */}
|
|
277
|
+
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))' }}>
|
|
211
278
|
<KpiCard label="sessions" value={data.analyzedChats} />
|
|
212
|
-
<KpiCard label="messages" value={formatNumber(data.totalMessages)} />
|
|
213
|
-
<KpiCard label="tool calls" value={formatNumber(data.totalToolCalls)} />
|
|
214
|
-
<KpiCard label="
|
|
215
|
-
<KpiCard label="
|
|
279
|
+
<KpiCard label="messages" value={formatNumber(data.totalMessages)} sub={`${insights.msgsPerSession}/session`} />
|
|
280
|
+
<KpiCard label="tool calls" value={formatNumber(data.totalToolCalls)} sub={`${insights.toolsPerSession}/session`} />
|
|
281
|
+
<KpiCard label="total tokens" value={formatNumber(insights.totalTok)} sub={`${formatNumber(insights.tokPerMsg)}/msg`} />
|
|
282
|
+
<KpiCard label="you wrote" value={formatNumber(data.totalUserChars)} sub={`AI: ${insights.aiVsHuman}× more`} />
|
|
216
283
|
</div>
|
|
217
284
|
|
|
218
|
-
|
|
285
|
+
{/* Token flow + Insights row */}
|
|
286
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
|
287
|
+
{/* Token flow visualization */}
|
|
288
|
+
<div className="card p-3">
|
|
289
|
+
<SectionTitle>token flow</SectionTitle>
|
|
290
|
+
<div className="space-y-3 mt-2">
|
|
291
|
+
{/* Input tokens breakdown */}
|
|
292
|
+
<div>
|
|
293
|
+
<div className="flex items-center justify-between text-[10px] mb-1">
|
|
294
|
+
<span style={{ color: 'var(--c-text2)' }}>input tokens</span>
|
|
295
|
+
<span className="font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(data.totalInputTokens)}</span>
|
|
296
|
+
</div>
|
|
297
|
+
<ProportionBar segments={[
|
|
298
|
+
{ label: 'Fresh input', value: data.totalInputTokens - data.totalCacheRead, color: '#6366f1' },
|
|
299
|
+
{ label: 'Cache read', value: data.totalCacheRead, color: '#34d399' },
|
|
300
|
+
]} />
|
|
301
|
+
<div className="flex items-center gap-3 mt-1 text-[9px]">
|
|
302
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#6366f1' }} /> fresh {formatNumber(data.totalInputTokens - data.totalCacheRead)}</span>
|
|
303
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#34d399' }} /> cached {formatNumber(data.totalCacheRead)}</span>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
{/* Output tokens */}
|
|
307
|
+
<div>
|
|
308
|
+
<div className="flex items-center justify-between text-[10px] mb-1">
|
|
309
|
+
<span style={{ color: 'var(--c-text2)' }}>output tokens</span>
|
|
310
|
+
<span className="font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(data.totalOutputTokens)}</span>
|
|
311
|
+
</div>
|
|
312
|
+
<ProportionBar segments={[
|
|
313
|
+
{ label: 'Output', value: data.totalOutputTokens, color: '#a78bfa' },
|
|
314
|
+
]} />
|
|
315
|
+
</div>
|
|
316
|
+
{/* Cache write */}
|
|
317
|
+
{data.totalCacheWrite > 0 && (
|
|
318
|
+
<div>
|
|
319
|
+
<div className="flex items-center justify-between text-[10px] mb-1">
|
|
320
|
+
<span style={{ color: 'var(--c-text2)' }}>cache write</span>
|
|
321
|
+
<span className="font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(data.totalCacheWrite)}</span>
|
|
322
|
+
</div>
|
|
323
|
+
<ProportionBar segments={[
|
|
324
|
+
{ label: 'Cache write', value: data.totalCacheWrite, color: '#fbbf24' },
|
|
325
|
+
]} />
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
{/* Overall ratio bar */}
|
|
329
|
+
<div className="pt-2" style={{ borderTop: '1px solid var(--c-border)' }}>
|
|
330
|
+
<div className="text-[9px] mb-1" style={{ color: 'var(--c-text3)' }}>overall token distribution</div>
|
|
331
|
+
<ProportionBar height={10} segments={[
|
|
332
|
+
{ label: 'Input', value: data.totalInputTokens, color: '#6366f1' },
|
|
333
|
+
{ label: 'Output', value: data.totalOutputTokens, color: '#a78bfa' },
|
|
334
|
+
{ label: 'Cache Read', value: data.totalCacheRead, color: '#34d399' },
|
|
335
|
+
{ label: 'Cache Write', value: data.totalCacheWrite, color: '#fbbf24' },
|
|
336
|
+
]} />
|
|
337
|
+
<div className="flex items-center gap-3 mt-1 text-[9px]" style={{ color: 'var(--c-text3)' }}>
|
|
338
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#6366f1' }} /> in</span>
|
|
339
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#a78bfa' }} /> out</span>
|
|
340
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#34d399' }} /> cache read</span>
|
|
341
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#fbbf24' }} /> cache write</span>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
{/* Efficiency insights */}
|
|
219
348
|
<div className="card p-3">
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
349
|
+
<SectionTitle>efficiency insights</SectionTitle>
|
|
350
|
+
<div className="space-y-3 mt-2">
|
|
351
|
+
{/* Human vs AI chars */}
|
|
352
|
+
<div>
|
|
353
|
+
<div className="text-[10px] mb-1" style={{ color: 'var(--c-text2)' }}>you vs AI (characters)</div>
|
|
354
|
+
<ProportionBar height={8} segments={[
|
|
355
|
+
{ label: 'You', value: data.totalUserChars, color: '#6366f1' },
|
|
356
|
+
{ label: 'AI', value: data.totalAssistantChars, color: '#34d399' },
|
|
357
|
+
]} />
|
|
358
|
+
<div className="flex items-center justify-between mt-1 text-[9px]">
|
|
359
|
+
<span style={{ color: '#6366f1' }}>You: {formatNumber(data.totalUserChars)}</span>
|
|
360
|
+
<span style={{ color: '#34d399' }}>AI: {formatNumber(data.totalAssistantChars)}</span>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
{/* Metric cards */}
|
|
365
|
+
<div className="grid grid-cols-2 gap-2">
|
|
366
|
+
<div className="p-2 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
367
|
+
<div className="flex items-center gap-1 text-[9px]" style={{ color: 'var(--c-text3)' }}>
|
|
368
|
+
<TrendingUp size={9} /> output/input ratio
|
|
369
|
+
</div>
|
|
370
|
+
<div className="text-sm font-bold mt-0.5" style={{ color: 'var(--c-white)' }}>{insights.outputRatio}×</div>
|
|
371
|
+
</div>
|
|
372
|
+
<div className="p-2 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
373
|
+
<div className="flex items-center gap-1 text-[9px]" style={{ color: 'var(--c-text3)' }}>
|
|
374
|
+
<Zap size={9} /> cache hit rate
|
|
375
|
+
</div>
|
|
376
|
+
<div className="text-sm font-bold mt-0.5" style={{ color: parseFloat(insights.cacheHitRate) > 50 ? '#34d399' : parseFloat(insights.cacheHitRate) > 20 ? '#fbbf24' : 'var(--c-white)' }}>{insights.cacheHitRate}%</div>
|
|
377
|
+
</div>
|
|
378
|
+
<div className="p-2 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
379
|
+
<div className="flex items-center gap-1 text-[9px]" style={{ color: 'var(--c-text3)' }}>
|
|
380
|
+
<MessageSquare size={9} /> tokens per message
|
|
381
|
+
</div>
|
|
382
|
+
<div className="text-sm font-bold mt-0.5" style={{ color: 'var(--c-white)' }}>{formatNumber(insights.tokPerMsg)}</div>
|
|
383
|
+
</div>
|
|
384
|
+
<div className="p-2 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
385
|
+
<div className="flex items-center gap-1 text-[9px]" style={{ color: 'var(--c-text3)' }}>
|
|
386
|
+
<Wrench size={9} /> tools per session
|
|
387
|
+
</div>
|
|
388
|
+
<div className="text-sm font-bold mt-0.5" style={{ color: 'var(--c-white)' }}>{insights.toolsPerSession}</div>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
{/* AI amplification */}
|
|
393
|
+
<div className="p-2 rounded-sm text-center" style={{ background: 'var(--c-code-bg)' }}>
|
|
394
|
+
<div className="text-[9px]" style={{ color: 'var(--c-text3)' }}>AI amplification factor</div>
|
|
395
|
+
<div className="text-lg font-bold" style={{ color: 'var(--c-accent)' }}>{insights.aiVsHuman}×</div>
|
|
396
|
+
<div className="text-[9px]" style={{ color: 'var(--c-text3)' }}>AI writes {insights.aiVsHuman}× more than you type</div>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
{/* Charts row: Tools + Models + Tool Categories */}
|
|
403
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
|
|
404
|
+
{/* Tools bar */}
|
|
405
|
+
<div className="card p-3 lg:col-span-2">
|
|
406
|
+
<SectionTitle>most used tools <span style={{ color: 'var(--c-text3)' }}>(click to drill down)</span></SectionTitle>
|
|
407
|
+
<div style={{ height: Math.max(tools.length * 20 + 10, 120) }}>
|
|
224
408
|
{tools.length > 0 ? (
|
|
225
409
|
<Bar
|
|
226
410
|
ref={chartRef}
|
|
227
411
|
data={{
|
|
228
412
|
labels: tools.map(t => t.name),
|
|
229
|
-
datasets: [{ data: tools.map(t => t.count), backgroundColor:
|
|
413
|
+
datasets: [{ data: tools.map(t => t.count), backgroundColor: TOOL_COLORS.concat(MODEL_COLORS).slice(0, tools.length), borderRadius: 2 }],
|
|
230
414
|
}}
|
|
231
415
|
options={{
|
|
232
416
|
indexAxis: 'y',
|
|
@@ -234,20 +418,20 @@ export default function DeepAnalysis({ overview }) {
|
|
|
234
418
|
maintainAspectRatio: false,
|
|
235
419
|
onClick: handleToolClick,
|
|
236
420
|
scales: {
|
|
237
|
-
x: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size:
|
|
238
|
-
y: { grid: { display: false }, ticks: { color: txtColor, font: { size:
|
|
421
|
+
x: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 8, family: MONO } } },
|
|
422
|
+
y: { grid: { display: false }, ticks: { color: txtColor, font: { size: 8, family: MONO } } },
|
|
239
423
|
},
|
|
240
|
-
plugins: { legend: { display: false }, tooltip: { bodyFont: { family: MONO, size: 10 } } },
|
|
424
|
+
plugins: { legend: { display: false }, tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } } },
|
|
241
425
|
}}
|
|
242
426
|
/>
|
|
243
|
-
) : (
|
|
244
|
-
<div className="text-[10px] text-center py-8" style={{ color: 'var(--c-text3)' }}>no tool calls found</div>
|
|
245
|
-
)}
|
|
427
|
+
) : <div className="text-[10px] text-center py-8" style={{ color: 'var(--c-text3)' }}>no tool calls found</div>}
|
|
246
428
|
</div>
|
|
247
429
|
</div>
|
|
430
|
+
|
|
431
|
+
{/* Models doughnut */}
|
|
248
432
|
<div className="card p-3">
|
|
249
|
-
<
|
|
250
|
-
<div style={{ height:
|
|
433
|
+
<SectionTitle>models</SectionTitle>
|
|
434
|
+
<div style={{ height: 160 }}>
|
|
251
435
|
{models.length > 0 ? (
|
|
252
436
|
<Doughnut
|
|
253
437
|
data={{
|
|
@@ -255,21 +439,31 @@ export default function DeepAnalysis({ overview }) {
|
|
|
255
439
|
datasets: [{ data: models.map(m => m.count), backgroundColor: MODEL_COLORS, borderWidth: 0 }],
|
|
256
440
|
}}
|
|
257
441
|
options={{
|
|
258
|
-
responsive: true,
|
|
259
|
-
maintainAspectRatio: false,
|
|
260
|
-
cutout: '55%',
|
|
442
|
+
responsive: true, maintainAspectRatio: false, cutout: '60%',
|
|
261
443
|
plugins: {
|
|
262
|
-
legend: {
|
|
263
|
-
|
|
264
|
-
labels: { color: legendColor, font: { size: 9, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 },
|
|
265
|
-
},
|
|
444
|
+
legend: { position: 'right', labels: { color: legendColor, font: { size: 8, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
|
|
445
|
+
tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
|
|
266
446
|
},
|
|
267
447
|
}}
|
|
268
448
|
/>
|
|
269
|
-
) : (
|
|
270
|
-
<div className="text-[10px] text-center py-8" style={{ color: 'var(--c-text3)' }}>no model data</div>
|
|
271
|
-
)}
|
|
449
|
+
) : <div className="text-[10px] text-center py-8" style={{ color: 'var(--c-text3)' }}>no model data</div>}
|
|
272
450
|
</div>
|
|
451
|
+
|
|
452
|
+
{/* Tool categories below models */}
|
|
453
|
+
{toolCategories.length > 0 && (
|
|
454
|
+
<div className="mt-3 pt-3" style={{ borderTop: '1px solid var(--c-border)' }}>
|
|
455
|
+
<div className="text-[9px] uppercase tracking-wider mb-2" style={{ color: 'var(--c-text3)' }}>tool categories</div>
|
|
456
|
+
<div className="space-y-1.5">
|
|
457
|
+
{toolCategories.map(([cat, { tools: catTools, total }]) => (
|
|
458
|
+
<div key={cat} className="flex items-center gap-2 text-[10px]">
|
|
459
|
+
<span className="truncate flex-1" style={{ color: 'var(--c-text2)' }}>{cat}</span>
|
|
460
|
+
<span className="font-bold" style={{ color: 'var(--c-white)' }}>{total}</span>
|
|
461
|
+
<span style={{ color: 'var(--c-text3)' }}>({catTools.length})</span>
|
|
462
|
+
</div>
|
|
463
|
+
))}
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
)}
|
|
273
467
|
</div>
|
|
274
468
|
</div>
|
|
275
469
|
|
|
@@ -277,26 +471,6 @@ export default function DeepAnalysis({ overview }) {
|
|
|
277
471
|
{selectedTool && (
|
|
278
472
|
<ToolDrillDown toolName={selectedTool} folder={folder} onClose={() => setSelectedTool(null)} />
|
|
279
473
|
)}
|
|
280
|
-
|
|
281
|
-
{/* Token breakdown */}
|
|
282
|
-
{(data.totalInputTokens > 0 || data.totalOutputTokens > 0) && (
|
|
283
|
-
<div className="card p-3">
|
|
284
|
-
<h3 className="text-[10px] font-medium uppercase tracking-wider mb-2" style={{ color: 'var(--c-text2)' }}>tokens</h3>
|
|
285
|
-
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
|
286
|
-
{[
|
|
287
|
-
['Input', data.totalInputTokens],
|
|
288
|
-
['Output', data.totalOutputTokens],
|
|
289
|
-
['Cache Read', data.totalCacheRead],
|
|
290
|
-
['Cache Write', data.totalCacheWrite],
|
|
291
|
-
].map(([label, val]) => (
|
|
292
|
-
<div key={label}>
|
|
293
|
-
<div className="text-[9px]" style={{ color: 'var(--c-text3)' }}>{label}</div>
|
|
294
|
-
<div className="text-sm font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(val)}</div>
|
|
295
|
-
</div>
|
|
296
|
-
))}
|
|
297
|
-
</div>
|
|
298
|
-
</div>
|
|
299
|
-
)}
|
|
300
474
|
</>
|
|
301
475
|
)}
|
|
302
476
|
</div>
|