@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.
Files changed (39) hide show
  1. package/lib/public/js/app.js +158 -1073
  2. package/lib/public/js/components/doctor/index.js +1 -2
  3. package/lib/public/js/components/general/index.js +155 -0
  4. package/lib/public/js/components/general/use-general-tab.js +233 -0
  5. package/lib/public/js/components/models-tab/index.js +286 -0
  6. package/lib/public/js/components/models-tab/provider-auth-card.js +369 -0
  7. package/lib/public/js/components/models-tab/use-models.js +262 -0
  8. package/lib/public/js/components/routes/browse-route.js +35 -0
  9. package/lib/public/js/components/routes/doctor-route.js +21 -0
  10. package/lib/public/js/components/routes/envars-route.js +11 -0
  11. package/lib/public/js/components/routes/general-route.js +45 -0
  12. package/lib/public/js/components/routes/index.js +11 -0
  13. package/lib/public/js/components/routes/models-route.js +11 -0
  14. package/lib/public/js/components/routes/providers-route.js +11 -0
  15. package/lib/public/js/components/routes/route-redirect.js +10 -0
  16. package/lib/public/js/components/routes/telegram-route.js +11 -0
  17. package/lib/public/js/components/routes/usage-route.js +15 -0
  18. package/lib/public/js/components/routes/watchdog-route.js +32 -0
  19. package/lib/public/js/components/routes/webhooks-route.js +43 -0
  20. package/lib/public/js/components/sidebar.js +2 -3
  21. package/lib/public/js/components/usage-tab/constants.js +1 -1
  22. package/lib/public/js/components/usage-tab/overview-section.js +124 -50
  23. package/lib/public/js/components/usage-tab/use-usage-tab.js +42 -11
  24. package/lib/public/js/hooks/use-app-shell-controller.js +230 -0
  25. package/lib/public/js/hooks/use-app-shell-ui.js +112 -0
  26. package/lib/public/js/hooks/use-browse-navigation.js +193 -0
  27. package/lib/public/js/hooks/use-hash-location.js +32 -0
  28. package/lib/public/js/lib/api.js +35 -0
  29. package/lib/public/js/lib/app-navigation.js +39 -0
  30. package/lib/public/js/lib/browse-restart-policy.js +28 -0
  31. package/lib/public/js/lib/browse-route.js +57 -0
  32. package/lib/public/js/lib/format.js +12 -0
  33. package/lib/server/auth-profiles.js +223 -52
  34. package/lib/server/doctor/prompt.js +4 -1
  35. package/lib/server/gateway.js +29 -9
  36. package/lib/server/routes/models.js +170 -2
  37. package/lib/server/watchdog.js +14 -1
  38. package/lib/server.js +1 -0
  39. 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 && !acDismissed
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 && !acDismissed
253
+ ${acHasUpdate && acLatest
255
254
  ? html`
256
255
  <${UpdateActionButton}
257
256
  onClick=${onAcUpdate}
@@ -1,8 +1,8 @@
1
1
  export const kColorPalette = [
2
2
  "#7dd3fc",
3
3
  "#22d3ee",
4
- "#34d399",
5
4
  "#fbbf24",
5
+ "#34d399",
6
6
  "#fb7185",
7
7
  "#a78bfa",
8
8
  "#f472b6",
@@ -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 { formatInteger, formatUsd } from "../../lib/format.js";
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
- }) => html`
171
- <div class="space-y-4">
172
- <div class="grid grid-cols-1 md:grid-cols-3 gap-3">
173
- <${SummaryCard}
174
- title="Today"
175
- tokens=${periodSummary.today.tokens}
176
- cost=${periodSummary.today.cost}
177
- />
178
- <${SummaryCard}
179
- title="Last 7 days"
180
- tokens=${periodSummary.week.tokens}
181
- cost=${periodSummary.week.cost}
182
- />
183
- <${SummaryCard}
184
- title="Last 30 days"
185
- tokens=${periodSummary.month.tokens}
186
- cost=${periodSummary.month.cost}
187
- />
188
- </div>
189
- <div class="bg-surface border border-border rounded-xl p-4">
190
- <div
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 style=${{ height: "280px" }}>
216
- <canvas ref=${overviewCanvasRef}></canvas>
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
- <${AgentCostDistribution} summary=${summary} />
220
- </div>
221
- `;
294
+ `;
295
+ };
@@ -1,4 +1,10 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
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(String(settings[kUsageDaysUiSettingKey] ?? ""), 10);
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" ? "cost" : kDefaultUsageMetric;
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) ? currentValue : [...currentValue, safeSessionId],
118
+ currentValue.includes(safeSessionId)
119
+ ? currentValue
120
+ : [...currentValue, safeSessionId],
108
121
  );
109
- if (!sessionDetailById[safeSessionId] && !loadingDetailById[safeSessionId]) {
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(new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000));
119
- const monthStart = toLocalDayKey(new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000));
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((m) => String(m.model || "") === model);
179
+ const found = (row.models || []).find(
180
+ (m) => String(m.model || "") === model,
181
+ );
160
182
  if (!found) return 0;
161
- return metric === "cost" ? Number(found.totalCost || 0) : Number(found.totalTokens || 0);
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) => (metric === "cost" ? `$${Number(v).toFixed(2)}` : formatInteger(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: { color: "rgba(209,213,219,1)", boxWidth: 10, boxHeight: 10 },
222
+ labels: {
223
+ color: "rgba(209,213,219,1)",
224
+ boxWidth: 10,
225
+ boxHeight: 10,
226
+ },
196
227
  },
197
228
  tooltip: {
198
229
  callbacks: {