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.
@@ -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 EditorDot from '../components/EditorDot'
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.5 px-4">
221
- <EditorDot source={c.source} showLabel size={7} />
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.5 px-4 font-medium truncate max-w-[300px]" style={{ color: 'var(--c-white)' }}>
224
- {c.name || <span style={{ color: 'var(--c-text3)' }}>(untitled)</span>}
225
- {c.encrypted && <span className="ml-2 text-[10px] text-yellow-500/60">locked</span>}
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.5 px-4 truncate max-w-[200px] text-xs" style={{ color: 'var(--c-text2)' }} title={c.folder}>
242
- {c.folder ? c.folder.split('/').pop() : ''}
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.5 px-4">
246
- <span className="text-xs" style={{ color: 'var(--c-text2)' }}>{c.mode}</span>
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.5 px-4 text-xs whitespace-nowrap" style={{ color: 'var(--c-text2)' }}>
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-4">
378
+ <div className="card p-3">
262
379
  <div className="flex items-center justify-between mb-2">
263
- <h3 className="text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--c-text2)' }}>
380
+ <SectionTitle>
264
381
  session timeline
265
- <span className="ml-2 font-normal" style={{ color: 'var(--c-text3)' }}>(drag to select range)</span>
266
- </h3>
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="">all editors</option>
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-[10px] uppercase tracking-wider" style={{ borderBottom: '1px solid var(--c-border)', color: 'var(--c-text3)' }}>
355
- <th className="text-left py-2.5 px-4 font-medium">editor</th>
356
- <th className="text-left py-2.5 px-4 font-medium">name</th>
357
- <th className="text-left py-2.5 px-4 font-medium">project</th>
358
- <th className="text-left py-2.5 px-4 font-medium">mode</th>
359
- <th className="text-left py-2.5 px-4 font-medium">model</th>
360
- <th className="text-left py-2.5 px-4 font-medium">updated</th>
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>