From eeb0dde8cabf0db5eea758d50ca7fd0b126a6404 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 29 Jan 2026 22:33:07 -0500 Subject: [PATCH] refactor(ui): extract shared QuotaTooltipContent component Address PR review feedback: - Create QuotaTooltipContent component in shared/ for DRY principle - Use component in account-item.tsx and account-card.tsx - Use QUOTA_SUPPORTED_PROVIDERS constant in model-config-tab.tsx - Reduces ~200 lines of duplicated tooltip rendering code --- .../account/flow-viz/account-card.tsx | 107 +--------------- .../cliproxy/provider-editor/account-item.tsx | 107 +--------------- .../provider-editor/model-config-tab.tsx | 5 +- ui/src/components/shared/index.ts | 1 + .../shared/quota-tooltip-content.tsx | 116 ++++++++++++++++++ 5 files changed, 129 insertions(+), 207 deletions(-) create mode 100644 ui/src/components/shared/quota-tooltip-content.tsx diff --git a/ui/src/components/account/flow-viz/account-card.tsx b/ui/src/components/account/flow-viz/account-card.tsx index babef8dc..7f678b3a 100644 --- a/ui/src/components/account/flow-viz/account-card.tsx +++ b/ui/src/components/account/flow-viz/account-card.tsx @@ -2,24 +2,14 @@ * Account Card Component for Flow Visualization */ -import { - cn, - formatResetTime, - getModelsWithTiers, - groupModelsByTier, - getProviderMinQuota, - getProviderResetTime, - isAgyQuotaResult, - isCodexQuotaResult, - isGeminiQuotaResult, - type ModelTier, -} from '@/lib/utils'; +import { cn, getProviderMinQuota, getProviderResetTime } from '@/lib/utils'; import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; -import { GripVertical, Loader2, Clock, Pause, Play } from 'lucide-react'; +import { GripVertical, Loader2, Pause, Play } from 'lucide-react'; import { useAccountQuota, QUOTA_SUPPORTED_PROVIDERS } from '@/hooks/use-cliproxy-stats'; import type { QuotaSupportedProvider } from '@/hooks/use-cliproxy-stats'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Button } from '@/components/ui/button'; +import { QuotaTooltipContent } from '@/components/shared/quota-tooltip-content'; import type { AccountData, DragOffset } from './types'; import { cleanEmail } from './utils'; @@ -105,6 +95,7 @@ export function AccountCard({ // Use shared helper for provider-specific minimum quota const minQuota = getProviderMinQuota(account.provider, quota); + const resetTime = getProviderResetTime(account.provider, quota); // Tier badge (AGY only) - show P for Pro, U for Ultra const showTierBadge = @@ -254,95 +245,7 @@ export function AccountCard({ - {quota && isAgyQuotaResult(quota) ? ( -
-

Model Quotas:

- {(() => { - const tiered = getModelsWithTiers(quota.models || []); - const groups = groupModelsByTier(tiered); - const tierOrder: ModelTier[] = ['primary', 'gemini-3', 'gemini-2', 'other']; - return tierOrder.map((tier, idx) => { - const models = groups.get(tier); - if (!models || models.length === 0) return null; - const isFirst = tierOrder - .slice(0, idx) - .every((t) => !groups.get(t)?.length); - return ( -
- {!isFirst &&
} - {models.map((m) => ( -
- - {m.displayName} - - - {m.percentage}% - -
- ))} -
- ); - }); - })()} - {(() => { - const resetTime = getProviderResetTime('agy', quota); - return resetTime ? ( -
- - - Resets {formatResetTime(resetTime)} - -
- ) : null; - })()} -
- ) : quota && isCodexQuotaResult(quota) ? ( -
-

Rate Limits:

- {quota.windows.map((w) => ( -
- - {w.label} - - {w.remainingPercent}% -
- ))} - {(() => { - const resetTime = getProviderResetTime('codex', quota); - return resetTime ? ( -
- - - Resets {formatResetTime(resetTime)} - -
- ) : null; - })()} -
- ) : quota && isGeminiQuotaResult(quota) ? ( -
-

Buckets:

- {quota.buckets.map((b) => ( -
- - {b.label} - - {b.remainingPercent}% -
- ))} - {(() => { - const resetTime = getProviderResetTime('gemini', quota); - return resetTime ? ( -
- - - Resets {formatResetTime(resetTime)} - -
- ) : null; - })()} -
- ) : null} + {quota && } diff --git a/ui/src/components/cliproxy/provider-editor/account-item.tsx b/ui/src/components/cliproxy/provider-editor/account-item.tsx index 474d4db4..a7e58558 100644 --- a/ui/src/components/cliproxy/provider-editor/account-item.tsx +++ b/ui/src/components/cliproxy/provider-editor/account-item.tsx @@ -29,20 +29,10 @@ import { FolderCode, Check, } from 'lucide-react'; -import { - cn, - formatResetTime, - getModelsWithTiers, - groupModelsByTier, - getProviderMinQuota, - getProviderResetTime, - isAgyQuotaResult, - isCodexQuotaResult, - isGeminiQuotaResult, - type ModelTier, -} from '@/lib/utils'; +import { cn, getProviderMinQuota, getProviderResetTime } from '@/lib/utils'; import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; import { useAccountQuota, useCliproxyStats } from '@/hooks/use-cliproxy-stats'; +import { QuotaTooltipContent } from '@/components/shared/quota-tooltip-content'; import type { AccountItemProps } from './types'; /** @@ -125,97 +115,6 @@ export function AccountItem({ const minQuota = getProviderMinQuota(account.provider, quota); const nextReset = getProviderResetTime(account.provider, quota); - // Provider-specific tooltip content renderer using type guards - const renderQuotaTooltip = () => { - if (!quota?.success) return null; - - // Antigravity (agy) provider tooltip - if (isAgyQuotaResult(quota)) { - const tiered = getModelsWithTiers(quota.models || []); - const groups = groupModelsByTier(tiered); - const tierOrder: ModelTier[] = ['primary', 'gemini-3', 'gemini-2', 'other']; - return ( -
-

Model Quotas:

- {tierOrder.map((tier, idx) => { - const models = groups.get(tier); - if (!models || models.length === 0) return null; - const isFirst = tierOrder.slice(0, idx).every((t) => !groups.get(t)?.length); - return ( -
- {!isFirst &&
} - {models.map((m) => ( -
- - {m.displayName} - - - {m.percentage}% - -
- ))} -
- ); - })} - {nextReset && ( -
- - Resets {formatResetTime(nextReset)} -
- )} -
- ); - } - - // Codex provider tooltip - if (isCodexQuotaResult(quota)) { - return ( -
-

Rate Limit Windows:

- {quota.planType &&

Plan: {quota.planType}

} - {quota.windows.map((w) => ( -
- {w.label} - {w.remainingPercent}% -
- ))} - {nextReset && ( -
- - Resets {formatResetTime(nextReset)} -
- )} -
- ); - } - - // Gemini provider tooltip - if (isGeminiQuotaResult(quota)) { - return ( -
-

Model Buckets:

- {quota.buckets.map((b) => ( -
- - {b.label} - {b.tokenType ? ` (${b.tokenType})` : ''} - - {b.remainingPercent}% -
- ))} - {nextReset && ( -
- - Resets {formatResetTime(nextReset)} -
- )} -
- ); - } - - return null; - }; - return (
- {renderQuotaTooltip()} + {quota && } diff --git a/ui/src/components/cliproxy/provider-editor/model-config-tab.tsx b/ui/src/components/cliproxy/provider-editor/model-config-tab.tsx index 464e30b4..ed5ff819 100644 --- a/ui/src/components/cliproxy/provider-editor/model-config-tab.tsx +++ b/ui/src/components/cliproxy/provider-editor/model-config-tab.tsx @@ -11,6 +11,7 @@ import { AccountsSection } from './accounts-section'; import { api } from '@/lib/api-client'; import type { ProviderCatalog } from '../provider-model-selector'; import type { OAuthAccount } from '@/lib/api-client'; +import { QUOTA_SUPPORTED_PROVIDERS, type QuotaSupportedProvider } from '@/hooks/use-cliproxy-stats'; interface ModelConfigTabProps { provider: string; @@ -167,7 +168,9 @@ export function ModelConfigTab({ isBulkPausing={isBulkPausing} isBulkResuming={isBulkResuming} privacyMode={privacyMode} - showQuota={['agy', 'codex', 'gemini'].includes(provider) && !isRemoteMode} + showQuota={ + QUOTA_SUPPORTED_PROVIDERS.includes(provider as QuotaSupportedProvider) && !isRemoteMode + } isKiro={isKiro} kiroNoIncognito={kiroNoIncognito} onKiroNoIncognitoChange={saveKiroNoIncognito} diff --git a/ui/src/components/shared/index.ts b/ui/src/components/shared/index.ts index 602a9712..ae67a7e4 100644 --- a/ui/src/components/shared/index.ts +++ b/ui/src/components/shared/index.ts @@ -17,6 +17,7 @@ export { PrivacyToggle } from './privacy-toggle'; export { ProjectSelectionDialog } from './project-selection-dialog'; export { ProviderIcon } from './provider-icon'; export { QuickCommands } from './quick-commands'; +export { QuotaTooltipContent } from './quota-tooltip-content'; export { SettingsDialog } from './settings-dialog'; export { SponsorButton } from './sponsor-button'; export { StatCard } from './stat-card'; diff --git a/ui/src/components/shared/quota-tooltip-content.tsx b/ui/src/components/shared/quota-tooltip-content.tsx new file mode 100644 index 00000000..177394e8 --- /dev/null +++ b/ui/src/components/shared/quota-tooltip-content.tsx @@ -0,0 +1,116 @@ +/** + * Shared Quota Tooltip Content Component + * Displays provider-specific quota information in tooltips + */ + +import { Clock } from 'lucide-react'; +import { + cn, + formatResetTime, + getModelsWithTiers, + groupModelsByTier, + isAgyQuotaResult, + isCodexQuotaResult, + isGeminiQuotaResult, + type ModelTier, + type UnifiedQuotaResult, +} from '@/lib/utils'; + +interface QuotaTooltipContentProps { + quota: UnifiedQuotaResult; + resetTime: string | null; +} + +/** + * Renders provider-specific quota tooltip content + * Uses type guards for proper TypeScript narrowing + */ +export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentProps) { + if (!quota?.success) return null; + + // Antigravity (agy) provider tooltip + if (isAgyQuotaResult(quota)) { + const tiered = getModelsWithTiers(quota.models || []); + const groups = groupModelsByTier(tiered); + const tierOrder: ModelTier[] = ['primary', 'gemini-3', 'gemini-2', 'other']; + + return ( +
+

Model Quotas:

+ {tierOrder.map((tier, idx) => { + const models = groups.get(tier); + if (!models || models.length === 0) return null; + const isFirst = tierOrder.slice(0, idx).every((t) => !groups.get(t)?.length); + return ( +
+ {!isFirst &&
} + {models.map((m) => ( +
+ + {m.displayName} + + + {m.percentage}% + +
+ ))} +
+ ); + })} + +
+ ); + } + + // Codex provider tooltip + if (isCodexQuotaResult(quota)) { + return ( +
+

Rate Limits:

+ {quota.planType &&

Plan: {quota.planType}

} + {quota.windows.map((w) => ( +
+ {w.label} + {w.remainingPercent}% +
+ ))} + +
+ ); + } + + // Gemini provider tooltip + if (isGeminiQuotaResult(quota)) { + return ( +
+

Buckets:

+ {quota.buckets.map((b) => ( +
+ + {b.label} + {b.tokenType ? ` (${b.tokenType})` : ''} + + {b.remainingPercent}% +
+ ))} + +
+ ); + } + + return null; +} + +/** + * Reset time indicator shown at bottom of tooltip + */ +function ResetTimeIndicator({ resetTime }: { resetTime: string | null }) { + if (!resetTime) return null; + + return ( +
+ + Resets {formatResetTime(resetTime)} +
+ ); +}