agentlytics 0.0.7 → 0.0.10
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 +8 -2
- package/cache.js +67 -24
- package/editors/codex.js +453 -0
- package/editors/commandcode.js +159 -0
- package/editors/index.js +3 -1
- package/index.js +1 -1
- package/package.json +3 -2
- package/server.js +60 -4
- package/ui/src/App.jsx +40 -3
- package/ui/src/components/ActivityHeatmap.jsx +11 -7
- package/ui/src/components/ChatSidebar.jsx +313 -0
- package/ui/src/components/DateRangePicker.jsx +104 -0
- package/ui/src/index.css +6 -0
- package/ui/src/lib/api.js +17 -3
- package/ui/src/lib/constants.js +16 -0
- package/ui/src/pages/ChatDetail.jsx +13 -3
- package/ui/src/pages/Dashboard.jsx +14 -14
- package/ui/src/pages/DeepAnalysis.jsx +6 -3
- package/ui/src/pages/ProjectDetail.jsx +236 -0
- package/ui/src/pages/Projects.jsx +50 -121
- package/ui/src/pages/Sessions.jsx +58 -46
|
@@ -3,9 +3,10 @@ import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearSca
|
|
|
3
3
|
import { Doughnut, Bar } from 'react-chartjs-2'
|
|
4
4
|
import { Loader2, X } from 'lucide-react'
|
|
5
5
|
import { fetchDeepAnalytics, fetchToolCalls } from '../lib/api'
|
|
6
|
-
import { editorLabel, editorColor, formatNumber, formatDateTime } from '../lib/constants'
|
|
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 DateRangePicker from '../components/DateRangePicker'
|
|
9
10
|
|
|
10
11
|
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
|
|
11
12
|
|
|
@@ -138,6 +139,7 @@ function ToolDrillDown({ toolName, folder, onClose }) {
|
|
|
138
139
|
export default function DeepAnalysis({ overview }) {
|
|
139
140
|
const [editor, setEditor] = useState('')
|
|
140
141
|
const [folder, setFolder] = useState('')
|
|
142
|
+
const [dateRange, setDateRange] = useState(null)
|
|
141
143
|
const [data, setData] = useState(null)
|
|
142
144
|
const [loading, setLoading] = useState(false)
|
|
143
145
|
const [selectedTool, setSelectedTool] = useState(null)
|
|
@@ -153,12 +155,12 @@ export default function DeepAnalysis({ overview }) {
|
|
|
153
155
|
|
|
154
156
|
async function analyze() {
|
|
155
157
|
setLoading(true)
|
|
156
|
-
const result = await fetchDeepAnalytics({ editor, folder: folder || undefined, limit: 500 })
|
|
158
|
+
const result = await fetchDeepAnalytics({ editor, folder: folder || undefined, limit: 500, ...dateRangeToApiParams(dateRange) })
|
|
157
159
|
setData(result)
|
|
158
160
|
setLoading(false)
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
useEffect(() => { analyze() }, [editor, folder])
|
|
163
|
+
useEffect(() => { analyze() }, [editor, folder, dateRange])
|
|
162
164
|
|
|
163
165
|
const tools = data?.topTools?.slice(0, 15) || []
|
|
164
166
|
const models = data?.topModels?.slice(0, 10) || []
|
|
@@ -200,6 +202,7 @@ export default function DeepAnalysis({ overview }) {
|
|
|
200
202
|
<Loader2 size={11} className="animate-spin" style={{ color: 'var(--c-text3)' }} />
|
|
201
203
|
)}
|
|
202
204
|
{data && <span className="text-[10px]" style={{ color: 'var(--c-text2)' }}>{data.analyzedChats} sessions</span>}
|
|
205
|
+
<div className="ml-auto"><DateRangePicker value={dateRange} onChange={setDateRange} /></div>
|
|
203
206
|
</div>
|
|
204
207
|
|
|
205
208
|
{data && (
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react'
|
|
2
|
+
import { useSearchParams, useNavigate } from 'react-router-dom'
|
|
3
|
+
import { ArrowLeft, Search } from 'lucide-react'
|
|
4
|
+
import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement } from 'chart.js'
|
|
5
|
+
import { Doughnut, Bar } from 'react-chartjs-2'
|
|
6
|
+
import { fetchProjects, fetchChats } from '../lib/api'
|
|
7
|
+
import { editorColor, editorLabel, formatNumber, formatDate } from '../lib/constants'
|
|
8
|
+
import { useTheme } from '../lib/theme'
|
|
9
|
+
import KpiCard from '../components/KpiCard'
|
|
10
|
+
import EditorDot from '../components/EditorDot'
|
|
11
|
+
import ChatSidebar from '../components/ChatSidebar'
|
|
12
|
+
|
|
13
|
+
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
|
|
14
|
+
|
|
15
|
+
const MONO = 'JetBrains Mono, monospace'
|
|
16
|
+
const MODEL_COLORS = ['#6366f1', '#a78bfa', '#818cf8', '#c084fc', '#e879f9', '#f472b6', '#fb7185', '#f87171', '#fbbf24', '#34d399']
|
|
17
|
+
|
|
18
|
+
export default function ProjectDetail() {
|
|
19
|
+
const [searchParams] = useSearchParams()
|
|
20
|
+
const navigate = useNavigate()
|
|
21
|
+
const folder = searchParams.get('folder')
|
|
22
|
+
const { dark } = useTheme()
|
|
23
|
+
const txtColor = dark ? '#888' : '#555'
|
|
24
|
+
const txtDim = dark ? '#555' : '#999'
|
|
25
|
+
const gridColor = dark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.06)'
|
|
26
|
+
|
|
27
|
+
const [project, setProject] = useState(null)
|
|
28
|
+
const [chats, setChats] = useState([])
|
|
29
|
+
const [loading, setLoading] = useState(true)
|
|
30
|
+
const [chatSearch, setChatSearch] = useState('')
|
|
31
|
+
const [selectedChatId, setSelectedChatId] = useState(null)
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!folder) return
|
|
35
|
+
setLoading(true)
|
|
36
|
+
Promise.all([
|
|
37
|
+
fetchProjects(),
|
|
38
|
+
fetchChats({ folder, limit: 1000 }),
|
|
39
|
+
]).then(([projects, chatData]) => {
|
|
40
|
+
const match = projects.find(p => p.folder === folder)
|
|
41
|
+
setProject(match || null)
|
|
42
|
+
setChats(chatData.chats || [])
|
|
43
|
+
setLoading(false)
|
|
44
|
+
})
|
|
45
|
+
}, [folder])
|
|
46
|
+
|
|
47
|
+
const filteredChats = useMemo(() => {
|
|
48
|
+
if (!chatSearch) return chats
|
|
49
|
+
const q = chatSearch.toLowerCase()
|
|
50
|
+
return chats.filter(c =>
|
|
51
|
+
(c.name && c.name.toLowerCase().includes(q)) ||
|
|
52
|
+
(c.topModel && c.topModel.toLowerCase().includes(q)) ||
|
|
53
|
+
(c.source && c.source.toLowerCase().includes(q))
|
|
54
|
+
)
|
|
55
|
+
}, [chats, chatSearch])
|
|
56
|
+
|
|
57
|
+
if (!folder) return <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text3)' }}>no project specified</div>
|
|
58
|
+
if (loading) return <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text2)' }}>loading project...</div>
|
|
59
|
+
if (!project) return <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text3)' }}>project not found</div>
|
|
60
|
+
|
|
61
|
+
const editorEntries = Object.entries(project.editors).sort((a, b) => b[1] - a[1])
|
|
62
|
+
const maxEditorCount = editorEntries.length > 0 ? editorEntries[0][1] : 1
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="fade-in space-y-5">
|
|
66
|
+
{/* Back + title */}
|
|
67
|
+
<div className="flex items-center gap-3">
|
|
68
|
+
<button
|
|
69
|
+
onClick={() => navigate('/projects')}
|
|
70
|
+
className="flex items-center gap-1.5 text-xs transition"
|
|
71
|
+
style={{ color: 'var(--c-text2)' }}
|
|
72
|
+
>
|
|
73
|
+
<ArrowLeft size={14} /> Projects
|
|
74
|
+
</button>
|
|
75
|
+
<div className="flex-1 min-w-0">
|
|
76
|
+
<h1 className="text-base font-semibold truncate" style={{ color: 'var(--c-white)' }}>{project.name}</h1>
|
|
77
|
+
<div className="text-[10px] truncate" style={{ color: 'var(--c-text3)' }}>{project.folder}</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{/* KPIs */}
|
|
82
|
+
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
|
83
|
+
<KpiCard label="sessions" value={project.totalSessions} />
|
|
84
|
+
<KpiCard label="messages" value={formatNumber(project.totalMessages)} />
|
|
85
|
+
<KpiCard label="tool calls" value={formatNumber(project.totalToolCalls)} />
|
|
86
|
+
<KpiCard label="input tokens" value={formatNumber(project.totalInputTokens)} />
|
|
87
|
+
<KpiCard label="output tokens" value={formatNumber(project.totalOutputTokens)} />
|
|
88
|
+
<KpiCard label="active since" value={formatDate(project.firstSeen)} />
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Charts row */}
|
|
92
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
|
93
|
+
{/* Editors */}
|
|
94
|
+
<div className="card p-4">
|
|
95
|
+
<h3 className="text-[10px] uppercase tracking-wider mb-3" style={{ color: 'var(--c-text2)' }}>editors</h3>
|
|
96
|
+
<div className="space-y-2">
|
|
97
|
+
{editorEntries.map(([e, c]) => (
|
|
98
|
+
<div key={e} className="flex items-center gap-2">
|
|
99
|
+
<span className="w-2 h-2 rounded-full flex-shrink-0" style={{ background: editorColor(e) }} />
|
|
100
|
+
<span className="text-xs flex-1 truncate" style={{ color: 'var(--c-text2)' }}>{editorLabel(e)}</span>
|
|
101
|
+
<div className="w-20 h-3 overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
|
|
102
|
+
<div className="h-full" style={{ width: `${(c / maxEditorCount * 100).toFixed(0)}%`, background: editorColor(e) + '60' }} />
|
|
103
|
+
</div>
|
|
104
|
+
<span className="text-[10px] w-6 text-right" style={{ color: 'var(--c-text3)' }}>{c}</span>
|
|
105
|
+
</div>
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{/* Models chart */}
|
|
111
|
+
<div className="card p-4">
|
|
112
|
+
<h3 className="text-[10px] uppercase tracking-wider mb-3" style={{ color: 'var(--c-text2)' }}>models</h3>
|
|
113
|
+
{project.topModels.length > 0 ? (
|
|
114
|
+
<div style={{ height: 180 }}>
|
|
115
|
+
<Doughnut
|
|
116
|
+
data={{
|
|
117
|
+
labels: project.topModels.map(m => m.name),
|
|
118
|
+
datasets: [{ data: project.topModels.map(m => m.count), backgroundColor: MODEL_COLORS, borderWidth: 0 }],
|
|
119
|
+
}}
|
|
120
|
+
options={{
|
|
121
|
+
responsive: true, maintainAspectRatio: false, cutout: '55%',
|
|
122
|
+
plugins: {
|
|
123
|
+
legend: { position: 'right', labels: { color: txtColor, font: { size: 8, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
|
|
124
|
+
tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
|
|
125
|
+
},
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
) : <div className="text-[10px] py-8 text-center" style={{ color: 'var(--c-text3)' }}>no model data</div>}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Top tools */}
|
|
133
|
+
<div className="card p-4">
|
|
134
|
+
<h3 className="text-[10px] uppercase tracking-wider mb-3" style={{ color: 'var(--c-text2)' }}>top tools</h3>
|
|
135
|
+
{project.topTools.length > 0 ? (
|
|
136
|
+
<div style={{ height: 180 }}>
|
|
137
|
+
<Bar
|
|
138
|
+
data={{
|
|
139
|
+
labels: project.topTools.map(t => t.name),
|
|
140
|
+
datasets: [{
|
|
141
|
+
data: project.topTools.map(t => t.count),
|
|
142
|
+
backgroundColor: 'rgba(99,102,241,0.4)',
|
|
143
|
+
borderRadius: 2,
|
|
144
|
+
}],
|
|
145
|
+
}}
|
|
146
|
+
options={{
|
|
147
|
+
responsive: true, maintainAspectRatio: false, indexAxis: 'y',
|
|
148
|
+
scales: {
|
|
149
|
+
x: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 8, family: MONO } } },
|
|
150
|
+
y: { grid: { display: false }, ticks: { color: txtColor, font: { size: 8, family: MONO } } },
|
|
151
|
+
},
|
|
152
|
+
plugins: { legend: { display: false }, tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } } },
|
|
153
|
+
}}
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
) : <div className="text-[10px] py-8 text-center" style={{ color: 'var(--c-text3)' }}>no tool data</div>}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{/* Token breakdown */}
|
|
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 */}
|
|
169
|
+
<div>
|
|
170
|
+
<div className="flex items-center gap-3 mb-3">
|
|
171
|
+
<h3 className="text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--c-text2)' }}>sessions ({chats.length})</h3>
|
|
172
|
+
<div className="relative max-w-xs flex-1">
|
|
173
|
+
<Search size={11} className="absolute left-2.5 top-1/2 -translate-y-1/2" style={{ color: 'var(--c-text3)' }} />
|
|
174
|
+
<input
|
|
175
|
+
type="text"
|
|
176
|
+
placeholder="filter sessions..."
|
|
177
|
+
value={chatSearch}
|
|
178
|
+
onChange={e => setChatSearch(e.target.value)}
|
|
179
|
+
className="w-full pl-7 pr-3 py-1 text-[11px] outline-none"
|
|
180
|
+
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div className="card overflow-hidden">
|
|
186
|
+
<table className="w-full text-sm">
|
|
187
|
+
<thead>
|
|
188
|
+
<tr className="text-[10px] uppercase tracking-wider" style={{ borderBottom: '1px solid var(--c-border)', color: 'var(--c-text3)' }}>
|
|
189
|
+
<th className="text-left py-2.5 px-4 font-medium">editor</th>
|
|
190
|
+
<th className="text-left py-2.5 px-4 font-medium">name</th>
|
|
191
|
+
<th className="text-left py-2.5 px-4 font-medium">mode</th>
|
|
192
|
+
<th className="text-left py-2.5 px-4 font-medium">model</th>
|
|
193
|
+
<th className="text-left py-2.5 px-4 font-medium">updated</th>
|
|
194
|
+
</tr>
|
|
195
|
+
</thead>
|
|
196
|
+
<tbody>
|
|
197
|
+
{filteredChats.map(c => (
|
|
198
|
+
<tr
|
|
199
|
+
key={c.id}
|
|
200
|
+
className="cursor-pointer transition"
|
|
201
|
+
style={{ borderBottom: '1px solid var(--c-border)' }}
|
|
202
|
+
onMouseEnter={e => e.currentTarget.style.background = 'var(--c-bg3)'}
|
|
203
|
+
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
|
204
|
+
onClick={() => setSelectedChatId(c.id)}
|
|
205
|
+
>
|
|
206
|
+
<td className="py-2.5 px-4">
|
|
207
|
+
<EditorDot source={c.source} showLabel size={7} />
|
|
208
|
+
</td>
|
|
209
|
+
<td className="py-2.5 px-4 font-medium truncate max-w-[300px]" style={{ color: 'var(--c-white)' }}>
|
|
210
|
+
{c.name || <span style={{ color: 'var(--c-text3)' }}>(untitled)</span>}
|
|
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>
|
|
215
|
+
</td>
|
|
216
|
+
<td className="py-2.5 px-4 text-xs truncate max-w-[180px] font-mono" style={{ color: 'var(--c-text2)' }} title={c.topModel || ''}>
|
|
217
|
+
{c.topModel || ''}
|
|
218
|
+
</td>
|
|
219
|
+
<td className="py-2.5 px-4 text-xs whitespace-nowrap" style={{ color: 'var(--c-text2)' }}>
|
|
220
|
+
{formatDate(c.lastUpdatedAt || c.createdAt)}
|
|
221
|
+
</td>
|
|
222
|
+
</tr>
|
|
223
|
+
))}
|
|
224
|
+
</tbody>
|
|
225
|
+
</table>
|
|
226
|
+
{filteredChats.length === 0 && (
|
|
227
|
+
<div className="text-center py-8 text-sm" style={{ color: 'var(--c-text3)' }}>no sessions found</div>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* Chat sidebar */}
|
|
233
|
+
<ChatSidebar chatId={selectedChatId} onClose={() => setSelectedChatId(null)} />
|
|
234
|
+
</div>
|
|
235
|
+
)
|
|
236
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
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, ChevronRight } from 'lucide-react'
|
|
5
|
+
import { useNavigate } from 'react-router-dom'
|
|
5
6
|
import { fetchProjects } from '../lib/api'
|
|
6
|
-
import { editorColor, editorLabel, formatNumber, formatDate } from '../lib/constants'
|
|
7
|
+
import { editorColor, editorLabel, formatNumber, formatDate, dateRangeToApiParams } from '../lib/constants'
|
|
7
8
|
import { useTheme } from '../lib/theme'
|
|
8
9
|
import KpiCard from '../components/KpiCard'
|
|
10
|
+
import DateRangePicker from '../components/DateRangePicker'
|
|
9
11
|
|
|
10
12
|
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
|
|
11
13
|
|
|
@@ -20,12 +22,13 @@ export default function Projects({ overview }) {
|
|
|
20
22
|
const [projects, setProjects] = useState(null)
|
|
21
23
|
const [search, setSearch] = useState('')
|
|
22
24
|
const [editorFilter, setEditorFilter] = useState('')
|
|
23
|
-
const [
|
|
25
|
+
const [dateRange, setDateRange] = useState(null)
|
|
26
|
+
const navigate = useNavigate()
|
|
24
27
|
const editors = overview?.editors || []
|
|
25
28
|
|
|
26
29
|
useEffect(() => {
|
|
27
|
-
fetchProjects().then(setProjects)
|
|
28
|
-
}, [])
|
|
30
|
+
fetchProjects(dateRangeToApiParams(dateRange)).then(setProjects)
|
|
31
|
+
}, [dateRange])
|
|
29
32
|
|
|
30
33
|
if (!projects) return <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text2)' }}>loading projects...</div>
|
|
31
34
|
|
|
@@ -109,133 +112,59 @@ export default function Projects({ overview }) {
|
|
|
109
112
|
|
|
110
113
|
{/* Filters */}
|
|
111
114
|
<div className="flex items-center gap-2">
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
117
|
-
>
|
|
118
|
-
<option value="">All Editors</option>
|
|
119
|
-
{editors.map(e => (
|
|
120
|
-
<option key={e.id} value={e.id}>{editorLabel(e.id)}</option>
|
|
121
|
-
))}
|
|
122
|
-
</select>
|
|
123
|
-
<div className="relative max-w-sm flex-1">
|
|
124
|
-
<Search size={13} className="absolute left-3 top-1/2 -translate-y-1/2" style={{ color: 'var(--c-text3)' }} />
|
|
125
|
-
<input
|
|
126
|
-
type="text"
|
|
127
|
-
placeholder="search projects..."
|
|
128
|
-
value={search}
|
|
129
|
-
onChange={e => setSearch(e.target.value)}
|
|
130
|
-
className="w-full pl-8 pr-3 py-2 text-sm outline-none"
|
|
115
|
+
<select
|
|
116
|
+
value={editorFilter}
|
|
117
|
+
onChange={e => setEditorFilter(e.target.value)}
|
|
118
|
+
className="px-2 py-1 text-[11px] outline-none"
|
|
131
119
|
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
132
|
-
|
|
133
|
-
|
|
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>
|
|
137
|
+
<div className="ml-auto"><DateRangePicker value={dateRange} onChange={setDateRange} /></div>
|
|
134
138
|
</div>
|
|
135
139
|
|
|
136
140
|
{/* Project list */}
|
|
137
141
|
<div className="space-y-2">
|
|
138
142
|
{filtered.map(p => {
|
|
139
|
-
const isOpen = expanded === p.folder
|
|
140
143
|
const editorEntries = Object.entries(p.editors).sort((a, b) => b[1] - a[1])
|
|
141
|
-
const maxEditorCount = editorEntries.length > 0 ? editorEntries[0][1] : 1
|
|
142
144
|
|
|
143
145
|
return (
|
|
144
|
-
<div
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
>
|
|
150
|
-
<div className="
|
|
151
|
-
|
|
152
|
-
<div className="text-[10px] truncate" style={{ color: 'var(--c-text3)' }}>{p.folder}</div>
|
|
153
|
-
</div>
|
|
154
|
-
<div className="flex items-center gap-4 text-[10px] flex-shrink-0" style={{ color: 'var(--c-text2)' }}>
|
|
155
|
-
<div className="flex items-center gap-1.5">
|
|
156
|
-
{editorEntries.slice(0, 4).map(([e]) => (
|
|
157
|
-
<span key={e} className="w-2 h-2 rounded-full" style={{ background: editorColor(e) }} title={editorLabel(e)} />
|
|
158
|
-
))}
|
|
159
|
-
</div>
|
|
160
|
-
<span>{p.totalSessions} sessions</span>
|
|
161
|
-
<span>{formatNumber(p.totalMessages)} msgs</span>
|
|
162
|
-
{p.totalToolCalls > 0 && <span>{formatNumber(p.totalToolCalls)} tools</span>}
|
|
163
|
-
{(p.totalInputTokens + p.totalOutputTokens) > 0 && <span>{formatNumber(p.totalInputTokens + p.totalOutputTokens)} tokens</span>}
|
|
164
|
-
<span>{formatDate(p.lastSeen)}</span>
|
|
165
|
-
{isOpen ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
|
166
|
-
</div>
|
|
146
|
+
<div
|
|
147
|
+
key={p.folder}
|
|
148
|
+
className="card px-4 py-3 flex items-center gap-3 cursor-pointer transition"
|
|
149
|
+
onClick={() => navigate(`/projects/detail?folder=${encodeURIComponent(p.folder)}`)}
|
|
150
|
+
>
|
|
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>
|
|
167
154
|
</div>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
<KpiCard label="sessions" value={p.totalSessions} />
|
|
174
|
-
<KpiCard label="messages" value={formatNumber(p.totalMessages)} />
|
|
175
|
-
<KpiCard label="tool calls" value={formatNumber(p.totalToolCalls)} />
|
|
176
|
-
<KpiCard label="input tokens" value={formatNumber(p.totalInputTokens)} />
|
|
177
|
-
<KpiCard label="output tokens" value={formatNumber(p.totalOutputTokens)} />
|
|
178
|
-
<KpiCard label="active since" value={formatDate(p.firstSeen)} />
|
|
179
|
-
</div>
|
|
180
|
-
|
|
181
|
-
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
|
182
|
-
{/* Editors */}
|
|
183
|
-
<div>
|
|
184
|
-
<h4 className="text-[10px] uppercase tracking-wider mb-2" style={{ color: 'var(--c-text2)' }}>editors</h4>
|
|
185
|
-
<div className="space-y-1.5">
|
|
186
|
-
{editorEntries.map(([e, c]) => (
|
|
187
|
-
<div key={e} className="flex items-center gap-2">
|
|
188
|
-
<span className="w-2 h-2 rounded-full flex-shrink-0" style={{ background: editorColor(e) }} />
|
|
189
|
-
<span className="text-xs flex-1 truncate" style={{ color: 'var(--c-text2)' }}>{editorLabel(e)}</span>
|
|
190
|
-
<div className="w-24 h-3 rounded-sm overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
|
|
191
|
-
<div className="h-full rounded-sm" style={{ width: `${(c / maxEditorCount * 100).toFixed(0)}%`, background: editorColor(e) + '60' }} />
|
|
192
|
-
</div>
|
|
193
|
-
<span className="text-[10px] w-6 text-right" style={{ color: 'var(--c-text3)' }}>{c}</span>
|
|
194
|
-
</div>
|
|
195
|
-
))}
|
|
196
|
-
</div>
|
|
197
|
-
</div>
|
|
198
|
-
|
|
199
|
-
{/* Models */}
|
|
200
|
-
<div>
|
|
201
|
-
<h4 className="text-[10px] uppercase tracking-wider mb-2" style={{ color: 'var(--c-text2)' }}>models</h4>
|
|
202
|
-
{p.topModels.length > 0 ? (
|
|
203
|
-
<div className="space-y-1.5">
|
|
204
|
-
{p.topModels.map(m => (
|
|
205
|
-
<div key={m.name} className="flex justify-between text-xs py-0.5">
|
|
206
|
-
<span className="truncate" style={{ color: 'var(--c-text2)' }}>{m.name}</span>
|
|
207
|
-
<span className="ml-2" style={{ color: 'var(--c-text3)' }}>{m.count}</span>
|
|
208
|
-
</div>
|
|
209
|
-
))}
|
|
210
|
-
</div>
|
|
211
|
-
) : <div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>no model data</div>}
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
{/* Tools */}
|
|
215
|
-
<div>
|
|
216
|
-
<h4 className="text-[10px] uppercase tracking-wider mb-2" style={{ color: 'var(--c-text2)' }}>top tools</h4>
|
|
217
|
-
{p.topTools.length > 0 ? (
|
|
218
|
-
<div className="space-y-1.5">
|
|
219
|
-
{p.topTools.map(t => (
|
|
220
|
-
<div key={t.name} className="flex justify-between text-xs py-0.5">
|
|
221
|
-
<span className="truncate" style={{ color: 'var(--c-text2)' }}>{t.name}</span>
|
|
222
|
-
<span className="ml-2" style={{ color: 'var(--c-text3)' }}>{t.count}</span>
|
|
223
|
-
</div>
|
|
224
|
-
))}
|
|
225
|
-
</div>
|
|
226
|
-
) : <div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>no tool data</div>}
|
|
227
|
-
</div>
|
|
228
|
-
</div>
|
|
229
|
-
|
|
230
|
-
{/* Token breakdown */}
|
|
231
|
-
{(p.totalCacheRead > 0 || p.totalCacheWrite > 0) && (
|
|
232
|
-
<div className="mt-3 flex gap-4 text-[10px]" style={{ color: 'var(--c-text3)' }}>
|
|
233
|
-
<span>cache read: {formatNumber(p.totalCacheRead)}</span>
|
|
234
|
-
<span>cache write: {formatNumber(p.totalCacheWrite)}</span>
|
|
235
|
-
</div>
|
|
236
|
-
)}
|
|
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">
|
|
157
|
+
{editorEntries.slice(0, 4).map(([e]) => (
|
|
158
|
+
<span key={e} className="w-2 h-2 rounded-full" style={{ background: editorColor(e) }} title={editorLabel(e)} />
|
|
159
|
+
))}
|
|
237
160
|
</div>
|
|
238
|
-
|
|
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} />
|
|
167
|
+
</div>
|
|
239
168
|
</div>
|
|
240
169
|
)
|
|
241
170
|
})}
|