agentlytics 0.1.9 → 0.1.11
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 +59 -32
- package/cache.js +76 -14
- package/editors/base.js +0 -116
- package/editors/codex.js +0 -11
- package/editors/index.js +1 -9
- package/editors/opencode.js +1 -1
- package/editors/windsurf.js +3 -3
- package/editors/zed.js +3 -3
- package/index.js +3 -1
- package/package.json +1 -3
- package/pricing.json +805 -70
- package/relay-client.js +0 -4
- package/server.js +10 -1
- package/share-image.js +288 -75
- package/ui/src/App.jsx +1 -2
- package/ui/src/components/LiveFeed.jsx +0 -10
- package/ui/src/components/MessageRenderer.jsx +0 -19
- package/ui/src/components/ShareModal.jsx +273 -0
- package/ui/src/lib/api.js +11 -7
- package/ui/src/pages/Dashboard.jsx +7 -54
- package/ui/src/pages/DeepAnalysis.jsx +2 -2
- package/ui/src/pages/ProjectDetail.jsx +0 -1
- package/ui/src/pages/Projects.jsx +1 -1
- package/ui/src/pages/RelayDashboard.jsx +2 -2
- package/ui/src/pages/RelayUserDetail.jsx +1 -3
- package/ui/src/components/EditorBreakdown.jsx +0 -22
- package/ui/src/components/ModelBreakdown.jsx +0 -23
- package/ui/src/pages/ChatDetail.jsx +0 -107
- package/ui/src/pages/RelaySessionDetail.jsx +0 -32
package/relay-client.js
CHANGED
package/server.js
CHANGED
|
@@ -266,7 +266,16 @@ app.get('/api/share-image', (req, res) => {
|
|
|
266
266
|
try {
|
|
267
267
|
const overview = cache.getCachedOverview();
|
|
268
268
|
const stats = cache.getCachedDashboardStats();
|
|
269
|
-
const
|
|
269
|
+
const costs = cache.getCostAnalytics({ hiddenFolders: getHiddenFolders() });
|
|
270
|
+
const opts = {};
|
|
271
|
+
if (req.query.showEditors !== undefined) opts.showEditors = req.query.showEditors !== 'false';
|
|
272
|
+
if (req.query.showModels !== undefined) opts.showModels = req.query.showModels !== 'false';
|
|
273
|
+
if (req.query.showCosts !== undefined) opts.showCosts = req.query.showCosts !== 'false';
|
|
274
|
+
if (req.query.showTokens !== undefined) opts.showTokens = req.query.showTokens !== 'false';
|
|
275
|
+
if (req.query.showHours !== undefined) opts.showHours = req.query.showHours !== 'false';
|
|
276
|
+
if (req.query.username) opts.username = req.query.username;
|
|
277
|
+
if (req.query.theme) opts.theme = req.query.theme;
|
|
278
|
+
const svg = generateShareSvg(overview, stats, costs, opts);
|
|
270
279
|
res.setHeader('Content-Type', 'image/svg+xml');
|
|
271
280
|
res.send(svg);
|
|
272
281
|
} catch (err) {
|
package/share-image.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generates a shareable SVG stats card from cached data.
|
|
3
|
+
* Accepts an `opts` object to toggle sections on/off.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
function fmt(n) {
|
|
@@ -9,6 +10,14 @@ function fmt(n) {
|
|
|
9
10
|
return n.toLocaleString();
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
function fmtCost(n) {
|
|
14
|
+
if (n == null || n === 0) return '$0';
|
|
15
|
+
if (n < 0.01) return '<$0.01';
|
|
16
|
+
if (n >= 1000) return '$' + (n / 1000).toFixed(1) + 'K';
|
|
17
|
+
if (n >= 100) return '$' + Math.round(n);
|
|
18
|
+
return '$' + n.toFixed(2);
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
const EDITOR_COLORS = {
|
|
13
22
|
'cursor': '#f59e0b',
|
|
14
23
|
'windsurf': '#06b6d4',
|
|
@@ -20,9 +29,11 @@ const EDITOR_COLORS = {
|
|
|
20
29
|
'vscode-insiders': '#60a5fa',
|
|
21
30
|
'zed': '#10b981',
|
|
22
31
|
'opencode': '#ec4899',
|
|
32
|
+
'codex': '#0f766e',
|
|
23
33
|
'gemini-cli': '#4285f4',
|
|
24
34
|
'copilot-cli': '#8957e5',
|
|
25
35
|
'cursor-agent': '#f59e0b',
|
|
36
|
+
'commandcode': '#e11d48',
|
|
26
37
|
};
|
|
27
38
|
|
|
28
39
|
const EDITOR_LABELS = {
|
|
@@ -36,107 +47,309 @@ const EDITOR_LABELS = {
|
|
|
36
47
|
'vscode-insiders': 'VS Code Ins.',
|
|
37
48
|
'zed': 'Zed',
|
|
38
49
|
'opencode': 'OpenCode',
|
|
50
|
+
'codex': 'Codex',
|
|
39
51
|
'gemini-cli': 'Gemini CLI',
|
|
40
52
|
'copilot-cli': 'Copilot CLI',
|
|
41
53
|
'cursor-agent': 'Cursor Agent',
|
|
54
|
+
'commandcode': 'Cmd Code',
|
|
42
55
|
};
|
|
43
56
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
/**
|
|
58
|
+
* @param {object} overview — from getCachedOverview()
|
|
59
|
+
* @param {object} stats — from getCachedDashboardStats()
|
|
60
|
+
* @param {object} costs — from getCostBreakdown()
|
|
61
|
+
* @param {object} [opts] — toggle sections
|
|
62
|
+
* @param {boolean} [opts.showEditors=true]
|
|
63
|
+
* @param {boolean} [opts.showModels=true]
|
|
64
|
+
* @param {boolean} [opts.showCosts=true]
|
|
65
|
+
* @param {boolean} [opts.showTokens=true]
|
|
66
|
+
* @param {boolean} [opts.showHours=true]
|
|
67
|
+
* @param {boolean} [opts.showProjects=true]
|
|
68
|
+
* @param {string} [opts.username]
|
|
69
|
+
* @param {string} [opts.theme='dark'] — 'dark' or 'light'
|
|
70
|
+
*/
|
|
71
|
+
const THEMES = {
|
|
72
|
+
dark: {
|
|
73
|
+
bg: '#09090f', bg2: '#111118', card: '#111', border: '#1e1e2a',
|
|
74
|
+
text: '#fff', text2: '#888', text3: '#666', text4: '#555', text5: '#444',
|
|
75
|
+
titleText: '#555',
|
|
76
|
+
hourHigh: '#818cf8', hourMed: '#6366f1', hourLow: '#4f46e5', hourMin: '#1e1b4b',
|
|
77
|
+
},
|
|
78
|
+
light: {
|
|
79
|
+
bg: '#f8f8fa', bg2: '#eeeef2', card: '#e8e8ee', border: '#d0d0d8',
|
|
80
|
+
text: '#111', text2: '#555', text3: '#888', text4: '#999', text5: '#aaa',
|
|
81
|
+
titleText: '#888',
|
|
82
|
+
hourHigh: '#6366f1', hourMed: '#818cf8', hourLow: '#a5b4fc', hourMin: '#e0e7ff',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function generateShareSvg(overview, stats, costs, opts = {}) {
|
|
87
|
+
const show = {
|
|
88
|
+
editors: opts.showEditors !== false,
|
|
89
|
+
models: opts.showModels !== false,
|
|
90
|
+
costs: opts.showCosts !== false,
|
|
91
|
+
tokens: opts.showTokens !== false,
|
|
92
|
+
hours: opts.showHours !== false,
|
|
93
|
+
};
|
|
94
|
+
const username = opts.username || '';
|
|
95
|
+
const t = THEMES[opts.theme] || THEMES.dark;
|
|
96
|
+
|
|
97
|
+
const W = 1200;
|
|
98
|
+
const H_FIXED = 675;
|
|
99
|
+
const F = "'Menlo','Monaco','Cascadia Code','Courier New',monospace";
|
|
47
100
|
const editors = overview.editors || [];
|
|
48
101
|
const tk = stats.tokens || {};
|
|
49
102
|
const streaks = stats.streaks || {};
|
|
50
103
|
const topModels = (stats.topModels || []).slice(0, 5);
|
|
104
|
+
const costData = costs || {};
|
|
105
|
+
const totalCost = costData.totalCost || 0;
|
|
106
|
+
const costByEditor = (costData.byEditor || []).slice(0, 6);
|
|
107
|
+
const totalTokens = (tk.input || 0) + (tk.output || 0);
|
|
108
|
+
const now = new Date();
|
|
109
|
+
const dateStr = now.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
110
|
+
|
|
111
|
+
// ── Compute layout ──
|
|
112
|
+
let y = 0;
|
|
113
|
+
const pad = 28;
|
|
114
|
+
|
|
115
|
+
// Title bar
|
|
116
|
+
y += 40;
|
|
117
|
+
// Header area (prompt + branding)
|
|
118
|
+
y += 54;
|
|
119
|
+
// KPI row
|
|
120
|
+
y += 80;
|
|
121
|
+
// Sections
|
|
122
|
+
const midSections = [];
|
|
123
|
+
if (show.editors) midSections.push('editors');
|
|
124
|
+
if (show.costs && totalCost > 0) midSections.push('costs');
|
|
125
|
+
if (show.hours) midSections.push('hours');
|
|
126
|
+
if (show.models && topModels.length > 0) midSections.push('models');
|
|
127
|
+
|
|
128
|
+
// Layout: 2 columns, each section ~row
|
|
129
|
+
const leftSections = [];
|
|
130
|
+
const rightSections = [];
|
|
131
|
+
midSections.forEach((s, i) => {
|
|
132
|
+
if (i % 2 === 0) leftSections.push(s);
|
|
133
|
+
else rightSections.push(s);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
function sectionHeight(name) {
|
|
137
|
+
if (name === 'editors') return Math.max(editors.slice(0, 8).length * 26 + 34, 70);
|
|
138
|
+
if (name === 'costs') return Math.max(costByEditor.length * 26 + 34, 70);
|
|
139
|
+
if (name === 'hours') return 110;
|
|
140
|
+
if (name === 'models') return topModels.length * 22 + 34;
|
|
141
|
+
return 70;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Mid-section pairs: compute max height of each row
|
|
145
|
+
const rowCount = Math.max(leftSections.length, rightSections.length);
|
|
146
|
+
let midHeight = 0;
|
|
147
|
+
for (let i = 0; i < rowCount; i++) {
|
|
148
|
+
const lh = leftSections[i] ? sectionHeight(leftSections[i]) : 0;
|
|
149
|
+
const rh = rightSections[i] ? sectionHeight(rightSections[i]) : 0;
|
|
150
|
+
midHeight += Math.max(lh, rh) + 8;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Token footer
|
|
154
|
+
const tokenFooterH = show.tokens ? 44 : 0;
|
|
155
|
+
// Footer
|
|
156
|
+
const footerH = 36;
|
|
157
|
+
|
|
158
|
+
const naturalH = y + midHeight + tokenFooterH + footerH + 20;
|
|
159
|
+
// Stretch: if content is shorter than 675, distribute extra space into sections
|
|
160
|
+
const extraSpace = Math.max(0, H_FIXED - naturalH);
|
|
161
|
+
const extraPerRow = rowCount > 0 ? extraSpace / rowCount : 0;
|
|
51
162
|
|
|
52
|
-
//
|
|
163
|
+
// ── KPI cards ──
|
|
164
|
+
const kpiY = 94;
|
|
165
|
+
const kpiCards = [];
|
|
166
|
+
const kpiItems = [
|
|
167
|
+
{ label: 'sessions', value: fmt(overview.totalChats) },
|
|
168
|
+
{ label: 'tokens', value: fmt(totalTokens) },
|
|
169
|
+
{ label: 'active days', value: String(streaks.totalDays || 0) },
|
|
170
|
+
{ label: 'streak', value: `${streaks.current || 0}d` },
|
|
171
|
+
];
|
|
172
|
+
if (show.costs && totalCost > 0) {
|
|
173
|
+
kpiItems.push({ label: 'est. cost', value: fmtCost(totalCost) });
|
|
174
|
+
}
|
|
175
|
+
const kpiW = (W - pad * 2 - (kpiItems.length - 1) * 8) / kpiItems.length;
|
|
176
|
+
kpiItems.forEach((item, i) => {
|
|
177
|
+
const x = pad + i * (kpiW + 8);
|
|
178
|
+
kpiCards.push(`
|
|
179
|
+
<rect x="${x}" y="${kpiY}" width="${kpiW}" height="64" rx="6" fill="${t.card}"/>
|
|
180
|
+
<text x="${x + 14}" y="${kpiY + 22}" fill="${t.text3}" font-size="11" font-family=${F}>${esc(item.label)}</text>
|
|
181
|
+
<text x="${x + 14}" y="${kpiY + 50}" fill="${t.text}" font-size="24" font-weight="bold" font-family=${F}>${esc(item.value)}</text>
|
|
182
|
+
`);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ── Editor bar chart ──
|
|
53
186
|
const maxEditorCount = Math.max(...editors.map(e => e.count), 1);
|
|
54
|
-
const
|
|
55
|
-
const
|
|
187
|
+
const editorBarsArr = editors.slice(0, 8).map((e, i) => {
|
|
188
|
+
const maxBarW = W / 2 - pad - 140;
|
|
189
|
+
const barW = Math.max((e.count / maxEditorCount) * maxBarW, 4);
|
|
56
190
|
const color = EDITOR_COLORS[e.id] || '#6b7280';
|
|
57
|
-
const label = (EDITOR_LABELS[e.id] || e.id)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
191
|
+
const label = (EDITOR_LABELS[e.id] || e.id);
|
|
192
|
+
return { label, barW, color, count: e.count };
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ── Cost bar chart ──
|
|
196
|
+
const maxCostVal = costByEditor.length > 0 ? Math.max(...costByEditor.map(c => c.cost), 0.01) : 1;
|
|
197
|
+
const costBarsArr = costByEditor.map(c => {
|
|
198
|
+
const maxBarW = W / 2 - pad - 140;
|
|
199
|
+
const barW = Math.max((c.cost / maxCostVal) * maxBarW, 4);
|
|
200
|
+
const color = EDITOR_COLORS[c.editor] || '#6b7280';
|
|
201
|
+
const label = EDITOR_LABELS[c.editor] || c.editor;
|
|
202
|
+
return { label, barW, color, value: fmtCost(c.cost) };
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ── Hourly sparkline ──
|
|
67
206
|
const hourly = stats.hourly || new Array(24).fill(0);
|
|
68
|
-
const
|
|
69
|
-
const sparkW = 180, sparkH = 40;
|
|
70
|
-
const sparkPoints = hourly.map((v, i) => {
|
|
71
|
-
const x = 590 + (i / 23) * sparkW;
|
|
72
|
-
const y = 180 + sparkH - (v / maxH) * sparkH;
|
|
73
|
-
return `${x},${y}`;
|
|
74
|
-
}).join(' ');
|
|
75
|
-
|
|
76
|
-
// Top models list
|
|
77
|
-
const modelsList = topModels.map((m, i) => {
|
|
78
|
-
const y = 274 + i * 16;
|
|
79
|
-
const name = m.name.length > 24 ? m.name.substring(0, 24) : m.name;
|
|
80
|
-
return `<text x="590" y="${y}" fill="#888" font-size="9" font-family="${F}">${esc(name)} <tspan fill="#555">${m.count}</tspan></text>`;
|
|
81
|
-
}).join('');
|
|
82
|
-
|
|
83
|
-
const dateStr = new Date().toISOString().split('T')[0];
|
|
84
|
-
|
|
85
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
|
86
|
-
<!-- Background -->
|
|
87
|
-
<rect width="${W}" height="${H}" fill="#000"/>
|
|
88
|
-
<rect x="0.5" y="0.5" width="${W - 1}" height="${H - 1}" fill="none" stroke="#222" stroke-width="1"/>
|
|
207
|
+
const maxHourly = Math.max(...hourly, 1);
|
|
89
208
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
209
|
+
// ── Build sections ──
|
|
210
|
+
let curY = kpiY + 64 + 18;
|
|
211
|
+
const sectionSvgs = [];
|
|
212
|
+
const colW = (W - pad * 2 - 20) / 2;
|
|
93
213
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
214
|
+
for (let row = 0; row < rowCount; row++) {
|
|
215
|
+
const lName = leftSections[row];
|
|
216
|
+
const rName = rightSections[row];
|
|
217
|
+
const lh = lName ? sectionHeight(lName) : 0;
|
|
218
|
+
const rh = rName ? sectionHeight(rName) : 0;
|
|
219
|
+
const rowH = Math.max(lh, rh) + Math.round(extraPerRow);
|
|
97
220
|
|
|
98
|
-
|
|
99
|
-
|
|
221
|
+
if (lName) sectionSvgs.push(renderSection(lName, pad, curY, colW, rowH));
|
|
222
|
+
if (rName) sectionSvgs.push(renderSection(rName, pad + colW + 20, curY, colW, rowH));
|
|
223
|
+
|
|
224
|
+
curY += rowH + 10;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function renderSection(name, sx, sy, sw, sh) {
|
|
228
|
+
let out = '';
|
|
229
|
+
if (name === 'editors') {
|
|
230
|
+
out += `<text x="${sx}" y="${sy + 14}" fill="${t.text4}" font-size="11" font-family=${F}># editors</text>`;
|
|
231
|
+
editorBarsArr.forEach((e, i) => {
|
|
232
|
+
const by = sy + 26 + i * 26;
|
|
233
|
+
out += `<text x="${sx}" y="${by + 14}" fill="${t.text2}" font-size="12" font-family=${F}>${esc(e.label)}</text>`;
|
|
234
|
+
out += `<rect x="${sx + 120}" y="${by + 1}" width="${e.barW}" height="16" rx="3" fill="${e.color}" opacity="0.85"/>`;
|
|
235
|
+
out += `<text x="${sx + 128 + e.barW}" y="${by + 14}" fill="${t.text3}" font-size="11" font-family=${F}>${e.count}</text>`;
|
|
236
|
+
});
|
|
237
|
+
} else if (name === 'costs') {
|
|
238
|
+
out += `<text x="${sx}" y="${sy + 14}" fill="${t.text4}" font-size="11" font-family=${F}># est. costs by editor</text>`;
|
|
239
|
+
costBarsArr.forEach((c, i) => {
|
|
240
|
+
const by = sy + 26 + i * 26;
|
|
241
|
+
out += `<text x="${sx}" y="${by + 14}" fill="${t.text2}" font-size="12" font-family=${F}>${esc(c.label)}</text>`;
|
|
242
|
+
out += `<rect x="${sx + 120}" y="${by + 1}" width="${c.barW}" height="16" rx="3" fill="${c.color}" opacity="0.85"/>`;
|
|
243
|
+
out += `<text x="${sx + 128 + c.barW}" y="${by + 14}" fill="${t.text3}" font-size="11" font-family=${F}>${c.value}</text>`;
|
|
244
|
+
});
|
|
245
|
+
} else if (name === 'hours') {
|
|
246
|
+
out += `<text x="${sx}" y="${sy + 14}" fill="${t.text4}" font-size="11" font-family=${F}># peak hours</text>`;
|
|
247
|
+
const sparkW = sw - 8;
|
|
248
|
+
const sparkH = 56;
|
|
249
|
+
const baseY = sy + 28;
|
|
250
|
+
const barW = sparkW / 24 - 1;
|
|
251
|
+
hourly.forEach((v, i) => {
|
|
252
|
+
const bh = Math.max((v / maxHourly) * sparkH, 1);
|
|
253
|
+
const bx = sx + i * (barW + 1);
|
|
254
|
+
const by = baseY + sparkH - bh;
|
|
255
|
+
const intensity = v / maxHourly;
|
|
256
|
+
const color = intensity > 0.75 ? t.hourHigh : intensity > 0.5 ? t.hourMed : intensity > 0.25 ? t.hourLow : t.hourMin;
|
|
257
|
+
out += `<rect x="${bx}" y="${by}" width="${barW}" height="${bh}" rx="1" fill="${color}" opacity="0.9"/>`;
|
|
258
|
+
});
|
|
259
|
+
out += `<text x="${sx}" y="${baseY + sparkH + 14}" fill="${t.text5}" font-size="9" font-family=${F}>00</text>`;
|
|
260
|
+
out += `<text x="${sx + sparkW / 4}" y="${baseY + sparkH + 14}" fill="${t.text5}" font-size="9" font-family=${F}>06</text>`;
|
|
261
|
+
out += `<text x="${sx + sparkW / 2}" y="${baseY + sparkH + 14}" fill="${t.text5}" font-size="9" font-family=${F}>12</text>`;
|
|
262
|
+
out += `<text x="${sx + sparkW * 3 / 4}" y="${baseY + sparkH + 14}" fill="${t.text5}" font-size="9" font-family=${F}>18</text>`;
|
|
263
|
+
out += `<text x="${sx + sparkW - 8}" y="${baseY + sparkH + 14}" fill="${t.text5}" font-size="9" font-family=${F}>23</text>`;
|
|
264
|
+
} else if (name === 'models') {
|
|
265
|
+
out += `<text x="${sx}" y="${sy + 14}" fill="${t.text4}" font-size="11" font-family=${F}># top models</text>`;
|
|
266
|
+
topModels.forEach((m, i) => {
|
|
267
|
+
const my = sy + 32 + i * 22;
|
|
268
|
+
const mName = m.name.length > 28 ? m.name.substring(0, 28) + '…' : m.name;
|
|
269
|
+
out += `<text x="${sx}" y="${my}" fill="${t.text2}" font-size="11" font-family=${F}>${esc(mName)}</text>`;
|
|
270
|
+
out += `<text x="${sx + sw - 4}" y="${my}" fill="${t.text4}" font-size="11" font-family=${F} text-anchor="end">${m.count}</text>`;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
100
275
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
276
|
+
// ── Token footer line ──
|
|
277
|
+
let tokenLine = '';
|
|
278
|
+
if (show.tokens) {
|
|
279
|
+
const ty = curY + 4;
|
|
280
|
+
const tkPairs = [
|
|
281
|
+
['input', fmt(tk.input)],
|
|
282
|
+
['output', fmt(tk.output)],
|
|
283
|
+
['cache read', fmt(tk.cacheRead)],
|
|
284
|
+
['cache write', fmt(tk.cacheWrite)],
|
|
285
|
+
['tools', fmt(stats.totalToolCalls || 0)],
|
|
286
|
+
['editors', String(editors.length)],
|
|
287
|
+
];
|
|
288
|
+
let tkX = pad;
|
|
289
|
+
let tkSvg = `<line x1="${pad}" y1="${ty}" x2="${W - pad}" y2="${ty}" stroke="${t.border}" stroke-width="1"/>`;
|
|
290
|
+
tkPairs.forEach(([label, val]) => {
|
|
291
|
+
tkSvg += `<text x="${tkX}" y="${ty + 20}" fill="${t.text5}" font-size="10" font-family=${F}>${label}</text>`;
|
|
292
|
+
tkSvg += `<text x="${tkX}" y="${ty + 34}" fill="${t.text2}" font-size="12" font-weight="600" font-family=${F}>${val}</text>`;
|
|
293
|
+
tkX += (W - pad * 2) / tkPairs.length;
|
|
294
|
+
});
|
|
295
|
+
tokenLine = tkSvg;
|
|
296
|
+
curY = ty + tokenFooterH;
|
|
297
|
+
}
|
|
105
298
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<text x="222" y="122" fill="#fff" font-size="22" font-weight="bold" font-family="${F}">${fmt((tk.input || 0) + (tk.output || 0))}</text>
|
|
299
|
+
// ── Final footer — pin to bottom of canvas ──
|
|
300
|
+
const footerY = H_FIXED - 28;
|
|
109
301
|
|
|
110
|
-
|
|
111
|
-
<text x="408" y="96" fill="#666" font-size="9" font-family="${F}">active_days</text>
|
|
112
|
-
<text x="408" y="122" fill="#fff" font-size="22" font-weight="bold" font-family="${F}">${streaks.totalDays || 0}</text>
|
|
302
|
+
const usernameText = username ? `<text x="${W / 2}" y="${footerY + 14}" fill="${t.text5}" font-size="11" font-family=${F} text-anchor="middle">${esc(username)}</text>` : '';
|
|
113
303
|
|
|
114
|
-
|
|
115
|
-
<
|
|
116
|
-
|
|
304
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="675" viewBox="0 0 ${W} ${H_FIXED}">
|
|
305
|
+
<defs>
|
|
306
|
+
<linearGradient id="accentGrad" x1="0" y1="0" x2="1" y2="0">
|
|
307
|
+
<stop offset="0%" stop-color="#818cf8"/>
|
|
308
|
+
<stop offset="50%" stop-color="#a78bfa"/>
|
|
309
|
+
<stop offset="100%" stop-color="#c084fc"/>
|
|
310
|
+
</linearGradient>
|
|
311
|
+
</defs>
|
|
117
312
|
|
|
118
|
-
<!--
|
|
119
|
-
<
|
|
120
|
-
${
|
|
313
|
+
<!-- Background -->
|
|
314
|
+
<rect width="${W}" height="${H_FIXED}" rx="12" fill="${t.bg}"/>
|
|
315
|
+
<rect x="0.5" y="0.5" width="${W - 1}" height="${H_FIXED - 1}" rx="12" fill="none" stroke="${t.border}" stroke-width="1"/>
|
|
316
|
+
|
|
317
|
+
<!-- Title bar -->
|
|
318
|
+
<rect x="1" y="1" width="${W - 2}" height="38" rx="12" fill="${t.bg2}"/>
|
|
319
|
+
<rect x="1" y="22" width="${W - 2}" height="17" fill="${t.bg2}"/>
|
|
320
|
+
<circle cx="20" cy="20" r="5" fill="#ef4444" opacity="0.7"/>
|
|
321
|
+
<circle cx="36" cy="20" r="5" fill="#f59e0b" opacity="0.7"/>
|
|
322
|
+
<circle cx="52" cy="20" r="5" fill="#22c55e" opacity="0.7"/>
|
|
323
|
+
<text x="${W / 2}" y="25" fill="${t.text2}" font-size="13" font-weight="600" font-family=${F} text-anchor="middle">agentlytics.io</text>
|
|
324
|
+
|
|
325
|
+
<!-- Accent line -->
|
|
326
|
+
<rect x="${pad}" y="44" width="60" height="2" rx="1" fill="url(#accentGrad)"/>
|
|
327
|
+
|
|
328
|
+
<!-- Branding -->
|
|
329
|
+
<g transform="translate(${pad}, 52) scale(0.8)" stroke="#818cf8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none">
|
|
330
|
+
<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
|
+
</g>
|
|
332
|
+
<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}>Your AI coding stats</text>
|
|
334
|
+
<text x="${W - pad}" y="68" fill="${t.text5}" font-size="12" font-family=${F} text-anchor="end">${esc(dateStr)}</text>
|
|
335
|
+
|
|
336
|
+
<!-- Divider -->
|
|
337
|
+
<line x1="${pad}" y1="78" x2="${W - pad}" y2="78" stroke="${t.border}" stroke-width="1"/>
|
|
121
338
|
|
|
122
|
-
<!--
|
|
123
|
-
|
|
124
|
-
<polyline points="${sparkPoints}" fill="none" stroke="#888" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/>
|
|
125
|
-
<text x="590" y="${180 + sparkH + 14}" fill="#555" font-size="8" font-family="${F}">00:00</text>
|
|
126
|
-
<text x="${590 + sparkW - 28}" y="${180 + sparkH + 14}" fill="#555" font-size="8" font-family="${F}">23:00</text>
|
|
339
|
+
<!-- KPI cards -->
|
|
340
|
+
${kpiCards.join('')}
|
|
127
341
|
|
|
128
|
-
<!--
|
|
129
|
-
|
|
130
|
-
${modelsList}
|
|
342
|
+
<!-- Sections -->
|
|
343
|
+
${sectionSvgs.join('')}
|
|
131
344
|
|
|
132
|
-
<!-- Token
|
|
133
|
-
|
|
134
|
-
<text x="24" y="${H - 44}" fill="#666" font-size="9" font-family="${F}">in:${fmt(tk.input)} out:${fmt(tk.output)} cache:${fmt(tk.cacheRead)} tools:${fmt(stats.totalToolCalls || 0)} editors:${editors.length}</text>
|
|
345
|
+
<!-- Token footer -->
|
|
346
|
+
${tokenLine}
|
|
135
347
|
|
|
136
348
|
<!-- Footer -->
|
|
137
|
-
<line x1="
|
|
138
|
-
<text x="
|
|
139
|
-
|
|
349
|
+
<line x1="${pad}" y1="${footerY}" x2="${W - pad}" y2="${footerY}" stroke="${t.border}" stroke-width="1"/>
|
|
350
|
+
<text x="${pad}" y="${footerY + 14}" fill="${t.text5}" font-size="11" font-family=${F}>github.com/f/agentlytics</text>
|
|
351
|
+
${usernameText}
|
|
352
|
+
<text x="${W - pad}" y="${footerY + 14}" fill="${t.text5}" font-size="11" font-family=${F} text-anchor="end">npx agentlytics</text>
|
|
140
353
|
</svg>`;
|
|
141
354
|
|
|
142
355
|
return svg;
|
package/ui/src/App.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
2
2
|
import { Routes, Route, NavLink } from 'react-router-dom'
|
|
3
|
-
import { Activity, BarChart3, GitCompare, MessageSquare, FolderOpen, DollarSign, Sun, Moon, RefreshCw, AlertTriangle, Github, Terminal, Database, Users,
|
|
3
|
+
import { Activity, BarChart3, GitCompare, MessageSquare, FolderOpen, DollarSign, Sun, Moon, RefreshCw, AlertTriangle, Github, Terminal, Database, Users, Plug, Copy, Check, Settings as SettingsIcon } from 'lucide-react'
|
|
4
4
|
import { fetchOverview, refetchAgents, fetchMode, fetchRelayConfig, getAuthToken, setOnAuthFailure } from './lib/api'
|
|
5
5
|
import { useTheme } from './lib/theme'
|
|
6
6
|
import AnimatedLogo from './components/AnimatedLogo'
|
|
@@ -9,7 +9,6 @@ import Dashboard from './pages/Dashboard'
|
|
|
9
9
|
import Sessions from './pages/Sessions'
|
|
10
10
|
import DeepAnalysis from './pages/DeepAnalysis'
|
|
11
11
|
import Compare from './pages/Compare'
|
|
12
|
-
import ChatDetail from './pages/ChatDetail'
|
|
13
12
|
import Projects from './pages/Projects'
|
|
14
13
|
import ProjectDetail from './pages/ProjectDetail'
|
|
15
14
|
import CostAnalysis from './pages/CostAnalysis'
|
|
@@ -4,15 +4,6 @@ import EditorDot from './EditorDot'
|
|
|
4
4
|
import { editorLabel, formatNumber } from '../lib/constants'
|
|
5
5
|
import { fetchRelayFeed } from '../lib/api'
|
|
6
6
|
|
|
7
|
-
function timeAgo(ts) {
|
|
8
|
-
if (!ts) return ''
|
|
9
|
-
const diff = Date.now() - ts
|
|
10
|
-
if (diff < 60000) return 'just now'
|
|
11
|
-
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`
|
|
12
|
-
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`
|
|
13
|
-
return `${Math.floor(diff / 86400000)}d ago`
|
|
14
|
-
}
|
|
15
|
-
|
|
16
7
|
function timeLabel(ts) {
|
|
17
8
|
if (!ts) return ''
|
|
18
9
|
const d = new Date(ts)
|
|
@@ -22,7 +13,6 @@ function timeLabel(ts) {
|
|
|
22
13
|
export default function LiveFeed({ onSessionClick }) {
|
|
23
14
|
const [items, setItems] = useState([])
|
|
24
15
|
const scrollRef = useRef(null)
|
|
25
|
-
const prevCountRef = useRef(0)
|
|
26
16
|
|
|
27
17
|
useEffect(() => {
|
|
28
18
|
const load = () => {
|
|
@@ -146,22 +146,3 @@ export default function MessageContent({ content, toolCallDetails }) {
|
|
|
146
146
|
})
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
/**
|
|
150
|
-
* Renders a single message bubble with role icon, model tag, and content.
|
|
151
|
-
*/
|
|
152
|
-
export function MessageBubble({ msg, toolCallDetails }) {
|
|
153
|
-
const cfg = ROLE_CONFIG[msg.role] || ROLE_CONFIG.system
|
|
154
|
-
const Icon = cfg.icon
|
|
155
|
-
return (
|
|
156
|
-
<div className="rounded-r-lg px-4 py-3" style={{ borderLeft: `2px solid ${cfg.borderColor}`, background: cfg.bg }}>
|
|
157
|
-
<div className="flex items-center gap-2 text-xs mb-2" style={{ color: 'var(--c-text2)' }}>
|
|
158
|
-
<Icon size={13} />
|
|
159
|
-
<span className="font-medium">{cfg.label}</span>
|
|
160
|
-
{msg.model && <span className="font-mono" style={{ color: 'var(--c-accent)', opacity: 0.6 }}>· {msg.model}</span>}
|
|
161
|
-
</div>
|
|
162
|
-
<div className="text-sm" style={{ color: 'var(--c-text)' }}>
|
|
163
|
-
<MessageContent content={msg.content} toolCallDetails={toolCallDetails} />
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
)
|
|
167
|
-
}
|