mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 12:19:35 +00:00
fix(accounts): show one codex plan badge
This commit is contained in:
@@ -6,6 +6,7 @@ import { AccountSurfaceCard } from '@/components/account/shared/account-surface-
|
|||||||
import { QuotaTooltipContent } from '@/components/shared/quota-tooltip-content';
|
import { QuotaTooltipContent } from '@/components/shared/quota-tooltip-content';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { getCodexIdentityBadge, type CodexIdentityBadge } from '@/lib/account-identity';
|
||||||
import {
|
import {
|
||||||
cn,
|
cn,
|
||||||
formatQuotaPercent,
|
formatQuotaPercent,
|
||||||
@@ -88,68 +89,56 @@ function getCompactQuotaColor(percentage: number) {
|
|||||||
return 'bg-red-500';
|
return 'bg-red-500';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCodexPlanAudience(quota: unknown): AccountAudience {
|
function getCodexQuotaBadge(quota: unknown): CodexIdentityBadge {
|
||||||
if (!quota || typeof quota !== 'object' || !('planType' in quota)) {
|
if (!quota || typeof quota !== 'object' || !('planType' in quota)) {
|
||||||
return 'unknown';
|
return { audience: 'unknown', label: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const planType = (quota as { planType?: unknown }).planType;
|
const planType = (quota as { planType?: unknown }).planType;
|
||||||
if (typeof planType !== 'string' || planType.trim().length === 0) {
|
if (typeof planType !== 'string' || planType.trim().length === 0) {
|
||||||
return 'unknown';
|
return { audience: 'unknown', label: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (planType === 'team') return 'business';
|
if (planType === 'team') {
|
||||||
if (planType === 'free') return 'free';
|
return { audience: 'business', label: 'Business' };
|
||||||
if (planType === 'plus' || planType === 'pro') return 'personal';
|
}
|
||||||
return 'unknown';
|
|
||||||
|
if (planType === 'free') {
|
||||||
|
return { audience: 'free', label: 'Free' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (planType === 'plus') {
|
||||||
|
return { audience: 'personal', label: 'Plus' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (planType === 'pro') {
|
||||||
|
return { audience: 'personal', label: 'Pro' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { audience: 'unknown', label: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveCodexAudience(
|
function resolveCodexBadge(
|
||||||
identityAudience: AccountAudience,
|
identityBadge: CodexIdentityBadge,
|
||||||
planAudience: AccountAudience
|
quotaBadge: CodexIdentityBadge
|
||||||
): AccountAudience {
|
): CodexIdentityBadge {
|
||||||
if (planAudience === 'unknown') {
|
if (!quotaBadge.label) {
|
||||||
return identityAudience;
|
return identityBadge;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (planAudience === 'business') {
|
if (
|
||||||
return 'business';
|
quotaBadge.label === 'Business' ||
|
||||||
|
quotaBadge.label === 'Plus' ||
|
||||||
|
quotaBadge.label === 'Pro'
|
||||||
|
) {
|
||||||
|
return quotaBadge;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (identityAudience === 'business') {
|
if (quotaBadge.label === 'Free' && identityBadge.label && identityBadge.label !== 'Free') {
|
||||||
return 'business';
|
return identityBadge;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (planAudience === 'personal') {
|
return quotaBadge;
|
||||||
return 'personal';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (planAudience === 'free') {
|
|
||||||
return identityAudience === 'unknown' || identityAudience === 'free'
|
|
||||||
? 'free'
|
|
||||||
: identityAudience;
|
|
||||||
}
|
|
||||||
|
|
||||||
return identityAudience;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCodexPlanDetailLabel(quota: unknown): string | null {
|
|
||||||
if (!quota || typeof quota !== 'object' || !('planType' in quota)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const planType = (quota as { planType?: unknown }).planType;
|
|
||||||
if (typeof planType !== 'string' || planType.trim().length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (planType === 'free' || planType === 'team') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return planType === 'plus' || planType === 'pro'
|
|
||||||
? planType[0].toUpperCase() + planType.slice(1)
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVariantDetailLabel(
|
function getVariantDetailLabel(
|
||||||
@@ -160,9 +149,32 @@ function getVariantDetailLabel(
|
|||||||
},
|
},
|
||||||
quota?: unknown
|
quota?: unknown
|
||||||
) {
|
) {
|
||||||
return (
|
return resolveCodexBadge(
|
||||||
variant.detailLabel ?? variant.compactDetailLabel ?? getCodexPlanDetailLabel(quota) ?? null
|
getCodexIdentityBadge({
|
||||||
);
|
audience: variant.audience,
|
||||||
|
detailLabel: variant.detailLabel,
|
||||||
|
compactDetailLabel: variant.compactDetailLabel,
|
||||||
|
}),
|
||||||
|
getCodexQuotaBadge(quota)
|
||||||
|
).label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVariantBadgeAudience(
|
||||||
|
variant: {
|
||||||
|
audience: AccountAudience;
|
||||||
|
detailLabel?: string | null;
|
||||||
|
compactDetailLabel?: string | null;
|
||||||
|
},
|
||||||
|
quota?: unknown
|
||||||
|
) {
|
||||||
|
return resolveCodexBadge(
|
||||||
|
getCodexIdentityBadge({
|
||||||
|
audience: variant.audience,
|
||||||
|
detailLabel: variant.detailLabel,
|
||||||
|
compactDetailLabel: variant.compactDetailLabel,
|
||||||
|
}),
|
||||||
|
getCodexQuotaBadge(quota)
|
||||||
|
).audience;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVariantCompactDetailLabel(
|
function getVariantCompactDetailLabel(
|
||||||
@@ -173,9 +185,7 @@ function getVariantCompactDetailLabel(
|
|||||||
},
|
},
|
||||||
quota?: unknown
|
quota?: unknown
|
||||||
) {
|
) {
|
||||||
return (
|
return getVariantDetailLabel(variant, quota);
|
||||||
variant.compactDetailLabel ?? variant.detailLabel ?? getCodexPlanDetailLabel(quota) ?? null
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDetailedAudienceLabel(audience: AccountAudience): string | null {
|
function getDetailedAudienceLabel(audience: AccountAudience): string | null {
|
||||||
@@ -191,7 +201,7 @@ function getVariantAudience(
|
|||||||
},
|
},
|
||||||
quota?: unknown
|
quota?: unknown
|
||||||
) {
|
) {
|
||||||
return resolveCodexAudience(variant.audience, getCodexPlanAudience(quota));
|
return getVariantBadgeAudience(variant, quota);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVariantInlineLabel(
|
function getVariantInlineLabel(
|
||||||
@@ -204,14 +214,7 @@ function getVariantInlineLabel(
|
|||||||
},
|
},
|
||||||
quota?: unknown
|
quota?: unknown
|
||||||
) {
|
) {
|
||||||
const detailLabel = getVariantDetailLabel(variant, quota);
|
return getVariantDetailLabel(variant, quota) || variant.inlineLabel || null;
|
||||||
const audience = getVariantAudience(variant, quota);
|
|
||||||
const audienceLabel =
|
|
||||||
variant.audience === audience
|
|
||||||
? (variant.audienceLabel ?? getDetailedAudienceLabel(audience))
|
|
||||||
: getDetailedAudienceLabel(audience);
|
|
||||||
const composedLabel = [audienceLabel, detailLabel].filter(Boolean).join(' · ');
|
|
||||||
return composedLabel || variant.inlineLabel || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVariantMarkerLabel(
|
function getVariantMarkerLabel(
|
||||||
@@ -227,8 +230,7 @@ function getVariantMarkerLabel(
|
|||||||
const audience = getVariantAudience(variant, quota);
|
const audience = getVariantAudience(variant, quota);
|
||||||
const compactDetailLabel = getVariantCompactDetailLabel(variant, quota);
|
const compactDetailLabel = getVariantCompactDetailLabel(variant, quota);
|
||||||
if (audience === 'business') {
|
if (audience === 'business') {
|
||||||
const businessVariantCount = audienceCounts.get('business') ?? 0;
|
return 'Biz';
|
||||||
return businessVariantCount > 1 && compactDetailLabel ? compactDetailLabel : 'Biz';
|
|
||||||
}
|
}
|
||||||
if (audience === 'free') {
|
if (audience === 'free') {
|
||||||
return compactDetailLabel ?? 'Free';
|
return compactDetailLabel ?? 'Free';
|
||||||
@@ -237,11 +239,7 @@ function getVariantMarkerLabel(
|
|||||||
return compactDetailLabel ?? 'Pers';
|
return compactDetailLabel ?? 'Pers';
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedFallback =
|
const normalizedFallback = compactDetailLabel?.trim() || getDetailedAudienceLabel(audience);
|
||||||
compactDetailLabel?.trim() ||
|
|
||||||
getDetailedAudienceLabel(audience) ||
|
|
||||||
variant.audienceLabel?.trim() ||
|
|
||||||
variant.detailLabel?.trim();
|
|
||||||
return normalizedFallback?.[0]?.toUpperCase() ?? '?';
|
return normalizedFallback?.[0]?.toUpperCase() ?? '?';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import type { ReactNode } from 'react';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||||
import type { UnifiedQuotaResult } from '@/hooks/use-cliproxy-stats';
|
import type { UnifiedQuotaResult } from '@/hooks/use-cliproxy-stats';
|
||||||
import { formatAccountVariantPart, getAccountIdentityPresentation } from '@/lib/account-identity';
|
import {
|
||||||
|
getAccountIdentityPresentation,
|
||||||
|
getCodexIdentityBadge,
|
||||||
|
type CodexIdentityBadge,
|
||||||
|
} from '@/lib/account-identity';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Pause, Star, User } from 'lucide-react';
|
import { Pause, Star, User } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -66,13 +70,6 @@ function getCompactAudienceBadgeLabel(audience: AccountAudience, t: (key: string
|
|||||||
return '?';
|
return '?';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDetailedAudienceLabel(audience: AccountAudience): string | null {
|
|
||||||
if (audience === 'business') return 'Business';
|
|
||||||
if (audience === 'free') return 'Free';
|
|
||||||
if (audience === 'personal') return 'Personal';
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCompactDetailBadgeClass(audience: AccountAudience) {
|
function getCompactDetailBadgeClass(audience: AccountAudience) {
|
||||||
if (audience === 'business') {
|
if (audience === 'business') {
|
||||||
return 'border-sky-500/30 bg-sky-500/10 text-sky-700 dark:border-sky-400/30 dark:bg-sky-500/15 dark:text-sky-200';
|
return 'border-sky-500/30 bg-sky-500/10 text-sky-700 dark:border-sky-400/30 dark:bg-sky-500/15 dark:text-sky-200';
|
||||||
@@ -102,56 +99,51 @@ function resolveEffectiveTier(
|
|||||||
return tier;
|
return tier;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCodexPlanAudience(quota: UnifiedQuotaResult | undefined): AccountAudience {
|
function getCodexQuotaBadge(quota: UnifiedQuotaResult | undefined): CodexIdentityBadge {
|
||||||
if (!quota || !('planType' in quota) || !quota.planType) {
|
if (!quota || !('planType' in quota) || !quota.planType) {
|
||||||
return 'unknown';
|
return { audience: 'unknown', label: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (quota.planType === 'team') return 'business';
|
if (quota.planType === 'team') {
|
||||||
if (quota.planType === 'free') return 'free';
|
return { audience: 'business', label: 'Business' };
|
||||||
if (quota.planType === 'plus' || quota.planType === 'pro') return 'personal';
|
}
|
||||||
return 'unknown';
|
|
||||||
|
if (quota.planType === 'free') {
|
||||||
|
return { audience: 'free', label: 'Free' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quota.planType === 'plus') {
|
||||||
|
return { audience: 'personal', label: 'Plus' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quota.planType === 'pro') {
|
||||||
|
return { audience: 'personal', label: 'Pro' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { audience: 'unknown', label: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveCodexAudience(
|
function resolveCodexBadge(
|
||||||
identityAudience: AccountAudience,
|
identityBadge: CodexIdentityBadge,
|
||||||
planAudience: AccountAudience
|
quotaBadge: CodexIdentityBadge
|
||||||
): AccountAudience {
|
): CodexIdentityBadge {
|
||||||
if (planAudience === 'unknown') {
|
if (!quotaBadge.label) {
|
||||||
return identityAudience;
|
return identityBadge;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (planAudience === 'business') {
|
if (
|
||||||
return 'business';
|
quotaBadge.label === 'Business' ||
|
||||||
|
quotaBadge.label === 'Plus' ||
|
||||||
|
quotaBadge.label === 'Pro'
|
||||||
|
) {
|
||||||
|
return quotaBadge;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (identityAudience === 'business') {
|
if (quotaBadge.label === 'Free' && identityBadge.label && identityBadge.label !== 'Free') {
|
||||||
return 'business';
|
return identityBadge;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (planAudience === 'personal') {
|
return quotaBadge;
|
||||||
return 'personal';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (planAudience === 'free') {
|
|
||||||
return identityAudience === 'unknown' || identityAudience === 'free'
|
|
||||||
? 'free'
|
|
||||||
: identityAudience;
|
|
||||||
}
|
|
||||||
|
|
||||||
return identityAudience;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCodexPlanDetailLabel(quota: UnifiedQuotaResult | undefined): string | null {
|
|
||||||
if (!quota || !('planType' in quota) || !quota.planType) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (quota.planType === 'free' || quota.planType === 'team') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatAccountVariantPart(quota.planType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AccountSurfaceCard({
|
export function AccountSurfaceCard({
|
||||||
@@ -182,22 +174,10 @@ export function AccountSurfaceCard({
|
|||||||
const title = displayEmail || identity.email || accountId;
|
const title = displayEmail || identity.email || accountId;
|
||||||
const normalizedProvider = provider.toLowerCase();
|
const normalizedProvider = provider.toLowerCase();
|
||||||
const effectiveTier = resolveEffectiveTier(tier, quota);
|
const effectiveTier = resolveEffectiveTier(tier, quota);
|
||||||
const codexPlanAudience =
|
const effectiveCodexBadge =
|
||||||
normalizedProvider === 'codex' ? getCodexPlanAudience(quota) : 'unknown';
|
|
||||||
const codexPlanDetailLabel =
|
|
||||||
normalizedProvider === 'codex' ? getCodexPlanDetailLabel(quota) : null;
|
|
||||||
const effectiveAudience =
|
|
||||||
normalizedProvider === 'codex'
|
normalizedProvider === 'codex'
|
||||||
? resolveCodexAudience(identity.audience, codexPlanAudience)
|
? resolveCodexBadge(getCodexIdentityBadge(identity), getCodexQuotaBadge(quota))
|
||||||
: identity.audience !== 'unknown'
|
: null;
|
||||||
? identity.audience
|
|
||||||
: codexPlanAudience;
|
|
||||||
const effectiveAudienceLabel =
|
|
||||||
identity.audience === effectiveAudience
|
|
||||||
? (identity.audienceLabel ?? getDetailedAudienceLabel(effectiveAudience))
|
|
||||||
: getDetailedAudienceLabel(effectiveAudience);
|
|
||||||
const resolvedDetailLabel = identity.detailLabel ?? codexPlanDetailLabel;
|
|
||||||
const resolvedCompactDetailLabel = identity.compactDetailLabel ?? codexPlanDetailLabel;
|
|
||||||
const showTierBadge =
|
const showTierBadge =
|
||||||
(normalizedProvider === 'agy' ||
|
(normalizedProvider === 'agy' ||
|
||||||
normalizedProvider === 'antigravity' ||
|
normalizedProvider === 'antigravity' ||
|
||||||
@@ -218,26 +198,38 @@ export function AccountSurfaceCard({
|
|||||||
{effectiveTier}
|
{effectiveTier}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{effectiveAudienceLabel && (
|
{normalizedProvider === 'codex'
|
||||||
<span
|
? effectiveCodexBadge?.label && (
|
||||||
title={effectiveAudienceLabel}
|
<span
|
||||||
className={cn(
|
title={effectiveCodexBadge.label}
|
||||||
'text-[8px] font-semibold px-1.5 py-0.5 rounded-md shrink-0',
|
className={cn(
|
||||||
getAudienceBadgeClass(effectiveAudience)
|
'text-[8px] font-semibold px-1.5 py-0.5 rounded-md border shrink-0',
|
||||||
|
getCompactDetailBadgeClass(effectiveCodexBadge.audience)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{effectiveCodexBadge.label}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
: identity.audienceLabel && (
|
||||||
|
<span
|
||||||
|
title={identity.audienceLabel}
|
||||||
|
className={cn(
|
||||||
|
'text-[8px] font-semibold px-1.5 py-0.5 rounded-md shrink-0',
|
||||||
|
getAudienceBadgeClass(identity.audience)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{getCompactAudienceBadgeLabel(identity.audience, t)}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
>
|
{normalizedProvider !== 'codex' && identity.compactDetailLabel && (
|
||||||
{getCompactAudienceBadgeLabel(effectiveAudience, t)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{resolvedCompactDetailLabel && (
|
|
||||||
<span
|
<span
|
||||||
title={resolvedDetailLabel ?? resolvedCompactDetailLabel}
|
title={identity.detailLabel ?? identity.compactDetailLabel}
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-[8px] font-semibold px-1.5 py-0.5 rounded-md border shrink-0',
|
'text-[8px] font-semibold px-1.5 py-0.5 rounded-md border shrink-0',
|
||||||
getCompactDetailBadgeClass(effectiveAudience)
|
getCompactDetailBadgeClass(identity.audience)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{resolvedCompactDetailLabel}
|
{identity.compactDetailLabel}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{paused && (
|
{paused && (
|
||||||
@@ -295,20 +287,31 @@ export function AccountSurfaceCard({
|
|||||||
{title}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
{isCompact && (compactMetaBadges ?? defaultCompactMetaBadges)}
|
{isCompact && (compactMetaBadges ?? defaultCompactMetaBadges)}
|
||||||
{!isCompact && effectiveAudienceLabel && (
|
{!isCompact && normalizedProvider === 'codex' && effectiveCodexBadge?.label && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-[10px] h-4 px-1.5 border-transparent',
|
'text-[10px] h-4 px-1.5 border-transparent',
|
||||||
getAudienceBadgeClass(effectiveAudience)
|
getAudienceBadgeClass(effectiveCodexBadge.audience)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{effectiveAudienceLabel}
|
{effectiveCodexBadge.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{!isCompact && resolvedDetailLabel && (
|
{!isCompact && normalizedProvider !== 'codex' && identity.audienceLabel && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
'text-[10px] h-4 px-1.5 border-transparent',
|
||||||
|
getAudienceBadgeClass(identity.audience)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{identity.audienceLabel}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{!isCompact && normalizedProvider !== 'codex' && identity.detailLabel && (
|
||||||
<Badge variant="outline" className="text-[10px] h-4 px-1.5">
|
<Badge variant="outline" className="text-[10px] h-4 px-1.5">
|
||||||
{resolvedDetailLabel}
|
{identity.detailLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{!isCompact && isDefault && (
|
{!isCompact && isDefault && (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { ChevronRight, ArrowLeft, User, ExternalLink } from 'lucide-react';
|
import { ChevronRight, ArrowLeft, User, ExternalLink } from 'lucide-react';
|
||||||
import { getAccountIdentityPresentation } from '@/lib/account-identity';
|
import { getAccountIdentityPresentation, getCodexIdentityBadge } from '@/lib/account-identity';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||||
import type { AccountStepProps } from '../types';
|
import type { AccountStepProps } from '../types';
|
||||||
@@ -30,6 +30,8 @@ export function AccountStep({
|
|||||||
<div className="grid gap-2 max-h-[320px] overflow-y-auto pr-1">
|
<div className="grid gap-2 max-h-[320px] overflow-y-auto pr-1">
|
||||||
{accounts.map((acc) => {
|
{accounts.map((acc) => {
|
||||||
const identity = getAccountIdentityPresentation(acc.id, acc.email, acc.tokenFile);
|
const identity = getAccountIdentityPresentation(acc.id, acc.email, acc.tokenFile);
|
||||||
|
const codexBadge =
|
||||||
|
acc.provider?.toLowerCase() === 'codex' ? getCodexIdentityBadge(identity) : null;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={acc.id}
|
key={acc.id}
|
||||||
@@ -46,7 +48,21 @@ export function AccountStep({
|
|||||||
{identity.email}
|
{identity.email}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
{identity.audienceLabel && (
|
{codexBadge?.label ? (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
'text-[10px] h-4 px-1.5 border-transparent',
|
||||||
|
codexBadge.audience === 'business'
|
||||||
|
? 'bg-sky-500/12 text-sky-700 dark:text-sky-300'
|
||||||
|
: codexBadge.audience === 'free'
|
||||||
|
? 'bg-slate-200/70 text-slate-700 dark:bg-slate-700/40 dark:text-slate-200'
|
||||||
|
: 'bg-emerald-500/12 text-emerald-700 dark:text-emerald-300'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{codexBadge.label}
|
||||||
|
</Badge>
|
||||||
|
) : identity.audienceLabel ? (
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -60,8 +76,8 @@ export function AccountStep({
|
|||||||
>
|
>
|
||||||
{identity.audienceLabel}
|
{identity.audienceLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
) : null}
|
||||||
{identity.detailLabel && (
|
{!codexBadge?.label && identity.detailLabel && (
|
||||||
<Badge variant="outline" className="text-[10px] h-4 px-1.5">
|
<Badge variant="outline" className="text-[10px] h-4 px-1.5">
|
||||||
{identity.detailLabel}
|
{identity.detailLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { getAccountIdentityPresentation } from '@/lib/account-identity';
|
import { getAccountIdentityPresentation, getCodexIdentityBadge } from '@/lib/account-identity';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -55,6 +55,10 @@ export function VariantStep({
|
|||||||
selectedAccount.tokenFile
|
selectedAccount.tokenFile
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
const selectedCodexBadge =
|
||||||
|
selectedProvider === 'codex' && selectedAccountIdentity
|
||||||
|
? getCodexIdentityBadge(selectedAccountIdentity)
|
||||||
|
: null;
|
||||||
|
|
||||||
const handleModelSelect = (value: string) => {
|
const handleModelSelect = (value: string) => {
|
||||||
if (value === CUSTOM_MODEL_VALUE) {
|
if (value === CUSTOM_MODEL_VALUE) {
|
||||||
@@ -80,7 +84,21 @@ export function VariantStep({
|
|||||||
</span>
|
</span>
|
||||||
{(selectedAccountIdentity?.audienceLabel || selectedAccountIdentity?.detailLabel) && (
|
{(selectedAccountIdentity?.audienceLabel || selectedAccountIdentity?.detailLabel) && (
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
{selectedAccountIdentity?.audienceLabel && (
|
{selectedCodexBadge?.label ? (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
'text-[10px] h-4 px-1.5 border-transparent',
|
||||||
|
selectedCodexBadge.audience === 'business'
|
||||||
|
? 'bg-sky-500/12 text-sky-700 dark:text-sky-300'
|
||||||
|
: selectedCodexBadge.audience === 'free'
|
||||||
|
? 'bg-slate-200/70 text-slate-700 dark:bg-slate-700/40 dark:text-slate-200'
|
||||||
|
: 'bg-emerald-500/12 text-emerald-700 dark:text-emerald-300'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selectedCodexBadge.label}
|
||||||
|
</Badge>
|
||||||
|
) : selectedAccountIdentity?.audienceLabel ? (
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -94,8 +112,8 @@ export function VariantStep({
|
|||||||
>
|
>
|
||||||
{selectedAccountIdentity.audienceLabel}
|
{selectedAccountIdentity.audienceLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
) : null}
|
||||||
{selectedAccountIdentity?.detailLabel && (
|
{!selectedCodexBadge?.label && selectedAccountIdentity?.detailLabel && (
|
||||||
<Badge variant="outline" className="text-[10px] h-4 px-1.5">
|
<Badge variant="outline" className="text-[10px] h-4 px-1.5">
|
||||||
{selectedAccountIdentity.detailLabel}
|
{selectedAccountIdentity.detailLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ export interface AccountIdentityPresentation {
|
|||||||
inlineLabel: string | null;
|
inlineLabel: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CodexIdentityBadge {
|
||||||
|
audience: AccountAudience;
|
||||||
|
label: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeVariantTokenPart(value: string): string {
|
function normalizeVariantTokenPart(value: string): string {
|
||||||
return value
|
return value
|
||||||
.trim()
|
.trim()
|
||||||
@@ -219,6 +224,27 @@ export function formatAccountVariantLabel(
|
|||||||
return getAccountIdentityPresentation(accountId, email, tokenFile).inlineLabel;
|
return getAccountIdentityPresentation(accountId, email, tokenFile).inlineLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCodexIdentityBadge(
|
||||||
|
presentation: Pick<AccountIdentityPresentation, 'audience' | 'detailLabel' | 'compactDetailLabel'>
|
||||||
|
): CodexIdentityBadge {
|
||||||
|
if (presentation.audience === 'business') {
|
||||||
|
return { audience: 'business', label: 'Business' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presentation.audience === 'free') {
|
||||||
|
return { audience: 'free', label: 'Free' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presentation.audience === 'personal') {
|
||||||
|
return {
|
||||||
|
audience: 'personal',
|
||||||
|
label: presentation.compactDetailLabel ?? presentation.detailLabel ?? 'Personal',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { audience: 'unknown', label: null };
|
||||||
|
}
|
||||||
|
|
||||||
export function formatAccountDisplayName(
|
export function formatAccountDisplayName(
|
||||||
accountId: string,
|
accountId: string,
|
||||||
email?: string,
|
email?: string,
|
||||||
|
|||||||
@@ -154,10 +154,10 @@ describe('AccountCard grouped quota tooltip', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByTitle('Business · Workspace 04a0f049 • Free')).toBeInTheDocument();
|
expect(screen.getByTitle('Business • Free')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Biz')).toBeInTheDocument();
|
expect(screen.getByText('Biz')).toBeInTheDocument();
|
||||||
|
|
||||||
await userEvent.hover(screen.getByText('Business · Workspace 04a0f049'));
|
await userEvent.hover(screen.getByText('Business'));
|
||||||
const businessPlan = (await screen.findAllByText('Plan: team')).find((node) =>
|
const businessPlan = (await screen.findAllByText('Plan: team')).find((node) =>
|
||||||
node.closest('[data-slot="tooltip-content"]')
|
node.closest('[data-slot="tooltip-content"]')
|
||||||
);
|
);
|
||||||
@@ -202,9 +202,8 @@ describe('AccountCard grouped quota tooltip', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByTitle('Business · Workspace 04a0f049 • Personal · Pro')).toBeInTheDocument();
|
expect(screen.getByTitle('Business • Pro')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Personal · Pro')).toBeInTheDocument();
|
expect(screen.getAllByText('Pro').length).toBeGreaterThan(0);
|
||||||
expect(screen.queryByText('Free')).not.toBeInTheDocument();
|
expect(screen.queryByText('Free')).not.toBeInTheDocument();
|
||||||
expect(screen.getByText('Pro')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -89,9 +89,9 @@ describe('AccountSurfaceCard', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText('Pers')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Pro')).toBeInTheDocument();
|
expect(screen.getByText('Pro')).toBeInTheDocument();
|
||||||
expect(screen.queryByText('Free')).not.toBeInTheDocument();
|
expect(screen.queryByText('Free')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Pers')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to a single free badge when Codex quota detects a free plan', () => {
|
it('falls back to a single free badge when Codex quota detects a free plan', () => {
|
||||||
@@ -124,8 +124,8 @@ describe('AccountSurfaceCard', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText('Pers')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Pro')).toBeInTheDocument();
|
expect(screen.getByText('Pro')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Pers')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('lets live paid Codex plans override stale free identity badges', () => {
|
it('lets live paid Codex plans override stale free identity badges', () => {
|
||||||
@@ -142,8 +142,8 @@ describe('AccountSurfaceCard', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText('Pers')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Plus')).toBeInTheDocument();
|
expect(screen.getByText('Plus')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Pers')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByTitle('Free')).not.toBeInTheDocument();
|
expect(screen.queryByTitle('Free')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user