@agent-native/dispatch 0.23.5 → 0.24.2
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/dist/actions/list-connected-agents.d.ts +1 -0
- package/dist/actions/list-connected-agents.js +5 -1
- package/dist/actions/list-connected-agents.js.map +1 -1
- package/dist/actions/list-dispatch-usage-metrics.d.ts +2 -0
- package/dist/actions/list-dispatch-usage-metrics.js +11 -1
- package/dist/actions/list-dispatch-usage-metrics.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +12 -12
- package/dist/actions/remix-workspace-template.js +4 -3
- package/dist/actions/remix-workspace-template.js.map +1 -1
- package/dist/actions/scaffold-workspace-app.js +3 -2
- package/dist/actions/scaffold-workspace-app.js.map +1 -1
- package/dist/actions/start-workspace-app-creation.js +3 -2
- package/dist/actions/start-workspace-app-creation.js.map +1 -1
- package/dist/actions/view-screen.js +11 -1
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/admin-navigation.d.ts.map +1 -1
- package/dist/components/admin-navigation.js +21 -5
- package/dist/components/admin-navigation.js.map +1 -1
- package/dist/components/app-icon.js +1 -1
- package/dist/components/app-icon.js.map +1 -1
- package/dist/components/app-list-row.d.ts +1 -1
- package/dist/components/app-list-row.d.ts.map +1 -1
- package/dist/components/app-list-row.js +1 -1
- package/dist/components/app-list-row.js.map +1 -1
- package/dist/components/app-open-actions.d.ts +1 -17
- package/dist/components/app-open-actions.d.ts.map +1 -1
- package/dist/components/app-open-actions.js +1 -20
- package/dist/components/app-open-actions.js.map +1 -1
- package/dist/components/app-row-settings.d.ts +6 -0
- package/dist/components/app-row-settings.d.ts.map +1 -0
- package/dist/components/app-row-settings.js +11 -0
- package/dist/components/app-row-settings.js.map +1 -0
- package/dist/components/automation-run-history-dialog.d.ts.map +1 -1
- package/dist/components/automation-run-history-dialog.js +3 -2
- package/dist/components/automation-run-history-dialog.js.map +1 -1
- package/dist/components/connected-app-card.d.ts.map +1 -1
- package/dist/components/connected-app-card.js +3 -2
- package/dist/components/connected-app-card.js.map +1 -1
- package/dist/components/create-app-popover.js +2 -2
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +23 -7
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/dispatch-shell.d.ts.map +1 -1
- package/dist/components/dispatch-shell.js +5 -1
- package/dist/components/dispatch-shell.js.map +1 -1
- package/dist/components/docs-link.d.ts +16 -0
- package/dist/components/docs-link.d.ts.map +1 -0
- package/dist/components/docs-link.js +34 -0
- package/dist/components/docs-link.js.map +1 -0
- package/dist/components/layout/Layout.d.ts +1 -0
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +43 -35
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/messaging-setup-panel.d.ts.map +1 -1
- package/dist/components/messaging-setup-panel.js +3 -2
- package/dist/components/messaging-setup-panel.js.map +1 -1
- package/dist/components/usage-alerts-panel.d.ts +11 -0
- package/dist/components/usage-alerts-panel.d.ts.map +1 -0
- package/dist/components/usage-alerts-panel.js +125 -0
- package/dist/components/usage-alerts-panel.js.map +1 -0
- package/dist/components/workspace-app-card.d.ts +3 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +70 -29
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-app-search.d.ts +10 -0
- package/dist/components/workspace-app-search.d.ts.map +1 -0
- package/dist/components/workspace-app-search.js +27 -0
- package/dist/components/workspace-app-search.js.map +1 -0
- package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -1
- package/dist/components/workspace-resource-effective-stack.js +3 -2
- package/dist/components/workspace-resource-effective-stack.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +10 -15
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/hooks/use-navigation-state.d.ts +2 -0
- package/dist/hooks/use-navigation-state.d.ts.map +1 -1
- package/dist/hooks/use-navigation-state.js +10 -0
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/lib/embed-session-recovery.d.ts +3 -0
- package/dist/lib/embed-session-recovery.d.ts.map +1 -0
- package/dist/lib/embed-session-recovery.js +18 -0
- package/dist/lib/embed-session-recovery.js.map +1 -0
- package/dist/lib/other-apps.d.ts +2 -0
- package/dist/lib/other-apps.d.ts.map +1 -1
- package/dist/lib/other-apps.js.map +1 -1
- package/dist/lib/workspace-app-layout.d.ts +19 -0
- package/dist/lib/workspace-app-layout.d.ts.map +1 -0
- package/dist/lib/workspace-app-layout.js +75 -0
- package/dist/lib/workspace-app-layout.js.map +1 -0
- package/dist/lib/workspace-apps.d.ts +14 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +66 -0
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/lib/workspace-apps.test.d.ts +2 -0
- package/dist/lib/workspace-apps.test.d.ts.map +1 -0
- package/dist/lib/workspace-apps.test.js +33 -0
- package/dist/lib/workspace-apps.test.js.map +1 -0
- package/dist/routes/pages/$appId.d.ts.map +1 -1
- package/dist/routes/pages/$appId.js +4 -2
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/routes/pages/agents.d.ts.map +1 -1
- package/dist/routes/pages/agents.js +2 -3
- package/dist/routes/pages/agents.js.map +1 -1
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +17 -13
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +31 -16
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/audit.d.ts.map +1 -1
- package/dist/routes/pages/audit.js +2 -1
- package/dist/routes/pages/audit.js.map +1 -1
- package/dist/routes/pages/chat.d.ts.map +1 -1
- package/dist/routes/pages/chat.js +7 -2
- package/dist/routes/pages/chat.js.map +1 -1
- package/dist/routes/pages/dreams.d.ts.map +1 -1
- package/dist/routes/pages/dreams.js +14 -18
- package/dist/routes/pages/dreams.js.map +1 -1
- package/dist/routes/pages/integrations.d.ts.map +1 -1
- package/dist/routes/pages/integrations.js +51 -14
- package/dist/routes/pages/integrations.js.map +1 -1
- package/dist/routes/pages/metrics.d.ts.map +1 -1
- package/dist/routes/pages/metrics.js +146 -17
- package/dist/routes/pages/metrics.js.map +1 -1
- package/dist/routes/pages/operations.js +1 -1
- package/dist/routes/pages/operations.js.map +1 -1
- package/dist/routes/pages/settings.d.ts.map +1 -1
- package/dist/routes/pages/settings.js +19 -2
- package/dist/routes/pages/settings.js.map +1 -1
- package/dist/routes/pages/thread-debug.d.ts.map +1 -1
- package/dist/routes/pages/thread-debug.js +5 -8
- package/dist/routes/pages/thread-debug.js.map +1 -1
- package/dist/routes/pages/vault.d.ts.map +1 -1
- package/dist/routes/pages/vault.js +1 -1
- package/dist/routes/pages/vault.js.map +1 -1
- package/dist/routes/pages/workspace.d.ts.map +1 -1
- package/dist/routes/pages/workspace.js +13 -10
- package/dist/routes/pages/workspace.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +12 -52
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/usage-metrics-store.d.ts +17 -0
- package/dist/server/lib/usage-metrics-store.d.ts.map +1 -1
- package/dist/server/lib/usage-metrics-store.js +187 -30
- package/dist/server/lib/usage-metrics-store.js.map +1 -1
- package/dist/server/plugins/agent-chat.js +2 -0
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/server/plugins/integrations.d.ts.map +1 -1
- package/dist/server/plugins/integrations.js +1 -0
- package/dist/server/plugins/integrations.js.map +1 -1
- package/dist/shared/app-roles.d.ts +9 -0
- package/dist/shared/app-roles.d.ts.map +1 -0
- package/dist/shared/app-roles.js +7 -0
- package/dist/shared/app-roles.js.map +1 -0
- package/package.json +4 -3
- package/src/actions/list-connected-agents.spec.ts +73 -0
- package/src/actions/list-connected-agents.ts +6 -2
- package/src/actions/list-dispatch-usage-metrics.ts +11 -1
- package/src/actions/remix-workspace-template.spec.ts +29 -0
- package/src/actions/remix-workspace-template.ts +13 -6
- package/src/actions/scaffold-workspace-app.ts +12 -5
- package/src/actions/start-workspace-app-creation.ts +8 -2
- package/src/actions/view-screen.ts +13 -1
- package/src/components/admin-navigation.tsx +57 -11
- package/src/components/app-icon.spec.tsx +34 -0
- package/src/components/app-icon.tsx +1 -1
- package/src/components/app-list-row.tsx +1 -2
- package/src/components/app-open-actions.tsx +5 -114
- package/src/components/app-row-settings.tsx +54 -0
- package/src/components/automation-run-history-dialog.tsx +8 -4
- package/src/components/connected-app-card.tsx +3 -2
- package/src/components/create-app-popover.spec.tsx +1 -1
- package/src/components/create-app-popover.tsx +2 -2
- package/src/components/dispatch-control-plane.spec.tsx +44 -3
- package/src/components/dispatch-control-plane.tsx +83 -27
- package/src/components/dispatch-shell.tsx +7 -1
- package/src/components/docs-link.tsx +52 -0
- package/src/components/layout/Layout.spec.tsx +13 -0
- package/src/components/layout/Layout.tsx +63 -32
- package/src/components/messaging-setup-panel.tsx +10 -3
- package/src/components/usage-alerts-panel.tsx +445 -0
- package/src/components/workspace-app-card.spec.tsx +130 -9
- package/src/components/workspace-app-card.tsx +128 -71
- package/src/components/workspace-app-search.tsx +96 -0
- package/src/components/workspace-resource-effective-stack.tsx +10 -5
- package/src/components/workspace-template-card.spec.tsx +53 -24
- package/src/components/workspace-template-card.tsx +16 -26
- package/src/hooks/use-navigation-state.ts +12 -0
- package/src/lib/embed-session-recovery.spec.ts +63 -0
- package/src/lib/embed-session-recovery.ts +26 -0
- package/src/lib/other-apps.ts +2 -0
- package/src/lib/workspace-app-layout.spec.ts +47 -0
- package/src/lib/workspace-app-layout.ts +121 -0
- package/src/lib/workspace-apps.test.ts +64 -0
- package/src/lib/workspace-apps.ts +82 -0
- package/src/routes/pages/$appId.tsx +3 -6
- package/src/routes/pages/agents.tsx +6 -3
- package/src/routes/pages/apps.$appId.tsx +16 -17
- package/src/routes/pages/apps.tsx +116 -62
- package/src/routes/pages/audit.tsx +28 -8
- package/src/routes/pages/chat.spec.tsx +10 -0
- package/src/routes/pages/chat.tsx +8 -0
- package/src/routes/pages/dreams.tsx +92 -142
- package/src/routes/pages/integrations.tsx +147 -68
- package/src/routes/pages/metrics.tsx +460 -102
- package/src/routes/pages/operations.tsx +2 -2
- package/src/routes/pages/settings.tsx +27 -10
- package/src/routes/pages/thread-debug.tsx +18 -36
- package/src/routes/pages/vault.tsx +7 -2
- package/src/routes/pages/workspace.tsx +92 -56
- package/src/server/index.ts +4 -0
- package/src/server/lib/app-creation-store.spec.ts +90 -43
- package/src/server/lib/app-creation-store.ts +15 -61
- package/src/server/lib/usage-metrics-store.spec.ts +117 -0
- package/src/server/lib/usage-metrics-store.ts +249 -30
- package/src/server/plugins/agent-chat.spec.ts +4 -0
- package/src/server/plugins/agent-chat.ts +2 -0
- package/src/server/plugins/integrations.ts +1 -0
- package/src/shared/app-roles.ts +8 -0
- package/src/styles/dispatch.css +9 -0
|
@@ -1,22 +1,39 @@
|
|
|
1
|
+
import { sendToAgentChat } from "@agent-native/core/client/agent-chat";
|
|
1
2
|
import { useActionQuery } from "@agent-native/core/client/hooks";
|
|
2
3
|
import { useT } from "@agent-native/core/client/i18n";
|
|
3
4
|
import {
|
|
4
5
|
IconActivity,
|
|
5
6
|
IconAlertTriangle,
|
|
7
|
+
IconArrowUpRight,
|
|
6
8
|
IconApps,
|
|
7
9
|
IconChartBar,
|
|
8
|
-
IconClockHour4,
|
|
9
10
|
IconCoin,
|
|
10
11
|
IconMessages,
|
|
11
12
|
IconUsersGroup,
|
|
12
13
|
} from "@tabler/icons-react";
|
|
13
14
|
import { useMemo, useState, type ReactNode } from "react";
|
|
15
|
+
import { Link, useSearchParams } from "react-router";
|
|
16
|
+
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
|
14
17
|
|
|
15
18
|
import { DispatchShell } from "../../components/dispatch-shell";
|
|
16
19
|
import { Alert, AlertDescription, AlertTitle } from "../../components/ui/alert";
|
|
17
20
|
import { Badge } from "../../components/ui/badge";
|
|
18
21
|
import { Button } from "../../components/ui/button";
|
|
22
|
+
import {
|
|
23
|
+
ChartContainer,
|
|
24
|
+
ChartTooltip,
|
|
25
|
+
ChartTooltipContent,
|
|
26
|
+
} from "../../components/ui/chart";
|
|
27
|
+
import {
|
|
28
|
+
Select,
|
|
29
|
+
SelectContent,
|
|
30
|
+
SelectGroup,
|
|
31
|
+
SelectItem,
|
|
32
|
+
SelectTrigger,
|
|
33
|
+
SelectValue,
|
|
34
|
+
} from "../../components/ui/select";
|
|
19
35
|
import { Skeleton } from "../../components/ui/skeleton";
|
|
36
|
+
import { UsageAlertsPanel } from "../../components/usage-alerts-panel";
|
|
20
37
|
import { cn } from "../../lib/utils";
|
|
21
38
|
|
|
22
39
|
export function meta() {
|
|
@@ -44,6 +61,11 @@ interface UserUsageMetric extends UsageMetricBucket {
|
|
|
44
61
|
role: string | null;
|
|
45
62
|
}
|
|
46
63
|
|
|
64
|
+
interface UsageUserOption {
|
|
65
|
+
email: string;
|
|
66
|
+
role: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
47
69
|
interface AppAccessMetric {
|
|
48
70
|
id: string;
|
|
49
71
|
name: string;
|
|
@@ -78,6 +100,13 @@ interface RecentUsageMetric {
|
|
|
78
100
|
inputTokens: number;
|
|
79
101
|
outputTokens: number;
|
|
80
102
|
costCents: number;
|
|
103
|
+
prompt: string | null;
|
|
104
|
+
promptSource: "thread" | "thread-preview" | "not-captured" | "unavailable";
|
|
105
|
+
threadId: string | null;
|
|
106
|
+
runId: string | null;
|
|
107
|
+
taskId: string | null;
|
|
108
|
+
sourcePlatform: string | null;
|
|
109
|
+
sourceId: string | null;
|
|
81
110
|
}
|
|
82
111
|
|
|
83
112
|
interface UsageBillingMode {
|
|
@@ -91,6 +120,9 @@ interface UsageBillingMode {
|
|
|
91
120
|
|
|
92
121
|
interface DispatchUsageMetrics {
|
|
93
122
|
billing?: UsageBillingMode;
|
|
123
|
+
viewScope?: "me" | "workspace";
|
|
124
|
+
selectedUserEmail?: string | null;
|
|
125
|
+
availableUsers?: UsageUserOption[];
|
|
94
126
|
sinceDays: number;
|
|
95
127
|
access: {
|
|
96
128
|
viewerEmail: string;
|
|
@@ -187,6 +219,39 @@ function timeAgo(timestamp: number | null): string {
|
|
|
187
219
|
return `${Math.floor(diff / 86_400_000)}d ago`;
|
|
188
220
|
}
|
|
189
221
|
|
|
222
|
+
function formatTrendDate(value: string): string {
|
|
223
|
+
const date = new Date(`${value}T12:00:00`);
|
|
224
|
+
return date.toLocaleDateString(undefined, {
|
|
225
|
+
month: "short",
|
|
226
|
+
day: "numeric",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function completeTrendRows(rows: DailyUsageMetric[]): DailyUsageMetric[] {
|
|
231
|
+
if (rows.length < 2) return rows;
|
|
232
|
+
const byDate = new Map(rows.map((row) => [row.date, row]));
|
|
233
|
+
const start = new Date(`${rows[0].date}T12:00:00`);
|
|
234
|
+
const end = new Date(`${rows[rows.length - 1].date}T12:00:00`);
|
|
235
|
+
const completed: DailyUsageMetric[] = [];
|
|
236
|
+
for (
|
|
237
|
+
const cursor = start;
|
|
238
|
+
cursor <= end;
|
|
239
|
+
cursor.setDate(cursor.getDate() + 1)
|
|
240
|
+
) {
|
|
241
|
+
const date = cursor.toISOString().slice(0, 10);
|
|
242
|
+
completed.push(
|
|
243
|
+
byDate.get(date) ?? {
|
|
244
|
+
date,
|
|
245
|
+
costCents: 0,
|
|
246
|
+
calls: 0,
|
|
247
|
+
chatCalls: 0,
|
|
248
|
+
activeUsers: 0,
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return completed;
|
|
253
|
+
}
|
|
254
|
+
|
|
190
255
|
function displayApp(value: string | null | undefined): string {
|
|
191
256
|
const trimmed = value?.trim();
|
|
192
257
|
if (!trimmed || trimmed === "unattributed") return "Unattributed";
|
|
@@ -307,6 +372,274 @@ function LoadingMetrics() {
|
|
|
307
372
|
);
|
|
308
373
|
}
|
|
309
374
|
|
|
375
|
+
function ScopeSelector({
|
|
376
|
+
value,
|
|
377
|
+
onChange,
|
|
378
|
+
}: {
|
|
379
|
+
value: "me" | "workspace";
|
|
380
|
+
onChange: (value: "me" | "workspace") => void;
|
|
381
|
+
}) {
|
|
382
|
+
return (
|
|
383
|
+
<div className="flex rounded-md bg-card p-0.5" aria-label="Usage scope">
|
|
384
|
+
{(
|
|
385
|
+
[
|
|
386
|
+
["me", "My usage"],
|
|
387
|
+
["workspace", "Workspace"],
|
|
388
|
+
] as const
|
|
389
|
+
).map(([scope, label]) => (
|
|
390
|
+
<Button
|
|
391
|
+
key={scope}
|
|
392
|
+
type="button"
|
|
393
|
+
variant={value === scope ? "secondary" : "ghost"}
|
|
394
|
+
size="sm"
|
|
395
|
+
className="h-7 px-3 text-xs"
|
|
396
|
+
aria-pressed={value === scope}
|
|
397
|
+
onClick={() => onChange(scope)}
|
|
398
|
+
>
|
|
399
|
+
{label}
|
|
400
|
+
</Button>
|
|
401
|
+
))}
|
|
402
|
+
</div>
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function UserSelector({
|
|
407
|
+
value,
|
|
408
|
+
users,
|
|
409
|
+
onChange,
|
|
410
|
+
}: {
|
|
411
|
+
value: string | null;
|
|
412
|
+
users: UsageUserOption[];
|
|
413
|
+
onChange: (value: string | null) => void;
|
|
414
|
+
}) {
|
|
415
|
+
return (
|
|
416
|
+
<Select
|
|
417
|
+
value={value ?? "all"}
|
|
418
|
+
onValueChange={(nextValue) =>
|
|
419
|
+
onChange(nextValue === "all" ? null : nextValue)
|
|
420
|
+
}
|
|
421
|
+
>
|
|
422
|
+
<SelectTrigger
|
|
423
|
+
className="h-8 w-[210px] text-xs"
|
|
424
|
+
aria-label="Filter usage by user"
|
|
425
|
+
>
|
|
426
|
+
<SelectValue placeholder="All users" />
|
|
427
|
+
</SelectTrigger>
|
|
428
|
+
<SelectContent>
|
|
429
|
+
<SelectGroup>
|
|
430
|
+
<SelectItem value="all">All users</SelectItem>
|
|
431
|
+
{users.map((user) => (
|
|
432
|
+
<SelectItem key={user.email} value={user.email}>
|
|
433
|
+
{user.email}
|
|
434
|
+
</SelectItem>
|
|
435
|
+
))}
|
|
436
|
+
</SelectGroup>
|
|
437
|
+
</SelectContent>
|
|
438
|
+
</Select>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function UsageTrend({
|
|
443
|
+
rows,
|
|
444
|
+
billing,
|
|
445
|
+
}: {
|
|
446
|
+
rows: DailyUsageMetric[];
|
|
447
|
+
billing: UsageBillingMode;
|
|
448
|
+
}) {
|
|
449
|
+
const chartData = completeTrendRows(rows).map((row) => ({
|
|
450
|
+
...row,
|
|
451
|
+
spend: displayAmountFromCostCents(row.costCents, billing),
|
|
452
|
+
}));
|
|
453
|
+
|
|
454
|
+
return (
|
|
455
|
+
<Panel
|
|
456
|
+
title={
|
|
457
|
+
billing.unit === "builder-credits"
|
|
458
|
+
? "Credit usage trend"
|
|
459
|
+
: "Usage trend"
|
|
460
|
+
}
|
|
461
|
+
icon={<IconChartBar size={16} />}
|
|
462
|
+
action={
|
|
463
|
+
<div className="hidden items-center gap-3 text-[11px] text-muted-foreground sm:flex">
|
|
464
|
+
<span className="flex items-center gap-1.5">
|
|
465
|
+
<span className="size-2 rounded-full bg-[hsl(var(--dispatch-brand-blue))]" />
|
|
466
|
+
{billing.shortLabel}
|
|
467
|
+
</span>
|
|
468
|
+
<span className="flex items-center gap-1.5">
|
|
469
|
+
<span className="size-2 rounded-full bg-muted-foreground" />
|
|
470
|
+
Calls
|
|
471
|
+
</span>
|
|
472
|
+
</div>
|
|
473
|
+
}
|
|
474
|
+
>
|
|
475
|
+
{chartData.length === 0 ? (
|
|
476
|
+
<div className="flex min-h-56 items-center justify-center rounded-md border border-dashed px-4 text-sm text-muted-foreground">
|
|
477
|
+
No usage in this window yet.
|
|
478
|
+
</div>
|
|
479
|
+
) : (
|
|
480
|
+
<ChartContainer
|
|
481
|
+
config={{
|
|
482
|
+
spend: {
|
|
483
|
+
label: billing.shortLabel,
|
|
484
|
+
color: "hsl(var(--dispatch-brand-blue))",
|
|
485
|
+
},
|
|
486
|
+
calls: {
|
|
487
|
+
label: "Calls",
|
|
488
|
+
color: "hsl(var(--muted-foreground))",
|
|
489
|
+
},
|
|
490
|
+
}}
|
|
491
|
+
className="h-[280px] w-full aspect-auto"
|
|
492
|
+
>
|
|
493
|
+
<AreaChart
|
|
494
|
+
data={chartData}
|
|
495
|
+
margin={{ top: 8, right: 8, left: 12, bottom: 0 }}
|
|
496
|
+
>
|
|
497
|
+
<defs>
|
|
498
|
+
<linearGradient id="usage-trend-fill" x1="0" y1="0" x2="0" y2="1">
|
|
499
|
+
<stop
|
|
500
|
+
offset="0%"
|
|
501
|
+
stopColor="var(--color-spend)"
|
|
502
|
+
stopOpacity={0.24}
|
|
503
|
+
/>
|
|
504
|
+
<stop
|
|
505
|
+
offset="100%"
|
|
506
|
+
stopColor="var(--color-spend)"
|
|
507
|
+
stopOpacity={0}
|
|
508
|
+
/>
|
|
509
|
+
</linearGradient>
|
|
510
|
+
</defs>
|
|
511
|
+
<CartesianGrid
|
|
512
|
+
vertical={false}
|
|
513
|
+
stroke="hsl(var(--border))"
|
|
514
|
+
strokeDasharray="3 3"
|
|
515
|
+
/>
|
|
516
|
+
<XAxis
|
|
517
|
+
dataKey="date"
|
|
518
|
+
stroke="hsl(var(--muted-foreground))"
|
|
519
|
+
fontSize={11}
|
|
520
|
+
axisLine={false}
|
|
521
|
+
tickLine={false}
|
|
522
|
+
tickMargin={8}
|
|
523
|
+
minTickGap={28}
|
|
524
|
+
tickFormatter={formatTrendDate}
|
|
525
|
+
/>
|
|
526
|
+
<YAxis
|
|
527
|
+
yAxisId="spend"
|
|
528
|
+
stroke="hsl(var(--muted-foreground))"
|
|
529
|
+
fontSize={11}
|
|
530
|
+
axisLine={false}
|
|
531
|
+
tickLine={false}
|
|
532
|
+
tickMargin={8}
|
|
533
|
+
width={50}
|
|
534
|
+
tickFormatter={(value) =>
|
|
535
|
+
billing.unit === "builder-credits"
|
|
536
|
+
? formatCredits(Number(value))
|
|
537
|
+
: formatSpend(Number(value), billing)
|
|
538
|
+
}
|
|
539
|
+
/>
|
|
540
|
+
<YAxis yAxisId="calls" orientation="right" hide />
|
|
541
|
+
<ChartTooltip
|
|
542
|
+
cursor={false}
|
|
543
|
+
content={
|
|
544
|
+
<ChartTooltipContent
|
|
545
|
+
labelFormatter={(value) => formatTrendDate(String(value))}
|
|
546
|
+
formatter={(value, name) => [
|
|
547
|
+
name === "spend"
|
|
548
|
+
? billing.unit === "builder-credits"
|
|
549
|
+
? formatCredits(Number(value))
|
|
550
|
+
: formatSpend(Number(value), billing)
|
|
551
|
+
: `${formatNumber(Number(value))} calls`,
|
|
552
|
+
name === "spend" ? billing.shortLabel : "Calls",
|
|
553
|
+
]}
|
|
554
|
+
/>
|
|
555
|
+
}
|
|
556
|
+
/>
|
|
557
|
+
<Area
|
|
558
|
+
yAxisId="spend"
|
|
559
|
+
dataKey="spend"
|
|
560
|
+
type="monotone"
|
|
561
|
+
stroke="var(--color-spend)"
|
|
562
|
+
strokeWidth={2}
|
|
563
|
+
fill="url(#usage-trend-fill)"
|
|
564
|
+
fillOpacity={1}
|
|
565
|
+
dot={false}
|
|
566
|
+
activeDot={{ r: 4, strokeWidth: 2 }}
|
|
567
|
+
isAnimationActive={false}
|
|
568
|
+
/>
|
|
569
|
+
<Area
|
|
570
|
+
yAxisId="calls"
|
|
571
|
+
dataKey="calls"
|
|
572
|
+
type="monotone"
|
|
573
|
+
stroke="var(--color-calls)"
|
|
574
|
+
strokeWidth={1.5}
|
|
575
|
+
fill="none"
|
|
576
|
+
dot={false}
|
|
577
|
+
activeDot={{ r: 3, strokeWidth: 2 }}
|
|
578
|
+
isAnimationActive={false}
|
|
579
|
+
/>
|
|
580
|
+
</AreaChart>
|
|
581
|
+
</ChartContainer>
|
|
582
|
+
)}
|
|
583
|
+
</Panel>
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function ReviewUsageButton({
|
|
588
|
+
metrics,
|
|
589
|
+
billing,
|
|
590
|
+
}: {
|
|
591
|
+
metrics: DispatchUsageMetrics;
|
|
592
|
+
billing: UsageBillingMode;
|
|
593
|
+
}) {
|
|
594
|
+
function reviewUsage() {
|
|
595
|
+
const topApps = metrics.byApp
|
|
596
|
+
.slice(0, 5)
|
|
597
|
+
.map(
|
|
598
|
+
(row) =>
|
|
599
|
+
`${displayApp(row.key)}: ${formatSpend(row.costCents, billing)} / ${row.calls} calls`,
|
|
600
|
+
)
|
|
601
|
+
.join("; ");
|
|
602
|
+
const topLabels = metrics.byLabel
|
|
603
|
+
.slice(0, 5)
|
|
604
|
+
.map(
|
|
605
|
+
(row) =>
|
|
606
|
+
`${row.label}: ${formatSpend(row.costCents, billing)} / ${row.calls} calls`,
|
|
607
|
+
)
|
|
608
|
+
.join("; ");
|
|
609
|
+
const recentPrompts = metrics.recent
|
|
610
|
+
.slice(0, 8)
|
|
611
|
+
.map((row) => {
|
|
612
|
+
const prompt = row.prompt
|
|
613
|
+
? row.prompt.slice(0, 180)
|
|
614
|
+
: "prompt not captured";
|
|
615
|
+
return `${timeAgo(row.createdAt)} | ${displayApp(row.app)} | ${row.label} | ${prompt}`;
|
|
616
|
+
})
|
|
617
|
+
.join("\n");
|
|
618
|
+
|
|
619
|
+
sendToAgentChat({
|
|
620
|
+
message:
|
|
621
|
+
"Review this LLM usage and explain where the spend is going. Identify repeated, background, or unexpectedly expensive work, cite the strongest evidence, and suggest concrete fixes. Call out missing attribution instead of guessing.",
|
|
622
|
+
context: [
|
|
623
|
+
`Dispatch usage scope: ${metrics.viewScope === "workspace" ? "workspace" : "my account"}.`,
|
|
624
|
+
`Lookback: ${metrics.sinceDays} days. Viewer: ${metrics.access.viewerEmail}.`,
|
|
625
|
+
`Total: ${formatSpend(metrics.totals.costCents, billing)}, ${metrics.totals.calls} calls, ${metrics.totals.chatCalls} chat calls, ${formatTokens(metrics.totals.inputTokens + metrics.totals.outputTokens)} input/output tokens.`,
|
|
626
|
+
`Top apps: ${topApps || "none"}.`,
|
|
627
|
+
`Top work types: ${topLabels || "none"}.`,
|
|
628
|
+
`Recent prompt evidence (bounded to the latest 8 rows):\n${recentPrompts || "none"}`,
|
|
629
|
+
].join("\n"),
|
|
630
|
+
submit: true,
|
|
631
|
+
openSidebar: true,
|
|
632
|
+
chatTarget: "local",
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return (
|
|
637
|
+
<Button type="button" variant="outline" size="sm" onClick={reviewUsage}>
|
|
638
|
+
Ask agent
|
|
639
|
+
</Button>
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
310
643
|
function AppSpendRows({
|
|
311
644
|
rows,
|
|
312
645
|
billing,
|
|
@@ -362,40 +695,6 @@ function AppSpendRows({
|
|
|
362
695
|
);
|
|
363
696
|
}
|
|
364
697
|
|
|
365
|
-
function DailyActivity({ rows }: { rows: DailyUsageMetric[] }) {
|
|
366
|
-
const max = Math.max(
|
|
367
|
-
1,
|
|
368
|
-
rows.reduce((value, row) => Math.max(value, row.calls), 0),
|
|
369
|
-
);
|
|
370
|
-
if (rows.length === 0) {
|
|
371
|
-
return (
|
|
372
|
-
<div className="rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground">
|
|
373
|
-
No activity in this window.
|
|
374
|
-
</div>
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
return (
|
|
378
|
-
<div className="flex h-44 items-end gap-1">
|
|
379
|
-
{rows.map((row) => (
|
|
380
|
-
<div
|
|
381
|
-
key={row.date}
|
|
382
|
-
className="group flex min-w-0 flex-1 flex-col items-center gap-2"
|
|
383
|
-
>
|
|
384
|
-
<div className="relative flex h-36 w-full items-end rounded-sm bg-muted/60">
|
|
385
|
-
<div
|
|
386
|
-
className="w-full rounded-sm bg-foreground transition group-hover:bg-primary"
|
|
387
|
-
style={{ height: `${Math.max(4, (row.calls / max) * 100)}%` }}
|
|
388
|
-
/>
|
|
389
|
-
</div>
|
|
390
|
-
<div className="hidden text-[10px] text-muted-foreground sm:block">
|
|
391
|
-
{row.date.slice(5)}
|
|
392
|
-
</div>
|
|
393
|
-
</div>
|
|
394
|
-
))}
|
|
395
|
-
</div>
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
698
|
function AppAccessTable({
|
|
400
699
|
rows,
|
|
401
700
|
billing,
|
|
@@ -588,52 +887,49 @@ function RecentTable({
|
|
|
588
887
|
if (rows.length === 0) {
|
|
589
888
|
return (
|
|
590
889
|
<div className="rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground">
|
|
591
|
-
No
|
|
890
|
+
No prompts or LLM calls in this window.
|
|
592
891
|
</div>
|
|
593
892
|
);
|
|
594
893
|
}
|
|
595
894
|
return (
|
|
596
|
-
<div className="
|
|
597
|
-
|
|
598
|
-
<
|
|
599
|
-
<
|
|
600
|
-
<
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
<
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
</
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
))}
|
|
635
|
-
</tbody>
|
|
636
|
-
</table>
|
|
895
|
+
<div className="divide-y">
|
|
896
|
+
{rows.slice(0, 10).map((row) => (
|
|
897
|
+
<article key={row.id} className="-mx-1 px-1 py-3 first:pt-0 last:pb-0">
|
|
898
|
+
<div className="flex items-start justify-between gap-4">
|
|
899
|
+
<p className="min-w-0 whitespace-pre-wrap text-sm leading-6 text-foreground">
|
|
900
|
+
{row.prompt ||
|
|
901
|
+
(row.promptSource === "unavailable"
|
|
902
|
+
? "Prompt unavailable - linked thread data could not be read."
|
|
903
|
+
: "Prompt not captured for this call.")}
|
|
904
|
+
</p>
|
|
905
|
+
<span className="shrink-0 text-xs text-muted-foreground">
|
|
906
|
+
{timeAgo(row.createdAt)}
|
|
907
|
+
</span>
|
|
908
|
+
</div>
|
|
909
|
+
<div className="mt-2 flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
|
|
910
|
+
<Badge variant="outline">{displayApp(row.app)}</Badge>
|
|
911
|
+
<Badge variant="secondary">{row.label}</Badge>
|
|
912
|
+
<span>{row.model}</span>
|
|
913
|
+
<span aria-hidden="true">·</span>
|
|
914
|
+
<span>{formatSpend(row.costCents, billing)}</span>
|
|
915
|
+
{row.ownerEmail ? (
|
|
916
|
+
<>
|
|
917
|
+
<span aria-hidden="true">·</span>
|
|
918
|
+
<span className="max-w-64 truncate">{row.ownerEmail}</span>
|
|
919
|
+
</>
|
|
920
|
+
) : null}
|
|
921
|
+
{row.threadId ? (
|
|
922
|
+
<Link
|
|
923
|
+
to={`/admin/thread-debug?threadId=${encodeURIComponent(row.threadId)}`}
|
|
924
|
+
className="inline-flex items-center gap-1 rounded-md border px-2 py-0.5 font-medium text-foreground transition-colors hover:bg-muted"
|
|
925
|
+
>
|
|
926
|
+
Inspect thread
|
|
927
|
+
<IconArrowUpRight size={12} />
|
|
928
|
+
</Link>
|
|
929
|
+
) : null}
|
|
930
|
+
</div>
|
|
931
|
+
</article>
|
|
932
|
+
))}
|
|
637
933
|
</div>
|
|
638
934
|
);
|
|
639
935
|
}
|
|
@@ -641,9 +937,30 @@ function RecentTable({
|
|
|
641
937
|
export default function MetricsRoute() {
|
|
642
938
|
const t = useT();
|
|
643
939
|
const [sinceDays, setSinceDays] = useState(30);
|
|
940
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
941
|
+
const scope: "me" | "workspace" =
|
|
942
|
+
searchParams.get("scope") === "workspace" ? "workspace" : "me";
|
|
943
|
+
const userEmail =
|
|
944
|
+
scope === "workspace" ? searchParams.get("user") || null : null;
|
|
945
|
+
|
|
946
|
+
function setScope(nextScope: "me" | "workspace") {
|
|
947
|
+
const next = new URLSearchParams(searchParams);
|
|
948
|
+
if (nextScope === "me") next.delete("scope");
|
|
949
|
+
else next.set("scope", nextScope);
|
|
950
|
+
if (nextScope === "me") next.delete("user");
|
|
951
|
+
setSearchParams(next, { replace: true });
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function setUserEmail(nextUserEmail: string | null) {
|
|
955
|
+
const next = new URLSearchParams(searchParams);
|
|
956
|
+
if (nextUserEmail) next.set("user", nextUserEmail);
|
|
957
|
+
else next.delete("user");
|
|
958
|
+
setSearchParams(next, { replace: true });
|
|
959
|
+
}
|
|
960
|
+
|
|
644
961
|
const { data, isLoading, error } = useActionQuery(
|
|
645
962
|
"list-dispatch-usage-metrics",
|
|
646
|
-
{ sinceDays },
|
|
963
|
+
{ sinceDays, scope, userEmail: userEmail ?? undefined },
|
|
647
964
|
);
|
|
648
965
|
const metrics = data as DispatchUsageMetrics | undefined;
|
|
649
966
|
const billing = metrics?.billing ?? USD_BILLING;
|
|
@@ -668,12 +985,33 @@ export default function MetricsRoute() {
|
|
|
668
985
|
>
|
|
669
986
|
<div className="space-y-4">
|
|
670
987
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
671
|
-
<div
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
988
|
+
<div>
|
|
989
|
+
<div className="text-sm font-medium text-foreground">
|
|
990
|
+
{metrics?.selectedUserEmail
|
|
991
|
+
? `${metrics.selectedUserEmail}'s usage`
|
|
992
|
+
: scope === "workspace"
|
|
993
|
+
? "Workspace usage"
|
|
994
|
+
: "Your usage"}
|
|
995
|
+
</div>
|
|
996
|
+
<div className="mt-0.5 text-xs text-muted-foreground">
|
|
997
|
+
{metrics?.selectedUserEmail
|
|
998
|
+
? "Filtered to this workspace member"
|
|
999
|
+
: scope === "workspace"
|
|
1000
|
+
? `${metrics?.access.totalUsers ?? 0} users with access`
|
|
1001
|
+
: metrics?.access.viewerEmail || "Signed-in account"}
|
|
1002
|
+
</div>
|
|
1003
|
+
</div>
|
|
1004
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1005
|
+
<ScopeSelector value={scope} onChange={setScope} />
|
|
1006
|
+
{scope === "workspace" && metrics ? (
|
|
1007
|
+
<UserSelector
|
|
1008
|
+
value={metrics.selectedUserEmail ?? userEmail}
|
|
1009
|
+
users={metrics.availableUsers ?? []}
|
|
1010
|
+
onChange={setUserEmail}
|
|
1011
|
+
/>
|
|
1012
|
+
) : null}
|
|
1013
|
+
<RangeSelector value={sinceDays} onChange={setSinceDays} />
|
|
675
1014
|
</div>
|
|
676
|
-
<RangeSelector value={sinceDays} onChange={setSinceDays} />
|
|
677
1015
|
</div>
|
|
678
1016
|
|
|
679
1017
|
{error ? (
|
|
@@ -692,6 +1030,22 @@ export default function MetricsRoute() {
|
|
|
692
1030
|
|
|
693
1031
|
{metrics ? (
|
|
694
1032
|
<>
|
|
1033
|
+
<UsageTrend rows={metrics.daily} billing={billing} />
|
|
1034
|
+
|
|
1035
|
+
<div className="flex items-center justify-between gap-3 border-y py-3">
|
|
1036
|
+
<span className="text-sm text-muted-foreground">
|
|
1037
|
+
Review this usage with the agent
|
|
1038
|
+
</span>
|
|
1039
|
+
<ReviewUsageButton metrics={metrics} billing={billing} />
|
|
1040
|
+
</div>
|
|
1041
|
+
|
|
1042
|
+
<UsageAlertsPanel
|
|
1043
|
+
scope={scope === "workspace" ? "workspace" : "user"}
|
|
1044
|
+
appOptions={metrics.byApp
|
|
1045
|
+
.filter((row) => row.key && row.key !== "unattributed")
|
|
1046
|
+
.map((row) => ({ id: row.key, label: displayApp(row.key) }))}
|
|
1047
|
+
/>
|
|
1048
|
+
|
|
695
1049
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-5">
|
|
696
1050
|
<MetricCard
|
|
697
1051
|
label={billing.label}
|
|
@@ -725,21 +1079,29 @@ export default function MetricsRoute() {
|
|
|
725
1079
|
/>
|
|
726
1080
|
</div>
|
|
727
1081
|
|
|
728
|
-
<
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1082
|
+
<Panel
|
|
1083
|
+
title={
|
|
1084
|
+
billing.unit === "builder-credits"
|
|
1085
|
+
? "Credit spend by app"
|
|
1086
|
+
: "Spend by app"
|
|
1087
|
+
}
|
|
1088
|
+
icon={<IconApps size={16} />}
|
|
1089
|
+
>
|
|
1090
|
+
<AppSpendRows rows={metrics.byApp} billing={billing} />
|
|
1091
|
+
</Panel>
|
|
1092
|
+
|
|
1093
|
+
<Panel
|
|
1094
|
+
title="Recent prompts"
|
|
1095
|
+
icon={<IconMessages size={16} />}
|
|
1096
|
+
action={
|
|
1097
|
+
<span className="text-xs text-muted-foreground">
|
|
1098
|
+
Latest {Math.min(metrics.recent.length, 10)} of{" "}
|
|
1099
|
+
{metrics.recent.length}
|
|
1100
|
+
</span>
|
|
1101
|
+
}
|
|
1102
|
+
>
|
|
1103
|
+
<RecentTable rows={metrics.recent} billing={billing} />
|
|
1104
|
+
</Panel>
|
|
743
1105
|
|
|
744
1106
|
<Panel title="Access By App" icon={<IconApps size={16} />}>
|
|
745
1107
|
<AppAccessTable rows={metrics.appAccess} billing={billing} />
|
|
@@ -765,10 +1127,6 @@ export default function MetricsRoute() {
|
|
|
765
1127
|
/>
|
|
766
1128
|
</Panel>
|
|
767
1129
|
</div>
|
|
768
|
-
|
|
769
|
-
<Panel title="Recent LLM Calls" icon={<IconActivity size={16} />}>
|
|
770
|
-
<RecentTable rows={metrics.recent} billing={billing} />
|
|
771
|
-
</Panel>
|
|
772
1130
|
</>
|
|
773
1131
|
) : null}
|
|
774
1132
|
</div>
|
|
@@ -105,11 +105,11 @@ export default function OperationsRoute() {
|
|
|
105
105
|
>
|
|
106
106
|
<TabsList className="w-fit">
|
|
107
107
|
<TabsTrigger value="monitoring">
|
|
108
|
-
<IconActivity size={15} />
|
|
108
|
+
<IconActivity size={15} className="mr-1.5" />
|
|
109
109
|
{t("dispatch.pages.monitoring")}
|
|
110
110
|
</TabsTrigger>
|
|
111
111
|
<TabsTrigger value="database">
|
|
112
|
-
<IconDatabase size={15} />
|
|
112
|
+
<IconDatabase size={15} className="mr-1.5" />
|
|
113
113
|
{t("dispatch.pages.database")}
|
|
114
114
|
</TabsTrigger>
|
|
115
115
|
</TabsList>
|