import type { UsageAlertRule } from "@agent-native/core"; import { useActionMutation, useActionQuery, } from "@agent-native/core/client/hooks"; import { IconBell, IconMail, IconPencil, IconX } from "@tabler/icons-react"; import { useMemo, useState } from "react"; import { toast } from "sonner"; import { Badge } from "./ui/badge"; import { Button } from "./ui/button"; import { Checkbox } from "./ui/checkbox"; import { Input } from "./ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "./ui/select"; import { Skeleton } from "./ui/skeleton"; import { Switch } from "./ui/switch"; export interface UsageAlertAppOption { id: string; label: string; } interface UsageAlertsPanelProps { scope: "user" | "workspace"; appOptions?: UsageAlertAppOption[]; } type AlertUnit = "usd" | "builder-credits" | "tokens"; type AlertChannel = "in-app" | "email"; interface AlertDraft { ruleId?: string; appId: string; unit: AlertUnit; period: "day" | "month"; limit: string; inApp: boolean; email: boolean; } function asRules(value: unknown): UsageAlertRule[] { if (Array.isArray(value)) return value as UsageAlertRule[]; if (value && typeof value === "object") { const rules = (value as { rules?: unknown }).rules; if (Array.isArray(rules)) return rules as UsageAlertRule[]; } return []; } function formatAlertValue(rule: UsageAlertRule, value: number): string { if (rule.unit === "usd") { return value.toLocaleString(undefined, { style: "currency", currency: "USD", maximumFractionDigits: 2, }); } return ( value.toLocaleString(undefined, { maximumFractionDigits: rule.unit === "builder-credits" ? 2 : 0, }) + " " + (rule.unit === "builder-credits" ? "credits" : "tokens") ); } function draftFromRule(rule: UsageAlertRule): AlertDraft { return { ruleId: rule.id, appId: rule.appId ?? "", unit: rule.unit, period: rule.period, limit: String(rule.limit), inApp: rule.channels.includes("in-app"), email: rule.channels.includes("email"), }; } function defaultDraft(): AlertDraft { return { appId: "", unit: "usd", period: "day", limit: "100", inApp: true, email: true, }; } function AlertEditor({ draft, appOptions, isExisting, onChange, onSave, onCancel, isPending, }: { draft: AlertDraft; appOptions: UsageAlertAppOption[]; isExisting: boolean; onChange: (patch: Partial) => void; onSave: () => void; onCancel: () => void; isPending: boolean; }) { return (
); } export function UsageAlertsPanel({ scope, appOptions = [], }: UsageAlertsPanelProps) { const query = useActionQuery("get-usage-alerts", { scope }); const mutation = useActionMutation("manage-usage-alert", { onSuccess: () => { setEditing(null); toast.success("Usage alert updated"); }, onError: (error) => toast.error(String(error)), }); const [editing, setEditing] = useState(null); const rules = asRules(query.data); const appLabels = useMemo( () => new Map(appOptions.map((app) => [app.id, app.label])), [appOptions], ); function saveDraft() { if (!editing) return; const limit = Number(editing.limit); const channels: AlertChannel[] = []; if (editing.inApp) channels.push("in-app"); if (editing.email) channels.push("email"); if (!Number.isFinite(limit) || limit <= 0) { toast.error("Enter a positive threshold."); return; } if (channels.length === 0) { toast.error("Choose at least one delivery channel."); return; } mutation.mutate({ operation: "save", scope, ruleId: editing.ruleId, appId: editing.appId || null, unit: editing.unit, period: editing.period, limit, channels, enabled: true, }); } return (

Usage alerts

Get a quiet in-app and email notice when usage crosses a daily or monthly limit.

{query.isLoading && rules.length === 0 ? (
{Array.from({ length: 3 }).map((_, index) => (
))}
) : null} {query.isError ? (
Usage alerts are unavailable for this scope.{" "} {query.error instanceof Error ? query.error.message : "Try again."}
) : null} {!query.isLoading && !query.isError && rules.length === 0 ? (
No alerts yet. The recommended default is $100 per day across all apps.
) : null} {rules.map((rule) => { const isEditing = editing?.ruleId === rule.id; const target = rule.appId ? appLabels.get(rule.appId) || rule.appId : "All apps"; return (
{target} {rule.isDefault ? ( Default ) : null} {rule.status === "triggered" ? "Over limit" : rule.status === "dismissed" ? "Dismissed" : "On track"}
{formatAlertValue(rule, rule.current)} of{" "} {formatAlertValue(rule, rule.limit)} · per {rule.period} {rule.percent > 0 ? " · " + rule.percent + "%" : ""}
mutation.mutate({ operation: "set-enabled", scope, ruleId: rule.id, enabled: checked, }) } aria-label={ (rule.enabled ? "Disable " : "Enable ") + target + " usage alert" } disabled={mutation.isPending} /> {rule.status === "triggered" ? ( ) : null}
{rule.channels.includes("in-app") ? ( In app ) : null} {rule.channels.includes("email") ? ( Email ) : null}
{isEditing && editing ? (
setEditing((current) => current ? { ...current, ...patch } : current, ) } onSave={saveDraft} onCancel={() => setEditing(null)} isPending={mutation.isPending} />
) : null}
); })} {editing && !editing.ruleId ? (
setEditing((current) => current ? { ...current, ...patch } : current, ) } onSave={saveDraft} onCancel={() => setEditing(null)} isPending={mutation.isPending} />
) : null}
); }