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