agentlytics 0.1.0 → 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/README.md +3 -3
- package/editors/base.js +24 -0
- package/editors/cursor.js +4 -2
- package/editors/vscode.js +3 -4
- package/editors/zed.js +2 -2
- package/package.json +1 -1
- package/ui/src/components/EditorDot.jsx +3 -5
- package/ui/src/components/EditorIcon.jsx +63 -0
- package/ui/src/components/KpiCard.jsx +3 -3
- package/ui/src/pages/Dashboard.jsx +169 -84
- 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,13 +1,15 @@
|
|
|
1
1
|
import { useState, useEffect } 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 { Search,
|
|
4
|
+
import { Search, MessageSquare, Wrench, Cpu, FolderOpen, Calendar, ArrowRight } from 'lucide-react'
|
|
5
5
|
import { useNavigate } from 'react-router-dom'
|
|
6
6
|
import { fetchProjects } from '../lib/api'
|
|
7
7
|
import { editorColor, editorLabel, formatNumber, formatDate, dateRangeToApiParams } from '../lib/constants'
|
|
8
8
|
import { useTheme } from '../lib/theme'
|
|
9
9
|
import KpiCard from '../components/KpiCard'
|
|
10
|
+
import EditorIcon from '../components/EditorIcon'
|
|
10
11
|
import DateRangePicker from '../components/DateRangePicker'
|
|
12
|
+
import SectionTitle from '../components/SectionTitle'
|
|
11
13
|
|
|
12
14
|
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
|
|
13
15
|
|
|
@@ -41,33 +43,75 @@ export default function Projects({ overview }) {
|
|
|
41
43
|
const totalSessions = projects.reduce((s, p) => s + p.totalSessions, 0)
|
|
42
44
|
const totalMessages = projects.reduce((s, p) => s + p.totalMessages, 0)
|
|
43
45
|
const totalTokens = projects.reduce((s, p) => s + p.totalInputTokens + p.totalOutputTokens, 0)
|
|
44
|
-
const
|
|
46
|
+
const maxSessions = Math.max(...projects.map(p => p.totalSessions), 1)
|
|
45
47
|
|
|
46
|
-
// Aggregate
|
|
48
|
+
// Aggregate for charts
|
|
47
49
|
const globalModels = {}
|
|
48
50
|
const globalEditors = {}
|
|
49
51
|
for (const p of projects) {
|
|
50
52
|
for (const m of p.topModels) globalModels[m.name] = (globalModels[m.name] || 0) + m.count
|
|
51
53
|
for (const [e, c] of Object.entries(p.editors)) globalEditors[e] = (globalEditors[e] || 0) + c
|
|
52
54
|
}
|
|
53
|
-
const topGlobalModels = Object.entries(globalModels).sort((a, b) => b[1] - a[1]).slice(0,
|
|
55
|
+
const topGlobalModels = Object.entries(globalModels).sort((a, b) => b[1] - a[1]).slice(0, 8)
|
|
54
56
|
const topGlobalEditors = Object.entries(globalEditors).sort((a, b) => b[1] - a[1])
|
|
57
|
+
const top10 = projects.slice(0, 10)
|
|
55
58
|
|
|
56
59
|
return (
|
|
57
|
-
<div className="fade-in space-y-
|
|
60
|
+
<div className="fade-in space-y-4">
|
|
58
61
|
{/* KPIs */}
|
|
59
|
-
<div className="grid
|
|
62
|
+
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))' }}>
|
|
60
63
|
<KpiCard label="projects" value={projects.length} />
|
|
61
|
-
<KpiCard label="
|
|
62
|
-
<KpiCard label="
|
|
63
|
-
<KpiCard label="
|
|
64
|
+
<KpiCard label="sessions" value={formatNumber(totalSessions)} onClick={() => navigate('/sessions')} />
|
|
65
|
+
<KpiCard label="messages" value={formatNumber(totalMessages)} />
|
|
66
|
+
<KpiCard label="tokens" value={formatNumber(totalTokens)} />
|
|
64
67
|
</div>
|
|
65
68
|
|
|
66
|
-
{/*
|
|
67
|
-
<div className="grid grid-cols-1 lg:grid-cols-
|
|
68
|
-
<div className="card p-
|
|
69
|
-
<
|
|
70
|
-
<div style={{ height:
|
|
69
|
+
{/* Charts row */}
|
|
70
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
|
|
71
|
+
<div className="card p-3">
|
|
72
|
+
<SectionTitle>top projects <span style={{ color: 'var(--c-text3)' }}>by sessions</span></SectionTitle>
|
|
73
|
+
<div style={{ height: 160 }}>
|
|
74
|
+
<Bar
|
|
75
|
+
data={{
|
|
76
|
+
labels: top10.map(p => p.name),
|
|
77
|
+
datasets: [{
|
|
78
|
+
data: top10.map(p => p.totalSessions),
|
|
79
|
+
backgroundColor: '#6366f1',
|
|
80
|
+
borderRadius: 3,
|
|
81
|
+
}],
|
|
82
|
+
}}
|
|
83
|
+
options={{
|
|
84
|
+
responsive: true, maintainAspectRatio: false, indexAxis: 'y',
|
|
85
|
+
scales: {
|
|
86
|
+
x: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 8, family: MONO } } },
|
|
87
|
+
y: { grid: { display: false }, ticks: { color: txtColor, font: { size: 8, family: MONO }, callback: (v, i) => top10[i]?.name?.length > 16 ? top10[i].name.slice(0, 15) + '…' : top10[i]?.name } },
|
|
88
|
+
},
|
|
89
|
+
plugins: { legend: { display: false }, tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } } },
|
|
90
|
+
}}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
<div className="card p-3">
|
|
95
|
+
<SectionTitle>editors</SectionTitle>
|
|
96
|
+
<div style={{ height: 160 }}>
|
|
97
|
+
<Doughnut
|
|
98
|
+
data={{
|
|
99
|
+
labels: topGlobalEditors.map(e => editorLabel(e[0])),
|
|
100
|
+
datasets: [{ data: topGlobalEditors.map(e => e[1]), backgroundColor: topGlobalEditors.map(e => editorColor(e[0])), borderWidth: 0 }],
|
|
101
|
+
}}
|
|
102
|
+
options={{
|
|
103
|
+
responsive: true, maintainAspectRatio: false, cutout: '60%',
|
|
104
|
+
plugins: {
|
|
105
|
+
legend: { position: 'right', labels: { color: txtColor, font: { size: 9, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
|
|
106
|
+
tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
|
|
107
|
+
},
|
|
108
|
+
}}
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div className="card p-3">
|
|
113
|
+
<SectionTitle>models</SectionTitle>
|
|
114
|
+
<div style={{ height: 160 }}>
|
|
71
115
|
{topGlobalModels.length > 0 ? (
|
|
72
116
|
<Doughnut
|
|
73
117
|
data={{
|
|
@@ -75,95 +119,114 @@ export default function Projects({ overview }) {
|
|
|
75
119
|
datasets: [{ data: topGlobalModels.map(m => m[1]), backgroundColor: MODEL_COLORS, borderWidth: 0 }],
|
|
76
120
|
}}
|
|
77
121
|
options={{
|
|
78
|
-
responsive: true, maintainAspectRatio: false, cutout: '
|
|
122
|
+
responsive: true, maintainAspectRatio: false, cutout: '60%',
|
|
79
123
|
plugins: {
|
|
80
|
-
legend: { position: 'right', labels: { color: txtColor, font: { size: 9, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding:
|
|
81
|
-
tooltip: { bodyFont: { family: MONO, size:
|
|
124
|
+
legend: { position: 'right', labels: { color: txtColor, font: { size: 9, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
|
|
125
|
+
tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
|
|
82
126
|
},
|
|
83
127
|
}}
|
|
84
128
|
/>
|
|
85
129
|
) : <div className="text-xs text-center py-12" style={{ color: 'var(--c-text3)' }}>no model data</div>}
|
|
86
130
|
</div>
|
|
87
131
|
</div>
|
|
88
|
-
<div className="card p-5">
|
|
89
|
-
<h3 className="text-xs font-medium uppercase tracking-wider mb-3" style={{ color: 'var(--c-text2)' }}>editors across projects</h3>
|
|
90
|
-
<div style={{ height: 240 }}>
|
|
91
|
-
<Bar
|
|
92
|
-
data={{
|
|
93
|
-
labels: topGlobalEditors.map(e => editorLabel(e[0])),
|
|
94
|
-
datasets: [{
|
|
95
|
-
data: topGlobalEditors.map(e => e[1]),
|
|
96
|
-
backgroundColor: topGlobalEditors.map(e => editorColor(e[0])),
|
|
97
|
-
borderRadius: 4,
|
|
98
|
-
}],
|
|
99
|
-
}}
|
|
100
|
-
options={{
|
|
101
|
-
responsive: true, maintainAspectRatio: false,
|
|
102
|
-
scales: {
|
|
103
|
-
x: { grid: { display: false }, ticks: { color: txtColor, font: { size: 9, family: MONO } } },
|
|
104
|
-
y: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 9, family: MONO } } },
|
|
105
|
-
},
|
|
106
|
-
plugins: { legend: { display: false }, tooltip: { bodyFont: { family: MONO, size: 11 }, titleFont: { family: MONO, size: 11 } } },
|
|
107
|
-
}}
|
|
108
|
-
/>
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
132
|
</div>
|
|
112
133
|
|
|
113
134
|
{/* Filters */}
|
|
114
135
|
<div className="flex items-center gap-2">
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
136
|
+
<select
|
|
137
|
+
value={editorFilter}
|
|
138
|
+
onChange={e => setEditorFilter(e.target.value)}
|
|
139
|
+
className="px-2 py-1.5 text-[11px] outline-none rounded-sm"
|
|
140
|
+
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
141
|
+
>
|
|
142
|
+
<option value="">All Editors</option>
|
|
143
|
+
{editors.map(e => (
|
|
144
|
+
<option key={e.id} value={e.id}>{editorLabel(e.id)}</option>
|
|
145
|
+
))}
|
|
146
|
+
</select>
|
|
147
|
+
<div className="relative max-w-sm flex-1">
|
|
148
|
+
<Search size={13} className="absolute left-3 top-1/2 -translate-y-1/2" style={{ color: 'var(--c-text3)' }} />
|
|
149
|
+
<input
|
|
150
|
+
type="text"
|
|
151
|
+
placeholder="search projects..."
|
|
152
|
+
value={search}
|
|
153
|
+
onChange={e => setSearch(e.target.value)}
|
|
154
|
+
className="w-full pl-8 pr-3 py-1.5 text-[11px] outline-none rounded-sm"
|
|
119
155
|
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{editors.map(e => (
|
|
123
|
-
<option key={e.id} value={e.id}>{editorLabel(e.id)}</option>
|
|
124
|
-
))}
|
|
125
|
-
</select>
|
|
126
|
-
<div className="relative max-w-sm flex-1">
|
|
127
|
-
<Search size={13} className="absolute left-3 top-1/2 -translate-y-1/2" style={{ color: 'var(--c-text3)' }} />
|
|
128
|
-
<input
|
|
129
|
-
type="text"
|
|
130
|
-
placeholder="search projects..."
|
|
131
|
-
value={search}
|
|
132
|
-
onChange={e => setSearch(e.target.value)}
|
|
133
|
-
className="w-full pl-8 pr-3 py-1 text-[11px] outline-none"
|
|
134
|
-
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
135
|
-
/>
|
|
136
|
-
</div>
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
137
158
|
<div className="ml-auto"><DateRangePicker value={dateRange} onChange={setDateRange} /></div>
|
|
138
159
|
</div>
|
|
139
160
|
|
|
140
|
-
{/* Project
|
|
141
|
-
<div className="
|
|
161
|
+
{/* Project cards */}
|
|
162
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
142
163
|
{filtered.map(p => {
|
|
143
164
|
const editorEntries = Object.entries(p.editors).sort((a, b) => b[1] - a[1])
|
|
165
|
+
const totalTok = p.totalInputTokens + p.totalOutputTokens
|
|
166
|
+
const topModel = p.topModels[0]
|
|
144
167
|
|
|
145
168
|
return (
|
|
146
169
|
<div
|
|
147
170
|
key={p.folder}
|
|
148
|
-
className="card
|
|
171
|
+
className="card p-3 cursor-pointer transition hover:opacity-90 flex flex-col gap-2.5"
|
|
149
172
|
onClick={() => navigate(`/projects/detail?folder=${encodeURIComponent(p.folder)}`)}
|
|
150
173
|
>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
174
|
+
{/* Header: name + editors */}
|
|
175
|
+
<div className="flex items-start gap-2 min-w-0">
|
|
176
|
+
<FolderOpen size={14} className="flex-shrink-0 mt-0.5" style={{ color: 'var(--c-accent)' }} />
|
|
177
|
+
<div className="flex-1 min-w-0">
|
|
178
|
+
<div className="text-[12px] font-bold truncate" style={{ color: 'var(--c-white)' }}>{p.name}</div>
|
|
179
|
+
<div className="text-[9px] truncate" style={{ color: 'var(--c-text3)' }}>{p.folder}</div>
|
|
180
|
+
</div>
|
|
181
|
+
<div className="flex items-center gap-1 flex-shrink-0">
|
|
157
182
|
{editorEntries.slice(0, 4).map(([e]) => (
|
|
158
|
-
<
|
|
183
|
+
<EditorIcon key={e} source={e} size={12} />
|
|
159
184
|
))}
|
|
160
185
|
</div>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
<
|
|
166
|
-
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{/* Activity bar */}
|
|
189
|
+
<div className="w-full h-1 rounded-full overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
|
|
190
|
+
<div className="h-full rounded-full" style={{ width: `${(p.totalSessions / maxSessions * 100).toFixed(1)}%`, background: 'var(--c-accent)' }} />
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{/* Stats grid */}
|
|
194
|
+
<div className="grid grid-cols-2 gap-x-3 gap-y-1 text-[10px]">
|
|
195
|
+
<div className="flex items-center gap-1">
|
|
196
|
+
<MessageSquare size={9} style={{ color: 'var(--c-text3)' }} />
|
|
197
|
+
<span style={{ color: 'var(--c-text2)' }}>{p.totalSessions} sessions</span>
|
|
198
|
+
<span className="ml-auto font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(p.totalMessages)} msgs</span>
|
|
199
|
+
</div>
|
|
200
|
+
<div className="flex items-center gap-1">
|
|
201
|
+
<Wrench size={9} style={{ color: 'var(--c-text3)' }} />
|
|
202
|
+
<span style={{ color: 'var(--c-text2)' }}>tools</span>
|
|
203
|
+
<span className="ml-auto font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(p.totalToolCalls)}</span>
|
|
204
|
+
</div>
|
|
205
|
+
<div className="flex items-center gap-1">
|
|
206
|
+
<Cpu size={9} style={{ color: 'var(--c-text3)' }} />
|
|
207
|
+
<span className="truncate" style={{ color: 'var(--c-text2)' }}>{topModel ? topModel.name : '—'}</span>
|
|
208
|
+
</div>
|
|
209
|
+
<div className="flex items-center gap-1">
|
|
210
|
+
<span style={{ color: 'var(--c-text3)' }}>tok</span>
|
|
211
|
+
<span className="ml-auto font-bold" style={{ color: totalTok > 0 ? 'var(--c-white)' : 'var(--c-text3)' }}>{totalTok > 0 ? formatNumber(totalTok) : '—'}</span>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* Footer: editors breakdown + date */}
|
|
216
|
+
<div className="flex items-center gap-2 pt-1 text-[9px]" style={{ borderTop: '1px solid var(--c-border)' }}>
|
|
217
|
+
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
|
218
|
+
{editorEntries.map(([e, c]) => (
|
|
219
|
+
<span key={e} className="inline-flex items-center gap-0.5 truncate">
|
|
220
|
+
<span className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ background: editorColor(e) }} />
|
|
221
|
+
<span style={{ color: 'var(--c-text3)' }}>{editorLabel(e)}</span>
|
|
222
|
+
<span className="font-bold" style={{ color: 'var(--c-text2)' }}>{c}</span>
|
|
223
|
+
</span>
|
|
224
|
+
))}
|
|
225
|
+
</div>
|
|
226
|
+
<div className="flex items-center gap-1 flex-shrink-0" style={{ color: 'var(--c-text3)' }}>
|
|
227
|
+
<Calendar size={8} />
|
|
228
|
+
<span>{formatDate(p.lastSeen)}</span>
|
|
229
|
+
</div>
|
|
167
230
|
</div>
|
|
168
231
|
</div>
|
|
169
232
|
)
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
|
|
2
|
-
import { useSearchParams } from 'react-router-dom'
|
|
2
|
+
import { useSearchParams, useNavigate } from 'react-router-dom'
|
|
3
3
|
import { Search, Filter, List, FolderOpen, ChevronDown, ChevronRight, X, AlertTriangle } from 'lucide-react'
|
|
4
|
-
import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Tooltip, Legend, Filler } from 'chart.js'
|
|
5
|
-
import { Line } from 'react-chartjs-2'
|
|
4
|
+
import { Chart as ChartJS, ArcElement, CategoryScale, LinearScale, PointElement, LineElement, BarElement, Tooltip, Legend, Filler } from 'chart.js'
|
|
5
|
+
import { Line, Doughnut, Bar } from 'react-chartjs-2'
|
|
6
6
|
import { fetchChats } from '../lib/api'
|
|
7
|
-
import { editorColor, editorLabel, formatDate, dateRangeToApiParams } from '../lib/constants'
|
|
7
|
+
import { editorColor, editorLabel, formatNumber, formatDate, dateRangeToApiParams } from '../lib/constants'
|
|
8
8
|
import { useTheme } from '../lib/theme'
|
|
9
|
-
import
|
|
9
|
+
import KpiCard from '../components/KpiCard'
|
|
10
|
+
import EditorIcon from '../components/EditorIcon'
|
|
11
|
+
import SectionTitle from '../components/SectionTitle'
|
|
10
12
|
import DateRangePicker from '../components/DateRangePicker'
|
|
11
13
|
import ChatSidebar from '../components/ChatSidebar'
|
|
12
14
|
|
|
13
|
-
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Tooltip, Legend, Filler)
|
|
15
|
+
ChartJS.register(ArcElement, CategoryScale, LinearScale, PointElement, LineElement, BarElement, Tooltip, Legend, Filler)
|
|
14
16
|
|
|
15
17
|
const MONO = 'JetBrains Mono, monospace'
|
|
16
18
|
|
|
@@ -105,6 +107,7 @@ export default function Sessions({ overview }) {
|
|
|
105
107
|
const [total, setTotal] = useState(0)
|
|
106
108
|
const [search, setSearch] = useState('')
|
|
107
109
|
const [searchParams] = useSearchParams()
|
|
110
|
+
const navigate = useNavigate()
|
|
108
111
|
const [editor, setEditor] = useState(searchParams.get('editor') || '')
|
|
109
112
|
const [loading, setLoading] = useState(true)
|
|
110
113
|
const [groupByProject, setGroupByProject] = useState(false)
|
|
@@ -149,6 +152,33 @@ export default function Sessions({ overview }) {
|
|
|
149
152
|
}, [searchFiltered, dateRange])
|
|
150
153
|
|
|
151
154
|
const editors = overview?.editors || []
|
|
155
|
+
const MODEL_COLORS = ['#6366f1', '#a78bfa', '#818cf8', '#c084fc', '#e879f9', '#f472b6', '#fb7185', '#f87171', '#fbbf24', '#34d399']
|
|
156
|
+
|
|
157
|
+
// Summary stats from filtered chats
|
|
158
|
+
const stats = useMemo(() => {
|
|
159
|
+
const editorCounts = {}
|
|
160
|
+
const modelCounts = {}
|
|
161
|
+
const modeCounts = {}
|
|
162
|
+
let bloatCount = 0
|
|
163
|
+
let largeCount = 0
|
|
164
|
+
const projectSet = new Set()
|
|
165
|
+
for (const c of filtered) {
|
|
166
|
+
if (c.source) editorCounts[c.source] = (editorCounts[c.source] || 0) + 1
|
|
167
|
+
if (c.topModel) modelCounts[c.topModel] = (modelCounts[c.topModel] || 0) + 1
|
|
168
|
+
if (c.mode) modeCounts[c.mode] = (modeCounts[c.mode] || 0) + 1
|
|
169
|
+
if (c.folder) projectSet.add(c.folder)
|
|
170
|
+
if (c.bubbleCount >= 500) bloatCount++
|
|
171
|
+
else if (c.bubbleCount >= 200) largeCount++
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
editorEntries: Object.entries(editorCounts).sort((a, b) => b[1] - a[1]),
|
|
175
|
+
modelEntries: Object.entries(modelCounts).sort((a, b) => b[1] - a[1]).slice(0, 10),
|
|
176
|
+
modeEntries: Object.entries(modeCounts).sort((a, b) => b[1] - a[1]),
|
|
177
|
+
bloatCount,
|
|
178
|
+
largeCount,
|
|
179
|
+
projectCount: projectSet.size,
|
|
180
|
+
}
|
|
181
|
+
}, [filtered])
|
|
152
182
|
|
|
153
183
|
// Timeline chart data: sessions per week by editor (always use full list, not date-filtered)
|
|
154
184
|
const timelineChart = useMemo(() => {
|
|
@@ -217,38 +247,45 @@ export default function Sessions({ overview }) {
|
|
|
217
247
|
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
|
218
248
|
onClick={() => setSelectedChatId(c.id)}
|
|
219
249
|
>
|
|
220
|
-
<td className="py-2
|
|
221
|
-
<
|
|
250
|
+
<td className="py-2 px-3">
|
|
251
|
+
<span className="inline-flex items-center gap-1.5">
|
|
252
|
+
<EditorIcon source={c.source} size={12} />
|
|
253
|
+
<span className="text-[11px]" style={{ color: 'var(--c-text2)' }}>{editorLabel(c.source)}</span>
|
|
254
|
+
</span>
|
|
222
255
|
</td>
|
|
223
|
-
<td className="py-2
|
|
224
|
-
{c.name || <span style={{ color: 'var(--c-text3)' }}>
|
|
225
|
-
{c.encrypted && <span className="ml-
|
|
226
|
-
{c.bubbleCount >= 200 && (
|
|
227
|
-
<span
|
|
228
|
-
className="ml-2 inline-flex items-center gap-0.5 text-[10px] px-1.5 py-0.5 rounded-sm"
|
|
229
|
-
style={{
|
|
230
|
-
color: c.bubbleCount >= 500 ? '#ef4444' : '#f59e0b',
|
|
231
|
-
background: c.bubbleCount >= 500 ? 'rgba(239,68,68,0.1)' : 'rgba(245,158,11,0.1)',
|
|
232
|
-
}}
|
|
233
|
-
title={`${c.bubbleCount} messages — large context may degrade AI performance`}
|
|
234
|
-
>
|
|
235
|
-
<AlertTriangle size={9} />
|
|
236
|
-
{c.bubbleCount >= 500 ? 'context bloat' : 'large context'}
|
|
237
|
-
</span>
|
|
238
|
-
)}
|
|
256
|
+
<td className="py-2 px-3 font-medium truncate max-w-[280px] text-[11px]" style={{ color: 'var(--c-white)' }}>
|
|
257
|
+
{c.name || <span style={{ color: 'var(--c-text3)' }}>Untitled</span>}
|
|
258
|
+
{c.encrypted && <span className="ml-1.5 text-[9px] text-yellow-500/60">locked</span>}
|
|
239
259
|
</td>
|
|
240
260
|
{!groupByProject && (
|
|
241
|
-
<td className="py-2
|
|
242
|
-
{c.folder ?
|
|
261
|
+
<td className="py-2 px-3 truncate max-w-[160px] text-[11px]" style={{ color: 'var(--c-text2)' }} title={c.folder}>
|
|
262
|
+
{c.folder ? (
|
|
263
|
+
<span
|
|
264
|
+
className="cursor-pointer hover:underline"
|
|
265
|
+
style={{ color: 'var(--c-accent)' }}
|
|
266
|
+
onClick={e => { e.stopPropagation(); navigate(`/projects/detail?folder=${encodeURIComponent(c.folder)}`) }}
|
|
267
|
+
>{c.folder.split('/').pop()}</span>
|
|
268
|
+
) : ''}
|
|
243
269
|
</td>
|
|
244
270
|
)}
|
|
245
|
-
<td className="py-2
|
|
246
|
-
|
|
247
|
-
</td>
|
|
248
|
-
<td className="py-2.5 px-4 text-xs truncate max-w-[180px] font-mono" style={{ color: 'var(--c-text2)' }} title={c.topModel || ''}>
|
|
271
|
+
<td className="py-2 px-3 text-[11px]" style={{ color: 'var(--c-text2)' }}>{c.mode || ''}</td>
|
|
272
|
+
<td className="py-2 px-3 text-[11px] font-mono truncate max-w-[150px]" style={{ color: 'var(--c-text2)' }} title={c.topModel || ''}>
|
|
249
273
|
{c.topModel || ''}
|
|
250
274
|
</td>
|
|
251
|
-
<td className="py-2
|
|
275
|
+
<td className="py-2 px-3 text-[11px]">
|
|
276
|
+
{c.bubbleCount >= 500 ? (
|
|
277
|
+
<span className="inline-flex items-center gap-0.5 font-bold" style={{ color: '#ef4444' }}>
|
|
278
|
+
<AlertTriangle size={9} />{c.bubbleCount}
|
|
279
|
+
</span>
|
|
280
|
+
) : c.bubbleCount >= 200 ? (
|
|
281
|
+
<span className="inline-flex items-center gap-0.5 font-bold" style={{ color: '#f59e0b' }}>
|
|
282
|
+
<AlertTriangle size={9} />{c.bubbleCount}
|
|
283
|
+
</span>
|
|
284
|
+
) : (
|
|
285
|
+
<span style={{ color: 'var(--c-text3)' }}>{c.bubbleCount || 0}</span>
|
|
286
|
+
)}
|
|
287
|
+
</td>
|
|
288
|
+
<td className="py-2 px-3 text-[11px] whitespace-nowrap" style={{ color: 'var(--c-text3)' }}>
|
|
252
289
|
{formatDate(c.lastUpdatedAt || c.createdAt)}
|
|
253
290
|
</td>
|
|
254
291
|
</tr>
|
|
@@ -256,14 +293,94 @@ export default function Sessions({ overview }) {
|
|
|
256
293
|
|
|
257
294
|
return (
|
|
258
295
|
<div className="fade-in space-y-4">
|
|
296
|
+
{/* KPIs */}
|
|
297
|
+
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))' }}>
|
|
298
|
+
<KpiCard label="sessions" value={formatNumber(filtered.length)} sub={filtered.length !== total ? `of ${formatNumber(total)}` : ''} />
|
|
299
|
+
<KpiCard label="projects" value={stats.projectCount} />
|
|
300
|
+
<KpiCard label="editors" value={stats.editorEntries.length} />
|
|
301
|
+
<KpiCard label="models" value={stats.modelEntries.length} />
|
|
302
|
+
{(stats.bloatCount + stats.largeCount) > 0 && (
|
|
303
|
+
<KpiCard label="large context" value={stats.bloatCount + stats.largeCount} sub={stats.bloatCount > 0 ? `${stats.bloatCount} bloated` : ''} />
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
{/* Summary charts */}
|
|
308
|
+
{filtered.length > 0 && (
|
|
309
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
|
|
310
|
+
<div className="card p-3">
|
|
311
|
+
<SectionTitle>editors</SectionTitle>
|
|
312
|
+
<div style={{ height: 140 }}>
|
|
313
|
+
<Doughnut
|
|
314
|
+
data={{
|
|
315
|
+
labels: stats.editorEntries.map(e => editorLabel(e[0])),
|
|
316
|
+
datasets: [{ data: stats.editorEntries.map(e => e[1]), backgroundColor: stats.editorEntries.map(e => editorColor(e[0])), borderWidth: 0 }],
|
|
317
|
+
}}
|
|
318
|
+
options={{
|
|
319
|
+
responsive: true, maintainAspectRatio: false, cutout: '60%',
|
|
320
|
+
plugins: {
|
|
321
|
+
legend: { position: 'right', labels: { color: legendColor, font: { size: 9, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
|
|
322
|
+
tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
|
|
323
|
+
},
|
|
324
|
+
}}
|
|
325
|
+
/>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
<div className="card p-3">
|
|
329
|
+
<SectionTitle>models</SectionTitle>
|
|
330
|
+
<div style={{ height: 140 }}>
|
|
331
|
+
{stats.modelEntries.length > 0 ? (
|
|
332
|
+
<Doughnut
|
|
333
|
+
data={{
|
|
334
|
+
labels: stats.modelEntries.map(m => m[0]),
|
|
335
|
+
datasets: [{ data: stats.modelEntries.map(m => m[1]), backgroundColor: MODEL_COLORS, borderWidth: 0 }],
|
|
336
|
+
}}
|
|
337
|
+
options={{
|
|
338
|
+
responsive: true, maintainAspectRatio: false, cutout: '60%',
|
|
339
|
+
plugins: {
|
|
340
|
+
legend: { position: 'right', labels: { color: legendColor, font: { size: 9, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
|
|
341
|
+
tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
|
|
342
|
+
},
|
|
343
|
+
}}
|
|
344
|
+
/>
|
|
345
|
+
) : <div className="text-[10px] py-8 text-center" style={{ color: 'var(--c-text3)' }}>no model data</div>}
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
<div className="card p-3">
|
|
349
|
+
<SectionTitle>modes</SectionTitle>
|
|
350
|
+
<div style={{ height: 140 }}>
|
|
351
|
+
{stats.modeEntries.length > 0 ? (
|
|
352
|
+
<Bar
|
|
353
|
+
data={{
|
|
354
|
+
labels: stats.modeEntries.map(m => m[0] || 'unknown'),
|
|
355
|
+
datasets: [{
|
|
356
|
+
data: stats.modeEntries.map(m => m[1]),
|
|
357
|
+
backgroundColor: '#6366f1',
|
|
358
|
+
borderRadius: 3,
|
|
359
|
+
}],
|
|
360
|
+
}}
|
|
361
|
+
options={{
|
|
362
|
+
responsive: true, maintainAspectRatio: false, indexAxis: 'y',
|
|
363
|
+
scales: {
|
|
364
|
+
x: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 8, family: MONO } } },
|
|
365
|
+
y: { grid: { display: false }, ticks: { color: legendColor, font: { size: 9, family: MONO } } },
|
|
366
|
+
},
|
|
367
|
+
plugins: { legend: { display: false }, tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } } },
|
|
368
|
+
}}
|
|
369
|
+
/>
|
|
370
|
+
) : <div className="text-[10px] py-8 text-center" style={{ color: 'var(--c-text3)' }}>no mode data</div>}
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
)}
|
|
375
|
+
|
|
259
376
|
{/* Timeline chart */}
|
|
260
377
|
{timelineChart && timelineChart.labels.length > 1 && (
|
|
261
|
-
<div className="card p-
|
|
378
|
+
<div className="card p-3">
|
|
262
379
|
<div className="flex items-center justify-between mb-2">
|
|
263
|
-
<
|
|
380
|
+
<SectionTitle>
|
|
264
381
|
session timeline
|
|
265
|
-
<span className="ml-2 font-normal" style={{ color: 'var(--c-text3)' }}>(drag to select range)</span>
|
|
266
|
-
</
|
|
382
|
+
<span className="ml-2 font-normal text-[9px]" style={{ color: 'var(--c-text3)' }}>(drag to select range)</span>
|
|
383
|
+
</SectionTitle>
|
|
267
384
|
{dateRange && (
|
|
268
385
|
<button
|
|
269
386
|
onClick={() => setDateRange(null)}
|
|
@@ -310,7 +427,7 @@ export default function Sessions({ overview }) {
|
|
|
310
427
|
className="pl-8 pr-3 py-1 text-[11px] outline-none appearance-none cursor-pointer"
|
|
311
428
|
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
312
429
|
>
|
|
313
|
-
<option value="">
|
|
430
|
+
<option value="">All Editors</option>
|
|
314
431
|
{editors.map(e => (
|
|
315
432
|
<option key={e.id} value={e.id}>{editorLabel(e.id)} ({e.count})</option>
|
|
316
433
|
))}
|
|
@@ -351,13 +468,14 @@ export default function Sessions({ overview }) {
|
|
|
351
468
|
<div className="card overflow-hidden">
|
|
352
469
|
<table className="w-full text-sm">
|
|
353
470
|
<thead>
|
|
354
|
-
<tr className="text-[
|
|
355
|
-
<th className="text-left py-2
|
|
356
|
-
<th className="text-left py-2
|
|
357
|
-
<th className="text-left py-2
|
|
358
|
-
<th className="text-left py-2
|
|
359
|
-
<th className="text-left py-2
|
|
360
|
-
<th className="text-left py-2
|
|
471
|
+
<tr className="text-[9px] uppercase tracking-wider" style={{ borderBottom: '1px solid var(--c-border)', color: 'var(--c-text3)' }}>
|
|
472
|
+
<th className="text-left py-2 px-3 font-medium">editor</th>
|
|
473
|
+
<th className="text-left py-2 px-3 font-medium">name</th>
|
|
474
|
+
<th className="text-left py-2 px-3 font-medium">project</th>
|
|
475
|
+
<th className="text-left py-2 px-3 font-medium">mode</th>
|
|
476
|
+
<th className="text-left py-2 px-3 font-medium">model</th>
|
|
477
|
+
<th className="text-left py-2 px-3 font-medium">context</th>
|
|
478
|
+
<th className="text-left py-2 px-3 font-medium">updated</th>
|
|
361
479
|
</tr>
|
|
362
480
|
</thead>
|
|
363
481
|
<tbody>
|