agentlytics 0.1.11 → 0.1.13
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/cache.js +6 -2
- package/package.json +1 -1
- package/server.js +6 -3
- package/share-image.js +6 -5
- package/ui/src/components/ShareModal.jsx +49 -17
- package/ui/src/lib/api.js +1 -0
package/cache.js
CHANGED
|
@@ -431,14 +431,17 @@ function getCachedOverview(opts = {}) {
|
|
|
431
431
|
const hf = hiddenFolderFilter(opts);
|
|
432
432
|
if (hf.sql) { conditions.push(hf.sql.replace(' AND ', '')); params.push(...hf.params); }
|
|
433
433
|
if (opts.editor) { conditions.push('source = ?'); params.push(opts.editor); }
|
|
434
|
+
if (opts.folder) { conditions.push('folder = ?'); params.push(opts.folder); }
|
|
434
435
|
if (opts.dateFrom) { conditions.push('COALESCE(last_updated_at, created_at) >= ?'); params.push(opts.dateFrom); }
|
|
435
436
|
if (opts.dateTo) { conditions.push('COALESCE(last_updated_at, created_at) <= ?'); params.push(opts.dateTo); }
|
|
436
437
|
const where = conditions.length > 0 ? ' WHERE ' + conditions.join(' AND ') : '';
|
|
437
438
|
const whereAnd = conditions.length > 0 ? ' AND ' + conditions.join(' AND ') : '';
|
|
438
439
|
|
|
439
440
|
const totalChats = db.prepare(`SELECT COUNT(*) as cnt FROM chats${where}`).get(...params).cnt;
|
|
440
|
-
//
|
|
441
|
-
const editors =
|
|
441
|
+
// When folder-filtered, show only that project's editors; otherwise show all
|
|
442
|
+
const editors = opts.folder
|
|
443
|
+
? db.prepare(`SELECT source, COUNT(*) as count FROM chats${where} GROUP BY source ORDER BY count DESC`).all(...params)
|
|
444
|
+
: db.prepare('SELECT source, COUNT(*) as count FROM chats GROUP BY source ORDER BY count DESC').all();
|
|
442
445
|
|
|
443
446
|
// By mode
|
|
444
447
|
const modes = db.prepare(`SELECT mode, COUNT(*) as count FROM chats WHERE mode IS NOT NULL${whereAnd} GROUP BY mode`).all(...params);
|
|
@@ -851,6 +854,7 @@ function getCachedDashboardStats(opts = {}) {
|
|
|
851
854
|
const hf = hiddenFolderFilter(opts);
|
|
852
855
|
if (hf.sql) { conditions.push(hf.sql.replace(' AND ', '')); params.push(...hf.params); }
|
|
853
856
|
if (opts.editor) { conditions.push('source = ?'); params.push(opts.editor); }
|
|
857
|
+
if (opts.folder) { conditions.push('folder = ?'); params.push(opts.folder); }
|
|
854
858
|
if (opts.dateFrom) { conditions.push('COALESCE(last_updated_at, created_at) >= ?'); params.push(opts.dateFrom); }
|
|
855
859
|
if (opts.dateTo) { conditions.push('COALESCE(last_updated_at, created_at) <= ?'); params.push(opts.dateTo); }
|
|
856
860
|
const where = conditions.length > 0 ? ' WHERE ' + conditions.join(' AND ') : '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlytics",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Comprehensive analytics dashboard for AI coding agents — Cursor, Windsurf, Claude Code, VS Code Copilot, Zed, Antigravity, OpenCode, Command Code",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/server.js
CHANGED
|
@@ -264,9 +264,11 @@ app.get('/api/schema', (req, res) => {
|
|
|
264
264
|
|
|
265
265
|
app.get('/api/share-image', (req, res) => {
|
|
266
266
|
try {
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
const
|
|
267
|
+
const filterOpts = { hiddenFolders: getHiddenFolders() };
|
|
268
|
+
if (req.query.folder) filterOpts.folder = req.query.folder;
|
|
269
|
+
const overview = cache.getCachedOverview(filterOpts);
|
|
270
|
+
const stats = cache.getCachedDashboardStats(filterOpts);
|
|
271
|
+
const costs = cache.getCostAnalytics(filterOpts);
|
|
270
272
|
const opts = {};
|
|
271
273
|
if (req.query.showEditors !== undefined) opts.showEditors = req.query.showEditors !== 'false';
|
|
272
274
|
if (req.query.showModels !== undefined) opts.showModels = req.query.showModels !== 'false';
|
|
@@ -275,6 +277,7 @@ app.get('/api/share-image', (req, res) => {
|
|
|
275
277
|
if (req.query.showHours !== undefined) opts.showHours = req.query.showHours !== 'false';
|
|
276
278
|
if (req.query.username) opts.username = req.query.username;
|
|
277
279
|
if (req.query.theme) opts.theme = req.query.theme;
|
|
280
|
+
if (req.query.folder) opts.folder = req.query.folder;
|
|
278
281
|
const svg = generateShareSvg(overview, stats, costs, opts);
|
|
279
282
|
res.setHeader('Content-Type', 'image/svg+xml');
|
|
280
283
|
res.send(svg);
|
package/share-image.js
CHANGED
|
@@ -93,10 +93,12 @@ function generateShareSvg(overview, stats, costs, opts = {}) {
|
|
|
93
93
|
};
|
|
94
94
|
const username = opts.username || '';
|
|
95
95
|
const t = THEMES[opts.theme] || THEMES.dark;
|
|
96
|
+
const projectFolder = opts.folder || '';
|
|
97
|
+
const projectName = projectFolder ? projectFolder.split(/[/\\]/).pop() : '';
|
|
96
98
|
|
|
97
99
|
const W = 1200;
|
|
98
100
|
const H_FIXED = 675;
|
|
99
|
-
const F = "'Menlo','Monaco','Cascadia Code','Courier New',monospace";
|
|
101
|
+
const F = "\"'Menlo','Monaco','Cascadia Code','Courier New',monospace\"";
|
|
100
102
|
const editors = overview.editors || [];
|
|
101
103
|
const tk = stats.tokens || {};
|
|
102
104
|
const streaks = stats.streaks || {};
|
|
@@ -183,9 +185,10 @@ function generateShareSvg(overview, stats, costs, opts = {}) {
|
|
|
183
185
|
});
|
|
184
186
|
|
|
185
187
|
// ── Editor bar chart ──
|
|
188
|
+
const colW = (W - pad * 2 - 20) / 2;
|
|
189
|
+
const maxBarW = colW - 120 - 60;
|
|
186
190
|
const maxEditorCount = Math.max(...editors.map(e => e.count), 1);
|
|
187
191
|
const editorBarsArr = editors.slice(0, 8).map((e, i) => {
|
|
188
|
-
const maxBarW = W / 2 - pad - 140;
|
|
189
192
|
const barW = Math.max((e.count / maxEditorCount) * maxBarW, 4);
|
|
190
193
|
const color = EDITOR_COLORS[e.id] || '#6b7280';
|
|
191
194
|
const label = (EDITOR_LABELS[e.id] || e.id);
|
|
@@ -195,7 +198,6 @@ function generateShareSvg(overview, stats, costs, opts = {}) {
|
|
|
195
198
|
// ── Cost bar chart ──
|
|
196
199
|
const maxCostVal = costByEditor.length > 0 ? Math.max(...costByEditor.map(c => c.cost), 0.01) : 1;
|
|
197
200
|
const costBarsArr = costByEditor.map(c => {
|
|
198
|
-
const maxBarW = W / 2 - pad - 140;
|
|
199
201
|
const barW = Math.max((c.cost / maxCostVal) * maxBarW, 4);
|
|
200
202
|
const color = EDITOR_COLORS[c.editor] || '#6b7280';
|
|
201
203
|
const label = EDITOR_LABELS[c.editor] || c.editor;
|
|
@@ -209,7 +211,6 @@ function generateShareSvg(overview, stats, costs, opts = {}) {
|
|
|
209
211
|
// ── Build sections ──
|
|
210
212
|
let curY = kpiY + 64 + 18;
|
|
211
213
|
const sectionSvgs = [];
|
|
212
|
-
const colW = (W - pad * 2 - 20) / 2;
|
|
213
214
|
|
|
214
215
|
for (let row = 0; row < rowCount; row++) {
|
|
215
216
|
const lName = leftSections[row];
|
|
@@ -330,7 +331,7 @@ function generateShareSvg(overview, stats, costs, opts = {}) {
|
|
|
330
331
|
<path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>
|
|
331
332
|
</g>
|
|
332
333
|
<text x="${pad + 26}" y="68" fill="${t.text}" font-size="16" font-weight="bold" font-family=${F}>Agentlytics</text>
|
|
333
|
-
<text x="${pad + 148}" y="68" fill="${t.text4}" font-size="13" font-family=${F}
|
|
334
|
+
<text x="${pad + 148}" y="68" fill="${t.text4}" font-size="13" font-family=${F}>${projectName ? esc(projectName) : 'Your AI coding stats'}</text>
|
|
334
335
|
<text x="${W - pad}" y="68" fill="${t.text5}" font-size="12" font-family=${F} text-anchor="end">${esc(dateStr)}</text>
|
|
335
336
|
|
|
336
337
|
<!-- Divider -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
2
|
-
import { X, Download, Share2, BarChart3, DollarSign, Clock, Cpu, Braces, User, Sun, Moon } from 'lucide-react'
|
|
3
|
-
import { fetchShareImage } from '../lib/api'
|
|
2
|
+
import { X, Download, Share2, BarChart3, DollarSign, Clock, Cpu, Braces, User, Sun, Moon, FolderOpen } from 'lucide-react'
|
|
3
|
+
import { fetchShareImage, fetchProjects } from '../lib/api'
|
|
4
4
|
|
|
5
5
|
const TOGGLE_ITEMS = [
|
|
6
6
|
{ key: 'showEditors', label: 'Editors', icon: BarChart3 },
|
|
@@ -23,6 +23,7 @@ export default function ShareModal({ open, onClose }) {
|
|
|
23
23
|
const [svg, setSvg] = useState('')
|
|
24
24
|
const [loading, setLoading] = useState(false)
|
|
25
25
|
const [downloading, setDownloading] = useState(false)
|
|
26
|
+
const [projects, setProjects] = useState([])
|
|
26
27
|
const debounceRef = useRef(null)
|
|
27
28
|
const backdropRef = useRef(null)
|
|
28
29
|
|
|
@@ -40,6 +41,7 @@ export default function ShareModal({ open, onClose }) {
|
|
|
40
41
|
useEffect(() => {
|
|
41
42
|
if (!open) return
|
|
42
43
|
loadPreview(opts)
|
|
44
|
+
fetchProjects().then(p => setProjects(p || [])).catch(() => {})
|
|
43
45
|
}, [open])
|
|
44
46
|
|
|
45
47
|
const updateOpt = (key, value) => {
|
|
@@ -53,25 +55,33 @@ export default function ShareModal({ open, onClose }) {
|
|
|
53
55
|
if (!svg) return
|
|
54
56
|
setDownloading(true)
|
|
55
57
|
try {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const ctx = canvas.getContext('2d')
|
|
58
|
+
const scale = 2
|
|
59
|
+
const svgBlob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' })
|
|
60
|
+
const url = URL.createObjectURL(svgBlob)
|
|
60
61
|
const img = new Image()
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
img.width = 1200 * scale
|
|
63
|
+
img.height = 675 * scale
|
|
63
64
|
await new Promise((resolve, reject) => {
|
|
64
65
|
img.onload = resolve
|
|
65
66
|
img.onerror = reject
|
|
66
|
-
img.src =
|
|
67
|
+
img.src = url
|
|
67
68
|
})
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
const canvas = document.createElement('canvas')
|
|
70
|
+
canvas.width = 1200 * scale
|
|
71
|
+
canvas.height = 675 * scale
|
|
72
|
+
const ctx = canvas.getContext('2d')
|
|
73
|
+
ctx.drawImage(img, 0, 0, 1200 * scale, 675 * scale)
|
|
74
|
+
URL.revokeObjectURL(url)
|
|
75
|
+
canvas.toBlob((blob) => {
|
|
76
|
+
if (!blob) return
|
|
77
|
+
const a = document.createElement('a')
|
|
78
|
+
a.href = URL.createObjectURL(blob)
|
|
79
|
+
a.download = 'agentlytics.png'
|
|
80
|
+
a.click()
|
|
81
|
+
setTimeout(() => URL.revokeObjectURL(a.href), 1000)
|
|
82
|
+
}, 'image/png')
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.error('PNG conversion failed:', e)
|
|
75
85
|
const blob = new Blob([svg], { type: 'image/svg+xml' })
|
|
76
86
|
const a = document.createElement('a')
|
|
77
87
|
a.href = URL.createObjectURL(blob)
|
|
@@ -94,7 +104,7 @@ export default function ShareModal({ open, onClose }) {
|
|
|
94
104
|
|
|
95
105
|
const handleShareTwitter = async () => {
|
|
96
106
|
await handleDownloadPng()
|
|
97
|
-
const text = encodeURIComponent("Here's my agentic coding stats using
|
|
107
|
+
const text = encodeURIComponent("Here's my agentic coding stats using agentlytics.io\n\nrun `npx agentlytics` to get yours ✨\n\ngithub.com/f/agentlytics")
|
|
98
108
|
window.open(`https://x.com/intent/post?text=${text}`, '_blank')
|
|
99
109
|
}
|
|
100
110
|
|
|
@@ -138,6 +148,28 @@ export default function ShareModal({ open, onClose }) {
|
|
|
138
148
|
Customize
|
|
139
149
|
</div>
|
|
140
150
|
|
|
151
|
+
<div className="mb-4">
|
|
152
|
+
<div className="text-[11px] font-medium mb-2" style={{ color: 'var(--c-text2)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
|
153
|
+
Project
|
|
154
|
+
</div>
|
|
155
|
+
<div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md" style={{ border: '1px solid var(--c-border)', background: 'var(--c-bg)' }}>
|
|
156
|
+
<FolderOpen size={11} style={{ color: 'var(--c-text3)' }} />
|
|
157
|
+
<select
|
|
158
|
+
value={opts.folder || ''}
|
|
159
|
+
onChange={(e) => updateOpt('folder', e.target.value || undefined)}
|
|
160
|
+
className="bg-transparent outline-none text-[12px] w-full cursor-pointer"
|
|
161
|
+
style={{ color: 'var(--c-text)', appearance: 'none' }}
|
|
162
|
+
>
|
|
163
|
+
<option value="">All Projects</option>
|
|
164
|
+
{projects.map(p => (
|
|
165
|
+
<option key={p.folder} value={p.folder}>
|
|
166
|
+
{p.name} ({p.totalSessions})
|
|
167
|
+
</option>
|
|
168
|
+
))}
|
|
169
|
+
</select>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
141
173
|
<div className="space-y-1.5">
|
|
142
174
|
{TOGGLE_ITEMS.map(({ key, label, icon: Icon }) => {
|
|
143
175
|
const active = opts[key]
|
package/ui/src/lib/api.js
CHANGED
|
@@ -189,6 +189,7 @@ export async function fetchShareImage(opts = {}) {
|
|
|
189
189
|
if (opts.showHours !== undefined) q.set('showHours', opts.showHours);
|
|
190
190
|
if (opts.username) q.set('username', opts.username);
|
|
191
191
|
if (opts.theme) q.set('theme', opts.theme);
|
|
192
|
+
if (opts.folder) q.set('folder', opts.folder);
|
|
192
193
|
const qs = q.toString();
|
|
193
194
|
const res = await fetch(`${BASE}/api/share-image${qs ? '?' + qs : ''}`);
|
|
194
195
|
return res.text();
|