ALwrity LinkedIn Writer: Billing Dashboard: Compact View, Billing Overview, System Health Indicator, Cost Breakdown, Usage Trends, Usage Alerts, Comprehensive API Breakdown

This commit is contained in:
ajaysi
2025-09-11 11:09:10 +05:30
parent b156298e82
commit 1b65a9487b
84 changed files with 10143 additions and 156 deletions

View File

@@ -0,0 +1,350 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Container,
Grid,
Card,
CardContent,
Typography,
Alert,
CircularProgress,
useTheme,
useMediaQuery,
} from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
import {
DollarSign,
TrendingUp,
AlertTriangle,
Activity,
Zap,
BarChart3,
PieChart,
Clock
} from 'lucide-react';
// Services
import { billingService } from '../../services/billingService';
import { monitoringService } from '../../services/monitoringService';
// Types
import { DashboardData, UsageStats } from '../../types/billing';
import { SystemHealth } from '../../types/monitoring';
// Components (we'll create these next)
import BillingOverview from './BillingOverview';
import CostBreakdown from './CostBreakdown';
import UsageTrends from './UsageTrends';
import SystemHealthIndicator from '../monitoring/SystemHealthIndicator';
import UsageAlerts from './UsageAlerts';
// Animation variants
const containerVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
staggerChildren: 0.1
}
}
};
const cardVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4 }
}
};
const BillingDashboard: React.FC = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// State management
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
const [systemHealth, setSystemHealth] = useState<SystemHealth | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
// Fetch dashboard data
const fetchDashboardData = async () => {
try {
setLoading(true);
setError(null);
console.log('🔍 [DASHBOARD DEBUG] Starting data fetch...');
// Fetch billing and monitoring data in parallel
const [billingData, healthData] = await Promise.all([
billingService.getDashboardData(),
monitoringService.getSystemHealth()
]);
console.log('🔍 [DASHBOARD DEBUG] Received billing data:', billingData);
console.log('🔍 [DASHBOARD DEBUG] Received health data:', healthData);
console.log('🔍 [DASHBOARD DEBUG] Billing data current_usage:', billingData?.current_usage);
console.log('🔍 [DASHBOARD DEBUG] Billing data summary:', billingData?.summary);
console.log('🔍 [DASHBOARD DEBUG] Billing data trends:', billingData?.trends);
setDashboardData(billingData);
setSystemHealth(healthData);
setLastUpdated(new Date());
console.log('✅ [DASHBOARD DEBUG] Data set successfully');
} catch (err) {
console.error('❌ [DASHBOARD DEBUG] Error fetching dashboard data:', err);
setError(err instanceof Error ? err.message : 'Failed to load dashboard data');
} finally {
setLoading(false);
}
};
// Initial data fetch
useEffect(() => {
fetchDashboardData();
}, []);
// Auto-refresh every 30 seconds
useEffect(() => {
const interval = setInterval(() => {
fetchDashboardData();
}, 30000);
return () => clearInterval(interval);
}, []);
// Loading state
if (loading && !dashboardData) {
return (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '400px',
flexDirection: 'column',
gap: 2
}}
>
<CircularProgress size={48} />
<Typography variant="body1" color="text.secondary">
Loading billing dashboard...
</Typography>
</Box>
);
}
// Error state
if (error && !dashboardData) {
return (
<Box sx={{ p: 3 }}>
<Alert
severity="error"
action={
<motion.button
onClick={fetchDashboardData}
style={{
background: 'none',
border: 'none',
color: 'inherit',
cursor: 'pointer',
textDecoration: 'underline'
}}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Retry
</motion.button>
}
>
{error}
</Alert>
</Box>
);
}
if (!dashboardData) {
return null;
}
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
>
<Container maxWidth="xl" sx={{ py: 4 }}>
{/* Section Header */}
<motion.div variants={cardVariants}>
<Box sx={{ mb: 4, textAlign: 'center' }}>
<Typography
variant="h4"
component="h2"
sx={{
fontWeight: 'bold',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
mb: 1
}}
>
💰 Billing & Usage Dashboard
</Typography>
<Typography variant="body1" color="text.secondary">
Monitor your API usage, costs, and system performance in real-time
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
Last updated: {lastUpdated.toLocaleTimeString()}
</Typography>
</Box>
</motion.div>
{/* Main Dashboard Grid */}
<Grid container spacing={3}>
{/* Top Row - Overview Cards */}
<Grid item xs={12} md={4}>
<motion.div variants={cardVariants}>
<BillingOverview
usageStats={dashboardData.current_usage}
onRefresh={fetchDashboardData}
/>
</motion.div>
</Grid>
<Grid item xs={12} md={4}>
<motion.div variants={cardVariants}>
<SystemHealthIndicator
systemHealth={systemHealth}
onRefresh={fetchDashboardData}
/>
</motion.div>
</Grid>
<Grid item xs={12} md={4}>
<motion.div variants={cardVariants}>
<UsageAlerts
alerts={dashboardData.alerts}
onMarkRead={billingService.markAlertRead}
/>
</motion.div>
</Grid>
{/* Middle Row - Cost Breakdown */}
<Grid item xs={12} lg={6}>
<motion.div variants={cardVariants}>
<CostBreakdown
providerBreakdown={dashboardData.current_usage.provider_breakdown}
totalCost={dashboardData.current_usage.total_cost}
/>
</motion.div>
</Grid>
{/* Middle Row - Usage Trends */}
<Grid item xs={12} lg={6}>
<motion.div variants={cardVariants}>
<UsageTrends
trends={dashboardData.trends}
projections={dashboardData.projections}
/>
</motion.div>
</Grid>
{/* Bottom Row - Detailed Metrics */}
<Grid item xs={12}>
<motion.div variants={cardVariants}>
<Card
sx={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3
}}
>
<CardContent>
<Typography variant="h6" sx={{ mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<BarChart3 size={20} />
Detailed Usage Metrics
</Typography>
<Grid container spacing={3}>
{/* Usage Summary */}
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h4" sx={{ color: 'primary.main', fontWeight: 'bold' }}>
{dashboardData.current_usage.total_calls.toLocaleString()}
</Typography>
<Typography variant="body2" color="text.secondary">
Total API Calls
</Typography>
<Typography variant="caption" color="text.secondary">
This month
</Typography>
</Box>
</Grid>
{/* Token Usage */}
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h4" sx={{ color: 'secondary.main', fontWeight: 'bold' }}>
{(dashboardData.current_usage.total_tokens / 1000).toFixed(1)}k
</Typography>
<Typography variant="body2" color="text.secondary">
Tokens Used
</Typography>
<Typography variant="caption" color="text.secondary">
This month
</Typography>
</Box>
</Grid>
{/* Average Response Time */}
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h4" sx={{ color: 'warning.main', fontWeight: 'bold' }}>
{dashboardData.current_usage.avg_response_time.toFixed(0)}ms
</Typography>
<Typography variant="body2" color="text.secondary">
Avg Response Time
</Typography>
<Typography variant="caption" color="text.secondary">
Last 24 hours
</Typography>
</Box>
</Grid>
{/* Error Rate */}
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center' }}>
<Typography
variant="h4"
sx={{
color: dashboardData.current_usage.error_rate > 5 ? 'error.main' : 'success.main',
fontWeight: 'bold'
}}
>
{dashboardData.current_usage.error_rate.toFixed(2)}%
</Typography>
<Typography variant="body2" color="text.secondary">
Error Rate
</Typography>
<Typography variant="caption" color="text.secondary">
Last 24 hours
</Typography>
</Box>
</Grid>
</Grid>
</CardContent>
</Card>
</motion.div>
</Grid>
</Grid>
</Container>
</motion.div>
);
};
export default BillingDashboard;

View File

