{row.prompt || (row.promptSource === "unavailable" ? "Prompt unavailable - linked thread data could not be read." : "Prompt not captured for this call.")}
{timeAgo(row.createdAt)}import { sendToAgentChat } from "@agent-native/core/client/agent-chat"; import { useActionQuery } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { IconActivity, IconAlertTriangle, IconArrowUpRight, IconApps, IconChartBar, IconCoin, IconMessages, IconUsersGroup, } from "@tabler/icons-react"; import { useMemo, useState, type ReactNode } from "react"; import { Link, useSearchParams } from "react-router"; import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { DispatchShell } from "../../components/dispatch-shell"; import { Alert, AlertDescription, AlertTitle } from "../../components/ui/alert"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { ChartContainer, ChartTooltip, ChartTooltipContent, } from "../../components/ui/chart"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from "../../components/ui/select"; import { Skeleton } from "../../components/ui/skeleton"; import { UsageAlertsPanel } from "../../components/usage-alerts-panel"; import { cn } from "../../lib/utils"; export function meta() { return [{ title: "Metrics — Dispatch" }]; } interface UsageMetricBucket { key: string; label: string; costCents: number; calls: number; chatCalls: number; inputTokens: number; outputTokens: number; activeUsers: number; lastActiveAt: number | null; } interface UserUsageMetric extends UsageMetricBucket { ownerEmail: string; chatThreads: number; chatMessages: number; lastChatAt: number | null; topApp: string | null; role: string | null; } interface UsageUserOption { email: string; role: string | null; } interface AppAccessMetric { id: string; name: string; path: string; status?: "ready" | "pending"; statusLabel?: string; isDispatch: boolean; accessLabel: string; accessUsers: number; usersWithUsage: number; usageCalls: number; chatCalls: number; costCents: number; lastActiveAt: number | null; } interface DailyUsageMetric { date: string; costCents: number; calls: number; chatCalls: number; activeUsers: number; } interface RecentUsageMetric { id: number; createdAt: number; ownerEmail: string; app: string; label: string; model: string; inputTokens: number; outputTokens: number; costCents: number; prompt: string | null; promptSource: "thread" | "thread-preview" | "not-captured" | "unavailable"; threadId: string | null; runId: string | null; taskId: string | null; sourcePlatform: string | null; sourceId: string | null; } interface UsageBillingMode { unit: "usd" | "builder-credits"; label: string; shortLabel: string; source: "estimated-provider-cost" | "builder-agent-credits"; hardCostMarginMultiplier?: number; creditsPerUsd?: number; } interface DispatchUsageMetrics { billing?: UsageBillingMode; viewScope?: "me" | "workspace"; selectedUserEmail?: string | null; availableUsers?: UsageUserOption[]; sinceDays: number; access: { viewerEmail: string; orgId: string | null; role: string | null; scope: "organization" | "solo"; totalUsers: number; }; totals: { costCents: number; calls: number; chatCalls: number; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; activeUsers: number; chatThreads: number; chatMessages: number; workspaceApps: number; }; byApp: UsageMetricBucket[]; byUser: UserUsageMetric[]; byLabel: UsageMetricBucket[]; byModel: UsageMetricBucket[]; daily: DailyUsageMetric[]; appAccess: AppAccessMetric[]; recent: RecentUsageMetric[]; } const RANGES = [7, 30, 90] as const; const USD_BILLING: UsageBillingMode = { unit: "usd", label: "Estimated spend", shortLabel: "Cost", source: "estimated-provider-cost", }; function displayAmountFromCostCents( cents: number, billing: UsageBillingMode, ): number { if (billing.unit !== "builder-credits") return cents; const margin = billing.hardCostMarginMultiplier ?? 1.25; const creditsPerUsd = billing.creditsPerUsd ?? 20; const credits = (cents / 100) * margin * creditsPerUsd; return credits <= 0 ? 0 : Math.ceil(credits * 1000) / 1000; } function formatCredits(credits: number): string { if (!Number.isFinite(credits) || credits === 0) return "0 credits"; const maximumFractionDigits = credits < 1 ? 3 : credits < 10 ? 2 : 1; const value = credits.toLocaleString(undefined, { maximumFractionDigits, }); return `${value} ${credits === 1 ? "credit" : "credits"}`; } function formatSpend(cents: number, billing: UsageBillingMode): string { if (billing.unit === "builder-credits") { return formatCredits(displayAmountFromCostCents(cents, billing)); } if (!Number.isFinite(cents) || cents === 0) return "$0.00"; if (Math.abs(cents) < 1) return `${cents.toFixed(3)}¢`; if (Math.abs(cents) < 100) return `${cents.toFixed(2)}¢`; return (cents / 100).toLocaleString(undefined, { style: "currency", currency: "USD", maximumFractionDigits: 2, }); } function formatNumber(value: number): string { return new Intl.NumberFormat(undefined, { notation: value >= 10_000 ? "compact" : "standard", maximumFractionDigits: value >= 10_000 ? 1 : 0, }).format(value); } function formatTokens(value: number): string { return new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1, }).format(value); } function timeAgo(timestamp: number | null): string { if (!timestamp) return "No activity"; const diff = Date.now() - timestamp; if (diff < 60_000) return "just now"; if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`; if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`; return `${Math.floor(diff / 86_400_000)}d ago`; } function formatTrendDate(value: string): string { const date = new Date(`${value}T12:00:00`); return date.toLocaleDateString(undefined, { month: "short", day: "numeric", }); } function completeTrendRows(rows: DailyUsageMetric[]): DailyUsageMetric[] { if (rows.length < 2) return rows; const byDate = new Map(rows.map((row) => [row.date, row])); const start = new Date(`${rows[0].date}T12:00:00`); const end = new Date(`${rows[rows.length - 1].date}T12:00:00`); const completed: DailyUsageMetric[] = []; for ( const cursor = start; cursor <= end; cursor.setDate(cursor.getDate() + 1) ) { const date = cursor.toISOString().slice(0, 10); completed.push( byDate.get(date) ?? { date, costCents: 0, calls: 0, chatCalls: 0, activeUsers: 0, }, ); } return completed; } function displayApp(value: string | null | undefined): string { const trimmed = value?.trim(); if (!trimmed || trimmed === "unattributed") return "Unattributed"; return trimmed; } function maxSpend( rows: Array<{ costCents: number }>, billing: UsageBillingMode, ): number { return rows.reduce( (max, row) => Math.max(max, displayAmountFromCostCents(row.costCents, billing)), 0, ); } function barWidth(value: number, max: number): string { if (max <= 0 || value <= 0) return "0%"; return `${Math.max(4, Math.round((value / max) * 100))}%`; } function RangeSelector({ value, onChange, }: { value: number; onChange: (value: number) => void; }) { return (
| App | Access | Users | Chats | {billing.shortLabel} | Last activity |
|---|---|---|---|---|---|
|
{row.name}
{row.path}
|
|
{formatNumber(row.usersWithUsage)} /{" "} {formatNumber(row.accessUsers)} | {formatNumber(row.chatCalls)} | {formatSpend(row.costCents, billing)} | {timeAgo(row.lastActiveAt)} |
| User | Role | Top app | Chats | Threads | Tokens | {billing.shortLabel} |
|---|---|---|---|---|---|---|
|
{row.ownerEmail}
{timeAgo(row.lastActiveAt ?? row.lastChatAt)}
|
|
{displayApp(row.topApp)} | {formatNumber(row.chatCalls)} | {formatNumber(row.chatThreads)} | {formatTokens(row.inputTokens + row.outputTokens)} | {formatSpend(row.costCents, billing)} |
{row.prompt || (row.promptSource === "unavailable" ? "Prompt unavailable - linked thread data could not be read." : "Prompt not captured for this call.")}
{timeAgo(row.createdAt)}