@chrysb/alphaclaw 0.4.6-beta.3 → 0.4.6-beta.5
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/lib/public/js/app.js +158 -1073
- package/lib/public/js/components/doctor/index.js +1 -2
- package/lib/public/js/components/general/index.js +155 -0
- package/lib/public/js/components/general/use-general-tab.js +233 -0
- package/lib/public/js/components/models-tab/index.js +286 -0
- package/lib/public/js/components/models-tab/provider-auth-card.js +369 -0
- package/lib/public/js/components/models-tab/use-models.js +262 -0
- package/lib/public/js/components/routes/browse-route.js +35 -0
- package/lib/public/js/components/routes/doctor-route.js +21 -0
- package/lib/public/js/components/routes/envars-route.js +11 -0
- package/lib/public/js/components/routes/general-route.js +45 -0
- package/lib/public/js/components/routes/index.js +11 -0
- package/lib/public/js/components/routes/models-route.js +11 -0
- package/lib/public/js/components/routes/providers-route.js +11 -0
- package/lib/public/js/components/routes/route-redirect.js +10 -0
- package/lib/public/js/components/routes/telegram-route.js +11 -0
- package/lib/public/js/components/routes/usage-route.js +15 -0
- package/lib/public/js/components/routes/watchdog-route.js +32 -0
- package/lib/public/js/components/routes/webhooks-route.js +43 -0
- package/lib/public/js/components/sidebar.js +2 -3
- package/lib/public/js/components/usage-tab/constants.js +1 -1
- package/lib/public/js/components/usage-tab/overview-section.js +124 -50
- package/lib/public/js/components/usage-tab/use-usage-tab.js +42 -11
- package/lib/public/js/hooks/use-app-shell-controller.js +230 -0
- package/lib/public/js/hooks/use-app-shell-ui.js +112 -0
- package/lib/public/js/hooks/use-browse-navigation.js +193 -0
- package/lib/public/js/hooks/use-hash-location.js +32 -0
- package/lib/public/js/lib/api.js +35 -0
- package/lib/public/js/lib/app-navigation.js +39 -0
- package/lib/public/js/lib/browse-restart-policy.js +28 -0
- package/lib/public/js/lib/browse-route.js +57 -0
- package/lib/public/js/lib/format.js +12 -0
- package/lib/server/auth-profiles.js +223 -52
- package/lib/server/doctor/prompt.js +4 -1
- package/lib/server/gateway.js +29 -9
- package/lib/server/routes/models.js +170 -2
- package/lib/server/watchdog.js +14 -1
- package/lib/server.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { GeneralTab } from "../general/index.js";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
export const GeneralRoute = ({
|
|
8
|
+
statusData = null,
|
|
9
|
+
watchdogData = null,
|
|
10
|
+
doctorStatusData = null,
|
|
11
|
+
doctorWarningDismissedUntilMs = 0,
|
|
12
|
+
onRefreshStatuses = () => {},
|
|
13
|
+
onSetLocation = () => {},
|
|
14
|
+
onNavigate = () => {},
|
|
15
|
+
restartingGateway = false,
|
|
16
|
+
onRestartGateway = () => {},
|
|
17
|
+
restartSignal = 0,
|
|
18
|
+
openclawUpdateInProgress = false,
|
|
19
|
+
onOpenclawVersionActionComplete = () => {},
|
|
20
|
+
onOpenclawUpdate = () => {},
|
|
21
|
+
onRestartRequired = () => {},
|
|
22
|
+
onDismissDoctorWarning = () => {},
|
|
23
|
+
}) => html`
|
|
24
|
+
<div class="pt-4">
|
|
25
|
+
<${GeneralTab}
|
|
26
|
+
statusData=${statusData}
|
|
27
|
+
watchdogData=${watchdogData}
|
|
28
|
+
doctorStatusData=${doctorStatusData}
|
|
29
|
+
doctorWarningDismissedUntilMs=${doctorWarningDismissedUntilMs}
|
|
30
|
+
onRefreshStatuses=${onRefreshStatuses}
|
|
31
|
+
onSwitchTab=${(nextTab) => onSetLocation(`/${nextTab}`)}
|
|
32
|
+
onNavigate=${onNavigate}
|
|
33
|
+
onOpenGmailWebhook=${() => onSetLocation("/webhooks/gmail")}
|
|
34
|
+
isActive=${true}
|
|
35
|
+
restartingGateway=${restartingGateway}
|
|
36
|
+
onRestartGateway=${onRestartGateway}
|
|
37
|
+
restartSignal=${restartSignal}
|
|
38
|
+
openclawUpdateInProgress=${openclawUpdateInProgress}
|
|
39
|
+
onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
|
|
40
|
+
onOpenclawUpdate=${onOpenclawUpdate}
|
|
41
|
+
onRestartRequired=${onRestartRequired}
|
|
42
|
+
onDismissDoctorWarning=${onDismissDoctorWarning}
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { BrowseRoute } from "./browse-route.js";
|
|
2
|
+
export { DoctorRoute } from "./doctor-route.js";
|
|
3
|
+
export { EnvarsRoute } from "./envars-route.js";
|
|
4
|
+
export { GeneralRoute } from "./general-route.js";
|
|
5
|
+
export { ModelsRoute } from "./models-route.js";
|
|
6
|
+
export { ProvidersRoute } from "./providers-route.js";
|
|
7
|
+
export { RouteRedirect } from "./route-redirect.js";
|
|
8
|
+
export { TelegramRoute } from "./telegram-route.js";
|
|
9
|
+
export { UsageRoute } from "./usage-route.js";
|
|
10
|
+
export { WatchdogRoute } from "./watchdog-route.js";
|
|
11
|
+
export { WebhooksRoute } from "./webhooks-route.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { Models } from "../models-tab/index.js";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
export const ModelsRoute = ({ onRestartRequired = () => {} }) => html`
|
|
8
|
+
<div class="pt-4">
|
|
9
|
+
<${Models} onRestartRequired=${onRestartRequired} />
|
|
10
|
+
</div>
|
|
11
|
+
`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { Providers } from "../providers.js";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
export const ProvidersRoute = ({ onRestartRequired = () => {} }) => html`
|
|
8
|
+
<div class="pt-4">
|
|
9
|
+
<${Providers} onRestartRequired=${onRestartRequired} />
|
|
10
|
+
</div>
|
|
11
|
+
`;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useEffect } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { useLocation } from "https://esm.sh/wouter-preact";
|
|
3
|
+
|
|
4
|
+
export const RouteRedirect = ({ to }) => {
|
|
5
|
+
const [, setLocation] = useLocation();
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
setLocation(to);
|
|
8
|
+
}, [to, setLocation]);
|
|
9
|
+
return null;
|
|
10
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { TelegramWorkspace } from "../telegram-workspace/index.js";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
export const TelegramRoute = ({ onBack = () => {} }) => html`
|
|
8
|
+
<div class="pt-4">
|
|
9
|
+
<${TelegramWorkspace} onBack=${onBack} />
|
|
10
|
+
</div>
|
|
11
|
+
`;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { UsageTab } from "../usage-tab/index.js";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
export const UsageRoute = ({ sessionId = "", onSetLocation = () => {} }) => html`
|
|
8
|
+
<div class="pt-4">
|
|
9
|
+
<${UsageTab}
|
|
10
|
+
sessionId=${sessionId}
|
|
11
|
+
onSelectSession=${(id) => onSetLocation(`/usage/${encodeURIComponent(String(id || ""))}`)}
|
|
12
|
+
onBackToSessions=${() => onSetLocation("/usage")}
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
`;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { WatchdogTab } from "../watchdog-tab.js";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
export const WatchdogRoute = ({
|
|
8
|
+
statusData = null,
|
|
9
|
+
watchdogStatus = null,
|
|
10
|
+
onRefreshStatuses = () => {},
|
|
11
|
+
restartingGateway = false,
|
|
12
|
+
onRestartGateway = () => {},
|
|
13
|
+
restartSignal = 0,
|
|
14
|
+
openclawUpdateInProgress = false,
|
|
15
|
+
onOpenclawVersionActionComplete = () => {},
|
|
16
|
+
onOpenclawUpdate = () => {},
|
|
17
|
+
}) => html`
|
|
18
|
+
<div class="pt-4">
|
|
19
|
+
<${WatchdogTab}
|
|
20
|
+
gatewayStatus=${statusData?.gateway || null}
|
|
21
|
+
openclawVersion=${statusData?.openclawVersion || null}
|
|
22
|
+
watchdogStatus=${watchdogStatus}
|
|
23
|
+
onRefreshStatuses=${onRefreshStatuses}
|
|
24
|
+
restartingGateway=${restartingGateway}
|
|
25
|
+
onRestartGateway=${onRestartGateway}
|
|
26
|
+
restartSignal=${restartSignal}
|
|
27
|
+
openclawUpdateInProgress=${openclawUpdateInProgress}
|
|
28
|
+
onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
|
|
29
|
+
onOpenclawUpdate=${onOpenclawUpdate}
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
`;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { Webhooks } from "../webhooks.js";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
export const WebhooksRoute = ({
|
|
8
|
+
hookName = "",
|
|
9
|
+
routeHistoryRef = null,
|
|
10
|
+
getCurrentPath = () => "",
|
|
11
|
+
onSetLocation = () => {},
|
|
12
|
+
onRestartRequired = () => {},
|
|
13
|
+
onNavigateToBrowseFile = () => {},
|
|
14
|
+
}) => {
|
|
15
|
+
const handleBackToList = () => {
|
|
16
|
+
const historyStack = routeHistoryRef?.current || [];
|
|
17
|
+
const hasPreviousRoute = historyStack.length > 1;
|
|
18
|
+
if (!hasPreviousRoute) {
|
|
19
|
+
onSetLocation("/webhooks");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const currentPath = getCurrentPath();
|
|
23
|
+
window.history.back();
|
|
24
|
+
window.setTimeout(() => {
|
|
25
|
+
if (getCurrentPath() === currentPath) {
|
|
26
|
+
onSetLocation("/webhooks");
|
|
27
|
+
}
|
|
28
|
+
}, 180);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return html`
|
|
32
|
+
<div class="pt-4">
|
|
33
|
+
<${Webhooks}
|
|
34
|
+
selectedHookName=${hookName}
|
|
35
|
+
onSelectHook=${(name) => onSetLocation(`/webhooks/${encodeURIComponent(name)}`)}
|
|
36
|
+
onBackToList=${handleBackToList}
|
|
37
|
+
onRestartRequired=${onRestartRequired}
|
|
38
|
+
onOpenFile=${(relativePath) =>
|
|
39
|
+
onNavigateToBrowseFile(String(relativePath || "").trim(), { view: "edit" })}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
`;
|
|
43
|
+
};
|
|
@@ -47,7 +47,6 @@ export const AppSidebar = ({
|
|
|
47
47
|
onPreviewBrowseFile = () => {},
|
|
48
48
|
acHasUpdate = false,
|
|
49
49
|
acLatest = "",
|
|
50
|
-
acDismissed = false,
|
|
51
50
|
acUpdating = false,
|
|
52
51
|
onAcUpdate = () => {},
|
|
53
52
|
}) => {
|
|
@@ -200,7 +199,7 @@ export const AppSidebar = ({
|
|
|
200
199
|
`,
|
|
201
200
|
)}
|
|
202
201
|
<div class="sidebar-footer">
|
|
203
|
-
${acHasUpdate && acLatest
|
|
202
|
+
${acHasUpdate && acLatest
|
|
204
203
|
? html`
|
|
205
204
|
<${UpdateActionButton}
|
|
206
205
|
onClick=${onAcUpdate}
|
|
@@ -251,7 +250,7 @@ export const AppSidebar = ({
|
|
|
251
250
|
onSelectFile=${onSelectBrowseFile}
|
|
252
251
|
isActive=${sidebarTab === "browse"}
|
|
253
252
|
/>
|
|
254
|
-
${acHasUpdate && acLatest
|
|
253
|
+
${acHasUpdate && acLatest
|
|
255
254
|
? html`
|
|
256
255
|
<${UpdateActionButton}
|
|
257
256
|
onClick=${onAcUpdate}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
2
|
import { useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
formatCompactNumber,
|
|
6
|
+
formatInteger,
|
|
7
|
+
formatUsd,
|
|
8
|
+
} from "../../lib/format.js";
|
|
5
9
|
import { SegmentedControl } from "../segmented-control.js";
|
|
6
10
|
import { kRangeOptions, kUsageSourceOrder } from "./constants.js";
|
|
7
11
|
import { renderSourceLabel } from "./formatters.js";
|
|
@@ -14,6 +18,34 @@ const formatCountLabel = (value, singular, plural) => {
|
|
|
14
18
|
return `${formatInteger(count)} ${label}`;
|
|
15
19
|
};
|
|
16
20
|
|
|
21
|
+
const formatPercent = (ratio) => `${(Number(ratio || 0) * 100).toFixed(1)}%`;
|
|
22
|
+
|
|
23
|
+
const getCacheHitRateValueClass = (ratio) => {
|
|
24
|
+
const percent = Number(ratio || 0) * 100;
|
|
25
|
+
if (percent <= 0) return "text-gray-300";
|
|
26
|
+
if (percent >= 70) return "text-green-400";
|
|
27
|
+
if (percent >= 40) return "text-amber-300";
|
|
28
|
+
return "text-red-400";
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getOverviewMetrics = (summary) => {
|
|
32
|
+
const totals = summary?.totals || {};
|
|
33
|
+
const cacheReadTokens = Number(totals.cacheReadTokens || 0);
|
|
34
|
+
const inputTokens = Number(totals.inputTokens || 0);
|
|
35
|
+
const promptTokens = inputTokens + cacheReadTokens;
|
|
36
|
+
const turnCount = Number(totals.turnCount || 0);
|
|
37
|
+
const totalTokens = Number(totals.totalTokens || 0);
|
|
38
|
+
const totalCost = Number(totals.totalCost || 0);
|
|
39
|
+
return {
|
|
40
|
+
cacheHitRate: promptTokens > 0 ? cacheReadTokens / promptTokens : 0,
|
|
41
|
+
cacheReadTokens,
|
|
42
|
+
promptTokens,
|
|
43
|
+
avgTokensPerTurn: turnCount > 0 ? totalTokens / turnCount : 0,
|
|
44
|
+
avgCostPerTurn: turnCount > 0 ? totalCost / turnCount : 0,
|
|
45
|
+
turnCount,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
17
49
|
const SummaryCard = ({ title, tokens, cost }) => html`
|
|
18
50
|
<div class="bg-surface border border-border rounded-xl p-4">
|
|
19
51
|
<h3 class="card-label text-xs">${title}</h3>
|
|
@@ -25,6 +57,25 @@ const SummaryCard = ({ title, tokens, cost }) => html`
|
|
|
25
57
|
</div>
|
|
26
58
|
`;
|
|
27
59
|
|
|
60
|
+
const MetricCard = ({
|
|
61
|
+
title,
|
|
62
|
+
value,
|
|
63
|
+
detail = "",
|
|
64
|
+
valueClass = "",
|
|
65
|
+
valueSuffix = "",
|
|
66
|
+
}) => html`
|
|
67
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
68
|
+
<h3 class="card-label text-xs">${title}</h3>
|
|
69
|
+
<div class=${`text-lg font-semibold mt-1 ${valueClass}`.trim()}>
|
|
70
|
+
${value}
|
|
71
|
+
${valueSuffix
|
|
72
|
+
? html`<span class="text-xs text-[var(--text-muted)] ml-1">${valueSuffix}</span>`
|
|
73
|
+
: null}
|
|
74
|
+
</div>
|
|
75
|
+
<div class="text-xs text-[var(--text-muted)] mt-1">${detail}</div>
|
|
76
|
+
</div>
|
|
77
|
+
`;
|
|
78
|
+
|
|
28
79
|
const AgentCostDistribution = ({ summary }) => {
|
|
29
80
|
const agents = Array.isArray(summary?.costByAgent?.agents)
|
|
30
81
|
? summary.costByAgent.agents
|
|
@@ -167,55 +218,78 @@ export const OverviewSection = ({
|
|
|
167
218
|
overviewCanvasRef,
|
|
168
219
|
onDaysChange = () => {},
|
|
169
220
|
onMetricChange = () => {},
|
|
170
|
-
}) =>
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
class="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-3"
|
|
192
|
-
>
|
|
193
|
-
<h2 class="card-label text-xs">
|
|
194
|
-
Daily ${metric === "tokens" ? "tokens" : "cost"} by model
|
|
195
|
-
</h2>
|
|
196
|
-
<div class="flex items-center gap-2">
|
|
197
|
-
<${SegmentedControl}
|
|
198
|
-
options=${kRangeOptions.map((option) => ({
|
|
199
|
-
label: option.label,
|
|
200
|
-
value: option.value,
|
|
201
|
-
}))}
|
|
202
|
-
value=${days}
|
|
203
|
-
onChange=${onDaysChange}
|
|
204
|
-
/>
|
|
205
|
-
<${SegmentedControl}
|
|
206
|
-
options=${[
|
|
207
|
-
{ label: "tokens", value: "tokens" },
|
|
208
|
-
{ label: "cost", value: "cost" },
|
|
209
|
-
]}
|
|
210
|
-
value=${metric}
|
|
211
|
-
onChange=${onMetricChange}
|
|
212
|
-
/>
|
|
213
|
-
</div>
|
|
221
|
+
}) => {
|
|
222
|
+
const overviewMetrics = getOverviewMetrics(summary);
|
|
223
|
+
|
|
224
|
+
return html`
|
|
225
|
+
<div class="space-y-4">
|
|
226
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
227
|
+
<${SummaryCard}
|
|
228
|
+
title="Today"
|
|
229
|
+
tokens=${periodSummary.today.tokens}
|
|
230
|
+
cost=${periodSummary.today.cost}
|
|
231
|
+
/>
|
|
232
|
+
<${SummaryCard}
|
|
233
|
+
title="Last 7 days"
|
|
234
|
+
tokens=${periodSummary.week.tokens}
|
|
235
|
+
cost=${periodSummary.week.cost}
|
|
236
|
+
/>
|
|
237
|
+
<${SummaryCard}
|
|
238
|
+
title="Last 30 days"
|
|
239
|
+
tokens=${periodSummary.month.tokens}
|
|
240
|
+
cost=${periodSummary.month.cost}
|
|
241
|
+
/>
|
|
214
242
|
</div>
|
|
215
|
-
<div
|
|
216
|
-
|
|
243
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
244
|
+
<${MetricCard}
|
|
245
|
+
title="Cache hit rate"
|
|
246
|
+
value=${formatPercent(overviewMetrics.cacheHitRate)}
|
|
247
|
+
valueClass=${getCacheHitRateValueClass(overviewMetrics.cacheHitRate)}
|
|
248
|
+
detail=${`${formatCompactNumber(overviewMetrics.cacheReadTokens)} cached · ${formatCompactNumber(overviewMetrics.promptTokens)} prompt`}
|
|
249
|
+
/>
|
|
250
|
+
<${MetricCard}
|
|
251
|
+
title="Avg tokens per turn"
|
|
252
|
+
value=${formatCompactNumber(overviewMetrics.avgTokensPerTurn)}
|
|
253
|
+
valueSuffix="tokens"
|
|
254
|
+
detail=${`${formatCountLabel(overviewMetrics.turnCount, "turn", "turns")} last ${days} days`}
|
|
255
|
+
/>
|
|
256
|
+
<${MetricCard}
|
|
257
|
+
title="Avg cost per turn"
|
|
258
|
+
value=${formatUsd(overviewMetrics.avgCostPerTurn)}
|
|
259
|
+
detail=${`${formatCountLabel(overviewMetrics.turnCount, "turn", "turns")} last ${days} days`}
|
|
260
|
+
/>
|
|
217
261
|
</div>
|
|
262
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
263
|
+
<div
|
|
264
|
+
class="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-3"
|
|
265
|
+
>
|
|
266
|
+
<h2 class="card-label text-xs">
|
|
267
|
+
Daily ${metric === "tokens" ? "tokens" : "cost"} by model
|
|
268
|
+
</h2>
|
|
269
|
+
<div class="flex items-center gap-2">
|
|
270
|
+
<${SegmentedControl}
|
|
271
|
+
options=${kRangeOptions.map((option) => ({
|
|
272
|
+
label: option.label,
|
|
273
|
+
value: option.value,
|
|
274
|
+
}))}
|
|
275
|
+
value=${days}
|
|
276
|
+
onChange=${onDaysChange}
|
|
277
|
+
/>
|
|
278
|
+
<${SegmentedControl}
|
|
279
|
+
options=${[
|
|
280
|
+
{ label: "tokens", value: "tokens" },
|
|
281
|
+
{ label: "cost", value: "cost" },
|
|
282
|
+
]}
|
|
283
|
+
value=${metric}
|
|
284
|
+
onChange=${onMetricChange}
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
<div style=${{ height: "280px" }}>
|
|
289
|
+
<canvas ref=${overviewCanvasRef}></canvas>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
<${AgentCostDistribution} summary=${summary} />
|
|
218
293
|
</div>
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
`;
|
|
294
|
+
`;
|
|
295
|
+
};
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from "https://esm.sh/preact/hooks";
|
|
2
8
|
import {
|
|
3
9
|
fetchUsageSessionDetail,
|
|
4
10
|
fetchUsageSessions,
|
|
@@ -17,12 +23,17 @@ import { toChartColor, toLocalDayKey } from "./formatters.js";
|
|
|
17
23
|
export const useUsageTab = ({ sessionId = "" }) => {
|
|
18
24
|
const [days, setDays] = useState(() => {
|
|
19
25
|
const settings = readUiSettings();
|
|
20
|
-
const parsedDays = Number.parseInt(
|
|
26
|
+
const parsedDays = Number.parseInt(
|
|
27
|
+
String(settings[kUsageDaysUiSettingKey] ?? ""),
|
|
28
|
+
10,
|
|
29
|
+
);
|
|
21
30
|
return [7, 30, 90].includes(parsedDays) ? parsedDays : kDefaultUsageDays;
|
|
22
31
|
});
|
|
23
32
|
const [metric, setMetric] = useState(() => {
|
|
24
33
|
const settings = readUiSettings();
|
|
25
|
-
return settings[kUsageMetricUiSettingKey] === "cost"
|
|
34
|
+
return settings[kUsageMetricUiSettingKey] === "cost"
|
|
35
|
+
? "cost"
|
|
36
|
+
: kDefaultUsageMetric;
|
|
26
37
|
});
|
|
27
38
|
const [summary, setSummary] = useState(null);
|
|
28
39
|
const [sessions, setSessions] = useState([]);
|
|
@@ -104,9 +115,14 @@ export const useUsageTab = ({ sessionId = "" }) => {
|
|
|
104
115
|
const safeSessionId = String(sessionId || "").trim();
|
|
105
116
|
if (!safeSessionId) return;
|
|
106
117
|
setExpandedSessionIds((currentValue) =>
|
|
107
|
-
currentValue.includes(safeSessionId)
|
|
118
|
+
currentValue.includes(safeSessionId)
|
|
119
|
+
? currentValue
|
|
120
|
+
: [...currentValue, safeSessionId],
|
|
108
121
|
);
|
|
109
|
-
if (
|
|
122
|
+
if (
|
|
123
|
+
!sessionDetailById[safeSessionId] &&
|
|
124
|
+
!loadingDetailById[safeSessionId]
|
|
125
|
+
) {
|
|
110
126
|
loadSessionDetail(safeSessionId);
|
|
111
127
|
}
|
|
112
128
|
}, [sessionId, sessionDetailById, loadingDetailById, loadSessionDetail]);
|
|
@@ -115,8 +131,12 @@ export const useUsageTab = ({ sessionId = "" }) => {
|
|
|
115
131
|
const rows = Array.isArray(summary?.daily) ? summary.daily : [];
|
|
116
132
|
const now = new Date();
|
|
117
133
|
const dayKey = toLocalDayKey(now);
|
|
118
|
-
const weekStart = toLocalDayKey(
|
|
119
|
-
|
|
134
|
+
const weekStart = toLocalDayKey(
|
|
135
|
+
new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000),
|
|
136
|
+
);
|
|
137
|
+
const monthStart = toLocalDayKey(
|
|
138
|
+
new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000),
|
|
139
|
+
);
|
|
120
140
|
const zero = { tokens: 0, cost: 0 };
|
|
121
141
|
return rows.reduce(
|
|
122
142
|
(acc, row) => {
|
|
@@ -156,9 +176,13 @@ export const useUsageTab = ({ sessionId = "" }) => {
|
|
|
156
176
|
const datasets = Array.from(allModels).map((model) => ({
|
|
157
177
|
label: model,
|
|
158
178
|
data: rows.map((row) => {
|
|
159
|
-
const found = (row.models || []).find(
|
|
179
|
+
const found = (row.models || []).find(
|
|
180
|
+
(m) => String(m.model || "") === model,
|
|
181
|
+
);
|
|
160
182
|
if (!found) return 0;
|
|
161
|
-
return metric === "cost"
|
|
183
|
+
return metric === "cost"
|
|
184
|
+
? Number(found.totalCost || 0)
|
|
185
|
+
: Number(found.totalTokens || 0);
|
|
162
186
|
}),
|
|
163
187
|
backgroundColor: toChartColor(model),
|
|
164
188
|
}));
|
|
@@ -186,13 +210,20 @@ export const useUsageTab = ({ sessionId = "" }) => {
|
|
|
186
210
|
stacked: true,
|
|
187
211
|
ticks: {
|
|
188
212
|
color: "rgba(156,163,175,1)",
|
|
189
|
-
callback: (v) =>
|
|
213
|
+
callback: (v) =>
|
|
214
|
+
metric === "cost"
|
|
215
|
+
? `$${Number(v).toFixed(2)}`
|
|
216
|
+
: formatInteger(v),
|
|
190
217
|
},
|
|
191
218
|
},
|
|
192
219
|
},
|
|
193
220
|
plugins: {
|
|
194
221
|
legend: {
|
|
195
|
-
labels: {
|
|
222
|
+
labels: {
|
|
223
|
+
color: "rgba(209,213,219,1)",
|
|
224
|
+
boxWidth: 10,
|
|
225
|
+
boxHeight: 10,
|
|
226
|
+
},
|
|
196
227
|
},
|
|
197
228
|
tooltip: {
|
|
198
229
|
callbacks: {
|