Files
goclaw/ui/web/src/pages/agents/agent-detail/chatgpt-oauth-quota-strip.tsx
T
Kai (Tam Nhu) Tran 30708ae79d feat(providers): support Codex OAuth pools with inherited routing defaults
* feat(auth): support named chatgpt oauth providers

- add provider-scoped ChatGPT OAuth routes and CLI support

- persist refresh tokens per provider and reject provider-type collisions

- wire provider OAuth setup flows in the dashboard and setup UI

Refs #448

* feat(agent): add chatgpt oauth account routing

- add agent other_config routing for manual and round-robin selection

- reuse routed provider resolution across resolver and pending loaders

- add router, parser, and agent advanced dialog coverage for multi-account use

Refs #448

* docs(api): describe chatgpt oauth routing

- document named-provider ChatGPT OAuth auth routes

- describe agent-side account routing and round-robin behavior

- update OpenAPI agent config schema and provider type enum

Refs #448

* fix(store): add missing agent key context helpers

* feat(ui): clarify chatgpt oauth account setup and routing

* docs(providers): align chatgpt oauth alias examples

* feat(agent): add codex pool activity dashboard

* fix(providers): harden codex oauth alias setup

* feat(codex-pool): improve routing dashboard UX

- redesign the Codex/OpenAI pool page around saved-pool checkpoints and live evidence

- add clearer selection, attention, and recent-proof states for pool members

- make the lower panels fill the remaining desktop viewport while staying responsive

* fix(store): resolve context helper merge duplication

* feat(oauth): add codex pool quota and observation APIs

- add quota inspection and observation endpoints for ChatGPT Subscription (OAuth) providers

- teach codex routing to surface pool activity, observation metadata, and quota-aware readiness

- extend tests and HTTP docs/OpenAPI for the new pool monitoring flows

* feat(web): add codex pool quota monitor and controls

- add provider quota fetching, readiness badges, and live routing evidence on the account pool page

- redesign pool setup and activity panels for multi-account management with localized copy updates

- keep the live monitor internally scrollable and compact the account cards for better viewport fit

* fix(web): clarify pool routing labels

- rename the recent request badge from Direct to Selected

- restore compact quota bars in the live pool cards

* feat(codex-pool): add runtime health dashboard

- derive per-provider success and failure health from routed Codex traces

- surface routing, quota, and recent request evidence in the pool UI

- align provider alias guidance and owner access with the dashboard role model

* docs(auth): document tenant scoping and key roles

* fix(auth): harden tenant and codex pool access control

* fix(providers): align codex pool runtime defaults

* feat(ui): tighten codex pool responsive layout

* feat(chatgpt-oauth): refine codex pool management UX

* feat(chatgpt-oauth): surface quota bars on provider pages

- add compact quota bars to Codex provider rows and provider detail

- fetch quota only for ready visible provider rows and ready detail aliases

- fix managed-member detail visibility and tighten provider locale copy
2026-03-27 09:35:57 +07:00

230 lines
7.8 KiB
TypeScript

