2026-02-03 21:54:11 -05:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useState } from "react";
|
|
|
|
|
import Card from "@/shared/components/Card";
|
2026-03-22 22:16:10 -04:00
|
|
|
import ProviderIcon from "@/shared/components/ProviderIcon";
|
2026-02-03 21:54:11 -05:00
|
|
|
import Badge from "@/shared/components/Badge";
|
|
|
|
|
import QuotaProgressBar from "./QuotaProgressBar";
|
|
|
|
|
import { calculatePercentage } from "./utils";
|
|
|
|
|
|
|
|
|
|
const planVariants = {
|
|
|
|
|
free: "default",
|
|
|
|
|
pro: "primary",
|
|
|
|
|
ultra: "success",
|
|
|
|
|
enterprise: "info",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default function ProviderLimitCard({
|
|
|
|
|
provider,
|
|
|
|
|
name,
|
|
|
|
|
plan,
|
|
|
|
|
quotas = [],
|
|
|
|
|
message = null,
|
|
|
|
|
loading = false,
|
|
|
|
|
error = null,
|
|
|
|
|
onRefresh,
|
|
|
|
|
}) {
|
|
|
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
|
|
|
|
|
|
const handleRefresh = async () => {
|
|
|
|
|
if (!onRefresh || refreshing) return;
|
2026-03-22 22:16:10 -04:00
|
|
|
|
2026-02-03 21:54:11 -05:00
|
|
|
setRefreshing(true);
|
|
|
|
|
try {
|
|
|
|
|
await onRefresh();
|
|
|
|
|
} finally {
|
|
|
|
|
setRefreshing(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Get provider info from config
|
|
|
|
|
const getProviderColor = () => {
|
|
|
|
|
const colors = {
|
|
|
|
|
github: "#000000",
|
|
|
|
|
antigravity: "#4285F4",
|
|
|
|
|
codex: "#10A37F",
|
|
|
|
|
kiro: "#FF9900",
|
|
|
|
|
claude: "#D97757",
|
|
|
|
|
};
|
|
|
|
|
return colors[provider?.toLowerCase()] || "#6B7280";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const providerColor = getProviderColor();
|
|
|
|
|
const planVariant = planVariants[plan?.toLowerCase()] || "default";
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Card padding="md" className="flex flex-col gap-4">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
{/* Provider Logo */}
|
|
|
|
|
<div
|
|
|
|
|
className="size-10 rounded-lg flex items-center justify-center p-1.5"
|
|
|
|
|
style={{ backgroundColor: `${providerColor}15` }}
|
|
|
|
|
>
|
2026-03-22 22:16:10 -04:00
|
|
|
<ProviderIcon
|
|
|
|
|
src={`/providers/${provider}.png`}
|
|
|
|
|
alt={provider || "Provider"}
|
|
|
|
|
size={40}
|
|
|
|
|
className="object-contain rounded-lg"
|
|
|
|
|
fallbackText={provider?.slice(0, 2).toUpperCase() || "PR"}
|
|
|
|
|
fallbackColor={providerColor}
|
|
|
|
|
/>
|
2026-02-03 21:54:11 -05:00
|
|
|
</div>
|
2026-03-22 22:16:10 -04:00
|
|
|
|
2026-02-03 21:54:11 -05:00
|
|
|
<div>
|
2026-03-22 22:16:10 -04:00
|
|
|
<h3 className="font-semibold text-text-primary">
|
|
|
|
|
{name || provider}
|
|
|
|
|
</h3>
|
2026-02-03 21:54:11 -05:00
|
|
|
{plan && (
|
|
|
|
|
<Badge
|
|
|
|
|
variant={planVariants[plan?.toLowerCase()] || "default"}
|
|
|
|
|
size="xs"
|
|
|
|
|
>
|
|
|
|
|
{plan}
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Refresh Button */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleRefresh}
|
|
|
|
|
disabled={refreshing || loading}
|
|
|
|
|
className="p-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
|
|
title="Refresh quota"
|
|
|
|
|
>
|
|
|
|
|
<span
|
|
|
|
|
className={`material-symbols-outlined text-[20px] text-text-muted ${
|
|
|
|
|
refreshing || loading ? "animate-spin" : ""
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
refresh
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Loading State */}
|
|
|
|
|
{loading && (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div className="h-4 bg-black/5 dark:bg-white/5 rounded animate-pulse" />
|
|
|
|
|
<div className="h-2 bg-black/5 dark:bg-white/5 rounded animate-pulse" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div className="h-4 bg-black/5 dark:bg-white/5 rounded animate-pulse" />
|
|
|
|
|
<div className="h-2 bg-black/5 dark:bg-white/5 rounded animate-pulse" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Error State */}
|
|
|
|
|
{!loading && error && (
|
|
|
|
|
<div className="p-4 rounded-lg bg-red-500/10 border border-red-500/20">
|
|
|
|
|
<div className="flex items-start gap-2">
|
|
|
|
|
<span className="material-symbols-outlined text-red-500 text-[20px]">
|
|
|
|
|
error
|
|
|
|
|
</span>
|
|
|
|
|
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Info Message (for providers without API) */}
|
|
|
|
|
{!loading && !error && message && (
|
|
|
|
|
<div className="p-4 rounded-lg bg-blue-500/10 border border-blue-500/20">
|
|
|
|
|
<div className="flex items-start gap-2">
|
|
|
|
|
<span className="material-symbols-outlined text-blue-500 text-[20px]">
|
|
|
|
|
info
|
|
|
|
|
</span>
|
2026-03-22 22:16:10 -04:00
|
|
|
<p className="text-sm text-blue-600 dark:text-blue-400">
|
|
|
|
|
{message}
|
|
|
|
|
</p>
|
2026-02-03 21:54:11 -05:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Quota Progress Bars */}
|
|
|
|
|
{!loading && !error && !message && quotas?.length > 0 && (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{quotas.map((quota, index) => {
|
2026-02-05 06:38:50 -05:00
|
|
|
// For Antigravity, use remainingPercentage if available, otherwise calculate
|
2026-03-22 22:16:10 -04:00
|
|
|
const percentage =
|
|
|
|
|
quota.remainingPercentage !== undefined
|
|
|
|
|
? Math.round(((quota.total - quota.used) / quota.total) * 100)
|
|
|
|
|
: calculatePercentage(quota.used, quota.total);
|
2026-02-03 21:54:11 -05:00
|
|
|
const unlimited = quota.total === 0 || quota.total === null;
|
2026-03-22 22:16:10 -04:00
|
|
|
|
2026-02-03 21:54:11 -05:00
|
|
|
return (
|
|
|
|
|
<QuotaProgressBar
|
|
|
|
|
key={`${quota.name}-${index}`}
|
|
|
|
|
label={quota.name}
|
|
|
|
|
used={quota.used}
|
|
|
|
|
total={quota.total}
|
|
|
|
|
percentage={percentage}
|
|
|
|
|
unlimited={unlimited}
|
|
|
|
|
resetTime={quota.resetAt}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Empty State */}
|
|
|
|
|
{!loading && !error && !message && quotas?.length === 0 && (
|
|
|
|
|
<div className="text-center py-8 text-text-muted">
|
|
|
|
|
<span className="material-symbols-outlined text-[48px] opacity-20">
|
|
|
|
|
data_usage
|
|
|
|
|
</span>
|
|
|
|
|
<p className="text-sm mt-2">No quota data available</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
}
|