Files
ALwrity/frontend/src/components/shared/UsageDashboard.tsx
ajaysi 928c2f20aa fix: WYSIWYG editor, content generation, and writing assistant bug fixes
- Fix text selection menu not showing: wire contentRef via inputRef on multiline TextField
- Fix blog title not truncating: add min-w-0 for flex item overflow
- Fix outline generation 500: escape curly braces in f-string prompt template
- Fix content generation 'NoneType not callable': replace SessionLocal() with get_session_for_user(), add db param to MediumBlogGenerator, fix signature mismatch in database_task_manager
- Fix writing assistant suggest 500: add auth + user_id to API endpoint and service, replace sync requests with httpx.AsyncClient
- Fix hallucination detector 404: explicitly include router in main.py and app.py
- Fix missing error_data in task failure responses
- Hide CopilotKit web inspector button
- Remove hardcoded fallback suggestions from SmartTypingAssist
- Fix stale closure refs in SmartTypingAssist handleTypingChange
- Add two-column editor layout, stats bar, section hover menu
- Various subscription, billing, and research module improvements
2026-05-14 09:11:51 +05:30

653 lines
26 KiB
TypeScript

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<string, {
calls: number;
tokens: number;
cost: number;
}>;
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 {
total_usage: UsageStats;
current_period_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<UsageDashboardProps> = ({
compact = true,
showFullDashboard = false
}) => {
const { subscription } = useSubscription();
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const [selectedPeriod, setSelectedPeriod] = useState<string>('');
const [availablePeriods, setAvailablePeriods] = useState<string[]>([]);
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<any>(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<HTMLElement>) => {
// 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<string, string> = {
'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 (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 2 }}>
<CircularProgress size={24} />
</Box>
);
}
if (error && !dashboardData) {
return (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
<IconButton size="small" onClick={() => fetchUsageData(selectedPeriod)}>
<Refresh fontSize="small" />
</IconButton>
</Alert>
);
}
if (!dashboardData) return null;
const totalUsage = dashboardData.total_usage;
const currentPeriodUsage = dashboardData.current_period_usage;
const limits = dashboardData.limits;
if (compact) {
// Compact view - show key metrics as chips
// Use total_usage for accurate cost (properly coerced from provider breakdown)
// Fallback to summary if total_usage is not available
const usageData = dashboardData?.total_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;
// Use current_period provider_breakdown for budget bars, total_usage for total display
const periodBreakdown = currentPeriodUsage?.provider_breakdown || {};
const totalBreakdown = usageData.provider_breakdown || {};
const providerLimits = dashboardData?.limits?.limits || {};
// Aggregate AI text calls (gemini + openai + anthropic + mistral) — from current period
const aiCalls = (periodBreakdown.gemini?.calls || 0) + (periodBreakdown.openai?.calls || 0) + (periodBreakdown.anthropic?.calls || 0) + (periodBreakdown.mistral?.calls || 0) + (periodBreakdown.huggingface?.calls || 0) + (periodBreakdown.wavespeed?.calls || 0);
const aiCallLimit = providerLimits.ai_text_generation_calls || providerLimits.gemini_calls || 0;
// Image calls (stability + wavespeed image) — from current period
const imageCalls = (periodBreakdown.stability?.calls || 0) + (periodBreakdown.image_edit?.calls || 0);
const imageCallLimit = providerLimits.stability_calls || 0;
const imageTotal = (totalBreakdown.stability?.calls || 0) + (totalBreakdown.image_edit?.calls || 0);
// Audio calls — from current period
const audioCalls = periodBreakdown.audio?.calls || 0;
const audioCallLimit = providerLimits.audio_calls || 0;
const audioTotal = totalBreakdown.audio?.calls || 0;
// Video calls — from current period
const videoCalls = periodBreakdown.video?.calls || 0;
const videoCallLimit = providerLimits.video_calls || 0;
const videoTotal = totalBreakdown.video?.calls || 0;
// Research calls (exa + tavily + serper + firecrawl) — from current period
const researchCalls = (periodBreakdown.exa?.calls || 0) + (periodBreakdown.tavily?.calls || 0) + (periodBreakdown.serper?.calls || 0) + (periodBreakdown.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) — from current period
const wavespeedCalls = periodBreakdown.wavespeed?.calls || 0;
const wavespeedCallLimit = providerLimits.wavespeed_calls || 0;
const wavespeedTotal = totalBreakdown.wavespeed?.calls || 0;
// All-time totals for rows without separate total variables
const aiTotal = (totalBreakdown.gemini?.calls || 0) + (totalBreakdown.openai?.calls || 0) + (totalBreakdown.anthropic?.calls || 0) + (totalBreakdown.mistral?.calls || 0) + (totalBreakdown.huggingface?.calls || 0) + (totalBreakdown.wavespeed?.calls || 0);
const researchTotal = (totalBreakdown.exa?.calls || 0) + (totalBreakdown.tavily?.calls || 0) + (totalBreakdown.serper?.calls || 0) + (totalBreakdown.firecrawl?.calls || 0);
const formatLimit = (used: number, limit: number, total?: number) => {
const periodStr = limit === 0 ? `${used} / ∞` : `${used} / ${limit}`;
if (total !== undefined && total !== used) {
return `${periodStr} • Total: ${total}`;
}
return periodStr;
};
return (
<Box sx={{ width: '100%' }}>
{/* Priority 2 Alert Banner (Usage limits) */}
{priority2Alerts.length > 0 && (
<Box sx={{ mb: 1 }}>
<Priority2AlertBanner
alerts={[priority2Alerts[0]]}
onDismiss={() => dismissPriority2Alert(priority2Alerts[0].id)}
/>
</Box>
)}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
{/* Month Selector for Compact View */}
{availablePeriods.length > 1 && (
<FormControl variant="standard" size="small" sx={{ minWidth: 100, mr: 1 }}>
<Select
value={selectedPeriod}
onChange={handlePeriodChange}
disableUnderline
sx={{
fontSize: '0.875rem',
fontWeight: 500,
color: '#374151',
'& .MuiSelect-select': { py: 0.5 }
}}
IconComponent={() => <CalendarMonth sx={{ fontSize: 16, color: '#6b7280', ml: 0.5 }} />}
>
{availablePeriods.map((period) => (
<MenuItem key={period} value={period} dense>
{period}
</MenuItem>
))}
</Select>
</FormControl>
)}
{/* Status Chip */}
<Tooltip title={`Status: ${usageData.usage_status}`}>
<Chip
icon={usageData.usage_status === 'active' ? <CheckCircle sx={{ fontSize: 14 }} /> : <Warning sx={{ fontSize: 14 }} />}
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 }}
/>
</Tooltip>
{/* Monthly Cost */}
<Tooltip title={`$${totalCost.toFixed(2)} of $${monthlyLimit} monthly limit`}>
<Chip
icon={<TrendingUp sx={{ fontSize: 14 }} />}
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)
}
}}
/>
</Tooltip>
{/* Usage Progress */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, minWidth: 60 }}>
<LinearProgress
variant="determinate"
value={Math.min(usagePercentage, 100)}
sx={{
width: 40,
height: 6,
borderRadius: 3,
bgcolor: '#e5e7eb',
'& .MuiLinearProgress-bar': {
bgcolor: getUsageColor(totalCost, monthlyLimit),
borderRadius: 3
}
}}
/>
<Typography variant="caption" sx={{ fontSize: '0.7rem', fontWeight: 600, color: '#374151' }}>
{usagePercentage.toFixed(0)}%
</Typography>
</Box>
{/* Refresh Button */}
<Tooltip title="Refresh usage data">
<IconButton
size="small"
onClick={handleRefresh}
disabled={loading}
sx={{
p: 0.5,
color: '#6b7280',
'&:hover': { bgcolor: '#f3f4f6' }
}}
>
<Refresh sx={{ fontSize: 16 }} />
</IconButton>
</Tooltip>
{/* More Options */}
<Tooltip title="Usage options">
<IconButton
size="small"
onClick={handleMenuOpen}
sx={{
p: 0.5,
color: '#6b7280',
'&:hover': { bgcolor: '#f3f4f6' }
}}
>
<MoreVert sx={{ fontSize: 16 }} />
</IconButton>
</Tooltip>
</Box>
{/* Per-Provider Usage Breakdown */}
<Box sx={{ mt: 1.5, display: 'flex', flexDirection: 'column', gap: 0.75 }}>
{aiCallLimit > 0 && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="caption" sx={{ fontSize: '0.7rem', fontWeight: 500, color: '#6b7280', minWidth: 60 }}>AI Calls</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1, ml: 1 }}>
<LinearProgress
variant="determinate"
value={aiCallLimit > 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 } }}
/>
<Typography variant="caption" sx={{ fontSize: '0.65rem', fontWeight: 600, color: getUsageColor(aiCalls, aiCallLimit), minWidth: 55, textAlign: 'right' }}>
{formatLimit(aiCalls, aiCallLimit, aiTotal)}
</Typography>
</Box>
</Box>
)}
{imageCallLimit > 0 && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="caption" sx={{ fontSize: '0.7rem', fontWeight: 500, color: '#6b7280', minWidth: 60 }}>Images</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1, ml: 1 }}>
<LinearProgress
variant="determinate"
value={imageCallLimit > 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 } }}
/>
<Typography variant="caption" sx={{ fontSize: '0.65rem', fontWeight: 600, color: getUsageColor(imageCalls, imageCallLimit), minWidth: 55, textAlign: 'right' }}>
{formatLimit(imageCalls, imageCallLimit, imageTotal)}
</Typography>
</Box>
</Box>
)}
{audioCallLimit > 0 && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="caption" sx={{ fontSize: '0.7rem', fontWeight: 500, color: '#6b7280', minWidth: 60 }}>Audio</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1, ml: 1 }}>
<LinearProgress
variant="determinate"
value={audioCallLimit > 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 } }}
/>
<Typography variant="caption" sx={{ fontSize: '0.65rem', fontWeight: 600, color: getUsageColor(audioCalls, audioCallLimit), minWidth: 55, textAlign: 'right' }}>
{formatLimit(audioCalls, audioCallLimit, audioTotal)}
</Typography>
</Box>
</Box>
)}
{videoCallLimit > 0 && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="caption" sx={{ fontSize: '0.7rem', fontWeight: 500, color: '#6b7280', minWidth: 60 }}>Video</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1, ml: 1 }}>
<LinearProgress
variant="determinate"
value={videoCallLimit > 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 } }}
/>
<Typography variant="caption" sx={{ fontSize: '0.65rem', fontWeight: 600, color: getUsageColor(videoCalls, videoCallLimit), minWidth: 55, textAlign: 'right' }}>
{formatLimit(videoCalls, videoCallLimit, videoTotal)}
</Typography>
</Box>
</Box>
)}
{researchCallLimit > 0 && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="caption" sx={{ fontSize: '0.7rem', fontWeight: 500, color: '#6b7280', minWidth: 60 }}>Research</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1, ml: 1 }}>
<LinearProgress
variant="determinate"
value={researchCallLimit > 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 } }}
/>
<Typography variant="caption" sx={{ fontSize: '0.65rem', fontWeight: 600, color: getUsageColor(researchCalls, researchCallLimit), minWidth: 55, textAlign: 'right' }}>
{formatLimit(researchCalls, researchCallLimit, researchTotal)}
</Typography>
</Box>
</Box>
)}
{wavespeedCallLimit > 0 && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="caption" sx={{ fontSize: '0.7rem', fontWeight: 500, color: '#6b7280', minWidth: 60 }}>WaveSpeed</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1, ml: 1 }}>
<LinearProgress
variant="determinate"
value={wavespeedCallLimit > 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 } }}
/>
<Typography variant="caption" sx={{ fontSize: '0.65rem', fontWeight: 600, color: getUsageColor(wavespeedCalls, wavespeedCallLimit), minWidth: 55, textAlign: 'right' }}>
{formatLimit(wavespeedCalls, wavespeedCallLimit, wavespeedTotal)}
</Typography>
</Box>
</Box>
)}
</Box>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
PaperProps={{
sx: {
bgcolor: '#ffffff',
border: '1px solid rgba(0,0,0,0.08)',
borderRadius: 2,
boxShadow: '0 4px 16px rgba(0,0,0,0.1)',
}
}}
>
<MenuItem onClick={handleViewFullDashboard} sx={{ color: '#374151', '&:hover': { bgcolor: '#f3f4f6' } }}>
<Dashboard sx={{ mr: 1, fontSize: 18 }} />
View Full Dashboard
</MenuItem>
<MenuItem onClick={handleRefresh} sx={{ color: '#374151', '&:hover': { bgcolor: '#f3f4f6' } }}>
<Refresh sx={{ mr: 1, fontSize: 18 }} />
Refresh Data
</MenuItem>
{lastUpdated && (
<Box sx={{ px: 2, py: 1 }}>
<Typography variant="caption" sx={{ color: '#9ca3af' }}>
Last updated: {lastUpdated.toLocaleTimeString()}
</Typography>
</Box>
)}
</Menu>
</Box>
);
}
// Full dashboard view (for dedicated usage page)
const usageData = dashboardData?.total_usage || {
total_calls: dashboardData?.summary?.total_api_calls_this_month || 0,
total_cost: dashboardData?.summary?.total_cost_this_month || 0,
provider_breakdown: {}
};
return (
<Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">
Usage Dashboard
</Typography>
{/* Month Selector for Full View */}
{availablePeriods.length > 1 && (
<FormControl variant="outlined" size="small" sx={{ minWidth: 150 }}>
<InputLabel>Billing Period</InputLabel>
<Select
value={selectedPeriod}
onChange={handlePeriodChange}
label="Billing Period"
startAdornment={<CalendarMonth sx={{ fontSize: 18, mr: 1, color: 'action.active' }} />}
>
{availablePeriods.map((period) => (
<MenuItem key={period} value={period}>
{period}
</MenuItem>
))}
</Select>
</FormControl>
)}
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: 2 }}>
{/* Total Calls */}
<Box sx={{ p: 2, border: '1px solid #e0e0e0', borderRadius: 2 }}>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
Total API Calls
</Typography>
<Typography variant="h4" color="primary">
{usageData.total_calls.toLocaleString()}
</Typography>
</Box>
{/* Total Cost */}
<Box sx={{ p: 2, border: '1px solid #e0e0e0', borderRadius: 2 }}>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
Monthly Cost
</Typography>
<Typography variant="h4" color="secondary">
${usageData.total_cost.toFixed(2)}
</Typography>
<Typography variant="caption" color="text.secondary">
of ${dashboardData?.limits?.limits?.monthly_cost || 0} limit
</Typography>
</Box>
{/* Usage by Provider */}
<Box sx={{ p: 2, border: '1px solid #e0e0e0', borderRadius: 2 }}>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
Usage by Provider
</Typography>
{Object.entries(usageData.provider_breakdown || {}).map(([provider, stats]) => (
<Box key={provider} sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2">
{getProviderDisplayName(provider)}
</Typography>
<Typography variant="body2" fontWeight={600}>
{(stats as any).calls?.toLocaleString() || 0}
</Typography>
</Box>
))}
{Object.keys(usageData.provider_breakdown || {}).length === 0 && (
<Typography variant="body2" color="text.secondary" fontStyle="italic">
No usage this period
</Typography>
)}
</Box>
</Box>
</Box>
);
};
export default UsageDashboard;