mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-11 03:13:24 +00:00
* 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
230 lines
7.8 KiB
TypeScript
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>
|
|
);
|
|
}
|