1import type { EnvironmentId, UsageLimitsReport } from "@t3tools/contracts";
2import { limitsNotice } from "@t3tools/shared/usageLimits";
3import { GaugeIcon } from "lucide-react";
4
5import { ensureLocalApi } from "../../localApi";
6import { Button } from "../ui/button";
7import { providerClients } from "../settings/providerDriverMeta";
8import { RedactedSensitiveText } from "../settings/RedactedSensitiveText";
9import { LimitWindows, ResetCredits } from "../usage/UsageLimits";
10import { ComposerBanner } from "./ComposerBanner";
11import type { ComposerBannerStackItem } from "./ComposerBannerStack";
12
13/** Driver name, then the instance when there could be more than one of that driver. */
14function accountLabel(account: UsageLimitsReport["accounts"][number]): string {
15 if (!account.instanceId) return account.label;
16 const driver = providerClients.get(account.driver)?.label ?? String(account.driver);
17 const instance =
18 account.displayName?.trim() ||
19 (String(account.instanceId) !== String(account.driver) ? account.instanceId : "");
20 // The default instance is often named after its driver; saying it twice adds nothing.
21 return instance && instance.toLowerCase() !== driver.toLowerCase()
22 ? `${driver} · ${instance}`
23 : driver;
24}
25
26function AccountSummary({ account }: { readonly account: UsageLimitsReport["accounts"][number] }) {
27 const label = accountLabel(account);
28 return (
29 <>
30 {label.includes("@") ? (
31 <RedactedSensitiveText
32 key={label}
33 value={label}
34 ariaLabel="Toggle account label visibility"
35 revealTooltip="Click to reveal account"
36 hideTooltip="Click to hide account"
37 className="max-w-full truncate align-bottom font-sans text-xs leading-normal"
38 />
39 ) : (
40 label
41 )}
42 {account.plan ? ` · ${account.plan}` : null}
43 </>
44 );
45}
46
47/** The /usage-limits result as a composer notice: it stacks under warnings and dismisses like one. */
48export function usageLimitsBannerItem(
49 id: string,
50 report: UsageLimitsReport,
51 environmentId: EnvironmentId,
52 onDismiss: () => void,
53): ComposerBannerStackItem {
54 const [first] = report.accounts;
55 const single = report.accounts.length === 1 && first ? first : null;
56 const summary = single ? (
57 <AccountSummary account={single} />
58 ) : (
59 `${report.accounts.length} accounts`
60 );
61 return {
62 id,
63 variant: "info",
64 priority: "notice",
65 icon: <GaugeIcon />,
66 title: "Usage limits",
67 description: summary,
68 dismissLabel: "Dismiss usage limits",
69 onDismiss,
70 children: <UsageLimitsBannerBody report={report} environmentId={environmentId} />,
71 };
72}
73
74function UsageLimitsBannerBody({
75 report,
76 environmentId,
77}: {
78 readonly report: UsageLimitsReport;
79 readonly environmentId: EnvironmentId;
80}) {
81 const now = Date.parse(report.createdAt);
82 return (
83 <ComposerBanner.Scroll>
84 <ComposerBanner.Body className="flex flex-col gap-2 pt-1 pb-1.5 pe-2">
85 {report.accounts.map((account) => {
86 const resetCreditInput =
87 account.resetCreditInput ??
88 (account.instanceId ? { instanceId: account.instanceId } : undefined);
89 const notice = limitsNotice(account.limits);
90 const externalUsage = account.limits.externalUsage;
91 return (
92 <div key={account.id} className="flex min-w-0 flex-col gap-1">
93 {report.accounts.length > 1 ? (
94 <span className="truncate text-xs text-muted-foreground">
95 <AccountSummary account={account} />
96 </span>
97 ) : null}
98 {notice ? (
99 <span className="text-xs text-muted-foreground">{notice}</span>
100 ) : (
101 <LimitWindows
102 compact
103 driver={account.driver}
104 windows={account.limits.windows}
105 now={now}
106 />
107 )}
108 {externalUsage ? (
109 <Button
110 size="sm"
111 variant="link"
112 className="self-start"
113 onClick={() => void ensureLocalApi().shell.openExternal(externalUsage.url)}
114 >
115 Manage usage
116 </Button>
117 ) : null}
118 {resetCreditInput && account.limits.resetCredits ? (
119 <ResetCredits
120 environmentId={environmentId}
121 input={resetCreditInput}
122 credits={account.limits.resetCredits}
123 now={now}
124 />
125 ) : null}
126 </div>
127 );
128 })}
129 {report.notices.map((notice) => (
130 <span key={notice} className="text-xs text-muted-foreground">
131 {notice}
132 </span>
133 ))}
134 </ComposerBanner.Body>
135 </ComposerBanner.Scroll>
136 );
137}