@@ -0,0 +1,286 @@
import React from 'react';
import {
Card,
CardContent,
Typography,
Box,
LinearProgress,
Chip,
IconButton,
Tooltip,
} from '@mui/material';
import { motion } from 'framer-motion';
import {
DollarSign,
TrendingUp,
RefreshCw,
AlertTriangle,
CheckCircle,
XCircle,
Info
} from 'lucide-react';
// Types
import { UsageStats } from '../../types/billing';
// Utils
import {
formatCurrency,
formatNumber,
formatPercentage,
getUsageStatusColor,
getUsageStatusIcon,
calculateUsagePercentage
} from '../../services/billingService';
interface BillingOverviewProps {
usageStats: UsageStats;
onRefresh: () => void;
}
const BillingOverview: React.FC<BillingOverviewProps> = ({
usageStats,
onRefresh
}) => {
// Debug logs removed to reduce console noise
const costUsagePercentage = calculateUsagePercentage(
usageStats.total_cost,
usageStats.limits.limits.monthly_cost || 1
);
// Debug logs removed to reduce console noise
const getStatusChip = () => {
const status = usageStats.usage_status;
const color = getUsageStatusColor(status);
const icon = getUsageStatusIcon(status);
let chipColor: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' = 'default';
if (status === 'active') chipColor = 'success';
else if (status === 'warning') chipColor = 'warning';
else if (status === 'limit_reached') chipColor = 'error';
return (
<Chip
icon={<span>{icon}</span>}
label={status.charAt(0).toUpperCase() + status.slice(1).replace('_', ' ')}
color={chipColor}
size="small"
sx={{ fontWeight: 'bold' }}
/>
);
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
whileHover={{ scale: 1.02 }}
>
<Card
sx={{
height: '100%',
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3,
position: 'relative',
overflow: 'hidden'
}}
>
{/* Header */}
<CardContent sx={{ pb: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 'bold' }}>
<DollarSign size={20} />
Billing Overview
</Typography>
<Tooltip title="View your current billing status, usage metrics, and subscription plan details">
<Info size={16} color="rgba(255,255,255,0.7)" />
</Tooltip>
<Tooltip title="Refresh data">
<IconButton
size="small"
onClick={onRefresh}
sx={{
color: 'text.secondary',
'&:hover': { color: 'primary.main' }
}}
>
<RefreshCw size={16} />
</IconButton>
</Tooltip>
</Box>
{/* Status Chip */}
<Box sx={{ mb: 3 }}>
{getStatusChip()}
</Box>
</CardContent>
<CardContent sx={{ pt: 0 }}>
{/* Current Cost */}
<Box sx={{ mb: 3, textAlign: 'center' }}>
<motion.div
initial={{ scale: 0.8 }}
animate={{ scale: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<Typography
variant="h3"
sx={{
fontWeight: 'bold',
color: '#ffffff',
textShadow: '0 2px 4px rgba(0,0,0,0.3)',
mb: 1
}}
>
{formatCurrency(usageStats.total_cost)}
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)' }}>
Total Cost This Month
</Typography>
</motion.div>
</Box>
{/* Usage Metrics */}
<Box sx={{ mb: 3 }}>
<Tooltip title="Total number of API requests made this billing period">
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
API Calls
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{formatNumber(usageStats.total_calls)}
</Typography>
</Box>
</Tooltip>
<Tooltip title="Total tokens processed across all API providers (input + output tokens)">
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Tokens Used
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{formatNumber(usageStats.total_tokens)}
</Typography>
</Box>
</Tooltip>
<Tooltip title="Average response time for API requests in the last 24 hours">
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Avg Response Time
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{usageStats.avg_response_time.toFixed(0)}ms
</Typography>
</Box>
</Tooltip>
</Box>
{/* Cost Usage Progress */}
{usageStats.limits.limits.monthly_cost > 0 && (
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2" color="text.secondary">
Monthly Cost Limit
</Typography>
<Typography variant="body2" fontWeight="bold">
{formatPercentage(costUsagePercentage)}
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={Math.min(costUsagePercentage, 100)}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(255,255,255,0.1)',
'& .MuiLinearProgress-bar': {
backgroundColor: costUsagePercentage > 80 ? '#ef4444' :
costUsagePercentage > 60 ? '#f59e0b' : '#22c55e',
borderRadius: 4,
}
}}
/>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
{formatCurrency(usageStats.total_cost)} of {formatCurrency(usageStats.limits.limits.monthly_cost)} limit
</Typography>
</Box>
)}
{/* Plan Information */}
<Box
sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)'
}}
>
<Typography variant="body2" sx={{ mb: 1, color: 'rgba(255,255,255,0.8)' }}>
Current Plan
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold', mb: 1, color: '#ffffff' }}>
{usageStats.limits.plan_name}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
{usageStats.limits.tier.charAt(0).toUpperCase() + usageStats.limits.tier.slice(1)} Tier
</Typography>
</Box>
{/* Quick Stats */}
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'space-around' }}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: 'primary.main' }}>
{usageStats.usage_percentages.gemini_calls.toFixed(0)}%
</Typography>
<Typography variant="caption" color="text.secondary">
Gemini Usage
</Typography>
</Box>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: 'secondary.main' }}>
{usageStats.error_rate.toFixed(1)}%
</Typography>
<Typography variant="caption" color="text.secondary">
Error Rate
</Typography>
</Box>
</Box>
</CardContent>
{/* Decorative Elements */}
<Box
sx={{
position: 'absolute',
top: -50,
right: -50,
width: 100,
height: 100,
background: 'radial-gradient(circle, rgba(102, 126, 234, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
<Box
sx={{
position: 'absolute',
bottom: -30,
left: -30,
width: 60,
height: 60,
background: 'radial-gradient(circle, rgba(118, 75, 162, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
</Card>
</motion.div>
);
};
export default BillingOverview;

View File

@@ -0,0 +1,614 @@
import React, { useState, useEffect, useRef } from 'react';
import {
Card,
CardContent,
Typography,
Box,
Grid,
Chip,
Tooltip,
LinearProgress,
IconButton,
} from '@mui/material';
import { motion } from 'framer-motion';
import {
AlertTriangle,
CheckCircle,
RefreshCw
} from 'lucide-react';
// Types
import { DashboardData } from '../../types/billing';
import { SystemHealth } from '../../types/monitoring';
// Services
import { billingService } from '../../services/billingService';
import { monitoringService } from '../../services/monitoringService';
import { onApiEvent } from '../../utils/apiEvents';
// Components
import ComprehensiveAPIBreakdown from './ComprehensiveAPIBreakdown';
interface CompactBillingDashboardProps {
userId?: string;
}
const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userId }) => {
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
const [systemHealth, setSystemHealth] = useState<SystemHealth | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const [billingData, healthData] = await Promise.all([
billingService.getDashboardData(userId),
monitoringService.getSystemHealth()
]);
setDashboardData(billingData);
setSystemHealth(healthData);
setLastUpdated(new Date());
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch data');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [userId]);
// Event-driven refresh
useEffect(() => {
const lastRefreshRef = { current: 0 } as { current: number };
const MIN_REFRESH_INTERVAL_MS = 4000;
const unsubscribe = onApiEvent((detail) => {
// Only react to non-billing/monitoring events to avoid feedback loops
if (detail.source && detail.source !== 'other') return;
const now = Date.now();
if (now - lastRefreshRef.current < MIN_REFRESH_INTERVAL_MS) return;
lastRefreshRef.current = now;
fetchData();
});
return unsubscribe;
}, []);
const formatCurrency = (amount: number) => `$${amount.toFixed(4)}`;
const formatNumber = (num: number) => num.toLocaleString();
const formatPercentage = (num: number) => `${num.toFixed(1)}%`;
if (loading && !dashboardData) {
return (
<Card sx={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3
}}>
<CardContent sx={{ textAlign: 'center', py: 4 }}>
<Typography sx={{ color: 'rgba(255,255,255,0.8)' }}>Loading billing data...</Typography>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card sx={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3
}}>
<CardContent sx={{ textAlign: 'center', py: 4 }}>
<Typography sx={{ color: '#ff6b6b' }}>Error: {error}</Typography>
<IconButton onClick={fetchData} sx={{ mt: 1 }}>
<RefreshCw size={16} />
</IconButton>
</CardContent>
</Card>
);
}
if (!dashboardData) return null;
const { current_usage, trends, limits, alerts } = dashboardData;
const activeProviders = Object.entries(current_usage.provider_breakdown)
.filter(([_, data]) => data.cost > 0);
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<Card
sx={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.12) 0%, rgba(255,255,255,0.08) 100%)',
backdropFilter: 'blur(15px)',
border: '1px solid rgba(255,255,255,0.15)',
borderRadius: 4,
position: 'relative',
overflow: 'hidden',
boxShadow: '0 8px 32px rgba(0,0,0,0.2), 0 0 0 1px rgba(255,255,255,0.1)',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '1px',
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent)',
zIndex: 1
}
}}
>
{/* Header - Removed to save space */}
<CardContent sx={{ pt: 2 }}>
{/* Compact Overview */}
<Grid container spacing={2} sx={{ mb: 2 }}>
{/* Total Cost */}
<Grid item xs={6} sm={3}>
<Tooltip
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
Monthly API Usage Cost
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Total spending across all AI providers this month
</Typography>
<Typography variant="caption" sx={{ opacity: 0.7, mt: 1, display: 'block' }}>
Includes: Gemini, OpenAI, Anthropic, Mistral
</Typography>
</Box>
}
arrow
placement="top"
>
<Box sx={{
textAlign: 'center',
p: 2.5,
background: 'linear-gradient(135deg, rgba(74, 222, 128, 0.12) 0%, rgba(34, 197, 94, 0.08) 100%)',
borderRadius: 3,
border: '1px solid rgba(74, 222, 128, 0.25)',
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: '0 8px 25px rgba(74, 222, 128, 0.2)',
border: '1px solid rgba(74, 222, 128, 0.4)'
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: 'linear-gradient(90deg, #4ade80, #22c55e)',
zIndex: 1
}
}}>
<Typography variant="h5" sx={{
fontWeight: 800,
color: '#ffffff',
textShadow: '0 2px 8px rgba(0,0,0,0.4)',
mb: 0.5
}}>
{formatCurrency(current_usage.total_cost)}
</Typography>
<Typography variant="body2" sx={{
color: 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.75rem'
}}>
Total Cost
</Typography>
</Box>
</Tooltip>
</Grid>
{/* API Calls */}
<Grid item xs={6} sm={3}>
<Tooltip
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
API Request Volume
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Total number of AI API requests made this month
</Typography>
<Typography variant="caption" sx={{ opacity: 0.7, mt: 1, display: 'block' }}>
Each request generates content, analyzes data, or processes information
</Typography>
</Box>
}
arrow
placement="top"
>
<Box sx={{
textAlign: 'center',
p: 2.5,
background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.12) 0%, rgba(37, 99, 235, 0.08) 100%)',
borderRadius: 3,
border: '1px solid rgba(59, 130, 246, 0.25)',
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: '0 8px 25px rgba(59, 130, 246, 0.2)',
border: '1px solid rgba(59, 130, 246, 0.4)'
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: 'linear-gradient(90deg, #3b82f6, #2563eb)',
zIndex: 1
}
}}>
<Typography variant="h5" sx={{
fontWeight: 800,
color: '#ffffff',
textShadow: '0 2px 8px rgba(0,0,0,0.4)',
mb: 0.5
}}>
{formatNumber(current_usage.total_calls)}
</Typography>
<Typography variant="body2" sx={{
color: 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.75rem'
}}>
API Calls
</Typography>
</Box>
</Tooltip>
</Grid>
{/* Tokens */}
<Grid item xs={6} sm={3}>
<Tooltip
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
AI Processing Units
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Total tokens processed by AI models this month
</Typography>
<Typography variant="caption" sx={{ opacity: 0.7, mt: 1, display: 'block' }}>
Tokens represent words, characters, and data processed by AI
</Typography>
</Box>
}
arrow
placement="top"
>
<Box sx={{
textAlign: 'center',
p: 2.5,
background: 'linear-gradient(135deg, rgba(168, 85, 247, 0.12) 0%, rgba(147, 51, 234, 0.08) 100%)',
borderRadius: 3,
border: '1px solid rgba(168, 85, 247, 0.25)',
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: '0 8px 25px rgba(168, 85, 247, 0.2)',
border: '1px solid rgba(168, 85, 247, 0.4)'
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: 'linear-gradient(90deg, #a855f7, #9333ea)',
zIndex: 1
}
}}>
<Typography variant="h5" sx={{
fontWeight: 800,
color: '#ffffff',
textShadow: '0 2px 8px rgba(0,0,0,0.4)',
mb: 0.5
}}>
{(current_usage.total_tokens / 1000).toFixed(1)}k
</Typography>
<Typography variant="body2" sx={{
color: 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.75rem'
}}>
Tokens
</Typography>
</Box>
</Tooltip>
</Grid>
{/* System Health */}
<Grid item xs={6} sm={3}>
<Tooltip
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
System Performance Status
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Real-time monitoring of API services and system performance
</Typography>
<Typography variant="caption" sx={{ opacity: 0.7, mt: 1, display: 'block' }}>
{systemHealth?.status === 'healthy'
? 'All systems operational and responding normally'
: 'Some services may be experiencing issues'
}
</Typography>
</Box>
}
arrow
placement="top"
>
<Box sx={{
textAlign: 'center',
p: 2.5,
background: systemHealth?.status === 'healthy'
? 'linear-gradient(135deg, rgba(34, 197, 94, 0.12) 0%, rgba(22, 163, 74, 0.08) 100%)'
: 'linear-gradient(135deg, rgba(239, 68, 68, 0.12) 0%, rgba(220, 38, 38, 0.08) 100%)',
borderRadius: 3,
border: systemHealth?.status === 'healthy'
? '1px solid rgba(34, 197, 94, 0.25)'
: '1px solid rgba(239, 68, 68, 0.25)',
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: systemHealth?.status === 'healthy'
? '0 8px 25px rgba(34, 197, 94, 0.2)'
: '0 8px 25px rgba(239, 68, 68, 0.2)',
border: systemHealth?.status === 'healthy'
? '1px solid rgba(34, 197, 94, 0.4)'
: '1px solid rgba(239, 68, 68, 0.4)'
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: systemHealth?.status === 'healthy'
? 'linear-gradient(90deg, #22c55e, #16a34a)'
: 'linear-gradient(90deg, #ef4444, #dc2626)',
zIndex: 1
}
}}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5, mb: 0.5 }}>
<CheckCircle size={18} color={systemHealth?.status === 'healthy' ? '#4ade80' : '#ff6b6b'} />
<Typography variant="body1" sx={{
color: systemHealth?.status === 'healthy' ? '#4ade80' : '#ff6b6b',
fontWeight: 700,
textTransform: 'capitalize',
textShadow: '0 2px 4px rgba(0,0,0,0.3)'
}}>
{systemHealth?.status || 'Unknown'}
</Typography>
</Box>
<Typography variant="body2" sx={{
color: 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.75rem'
}}>
System Health
</Typography>
</Box>
</Tooltip>
</Grid>
</Grid>
{/* Usage Progress */}
{limits.limits.monthly_cost > 0 && (
<Box sx={{
mb: 3,
p: 2.5,
background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.04) 100%)',
borderRadius: 3,
border: '1px solid rgba(255,255,255,0.1)'
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Box>
<Typography variant="subtitle2" sx={{
color: '#ffffff',
fontWeight: 600,
mb: 0.5
}}>
Monthly Budget Usage
</Typography>
<Typography variant="caption" sx={{
color: 'rgba(255,255,255,0.7)',
display: 'block'
}}>
Track your AI spending against monthly limits
</Typography>
</Box>
<Box sx={{ textAlign: 'right' }}>
<Typography variant="h6" sx={{
color: '#ffffff',
fontWeight: 'bold',
textShadow: '0 2px 4px rgba(0,0,0,0.3)'
}}>
{formatCurrency(current_usage.total_cost)}
</Typography>
<Typography variant="caption" sx={{
color: 'rgba(255,255,255,0.7)',
display: 'block'
}}>
of {formatCurrency(limits.limits.monthly_cost)}
</Typography>
</Box>
</Box>
<LinearProgress
variant="determinate"
value={(current_usage.total_cost / limits.limits.monthly_cost) * 100}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(255,255,255,0.1)',
'& .MuiLinearProgress-bar': {
background: current_usage.total_cost / limits.limits.monthly_cost > 0.8
? 'linear-gradient(90deg, #ff6b6b, #ff5252)'
: current_usage.total_cost / limits.limits.monthly_cost > 0.6
? 'linear-gradient(90deg, #ffa726, #ff9800)'
: 'linear-gradient(90deg, #4ade80, #22c55e)',
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0,0,0,0.2)'
}
}}
/>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 1 }}>
<Typography variant="caption" sx={{
color: current_usage.total_cost / limits.limits.monthly_cost > 0.8 ? '#ff6b6b' : 'rgba(255,255,255,0.7)',
fontWeight: current_usage.total_cost / limits.limits.monthly_cost > 0.8 ? 600 : 400
}}>
{current_usage.total_cost / limits.limits.monthly_cost > 0.8
? '⚠️ Approaching limit'
: current_usage.total_cost / limits.limits.monthly_cost > 0.6
? '⚡ Moderate usage'
: '✅ Within budget'
}
</Typography>
<Typography variant="caption" sx={{
color: 'rgba(255,255,255,0.7)',
fontWeight: 500
}}>
{((current_usage.total_cost / limits.limits.monthly_cost) * 100).toFixed(1)}% used
</Typography>
</Box>
</Box>
)}
{/* Alerts */}
{alerts.length > 0 && (
<Box sx={{
mb: 3,
p: 2.5,
background: 'linear-gradient(135deg, rgba(255, 107, 107, 0.1) 0%, rgba(239, 68, 68, 0.05) 100%)',
borderRadius: 3,
border: '1px solid rgba(255, 107, 107, 0.2)',
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: 'linear-gradient(90deg, #ff6b6b, #ef4444)',
borderRadius: '3px 3px 0 0'
}
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<AlertTriangle size={18} color="#ff6b6b" />
<Typography variant="subtitle2" sx={{
fontWeight: 700,
color: '#ff6b6b',
textShadow: '0 1px 2px rgba(0,0,0,0.3)'
}}>
System Alerts ({alerts.length})
</Typography>
</Box>
<Typography variant="caption" sx={{
color: 'rgba(255,255,255,0.8)',
display: 'block',
mb: 2
}}>
Important notifications requiring your attention
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{alerts.slice(0, 3).map((alert) => (
<Tooltip
key={alert.id}
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
{alert.title}
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
{alert.message}
</Typography>
</Box>
}
arrow
placement="top"
>
<Chip
label={alert.title}
size="small"
icon={<AlertTriangle size={14} />}
sx={{
backgroundColor: 'rgba(255, 107, 107, 0.2)',
color: '#ff6b6b',
border: '1px solid rgba(255, 107, 107, 0.3)',
fontWeight: 500,
'&:hover': {
backgroundColor: 'rgba(255, 107, 107, 0.3)',
transform: 'translateY(-1px)'
},
transition: 'all 0.2s ease'
}}
/>
</Tooltip>
))}
{alerts.length > 3 && (
<Chip
label={`+${alerts.length - 3} more`}
size="small"
sx={{
backgroundColor: 'rgba(255,255,255,0.1)',
color: 'rgba(255,255,255,0.8)',
border: '1px solid rgba(255,255,255,0.2)',
fontWeight: 500
}}
/>
)}
</Box>
</Box>
)}
</CardContent>
</Card>
</motion.div>
);
};
export default CompactBillingDashboard;

