import React, { useState, useEffect, useCallback } from 'react'; import { Box, Chip, Typography, Tooltip, CircularProgress, Alert, IconButton, Menu, MenuItem, LinearProgress, Select, FormControl, InputLabel, SelectChangeEvent } from '@mui/material'; import { TrendingUp, Warning, CheckCircle, Refresh, MoreVert, Dashboard, CalendarMonth } from '@mui/icons-material'; import { useUser } from '@clerk/clerk-react'; import { apiClient } from '../../api/client'; import { useSubscription } from '../../contexts/SubscriptionContext'; import { usePriority2Alerts } from '../../hooks/usePriority2Alerts'; import Priority2AlertBanner from './Priority2AlertBanner'; interface UsageStats { total_calls: number; total_cost: number; usage_status: string; provider_breakdown: Record; billing_period?: string; } interface UsageLimits { limits: { ai_text_generation_calls: number; gemini_calls: number; openai_calls: number; anthropic_calls: number; mistral_calls: number; tavily_calls: number; serper_calls: number; metaphor_calls: number; exa_calls: number; firecrawl_calls: number; stability_calls: number; video_calls: number; image_edit_calls: number; audio_calls: number; wavespeed_calls: number; monthly_cost: number; }; } interface DashboardData { current_usage: UsageStats; limits: UsageLimits; projections: { projected_monthly_cost: number; cost_limit: number; projected_usage_percentage: number; }; summary: { total_api_calls_this_month: number; total_cost_this_month: number; usage_status: string; unread_alerts: number; }; trends?: { periods: string[] }; } interface UsageDashboardProps { compact?: boolean; showFullDashboard?: boolean; } const UsageDashboard: React.FC = ({ compact = true, showFullDashboard = false }) => { const { subscription } = useSubscription(); const [dashboardData, setDashboardData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [anchorEl, setAnchorEl] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); const [selectedPeriod, setSelectedPeriod] = useState(''); const [availablePeriods, setAvailablePeriods] = useState([]); const { user } = useUser(); const userId = localStorage.getItem('user_id') || user?.id; // Priority 2 Alerts - automatically appears in all tool headers const { alerts: priority2Alerts, dismissAlert: dismissPriority2Alert } = usePriority2Alerts({ userId: userId || undefined, enabled: !!userId && subscription?.active, checkInterval: 120000, // Check every 2 minutes }); const fetchUsageData = useCallback(async (period?: string, silent = false) => { if (!userId) return; // Don't block UI for silent background refreshes (menu open, visibility change) if (!silent) { setLoading(true); } setError(null); try { const url = period ? `/api/subscription/dashboard/${userId}?billing_period=${period}` : `/api/subscription/dashboard/${userId}`; const response = await apiClient.get(url); if (response.data && response.data.success) { setDashboardData(response.data.data); setLastUpdated(new Date()); // Extract available periods from trends if not set if (!period && response.data.data.trends?.periods) { setAvailablePeriods(response.data.data.trends.periods); // Set current period if not selected if (!selectedPeriod) { const current = new Date().toISOString().slice(0, 7); // YYYY-MM setSelectedPeriod(current); } } } else { throw new Error(response.data?.error || 'Failed to fetch usage data'); } } catch (err: any) { if (!silent) { console.error('Error fetching usage data:', err); setError(err.message || 'Failed to load usage statistics'); } } finally { if (!silent) { setLoading(false); } } }, [userId]); const handlePeriodChange = (event: SelectChangeEvent) => { const period = event.target.value; setSelectedPeriod(period); fetchUsageData(period); }; useEffect(() => { // Initial fetch if (userId) { fetchUsageData(); } }, [userId, fetchUsageData]); // Refresh on visibility change (user returns to tab) - only if data is stale (>60s old) useEffect(() => { const STALE_THRESHOLD_MS = 60000; // 60 seconds const handleVisibilityChange = () => { if (document.visibilityState === 'visible' && userId && lastUpdated) { const ageMs = Date.now() - lastUpdated.getTime(); if (ageMs > STALE_THRESHOLD_MS) { fetchUsageData(selectedPeriod, true); } } }; document.addEventListener('visibilitychange', handleVisibilityChange); return () => document.removeEventListener('visibilitychange', handleVisibilityChange); }, [userId, fetchUsageData, selectedPeriod, lastUpdated]); const handleRefresh = () => { fetchUsageData(selectedPeriod); }; const handleMenuOpen = (event: React.MouseEvent) => { // Show cached data immediately, don't wait for fetch // Data will refresh when user clicks the manual refresh button setAnchorEl(event.currentTarget); }; const handleMenuClose = () => { setAnchorEl(null); }; const handleViewFullDashboard = () => { handleMenuClose(); window.location.href = '/dashboard'; }; const getUsageColor = (current: number, max: number) => { if (max === 0) return '#9ca3af'; const percentage = (current / max) * 100; if (percentage >= 100) return '#dc2626'; if (percentage >= 80) return '#ea580c'; return '#16a34a'; }; const getProviderDisplayName = (provider: string) => { // Map internal provider names to display names const displayNames: Record = { 'gemini': 'Google Gemini', 'openai': 'OpenAI GPT-4', 'anthropic': 'Anthropic Claude', 'mistral': 'HuggingFace (Mistral)', 'tavily': 'Tavily Search', 'serper': 'Serper Google', 'metaphor': 'Exa Search', // Metaphor is now Exa 'exa': 'Exa Search', 'firecrawl': 'Firecrawl', 'stability': 'Stability AI', 'video': 'Video Gen', 'audio': 'Audio Gen', 'image_edit': 'Image Edit', 'wavespeed': 'WaveSpeed' }; return displayNames[provider] || provider.charAt(0).toUpperCase() + provider.slice(1); }; if (!dashboardData && loading) { return ( ); } if (error && !dashboardData) { return ( {error} fetchUsageData(selectedPeriod)}> ); } if (!dashboardData) return null; const currentUsage = dashboardData.current_usage; const limits = dashboardData.limits; if (compact) { // Compact view - show key metrics as chips // Use current_usage for accurate cost (properly coerced from provider breakdown) // Fallback to summary if current_usage is not available const usageData = dashboardData?.current_usage || { total_calls: dashboardData?.summary?.total_api_calls_this_month || 0, total_cost: dashboardData?.summary?.total_cost_this_month || 0, usage_status: dashboardData?.summary?.usage_status || 'active', provider_breakdown: {} }; const totalCalls = usageData.total_calls; const totalCost = usageData.total_cost; const monthlyLimit = dashboardData?.limits?.limits?.monthly_cost || 0; const usagePercentage = monthlyLimit > 0 ? (totalCost / monthlyLimit) * 100 : 0; // Build per-category usage summaries from provider_breakdown and limits const providerBreakdown = usageData.provider_breakdown || {}; const providerLimits = dashboardData?.limits?.limits || {}; // Aggregate AI text calls (gemini + openai + anthropic + mistral) const aiCalls = (providerBreakdown.gemini?.calls || 0) + (providerBreakdown.openai?.calls || 0) + (providerBreakdown.anthropic?.calls || 0) + (providerBreakdown.mistral?.calls || 0) + (providerBreakdown.huggingface?.calls || 0) + (providerBreakdown.wavespeed?.calls || 0); const aiCallLimit = providerLimits.ai_text_generation_calls || providerLimits.gemini_calls || 0; // Image calls (stability + wavespeed image) const imageCalls = (providerBreakdown.stability?.calls || 0) + (providerBreakdown.image_edit?.calls || 0); const imageCallLimit = providerLimits.stability_calls || 0; // Audio calls const audioCalls = providerBreakdown.audio?.calls || 0; const audioCallLimit = providerLimits.audio_calls || 0; // Video calls const videoCalls = providerBreakdown.video?.calls || 0; const videoCallLimit = providerLimits.video_calls || 0; // Research calls (exa + tavily + serper + firecrawl) const researchCalls = (providerBreakdown.exa?.calls || 0) + (providerBreakdown.tavily?.calls || 0) + (providerBreakdown.serper?.calls || 0) + (providerBreakdown.firecrawl?.calls || 0); const researchCallLimit = (providerLimits.exa_calls || 0) + (providerLimits.tavily_calls || 0) + (providerLimits.serper_calls || 0) + (providerLimits.firecrawl_calls || 0); // WaveSpeed calls (all WaveSpeed API calls) const wavespeedCalls = providerBreakdown.wavespeed?.calls || 0; const wavespeedCallLimit = providerLimits.wavespeed_calls || 0; const formatLimit = (used: number, limit: number) => { if (limit === 0) return `${used} / ∞`; return `${used} / ${limit}`; }; return ( {/* Priority 2 Alert Banner (Usage limits) */} {priority2Alerts.length > 0 && ( dismissPriority2Alert(priority2Alerts[0].id)} /> )} {/* Month Selector for Compact View */} {availablePeriods.length > 1 && ( )} {/* Status Chip */} : } label={usageData.usage_status === 'limit_reached' ? 'Limit Reached' : 'Active'} size="small" color={usageData.usage_status === 'limit_reached' ? 'error' : usageData.usage_status === 'warning' ? 'warning' : 'success'} variant="outlined" sx={{ fontWeight: 600 }} /> {/* Monthly Cost */} } label={`$${totalCost.toFixed(2)}`} size="small" variant="outlined" sx={{ bgcolor: `${getUsageColor(totalCost, monthlyLimit)}10`, borderColor: `${getUsageColor(totalCost, monthlyLimit)}60`, color: getUsageColor(totalCost, monthlyLimit), fontWeight: 600, '& .MuiChip-icon': { color: getUsageColor(totalCost, monthlyLimit) } }} /> {/* Usage Progress */} {usagePercentage.toFixed(0)}% {/* Refresh Button */} {/* More Options */} {/* Per-Provider Usage Breakdown */} {aiCallLimit > 0 && ( AI Calls 0 ? Math.min((aiCalls / aiCallLimit) * 100, 100) : 0} sx={{ flex: 1, height: 4, borderRadius: 2, bgcolor: '#e5e7eb', '& .MuiLinearProgress-bar': { bgcolor: getUsageColor(aiCalls, aiCallLimit), borderRadius: 2 } }} /> {formatLimit(aiCalls, aiCallLimit)} )} {imageCallLimit > 0 && ( Images 0 ? Math.min((imageCalls / imageCallLimit) * 100, 100) : 0} sx={{ flex: 1, height: 4, borderRadius: 2, bgcolor: '#e5e7eb', '& .MuiLinearProgress-bar': { bgcolor: getUsageColor(imageCalls, imageCallLimit), borderRadius: 2 } }} /> {formatLimit(imageCalls, imageCallLimit)} )} {audioCallLimit > 0 && ( Audio 0 ? Math.min((audioCalls / audioCallLimit) * 100, 100) : 0} sx={{ flex: 1, height: 4, borderRadius: 2, bgcolor: '#e5e7eb', '& .MuiLinearProgress-bar': { bgcolor: getUsageColor(audioCalls, audioCallLimit), borderRadius: 2 } }} /> {formatLimit(audioCalls, audioCallLimit)} )} {videoCallLimit > 0 && ( Video 0 ? Math.min((videoCalls / videoCallLimit) * 100, 100) : 0} sx={{ flex: 1, height: 4, borderRadius: 2, bgcolor: '#e5e7eb', '& .MuiLinearProgress-bar': { bgcolor: getUsageColor(videoCalls, videoCallLimit), borderRadius: 2 } }} /> {formatLimit(videoCalls, videoCallLimit)} )} {researchCallLimit > 0 && ( Research 0 ? Math.min((researchCalls / researchCallLimit) * 100, 100) : 0} sx={{ flex: 1, height: 4, borderRadius: 2, bgcolor: '#e5e7eb', '& .MuiLinearProgress-bar': { bgcolor: getUsageColor(researchCalls, researchCallLimit), borderRadius: 2 } }} /> {formatLimit(researchCalls, researchCallLimit)} )} {wavespeedCallLimit > 0 && ( WaveSpeed 0 ? Math.min((wavespeedCalls / wavespeedCallLimit) * 100, 100) : 0} sx={{ flex: 1, height: 4, borderRadius: 2, bgcolor: '#e5e7eb', '& .MuiLinearProgress-bar': { bgcolor: getUsageColor(wavespeedCalls, wavespeedCallLimit), borderRadius: 2 } }} /> {formatLimit(wavespeedCalls, wavespeedCallLimit)} )} View Full Dashboard Refresh Data {lastUpdated && ( Last updated: {lastUpdated.toLocaleTimeString()} )} ); } // Full dashboard view (for dedicated usage page) const usageData = dashboardData?.current_usage || { total_calls: dashboardData?.summary?.total_api_calls_this_month || 0, total_cost: dashboardData?.summary?.total_cost_this_month || 0, provider_breakdown: {} }; return ( Usage Dashboard {/* Month Selector for Full View */} {availablePeriods.length > 1 && ( Billing Period )} {/* Total Calls */} Total API Calls {usageData.total_calls.toLocaleString()} {/* Total Cost */} Monthly Cost ${usageData.total_cost.toFixed(2)} of ${dashboardData?.limits?.limits?.monthly_cost || 0} limit {/* Usage by Provider */} Usage by Provider {Object.entries(usageData.provider_breakdown || {}).map(([provider, stats]) => ( {getProviderDisplayName(provider)} {(stats as any).calls?.toLocaleString() || 0} ))} {Object.keys(usageData.provider_breakdown || {}).length === 0 && ( No usage this period )} ); }; export default UsageDashboard;