claudeck 1.0.0
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/LICENSE +21 -0
- package/README.md +233 -0
- package/cli.js +2 -0
- package/config/agent-chains.json +16 -0
- package/config/agent-dags.json +16 -0
- package/config/agents.json +46 -0
- package/config/bot-prompt.json +3 -0
- package/config/folders.json +66 -0
- package/config/prompts.json +92 -0
- package/config/repos.json +86 -0
- package/config/telegram-config.json +17 -0
- package/config/workflows.json +90 -0
- package/db.js +1198 -0
- package/package.json +55 -0
- package/plugins/claude-editor/client.css +171 -0
- package/plugins/claude-editor/client.js +183 -0
- package/plugins/event-stream/client.css +207 -0
- package/plugins/event-stream/client.js +271 -0
- package/plugins/linear/client.css +345 -0
- package/plugins/linear/client.js +380 -0
- package/plugins/linear/config.json +5 -0
- package/plugins/linear/server.js +312 -0
- package/plugins/repos/client.css +549 -0
- package/plugins/repos/client.js +663 -0
- package/plugins/repos/server.js +232 -0
- package/plugins/sudoku/client.css +196 -0
- package/plugins/sudoku/client.js +329 -0
- package/plugins/tasks/client.css +414 -0
- package/plugins/tasks/client.js +394 -0
- package/plugins/tasks/server.js +116 -0
- package/plugins/tic-tac-toe/client.css +167 -0
- package/plugins/tic-tac-toe/client.js +241 -0
- package/public/css/core/components.css +232 -0
- package/public/css/core/layout.css +330 -0
- package/public/css/core/print.css +18 -0
- package/public/css/core/reset.css +36 -0
- package/public/css/core/responsive.css +378 -0
- package/public/css/core/theme.css +116 -0
- package/public/css/core/variables.css +93 -0
- package/public/css/features/agent-monitor.css +297 -0
- package/public/css/features/agent-sidebar.css +525 -0
- package/public/css/features/agents.css +996 -0
- package/public/css/features/analytics.css +181 -0
- package/public/css/features/background-sessions.css +321 -0
- package/public/css/features/cost-dashboard.css +168 -0
- package/public/css/features/home.css +313 -0
- package/public/css/features/retro-terminal.css +88 -0
- package/public/css/features/telegram.css +127 -0
- package/public/css/features/tour.css +148 -0
- package/public/css/features/voice-input.css +60 -0
- package/public/css/features/welcome.css +241 -0
- package/public/css/panels/assistant-bot.css +442 -0
- package/public/css/panels/dev-docs.css +292 -0
- package/public/css/panels/file-explorer.css +322 -0
- package/public/css/panels/git-panel.css +221 -0
- package/public/css/panels/mcp-manager.css +199 -0
- package/public/css/panels/tips-feed.css +353 -0
- package/public/css/ui/commands.css +273 -0
- package/public/css/ui/context-gauge.css +76 -0
- package/public/css/ui/file-picker.css +69 -0
- package/public/css/ui/image-attachments.css +106 -0
- package/public/css/ui/messages.css +884 -0
- package/public/css/ui/modals.css +122 -0
- package/public/css/ui/parallel.css +217 -0
- package/public/css/ui/permissions.css +110 -0
- package/public/css/ui/right-panel.css +481 -0
- package/public/css/ui/sessions.css +689 -0
- package/public/css/ui/status-bar.css +425 -0
- package/public/css/ui/toolbox.css +206 -0
- package/public/data/tips.json +218 -0
- package/public/icons/favicon.png +0 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/whaly.png +0 -0
- package/public/index.html +1140 -0
- package/public/js/core/api.js +591 -0
- package/public/js/core/constants.js +3 -0
- package/public/js/core/dom.js +270 -0
- package/public/js/core/events.js +10 -0
- package/public/js/core/plugin-loader.js +153 -0
- package/public/js/core/store.js +39 -0
- package/public/js/core/utils.js +25 -0
- package/public/js/core/ws.js +64 -0
- package/public/js/features/agent-monitor.js +222 -0
- package/public/js/features/agents.js +1209 -0
- package/public/js/features/analytics.js +397 -0
- package/public/js/features/attachments.js +251 -0
- package/public/js/features/background-sessions.js +475 -0
- package/public/js/features/chat.js +589 -0
- package/public/js/features/cost-dashboard.js +152 -0
- package/public/js/features/dag-editor.js +399 -0
- package/public/js/features/easter-egg.js +46 -0
- package/public/js/features/home.js +270 -0
- package/public/js/features/projects.js +372 -0
- package/public/js/features/prompts.js +228 -0
- package/public/js/features/sessions.js +332 -0
- package/public/js/features/telegram.js +131 -0
- package/public/js/features/tour.js +210 -0
- package/public/js/features/voice-input.js +185 -0
- package/public/js/features/welcome.js +43 -0
- package/public/js/features/workflows.js +277 -0
- package/public/js/main.js +51 -0
- package/public/js/panels/assistant-bot.js +445 -0
- package/public/js/panels/dev-docs.js +380 -0
- package/public/js/panels/file-explorer.js +486 -0
- package/public/js/panels/git-panel.js +285 -0
- package/public/js/panels/mcp-manager.js +311 -0
- package/public/js/panels/tips-feed.js +303 -0
- package/public/js/ui/commands.js +114 -0
- package/public/js/ui/context-gauge.js +100 -0
- package/public/js/ui/diff.js +124 -0
- package/public/js/ui/disabled-tools.js +36 -0
- package/public/js/ui/export.js +74 -0
- package/public/js/ui/formatting.js +206 -0
- package/public/js/ui/header-dropdowns.js +72 -0
- package/public/js/ui/input-meta.js +71 -0
- package/public/js/ui/max-turns.js +21 -0
- package/public/js/ui/messages.js +387 -0
- package/public/js/ui/model-selector.js +20 -0
- package/public/js/ui/notifications.js +232 -0
- package/public/js/ui/parallel.js +176 -0
- package/public/js/ui/permissions.js +168 -0
- package/public/js/ui/right-panel.js +173 -0
- package/public/js/ui/shortcuts.js +143 -0
- package/public/js/ui/sidebar-toggle.js +29 -0
- package/public/js/ui/status-bar.js +172 -0
- package/public/js/ui/tab-sdk.js +623 -0
- package/public/js/ui/theme.js +38 -0
- package/public/manifest.json +13 -0
- package/public/offline.html +190 -0
- package/public/style.css +42 -0
- package/public/sw.js +91 -0
- package/server/agent-loop.js +385 -0
- package/server/dag-executor.js +265 -0
- package/server/orchestrator.js +514 -0
- package/server/paths.js +61 -0
- package/server/plugin-mount.js +56 -0
- package/server/push-sender.js +31 -0
- package/server/routes/agents.js +294 -0
- package/server/routes/bot.js +45 -0
- package/server/routes/exec.js +35 -0
- package/server/routes/files.js +218 -0
- package/server/routes/mcp.js +82 -0
- package/server/routes/messages.js +36 -0
- package/server/routes/notifications.js +37 -0
- package/server/routes/projects.js +207 -0
- package/server/routes/prompts.js +53 -0
- package/server/routes/sessions.js +103 -0
- package/server/routes/stats.js +143 -0
- package/server/routes/telegram.js +71 -0
- package/server/routes/tips.js +135 -0
- package/server/routes/workflows.js +81 -0
- package/server/summarizer.js +55 -0
- package/server/telegram-poller.js +205 -0
- package/server/telegram-sender.js +304 -0
- package/server/ws-handler.js +926 -0
- package/server.js +179 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// Analytics dashboard — renders inline on the home page
|
|
2
|
+
import { $ } from '../core/dom.js';
|
|
3
|
+
import { escapeHtml } from '../core/utils.js';
|
|
4
|
+
import { getState } from '../core/store.js';
|
|
5
|
+
import * as api from '../core/api.js';
|
|
6
|
+
|
|
7
|
+
function formatCost(n) {
|
|
8
|
+
if (n >= 100) return '$' + n.toFixed(0);
|
|
9
|
+
if (n >= 1) return '$' + n.toFixed(2);
|
|
10
|
+
return '$' + n.toFixed(4);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatNumber(n) {
|
|
14
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
|
|
15
|
+
if (n >= 1_000) return (n / 1_000).toFixed(1) + 'k';
|
|
16
|
+
return String(n);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function pct(value, max) {
|
|
20
|
+
return max > 0 ? Math.round((value / max) * 100) : 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function renderBarChart(container, items, labelKey, valueKey, formatFn = String) {
|
|
24
|
+
container.innerHTML = '';
|
|
25
|
+
if (!items || items.length === 0) {
|
|
26
|
+
container.innerHTML = '<div class="analytics-empty">No data</div>';
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const maxVal = Math.max(...items.map(d => d[valueKey]), 0.001);
|
|
30
|
+
for (const item of items) {
|
|
31
|
+
const p = pct(item[valueKey], maxVal);
|
|
32
|
+
const row = document.createElement('div');
|
|
33
|
+
row.className = 'cost-chart-row';
|
|
34
|
+
row.innerHTML = `
|
|
35
|
+
<span class="cost-chart-label">${escapeHtml(String(item[labelKey]))}</span>
|
|
36
|
+
<div class="cost-chart-bar-bg"><div class="cost-chart-bar" style="width:${p}%"></div></div>
|
|
37
|
+
<span class="cost-chart-value">${formatFn(item[valueKey])}</span>
|
|
38
|
+
`;
|
|
39
|
+
container.appendChild(row);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sortTable(tbody, colIdx, numeric) {
|
|
44
|
+
const current = tbody.dataset.sortCol === String(colIdx) && tbody.dataset.sortDir === 'desc' ? 'asc' : 'desc';
|
|
45
|
+
tbody.dataset.sortCol = colIdx;
|
|
46
|
+
tbody.dataset.sortDir = current;
|
|
47
|
+
const rows = [...tbody.querySelectorAll('tr')];
|
|
48
|
+
rows.sort((a, b) => {
|
|
49
|
+
const aVal = a.children[colIdx].textContent;
|
|
50
|
+
const bVal = b.children[colIdx].textContent;
|
|
51
|
+
if (!numeric) return current === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
|
|
52
|
+
const aNum = parseFloat(aVal.replace(/[$,%km]/g, '')) || 0;
|
|
53
|
+
const bNum = parseFloat(bVal.replace(/[$,%km]/g, '')) || 0;
|
|
54
|
+
return current === 'asc' ? aNum - bNum : bNum - aNum;
|
|
55
|
+
});
|
|
56
|
+
tbody.innerHTML = '';
|
|
57
|
+
rows.forEach(r => tbody.appendChild(r));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function renderTable(container, columns, rows) {
|
|
61
|
+
if (!rows || rows.length === 0) {
|
|
62
|
+
container.innerHTML += '<div class="analytics-empty">No data</div>';
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const table = document.createElement('table');
|
|
66
|
+
table.className = 'cost-table';
|
|
67
|
+
|
|
68
|
+
const thead = document.createElement('thead');
|
|
69
|
+
const headRow = document.createElement('tr');
|
|
70
|
+
columns.forEach((col, idx) => {
|
|
71
|
+
const th = document.createElement('th');
|
|
72
|
+
th.textContent = col.label;
|
|
73
|
+
th.onclick = () => sortTable(tbody, idx, col.numeric);
|
|
74
|
+
headRow.appendChild(th);
|
|
75
|
+
});
|
|
76
|
+
thead.appendChild(headRow);
|
|
77
|
+
table.appendChild(thead);
|
|
78
|
+
|
|
79
|
+
const tbody = document.createElement('tbody');
|
|
80
|
+
for (const row of rows) {
|
|
81
|
+
const tr = document.createElement('tr');
|
|
82
|
+
columns.forEach(col => {
|
|
83
|
+
const td = document.createElement('td');
|
|
84
|
+
const val = row[col.key];
|
|
85
|
+
td.textContent = col.format ? col.format(val) : (val ?? '');
|
|
86
|
+
if (col.title && row[col.title]) td.title = row[col.title];
|
|
87
|
+
tr.appendChild(td);
|
|
88
|
+
});
|
|
89
|
+
tbody.appendChild(tr);
|
|
90
|
+
}
|
|
91
|
+
table.appendChild(tbody);
|
|
92
|
+
|
|
93
|
+
const wrap = document.createElement('div');
|
|
94
|
+
wrap.className = 'cost-table-container';
|
|
95
|
+
wrap.appendChild(table);
|
|
96
|
+
container.appendChild(wrap);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function section(title) {
|
|
100
|
+
const div = document.createElement('div');
|
|
101
|
+
div.className = 'analytics-section';
|
|
102
|
+
const h4 = document.createElement('h4');
|
|
103
|
+
h4.textContent = title;
|
|
104
|
+
div.appendChild(h4);
|
|
105
|
+
return div;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function renderAnalytics(data, el) {
|
|
109
|
+
el.innerHTML = '';
|
|
110
|
+
|
|
111
|
+
// 1. Overview cards
|
|
112
|
+
const o = data.overview;
|
|
113
|
+
const cardsDiv = document.createElement('div');
|
|
114
|
+
cardsDiv.className = 'analytics-cards';
|
|
115
|
+
cardsDiv.innerHTML = `
|
|
116
|
+
<div class="cost-card">
|
|
117
|
+
<div class="cost-card-label">Total Cost</div>
|
|
118
|
+
<div class="cost-card-value">${formatCost(o.totalCost)}</div>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="cost-card">
|
|
121
|
+
<div class="cost-card-label">Sessions</div>
|
|
122
|
+
<div class="cost-card-value">${formatNumber(o.sessions)}</div>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="cost-card">
|
|
125
|
+
<div class="cost-card-label">Queries</div>
|
|
126
|
+
<div class="cost-card-value">${formatNumber(o.queries)}</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="cost-card">
|
|
129
|
+
<div class="cost-card-label">Turns</div>
|
|
130
|
+
<div class="cost-card-value">${formatNumber(o.totalTurns)}</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="cost-card">
|
|
133
|
+
<div class="cost-card-label">Output Tokens</div>
|
|
134
|
+
<div class="cost-card-value">${formatNumber(o.totalOutputTokens)}</div>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="cost-card">
|
|
137
|
+
<div class="cost-card-label">Errors</div>
|
|
138
|
+
<div class="cost-card-value${o.errorRate > 5 ? ' analytics-error-value' : ''}">${data.errorCategories ? formatNumber(data.errorCategories.reduce((s, c) => s + c.count, 0)) : '0'} <span style="font-size:10px;color:var(--text-dim)">(${o.errorRate.toFixed(1)}%)</span></div>
|
|
139
|
+
${data.errorCategories && data.errorCategories.length > 0 ? `<div style="font-size:9px;color:var(--text-dim);margin-top:2px">Top: ${escapeHtml(data.errorCategories[0].category)}</div>` : ''}
|
|
140
|
+
</div>
|
|
141
|
+
`;
|
|
142
|
+
el.appendChild(cardsDiv);
|
|
143
|
+
|
|
144
|
+
// 2. Daily Activity
|
|
145
|
+
const dailySec = section('Daily Activity (Last 30 Days)');
|
|
146
|
+
const dailyChart = document.createElement('div');
|
|
147
|
+
dailyChart.className = 'cost-chart';
|
|
148
|
+
renderBarChart(dailyChart, data.dailyBreakdown, 'date', 'cost', v => formatCost(v));
|
|
149
|
+
dailyChart.querySelectorAll('.cost-chart-label').forEach(lbl => {
|
|
150
|
+
lbl.textContent = lbl.textContent.slice(5);
|
|
151
|
+
});
|
|
152
|
+
dailySec.appendChild(dailyChart);
|
|
153
|
+
el.appendChild(dailySec);
|
|
154
|
+
|
|
155
|
+
// 3. Hourly Activity
|
|
156
|
+
const hourlySec = section('Activity by Hour');
|
|
157
|
+
const hourlyChart = document.createElement('div');
|
|
158
|
+
hourlyChart.className = 'cost-chart';
|
|
159
|
+
const hourMap = new Map(data.hourlyActivity.map(h => [h.hour, h]));
|
|
160
|
+
const allHours = [];
|
|
161
|
+
for (let h = 0; h < 24; h++) {
|
|
162
|
+
allHours.push(hourMap.get(h) || { hour: h, queries: 0, cost: 0 });
|
|
163
|
+
}
|
|
164
|
+
renderBarChart(hourlyChart, allHours, 'hour', 'queries', v => String(v));
|
|
165
|
+
hourlyChart.querySelectorAll('.cost-chart-label').forEach(lbl => {
|
|
166
|
+
const h = parseInt(lbl.textContent);
|
|
167
|
+
lbl.textContent = `${h.toString().padStart(2, '0')}:00`;
|
|
168
|
+
});
|
|
169
|
+
hourlySec.appendChild(hourlyChart);
|
|
170
|
+
el.appendChild(hourlySec);
|
|
171
|
+
|
|
172
|
+
// 4. Project Breakdown
|
|
173
|
+
if (data.projectBreakdown.length > 1) {
|
|
174
|
+
const projSec = section('Project Breakdown');
|
|
175
|
+
renderTable(projSec, [
|
|
176
|
+
{ label: 'Project', key: 'name' },
|
|
177
|
+
{ label: 'Sessions', key: 'sessions', numeric: true },
|
|
178
|
+
{ label: 'Queries', key: 'queries', numeric: true },
|
|
179
|
+
{ label: 'Cost', key: 'totalCost', numeric: true, format: formatCost },
|
|
180
|
+
{ label: 'Avg Cost', key: 'avgCost', numeric: true, format: formatCost },
|
|
181
|
+
{ label: 'Avg Turns', key: 'avgTurns', numeric: true, format: v => String(Math.round(v)) },
|
|
182
|
+
], data.projectBreakdown);
|
|
183
|
+
el.appendChild(projSec);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 5. Tool Usage
|
|
187
|
+
if (data.toolUsage.length > 0) {
|
|
188
|
+
const toolSec = section('Tool Usage');
|
|
189
|
+
const toolChart = document.createElement('div');
|
|
190
|
+
toolChart.className = 'cost-chart';
|
|
191
|
+
const errorMap = new Map((data.toolErrors || []).map(e => [e.name, e]));
|
|
192
|
+
const maxCount = Math.max(...data.toolUsage.map(t => t.count), 1);
|
|
193
|
+
for (const tool of data.toolUsage) {
|
|
194
|
+
const p = pct(tool.count, maxCount);
|
|
195
|
+
const row = document.createElement('div');
|
|
196
|
+
row.className = 'cost-chart-row';
|
|
197
|
+
const err = errorMap.get(tool.name);
|
|
198
|
+
const errHtml = err
|
|
199
|
+
? ` <span class="analytics-error-badge">${err.errors} err (${err.errorRate.toFixed(0)}%)</span>`
|
|
200
|
+
: '';
|
|
201
|
+
row.innerHTML = `
|
|
202
|
+
<span class="cost-chart-label">${escapeHtml(tool.name)}</span>
|
|
203
|
+
<div class="cost-chart-bar-bg"><div class="cost-chart-bar" style="width:${p}%"></div></div>
|
|
204
|
+
<span class="cost-chart-value">${tool.count}${errHtml}</span>
|
|
205
|
+
`;
|
|
206
|
+
toolChart.appendChild(row);
|
|
207
|
+
}
|
|
208
|
+
toolSec.appendChild(toolChart);
|
|
209
|
+
el.appendChild(toolSec);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 5b. Error Categories
|
|
213
|
+
if (data.errorCategories && data.errorCategories.length > 0) {
|
|
214
|
+
const errCatSec = section('Error Categories');
|
|
215
|
+
const errCatChart = document.createElement('div');
|
|
216
|
+
errCatChart.className = 'cost-chart analytics-error-bar';
|
|
217
|
+
const totalErrors = data.errorCategories.reduce((s, c) => s + c.count, 0);
|
|
218
|
+
const maxCat = Math.max(...data.errorCategories.map(c => c.count), 1);
|
|
219
|
+
for (const cat of data.errorCategories) {
|
|
220
|
+
const p = pct(cat.count, maxCat);
|
|
221
|
+
const catPct = totalErrors > 0 ? (cat.count / totalErrors * 100).toFixed(1) : '0';
|
|
222
|
+
const row = document.createElement('div');
|
|
223
|
+
row.className = 'cost-chart-row';
|
|
224
|
+
row.innerHTML = `
|
|
225
|
+
<span class="cost-chart-label analytics-error-category-label">${escapeHtml(cat.category)}</span>
|
|
226
|
+
<div class="cost-chart-bar-bg"><div class="cost-chart-bar" style="width:${p}%"></div></div>
|
|
227
|
+
<span class="cost-chart-value">${cat.count} (${catPct}%)</span>
|
|
228
|
+
`;
|
|
229
|
+
errCatChart.appendChild(row);
|
|
230
|
+
}
|
|
231
|
+
errCatSec.appendChild(errCatChart);
|
|
232
|
+
el.appendChild(errCatSec);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 5c. Error Timeline
|
|
236
|
+
if (data.errorTimeline && data.errorTimeline.length > 0) {
|
|
237
|
+
const errTimeSec = section('Error Timeline (Last 30 Days)');
|
|
238
|
+
const errTimeChart = document.createElement('div');
|
|
239
|
+
errTimeChart.className = 'cost-chart analytics-error-bar';
|
|
240
|
+
renderBarChart(errTimeChart, data.errorTimeline, 'date', 'errors', String);
|
|
241
|
+
errTimeChart.querySelectorAll('.cost-chart-label').forEach(lbl => {
|
|
242
|
+
lbl.textContent = lbl.textContent.slice(5); // MM-DD
|
|
243
|
+
});
|
|
244
|
+
errTimeSec.appendChild(errTimeChart);
|
|
245
|
+
el.appendChild(errTimeSec);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 5d. Top Failing Tools
|
|
249
|
+
if (data.errorsByTool && data.errorsByTool.length > 0) {
|
|
250
|
+
const errToolSec = section('Top Failing Tools');
|
|
251
|
+
const errToolChart = document.createElement('div');
|
|
252
|
+
errToolChart.className = 'cost-chart analytics-error-bar';
|
|
253
|
+
const toolMap = new Map();
|
|
254
|
+
for (const row of data.errorsByTool) {
|
|
255
|
+
if (!toolMap.has(row.tool)) toolMap.set(row.tool, { tool: row.tool, errors: 0, categories: [] });
|
|
256
|
+
const entry = toolMap.get(row.tool);
|
|
257
|
+
entry.errors += row.errors;
|
|
258
|
+
if (entry.categories.length < 2) entry.categories.push(row.category);
|
|
259
|
+
}
|
|
260
|
+
const toolList = [...toolMap.values()].sort((a, b) => b.errors - a.errors);
|
|
261
|
+
const maxToolErr = Math.max(...toolList.map(t => t.errors), 1);
|
|
262
|
+
for (const tool of toolList) {
|
|
263
|
+
const p = pct(tool.errors, maxToolErr);
|
|
264
|
+
const badges = tool.categories.map(c => `<span class="analytics-error-category-badge">${escapeHtml(c)}</span>`).join('');
|
|
265
|
+
const row = document.createElement('div');
|
|
266
|
+
row.className = 'cost-chart-row';
|
|
267
|
+
row.innerHTML = `
|
|
268
|
+
<span class="cost-chart-label analytics-error-category-label">${escapeHtml(tool.tool)}</span>
|
|
269
|
+
<div class="cost-chart-bar-bg"><div class="cost-chart-bar" style="width:${p}%"></div></div>
|
|
270
|
+
<span class="cost-chart-value">${tool.errors}${badges}</span>
|
|
271
|
+
`;
|
|
272
|
+
errToolChart.appendChild(row);
|
|
273
|
+
}
|
|
274
|
+
errToolSec.appendChild(errToolChart);
|
|
275
|
+
el.appendChild(errToolSec);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 5e. Recent Errors
|
|
279
|
+
if (data.recentErrors && data.recentErrors.length > 0) {
|
|
280
|
+
const errRecSec = section('Recent Errors');
|
|
281
|
+
const errList = document.createElement('div');
|
|
282
|
+
errList.className = 'analytics-error-list';
|
|
283
|
+
for (const err of data.recentErrors) {
|
|
284
|
+
const item = document.createElement('div');
|
|
285
|
+
item.className = 'analytics-error-item';
|
|
286
|
+
const ts = new Date(err.timestamp * 1000);
|
|
287
|
+
const timeStr = ts.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ' ' + ts.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
|
288
|
+
item.innerHTML = `
|
|
289
|
+
<div class="analytics-error-item-header">
|
|
290
|
+
<span class="analytics-error-tool">${escapeHtml(err.tool)}</span>
|
|
291
|
+
${err.session_title ? `<span style="font-size:10px;color:var(--text-dim)">${escapeHtml(err.session_title)}</span>` : ''}
|
|
292
|
+
<span class="analytics-error-time">${timeStr}</span>
|
|
293
|
+
</div>
|
|
294
|
+
<div class="analytics-error-preview">${escapeHtml(err.preview || '')}</div>
|
|
295
|
+
`;
|
|
296
|
+
item.addEventListener('click', () => {
|
|
297
|
+
const existing = item.querySelector('.analytics-error-full');
|
|
298
|
+
if (existing) { existing.remove(); return; }
|
|
299
|
+
const full = document.createElement('div');
|
|
300
|
+
full.className = 'analytics-error-full';
|
|
301
|
+
full.textContent = err.full_content || err.preview || '';
|
|
302
|
+
item.appendChild(full);
|
|
303
|
+
});
|
|
304
|
+
errList.appendChild(item);
|
|
305
|
+
}
|
|
306
|
+
errRecSec.appendChild(errList);
|
|
307
|
+
el.appendChild(errRecSec);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 6. Top Sessions
|
|
311
|
+
if (data.topSessions.length > 0) {
|
|
312
|
+
const sesSec = section('Top Sessions by Cost');
|
|
313
|
+
renderTable(sesSec, [
|
|
314
|
+
{ label: 'Session', key: 'title', format: v => v || 'Untitled' },
|
|
315
|
+
{ label: 'Project', key: 'project' },
|
|
316
|
+
{ label: 'Cost', key: 'cost', numeric: true, format: formatCost },
|
|
317
|
+
{ label: 'Turns', key: 'turns', numeric: true },
|
|
318
|
+
{ label: 'Duration', key: 'duration_min', numeric: true, format: v => v > 0 ? Math.round(v) + 'm' : '-' },
|
|
319
|
+
], data.topSessions);
|
|
320
|
+
el.appendChild(sesSec);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 7. Session Depth
|
|
324
|
+
if (data.sessionDepth.length > 0) {
|
|
325
|
+
const depthSec = section('Session Depth');
|
|
326
|
+
const depthChart = document.createElement('div');
|
|
327
|
+
depthChart.className = 'cost-chart';
|
|
328
|
+
renderBarChart(depthChart, data.sessionDepth, 'bucket', 'count', String);
|
|
329
|
+
depthSec.appendChild(depthChart);
|
|
330
|
+
el.appendChild(depthSec);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 8. Message Length Distribution
|
|
334
|
+
if (data.msgLength.length > 0) {
|
|
335
|
+
const msgSec = section('Message Length Distribution');
|
|
336
|
+
const msgChart = document.createElement('div');
|
|
337
|
+
msgChart.className = 'cost-chart';
|
|
338
|
+
renderBarChart(msgChart, data.msgLength, 'bucket', 'count', String);
|
|
339
|
+
msgSec.appendChild(msgChart);
|
|
340
|
+
el.appendChild(msgSec);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 9. Top Bash Commands
|
|
344
|
+
if (data.topBashCommands.length > 0) {
|
|
345
|
+
const bashSec = section('Top Bash Commands');
|
|
346
|
+
renderTable(bashSec, [
|
|
347
|
+
{ label: 'Command', key: 'command' },
|
|
348
|
+
{ label: 'Count', key: 'count', numeric: true },
|
|
349
|
+
], data.topBashCommands);
|
|
350
|
+
el.appendChild(bashSec);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 10. Top Files
|
|
354
|
+
if (data.topFiles.length > 0) {
|
|
355
|
+
const filesSec = section('Top Files');
|
|
356
|
+
renderTable(filesSec, [
|
|
357
|
+
{ label: 'Path', key: 'path', format: v => v ? v.split('/').slice(-2).join('/') : '', title: 'path' },
|
|
358
|
+
{ label: 'Tool', key: 'tool' },
|
|
359
|
+
{ label: 'Count', key: 'count', numeric: true },
|
|
360
|
+
], data.topFiles);
|
|
361
|
+
el.appendChild(filesSec);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ── Home page analytics ────────────────────────────────
|
|
366
|
+
const homeAnalyticsFilter = document.getElementById('home-analytics-filter');
|
|
367
|
+
const homeAnalyticsContent = document.getElementById('home-analytics-content');
|
|
368
|
+
|
|
369
|
+
async function loadAnalyticsData(projectPath) {
|
|
370
|
+
homeAnalyticsContent.innerHTML = '<div class="analytics-loading">Loading analytics...</div>';
|
|
371
|
+
try {
|
|
372
|
+
const data = await api.fetchAnalytics(projectPath || undefined);
|
|
373
|
+
renderAnalytics(data, homeAnalyticsContent);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
homeAnalyticsContent.innerHTML = `<div class="analytics-empty">Failed to load analytics: ${escapeHtml(err.message)}</div>`;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export async function loadHomeAnalytics() {
|
|
380
|
+
// Populate project filter — use store data or fetch directly
|
|
381
|
+
let projects = getState('projectsData');
|
|
382
|
+
if (!projects || projects.length === 0) {
|
|
383
|
+
try { projects = await api.fetchProjects(); } catch { projects = []; }
|
|
384
|
+
}
|
|
385
|
+
homeAnalyticsFilter.innerHTML = '<option value="">All Projects</option>';
|
|
386
|
+
for (const p of projects) {
|
|
387
|
+
const opt = document.createElement('option');
|
|
388
|
+
opt.value = p.path;
|
|
389
|
+
opt.textContent = p.name;
|
|
390
|
+
homeAnalyticsFilter.appendChild(opt);
|
|
391
|
+
}
|
|
392
|
+
await loadAnalyticsData(homeAnalyticsFilter.value);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
homeAnalyticsFilter.addEventListener('change', () => {
|
|
396
|
+
loadAnalyticsData(homeAnalyticsFilter.value);
|
|
397
|
+
});
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// File & image attachments
|
|
2
|
+
import { $ } from '../core/dom.js';
|
|
3
|
+
import { getState, setState } from '../core/store.js';
|
|
4
|
+
import * as api from '../core/api.js';
|
|
5
|
+
import { registerCommand } from '../ui/commands.js';
|
|
6
|
+
|
|
7
|
+
const SUPPORTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
8
|
+
const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
9
|
+
|
|
10
|
+
// ── Badge ────────────────────────────────────────────────
|
|
11
|
+
export function updateAttachmentBadge() {
|
|
12
|
+
const attachedFiles = getState("attachedFiles");
|
|
13
|
+
const imageAttachments = getState("imageAttachments");
|
|
14
|
+
const total = attachedFiles.length + imageAttachments.length;
|
|
15
|
+
if (total > 0) {
|
|
16
|
+
$.attachBadge.textContent = total;
|
|
17
|
+
$.attachBadge.classList.remove("hidden");
|
|
18
|
+
} else {
|
|
19
|
+
$.attachBadge.classList.add("hidden");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── File picker (existing) ───────────────────────────────
|
|
24
|
+
export async function openFilePicker() {
|
|
25
|
+
const cwd = $.projectSelect.value;
|
|
26
|
+
if (!cwd) return;
|
|
27
|
+
$.fpModal.classList.remove("hidden");
|
|
28
|
+
$.fpSearch.value = "";
|
|
29
|
+
$.fpSearch.focus();
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const files = await api.fetchFiles(cwd);
|
|
33
|
+
setState("allProjectFiles", files);
|
|
34
|
+
renderFilePicker("");
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error("Failed to load files:", err);
|
|
37
|
+
setState("allProjectFiles", []);
|
|
38
|
+
renderFilePicker("");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function renderFilePicker(filter) {
|
|
43
|
+
$.fpList.innerHTML = "";
|
|
44
|
+
const lower = filter.toLowerCase();
|
|
45
|
+
const allProjectFiles = getState("allProjectFiles");
|
|
46
|
+
const attachedFiles = getState("attachedFiles");
|
|
47
|
+
const filtered = lower
|
|
48
|
+
? allProjectFiles.filter((f) => f.toLowerCase().includes(lower))
|
|
49
|
+
: allProjectFiles;
|
|
50
|
+
|
|
51
|
+
for (const filePath of filtered.slice(0, 200)) {
|
|
52
|
+
const item = document.createElement("div");
|
|
53
|
+
item.className = "file-picker-item";
|
|
54
|
+
const isSelected = attachedFiles.some((f) => f.path === filePath);
|
|
55
|
+
if (isSelected) item.classList.add("selected");
|
|
56
|
+
item.textContent = filePath;
|
|
57
|
+
item.addEventListener("click", () => toggleFileAttachment(filePath, item));
|
|
58
|
+
$.fpList.appendChild(item);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function toggleFileAttachment(filePath, itemEl) {
|
|
63
|
+
const attachedFiles = [...getState("attachedFiles")];
|
|
64
|
+
const idx = attachedFiles.findIndex((f) => f.path === filePath);
|
|
65
|
+
if (idx >= 0) {
|
|
66
|
+
attachedFiles.splice(idx, 1);
|
|
67
|
+
setState("attachedFiles", attachedFiles);
|
|
68
|
+
itemEl.classList.remove("selected");
|
|
69
|
+
} else {
|
|
70
|
+
try {
|
|
71
|
+
const cwd = $.projectSelect.value;
|
|
72
|
+
const data = await api.fetchFileContent(cwd, filePath);
|
|
73
|
+
attachedFiles.push({ path: filePath, content: data.content });
|
|
74
|
+
setState("attachedFiles", attachedFiles);
|
|
75
|
+
itemEl.classList.add("selected");
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error("Failed to read file:", err);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
$.fpCount.textContent = `${attachedFiles.length} file${attachedFiles.length !== 1 ? "s" : ""} selected`;
|
|
82
|
+
updateAttachmentBadge();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function closeFilePicker() {
|
|
86
|
+
$.fpModal.classList.add("hidden");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Image attachments ────────────────────────────────────
|
|
90
|
+
export function addImageAttachment(file) {
|
|
91
|
+
if (!SUPPORTED_IMAGE_TYPES.includes(file.type)) {
|
|
92
|
+
showImageError(`Unsupported image type: ${file.type}. Use PNG, JPEG, GIF, or WebP.`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (file.size > MAX_IMAGE_SIZE) {
|
|
96
|
+
showImageError(`Image too large (${(file.size / 1024 / 1024).toFixed(1)}MB). Max 5MB.`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const reader = new FileReader();
|
|
101
|
+
reader.onload = () => {
|
|
102
|
+
const base64 = reader.result.split(",")[1];
|
|
103
|
+
const images = [...getState("imageAttachments")];
|
|
104
|
+
images.push({ name: file.name, data: base64, mimeType: file.type });
|
|
105
|
+
setState("imageAttachments", images);
|
|
106
|
+
renderImagePreview();
|
|
107
|
+
updateAttachmentBadge();
|
|
108
|
+
};
|
|
109
|
+
reader.readAsDataURL(file);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function removeImageAttachment(index) {
|
|
113
|
+
const images = [...getState("imageAttachments")];
|
|
114
|
+
images.splice(index, 1);
|
|
115
|
+
setState("imageAttachments", images);
|
|
116
|
+
renderImagePreview();
|
|
117
|
+
updateAttachmentBadge();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getImageAttachments() {
|
|
121
|
+
return getState("imageAttachments");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function clearImageAttachments() {
|
|
125
|
+
setState("imageAttachments", []);
|
|
126
|
+
renderImagePreview();
|
|
127
|
+
updateAttachmentBadge();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderImagePreview() {
|
|
131
|
+
const strip = $.imagePreviewStrip;
|
|
132
|
+
const images = getState("imageAttachments");
|
|
133
|
+
strip.innerHTML = "";
|
|
134
|
+
|
|
135
|
+
if (images.length === 0) {
|
|
136
|
+
strip.classList.add("hidden");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
strip.classList.remove("hidden");
|
|
141
|
+
images.forEach((img, i) => {
|
|
142
|
+
const item = document.createElement("div");
|
|
143
|
+
item.className = "image-preview-item";
|
|
144
|
+
|
|
145
|
+
const imgEl = document.createElement("img");
|
|
146
|
+
imgEl.src = `data:${img.mimeType};base64,${img.data}`;
|
|
147
|
+
imgEl.alt = img.name;
|
|
148
|
+
imgEl.title = img.name;
|
|
149
|
+
|
|
150
|
+
const removeBtn = document.createElement("button");
|
|
151
|
+
removeBtn.className = "image-preview-remove";
|
|
152
|
+
removeBtn.textContent = "\u00d7";
|
|
153
|
+
removeBtn.addEventListener("click", (e) => {
|
|
154
|
+
e.stopPropagation();
|
|
155
|
+
removeImageAttachment(i);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
item.appendChild(imgEl);
|
|
159
|
+
item.appendChild(removeBtn);
|
|
160
|
+
strip.appendChild(item);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function showImageError(message) {
|
|
165
|
+
// Use toast container if available
|
|
166
|
+
const container = document.getElementById("toast-container");
|
|
167
|
+
if (container) {
|
|
168
|
+
const toast = document.createElement("div");
|
|
169
|
+
toast.className = "toast error";
|
|
170
|
+
toast.textContent = message;
|
|
171
|
+
container.appendChild(toast);
|
|
172
|
+
setTimeout(() => toast.remove(), 4000);
|
|
173
|
+
} else {
|
|
174
|
+
alert(message);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Event listeners ──────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
// File picker buttons
|
|
181
|
+
$.attachBtn.addEventListener("click", openFilePicker);
|
|
182
|
+
document.getElementById("fp-modal-close").addEventListener("click", closeFilePicker);
|
|
183
|
+
document.getElementById("fp-done-btn").addEventListener("click", closeFilePicker);
|
|
184
|
+
$.fpModal.addEventListener("click", (e) => {
|
|
185
|
+
if (e.target === $.fpModal) closeFilePicker();
|
|
186
|
+
});
|
|
187
|
+
$.fpSearch.addEventListener("input", () => {
|
|
188
|
+
renderFilePicker($.fpSearch.value.trim());
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Image button → open file picker
|
|
192
|
+
$.imageBtn.addEventListener("click", () => {
|
|
193
|
+
$.imageFileInput.click();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Hidden file input change
|
|
197
|
+
$.imageFileInput.addEventListener("change", () => {
|
|
198
|
+
for (const file of $.imageFileInput.files) {
|
|
199
|
+
addImageAttachment(file);
|
|
200
|
+
}
|
|
201
|
+
$.imageFileInput.value = "";
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Paste handler — detect images in clipboard
|
|
205
|
+
document.addEventListener("paste", (e) => {
|
|
206
|
+
// Only handle when focus is in the chat input area
|
|
207
|
+
const active = document.activeElement;
|
|
208
|
+
if (active !== $.messageInput && !$.messageInput.contains(active)) return;
|
|
209
|
+
|
|
210
|
+
const items = e.clipboardData?.items;
|
|
211
|
+
if (!items) return;
|
|
212
|
+
|
|
213
|
+
for (const item of items) {
|
|
214
|
+
if (item.kind === "file" && SUPPORTED_IMAGE_TYPES.includes(item.type)) {
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
addImageAttachment(item.getAsFile());
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Drag-and-drop on message input
|
|
222
|
+
$.messageInput.addEventListener("dragover", (e) => {
|
|
223
|
+
if ([...e.dataTransfer.types].includes("Files")) {
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
$.messageInput.classList.add("drag-highlight");
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
$.messageInput.addEventListener("dragleave", () => {
|
|
230
|
+
$.messageInput.classList.remove("drag-highlight");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
$.messageInput.addEventListener("drop", (e) => {
|
|
234
|
+
$.messageInput.classList.remove("drag-highlight");
|
|
235
|
+
if (!e.dataTransfer.files.length) return;
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
for (const file of e.dataTransfer.files) {
|
|
238
|
+
if (SUPPORTED_IMAGE_TYPES.includes(file.type)) {
|
|
239
|
+
addImageAttachment(file);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ── Commands ─────────────────────────────────────────────
|
|
245
|
+
registerCommand("attach", {
|
|
246
|
+
category: "app",
|
|
247
|
+
description: "Attach files to next message",
|
|
248
|
+
execute() {
|
|
249
|
+
openFilePicker();
|
|
250
|
+
},
|
|
251
|
+
});
|