bookwiz.io / components / UsageChart.tsx
UsageChart.tsx
Raw
'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>
  )
}