@chrysb/alphaclaw 0.3.3 → 0.3.4
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/bin/alphaclaw.js +18 -0
- package/lib/plugin/usage-tracker/index.js +308 -0
- package/lib/plugin/usage-tracker/openclaw.plugin.json +8 -0
- package/lib/public/css/explorer.css +51 -1
- package/lib/public/css/shell.css +3 -1
- package/lib/public/css/theme.css +35 -0
- package/lib/public/js/app.js +73 -24
- package/lib/public/js/components/file-tree.js +231 -28
- package/lib/public/js/components/file-viewer.js +193 -20
- package/lib/public/js/components/segmented-control.js +33 -0
- package/lib/public/js/components/sidebar.js +14 -32
- package/lib/public/js/components/telegram-workspace/index.js +353 -0
- package/lib/public/js/components/telegram-workspace/manage.js +397 -0
- package/lib/public/js/components/telegram-workspace/onboarding.js +616 -0
- package/lib/public/js/components/usage-tab.js +528 -0
- package/lib/public/js/components/watchdog-tab.js +1 -1
- package/lib/public/js/lib/api.js +25 -1
- package/lib/public/js/lib/telegram-api.js +78 -0
- package/lib/public/js/lib/ui-settings.js +38 -0
- package/lib/public/setup.html +34 -30
- package/lib/server/alphaclaw-version.js +3 -3
- package/lib/server/constants.js +1 -0
- package/lib/server/onboarding/openclaw.js +15 -0
- package/lib/server/routes/auth.js +5 -1
- package/lib/server/routes/telegram.js +185 -60
- package/lib/server/routes/usage.js +133 -0
- package/lib/server/usage-db.js +570 -0
- package/lib/server.js +21 -1
- package/lib/setup/core-prompts/AGENTS.md +0 -101
- package/package.json +1 -1
- package/lib/public/js/components/telegram-workspace.js +0 -1365
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import {
|
|
5
|
+
fetchUsageSummary,
|
|
6
|
+
fetchUsageSessions,
|
|
7
|
+
fetchUsageSessionDetail,
|
|
8
|
+
} from "../lib/api.js";
|
|
9
|
+
import { readUiSettings, writeUiSettings } from "../lib/ui-settings.js";
|
|
10
|
+
import { PageHeader } from "./page-header.js";
|
|
11
|
+
import { ActionButton } from "./action-button.js";
|
|
12
|
+
import { SegmentedControl } from "./segmented-control.js";
|
|
13
|
+
|
|
14
|
+
const html = htm.bind(h);
|
|
15
|
+
|
|
16
|
+
const kColorPalette = [
|
|
17
|
+
"#7dd3fc",
|
|
18
|
+
"#22d3ee",
|
|
19
|
+
"#34d399",
|
|
20
|
+
"#fbbf24",
|
|
21
|
+
"#fb7185",
|
|
22
|
+
"#a78bfa",
|
|
23
|
+
"#f472b6",
|
|
24
|
+
"#60a5fa",
|
|
25
|
+
"#4ade80",
|
|
26
|
+
"#f97316",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const kTokenFormatter = new Intl.NumberFormat("en-US");
|
|
30
|
+
const kMoneyFormatter = new Intl.NumberFormat("en-US", {
|
|
31
|
+
style: "currency",
|
|
32
|
+
currency: "USD",
|
|
33
|
+
minimumFractionDigits: 2,
|
|
34
|
+
maximumFractionDigits: 3,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const formatTokens = (value) => kTokenFormatter.format(Number(value || 0));
|
|
38
|
+
const formatUsd = (value) => kMoneyFormatter.format(Number(value || 0));
|
|
39
|
+
const formatCountLabel = (value, singular, plural) => {
|
|
40
|
+
const count = Number(value || 0);
|
|
41
|
+
const label = count === 1 ? singular : plural;
|
|
42
|
+
return `${formatTokens(count)} ${label}`;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const formatDateTime = (value) => {
|
|
46
|
+
if (!value) return "n/a";
|
|
47
|
+
try {
|
|
48
|
+
const d = new Date(Number(value));
|
|
49
|
+
if (Number.isNaN(d.getTime())) return "n/a";
|
|
50
|
+
const now = new Date();
|
|
51
|
+
const isToday =
|
|
52
|
+
d.getFullYear() === now.getFullYear()
|
|
53
|
+
&& d.getMonth() === now.getMonth()
|
|
54
|
+
&& d.getDate() === now.getDate();
|
|
55
|
+
if (isToday) {
|
|
56
|
+
return d.toLocaleTimeString();
|
|
57
|
+
}
|
|
58
|
+
return d.toLocaleString();
|
|
59
|
+
} catch {
|
|
60
|
+
return "n/a";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const formatDuration = (value) => {
|
|
65
|
+
const ms = Number(value || 0);
|
|
66
|
+
if (!Number.isFinite(ms) || ms <= 0) return "0s";
|
|
67
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
68
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
69
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
70
|
+
const seconds = totalSeconds % 60;
|
|
71
|
+
if (minutes <= 0) return `${seconds}s`;
|
|
72
|
+
return `${minutes}m ${seconds}s`;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const kBadgeToneClass = {
|
|
76
|
+
cyan: "border-cyan-400/30 text-cyan-300 bg-cyan-400/10",
|
|
77
|
+
blue: "border-blue-400/30 text-blue-300 bg-blue-400/10",
|
|
78
|
+
purple: "border-purple-400/30 text-purple-300 bg-purple-400/10",
|
|
79
|
+
gray: "border-gray-400/30 text-gray-400 bg-gray-400/10",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const SessionBadges = ({ session }) => {
|
|
83
|
+
const labels = session?.labels;
|
|
84
|
+
if (!Array.isArray(labels) || labels.length === 0) {
|
|
85
|
+
const fallback = String(session?.sessionKey || session?.sessionId || "");
|
|
86
|
+
return html`<span class="truncate">${fallback}</span>`;
|
|
87
|
+
}
|
|
88
|
+
return html`
|
|
89
|
+
<span class="inline-flex items-center gap-1.5 flex-wrap">
|
|
90
|
+
${labels.map(
|
|
91
|
+
(badge) => html`
|
|
92
|
+
<span
|
|
93
|
+
class=${`inline-flex items-center px-1.5 py-0.5 rounded border text-[11px] leading-tight ${kBadgeToneClass[badge.tone] || kBadgeToneClass.gray}`}
|
|
94
|
+
>
|
|
95
|
+
${badge.label}
|
|
96
|
+
</span>
|
|
97
|
+
`,
|
|
98
|
+
)}
|
|
99
|
+
</span>
|
|
100
|
+
`;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const toChartColor = (key) => {
|
|
104
|
+
const raw = String(key || "");
|
|
105
|
+
let hash = 0;
|
|
106
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
107
|
+
hash = ((hash << 5) - hash + raw.charCodeAt(index)) | 0;
|
|
108
|
+
}
|
|
109
|
+
return kColorPalette[Math.abs(hash) % kColorPalette.length];
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const kRangeOptions = [
|
|
113
|
+
{ label: "7d", value: 7 },
|
|
114
|
+
{ label: "30d", value: 30 },
|
|
115
|
+
{ label: "90d", value: 90 },
|
|
116
|
+
];
|
|
117
|
+
const kDefaultUsageDays = 30;
|
|
118
|
+
const kDefaultUsageMetric = "tokens";
|
|
119
|
+
const kUsageDaysUiSettingKey = "usageDays";
|
|
120
|
+
const kUsageMetricUiSettingKey = "usageMetric";
|
|
121
|
+
|
|
122
|
+
const SummaryCard = ({ title, tokens, cost }) => html`
|
|
123
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
124
|
+
<h3 class="card-label text-xs">${title}</h3>
|
|
125
|
+
<div class="text-lg font-semibold mt-1">${formatTokens(tokens)} tokens</div>
|
|
126
|
+
<div class="text-xs text-[var(--text-muted)] mt-1">${formatUsd(cost)}</div>
|
|
127
|
+
</div>
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
export const UsageTab = ({ sessionId = "" }) => {
|
|
131
|
+
const [days, setDays] = useState(() => {
|
|
132
|
+
const settings = readUiSettings();
|
|
133
|
+
const parsedDays = Number.parseInt(
|
|
134
|
+
String(settings[kUsageDaysUiSettingKey] ?? ""),
|
|
135
|
+
10,
|
|
136
|
+
);
|
|
137
|
+
return kRangeOptions.some((option) => option.value === parsedDays)
|
|
138
|
+
? parsedDays
|
|
139
|
+
: kDefaultUsageDays;
|
|
140
|
+
});
|
|
141
|
+
const [metric, setMetric] = useState(() => {
|
|
142
|
+
const settings = readUiSettings();
|
|
143
|
+
return settings[kUsageMetricUiSettingKey] === "cost"
|
|
144
|
+
? "cost"
|
|
145
|
+
: kDefaultUsageMetric;
|
|
146
|
+
});
|
|
147
|
+
const [summary, setSummary] = useState(null);
|
|
148
|
+
const [sessions, setSessions] = useState([]);
|
|
149
|
+
const [sessionDetailById, setSessionDetailById] = useState({});
|
|
150
|
+
const [loadingSummary, setLoadingSummary] = useState(false);
|
|
151
|
+
const [loadingSessions, setLoadingSessions] = useState(false);
|
|
152
|
+
const [loadingDetailById, setLoadingDetailById] = useState({});
|
|
153
|
+
const [expandedSessionIds, setExpandedSessionIds] = useState(() =>
|
|
154
|
+
sessionId ? [String(sessionId)] : [],
|
|
155
|
+
);
|
|
156
|
+
const [error, setError] = useState("");
|
|
157
|
+
const overviewCanvasRef = useRef(null);
|
|
158
|
+
const overviewChartRef = useRef(null);
|
|
159
|
+
|
|
160
|
+
const loadSummary = async () => {
|
|
161
|
+
setLoadingSummary(true);
|
|
162
|
+
setError("");
|
|
163
|
+
try {
|
|
164
|
+
const data = await fetchUsageSummary(days);
|
|
165
|
+
setSummary(data.summary || null);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
setError(err.message || "Could not load usage summary");
|
|
168
|
+
} finally {
|
|
169
|
+
setLoadingSummary(false);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const loadSessions = async () => {
|
|
174
|
+
setLoadingSessions(true);
|
|
175
|
+
try {
|
|
176
|
+
const data = await fetchUsageSessions(100);
|
|
177
|
+
setSessions(Array.isArray(data.sessions) ? data.sessions : []);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
setError(err.message || "Could not load sessions");
|
|
180
|
+
} finally {
|
|
181
|
+
setLoadingSessions(false);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const loadSessionDetail = async (selectedSessionId) => {
|
|
186
|
+
const safeSessionId = String(selectedSessionId || "").trim();
|
|
187
|
+
if (!safeSessionId) return;
|
|
188
|
+
setLoadingDetailById((currentValue) => ({
|
|
189
|
+
...currentValue,
|
|
190
|
+
[safeSessionId]: true,
|
|
191
|
+
}));
|
|
192
|
+
try {
|
|
193
|
+
const detailPayload = await fetchUsageSessionDetail(safeSessionId);
|
|
194
|
+
setSessionDetailById((currentValue) => ({
|
|
195
|
+
...currentValue,
|
|
196
|
+
[safeSessionId]: detailPayload.detail || null,
|
|
197
|
+
}));
|
|
198
|
+
} catch (err) {
|
|
199
|
+
setError(err.message || "Could not load session detail");
|
|
200
|
+
} finally {
|
|
201
|
+
setLoadingDetailById((currentValue) => ({
|
|
202
|
+
...currentValue,
|
|
203
|
+
[safeSessionId]: false,
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
loadSummary();
|
|
210
|
+
}, [days]);
|
|
211
|
+
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
const settings = readUiSettings();
|
|
214
|
+
settings[kUsageDaysUiSettingKey] = days;
|
|
215
|
+
settings[kUsageMetricUiSettingKey] = metric;
|
|
216
|
+
writeUiSettings(settings);
|
|
217
|
+
}, [days, metric]);
|
|
218
|
+
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
loadSessions();
|
|
221
|
+
}, []);
|
|
222
|
+
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
const safeSessionId = String(sessionId || "").trim();
|
|
225
|
+
if (!safeSessionId) return;
|
|
226
|
+
setExpandedSessionIds((currentValue) =>
|
|
227
|
+
currentValue.includes(safeSessionId)
|
|
228
|
+
? currentValue
|
|
229
|
+
: [...currentValue, safeSessionId],
|
|
230
|
+
);
|
|
231
|
+
if (!sessionDetailById[safeSessionId] && !loadingDetailById[safeSessionId]) {
|
|
232
|
+
loadSessionDetail(safeSessionId);
|
|
233
|
+
}
|
|
234
|
+
}, [sessionId]);
|
|
235
|
+
|
|
236
|
+
const periodSummary = useMemo(() => {
|
|
237
|
+
const rows = Array.isArray(summary?.daily) ? summary.daily : [];
|
|
238
|
+
const now = new Date();
|
|
239
|
+
const dayKey = now.toISOString().slice(0, 10);
|
|
240
|
+
const weekStart = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
241
|
+
.toISOString()
|
|
242
|
+
.slice(0, 10);
|
|
243
|
+
const monthStart = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
|
|
244
|
+
.toISOString()
|
|
245
|
+
.slice(0, 10);
|
|
246
|
+
const zero = { tokens: 0, cost: 0 };
|
|
247
|
+
return rows.reduce(
|
|
248
|
+
(acc, row) => {
|
|
249
|
+
const tokens = Number(row.totalTokens || 0);
|
|
250
|
+
const cost = Number(row.totalCost || 0);
|
|
251
|
+
if (String(row.date) === dayKey) {
|
|
252
|
+
acc.today.tokens += tokens;
|
|
253
|
+
acc.today.cost += cost;
|
|
254
|
+
}
|
|
255
|
+
if (String(row.date) >= weekStart) {
|
|
256
|
+
acc.week.tokens += tokens;
|
|
257
|
+
acc.week.cost += cost;
|
|
258
|
+
}
|
|
259
|
+
if (String(row.date) >= monthStart) {
|
|
260
|
+
acc.month.tokens += tokens;
|
|
261
|
+
acc.month.cost += cost;
|
|
262
|
+
}
|
|
263
|
+
return acc;
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
today: { ...zero },
|
|
267
|
+
week: { ...zero },
|
|
268
|
+
month: { ...zero },
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
}, [summary]);
|
|
272
|
+
|
|
273
|
+
const overviewDatasets = useMemo(() => {
|
|
274
|
+
const rows = Array.isArray(summary?.daily) ? summary.daily : [];
|
|
275
|
+
const allModels = new Set();
|
|
276
|
+
for (const dayRow of rows) {
|
|
277
|
+
for (const modelRow of dayRow.models || []) {
|
|
278
|
+
allModels.add(String(modelRow.model || "unknown"));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const labels = rows.map((row) => String(row.date || ""));
|
|
282
|
+
const datasets = Array.from(allModels).map((model) => {
|
|
283
|
+
const values = rows.map((row) => {
|
|
284
|
+
const found = (row.models || []).find((m) => String(m.model || "") === model);
|
|
285
|
+
if (!found) return 0;
|
|
286
|
+
return metric === "cost" ? Number(found.totalCost || 0) : Number(found.totalTokens || 0);
|
|
287
|
+
});
|
|
288
|
+
return {
|
|
289
|
+
label: model,
|
|
290
|
+
data: values,
|
|
291
|
+
backgroundColor: toChartColor(model),
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
return { labels, datasets };
|
|
295
|
+
}, [summary, metric]);
|
|
296
|
+
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
const canvas = overviewCanvasRef.current;
|
|
299
|
+
const Chart = window.Chart;
|
|
300
|
+
if (!canvas || !Chart) return;
|
|
301
|
+
if (overviewChartRef.current) {
|
|
302
|
+
overviewChartRef.current.destroy();
|
|
303
|
+
overviewChartRef.current = null;
|
|
304
|
+
}
|
|
305
|
+
overviewChartRef.current = new Chart(canvas, {
|
|
306
|
+
type: "bar",
|
|
307
|
+
data: overviewDatasets,
|
|
308
|
+
options: {
|
|
309
|
+
responsive: true,
|
|
310
|
+
maintainAspectRatio: false,
|
|
311
|
+
interaction: { mode: "index", intersect: false },
|
|
312
|
+
scales: {
|
|
313
|
+
x: { stacked: true, ticks: { color: "rgba(156,163,175,1)" } },
|
|
314
|
+
y: {
|
|
315
|
+
stacked: true,
|
|
316
|
+
ticks: {
|
|
317
|
+
color: "rgba(156,163,175,1)",
|
|
318
|
+
callback: (v) => (metric === "cost" ? `$${Number(v).toFixed(2)}` : formatTokens(v)),
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
plugins: {
|
|
323
|
+
legend: {
|
|
324
|
+
labels: { color: "rgba(209,213,219,1)", boxWidth: 10, boxHeight: 10 },
|
|
325
|
+
},
|
|
326
|
+
tooltip: {
|
|
327
|
+
callbacks: {
|
|
328
|
+
label: (context) => {
|
|
329
|
+
const value = Number(context.parsed.y || 0);
|
|
330
|
+
return metric === "cost"
|
|
331
|
+
? `${context.dataset.label}: ${formatUsd(value)}`
|
|
332
|
+
: `${context.dataset.label}: ${formatTokens(value)} tokens`;
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
return () => {
|
|
340
|
+
if (overviewChartRef.current) {
|
|
341
|
+
overviewChartRef.current.destroy();
|
|
342
|
+
overviewChartRef.current = null;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}, [overviewDatasets, metric]);
|
|
346
|
+
|
|
347
|
+
const renderOverview = () => html`
|
|
348
|
+
<div class="space-y-4">
|
|
349
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
350
|
+
<${SummaryCard} title="Today" tokens=${periodSummary.today.tokens} cost=${periodSummary.today.cost} />
|
|
351
|
+
<${SummaryCard} title="Last 7 days" tokens=${periodSummary.week.tokens} cost=${periodSummary.week.cost} />
|
|
352
|
+
<${SummaryCard} title="Last 30 days" tokens=${periodSummary.month.tokens} cost=${periodSummary.month.cost} />
|
|
353
|
+
</div>
|
|
354
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
355
|
+
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-3">
|
|
356
|
+
<h2 class="card-label text-xs">Daily ${metric === "tokens" ? "tokens" : "cost"} by model</h2>
|
|
357
|
+
<div class="flex items-center gap-2">
|
|
358
|
+
<${SegmentedControl}
|
|
359
|
+
options=${kRangeOptions.map((o) => ({ label: o.label, value: o.value }))}
|
|
360
|
+
value=${days}
|
|
361
|
+
onChange=${setDays}
|
|
362
|
+
/>
|
|
363
|
+
<${SegmentedControl}
|
|
364
|
+
options=${[
|
|
365
|
+
{ label: "tokens", value: "tokens" },
|
|
366
|
+
{ label: "cost", value: "cost" },
|
|
367
|
+
]}
|
|
368
|
+
value=${metric}
|
|
369
|
+
onChange=${setMetric}
|
|
370
|
+
/>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
<div style=${{ height: "280px" }}>
|
|
374
|
+
<canvas ref=${overviewCanvasRef}></canvas>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
`;
|
|
379
|
+
|
|
380
|
+
const renderSessionInlineDetail = (item) => {
|
|
381
|
+
const itemSessionId = String(item.sessionId || "");
|
|
382
|
+
const isExpanded = expandedSessionIds.includes(itemSessionId);
|
|
383
|
+
if (!isExpanded) return null;
|
|
384
|
+
const detail = sessionDetailById[itemSessionId];
|
|
385
|
+
const loadingDetail = !!loadingDetailById[itemSessionId];
|
|
386
|
+
if (loadingDetail) {
|
|
387
|
+
return html`
|
|
388
|
+
<div class="ac-history-body">
|
|
389
|
+
<p class="text-xs text-gray-500">Loading session detail...</p>
|
|
390
|
+
</div>
|
|
391
|
+
`;
|
|
392
|
+
}
|
|
393
|
+
if (!detail) {
|
|
394
|
+
return html`
|
|
395
|
+
<div class="ac-history-body">
|
|
396
|
+
<p class="text-xs text-gray-500">Session detail not available.</p>
|
|
397
|
+
</div>
|
|
398
|
+
`;
|
|
399
|
+
}
|
|
400
|
+
return html`
|
|
401
|
+
<div class="ac-history-body space-y-3 border-0 pt-0 mt-0">
|
|
402
|
+
<div class="mt-1.5">
|
|
403
|
+
<p class="text-[11px] text-gray-500 mb-1">Model breakdown</p>
|
|
404
|
+
${(detail.modelBreakdown || []).length === 0
|
|
405
|
+
? html`<p class="text-xs text-gray-500">No model usage recorded.</p>`
|
|
406
|
+
: html`
|
|
407
|
+
<div class="space-y-1.5">
|
|
408
|
+
${(detail.modelBreakdown || []).map(
|
|
409
|
+
(row) => html`
|
|
410
|
+
<div class="flex items-center justify-between gap-3 text-xs px-1 py-0.5 rounded hover:bg-white/5 transition-colors">
|
|
411
|
+
<span class="text-gray-300 truncate">${row.model || "unknown"}</span>
|
|
412
|
+
<span class="inline-flex items-center gap-3 text-gray-500 shrink-0">
|
|
413
|
+
<span>${formatTokens(row.totalTokens)} tok</span>
|
|
414
|
+
<span>${formatUsd(row.totalCost)}</span>
|
|
415
|
+
<span>${formatCountLabel(row.turnCount, "turn", "turns")}</span>
|
|
416
|
+
</span>
|
|
417
|
+
</div>
|
|
418
|
+
`,
|
|
419
|
+
)}
|
|
420
|
+
</div>
|
|
421
|
+
`}
|
|
422
|
+
</div>
|
|
423
|
+
<div>
|
|
424
|
+
<p class="text-[11px] text-gray-500 mb-1">Tool usage</p>
|
|
425
|
+
${(detail.toolUsage || []).length === 0
|
|
426
|
+
? html`<p class="text-xs text-gray-500">No tool calls recorded.</p>`
|
|
427
|
+
: html`
|
|
428
|
+
<div class="space-y-1.5">
|
|
429
|
+
${(detail.toolUsage || []).map(
|
|
430
|
+
(row) => html`
|
|
431
|
+
<div class="flex items-center justify-between gap-3 text-xs px-1 py-0.5 rounded hover:bg-white/5 transition-colors">
|
|
432
|
+
<span class="text-gray-300 truncate">${row.toolName}</span>
|
|
433
|
+
<span class="inline-flex items-center gap-3 text-gray-500 shrink-0">
|
|
434
|
+
<span>${formatCountLabel(row.callCount, "call", "calls")}</span>
|
|
435
|
+
<span>${(Number(row.errorRate || 0) * 100).toFixed(1)}% err</span>
|
|
436
|
+
<span>${formatDuration(row.avgDurationMs)}</span>
|
|
437
|
+
</span>
|
|
438
|
+
</div>
|
|
439
|
+
`,
|
|
440
|
+
)}
|
|
441
|
+
</div>
|
|
442
|
+
`}
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
`;
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const renderSessions = () => html`
|
|
449
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
450
|
+
<h2 class="card-label text-xs mb-3">Sessions</h2>
|
|
451
|
+
<div class="ac-history-list">
|
|
452
|
+
${sessions.length === 0
|
|
453
|
+
? html`<p class="text-xs text-gray-500">
|
|
454
|
+
${loadingSessions ? "Loading sessions..." : "No sessions recorded yet."}
|
|
455
|
+
</p>`
|
|
456
|
+
: sessions.map(
|
|
457
|
+
(item) => html`
|
|
458
|
+
<details
|
|
459
|
+
class="ac-history-item"
|
|
460
|
+
open=${expandedSessionIds.includes(String(item.sessionId || ""))}
|
|
461
|
+
ontoggle=${(e) => {
|
|
462
|
+
const itemSessionId = String(item.sessionId || "");
|
|
463
|
+
const isOpen = !!e.currentTarget?.open;
|
|
464
|
+
if (isOpen) {
|
|
465
|
+
setExpandedSessionIds((currentValue) =>
|
|
466
|
+
currentValue.includes(itemSessionId)
|
|
467
|
+
? currentValue
|
|
468
|
+
: [...currentValue, itemSessionId],
|
|
469
|
+
);
|
|
470
|
+
if (
|
|
471
|
+
!sessionDetailById[itemSessionId]
|
|
472
|
+
&& !loadingDetailById[itemSessionId]
|
|
473
|
+
) {
|
|
474
|
+
loadSessionDetail(itemSessionId);
|
|
475
|
+
}
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
setExpandedSessionIds((currentValue) =>
|
|
479
|
+
currentValue.filter((value) => value !== itemSessionId),
|
|
480
|
+
);
|
|
481
|
+
}}
|
|
482
|
+
>
|
|
483
|
+
<summary class="ac-history-summary hover:bg-white/5 transition-colors">
|
|
484
|
+
<div class="ac-history-summary-row">
|
|
485
|
+
<span class="inline-flex items-center gap-2 min-w-0">
|
|
486
|
+
<span class="ac-history-toggle shrink-0" aria-hidden="true">▸</span>
|
|
487
|
+
<${SessionBadges} session=${item} />
|
|
488
|
+
</span>
|
|
489
|
+
<span class="inline-flex items-center gap-3 shrink-0 text-xs text-gray-500">
|
|
490
|
+
<span>${formatTokens(item.totalTokens)} tok</span>
|
|
491
|
+
<span>${formatUsd(item.totalCost)}</span>
|
|
492
|
+
<span>${formatDateTime(item.lastActivityMs)}</span>
|
|
493
|
+
</span>
|
|
494
|
+
</div>
|
|
495
|
+
</summary>
|
|
496
|
+
${renderSessionInlineDetail(item)}
|
|
497
|
+
</details>
|
|
498
|
+
`,
|
|
499
|
+
)}
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
`;
|
|
503
|
+
|
|
504
|
+
return html`
|
|
505
|
+
<div class="space-y-4">
|
|
506
|
+
<${PageHeader}
|
|
507
|
+
title="Usage"
|
|
508
|
+
actions=${html`
|
|
509
|
+
<${ActionButton}
|
|
510
|
+
onClick=${loadSummary}
|
|
511
|
+
loading=${loadingSummary}
|
|
512
|
+
tone="secondary"
|
|
513
|
+
size="sm"
|
|
514
|
+
idleLabel="Refresh"
|
|
515
|
+
loadingMode="inline"
|
|
516
|
+
/>
|
|
517
|
+
`}
|
|
518
|
+
/>
|
|
519
|
+
${error
|
|
520
|
+
? html`<div class="text-xs text-red-300 bg-red-950/30 border border-red-900 rounded px-3 py-2">${error}</div>`
|
|
521
|
+
: null}
|
|
522
|
+
${loadingSummary && !summary
|
|
523
|
+
? html`<div class="text-sm text-[var(--text-muted)]">Loading usage summary...</div>`
|
|
524
|
+
: renderOverview()}
|
|
525
|
+
${renderSessions()}
|
|
526
|
+
</div>
|
|
527
|
+
`;
|
|
528
|
+
};
|
|
@@ -50,7 +50,7 @@ const ResourceBar = ({
|
|
|
50
50
|
>${label}</span
|
|
51
51
|
>
|
|
52
52
|
<div
|
|
53
|
-
class=${`h-
|
|
53
|
+
class=${`h-0.5 w-full bg-white/15 rounded-full overflow-hidden mt-1.5 flex ${onToggle ? "group-hover:bg-white/10 transition-colors" : ""}`}
|
|
54
54
|
>
|
|
55
55
|
${expanded && segments
|
|
56
56
|
? segments.map(
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const authFetch = async (url, opts = {}) => {
|
|
1
|
+
export const authFetch = async (url, opts = {}) => {
|
|
2
2
|
const res = await fetch(url, opts);
|
|
3
3
|
if (res.status === 401) {
|
|
4
4
|
try {
|
|
@@ -77,6 +77,30 @@ export async function fetchWatchdogStatus() {
|
|
|
77
77
|
return parseJsonOrThrow(res, 'Could not load watchdog status');
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
export async function fetchUsageSummary(days = 30) {
|
|
81
|
+
const params = new URLSearchParams({ days: String(days) });
|
|
82
|
+
const res = await authFetch(`/api/usage/summary?${params.toString()}`);
|
|
83
|
+
return parseJsonOrThrow(res, 'Could not load usage summary');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function fetchUsageSessions(limit = 50) {
|
|
87
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
88
|
+
const res = await authFetch(`/api/usage/sessions?${params.toString()}`);
|
|
89
|
+
return parseJsonOrThrow(res, 'Could not load usage sessions');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function fetchUsageSessionDetail(sessionId) {
|
|
93
|
+
const res = await authFetch(`/api/usage/sessions/${encodeURIComponent(String(sessionId || ''))}`);
|
|
94
|
+
return parseJsonOrThrow(res, 'Could not load usage session detail');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function fetchUsageSessionTimeSeries(sessionId, maxPoints = 100) {
|
|
98
|
+
const params = new URLSearchParams({ maxPoints: String(maxPoints) });
|
|
99
|
+
const safeSessionId = encodeURIComponent(String(sessionId || ''));
|
|
100
|
+
const res = await authFetch(`/api/usage/sessions/${safeSessionId}/timeseries?${params.toString()}`);
|
|
101
|
+
return parseJsonOrThrow(res, 'Could not load usage time series');
|
|
102
|
+
}
|
|
103
|
+
|
|
80
104
|
export async function fetchWatchdogEvents(limit = 20) {
|
|
81
105
|
const res = await authFetch(`/api/watchdog/events?limit=${encodeURIComponent(String(limit))}`);
|
|
82
106
|
return parseJsonOrThrow(res, 'Could not load watchdog events');
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { authFetch } from "./api.js";
|
|
2
|
+
|
|
3
|
+
export const verifyBot = async () => {
|
|
4
|
+
const res = await authFetch("/api/telegram/bot");
|
|
5
|
+
return res.json();
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const workspace = async () => {
|
|
9
|
+
const res = await authFetch("/api/telegram/workspace");
|
|
10
|
+
return res.json();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const resetWorkspace = async () => {
|
|
14
|
+
const res = await authFetch("/api/telegram/workspace/reset", {
|
|
15
|
+
method: "POST",
|
|
16
|
+
});
|
|
17
|
+
return res.json();
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const verifyGroup = async (groupId) => {
|
|
21
|
+
const res = await authFetch("/api/telegram/groups/verify", {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: { "Content-Type": "application/json" },
|
|
24
|
+
body: JSON.stringify({ groupId }),
|
|
25
|
+
});
|
|
26
|
+
return res.json();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const listTopics = async (groupId) => {
|
|
30
|
+
const res = await authFetch(
|
|
31
|
+
`/api/telegram/groups/${encodeURIComponent(groupId)}/topics`,
|
|
32
|
+
);
|
|
33
|
+
return res.json();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const createTopicsBulk = async (groupId, topics) => {
|
|
37
|
+
const res = await authFetch(
|
|
38
|
+
`/api/telegram/groups/${encodeURIComponent(groupId)}/topics/bulk`,
|
|
39
|
+
{
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "Content-Type": "application/json" },
|
|
42
|
+
body: JSON.stringify({ topics }),
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
return res.json();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const deleteTopic = async (groupId, topicId) => {
|
|
49
|
+
const res = await authFetch(
|
|
50
|
+
`/api/telegram/groups/${encodeURIComponent(groupId)}/topics/${topicId}`,
|
|
51
|
+
{ method: "DELETE" },
|
|
52
|
+
);
|
|
53
|
+
return res.json();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const updateTopic = async (groupId, topicId, payload) => {
|
|
57
|
+
const res = await authFetch(
|
|
58
|
+
`/api/telegram/groups/${encodeURIComponent(groupId)}/topics/${encodeURIComponent(topicId)}`,
|
|
59
|
+
{
|
|
60
|
+
method: "PUT",
|
|
61
|
+
headers: { "Content-Type": "application/json" },
|
|
62
|
+
body: JSON.stringify(payload),
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
return res.json();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const configureGroup = async (groupId, payload) => {
|
|
69
|
+
const res = await authFetch(
|
|
70
|
+
`/api/telegram/groups/${encodeURIComponent(groupId)}/configure`,
|
|
71
|
+
{
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "Content-Type": "application/json" },
|
|
74
|
+
body: JSON.stringify(payload),
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
return res.json();
|
|
78
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const kUiSettingsStorageKey = "alphaclaw.uiSettings";
|
|
2
|
+
|
|
3
|
+
const parseSettings = (rawValue) => {
|
|
4
|
+
if (!rawValue) return {};
|
|
5
|
+
try {
|
|
6
|
+
const parsed = JSON.parse(rawValue);
|
|
7
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
8
|
+
} catch {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const readUiSettings = () => {
|
|
14
|
+
try {
|
|
15
|
+
const rawValue = window.localStorage.getItem(kUiSettingsStorageKey);
|
|
16
|
+
return parseSettings(rawValue);
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const writeUiSettings = (nextSettings) => {
|
|
23
|
+
try {
|
|
24
|
+
window.localStorage.setItem(
|
|
25
|
+
kUiSettingsStorageKey,
|
|
26
|
+
JSON.stringify(
|
|
27
|
+
nextSettings && typeof nextSettings === "object" ? nextSettings : {},
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
} catch {}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const updateUiSettings = (updater) => {
|
|
34
|
+
const currentSettings = readUiSettings();
|
|
35
|
+
const nextSettings = updater(currentSettings);
|
|
36
|
+
writeUiSettings(nextSettings);
|
|
37
|
+
return nextSettings;
|
|
38
|
+
};
|