View File

@@ -0,0 +1,414 @@
import React from 'react';
import {
Card,
CardContent,
Typography,
Box,
Grid,
Chip,
Tooltip,
Accordion,
AccordionSummary,
AccordionDetails,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
} from '@mui/material';
import { ExpandMore } from '@mui/icons-material';
import { motion } from 'framer-motion';
import {
Info,
DollarSign,
Activity,
Zap,
Search,
Image,
Code,
Database,
Globe,
FileText,
BarChart3
} from 'lucide-react';
// Types
import { ProviderBreakdown } from '../../types/billing';
interface ComprehensiveAPIBreakdownProps {
providerBreakdown: ProviderBreakdown;
totalCost: number;
}
// Comprehensive API categories and their descriptions
const API_CATEGORIES = {
llm_models: {
title: 'Large Language Models',
description: 'AI models for text generation, analysis, and processing',
icon: <Code size={20} />,
apis: [
{
name: 'Gemini',
description: 'Google\'s advanced AI model for complex reasoning and coding',
models: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-1.5-pro', 'gemini-1.5-flash'],
pricing: 'From $0.10/1M tokens (Flash-Lite) to $15.00/1M tokens (Pro)',
use_cases: ['Content generation', 'Code analysis', 'Complex reasoning']
},
{
name: 'OpenAI',
description: 'GPT models for natural language processing and generation',
models: ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo'],
pricing: 'From $0.15/1M tokens (GPT-4o Mini) to $10.00/1M tokens (GPT-4o)',
use_cases: ['Chat completion', 'Text analysis', 'Creative writing']
},
{
name: 'Anthropic',
description: 'Claude models for safe and helpful AI assistance',
models: ['claude-3.5-sonnet', 'claude-3-haiku', 'claude-3-opus'],
pricing: 'From $3.00/1M tokens (Sonnet) to $15.00/1M tokens (Opus)',
use_cases: ['Safe AI assistance', 'Long-form content', 'Analysis tasks']
},
{
name: 'Mistral',
description: 'European AI models for efficient text processing',
models: ['mistral-large', 'mistral-medium', 'mistral-small'],
pricing: 'From $2.00/1M tokens (Small) to $8.00/1M tokens (Large)',
use_cases: ['Multilingual support', 'Efficient processing', 'European compliance']
}
]
},
search_apis: {
title: 'Search & Research APIs',
description: 'APIs for web search, content discovery, and research',
icon: <Search size={20} />,
apis: [
{
name: 'Tavily',
description: 'AI-powered search for real-time information',
models: ['tavily-search'],
pricing: '$0.001 per search request',
use_cases: ['Real-time search', 'Fact checking', 'Research assistance']
},
{
name: 'Serper',
description: 'Google Search API for web results',
models: ['serper-search'],
pricing: '$0.001 per search request',
use_cases: ['Web search', 'SEO analysis', 'Content research']
},
{
name: 'Metaphor',
description: 'Advanced search and content discovery',
models: ['metaphor-search'],
pricing: '$0.003 per search request',
use_cases: ['Content discovery', 'Link analysis', 'Research automation']
}
]
},
content_processing: {
title: 'Content Processing APIs',
description: 'APIs for web scraping, content extraction, and processing',
icon: <FileText size={20} />,
apis: [
{
name: 'Firecrawl',
description: 'Web scraping and content extraction service',
models: ['firecrawl-extract', 'firecrawl-scrape'],
pricing: '$0.002 per page crawled',
use_cases: ['Web scraping', 'Content extraction', 'Data collection']
}
]
},
image_generation: {
title: 'Image Generation APIs',
description: 'APIs for creating and processing images',
icon: <Image size={20} />,
apis: [
{
name: 'Stability AI',
description: 'AI-powered image generation and editing',
models: ['stable-diffusion-xl', 'stable-diffusion-3'],
pricing: '$0.04 per image generated',
use_cases: ['Image generation', 'Art creation', 'Visual content']
}
]
},
embeddings: {
title: 'Embeddings & Vector APIs',
description: 'APIs for text embeddings and vector operations',
icon: <Database size={20} />,
apis: [
{
name: 'Gemini Embeddings',
description: 'Text embeddings for semantic search and analysis',
models: ['gemini-embedding'],
pricing: '$0.15 per 1M input tokens',
use_cases: ['Semantic search', 'Text similarity', 'Vector databases']
}
]
}
};
const ComprehensiveAPIBreakdown: React.FC<ComprehensiveAPIBreakdownProps> = ({
providerBreakdown,
totalCost
}) => {
// Get active providers from breakdown
const activeProviders = Object.entries(providerBreakdown)
.filter(([_, data]) => data.cost > 0)
.map(([provider, data]) => ({ provider, ...data }));
const getProviderCategory = (providerName: string) => {
const provider = providerName.toLowerCase();
if (['gemini', 'openai', 'anthropic', 'mistral'].includes(provider)) {
return 'llm_models';
}
if (['tavily', 'serper', 'metaphor'].includes(provider)) {
return 'search_apis';
}
if (['firecrawl'].includes(provider)) {
return 'content_processing';
}
if (['stability'].includes(provider)) {
return 'image_generation';
}
return 'llm_models'; // default
};
const getCategoryStats = (categoryKey: string) => {
const categoryProviders = activeProviders.filter(p =>
getProviderCategory(p.provider) === categoryKey
);
return {
count: categoryProviders.length,
totalCost: categoryProviders.reduce((sum, p) => sum + p.cost, 0),
totalCalls: categoryProviders.reduce((sum, p) => sum + p.calls, 0),
totalTokens: categoryProviders.reduce((sum, p) => sum + p.tokens, 0)
};
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<Card
sx={{
height: '100%',
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3,
position: 'relative',
overflow: 'hidden'
}}
>
{/* Header */}
<CardContent sx={{ pb: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 'bold', color: '#ffffff' }}>
<BarChart3 size={20} />
Comprehensive API Breakdown
</Typography>
<Tooltip title="Detailed breakdown of all API usage across categories">
<Info size={16} color="rgba(255,255,255,0.7)" />
</Tooltip>
</Box>
</CardContent>
<CardContent sx={{ pt: 0 }}>
{/* Summary Stats */}
<Box sx={{ mb: 3, p: 2, backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 2 }}>
<Grid container spacing={2}>
<Grid item xs={6} sm={3}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{Object.keys(API_CATEGORIES).length}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
API Categories
</Typography>
</Box>
</Grid>
<Grid item xs={6} sm={3}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{activeProviders.length}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Active Providers
</Typography>
</Box>
</Grid>
<Grid item xs={6} sm={3}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
${totalCost.toFixed(4)}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Total Cost
</Typography>
</Box>
</Grid>
<Grid item xs={6} sm={3}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{activeProviders.reduce((sum, p) => sum + p.calls, 0)}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Total Calls
</Typography>
</Box>
</Grid>
</Grid>
</Box>
{/* API Categories */}
{Object.entries(API_CATEGORIES).map(([categoryKey, category]) => {
const stats = getCategoryStats(categoryKey);
const hasUsage = stats.count > 0;
return (
<Accordion
key={categoryKey}
sx={{
mb: 1,
backgroundColor: 'rgba(255,255,255,0.05)',
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' }
}}
>
<AccordionSummary
expandIcon={<ExpandMore sx={{ color: 'rgba(255,255,255,0.7)' }} />}
sx={{
minHeight: 48,
'&.Mui-expanded': { minHeight: 48 }
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
<Box sx={{ color: hasUsage ? '#4ade80' : 'rgba(255,255,255,0.5)' }}>
{category.icon}
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{category.title}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
{category.description}
</Typography>
</Box>
{hasUsage && (
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
<Chip
label={`${stats.count} active`}
size="small"
sx={{
backgroundColor: 'rgba(74, 222, 128, 0.2)',
color: '#4ade80',
border: '1px solid rgba(74, 222, 128, 0.3)'
}}
/>
<Typography variant="caption" sx={{ color: '#4ade80', fontWeight: 'bold' }}>
${stats.totalCost.toFixed(4)}
</Typography>
</Box>
)}
</Box>
</AccordionSummary>
<AccordionDetails sx={{ pt: 0 }}>
<Grid container spacing={2}>
{category.apis.map((api) => {
const providerData = activeProviders.find(p =>
p.provider.toLowerCase() === api.name.toLowerCase()
);
return (
<Grid item xs={12} md={6} key={api.name}>
<Box
sx={{
p: 2,
backgroundColor: providerData ? 'rgba(74, 222, 128, 0.1)' : 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: providerData ? '1px solid rgba(74, 222, 128, 0.2)' : '1px solid rgba(255,255,255,0.1)'
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{api.name}
</Typography>
{providerData && (
<Chip
label="Active"
size="small"
sx={{
backgroundColor: 'rgba(74, 222, 128, 0.2)',
color: '#4ade80'
}}
/>
)}
</Box>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)', display: 'block', mb: 1 }}>
{api.description}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)', display: 'block', mb: 1 }}>
Pricing: {api.pricing}
</Typography>
{providerData && (
<Box sx={{ mt: 2, p: 1, backgroundColor: 'rgba(0,0,0,0.2)', borderRadius: 1 }}>
<Grid container spacing={1}>
<Grid item xs={4}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Cost
</Typography>
<Typography variant="caption" sx={{ display: 'block', color: '#4ade80', fontWeight: 'bold' }}>
${providerData.cost.toFixed(4)}
</Typography>
</Grid>
<Grid item xs={4}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Calls
</Typography>
<Typography variant="caption" sx={{ display: 'block', color: '#ffffff', fontWeight: 'bold' }}>
{providerData.calls}
</Typography>
</Grid>
<Grid item xs={4}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Tokens
</Typography>
<Typography variant="caption" sx={{ display: 'block', color: '#ffffff', fontWeight: 'bold' }}>
{providerData.tokens.toLocaleString()}
</Typography>
</Grid>
</Grid>
</Box>
)}
<Box sx={{ mt: 1 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
Use cases: {api.use_cases.join(', ')}
</Typography>
</Box>
</Box>
</Grid>
);
})}
</Grid>
</AccordionDetails>
</Accordion>
);
})}
</CardContent>
</Card>
</motion.div>
);
};
export default ComprehensiveAPIBreakdown;

View File

@@ -0,0 +1,292 @@
import React from 'react';
import {
Card,
CardContent,
Typography,
Box,
Grid,
Chip,
} from '@mui/material';
import { motion } from 'framer-motion';
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts';
import {
DollarSign,
TrendingUp,
BarChart3,
PieChart as PieChartIcon
} from 'lucide-react';
// Types
import { ProviderBreakdown } from '../../types/billing';
// Utils
import {
formatCurrency,
formatNumber,
getProviderIcon,
getProviderColor
} from '../../services/billingService';
interface CostBreakdownProps {
providerBreakdown: ProviderBreakdown;
totalCost: number;
}
const CostBreakdown: React.FC<CostBreakdownProps> = ({
providerBreakdown,
totalCost
}) => {
// Transform data for pie chart
const chartData = Object.entries(providerBreakdown)
.filter(([_, data]) => data.cost > 0)
.map(([provider, data]) => ({
name: provider.charAt(0).toUpperCase() + provider.slice(1),
value: data.cost,
calls: data.calls,
tokens: data.tokens,
color: getProviderColor(provider),
icon: getProviderIcon(provider)
}))
.sort((a, b) => b.value - a.value);
// Custom tooltip for pie chart
const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<Box
sx={{
backgroundColor: 'rgba(0, 0, 0, 0.8)',
color: 'white',
padding: 2,
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)'
}}
>
<Typography variant="body2" sx={{ fontWeight: 'bold', mb: 1 }}>
{data.icon} {data.name}
</Typography>
<Typography variant="body2">
Cost: {formatCurrency(data.value)}
</Typography>
<Typography variant="body2">
Calls: {formatNumber(data.calls)}
</Typography>
<Typography variant="body2">
Tokens: {formatNumber(data.tokens)}
</Typography>
</Box>
);
}
return null;
};
// Custom label for pie chart
const renderLabel = (entry: any) => {
const percent = ((entry.value / totalCost) * 100).toFixed(1);
return `${entry.name}: ${percent}%`;
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
whileHover={{ scale: 1.02 }}
>
<Card
sx={{
height: '100%',
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3,
position: 'relative',
overflow: 'hidden'
}}
>
{/* Header */}
<CardContent sx={{ pb: 1 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 'bold', mb: 2 }}>
<PieChartIcon size={20} />
Cost Breakdown by Provider
</Typography>
</CardContent>
<CardContent sx={{ pt: 0 }}>
{/* Pie Chart */}
<Box sx={{ height: 300, mb: 3 }}>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
labelLine={false}
label={renderLabel}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
</PieChart>
</ResponsiveContainer>
</Box>
{/* Provider Details */}
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold' }}>
Provider Details
</Typography>
<Grid container spacing={2}>
{chartData.map((provider, index) => {
const percentage = ((provider.value / totalCost) * 100).toFixed(1);
return (
<Grid item xs={12} sm={6} key={provider.name}>
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
>
<Box
sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)',
position: 'relative'
}}
>
{/* Provider Header */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<span style={{ fontSize: '18px' }}>{provider.icon}</span>
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>
{provider.name}
</Typography>
</Box>
<Chip
label={`${percentage}%`}
size="small"
sx={{
backgroundColor: `${provider.color}20`,
color: provider.color,
fontWeight: 'bold'
}}
/>
</Box>
{/* Metrics */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Cost:
</Typography>
<Typography variant="caption" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{formatCurrency(provider.value)}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Calls:
</Typography>
<Typography variant="caption" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{formatNumber(provider.calls)}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Tokens:
</Typography>
<Typography variant="caption" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{formatNumber(provider.tokens)}
</Typography>
</Box>
{/* Progress bar */}
<Box sx={{ mt: 1 }}>
<Box
sx={{
height: 4,
backgroundColor: 'rgba(255,255,255,0.1)',
borderRadius: 2,
overflow: 'hidden'
}}
>
<motion.div
initial={{ width: 0 }}
animate={{ width: `${percentage}%` }}
transition={{ duration: 1, delay: index * 0.1 }}
style={{
height: '100%',
backgroundColor: provider.color,
borderRadius: 2
}}
/>
</Box>
</Box>
</Box>
</motion.div>
</Grid>
);
})}
</Grid>
</Box>
{/* Summary Stats */}
<Box
sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)'
}}
>
<Typography variant="body2" sx={{ mb: 1, color: 'rgba(255,255,255,0.8)' }}>
Total Monthly Cost
</Typography>
<Typography variant="h5" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{formatCurrency(totalCost)}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Across {chartData.length} active providers
</Typography>
</Box>
</CardContent>
{/* Decorative Elements */}
<Box
sx={{
position: 'absolute',
top: -50,
right: -50,
width: 100,
height: 100,
background: 'radial-gradient(circle, rgba(102, 126, 234, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
<Box
sx={{
position: 'absolute',
bottom: -30,
left: -30,
width: 60,
height: 60,
background: 'radial-gradient(circle, rgba(118, 75, 162, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
</Card>
</motion.div>
);
};
export default CostBreakdown;

View File

@@ -0,0 +1,392 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Container,
Grid,
Card,
CardContent,
Typography,
Alert,
CircularProgress,
useTheme,
useMediaQuery,
ToggleButton,
ToggleButtonGroup,
Tooltip,
Chip,
IconButton,
} from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
import {
DollarSign,
TrendingUp,
AlertTriangle,
Activity,
Zap,
BarChart3,
PieChart,
Clock,
Grid3X3,
List,
Info,
RefreshCw
} from 'lucide-react';
// Services
import { billingService } from '../../services/billingService';
import { monitoringService } from '../../services/monitoringService';
import { onApiEvent } from '../../utils/apiEvents';
// Types
import { DashboardData } from '../../types/billing';
import { SystemHealth } from '../../types/monitoring';
// Components
import CompactBillingDashboard from './CompactBillingDashboard';
import BillingOverview from './BillingOverview';
import SystemHealthIndicator from '../monitoring/SystemHealthIndicator';
import CostBreakdown from './CostBreakdown';
import UsageTrends from './UsageTrends';
import UsageAlerts from './UsageAlerts';
import ComprehensiveAPIBreakdown from './ComprehensiveAPIBreakdown';
interface EnhancedBillingDashboardProps {
userId?: string;
}
type ViewMode = 'compact' | 'detailed';
const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ userId }) => {
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
const [systemHealth, setSystemHealth] = useState<SystemHealth | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const [viewMode, setViewMode] = useState<ViewMode>('compact');
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const fetchDashboardData = async () => {
try {
const [billingData, healthData] = await Promise.all([
billingService.getDashboardData(),
monitoringService.getSystemHealth()
]);
setDashboardData(billingData);
setSystemHealth(healthData);
setLastUpdated(new Date());
} catch (error) {
setError(error instanceof Error ? error.message : 'Failed to fetch dashboard data');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchDashboardData();
}, [userId]);
// Event-driven refresh: refresh only when non-billing/monitoring APIs complete
useEffect(() => {
const unsubscribe = onApiEvent((detail) => {
if (detail.source && detail.source !== 'other') return;
Promise.all([billingService.getDashboardData(), monitoringService.getSystemHealth()])
.then(([billingData, health]) => {
setDashboardData(billingData);
setSystemHealth(health);
setLastUpdated(new Date());
})
.catch(() => {/* ignore */});
});
return unsubscribe;
}, []);
// Refetch when tab becomes visible again (cheap, avoids polling)
useEffect(() => {
const onVisible = () => {
if (document.visibilityState === 'visible') {
fetchDashboardData();
}
};
document.addEventListener('visibilitychange', onVisible);
return () => document.removeEventListener('visibilitychange', onVisible);
}, []);
const handleViewModeChange = (
event: React.MouseEvent<HTMLElement>,
newViewMode: ViewMode | null,
) => {
if (newViewMode !== null) {
setViewMode(newViewMode);
}
};
if (loading) {
return (
<Container maxWidth="xl" sx={{ py: 4 }}>
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 400 }}>
<CircularProgress sx={{ color: 'primary.main' }} />
</Box>
</Container>
);
}
if (error) {
return (
<Container maxWidth="xl" sx={{ py: 4 }}>
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
</Container>
);
}
if (!dashboardData) {
return (
<Container maxWidth="xl" sx={{ py: 4 }}>
<Alert severity="warning">
No billing data available. Please check your subscription status.
</Alert>
</Container>
);
}
return (
<Container maxWidth="xl" sx={{ py: 4 }}>
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<Box sx={{ mb: 4 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flex: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography
variant="h4"
sx={{
fontWeight: 800,
mb: 1.5,
fontSize: '1.1rem',
color: 'rgba(255,255,255,0.95)',
}}
>
Billing & Usage Dashboard
</Typography>
<Tooltip
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
AI Usage Monitoring
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Track your AI API costs, usage patterns, and system performance in real-time
</Typography>
</Box>
}
arrow
placement="top"
>
<Info size={16} color="rgba(255,255,255,0.6)" style={{ cursor: 'help' }} />
</Tooltip>
</Box>
{/* Active Providers Chips */}
{dashboardData && (
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{Object.entries(dashboardData.current_usage.provider_breakdown)
.filter(([_, data]) => data.cost > 0)
.map(([provider, data]) => (
<Tooltip
key={provider}
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
{provider.toUpperCase()} Usage
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Cost: ${data.cost.toFixed(4)}
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Calls: {data.calls.toLocaleString()}
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Tokens: {data.tokens.toLocaleString()}
</Typography>
</Box>
}
arrow
placement="top"
>
<Chip
label={`${provider}: $${data.cost.toFixed(4)}`}
size="small"
sx={{
backgroundColor: 'rgba(74, 222, 128, 0.2)',
color: '#4ade80',
border: '1px solid rgba(74, 222, 128, 0.3)',
fontSize: '0.7rem',
height: 24,
fontWeight: 500,
'&:hover': {
backgroundColor: 'rgba(74, 222, 128, 0.3)',
transform: 'translateY(-1px)',
boxShadow: '0 4px 12px rgba(74, 222, 128, 0.2)'
},
transition: 'all 0.2s ease'
}}
/>
</Tooltip>
))}
</Box>
)}
</Box>
{/* View Mode Toggle and Refresh */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Tooltip title="Refresh billing data">
<IconButton
size="small"
onClick={fetchDashboardData}
disabled={loading}
sx={{
color: 'rgba(255,255,255,0.7)',
'&:hover': {
color: '#ffffff',
backgroundColor: 'rgba(255,255,255,0.1)'
}
}}
>
<RefreshCw size={16} className={loading ? 'animate-spin' : ''} />
</IconButton>
</Tooltip>
<Tooltip
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
View Modes
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
<strong>Compact:</strong> Essential metrics only
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
<strong>Detailed:</strong> Full breakdown with charts
</Typography>
</Box>
}
arrow
placement="top"
>
<Info size={16} color="rgba(255,255,255,0.7)" style={{ cursor: 'help' }} />
</Tooltip>
<ToggleButtonGroup
value={viewMode}
exclusive
onChange={handleViewModeChange}
size="small"
sx={{
backgroundColor: 'rgba(255,255,255,0.1)',
border: '1px solid rgba(255,255,255,0.2)',
'& .MuiToggleButton-root': {
color: 'rgba(255,255,255,0.7)',
border: 'none',
'&.Mui-selected': {
backgroundColor: 'rgba(255,255,255,0.2)',
color: '#ffffff'
}
}
}}
>
<ToggleButton value="compact">
<Grid3X3 size={16} style={{ marginRight: 8 }} />
Compact
</ToggleButton>
<ToggleButton value="detailed">
<List size={16} style={{ marginRight: 8 }} />
Detailed
</ToggleButton>
</ToggleButtonGroup>
</Box>
</Box>
</Box>
</motion.div>
{/* Dashboard Content */}
<AnimatePresence mode="wait">
{viewMode === 'compact' ? (
<motion.div
key="compact"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.3 }}
>
<CompactBillingDashboard userId={userId} />
</motion.div>
) : (
<motion.div
key="detailed"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
<Grid container spacing={3}>
{/* Top Row */}
<Grid item xs={12} md={4}>
<BillingOverview
usageStats={dashboardData.current_usage}
onRefresh={fetchDashboardData}
/>
</Grid>
<Grid item xs={12} md={4}>
<SystemHealthIndicator
systemHealth={systemHealth}
onRefresh={fetchDashboardData}
/>
</Grid>
<Grid item xs={12} md={4}>
<UsageAlerts
alerts={dashboardData.alerts}
onMarkRead={async (alertId) => {
// TODO: Implement mark as read functionality
console.log('Mark alert as read:', alertId);
}}
/>
</Grid>
{/* Middle Row */}
<Grid item xs={12} md={6}>
<CostBreakdown
providerBreakdown={dashboardData.current_usage.provider_breakdown}
totalCost={dashboardData.current_usage.total_cost}
/>
</Grid>
<Grid item xs={12} md={6}>
<UsageTrends
trends={dashboardData.trends}
projections={dashboardData.projections}
/>
</Grid>
{/* Bottom Row - Comprehensive API Breakdown */}
<Grid item xs={12}>
<ComprehensiveAPIBreakdown
providerBreakdown={dashboardData.current_usage.provider_breakdown}
totalCost={dashboardData.current_usage.total_cost}
/>
</Grid>
</Grid>
</motion.div>
)}
</AnimatePresence>
</Container>
);
};
export default EnhancedBillingDashboard;

