← vibesdk
/**
* Usage Limits Card Component
* Displays user's free tier usage and remaining limits
*/
import { cn } from '@cloudflare/kumo';
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription } from '@/components/ui/alert';
import {
Zap,
AlertTriangle,
CheckCircle2,
TrendingUp,
RefreshCw,
Calendar,
BarChart3,
} from 'lucide-react';
import { useLimitsContext } from '@/contexts/limits-context';
import CloudflareLogo from '@/assets/provider-logos/cloudflare.svg?react';
interface UsageLimitsCardProps {
className?: string;
onConnectToken?: () => void;
}
export function UsageLimitsCard({
className,
onConnectToken,
}: UsageLimitsCardProps) {
const { data, loading, error, refetch } = useLimitsContext();
if (loading) {
return (
<Card className={cn('dark:bg-bg-4/50', className)}>
<CardHeader className="border-b">
<div className="flex items-center gap-2">
<Zap className="h-5 w-5 text-kumo-brand-primary" />
<CardTitle>Usage Limits</CardTitle>
</div>
<CardDescription>Your free tier usage</CardDescription>
</CardHeader>
<CardContent className="space-y-4 pt-6">
<Skeleton className="h-20 w-full" />
<Skeleton className="h-20 w-full" />
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className={cn('dark:bg-bg-4/50', className)}>
<CardHeader className="border-b">
<div className="flex items-center gap-2">
<Zap className="h-5 w-5 text-kumo-brand-primary" />
<CardTitle>Usage Limits</CardTitle>
</div>
</CardHeader>
<CardContent className="pt-6">
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
<Button
onClick={refetch}
variant="outline"
size="sm"
className="mt-4 w-full"
>
<RefreshCw className="h-4 w-4 mr-2" />
Retry
</Button>
</CardContent>
</Card>
);
}
if (!data) return null;
const { config, usage, limitCheck, hasUserToken } = data;
const isUnlimited = config.unlimited;
const limit = config.limit;
// When unlimited, the backend omits `config.limit` (see worker controller).
// Bail out of finite-quota math so we don't read properties off undefined.
const current = limit ? usage[limit.type]?.[limit.window] || 0 : 0;
const max = limit?.maxValue ?? 0;
const percentUsed = max > 0 ? (current / max) * 100 : 0;
const remaining = Math.max(0, max - current);
// Determine status
const isExceeded = !limitCheck.withinLimits;
const isWarning = percentUsed >= 80 && percentUsed < 100;
// Format values based on type
const formatValue = (value: number, type: string) => {
switch (type) {
case 'prompts':
return value.toString();
case 'tokens':
return `${(value / 1000).toFixed(1)}K`;
case 'cost':
return `$${value.toFixed(2)}`;
default:
return value.toString();
}
};
const getTypeLabel = (type: string) => {
switch (type) {
case 'prompts':
return 'Prompts';
case 'tokens':
return 'Tokens';
case 'cost':
return 'Cost';
default:
return type;
}
};
const getWindowLabel = (window: string) => {
switch (window) {
case 'daily':
return 'Today';
case 'weekly':
return 'This Week';
case 'monthly':
return 'This Month';
case 'lifetime':
return 'All Time';
default:
return window;
}
};
return (
<Card className={cn('dark:bg-bg-4/50', className)}>
<CardHeader className="border-b">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Zap className="h-5 w-5 text-kumo-brand-primary" />
<CardTitle>Free Tier Usage</CardTitle>
</div>
<Button
onClick={refetch}
variant="ghost"
size="icon"
className="h-8 w-8"
>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
{limit && (
<CardDescription className="flex items-center gap-2 mt-2">
<Calendar className="h-3 w-3" />
{getWindowLabel(limit.window)}
</CardDescription>
)}
</CardHeader>
<CardContent className="pt-6">
{isUnlimited ? (
<Alert className="bg-gradient-to-r from-green-500/10 to-emerald-500/10 border-green-500/20">
<CheckCircle2 className="h-4 w-4 text-green-600" />
<AlertDescription className="text-green-700 dark:text-green-400">
You have unlimited access! 🎉
</AlertDescription>
</Alert>
) : (
<>
{/* Main Content: Usage Stats (Left) + Connect Button (Right) */}
<div className="flex flex-col lg:flex-row gap-6 items-start">
{/* Left: Usage Stats */}
<div className="flex-1 space-y-4 w-full">
{/* Usage Progress */}
<div className="space-y-3">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<BarChart3 className="h-4 w-4 text-kumo-subtle" />
<span className="font-medium">
{getTypeLabel(limit!.type)}
</span>
</div>
<div className="flex items-center gap-2">
<span
className={cn(
'font-semibold',
isExceeded &&
'text-red-600',
isWarning &&
'text-amber-600',
)}
>
{formatValue(
current,
limit!.type,
)}
</span>
<span className="text-kumo-subtle">
/
</span>
<span className="text-kumo-subtle">
{formatValue(max, limit!.type)}
</span>
</div>
</div>
<Progress
value={Math.min(percentUsed, 100)}
className={cn(
'h-2',
isExceeded && '[&>div]:bg-red-500',
isWarning && '[&>div]:bg-amber-500',
)}
/>
<div className="flex items-center justify-between text-xs text-kumo-subtle">
<span>
{percentUsed.toFixed(1)}% used
</span>
<span className="font-medium text-foreground">
{formatValue(
remaining,
limit!.type,
)}{' '}
remaining
</span>
</div>
</div>
{/* Status Alert */}
{isExceeded && (
<Alert
variant="destructive"
className="border-red-500/50"
>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<p className="font-medium mb-1">
Limit Exceeded
</p>
<p className="text-sm">
{hasUserToken
? 'Using your connected Cloudflare AI Gateway token.'
: 'Connect your Cloudflare AI Gateway token to continue.'}
</p>
</AlertDescription>
</Alert>
)}
{isWarning && !isExceeded && (
<Alert className="bg-amber-500/10 border-amber-500/20">
<TrendingUp className="h-4 w-4 text-amber-600" />
<AlertDescription className="text-amber-700 dark:text-amber-400">
<p className="font-medium mb-1">
Approaching Limit
</p>
<p className="text-sm">
You've used{' '}
{percentUsed.toFixed(0)}% of
your free tier.
</p>
</AlertDescription>
</Alert>
)}
</div>
{/* Right: Cloudflare Connect Button - ALWAYS VISIBLE */}
<div className="flex-shrink-0 w-full lg:w-auto">
<div className="space-y-3">
<Button
onClick={onConnectToken}
className="w-full lg:w-auto gap-2 bg-gradient-to-r from-brand-primary to-brand-light hover:from-brand-primary/90 hover:to-brand-light/90"
size="lg"
>
<CloudflareLogo className="h-5 w-5" />
{hasUserToken
? 'Manage Connection'
: 'Connect Cloudflare'}
</Button>
{hasUserToken ? (
<div className="flex items-center justify-center gap-2 text-xs text-green-600 dark:text-green-400">
<CheckCircle2 className="h-3 w-3" />
<span>Connected</span>
</div>
) : (
<p className="text-xs text-center text-kumo-subtle max-w-[200px]">
Continue using with your own token
</p>
)}
</div>
</div>
</div>
</>
)}
{/* Additional Usage Stats */}
<div className="pt-4 border-t space-y-2">
<p className="text-xs font-medium text-kumo-subtle uppercase tracking-wide">
All Usage
</p>
{Object.entries(usage).map(([type, windows]) => (
<div key={type} className="space-y-1">
<p className="text-xs font-medium text-text-secondary">
{getTypeLabel(type)}
</p>
{Object.entries(windows).map(([window, value]) => (
<div
key={`${type}-${window}`}
className="flex items-center justify-between text-sm pl-2"
>
<span className="text-kumo-subtle text-xs">
{getWindowLabel(window)}
</span>
<span className="font-medium">
{formatValue(value as number, type)}
</span>
</div>
))}
</div>
))}
</div>
</CardContent>
</Card>
);
}