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,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, ChevronRight } from 'lucide-react'
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 totalTools = projects.reduce((s, p) => s + p.totalToolCalls, 0)
46
+ const maxSessions = Math.max(...projects.map(p => p.totalSessions), 1)
45
47
 
46
- // Aggregate model usage across all projects
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, 10)
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-5">
60
+ <div className="fade-in space-y-4">
58
61
  {/* KPIs */}
59
- <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
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="total sessions" value={formatNumber(totalSessions)} />
62
- <KpiCard label="total messages" value={formatNumber(totalMessages)} />
63
- <KpiCard label="total tokens" value={formatNumber(totalTokens)} />
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
- {/* Global model & editor breakdown */}
67
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
68
- <div className="card p-5">
69
- <h3 className="text-xs font-medium uppercase tracking-wider mb-3" style={{ color: 'var(--c-text2)' }}>models across projects</h3>
70
- <div style={{ height: 240 }}>
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: '55%',
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: 8 } },
81
- tooltip: { bodyFont: { family: MONO, size: 11 }, titleFont: { family: MONO, size: 11 } },
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
- <select
116
- value={editorFilter}
117
- onChange={e => setEditorFilter(e.target.value)}
118
- className="px-2 py-1 text-[11px] outline-none"
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
- <option value="">All Editors</option>
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 list */}
141
- <div className="space-y-2">
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 px-4 py-3 flex items-center gap-3 cursor-pointer transition"
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
- <div className="flex-1 min-w-0">
152
- <div className="text-sm font-medium truncate" style={{ color: 'var(--c-white)' }}>{p.name}</div>
153
- <div className="text-[10px] truncate" style={{ color: 'var(--c-text3)' }}>{p.folder}</div>
154
- </div>
155
- <div className="flex items-center gap-4 text-[10px] flex-shrink-0" style={{ color: 'var(--c-text2)' }}>
156
- <div className="flex items-center gap-1.5">
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
- <span key={e} className="w-2 h-2 rounded-full" style={{ background: editorColor(e) }} title={editorLabel(e)} />
183
+ <EditorIcon key={e} source={e} size={12} />
159
184
  ))}
160
185
  </div>
161
- <span>{p.totalSessions} sessions</span>
162
- <span>{formatNumber(p.totalMessages)} msgs</span>
163
- {p.totalToolCalls > 0 && <span>{formatNumber(p.totalToolCalls)} tools</span>}
164
- {(p.totalInputTokens + p.totalOutputTokens) > 0 && <span>{formatNumber(p.totalInputTokens + p.totalOutputTokens)} tokens</span>}
165
- <span>{formatDate(p.lastSeen)}</span>
166
- <ChevronRight size={14} />
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
  )