mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-20 06:23:10 +00:00
fix(ui): normalize ghcp quota tooltip and labels
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
cn,
|
cn,
|
||||||
|
formatQuotaPercent,
|
||||||
getCodexQuotaBreakdown,
|
getCodexQuotaBreakdown,
|
||||||
getProviderMinQuota,
|
getProviderMinQuota,
|
||||||
getProviderResetTime,
|
getProviderResetTime,
|
||||||
@@ -110,6 +111,7 @@ export function AccountCard({
|
|||||||
{ label: '5h', value: codexBreakdown?.fiveHourWindow?.remainingPercent ?? null },
|
{ label: '5h', value: codexBreakdown?.fiveHourWindow?.remainingPercent ?? null },
|
||||||
{ label: 'Wk', value: codexBreakdown?.weeklyWindow?.remainingPercent ?? null },
|
{ label: 'Wk', value: codexBreakdown?.weeklyWindow?.remainingPercent ?? null },
|
||||||
].filter((row): row is { label: string; value: number } => row.value !== null);
|
].filter((row): row is { label: string; value: number } => row.value !== null);
|
||||||
|
const minQuotaLabel = minQuota !== null ? formatQuotaPercent(minQuota) : null;
|
||||||
|
|
||||||
// Tier badge (AGY only) - show P for Pro, U for Ultra
|
// Tier badge (AGY only) - show P for Pro, U for Ultra
|
||||||
const showTierBadge =
|
const showTierBadge =
|
||||||
@@ -240,7 +242,7 @@ export function AccountCard({
|
|||||||
: 'text-red-500'
|
: 'text-red-500'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{minQuota}%
|
{minQuotaLabel}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{account.provider === 'codex' && codexQuotaRows.length > 0 && (
|
{account.provider === 'codex' && codexQuotaRows.length > 0 && (
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
cn,
|
cn,
|
||||||
|
formatQuotaPercent,
|
||||||
getCodexQuotaBreakdown,
|
getCodexQuotaBreakdown,
|
||||||
getProviderMinQuota,
|
getProviderMinQuota,
|
||||||
getProviderResetTime,
|
getProviderResetTime,
|
||||||
@@ -129,6 +130,7 @@ export function AccountItem({
|
|||||||
{ label: '5h', value: codexBreakdown?.fiveHourWindow?.remainingPercent ?? null },
|
{ label: '5h', value: codexBreakdown?.fiveHourWindow?.remainingPercent ?? null },
|
||||||
{ label: 'Weekly', value: codexBreakdown?.weeklyWindow?.remainingPercent ?? null },
|
{ label: 'Weekly', value: codexBreakdown?.weeklyWindow?.remainingPercent ?? null },
|
||||||
].filter((row): row is { label: string; value: number } => row.value !== null);
|
].filter((row): row is { label: string; value: number } => row.value !== null);
|
||||||
|
const minQuotaLabel = minQuota !== null ? formatQuotaPercent(minQuota) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -376,7 +378,9 @@ export function AccountItem({
|
|||||||
className="h-2 flex-1"
|
className="h-2 flex-1"
|
||||||
indicatorClassName={getQuotaColor(minQuota)}
|
indicatorClassName={getQuotaColor(minQuota)}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs font-medium w-10 text-right">{minQuota}%</span>
|
<span className="text-xs font-medium w-10 text-right">
|
||||||
|
{minQuotaLabel}%
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { Clock } from 'lucide-react';
|
import { Clock } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
cn,
|
cn,
|
||||||
|
formatQuotaPercent,
|
||||||
formatResetTime,
|
formatResetTime,
|
||||||
getCodexQuotaBreakdown,
|
getCodexQuotaBreakdown,
|
||||||
getCodexWindowDisplayLabel,
|
getCodexWindowDisplayLabel,
|
||||||
@@ -24,6 +25,16 @@ interface QuotaTooltipContentProps {
|
|||||||
resetTime: string | null;
|
resetTime: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatPlanLabel(planType: string | null | undefined): string | null {
|
||||||
|
if (!planType) return null;
|
||||||
|
const normalized = planType
|
||||||
|
.split(/[\s_-]+/g)
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.filter((part) => part.length > 0)
|
||||||
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1));
|
||||||
|
return normalized.length > 0 ? normalized.join(' ') : planType;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders provider-specific quota tooltip content
|
* Renders provider-specific quota tooltip content
|
||||||
* Uses type guards for proper TypeScript narrowing
|
* Uses type guards for proper TypeScript narrowing
|
||||||
@@ -131,24 +142,32 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
|
|||||||
{ label: 'Completions', snapshot: quota.snapshots.completions },
|
{ label: 'Completions', snapshot: quota.snapshots.completions },
|
||||||
];
|
];
|
||||||
const effectiveResetTime = quota.quotaResetDate ?? resetTime;
|
const effectiveResetTime = quota.quotaResetDate ?? resetTime;
|
||||||
|
const planLabel = formatPlanLabel(quota.planType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-xs space-y-1">
|
<div className="text-xs space-y-1">
|
||||||
<p className="font-medium">Quota Snapshots:</p>
|
<p className="font-medium">Quota Snapshots:</p>
|
||||||
{quota.planType && <p className="text-muted-foreground">Plan: {quota.planType}</p>}
|
{planLabel && <p className="text-muted-foreground">Plan: {planLabel}</p>}
|
||||||
{snapshotRows.map(({ label, snapshot }) => (
|
{snapshotRows.map(({ label, snapshot }) => {
|
||||||
<div key={label} className="flex justify-between gap-4">
|
const isLow = snapshot.percentRemaining < 20;
|
||||||
<span className={cn(snapshot.percentRemaining < 20 && 'text-red-500')}>
|
return (
|
||||||
{label}
|
<div key={label} className="space-y-0.5">
|
||||||
{snapshot.unlimited ? ' (Unlimited)' : ''}
|
<div className="flex justify-between gap-4">
|
||||||
</span>
|
<span className={cn(isLow && 'text-red-500')}>{label}</span>
|
||||||
<span className={cn('font-mono', snapshot.percentRemaining < 20 && 'text-red-500')}>
|
<span className={cn('font-mono', isLow && 'text-red-500')}>
|
||||||
{snapshot.unlimited
|
{snapshot.unlimited
|
||||||
? 'inf'
|
? 'Unlimited'
|
||||||
: `${snapshot.percentRemaining}% (${snapshot.remaining}/${snapshot.entitlement})`}
|
: `${formatQuotaPercent(snapshot.percentRemaining)}%`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
{!snapshot.unlimited && (
|
||||||
|
<div className="text-[11px] text-muted-foreground">
|
||||||
|
{snapshot.remaining}/{snapshot.entitlement} remaining
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<ResetTimeIndicator resetTime={effectiveResetTime} />
|
<ResetTimeIndicator resetTime={effectiveResetTime} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format quota percentage for UI display.
|
||||||
|
* Uses rounded whole numbers to keep quota labels compact and consistent.
|
||||||
|
*/
|
||||||
|
export function formatQuotaPercent(value: number): string {
|
||||||
|
if (!Number.isFinite(value)) return '0';
|
||||||
|
const clamped = Math.max(0, Math.min(100, value));
|
||||||
|
return `${Math.round(clamped)}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Vibrant Tones Palette
|
// Vibrant Tones Palette
|
||||||
const VIBRANT_TONES = [
|
const VIBRANT_TONES = [
|
||||||
'#f94144', // Strawberry Red
|
'#f94144', // Strawberry Red
|
||||||
|
|||||||
Reference in New Issue
Block a user