View File

@@ -0,0 +1,368 @@
import React, { useState } from 'react';
import {
Card,
CardContent,
Typography,
Box,
List,
ListItem,
ListItemText,
ListItemIcon,
Chip,
IconButton,
Tooltip,
Collapse,
Alert,
} from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
import {
AlertTriangle,
Info,
XCircle,
ChevronDown,
ChevronUp,
Bell,
BellOff,
CheckCircle
} from 'lucide-react';
// Types
import { UsageAlert } from '../../types/billing';
interface UsageAlertsProps {
alerts: UsageAlert[];
onMarkRead: (alertId: number) => Promise<void>;
}
const UsageAlerts: React.FC<UsageAlertsProps> = ({
alerts,
onMarkRead
}) => {
const [expanded, setExpanded] = useState(false);
const [processing, setProcessing] = useState<number | null>(null);
// Separate alerts by read status
const unreadAlerts = alerts.filter(alert => !alert.is_read);
const readAlerts = alerts.filter(alert => alert.is_read);
const getSeverityIcon = (severity: string) => {
switch (severity) {
case 'error':
return <XCircle size={16} color="#ef4444" />;
case 'warning':
return <AlertTriangle size={16} color="#f59e0b" />;
case 'info':
return <Info size={16} color="#3b82f6" />;
default:
return <Info size={16} color="#6b7280" />;
}
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'error':
return '#ef4444';
case 'warning':
return '#f59e0b';
case 'info':
return '#3b82f6';
default:
return '#6b7280';
}
};
const handleMarkAsRead = async (alertId: number) => {
try {
setProcessing(alertId);
await onMarkRead(alertId);
} catch (error) {
console.error('Error marking alert as read:', error);
} finally {
setProcessing(null);
}
};
const formatAlertTime = (timestamp: string) => {
const date = new Date(timestamp);
const now = new Date();
const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
if (diffInHours < 1) {
return 'Just now';
} else if (diffInHours < 24) {
return `${diffInHours}h ago`;
} else {
const diffInDays = Math.floor(diffInHours / 24);
return `${diffInDays}d ago`;
}
};
const renderAlertItem = (alert: UsageAlert, index: number) => (
<motion.div
key={alert.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
exit={{ opacity: 0, y: -20 }}
>
<ListItem
sx={{
backgroundColor: alert.is_read ? 'rgba(255,255,255,0.02)' : 'rgba(255,255,255,0.05)',
borderRadius: 2,
mb: 1,
border: `1px solid ${getSeverityColor(alert.severity)}20`,
position: 'relative',
'&:hover': {
backgroundColor: 'rgba(255,255,255,0.08)',
}
}}
>
<ListItemIcon sx={{ minWidth: 40 }}>
{getSeverityIcon(alert.severity)}
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 'bold', flex: 1 }}>
{alert.title}
</Typography>
{!alert.is_read && (
<Chip
label="New"
size="small"
sx={{
backgroundColor: '#ef4444',
color: 'white',
fontSize: '0.7rem',
height: 20
}}
/>
)}
</Box>
}
secondary={
<Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{alert.message}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<Typography variant="caption" color="text.secondary">
{formatAlertTime(alert.created_at)}
</Typography>
{alert.provider && (
<Chip
label={alert.provider}
size="small"
variant="outlined"
sx={{ fontSize: '0.7rem', height: 20 }}
/>
)}
<Chip
label={`${alert.threshold_percentage}% threshold`}
size="small"
variant="outlined"
sx={{ fontSize: '0.7rem', height: 20 }}
/>
</Box>
</Box>
}
/>
{!alert.is_read && (
<Tooltip title="Mark as read">
<IconButton
size="small"
onClick={() => handleMarkAsRead(alert.id)}
disabled={processing === alert.id}
sx={{
color: getSeverityColor(alert.severity),
'&:hover': {
backgroundColor: `${getSeverityColor(alert.severity)}20`
}
}}
>
{processing === alert.id ? (
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
>
<CheckCircle size={16} />
</motion.div>
) : (
<CheckCircle size={16} />
)}
</IconButton>
</Tooltip>
)}
</ListItem>
</motion.div>
);
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
whileHover={{ scale: 1.02 }}
>
<Card
sx={{
height: '100%',
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3,
position: 'relative',
overflow: 'hidden'
}}
>
{/* Header */}
<CardContent sx={{ pb: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 'bold' }}>
<Bell size={20} />
Usage Alerts
</Typography>
{unreadAlerts.length > 0 && (
<Chip
label={unreadAlerts.length}
size="small"
sx={{
backgroundColor: '#ef4444',
color: 'white',
fontWeight: 'bold'
}}
/>
)}
</Box>
</CardContent>
<CardContent sx={{ pt: 0 }}>
{/* No alerts state */}
{alerts.length === 0 && (
<Box sx={{ textAlign: 'center', py: 4 }}>
<BellOff size={48} color="#6b7280" />
<Typography variant="body2" sx={{ mt: 2, color: 'rgba(255,255,255,0.8)' }}>
No alerts at this time
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
You'll be notified when usage thresholds are reached
</Typography>
</Box>
)}
{/* Unread alerts */}
{unreadAlerts.length > 0 && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold', color: 'error.main' }}>
Unread Alerts ({unreadAlerts.length})
</Typography>
<List sx={{ p: 0 }}>
<AnimatePresence>
{unreadAlerts.slice(0, 3).map((alert, index) => renderAlertItem(alert, index))}
</AnimatePresence>
</List>
</Box>
)}
{/* Read alerts (collapsible) */}
{readAlerts.length > 0 && (
<Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
cursor: 'pointer',
p: 1,
borderRadius: 1,
'&:hover': { backgroundColor: 'rgba(255,255,255,0.05)' }
}}
onClick={() => setExpanded(!expanded)}
>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
Read Alerts ({readAlerts.length})
</Typography>
{expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</Box>
<Collapse in={expanded}>
<List sx={{ p: 0, mt: 1 }}>
<AnimatePresence>
{readAlerts.map((alert, index) => renderAlertItem(alert, index))}
</AnimatePresence>
</List>
</Collapse>
</Box>
)}
{/* Alert summary */}
{alerts.length > 0 && (
<Box
sx={{
mt: 3,
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)'
}}
>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Alert Summary
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{['error', 'warning', 'info'].map(severity => {
const count = alerts.filter(alert => alert.severity === severity).length;
if (count === 0) return null;
return (
<Chip
key={severity}
label={`${count} ${severity}`}
size="small"
sx={{
backgroundColor: `${getSeverityColor(severity)}20`,
color: getSeverityColor(severity),
fontSize: '0.7rem',
height: 24
}}
/>
);
})}
</Box>
</Box>
)}
</CardContent>
{/* Decorative Elements */}
<Box
sx={{
position: 'absolute',
top: -50,
right: -50,
width: 100,
height: 100,
background: 'radial-gradient(circle, rgba(239, 68, 68, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
<Box
sx={{
position: 'absolute',
bottom: -30,
left: -30,
width: 60,
height: 60,
background: 'radial-gradient(circle, rgba(245, 158, 11, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
</Card>
</motion.div>
);
};
export default UsageAlerts;

View File

@@ -0,0 +1,365 @@
import React from 'react';
import {
Card,
CardContent,
Typography,
Box,
Grid,
Chip,
} from '@mui/material';
import { motion } from 'framer-motion';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Area,
AreaChart
} from 'recharts';
import {
TrendingUp,
TrendingDown,
BarChart3,
Calendar
} from 'lucide-react';
// Types
import { UsageTrends as UsageTrendsType, CostProjections } from '../../types/billing';
// Utils
import {
formatCurrency,
formatNumber,
formatPercentage
} from '../../services/billingService';
interface UsageTrendsProps {
trends: UsageTrendsType;
projections: CostProjections;
}
const UsageTrends: React.FC<UsageTrendsProps> = ({
trends,
projections
}) => {
// Transform data for charts
const chartData = trends.periods.map((period, index) => ({
period,
calls: trends.total_calls[index] || 0,
cost: trends.total_cost[index] || 0,
tokens: trends.total_tokens[index] || 0,
}));
// Calculate growth rates (handle division by zero)
const costGrowth = chartData.length > 1
? chartData[0].cost > 0
? ((chartData[chartData.length - 1].cost - chartData[0].cost) / chartData[0].cost) * 100
: chartData[chartData.length - 1].cost > 0 ? 100 : 0
: 0;
const callsGrowth = chartData.length > 1
? chartData[0].calls > 0
? ((chartData[chartData.length - 1].calls - chartData[0].calls) / chartData[0].calls) * 100
: chartData[chartData.length - 1].calls > 0 ? 100 : 0
: 0;
// Custom tooltip for charts
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<Box
sx={{
backgroundColor: 'rgba(0, 0, 0, 0.8)',
color: 'white',
padding: 2,
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)'
}}
>
<Typography variant="body2" sx={{ fontWeight: 'bold', mb: 1 }}>
{label}
</Typography>
{payload.map((entry: any, index: number) => (
<Typography key={index} variant="body2" sx={{ color: entry.color }}>
{entry.name}: {entry.name === 'Cost' ? formatCurrency(entry.value) : formatNumber(entry.value)}
</Typography>
))}
</Box>
);
}
return null;
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
whileHover={{ scale: 1.02 }}
>
<Card
sx={{
height: '100%',
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3,
position: 'relative',
overflow: 'hidden'
}}
>
{/* Header */}
<CardContent sx={{ pb: 1 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 'bold', mb: 2 }}>
<TrendingUp size={20} />
Usage Trends & Projections
</Typography>
</CardContent>
<CardContent sx={{ pt: 0 }}>
{/* Growth Indicators */}
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={6}>
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}
>
<Box
sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)',
textAlign: 'center'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, mb: 1 }}>
{costGrowth >= 0 ? (
<TrendingUp size={16} color="#22c55e" />
) : (
<TrendingDown size={16} color="#ef4444" />
)}
<Typography variant="body2" color="text.secondary">
Cost Growth
</Typography>
</Box>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: costGrowth >= 0 ? '#22c55e' : '#ef4444'
}}
>
{costGrowth >= 0 ? '+' : ''}{costGrowth.toFixed(1)}%
</Typography>
</Box>
</motion.div>
</Grid>
<Grid item xs={6}>
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, delay: 0.1 }}
>
<Box
sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)',
textAlign: 'center'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, mb: 1 }}>
{callsGrowth >= 0 ? (
<TrendingUp size={16} color="#22c55e" />
) : (
<TrendingDown size={16} color="#ef4444" />
)}
<Typography variant="body2" color="text.secondary">
Calls Growth
</Typography>
</Box>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: callsGrowth >= 0 ? '#22c55e' : '#ef4444'
}}
>
{callsGrowth >= 0 ? '+' : ''}{callsGrowth.toFixed(1)}%
</Typography>
</Box>
</motion.div>
</Grid>
</Grid>
{/* Cost Trend Chart */}
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold', color: '#ffffff' }}>
Monthly Cost Trend
</Typography>
<Box sx={{ height: 200 }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
<defs>
<linearGradient id="costGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#667eea" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#667eea" stopOpacity={0}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
<XAxis
dataKey="period"
stroke="rgba(255,255,255,0.9)"
fontSize={12}
tick={{ fill: 'rgba(255,255,255,0.9)' }}
/>
<YAxis
stroke="rgba(255,255,255,0.9)"
fontSize={12}
tick={{ fill: 'rgba(255,255,255,0.9)' }}
tickFormatter={(value) => `$${value.toFixed(0)}`}
/>
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="cost"
stroke="#667eea"
strokeWidth={2}
fillOpacity={1}
fill="url(#costGradient)"
/>
</AreaChart>
</ResponsiveContainer>
</Box>
</Box>
{/* API Calls Trend Chart */}
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold', color: '#ffffff' }}>
API Calls Trend
</Typography>
<Box sx={{ height: 150 }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
<XAxis
dataKey="period"
stroke="rgba(255,255,255,0.6)"
fontSize={12}
/>
<YAxis
stroke="rgba(255,255,255,0.6)"
fontSize={12}
tickFormatter={(value) => formatNumber(value)}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="calls"
stroke="#764ba2"
strokeWidth={2}
dot={{ fill: '#764ba2', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: '#764ba2', strokeWidth: 2 }}
/>
</LineChart>
</ResponsiveContainer>
</Box>
</Box>
{/* Projections */}
<Box
sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)'
}}
>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold', display: 'flex', alignItems: 'center', gap: 1 }}>
<Calendar size={16} />
Monthly Projections
</Typography>
<Grid container spacing={2}>
<Grid item xs={6}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
Projected Cost
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: 'primary.main' }}>
{formatCurrency(projections.projected_monthly_cost)}
</Typography>
</Box>
</Grid>
<Grid item xs={6}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
Usage %
</Typography>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: projections.projected_usage_percentage > 80 ? 'error.main' :
projections.projected_usage_percentage > 60 ? 'warning.main' : 'success.main'
}}
>
{formatPercentage(projections.projected_usage_percentage)}
</Typography>
</Box>
</Grid>
</Grid>
<Box sx={{ mt: 2, textAlign: 'center' }}>
<Chip
label={`Limit: ${formatCurrency(projections.cost_limit)}`}
size="small"
sx={{
backgroundColor: 'rgba(255,255,255,0.1)',
color: 'text.secondary',
fontWeight: 'bold'
}}
/>
</Box>
</Box>
</CardContent>
{/* Decorative Elements */}
<Box
sx={{
position: 'absolute',
top: -50,
right: -50,
width: 100,
height: 100,
background: 'radial-gradient(circle, rgba(118, 75, 162, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
<Box
sx={{
position: 'absolute',
bottom: -30,
left: -30,
width: 60,
height: 60,
background: 'radial-gradient(circle, rgba(102, 126, 234, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
</Card>
</motion.div>
);
};
export default UsageTrends;