import { useMemo, useState } from 'react' import { Copy, Check, RefreshCw, ChevronDown, ChevronUp, User, Mail, Hash, } from 'lucide-react' import { useTranslation } from 'react-i18next' import dayjs from '@/lib/dayjs' import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Progress } from '@/components/ui/progress' import { ScrollArea } from '@/components/ui/scroll-area' import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge' type CodexRateLimitWindow = { used_percent?: number reset_at?: number reset_after_seconds?: number limit_window_seconds?: number } type CodexRateLimit = { plan_type?: string allowed?: boolean limit_reached?: boolean primary_window?: CodexRateLimitWindow secondary_window?: CodexRateLimitWindow } type CodexAdditionalRateLimit = { limit_name?: string metered_feature?: string rate_limit?: CodexRateLimit primary_window?: CodexRateLimitWindow secondary_window?: CodexRateLimitWindow plan_type?: string } type CodexUsagePayload = { plan_type?: string user_id?: string email?: string account_id?: string rate_limit?: CodexRateLimit additional_rate_limits?: CodexAdditionalRateLimit[] } export type CodexUsageDialogData = { success: boolean message?: string upstream_status?: number data?: Record } type CodexUsageDialogProps = { open: boolean onOpenChange: (open: boolean) => void channelName?: string channelId?: number response: CodexUsageDialogData | null onRefresh?: () => void isRefreshing?: boolean } function clampPercent(value: unknown): number { const v = Number(value) return Number.isFinite(v) ? Math.max(0, Math.min(100, v)) : 0 } function formatUnixSeconds(unixSeconds: unknown): string { const v = Number(unixSeconds) if (!Number.isFinite(v) || v <= 0) return '-' try { return dayjs(v * 1000).format('YYYY-MM-DD HH:mm:ss') } catch { return String(unixSeconds) } } function formatDurationSeconds( seconds: unknown, t: (key: string) => string ): string { const s = Number(seconds) if (!Number.isFinite(s) || s <= 0) return '-' const total = Math.floor(s) const hours = Math.floor(total / 3600) const minutes = Math.floor((total % 3600) / 60) const secs = total % 60 if (hours > 0) return `${hours}${t('h')} ${minutes}${t('m')}` if (minutes > 0) return `${minutes}${t('m')} ${secs}${t('s')}` return `${secs}${t('s')}` } function normalizePlanType(value: unknown): string { if (value == null) return '' return String(value).trim().toLowerCase() } function classifyWindowByDuration( windowData?: CodexRateLimitWindow | null ): 'weekly' | 'fiveHour' | null { const seconds = Number(windowData?.limit_window_seconds) if (!Number.isFinite(seconds) || seconds <= 0) return null return seconds >= 24 * 60 * 60 ? 'weekly' : 'fiveHour' } type RateLimitSource = { plan_type?: string rate_limit?: CodexRateLimit } function resolveRateLimitWindows(data: RateLimitSource | null): { fiveHourWindow: CodexRateLimitWindow | null weeklyWindow: CodexRateLimitWindow | null } { const rateLimit = data?.rate_limit ?? {} const primary = rateLimit?.primary_window ?? null const secondary = rateLimit?.secondary_window ?? null const windows = [primary, secondary].filter(Boolean) as CodexRateLimitWindow[] const planType = normalizePlanType(data?.plan_type ?? rateLimit?.plan_type) let fiveHourWindow: CodexRateLimitWindow | null = null let weeklyWindow: CodexRateLimitWindow | null = null for (const w of windows) { const bucket = classifyWindowByDuration(w) if (bucket === 'fiveHour' && !fiveHourWindow) { fiveHourWindow = w continue } if (bucket === 'weekly' && !weeklyWindow) { weeklyWindow = w } } if (planType === 'free') { if (!weeklyWindow) weeklyWindow = primary ?? secondary ?? null return { fiveHourWindow: null, weeklyWindow } } if (!fiveHourWindow && !weeklyWindow) { return { fiveHourWindow: primary, weeklyWindow: secondary } } if (!fiveHourWindow) { fiveHourWindow = windows.find((w) => w !== weeklyWindow) ?? null } if (!weeklyWindow) { weeklyWindow = windows.find((w) => w !== fiveHourWindow) ?? null } return { fiveHourWindow, weeklyWindow } } const PLAN_TYPE_BADGE: Record< string, { label: string; variant: StatusBadgeProps['variant'] } > = { enterprise: { label: 'Enterprise', variant: 'success' }, team: { label: 'Team', variant: 'info' }, pro: { label: 'Pro', variant: 'blue' }, plus: { label: 'Plus', variant: 'purple' }, free: { label: 'Free', variant: 'warning' }, } function getAccountTypeBadge( value: unknown, t: (key: string) => string ): { label: string; variant: StatusBadgeProps['variant'] } { const normalized = normalizePlanType(value) return ( PLAN_TYPE_BADGE[normalized] ?? { label: String(value || '') || t('Unknown'), variant: 'neutral' as const, } ) } function windowLabel(windowData?: CodexRateLimitWindow | null) { const percent = clampPercent(windowData?.used_percent) const variant: StatusBadgeProps['variant'] = percent >= 95 ? 'danger' : percent >= 80 ? 'warning' : 'info' return { percent, variant } } type RateLimitWindowProps = { title: string window?: CodexRateLimitWindow | null } function RateLimitWindow(props: RateLimitWindowProps) { const { t } = useTranslation() const hasData = !!props.window && typeof props.window === 'object' && Object.keys(props.window).length > 0 const { percent, variant } = windowLabel(props.window) return (
{props.title}
{hasData ? (
{t('Reset at:')} {formatUnixSeconds(props.window?.reset_at)}
{t('Resets in:')}{' '} {formatDurationSeconds(props.window?.reset_after_seconds, t)}
{t('Window:')}{' '} {formatDurationSeconds(props.window?.limit_window_seconds, t)}
) : (
-
)}
) } type RateLimitGroupSectionProps = { title: string description?: string source: RateLimitSource | null meteredFeature?: string } function RateLimitGroupSection(props: RateLimitGroupSectionProps) { const { t } = useTranslation() const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(props.source) return (
{props.title}
{(props.description || props.meteredFeature) && (
{props.description && {props.description}} {props.meteredFeature && ( metered_feature {props.meteredFeature} )}
)}
) } function CopyableField(props: { icon: React.ReactNode label: string value?: string | null mono?: boolean }) { const { copyToClipboard, copiedText } = useCopyToClipboard({ notify: false }) const text = props.value?.trim() || '' const hasCopied = copiedText === text return (
{props.icon} {props.label} {text || '-'}
{text && ( )}
) } export function CodexUsageDialog({ open, onOpenChange, channelName, channelId, response, onRefresh, isRefreshing, }: CodexUsageDialogProps) { const { t } = useTranslation() const { copiedText, copyToClipboard } = useCopyToClipboard({ notify: false }) const [showRawJson, setShowRawJson] = useState(false) const payload: CodexUsagePayload | null = useMemo(() => { const raw = response?.data if (!raw || typeof raw !== 'object') return null return raw as CodexUsagePayload }, [response?.data]) const rateLimit = payload?.rate_limit const accountType = payload?.plan_type ?? rateLimit?.plan_type const accountBadge = getAccountTypeBadge(accountType, t) const additionalRateLimits = (payload?.additional_rate_limits ?? []).filter( (item) => item && Object.keys(item).length > 0 ) const statusBadge = (() => { if (!rateLimit || Object.keys(rateLimit).length === 0) { return ( ) } if (rateLimit.allowed && !rateLimit.limit_reached) { return ( ) } return ( ) })() const errorMessage = response?.success === false ? response?.message?.trim() || t('Failed to fetch usage') : '' const rawJsonText = useMemo(() => { if (!response) return '' try { return JSON.stringify( { success: response.success, message: response.message, upstream_status: response.upstream_status, data: response.data, }, null, 2 ) } catch { return String(response?.data ?? '') } }, [response]) return ( {t('Codex Account & Usage')} {t('Channel:')} {channelName || '-'}{' '} {channelId ? `(#${channelId})` : ''}
{errorMessage && (
{errorMessage}
)} {/* Account summary */}
{statusBadge} {typeof response?.upstream_status === 'number' && ( )}
{onRefresh && ( )}
{/* Account identity info */}
} label='User ID' value={payload?.user_id} mono /> } label={t('Email')} value={payload?.email} /> } label='Account ID' value={payload?.account_id} mono />
{/* Rate limit windows */}
{t('Rate Limit Windows')}

{t( 'Tracks current account base limits and additional metered usage on Codex upstream.' )}

{additionalRateLimits.length > 0 && (
{t('Additional Limits')}

{t( 'Per-feature metered windows split by model or capability.' )}

{additionalRateLimits.map((item, index) => { const limitName = item.limit_name || item.metered_feature || `${t('Additional Limit')} ${index + 1}` return (
0 ? 'border-t pt-4' : ''} >
) })}
)}
{/* Raw JSON collapsible */}
{showRawJson && ( <>
                    {rawJsonText || '-'}
                  
)}
) }