/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { Link } from '@tanstack/react-router' import { ArrowRight, Flame, ShieldCheck, TrendingDown, } from 'lucide-react' import { useTranslation } from 'react-i18next' import { useAuthStore } from '@/stores/auth-store' import { getCurrencyLabel, isCurrencyDisplayEnabled } from '@/lib/currency' import { formatNumber, formatQuota } from '@/lib/format' import { computeTimeRange } from '@/lib/time' import { cn } from '@/lib/utils' import { useStatus } from '@/hooks/use-status' import { Button } from '@/components/ui/button' import { StaggerContainer, StaggerItem } from '@/components/page-transition' import { getUserQuotaDates } from '@/features/dashboard/api' import { useSummaryCardsConfig } from '@/features/dashboard/hooks/use-dashboard-config' import type { QuotaDataItem } from '@/features/dashboard/types' import { StatCard } from '../ui/stat-card' const SUMMARY_SPARKLINE_BUCKETS = 12 type SummarySparklineKey = 'balance' | 'usage' | 'requests' function getBucketIndex( timestamp: number, start: number, end: number, bucketCount: number ): number { if (end <= start) return 0 const ratio = (timestamp - start) / (end - start) return Math.min(bucketCount - 1, Math.max(0, Math.floor(ratio * bucketCount))) } function buildSummarySparklines( data: QuotaDataItem[], currentBalance: number, start: number, end: number ): Record { const usage = Array.from({ length: SUMMARY_SPARKLINE_BUCKETS }, () => 0) const requests = Array.from({ length: SUMMARY_SPARKLINE_BUCKETS }, () => 0) for (const item of data) { const timestamp = Number(item.created_at) || start const index = getBucketIndex( timestamp, start, end, SUMMARY_SPARKLINE_BUCKETS ) usage[index] += Number(item.quota) || 0 requests[index] += Number(item.count) || 0 } let balance = currentBalance const balanceTrend = Array.from( { length: SUMMARY_SPARKLINE_BUCKETS }, () => 0 ) for (let index = SUMMARY_SPARKLINE_BUCKETS - 1; index >= 0; index--) { balanceTrend[index] = Math.max(0, balance) balance += usage[index] } return { balance: balanceTrend, usage, requests, } } function getSummarySparkline( key: string, sparklineData: Record ): number[] | undefined { if (key === 'usage') return sparklineData.usage if (key === 'requests') return sparklineData.requests return undefined } function getRunwayDays(remainQuota: number, recentUsage: number): number | null { if (remainQuota <= 0 || recentUsage <= 0) return null const days = remainQuota / recentUsage if (!Number.isFinite(days)) return null return days } type HealthLevel = 'healthy' | 'caution' | 'critical' function getHealthLevel( remainQuota: number, recentUsage: number ): HealthLevel { if (remainQuota <= 0) return 'critical' const days = getRunwayDays(remainQuota, recentUsage) if (days !== null && days < 3) return 'caution' return 'healthy' } const HEALTH_CONFIG: Record< HealthLevel, { dotClass: string; labelKey: string } > = { healthy: { dotClass: 'bg-success', labelKey: 'Healthy', }, caution: { dotClass: 'bg-warning', labelKey: 'Low balance', }, critical: { dotClass: 'bg-destructive', labelKey: 'Balance depleted', }, } export function SummaryCards() { const { t } = useTranslation() const user = useAuthStore((state) => state.auth.user) const { status, loading } = useStatus() const summaryTimeRange = useMemo(() => computeTimeRange(1), []) const remainQuota = Number(user?.quota ?? 0) const usedQuota = Number(user?.used_quota ?? 0) const requestCount = Number(user?.request_count ?? 0) const usageTrendQuery = useQuery({ queryKey: [ 'dashboard', 'overview', 'summary-sparklines', summaryTimeRange.start_timestamp, summaryTimeRange.end_timestamp, ], queryFn: async () => getUserQuotaDates({ start_timestamp: summaryTimeRange.start_timestamp, end_timestamp: summaryTimeRange.end_timestamp, default_time: 'hour', }), staleTime: 60 * 1000, }) const summaryValues = useMemo(() => { return { usedDisplay: formatQuota(usedQuota), requestCountDisplay: formatNumber(requestCount), } }, [requestCount, usedQuota]) const currencyEnabledFromStore = isCurrencyDisplayEnabled() const statusCurrencyFlag = typeof status?.display_in_currency === 'boolean' ? Boolean(status.display_in_currency) : undefined const currencyEnabled = statusCurrencyFlag !== undefined ? statusCurrencyFlag : currencyEnabledFromStore const currencyLabel = currencyEnabled ? getCurrencyLabel() : 'Tokens' const sparklineData = useMemo( () => buildSummarySparklines( usageTrendQuery.data?.data ?? [], remainQuota, summaryTimeRange.start_timestamp, summaryTimeRange.end_timestamp ), [ remainQuota, summaryTimeRange.end_timestamp, summaryTimeRange.start_timestamp, usageTrendQuery.data?.data, ] ) const recentUsage = useMemo( () => (usageTrendQuery.data?.data ?? []).reduce( (total, item) => total + (Number(item.quota) || 0), 0 ), [usageTrendQuery.data?.data] ) const healthLevel = getHealthLevel(remainQuota, recentUsage) const healthCfg = HEALTH_CONFIG[healthLevel] const runwayDays = getRunwayDays(remainQuota, recentUsage) const todayUsageDisplay = formatQuota(recentUsage) const items = useSummaryCardsConfig({ ...summaryValues, todayUsageDisplay, currencyEnabled, currencyLabel, }).map((config, index) => { const tones = ['rose', 'teal', 'gray'] as const return { key: config.key, title: config.title, value: config.value, desc: config.description, icon: config.icon, tone: tones[index] ?? 'gray', sparkline: config.key === 'todayUsage' ? sparklineData.usage : getSummarySparkline(config.key, sparklineData), sparklineVariant: 'line' as const, } }) return ( {t('Usage at a glance')} {t('Monitor balance, usage, and request volume')} {items.map((it) => ( ))} {t('Credit remaining')} {t(healthCfg.labelKey)} {formatQuota(remainQuota)} {t('Last 24h usage')} {formatQuota(recentUsage)} {runwayDays !== null && runwayDays < 3 ? ( ) : ( )} {t('Runway')} {runwayDays !== null ? runwayDays < 1 ? t('Less than 1 day left') : runwayDays > 999 ? `999+ ${t('days')}` : `~${formatNumber(Math.floor(runwayDays))} ${t('days')}` : remainQuota <= 0 ? t('Balance depleted') : t('No recent usage')} } > {t('Wallet')} ) }
{t('Monitor balance, usage, and request volume')}