import { useTranslation } from "react-i18next";
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { formatDate, formatRelativeTime } from "@/lib/format";
import { cn } from "@/lib/utils";
import type { ChatGPTOAuthProviderQuota } from "@/pages/providers/hooks/use-chatgpt-oauth-provider-quotas";
import {
getQuotaBadgeVariant,
getQuotaFailureKind,
getQuotaPlanLabel,
getQuotaSignals,
} from "./chatgpt-oauth-quota-utils";
interface ChatGPTOAuthQuotaStripProps {
quota?: ChatGPTOAuthProviderQuota | null;
loading?: boolean;
translationNamespace?: "agents" | "providers";
translationKeyPrefix?: string;
className?: string;
compact?: boolean;
layout?: "stacked" | "inline";
embedded?: boolean;
showSignalBadges?: boolean;
}
const failureVariantByKind = {
billing: "destructive",
exhausted: "destructive",
reauth: "warning",
forbidden: "destructive",
needs_setup: "warning",
retry_later: "outline",
unavailable: "outline",
} as const;
function quotaBarClass(remaining: number): string {
if (remaining <= 20) return "bg-destructive";
if (remaining <= 50) return "bg-amber-500";
return "bg-emerald-500";
}
export function ChatGPTOAuthQuotaStrip({
quota,
loading = false,
translationNamespace = "agents",
translationKeyPrefix = "chatgptOAuthRouting.quota",
className,
compact = false,
layout = "stacked",
embedded = false,
showSignalBadges = !compact,
}: ChatGPTOAuthQuotaStripProps) {
const { t } = useTranslation(translationNamespace);
if (loading && !quota) {
return (
<Badge variant="outline" className={cn("h-5 px-1.5 text-[10px]", className)}>
{t(`${translationKeyPrefix}.checking`)}
</Badge>
);
}
if (!quota) return null;
const failureKind = getQuotaFailureKind(quota);
const signals = getQuotaSignals(quota);
const planLabel = getQuotaPlanLabel(quota.plan_type);
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
layout === "inline"
? "flex min-w-0 items-center gap-1.5"
: compact
? cn(
"space-y-1 rounded-md px-2.5 py-1.5",
embedded ? "bg-transparent px-0 py-0" : "border bg-background/70",
)
: cn(
"space-y-1.5 rounded-md px-2.5 py-2",
embedded ? "bg-transparent px-0 py-0" : "border bg-background/70",
),
className,
)}
>
{failureKind ? (
<Badge
variant={failureVariantByKind[failureKind]}
className="h-5 px-1.5 text-[10px]"
>
{t(`${translationKeyPrefix}.failure.${failureKind}.label`)}
</Badge>
) : layout === "inline" ? (
<>
{planLabel && (
<Badge variant="outline" className="h-5 px-1.5 text-[10px]">
{planLabel}
</Badge>
)}
{signals.map((signal) => (
<div
key={signal.shortLabel}
className="flex items-center gap-1 rounded-full border bg-background/70 px-1.5 py-1"
>
<span className="text-[9px] font-medium uppercase tracking-wide text-muted-foreground">
{signal.shortLabel}
</span>
<div className="h-1.5 w-11 overflow-hidden rounded-full bg-muted">
<div
className={cn(
"h-full rounded-full transition-all",
quotaBarClass(signal.remaining),
)}
style={{
width: `${Math.max(6, Math.min(100, signal.remaining))}%`,
}}
/>
</div>
</div>
))}
</>
) : (
<>
<div className="flex flex-wrap items-center gap-1.5">
{planLabel && <Badge variant="outline">{planLabel}</Badge>}
{showSignalBadges &&
signals.map((signal) => (
<Badge
key={signal.shortLabel}
variant={getQuotaBadgeVariant(signal.remaining)}
>
{signal.shortLabel} {signal.remaining}%
</Badge>
))}
</div>
{signals.length > 0 && (
<div className="grid gap-1">
{signals.map((signal) => (
<div
key={signal.shortLabel}
className={cn(
"flex items-center gap-2",
compact && "gap-1.5",
)}
>
<span className="w-7 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
{signal.shortLabel}
</span>
<div className="flex-1 overflow-hidden rounded-full bg-muted h-1.5">
<div
className={cn(
"h-full rounded-full transition-all",
quotaBarClass(signal.remaining),
)}
style={{
width: `${Math.max(6, Math.min(100, signal.remaining))}%`,
}}
/>
</div>
{!compact && (
<span className="w-10 text-right text-[11px] font-medium">
{signal.remaining}%
</span>
)}
</div>
))}
</div>
)}
</>
)}
</div>
</TooltipTrigger>
<TooltipContent sideOffset={6} className="max-w-72 px-3 py-2">
{failureKind ? (
<div className="space-y-1.5">
<p className="font-medium">
{t(`${translationKeyPrefix}.failure.${failureKind}.label`)}
</p>
<p className="text-muted-foreground">
{t(`${translationKeyPrefix}.failure.${failureKind}.description`)}
</p>
{quota.action_hint && (
<p className="text-muted-foreground">{quota.action_hint}</p>
)}
</div>
) : (
<div className="space-y-1.5">
{planLabel && (
<div className="flex justify-between gap-3">
<span className="text-muted-foreground">
{t(`${translationKeyPrefix}.plan`)}
</span>
<span>{planLabel}</span>
</div>
)}
{signals.map((signal) => (
<div
key={signal.shortLabel}
className="flex justify-between gap-3"
>
<span>{signal.shortLabel}</span>
<span>
{signal.remaining}%
{signal.resetAt ? ` · ${formatDate(signal.resetAt)}` : ""}
</span>
</div>
))}
<p className="text-muted-foreground">
{t(`${translationKeyPrefix}.lastChecked`, {
value: formatRelativeTime(quota.last_updated),
})}
</p>
</div>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}