Spaces:
Build error
Build error
File size: 5,464 Bytes
cf9339a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | import type { QuotaWindow } from "@paperclipai/shared";
import { cn, quotaSourceDisplayName } from "@/lib/utils";
interface CodexSubscriptionPanelProps {
windows: QuotaWindow[];
source?: string | null;
error?: string | null;
}
const WINDOW_PRIORITY = [
"5hlimit",
"weeklylimit",
"credits",
] as const;
function normalizeLabel(text: string): string {
return text.toLowerCase().replace(/[^a-z0-9]+/g, "");
}
function orderedWindows(windows: QuotaWindow[]): QuotaWindow[] {
return [...windows].sort((a, b) => {
const aBase = normalizeLabel(a.label).replace(/^gpt53codexspark/, "");
const bBase = normalizeLabel(b.label).replace(/^gpt53codexspark/, "");
const aIndex = WINDOW_PRIORITY.indexOf(aBase as (typeof WINDOW_PRIORITY)[number]);
const bIndex = WINDOW_PRIORITY.indexOf(bBase as (typeof WINDOW_PRIORITY)[number]);
return (aIndex === -1 ? WINDOW_PRIORITY.length : aIndex) - (bIndex === -1 ? WINDOW_PRIORITY.length : bIndex);
});
}
function detailText(window: QuotaWindow): string | null {
if (typeof window.detail === "string" && window.detail.trim().length > 0) return window.detail.trim();
if (!window.resetsAt) return null;
const formatted = new Date(window.resetsAt).toLocaleString(undefined, {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
timeZoneName: "short",
});
return `Resets ${formatted}`;
}
function fillClass(usedPercent: number | null): string {
if (usedPercent == null) return "bg-zinc-700";
if (usedPercent >= 90) return "bg-red-400";
if (usedPercent >= 70) return "bg-amber-400";
return "bg-primary/70";
}
function isModelSpecific(label: string): boolean {
const normalized = normalizeLabel(label);
return normalized.includes("gpt53codexspark") || normalized.includes("gpt5");
}
export function CodexSubscriptionPanel({
windows,
source = null,
error = null,
}: CodexSubscriptionPanelProps) {
const ordered = orderedWindows(windows);
const accountWindows = ordered.filter((window) => !isModelSpecific(window.label));
const modelWindows = ordered.filter((window) => isModelSpecific(window.label));
return (
<div className="border border-border px-4 py-4">
<div className="flex items-start justify-between gap-3 border-b border-border pb-3">
<div className="min-w-0">
<div className="text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground">
Codex subscription
</div>
<div className="mt-1 text-sm text-muted-foreground">
Live Codex quota windows.
</div>
</div>
{source ? (
<span className="shrink-0 border border-border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
{quotaSourceDisplayName(source)}
</span>
) : null}
</div>
{error ? (
<div className="mt-4 border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{error}
</div>
) : null}
<div className="mt-4 space-y-5">
<div className="space-y-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Account windows
</div>
<div className="space-y-3">
{accountWindows.map((window) => (
<QuotaWindowRow key={window.label} window={window} />
))}
</div>
</div>
{modelWindows.length > 0 ? (
<div className="space-y-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Model windows
</div>
<div className="space-y-3">
{modelWindows.map((window) => (
<QuotaWindowRow key={window.label} window={window} />
))}
</div>
</div>
) : null}
</div>
</div>
);
}
function QuotaWindowRow({ window }: { window: QuotaWindow }) {
const detail = detailText(window);
if (window.usedPercent == null) {
return (
<div className="border border-border px-3.5 py-3">
<div className="flex items-center justify-between gap-3">
<div className="text-sm font-medium text-foreground">{window.label}</div>
{window.valueLabel ? (
<div className="text-sm font-semibold tabular-nums text-foreground">{window.valueLabel}</div>
) : null}
</div>
{detail ? (
<div className="mt-2 text-xs text-muted-foreground">{detail}</div>
) : null}
</div>
);
}
return (
<div className="border border-border px-3.5 py-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="text-sm font-medium text-foreground">{window.label}</div>
{detail ? (
<div className="mt-1 text-xs text-muted-foreground">{detail}</div>
) : null}
</div>
<div className="shrink-0 text-sm font-semibold tabular-nums text-foreground">
{window.usedPercent}% used
</div>
</div>
<div className="mt-3 h-2 overflow-hidden bg-muted">
<div
className={cn("h-full transition-[width] duration-200", fillClass(window.usedPercent))}
style={{ width: `${Math.max(0, Math.min(100, window.usedPercent))}%` }}
/>
</div>
</div>
);
}
|