'use client'; import { useState, useEffect } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; import { cn } from '@/lib/utils'; import { Sparkles, ChevronDown, X, Crown } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import { useLanguage } from '@/lib/i18n'; interface QuotaData { remaining: number; limit: number; used: number; } interface UsageMeterProps { className?: string; } export function UsageMeter({ className }: UsageMeterProps) { const { t } = useLanguage(); const router = useRouter(); const queryClient = useQueryClient(); const [expanded, setExpanded] = useState(false); const [showModal, setShowModal] = useState(false); const { data, isLoading } = useQuery({ queryKey: ['usage', 'current'], queryFn: async () => { const res = await fetch('/api/usage/current'); if (!res.ok) throw new Error('Failed to fetch quotas'); const json = await res.json(); return { quotas: json.quotas as Record, tier: json.tier as string }; }, staleTime: 5000, refetchInterval: 10000, }); useEffect(() => { const handler = () => queryClient.invalidateQueries({ queryKey: ['usage', 'current'] }); window.addEventListener('ai-usage-changed', handler); return () => window.removeEventListener('ai-usage-changed', handler); }, [queryClient]); if (isLoading || !data || !data.quotas) { return (
); } const isProPlus = data.tier && data.tier !== 'BASIC'; const featureLabels: Record = { semantic_search: t('usageMeter.featureSearch'), auto_tag: t('usageMeter.featureTags'), auto_title: t('usageMeter.featureTitles'), reformulate: t('usageMeter.featureReformulate'), chat: t('usageMeter.featureChat'), brainstorm_create: t('usageMeter.featureBrainstormCreate'), brainstorm_expand: t('usageMeter.featureBrainstormExpand'), brainstorm_enrich: t('usageMeter.featureBrainstormEnrich'), }; const featureQuotas = Object.entries(data.quotas) .filter(([_, q]) => q.limit > 0) .map(([key, quota]) => ({ key, label: featureLabels[key] || key, used: quota.used, limit: quota.limit, remaining: quota.remaining, })); const isUnlimited = featureQuotas.every((f) => !Number.isFinite(f.limit)); const totalUsed = featureQuotas.reduce( (sum, f) => sum + (Number.isFinite(f.used) ? f.used : 0), 0, ); const totalLimit = featureQuotas.reduce( (sum, f) => sum + (Number.isFinite(f.limit) ? f.limit : 0), 0, ); const totalRemaining = totalLimit - totalUsed; const totalPct = totalLimit > 0 ? (totalUsed / totalLimit) * 100 : 0; const isExhausted = !isUnlimited && totalRemaining <= 0; return ( <>
{expanded && (
{featureQuotas.map((f) => { const fPct = Number.isFinite(f.limit) && f.limit > 0 ? (f.used / f.limit) * 100 : 0 return (
{f.label} = 90 ? 'text-rose-500' : fPct >= 70 ? 'text-amber-500' : 'text-ink/60' )}> {!Number.isFinite(f.remaining) ? '∞' : f.remaining}/{!Number.isFinite(f.limit) ? '∞' : f.limit}
{Number.isFinite(f.limit) && (
= 90 ? 'bg-rose-400' : fPct >= 70 ? 'bg-amber-400' : 'bg-brand-accent', )} style={{ width: `${Math.min(fPct, 100)}%` }} />
)}
) })} {!isProPlus && ( )}
)}
{showModal && (

{t('usageMeter.upgradeTitle')}

{t('usageMeter.upgradeDescription')}

{t('usageMeter.proIncludes')}
  • • {t('usageMeter.proSearch')}
  • • {t('usageMeter.proTags')}
  • • {t('usageMeter.proTitles')}
  • • {t('usageMeter.proReformulate')}
  • • {t('usageMeter.proChat')}
)} ); }