agentlytics 0.1.5 → 0.1.7
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 +1 -1
- package/cache.js +440 -9
- package/editors/cursor.js +28 -6
- package/editors/vscode.js +6 -0
- package/package.json +1 -1
- package/server.js +84 -4
- package/ui/package-lock.json +60 -375
- package/ui/package.json +1 -1
- package/ui/src/App.jsx +14 -1
- package/ui/src/lib/api.js +39 -0
- package/ui/src/lib/constants.js +8 -0
- package/ui/src/pages/ChatDetail.jsx +5 -2
- package/ui/src/pages/CostAnalysis.jsx +356 -0
- package/ui/src/pages/Dashboard.jsx +29 -8
- package/ui/src/pages/DeepAnalysis.jsx +11 -4
- package/ui/src/pages/ProjectDetail.jsx +12 -4
- package/ui/src/pages/Projects.jsx +9 -3
- package/ui/src/pages/Sessions.jsx +5 -1
- package/ui/src/pages/Settings.jsx +142 -0
|
@@ -3,8 +3,8 @@ import { useSearchParams, useNavigate } from 'react-router-dom'
|
|
|
3
3
|
import { ArrowLeft, Search, FolderOpen, Calendar, MessageSquare, Wrench, Cpu, Zap, AlertTriangle } from 'lucide-react'
|
|
4
4
|
import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement } from 'chart.js'
|
|
5
5
|
import { Doughnut, Bar } from 'react-chartjs-2'
|
|
6
|
-
import { fetchProjects, fetchChats } from '../lib/api'
|
|
7
|
-
import { editorColor, editorLabel, formatNumber, formatDate } from '../lib/constants'
|
|
6
|
+
import { fetchProjects, fetchChats, fetchCosts } from '../lib/api'
|
|
7
|
+
import { editorColor, editorLabel, formatNumber, formatDate, formatCost } from '../lib/constants'
|
|
8
8
|
import { useTheme } from '../lib/theme'
|
|
9
9
|
import KpiCard from '../components/KpiCard'
|
|
10
10
|
import EditorIcon from '../components/EditorIcon'
|
|
@@ -31,6 +31,7 @@ export default function ProjectDetail() {
|
|
|
31
31
|
const [loading, setLoading] = useState(true)
|
|
32
32
|
const [chatSearch, setChatSearch] = useState('')
|
|
33
33
|
const [selectedChatId, setSelectedChatId] = useState(null)
|
|
34
|
+
const [costs, setCosts] = useState(null)
|
|
34
35
|
const [enabledEditors, setEnabledEditors] = useState(null)
|
|
35
36
|
|
|
36
37
|
useEffect(() => {
|
|
@@ -39,10 +40,12 @@ export default function ProjectDetail() {
|
|
|
39
40
|
Promise.all([
|
|
40
41
|
fetchProjects(),
|
|
41
42
|
fetchChats({ folder, limit: 1000 }),
|
|
42
|
-
|
|
43
|
+
fetchCosts({ folder }),
|
|
44
|
+
]).then(([projects, chatData, costData]) => {
|
|
43
45
|
const match = projects.find(p => p.folder === folder)
|
|
44
46
|
setProject(match || null)
|
|
45
47
|
setChats(chatData.chats || [])
|
|
48
|
+
setCosts(costData)
|
|
46
49
|
if (match) setEnabledEditors(new Set(Object.keys(match.editors)))
|
|
47
50
|
setLoading(false)
|
|
48
51
|
})
|
|
@@ -144,11 +147,12 @@ export default function ProjectDetail() {
|
|
|
144
147
|
<KpiCard label="sessions" value={displaySessions} sub={!allEnabled ? 'filtered' : ''} />
|
|
145
148
|
<KpiCard label="messages" value={formatNumber(project.totalMessages)} sub={`${avgMsgs} avg/session`} />
|
|
146
149
|
<KpiCard label="tool calls" value={formatNumber(project.totalToolCalls)} sub={<span className="flex items-center gap-0.5"><Wrench size={8} /> invocations</span>} />
|
|
147
|
-
<KpiCard label="tokens" value={formatNumber(totalTok)} sub={`${outputRatio}
|
|
150
|
+
<KpiCard label="tokens" value={formatNumber(totalTok)} sub={`${outputRatio}\u00d7 out/in`} />
|
|
148
151
|
{project.totalCacheRead > 0 && (
|
|
149
152
|
<KpiCard label="cache read" value={formatNumber(project.totalCacheRead)} sub={`write: ${formatNumber(project.totalCacheWrite)}`} />
|
|
150
153
|
)}
|
|
151
154
|
<KpiCard label="you wrote" value={formatNumber(project.totalUserChars)} sub={`AI: ${formatNumber(project.totalAssistantChars)}`} />
|
|
155
|
+
<KpiCard label="est. cost" value={costs && costs.totalCost > 0 ? formatCost(costs.totalCost) : '\u2014'} sub={costs && costs.byModel.length > 0 ? `${costs.byModel.length} model${costs.byModel.length !== 1 ? 's' : ''}` : undefined} />
|
|
152
156
|
</div>
|
|
153
157
|
|
|
154
158
|
{/* Charts row */}
|
|
@@ -256,6 +260,7 @@ export default function ProjectDetail() {
|
|
|
256
260
|
<th className="text-left py-2 px-3 font-medium">mode</th>
|
|
257
261
|
<th className="text-left py-2 px-3 font-medium">model</th>
|
|
258
262
|
<th className="text-left py-2 px-3 font-medium">context</th>
|
|
263
|
+
<th className="text-right py-2 px-3 font-medium">est. cost</th>
|
|
259
264
|
<th className="text-left py-2 px-3 font-medium">updated</th>
|
|
260
265
|
</tr>
|
|
261
266
|
</thead>
|
|
@@ -294,6 +299,9 @@ export default function ProjectDetail() {
|
|
|
294
299
|
<span style={{ color: 'var(--c-text3)' }}>{c.bubbleCount || 0} msgs</span>
|
|
295
300
|
)}
|
|
296
301
|
</td>
|
|
302
|
+
<td className="py-2 px-3 font-mono text-right" style={{ color: c.cost > 0 ? 'var(--c-text2)' : 'var(--c-text3)' }}>
|
|
303
|
+
{c.cost > 0 ? formatCost(c.cost) : ''}
|
|
304
|
+
</td>
|
|
297
305
|
<td className="py-2 px-3 whitespace-nowrap" style={{ color: 'var(--c-text3)' }}>{formatDate(c.lastUpdatedAt || c.createdAt)}</td>
|
|
298
306
|
</tr>
|
|
299
307
|
))}
|
|
@@ -3,8 +3,8 @@ import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearSca
|
|
|
3
3
|
import { Doughnut, Bar } from 'react-chartjs-2'
|
|
4
4
|
import { Search, MessageSquare, Wrench, Cpu, FolderOpen, Calendar, ArrowRight } from 'lucide-react'
|
|
5
5
|
import { useNavigate } from 'react-router-dom'
|
|
6
|
-
import { fetchProjects } from '../lib/api'
|
|
7
|
-
import { editorColor, editorLabel, formatNumber, formatDate, dateRangeToApiParams } from '../lib/constants'
|
|
6
|
+
import { fetchProjects, fetchCosts } from '../lib/api'
|
|
7
|
+
import { editorColor, editorLabel, formatNumber, formatCost, formatDate, dateRangeToApiParams } from '../lib/constants'
|
|
8
8
|
import { useTheme } from '../lib/theme'
|
|
9
9
|
import KpiCard from '../components/KpiCard'
|
|
10
10
|
import EditorIcon from '../components/EditorIcon'
|
|
@@ -25,11 +25,16 @@ export default function Projects({ overview }) {
|
|
|
25
25
|
const [search, setSearch] = useState('')
|
|
26
26
|
const [editorFilter, setEditorFilter] = useState('')
|
|
27
27
|
const [dateRange, setDateRange] = useState(null)
|
|
28
|
+
const [costs, setCosts] = useState(null)
|
|
28
29
|
const navigate = useNavigate()
|
|
29
30
|
const editors = overview?.editors || []
|
|
30
31
|
|
|
31
32
|
useEffect(() => {
|
|
32
|
-
|
|
33
|
+
const dateParams = dateRangeToApiParams(dateRange)
|
|
34
|
+
Promise.all([
|
|
35
|
+
fetchProjects(dateParams),
|
|
36
|
+
fetchCosts(dateParams),
|
|
37
|
+
]).then(([p, c]) => { setProjects(p); setCosts(c) })
|
|
33
38
|
}, [dateRange])
|
|
34
39
|
|
|
35
40
|
if (!projects) return <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text2)' }}>loading projects...</div>
|
|
@@ -64,6 +69,7 @@ export default function Projects({ overview }) {
|
|
|
64
69
|
<KpiCard label="sessions" value={formatNumber(totalSessions)} onClick={() => navigate('/sessions')} />
|
|
65
70
|
<KpiCard label="messages" value={formatNumber(totalMessages)} />
|
|
66
71
|
<KpiCard label="tokens" value={formatNumber(totalTokens)} />
|
|
72
|
+
<KpiCard label="est. cost" value={costs && costs.totalCost > 0 ? formatCost(costs.totalCost) : '\u2014'} />
|
|
67
73
|
</div>
|
|
68
74
|
|
|
69
75
|
{/* Charts row */}
|
|
@@ -4,7 +4,7 @@ import { Search, Filter, List, FolderOpen, ChevronDown, ChevronRight, X, AlertTr
|
|
|
4
4
|
import { Chart as ChartJS, ArcElement, CategoryScale, LinearScale, PointElement, LineElement, BarElement, Tooltip, Legend, Filler } from 'chart.js'
|
|
5
5
|
import { Line, Doughnut, Bar } from 'react-chartjs-2'
|
|
6
6
|
import { fetchChats } from '../lib/api'
|
|
7
|
-
import { editorColor, editorLabel, formatNumber, formatDate, dateRangeToApiParams } from '../lib/constants'
|
|
7
|
+
import { editorColor, editorLabel, formatNumber, formatCost, formatDate, dateRangeToApiParams } from '../lib/constants'
|
|
8
8
|
import { useTheme } from '../lib/theme'
|
|
9
9
|
import KpiCard from '../components/KpiCard'
|
|
10
10
|
import EditorIcon from '../components/EditorIcon'
|
|
@@ -285,6 +285,9 @@ export default function Sessions({ overview }) {
|
|
|
285
285
|
<span style={{ color: 'var(--c-text3)' }}>{c.bubbleCount || 0}</span>
|
|
286
286
|
)}
|
|
287
287
|
</td>
|
|
288
|
+
<td className="py-2 px-3 text-[12px] font-mono text-right" style={{ color: c.cost > 0 ? 'var(--c-text2)' : 'var(--c-text3)' }}>
|
|
289
|
+
{c.cost > 0 ? formatCost(c.cost) : ''}
|
|
290
|
+
</td>
|
|
288
291
|
<td className="py-2 px-3 text-[12px] whitespace-nowrap" style={{ color: 'var(--c-text3)' }}>
|
|
289
292
|
{formatDate(c.lastUpdatedAt || c.createdAt)}
|
|
290
293
|
</td>
|
|
@@ -475,6 +478,7 @@ export default function Sessions({ overview }) {
|
|
|
475
478
|
<th className="text-left py-2 px-3 font-medium">mode</th>
|
|
476
479
|
<th className="text-left py-2 px-3 font-medium">model</th>
|
|
477
480
|
<th className="text-left py-2 px-3 font-medium">context</th>
|
|
481
|
+
<th className="text-right py-2 px-3 font-medium">est. cost</th>
|
|
478
482
|
<th className="text-left py-2 px-3 font-medium">updated</th>
|
|
479
483
|
</tr>
|
|
480
484
|
</thead>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { Settings as SettingsIcon, EyeOff, Eye, FolderOpen, Search } from 'lucide-react'
|
|
3
|
+
import { fetchConfig, updateConfig, fetchAllProjects } from '../lib/api'
|
|
4
|
+
import { editorLabel, formatNumber, formatDate } from '../lib/constants'
|
|
5
|
+
import EditorIcon from '../components/EditorIcon'
|
|
6
|
+
import SectionTitle from '../components/SectionTitle'
|
|
7
|
+
|
|
8
|
+
export default function Settings() {
|
|
9
|
+
const [config, setConfig] = useState(null)
|
|
10
|
+
const [projects, setProjects] = useState([])
|
|
11
|
+
const [loading, setLoading] = useState(true)
|
|
12
|
+
const [saving, setSaving] = useState(false)
|
|
13
|
+
const [search, setSearch] = useState('')
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
Promise.all([fetchConfig(), fetchAllProjects()]).then(([cfg, projs]) => {
|
|
17
|
+
setConfig(cfg)
|
|
18
|
+
setProjects(projs)
|
|
19
|
+
setLoading(false)
|
|
20
|
+
}).catch(() => setLoading(false))
|
|
21
|
+
}, [])
|
|
22
|
+
|
|
23
|
+
if (loading || !config) {
|
|
24
|
+
return <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text2)' }}>loading settings...</div>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const hiddenProjects = config.hiddenProjects || []
|
|
28
|
+
|
|
29
|
+
const toggleProject = async (folder) => {
|
|
30
|
+
setSaving(true)
|
|
31
|
+
const isHidden = hiddenProjects.includes(folder)
|
|
32
|
+
const updated = isHidden
|
|
33
|
+
? hiddenProjects.filter(f => f !== folder)
|
|
34
|
+
: [...hiddenProjects, folder]
|
|
35
|
+
const newConfig = await updateConfig({ hiddenProjects: updated })
|
|
36
|
+
setConfig(newConfig)
|
|
37
|
+
setSaving(false)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const filtered = projects.filter(p => {
|
|
41
|
+
if (!search) return true
|
|
42
|
+
const q = search.toLowerCase()
|
|
43
|
+
return p.folder.toLowerCase().includes(q) || p.name.toLowerCase().includes(q)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const sorted = [...filtered].sort((a, b) => a.name.localeCompare(b.name))
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="fade-in space-y-4">
|
|
50
|
+
<div className="flex items-center gap-1.5 text-[13px] font-bold" style={{ color: 'var(--c-white)' }}>
|
|
51
|
+
<SettingsIcon size={14} style={{ color: '#6366f1' }} />
|
|
52
|
+
Settings
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="card overflow-hidden">
|
|
56
|
+
<div className="px-3 py-2 flex items-center justify-between" style={{ borderBottom: '1px solid var(--c-border)' }}>
|
|
57
|
+
<SectionTitle>
|
|
58
|
+
<FolderOpen size={11} className="inline mr-1" />
|
|
59
|
+
projects ({projects.length})
|
|
60
|
+
</SectionTitle>
|
|
61
|
+
<div className="flex items-center gap-2">
|
|
62
|
+
{hiddenProjects.length > 0 && (
|
|
63
|
+
<span className="text-[10px] px-1.5 py-0.5" style={{ background: 'rgba(239,68,68,0.08)', color: '#ef4444', border: '1px solid rgba(239,68,68,0.15)' }}>
|
|
64
|
+
{hiddenProjects.length} hidden
|
|
65
|
+
</span>
|
|
66
|
+
)}
|
|
67
|
+
<div className="relative">
|
|
68
|
+
<Search size={11} className="absolute left-2 top-1/2 -translate-y-1/2" style={{ color: 'var(--c-text3)' }} />
|
|
69
|
+
<input
|
|
70
|
+
type="text"
|
|
71
|
+
value={search}
|
|
72
|
+
onChange={e => setSearch(e.target.value)}
|
|
73
|
+
placeholder="Filter projects..."
|
|
74
|
+
className="pl-6 pr-2 py-1 text-[11px] outline-none w-[180px]"
|
|
75
|
+
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="text-[11px] px-3 py-1.5" style={{ color: 'var(--c-text3)', borderBottom: '1px solid var(--c-border)', background: 'var(--c-bg3)' }}>
|
|
81
|
+
Hidden projects are excluded from all dashboard stats, sessions, costs, and analytics.
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{sorted.map(p => (
|
|
85
|
+
<ProjectRow key={p.folder} project={p} hidden={hiddenProjects.includes(p.folder)} onToggle={toggleProject} saving={saving} />
|
|
86
|
+
))}
|
|
87
|
+
|
|
88
|
+
{sorted.length === 0 && (
|
|
89
|
+
<div className="text-center py-6 text-[12px]" style={{ color: 'var(--c-text3)' }}>no projects match filter</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function ProjectRow({ project: p, hidden, onToggle, saving }) {
|
|
97
|
+
const editors = Object.entries(p.editors || {}).sort((a, b) => b[1] - a[1])
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
className="flex items-center gap-3 px-3 py-2 transition"
|
|
102
|
+
style={{
|
|
103
|
+
borderBottom: '1px solid var(--c-border)',
|
|
104
|
+
opacity: hidden ? 0.5 : 1,
|
|
105
|
+
}}
|
|
106
|
+
onMouseEnter={e => e.currentTarget.style.background = 'var(--c-bg3)'}
|
|
107
|
+
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
|
108
|
+
>
|
|
109
|
+
<button
|
|
110
|
+
onClick={() => onToggle(p.folder)}
|
|
111
|
+
disabled={saving}
|
|
112
|
+
className="shrink-0 p-1 rounded transition hover:bg-[var(--c-bg)]"
|
|
113
|
+
style={{ color: hidden ? '#ef4444' : 'var(--c-text3)', border: '1px solid var(--c-border)' }}
|
|
114
|
+
title={hidden ? 'Show this project' : 'Hide this project'}
|
|
115
|
+
>
|
|
116
|
+
{hidden ? <EyeOff size={12} /> : <Eye size={12} />}
|
|
117
|
+
</button>
|
|
118
|
+
<div className="min-w-0 flex-1">
|
|
119
|
+
<div className="text-[12px] font-medium truncate" style={{ color: hidden ? 'var(--c-text3)' : 'var(--c-white)' }}>
|
|
120
|
+
{p.name}
|
|
121
|
+
</div>
|
|
122
|
+
<div className="text-[10px] truncate" style={{ color: 'var(--c-text3)' }} title={p.folder}>
|
|
123
|
+
{p.folder}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
127
|
+
{editors.slice(0, 3).map(([src, count]) => (
|
|
128
|
+
<span key={src} className="flex items-center gap-1 text-[10px]" style={{ color: 'var(--c-text3)' }}>
|
|
129
|
+
<EditorIcon source={src} size={10} />
|
|
130
|
+
{count}
|
|
131
|
+
</span>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
<div className="text-[11px] font-mono shrink-0" style={{ color: 'var(--c-text2)' }}>
|
|
135
|
+
{formatNumber(p.totalSessions)}
|
|
136
|
+
</div>
|
|
137
|
+
<div className="text-[10px] shrink-0 w-[80px] text-right" style={{ color: 'var(--c-text3)' }}>
|
|
138
|
+
{formatDate(p.lastSeen)}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
}
|