'use client'
import { useState, useEffect } from 'react'
import { useAuth } from '@/components/AuthProvider'
import { ChartBarIcon, CpuChipIcon, ClockIcon, CurrencyDollarIcon, SparklesIcon, BoltIcon, ChevronLeftIcon, ChevronRightIcon, CalendarIcon } from '@heroicons/react/24/outline'
interface BillingUsageData {
period: {
start: string
end: string
}
summary: {
totalPromptTokens: number
totalCompletionTokens: number
totalTokens: number
totalRequests: number
totalCost: number
}
models: Array<{
modelName: string
modelTier: string
provider: string
promptTokens: number
completionTokens: number
totalTokens: number
requests: number
cost: number
}>
daily: Array<{
date: string
promptTokens: number
completionTokens: number
totalTokens: number
requests: number
cost: number
models: Array<{
modelName: string
modelTier: string
provider: string
totalTokens: number
requests: number
}>
}>
}
export default function UsageChart() {
const { user } = useAuth()
const [usageData, setUsageData] = useState<BillingUsageData | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [currentDate, setCurrentDate] = useState(new Date())
useEffect(() => {
if (!user?.id) {
setLoading(false)
return
}
fetchUsageData()
}, [user?.id, currentDate])
const fetchUsageData = async () => {
if (!user?.id) return
try {
setLoading(true)
setError(null)
const year = currentDate.getFullYear()
const month = currentDate.getMonth() + 1 // JavaScript months are 0-indexed
const response = await fetch(`/api/usage/billing?userId=${user.id}&year=${year}&month=${month}&t=${Date.now()}`)
if (!response.ok) {
throw new Error('Failed to fetch usage data')
}
const data = await response.json()
setUsageData(data)
} catch (error) {
console.error('Error fetching usage data:', error)
setError('Failed to load usage data')
} finally {
setLoading(false)
}
}
const goToPreviousMonth = () => {
setCurrentDate(prevDate => {
const newDate = new Date(prevDate)
newDate.setMonth(newDate.getMonth() - 1)
return newDate
})
}
const goToNextMonth = () => {
setCurrentDate(prevDate => {
const newDate = new Date(prevDate)
newDate.setMonth(newDate.getMonth() + 1)
return newDate
})
}
const goToCurrentMonth = () => {
setCurrentDate(new Date())
}
const isCurrentMonth = () => {
const now = new Date()
return currentDate.getFullYear() === now.getFullYear() &&
currentDate.getMonth() === now.getMonth()
}
const canGoToNextMonth = () => {
const now = new Date()
return currentDate < now
}
const formatMonthYear = (date: Date) => {
return date.toLocaleDateString('en-US', {
month: 'long',
year: 'numeric'
})
}
const formatNumber = (num: number) => {
if (num >= 1000000) {
return `${Math.round(num / 1000000)}M`
}
if (num >= 10000) {
return `${Math.round(num / 1000)}K`
}
if (num >= 1000) {
return `${(num / 1000).toFixed(1)}K`
}
return num.toLocaleString()
}
const formatCost = (cost: number) => {
if (cost === 0) return '$0.00'
return `$${cost.toFixed(4)}`
}
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
})
}
const getTierIcon = (tier: string) => {
return tier === 'smart' ? (
<SparklesIcon className="w-4 h-4 text-purple-400" />
) : (
<BoltIcon className="w-4 h-4 text-blue-400" />
)
}
const getTierColor = (tier: string) => {
return tier === 'smart'
? 'from-purple-500/20 to-pink-500/20 border-purple-500/30'
: 'from-blue-500/20 to-cyan-500/20 border-blue-500/30'
}
const getTierLabel = (tier: string) => {
return tier === 'smart' ? '🧠 Smart AI' : '⚡ Fast AI'
}
if (loading) {
return (
<div className="bg-gray-900/40 backdrop-blur-sm border border-gray-800/50 rounded-2xl overflow-hidden transition-all duration-300">
{/* Header - Always visible, even during loading */}
<div className="px-6 lg:px-8 py-6 border-b border-gray-800/50">
<div className="flex items-center justify-between">
<div className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<CalendarIcon className="w-5 h-5 text-gray-400" />
<span className="text-lg font-semibold text-white">{formatMonthYear(currentDate)}</span>
</div>
<div className="text-xs text-gray-400 ml-7">
<div className="h-4 bg-gray-700 rounded w-32 animate-pulse"></div>
</div>
</div>
<div className="flex items-center space-x-1">
{!isCurrentMonth() && (
<button
onClick={goToCurrentMonth}
className="px-3 py-1.5 text-xs font-medium text-gray-400 hover:text-white hover:bg-gray-700/50 rounded-lg transition-colors"
title="Go to current month"
>
Current
</button>
)}
<button
onClick={goToPreviousMonth}
className="p-1.5 rounded-lg text-gray-400 hover:text-white hover:bg-gray-700/50 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Previous month"
>
<ChevronLeftIcon className="w-4 h-4" />
</button>
<button
onClick={goToNextMonth}
disabled={!canGoToNextMonth()}
className="p-1.5 rounded-lg text-gray-400 hover:text-white hover:bg-gray-700/50 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Next month"
>
<ChevronRightIcon className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Loading skeleton for content */}
<div className="px-6 lg:px-8 py-6 border-b border-gray-800/50">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="text-center">
<div className="w-10 h-10 bg-gray-700 rounded-lg mb-2 mx-auto animate-pulse"></div>
<div className="h-6 bg-gray-700 rounded mb-1 w-16 mx-auto animate-pulse"></div>
<div className="h-3 bg-gray-700 rounded w-20 mx-auto animate-pulse"></div>
</div>
))}
</div>
</div>
{/* Chart skeleton */}
<div className="px-6 lg:px-8 py-6">
<div className="h-4 bg-gray-700 rounded w-32 mb-6 animate-pulse"></div>
<div className="bg-gray-800/30 rounded-lg p-4">
<div className="h-48 bg-gray-700/50 rounded animate-pulse"></div>
</div>
</div>
</div>
)
}
if (error || !usageData) {
return (
<div className="bg-gray-900/40 backdrop-blur-sm border border-gray-800/50 rounded-2xl p-6 lg:p-8">
<div className="text-center py-8">
<ChartBarIcon className="w-12 h-12 mx-auto mb-4 text-gray-600" />
<p className="text-gray-400">
{error || 'Unable to load usage data'}
</p>
</div>
</div>
)
}
const hasUsage = usageData.summary.totalRequests > 0
// Always show the main container with header, but conditionally show content
return (
<div className="bg-gray-900/40 backdrop-blur-sm border border-gray-800/50 rounded-2xl overflow-hidden transition-all duration-300">
{/* Header - Always visible */}
<div className="px-6 lg:px-8 py-6 border-b border-gray-800/50">
<div className="flex items-center justify-between">
<div className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<CalendarIcon className="w-5 h-5 text-gray-400" />
<span className="text-lg font-semibold text-white">{formatMonthYear(currentDate)}</span>
</div>
{usageData?.period && (
<span className="text-xs text-gray-400 ml-7">
{formatDate(usageData.period.start)} - {formatDate(usageData.period.end)}
</span>
)}
</div>
<div className="flex items-center space-x-1">
{!isCurrentMonth() && (
<button
onClick={goToCurrentMonth}
className="px-3 py-1.5 text-xs font-medium text-gray-400 hover:text-white hover:bg-gray-700/50 rounded-lg transition-colors"
title="Go to current month"
>
Current
</button>
)}
<button
onClick={goToPreviousMonth}
className="p-1.5 rounded-lg text-gray-400 hover:text-white hover:bg-gray-700/50 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Previous month"
>
<ChevronLeftIcon className="w-4 h-4" />
</button>
<button
onClick={goToNextMonth}
disabled={!canGoToNextMonth()}
className="p-1.5 rounded-lg text-gray-400 hover:text-white hover:bg-gray-700/50 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Next month"
>
<ChevronRightIcon className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Content - Conditionally rendered based on usage */}
{!hasUsage ? (
<div className="px-6 lg:px-8 py-6 border-b border-gray-800/50">
<div className="text-center">
<ChartBarIcon className="w-12 h-12 mx-auto mb-4 text-gray-600" />
<p className="text-gray-400">No AI usage in this billing period yet.</p>
<p className="text-sm text-gray-500 mt-2">
Usage will appear here after you interact with AI features.
</p>
</div>
</div>
) : (
<>
{/* Summary Stats */}
<div className="px-6 lg:px-8 py-6 border-b border-gray-800/50">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="text-center">
<div className="flex items-center justify-center w-10 h-10 bg-teal-500/20 rounded-lg mb-2 mx-auto">
<CpuChipIcon className="w-5 h-5 text-teal-400" />
</div>
<div className="text-lg font-semibold text-white">{formatNumber(usageData.summary.totalTokens)}</div>
<div className="text-xs text-gray-400">Total Tokens</div>
</div>
<div className="text-center">
<div className="flex items-center justify-center w-10 h-10 bg-blue-500/20 rounded-lg mb-2 mx-auto">
<ChartBarIcon className="w-5 h-5 text-blue-400" />
</div>
<div className="text-lg font-semibold text-white">{usageData.summary.totalRequests}</div>
<div className="text-xs text-gray-400">Requests</div>
</div>
<div className="text-center">
<div className="flex items-center justify-center w-10 h-10 bg-purple-500/20 rounded-lg mb-2 mx-auto">
<ClockIcon className="w-5 h-5 text-purple-400" />
</div>
<div className="text-lg font-semibold text-white">{formatNumber(usageData.summary.totalPromptTokens)}</div>
<div className="text-xs text-gray-400">Input Tokens</div>
</div>
<div className="text-center">
<div className="flex items-center justify-center w-10 h-10 bg-emerald-500/20 rounded-lg mb-2 mx-auto">
<CurrencyDollarIcon className="w-5 h-5 text-emerald-400" />
</div>
<div className="text-lg font-semibold text-white">{formatCost(usageData.summary.totalCost)}</div>
<div className="text-xs text-gray-400">Est. Cost</div>
</div>
</div>
</div>
{/* Daily Usage Chart */}
{usageData.daily.length > 0 && (
<div className="px-6 lg:px-8 py-6">
<h4 className="text-md font-semibold text-white mb-6">Daily Usage Chart</h4>
<div className="bg-gray-800/30 rounded-lg p-4">
{/* Chart */}
<div className="mb-6">
<div className="flex flex-col">
{/* Chart area with Y-axis */}
<div className="flex">
{/* Y-axis */}
<div className="flex flex-col-reverse justify-between h-48 w-20 pr-3 text-xs text-gray-500 text-right">
<span>0</span>
<span>{formatNumber(Math.round(Math.max(...usageData.daily.map(d => d.totalTokens)) * 0.25))}</span>
<span>{formatNumber(Math.round(Math.max(...usageData.daily.map(d => d.totalTokens)) * 0.5))}</span>
<span>{formatNumber(Math.round(Math.max(...usageData.daily.map(d => d.totalTokens)) * 0.75))}</span>
<span>{formatNumber(Math.max(...usageData.daily.map(d => d.totalTokens)))}</span>
</div>
{/* Chart area */}
<div className="flex-1 relative">
{/* Horizontal grid lines */}
<div className="absolute inset-0 flex flex-col justify-between h-48">
{[0, 1, 2, 3, 4].map((line) => (
<div key={line} className="border-t border-gray-700/30 w-full"></div>
))}
</div>
{/* Bars */}
<div className="flex items-end justify-between h-48 relative">
{usageData.daily.slice(-14).map((day, index) => {
const maxDailyTokens = Math.max(...usageData.daily.map(d => d.totalTokens))
const heightPixels = maxDailyTokens > 0 ? (day.totalTokens / maxDailyTokens) * 192 : 0 // 192px = h-48
// Calculate tier breakdown for this day
const smartTokens = day.models?.filter(m => m.modelTier === 'smart').reduce((sum, m) => sum + m.totalTokens, 0) || 0
const fastTokens = day.models?.filter(m => m.modelTier === 'fast').reduce((sum, m) => sum + m.totalTokens, 0) || 0
// Calculate proportional pixels with minimum visibility
let smartPixels = day.totalTokens > 0 ? (smartTokens / day.totalTokens) * heightPixels : 0
let fastPixels = day.totalTokens > 0 ? (fastTokens / day.totalTokens) * heightPixels : 0
// Ensure minimum visibility for small values (at least 3px if there's any usage)
if (smartTokens > 0 && smartPixels < 3) {
smartPixels = 3
}
if (fastTokens > 0 && fastPixels < 3) {
fastPixels = 3
}
// Adjust total height if we've added minimum heights
const totalCalculatedPixels = smartPixels + fastPixels
if (totalCalculatedPixels > heightPixels && heightPixels > 0) {
// Scale down proportionally but maintain minimums
const scale = (heightPixels - (smartTokens > 0 ? 3 : 0) - (fastTokens > 0 ? 3 : 0)) / (totalCalculatedPixels - (smartTokens > 0 ? 3 : 0) - (fastTokens > 0 ? 3 : 0))
if (scale > 0) {
smartPixels = smartTokens > 0 ? Math.max(3, smartPixels * scale) : 0
fastPixels = fastTokens > 0 ? Math.max(3, fastPixels * scale) : 0
}
}
return (
<div key={day.date} className="flex flex-col items-center flex-1 group">
{/* Bar */}
<div
className="w-full max-w-8 bg-gray-700/50 rounded-t relative"
style={{ height: `${Math.max(heightPixels, 4)}px` }}
>
{/* Smart AI portion (bottom layer) */}
{smartPixels > 0 && (
<div
className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-purple-500 to-pink-500 rounded-t"
style={{ height: `${smartPixels}px` }}
/>
)}
{/* Fast AI portion (top layer) */}
{fastPixels > 0 && (
<div
className="absolute left-0 right-0 bg-gradient-to-t from-blue-500 to-cyan-500 rounded-t"
style={{
height: `${fastPixels}px`,
bottom: `${smartPixels}px`
}}
/>
)}
{/* Tooltip on hover */}
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 bg-gray-900 border border-gray-700 text-white text-xs rounded-lg px-3 py-2 whitespace-nowrap z-10 pointer-events-none shadow-lg">
<div className="font-semibold">{formatDate(day.date)}</div>
<div className="text-teal-400">{formatNumber(day.totalTokens)} tokens</div>
<div className="text-gray-300">{day.requests} requests</div>
{smartTokens > 0 && <div className="text-purple-400">• Smart: {formatNumber(smartTokens)}</div>}
{fastTokens > 0 && <div className="text-blue-400">• Fast: {formatNumber(fastTokens)}</div>}
{/* Tooltip arrow */}
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
</div>
</div>
</div>
)
})}
</div>
</div>
</div>
{/* X-axis date labels */}
<div className="flex mt-2">
{/* Empty space for Y-axis alignment */}
<div className="w-20"></div>
{/* Date labels */}
<div className="flex-1 flex justify-between">
{usageData.daily.slice(-14).map((day, index) => (
<div key={day.date} className="flex-1 text-center">
<div className="text-xs text-gray-400 transform -rotate-45 origin-center whitespace-nowrap">
{formatDate(day.date).replace(' ', '\n').split('\n').map((part, i) => (
<div key={i}>{part}</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* Legend */}
<div className="flex items-center justify-center space-x-6 text-xs">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-gradient-to-r from-purple-500 to-pink-500 rounded"></div>
<span className="text-gray-300">🧠 Smart AI</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-gradient-to-r from-blue-500 to-cyan-500 rounded"></div>
<span className="text-gray-300">⚡ Fast AI</span>
</div>
</div>
</div>
</div>
)}
{/* Model Summary */}
{usageData.models.length > 0 && (
<div className="px-6 lg:px-8 py-6">
<h4 className="text-md font-semibold text-white mb-4">Model Summary</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{usageData.models.slice(0, 6).map((model, index) => (
<div
key={`${model.modelName}-${model.modelTier}`}
className={`bg-gradient-to-r ${getTierColor(model.modelTier)} border rounded-lg p-4`}
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center space-x-2">
{getTierIcon(model.modelTier)}
<div>
<div className="text-sm font-medium text-white">{model.modelName}</div>
<div className="text-xs text-gray-400">
{getTierLabel(model.modelTier)} • {model.provider}
</div>
</div>
</div>
<div className="text-right">
<div className="text-sm font-medium text-white">{formatNumber(model.totalTokens)}</div>
<div className="text-xs text-gray-400">tokens</div>
</div>
</div>
<div className="grid grid-cols-3 gap-2 text-xs">
<div>
<span className="text-gray-400">Input:</span>
<span className="text-gray-300 ml-1">{formatNumber(model.promptTokens)}</span>
</div>
<div>
<span className="text-gray-400">Output:</span>
<span className="text-gray-300 ml-1">{formatNumber(model.completionTokens)}</span>
</div>
<div>
<span className="text-gray-400">Requests:</span>
<span className="text-gray-300 ml-1">{model.requests}</span>
</div>
</div>
</div>
))}
</div>
</div>
)}
</>
)}
</div>
)
}