| 1 | import type { EnvironmentId, UsageLimitsReport } from "@t3tools/contracts"; |
| 2 | import { limitsNotice } from "@t3tools/shared/usageLimits"; |
| 3 | import { GaugeIcon } from "lucide-react"; |
| 4 | |
| 5 | import { ensureLocalApi } from "../../localApi"; |
| 6 | import { Button } from "../ui/button"; |
| 7 | import { providerClients } from "../settings/providerDriverMeta"; |
| 8 | import { RedactedSensitiveText } from "../settings/RedactedSensitiveText"; |
| 9 | import { LimitWindows, ResetCredits } from "../usage/UsageLimits"; |
| 10 | import { ComposerBanner } from "./ComposerBanner"; |
| 11 | import type { ComposerBannerStackItem } from "./ComposerBannerStack"; |
| 12 | |
| 13 | /** Driver name, then the instance when there could be more than one of that driver. */ |
| 14 | function 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 | |
| 26 | function 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. */ |
| 48 | export 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 | |
| 74 | function 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 | } |