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:
350
frontend/src/components/billing/BillingDashboard.tsx
Normal file
350
frontend/src/components/billing/BillingDashboard.tsx
Normal 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;
|
||||
286
frontend/src/components/billing/BillingOverview.tsx
Normal file
286
frontend/src/components/billing/BillingOverview.tsx
Normal 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;
|
||||
614
frontend/src/components/billing/CompactBillingDashboard.tsx
Normal file
614
frontend/src/components/billing/CompactBillingDashboard.tsx
Normal 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;
|
||||
414
frontend/src/components/billing/ComprehensiveAPIBreakdown.tsx
Normal file
414
frontend/src/components/billing/ComprehensiveAPIBreakdown.tsx
Normal 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;
|
||||
292
frontend/src/components/billing/CostBreakdown.tsx
Normal file
292
frontend/src/components/billing/CostBreakdown.tsx
Normal 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;
|
||||
392
frontend/src/components/billing/EnhancedBillingDashboard.tsx
Normal file
392
frontend/src/components/billing/EnhancedBillingDashboard.tsx
Normal 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;
|
||||
368
frontend/src/components/billing/UsageAlerts.tsx
Normal file
368
frontend/src/components/billing/UsageAlerts.tsx
Normal 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;
|
||||
365
frontend/src/components/billing/UsageTrends.tsx
Normal file
365
frontend/src/components/billing/UsageTrends.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user