/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { type ReactNode, useCallback, useMemo, useState } from 'react' import { Copy, Check, RefreshCw, ChevronDown, ChevronUp, RotateCcw, AlertTriangle, } from 'lucide-react' import { useTranslation } from 'react-i18next' import dayjs from '@/lib/dayjs' import { formatDateTimeStr, formatTimestampToDate } from '@/lib/format' import { cn } from '@/lib/utils' import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Button } from '@/components/ui/button' import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' import { Empty, EmptyDescription, EmptyHeader, EmptyTitle, } from '@/components/ui/empty' import { Progress } from '@/components/ui/progress' import { ScrollArea } from '@/components/ui/scroll-area' import { Skeleton } from '@/components/ui/skeleton' import { ConfirmDialog } from '@/components/confirm-dialog' import { Dialog } from '@/components/dialog' import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge' import { getCodexResetCredits, resetCodexUsage, type CodexResetCreditsResponse, } from '../../api' 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 CodexResetCredit = { id?: string reset_type?: string status?: string granted_at?: string | null expires_at?: string | null redeem_started_at?: string | null redeemed_at?: string | null profile_image_url?: string profile_user_id?: string title?: string description?: string } type CodexResetCreditsPayload = { credits?: CodexResetCredit[] available_count?: number total_earned_count?: number } type CodexUsagePayload = { plan_type?: string user_id?: string email?: string rate_limit?: CodexRateLimit additional_rate_limits?: CodexAdditionalRateLimit[] rate_limit_reset_credits?: { available_count?: number } credits?: { overage_limit_reached?: boolean } spend_control?: { reached?: boolean } } 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 | Promise 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) return Number.isFinite(v) && v > 0 ? formatTimestampToDate(v) : '-' } function formatIsoTimestamp(value: unknown): string { if (typeof value !== 'string' || value.trim() === '') return '-' const d = dayjs(value) if (!d.isValid()) return value return formatDateTimeStr(d.toDate()) } 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 formatTimeLeftUntil( value: unknown, t: (key: string) => string ): string { if (typeof value !== 'string' || value.trim() === '') return '-' const expiresAt = dayjs(value) if (!expiresAt.isValid()) return '-' const secondsLeft = expiresAt.diff(dayjs(), 'second') if (secondsLeft <= 0) return t('Expired') const days = Math.floor(secondsLeft / (24 * 60 * 60)) const remainingSeconds = secondsLeft % (24 * 60 * 60) if (days > 0) { const hours = Math.floor(remainingSeconds / 3600) return `${days} ${t('days')} ${hours}${t('h')}` } return formatDurationSeconds(secondsLeft, t) } function normalizePlanType(value: unknown): string { if (value == null) return '' return String(value).trim().toLowerCase() } function parseTimeValue(value: unknown): number { if (typeof value !== 'string' || value.trim() === '') { return Number.POSITIVE_INFINITY } const d = dayjs(value) return d.isValid() ? d.valueOf() : Number.POSITIVE_INFINITY } function normalizeResetCreditStatus(value: unknown): string { return String(value || '') .trim() .toLowerCase() } function sortResetCredits(credits: CodexResetCredit[]): CodexResetCredit[] { return [...credits].sort((a, b) => { const aAvailable = normalizeResetCreditStatus(a.status) === 'available' const bAvailable = normalizeResetCreditStatus(b.status) === 'available' if (aAvailable !== bAvailable) return aAvailable ? -1 : 1 const expiresDiff = parseTimeValue(a.expires_at) - parseTimeValue(b.expires_at) if (expiresDiff !== 0) return expiresDiff const grantedDiff = parseTimeValue(a.granted_at) - parseTimeValue(b.granted_at) if (grantedDiff !== 0) return grantedDiff return String(a.id || '').localeCompare(String(b.id || '')) }) } 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' }, } const RESET_CREDIT_STATUS_BADGE: Record< string, { label: string; variant: StatusBadgeProps['variant'] } > = { available: { label: 'Available', variant: 'success' }, redeemed: { label: 'Redeemed', variant: 'neutral' }, expired: { label: 'Expired', 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 getResetCreditStatusBadge( value: unknown, t: (key: string) => string ): { label: string; variant: StatusBadgeProps['variant'] } { const normalized = normalizeResetCreditStatus(value) return ( RESET_CREDIT_STATUS_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 } } function getUsageStatusBadge( rateLimit: CodexRateLimit | undefined, t: (key: string) => string ) { if (!rateLimit || Object.keys(rateLimit).length === 0) { return ( ) } if (rateLimit.allowed && !rateLimit.limit_reached) { return ( ) } return } function formatLabelValue(label: string, value: string) { return label.endsWith(':') ? `${label}${value}` : `${label} ${value}` } const percentTextClassName: Record< NonNullable, string > = { success: 'text-success', warning: 'text-warning', danger: 'text-destructive', info: 'text-info', neutral: 'text-muted-foreground', purple: 'text-chart-4', amber: 'text-warning', blue: 'text-chart-1', cyan: 'text-chart-2', green: 'text-success', grey: 'text-muted-foreground', indigo: 'text-chart-1', 'light-blue': 'text-info', 'light-green': 'text-success', lime: 'text-chart-3', orange: 'text-warning', pink: 'text-chart-5', red: 'text-destructive', teal: 'text-chart-2', violet: 'text-chart-4', yellow: 'text-warning', } 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} {t('Window:')}{' '} {hasData ? formatDurationSeconds(props.window?.limit_window_seconds, t) : '-'}
{hasData ? `${percent}%` : '-'}
{t('Used')}
{hasData ? ( ) : (
-
)}
{t('Reset at:')}
{hasData ? formatUnixSeconds(props.window?.reset_at) : '-'}
{t('Resets in:')}
{hasData ? formatDurationSeconds(props.window?.reset_after_seconds, t) : '-'}
) } function RateLimitWindowGrid(props: { fiveHourWindow?: CodexRateLimitWindow | null weeklyWindow?: CodexRateLimitWindow | null }) { const { t } = useTranslation() return (
) } function SectionHeading(props: { title: string description?: string children?: ReactNode }) { return (
{props.title}
{props.description ? (
{props.description}
) : null}
{props.children ? (
{props.children}
) : null}
) } type RateLimitGroupSectionProps = { title: string description?: string source: RateLimitSource | null meteredFeature?: string } function RateLimitGroupSection(props: RateLimitGroupSectionProps) { const { t } = useTranslation() const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(props.source) const statusBadge = getUsageStatusBadge(props.source?.rate_limit, t) return (
{statusBadge} {props.meteredFeature ? (
metered_feature {props.meteredFeature}
) : null}
) } function InfoField(props: { label: string value?: string | null mono?: boolean copyable?: boolean className?: string }) { const { t } = useTranslation() const { copyToClipboard, copiedText } = useCopyToClipboard({ notify: false }) const text = props.value?.trim() || '' const hasCopied = copiedText === text return (
{props.label}
{text || '-'} {props.copyable !== false && text ? ( ) : null}
) } function ResetCreditTimeField(props: { label: string value: string emphasis?: boolean }) { return (
{props.label}
{props.value}
) } function ResetCreditItem(props: { credit: CodexResetCredit; index: number }) { const { t } = useTranslation() const statusBadge = getResetCreditStatusBadge(props.credit.status, t) const title = props.credit.title?.trim() || `${t('Reset Credit')} ${props.index + 1}` const expiresIn = formatTimeLeftUntil(props.credit.expires_at, t) const isAvailable = normalizeResetCreditStatus(props.credit.status) === 'available' return (
{title}
{props.credit.description ? (
{props.credit.description}
) : null} {props.credit.id ? (
{props.credit.id}
) : null}
{t('Expires in')}
{expiresIn}
) } function ResetCreditsPanel(props: { payload: CodexResetCreditsPayload | null response: CodexResetCreditsResponse | null usageAvailableCount: string isLoading: boolean isResetting: boolean errorMessage: string resetErrorMessage: string resetSuccessMessage: string onRefresh: () => void onRequestReset: () => void }) { const { t } = useTranslation() const credits = useMemo( () => sortResetCredits(props.payload?.credits ?? []), [props.payload?.credits] ) const detailAvailableCount = props.payload?.available_count const availableCount = Number.isFinite(Number(detailAvailableCount)) ? String(detailAvailableCount) : props.usageAvailableCount const totalEarnedCount = Number.isFinite( Number(props.payload?.total_earned_count) ) ? String(props.payload?.total_earned_count) : '-' const canReset = Number(availableCount) > 0 return (
{t('Available credits are ordered by soonest expiration.')}
{t('Reset usage window')}
{t( 'Use one available reset credit to refresh the current Codex usage windows.' )}
{!canReset ? ( {t('No reset credits available')} {t('The reset request stays disabled until a credit is available.')} ) : null} {props.resetSuccessMessage ? ( {t('Reset completed')} {props.resetSuccessMessage} ) : null} {props.resetErrorMessage ? ( {t('Reset failed')} {props.resetErrorMessage} ) : null} {props.errorMessage ? (
{props.errorMessage}
) : props.isLoading ? (
) : credits.length > 0 ? (
{credits.map((credit, index) => ( ))}
) : ( {t('No reset credits')} {t('Upstream did not return reset credit details.')} )}
) } 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 [showResetCredits, setShowResetCredits] = useState(false) const [resetCreditsResponse, setResetCreditsResponse] = useState(null) const [isLoadingResetCredits, setIsLoadingResetCredits] = useState(false) const [resetCreditsError, setResetCreditsError] = useState('') const [resetConfirmOpen, setResetConfirmOpen] = useState(false) const [isResetting, setIsResetting] = useState(false) const [resetActionError, setResetActionError] = useState('') const [resetActionMessage, setResetActionMessage] = useState('') const payload: CodexUsagePayload | null = useMemo(() => { const raw = response?.data if (!raw || typeof raw !== 'object') return null return raw as CodexUsagePayload }, [response?.data]) const resetCreditsPayload: CodexResetCreditsPayload | null = useMemo(() => { const raw = resetCreditsResponse?.data if (!raw || typeof raw !== 'object') return null return raw as CodexResetCreditsPayload }, [resetCreditsResponse?.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 resetCredits = resetCreditsPayload?.available_count ?? payload?.rate_limit_reset_credits?.available_count const resetCreditsText = Number.isFinite(Number(resetCredits)) ? String(resetCredits) : '-' const canResetCodexUsage = Number(resetCredits) > 0 const channelLabel = `${channelName || '-'}${ channelId ? ` (#${channelId})` : '' }` const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(payload) const errorMessage = response?.success === false ? response?.message?.trim() || t('Failed to fetch usage') : '' const loadResetCredits = useCallback( async (force = false) => { if (!channelId) { setResetCreditsError(t('Channel ID is required')) return } if (isLoadingResetCredits || (!force && resetCreditsResponse)) return setIsLoadingResetCredits(true) setResetCreditsError('') try { const res = await getCodexResetCredits(channelId) if (!res.success) { throw new Error( res.message || t('Failed to fetch reset credit details') ) } setResetCreditsResponse(res) } catch (error) { setResetCreditsError( error instanceof Error ? error.message : t('Failed to fetch reset credit details') ) } finally { setIsLoadingResetCredits(false) } }, [channelId, isLoadingResetCredits, resetCreditsResponse, t] ) const handleResetCreditsOpenChange = (nextOpen: boolean) => { setShowResetCredits(nextOpen) if (nextOpen) { void loadResetCredits(false) } } const handleDialogOpenChange = (nextOpen: boolean) => { if (!nextOpen) { setShowRawJson(false) setShowResetCredits(false) setResetCreditsResponse(null) setResetCreditsError('') setIsLoadingResetCredits(false) setResetConfirmOpen(false) setIsResetting(false) setResetActionError('') setResetActionMessage('') } onOpenChange(nextOpen) } const handleConfirmReset = async () => { if (!channelId || isResetting || !canResetCodexUsage) return setIsResetting(true) setResetActionError('') setResetActionMessage('') try { const res = await resetCodexUsage(channelId) if (!res.success) { throw new Error(res.message || t('Failed to reset usage')) } const resetPayload = res.data as | { windows_reset?: number; code?: string } | undefined const windowsReset = Number(resetPayload?.windows_reset) setResetActionMessage( Number.isFinite(windowsReset) ? `${t('Reset completed. Latest usage has been refreshed.')} ${t( 'Affected windows:' )} ${windowsReset}` : t('Reset completed. Latest usage has been refreshed.') ) setResetConfirmOpen(false) await Promise.resolve(onRefresh?.()) await loadResetCredits(true) } catch (error) { setResetActionError( error instanceof Error ? error.message : t('Failed to reset usage') ) } finally { setIsResetting(false) } } 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 ( } >
{errorMessage && (
{errorMessage}
)} {t('Codex Account Status')} {onRefresh ? ( ) : null}
{getUsageStatusBadge(rateLimit, t)} 0 ? 'blue' : 'neutral'} copyable={false} /> {payload?.credits?.overage_limit_reached ? ( ) : null} {payload?.spend_control?.reached ? ( ) : null}
} >
{t('Reset Credits')}
0 ? 'blue' : 'neutral'} copyable={false} />
{t('View issued reset credits, grant dates, and expiration.')}
{showResetCredits ? ( ) : ( )}
void loadResetCredits(true)} onRequestReset={() => { setResetActionError('') setResetActionMessage('') setResetConfirmOpen(true) }} />
{getUsageStatusBadge(rateLimit, t)}
{additionalRateLimits.length > 0 ? (
{additionalRateLimits.map((item, index) => { const limitName = item.limit_name || item.metered_feature || `${t('Additional Limit')} ${index + 1}` return ( ) })}
) : null} } >
{t('Raw JSON')}
{showRawJson ? ( ) : ( )}
<>
                  {rawJsonText || '-'}
                

{t( 'Use one available reset credit for this channel. The reset request is sent only after confirmation.' )}

{channelLabel}
{t('Available reset credits')}: {resetCreditsText}

{t('Used reset credits cannot be restored.')}

} destructive disabled={!canResetCodexUsage} isLoading={isResetting} cancelBtnText={t('Cancel')} confirmText={isResetting ? t('Resetting...') : t('Apply reset')} handleConfirm={handleConfirmReset} />
) }