AI Analysis and Content Strategy fixes. Enhanced Strategy Routes refactoring.

This commit is contained in:
ajaysi
2026-01-10 19:32:50 +05:30
parent 0b63ae7fc1
commit 8193cdba67
298 changed files with 45678 additions and 10952 deletions

View File

@@ -0,0 +1,590 @@
import React, { useState, useEffect, useMemo, Suspense } from 'react';
import {
Card,
CardContent,
Typography,
Box,
Grid,
Chip,
Tooltip,
CircularProgress,
Tabs,
Tab,
Alert,
} from '@mui/material';
import { motion } from 'framer-motion';
import {
Clock,
Activity,
TrendingUp,
BarChart3,
Zap,
Calendar
} from 'lucide-react';
import {
LazyBarChart,
LazyPieChart,
Bar,
Pie,
Cell,
XAxis,
YAxis,
CartesianGrid,
Tooltip as RechartsTooltip,
ResponsiveContainer,
Legend,
ChartLoadingFallback
} from '../../utils/lazyRecharts';
// Types
import { UsageLog } from '../../types/billing';
// Services
import { billingService, formatCurrency, formatNumber } from '../../services/billingService';
// Components
import DailyCostHeatmap from './DailyCostHeatmap';
interface AdvancedCostAnalyticsProps {
userId?: string;
terminalTheme?: boolean;
}
interface TimeOfDayData {
hour: number;
label: string;
cost: number;
calls: number;
avgCostPerCall: number;
}
interface UserActionData {
endpoint: string;
action: string;
cost: number;
calls: number;
avgCostPerCall: number;
avgResponseTime: number;
}
interface EfficiencyMetric {
label: string;
value: number;
unit: string;
trend: 'up' | 'down' | 'stable';
description: string;
}
const AdvancedCostAnalytics: React.FC<AdvancedCostAnalyticsProps> = ({
userId,
terminalTheme = false
}) => {
const [usageLogs, setUsageLogs] = useState<UsageLog[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState(0);
useEffect(() => {
const fetchUsageLogs = async () => {
try {
setLoading(true);
setError(null);
const currentDate = new Date();
const billingPeriod = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`;
console.log('[AdvancedCostAnalytics] Fetching usage logs for period:', billingPeriod);
// Try with billing period first, then without if no results
let response = await billingService.getUsageLogs(2000, 0, undefined, undefined, billingPeriod);
let logs = response.logs || [];
// If no logs for current period, try without billing period filter to get recent logs
if (logs.length === 0) {
console.log('[AdvancedCostAnalytics] No logs for current period, fetching all recent logs...');
response = await billingService.getUsageLogs(2000, 0);
logs = response.logs || [];
}
console.log('[AdvancedCostAnalytics] Received logs:', {
total: logs.length,
sample: logs.slice(0, 3),
totalCost: logs.reduce((sum, log) => sum + (log.cost_total || 0), 0),
totalCalls: logs.length,
logsWithCost: logs.filter(log => (log.cost_total || 0) > 0).length,
logsWithTokens: logs.filter(log => (log.tokens_total || 0) > 0).length,
sampleLogStructure: logs[0] ? {
id: logs[0].id,
cost_total: logs[0].cost_total,
tokens_total: logs[0].tokens_total,
status: logs[0].status,
status_code: logs[0].status_code,
response_time: logs[0].response_time
} : null
});
setUsageLogs(logs);
} catch (err) {
console.error('[AdvancedCostAnalytics] Error fetching usage logs:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch usage logs');
// Set empty array on error to prevent showing stale data
setUsageLogs([]);
} finally {
setLoading(false);
}
};
fetchUsageLogs();
}, [userId]);
// Time of Day Analysis
const timeOfDayData = useMemo(() => {
const hourlyData: Record<number, { cost: number; calls: number }> = {};
usageLogs.forEach(log => {
// Include all logs, not just those with cost > 0, for accurate call counts
const cost = log.cost_total || 0;
try {
const date = new Date(log.timestamp);
if (isNaN(date.getTime())) {
console.warn('[AdvancedCostAnalytics] Invalid timestamp:', log.timestamp);
return;
}
const hour = date.getHours();
if (!hourlyData[hour]) {
hourlyData[hour] = { cost: 0, calls: 0 };
}
hourlyData[hour].cost += cost;
hourlyData[hour].calls += 1;
} catch (err) {
console.warn('[AdvancedCostAnalytics] Error processing log timestamp:', err, log);
}
});
return Array.from({ length: 24 }, (_, hour) => {
const data = hourlyData[hour] || { cost: 0, calls: 0 };
return {
hour,
label: `${hour.toString().padStart(2, '0')}:00`,
cost: data.cost,
calls: data.calls,
avgCostPerCall: data.calls > 0 ? data.cost / data.calls : 0
} as TimeOfDayData;
});
}, [usageLogs]);
// User Action Breakdown
const userActionData = useMemo(() => {
const actionMap: Record<string, { cost: number; calls: number; responseTime: number }> = {};
usageLogs.forEach(log => {
// Include all logs for accurate call counts
const cost = log.cost_total || 0;
// Extract action from endpoint (e.g., /api/blog-writer/generate -> blog-writer)
const endpoint = log.endpoint || '';
const endpointParts = endpoint.split('/');
const action = endpointParts.length > 2 ? endpointParts[2] : 'other';
if (!actionMap[action]) {
actionMap[action] = { cost: 0, calls: 0, responseTime: 0 };
}
actionMap[action].cost += cost;
actionMap[action].calls += 1;
actionMap[action].responseTime += log.response_time || 0;
});
return Object.entries(actionMap)
.map(([endpoint, data]) => ({
endpoint,
action: endpoint.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
cost: data.cost,
calls: data.calls,
avgCostPerCall: data.calls > 0 ? data.cost / data.calls : 0,
avgResponseTime: data.calls > 0 ? data.responseTime / data.calls : 0
} as UserActionData))
.sort((a, b) => b.cost - a.cost)
.slice(0, 10); // Top 10 actions
}, [usageLogs]);
// Cost Efficiency Metrics
const efficiencyMetrics = useMemo(() => {
const totalCost = usageLogs.reduce((sum, log) => sum + (log.cost_total || 0), 0);
const totalCalls = usageLogs.length;
const totalTokens = usageLogs.reduce((sum, log) => sum + (log.tokens_total || 0), 0);
const totalResponseTime = usageLogs.reduce((sum, log) => sum + (log.response_time || 0), 0);
const successfulCalls = usageLogs.filter(log => log.status === 'success' || (log.status_code && log.status_code < 400)).length;
const failedCalls = usageLogs.filter(log => log.status === 'failed' || (log.status_code && log.status_code >= 400)).length;
// Debug logging
if (usageLogs.length > 0) {
console.log('[AdvancedCostAnalytics] Efficiency metrics calculation:', {
totalLogs: usageLogs.length,
totalCost,
totalTokens,
totalCalls,
successfulCalls,
failedCalls,
sampleLog: usageLogs[0]
});
}
const avgCostPerCall = totalCalls > 0 ? totalCost / totalCalls : 0;
const avgCostPerToken = totalTokens > 0 ? totalCost / totalTokens : 0;
const avgResponseTime = totalCalls > 0 ? totalResponseTime / totalCalls : 0;
const successRate = totalCalls > 0 ? (successfulCalls / totalCalls) * 100 : 0;
// Tokens per dollar - handle division by zero
const costEfficiency = totalCost > 0 && totalTokens > 0 ? totalTokens / totalCost : 0;
// Calculate trends (simplified - compare first half vs second half)
const midPoint = Math.floor(usageLogs.length / 2);
const firstHalf = usageLogs.slice(0, midPoint);
const secondHalf = usageLogs.slice(midPoint);
const firstHalfAvgCost = firstHalf.length > 0
? firstHalf.reduce((sum, log) => sum + (log.cost_total || 0), 0) / firstHalf.length
: 0;
const secondHalfAvgCost = secondHalf.length > 0
? secondHalf.reduce((sum, log) => sum + (log.cost_total || 0), 0) / secondHalf.length
: 0;
const costTrend = secondHalfAvgCost > firstHalfAvgCost * 1.05 ? 'up' :
secondHalfAvgCost < firstHalfAvgCost * 0.95 ? 'down' : 'stable';
return [
{
label: 'Avg Cost per Call',
value: avgCostPerCall,
unit: '$',
trend: costTrend,
description: 'Average cost per API call'
},
{
label: 'Cost per 1K Tokens',
value: avgCostPerToken * 1000,
unit: '$',
trend: costTrend,
description: 'Cost efficiency for token usage'
},
{
label: 'Tokens per Dollar',
value: costEfficiency,
unit: '',
trend: costTrend === 'down' ? 'up' : costTrend === 'up' ? 'down' : 'stable',
description: 'How many tokens you get per dollar spent'
},
{
label: 'Avg Response Time',
value: avgResponseTime,
unit: 's',
trend: 'stable',
description: 'Average API response time'
},
{
label: 'Success Rate',
value: successRate,
unit: '%',
trend: successRate > 95 ? 'up' : successRate < 90 ? 'down' : 'stable',
description: 'Percentage of successful API calls'
},
{
label: 'Failed Calls',
value: failedCalls,
unit: '',
trend: failedCalls > 0 ? 'down' : 'stable',
description: 'Number of failed API calls'
}
] as EfficiencyMetric[];
}, [usageLogs]);
const COLORS = ['#667eea', '#764ba2', '#f093fb', '#4facfe', '#00f2fe', '#43e97b', '#fa709a', '#fee140', '#30cfd0', '#a8edea'];
if (loading) {
return (
<Card sx={{ p: 3, textAlign: 'center' }}>
<CircularProgress />
<Typography sx={{ mt: 2, color: 'rgba(255,255,255,0.7)' }}>
Loading analytics...
</Typography>
</Card>
);
}
if (error) {
return (
<Card sx={{ p: 3 }}>
<Alert severity="error">
{error}
<Typography variant="body2" sx={{ mt: 1, color: 'rgba(255,255,255,0.7)' }}>
Unable to load usage analytics. Please try refreshing the page.
</Typography>
</Alert>
</Card>
);
}
// Show message if no logs available
if (usageLogs.length === 0) {
return (
<Card sx={{ p: 3, textAlign: 'center' }}>
<Alert severity="info">
<Typography variant="h6" sx={{ mb: 1 }}>
No Usage Data Available
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Usage analytics will appear here once you start making API calls.
</Typography>
</Alert>
</Card>
);
}
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,
}}
>
<CardContent sx={{ pb: 1 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 'bold', color: '#ffffff', mb: 2 }}>
<BarChart3 size={20} />
Advanced Cost Analytics
</Typography>
<Tabs
value={activeTab}
onChange={(_, newValue) => setActiveTab(newValue)}
sx={{
borderBottom: '1px solid rgba(255,255,255,0.1)',
mb: 2,
'& .MuiTab-root': {
color: 'rgba(255,255,255,0.7)',
'&.Mui-selected': {
color: '#667eea',
fontWeight: 'bold'
}
},
'& .MuiTabs-indicator': {
backgroundColor: '#667eea'
}
}}
>
<Tab icon={<Clock size={16} />} label="Time of Day" iconPosition="start" />
<Tab icon={<Activity size={16} />} label="User Actions" iconPosition="start" />
<Tab icon={<Zap size={16} />} label="Efficiency Metrics" iconPosition="start" />
<Tab icon={<Calendar size={16} />} label="Daily Heatmap" iconPosition="start" />
</Tabs>
</CardContent>
<CardContent sx={{ pt: 0 }}>
{/* Time of Day Tab */}
{activeTab === 0 && (
<Box>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold', color: '#ffffff' }}>
Cost Distribution by Hour of Day
</Typography>
<Box sx={{ height: 300, mb: 3 }}>
<ResponsiveContainer width="100%" height="100%">
<Suspense fallback={<ChartLoadingFallback />}>
<LazyBarChart data={timeOfDayData}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
<XAxis
dataKey="label"
stroke="rgba(255,255,255,0.9)"
fontSize={10}
angle={-45}
textAnchor="end"
height={60}
/>
<YAxis
stroke="rgba(255,255,255,0.9)"
fontSize={12}
tickFormatter={(value) => `$${value.toFixed(2)}`}
/>
<RechartsTooltip
contentStyle={{
backgroundColor: 'rgba(0, 0, 0, 0.8)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 8
}}
formatter={(value: number) => [formatCurrency(value), 'Cost']}
/>
<Bar dataKey="cost" fill="#667eea" radius={[4, 4, 0, 0]} />
</LazyBarChart>
</Suspense>
</ResponsiveContainer>
</Box>
{/* Peak Hours Summary */}
<Grid container spacing={2}>
{timeOfDayData
.sort((a, b) => b.cost - a.cost)
.slice(0, 3)
.map((data, idx) => (
<Grid item xs={4} key={data.hour}>
<Box sx={{ p: 1.5, backgroundColor: 'rgba(102, 126, 234, 0.1)', borderRadius: 1, textAlign: 'center' }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Peak Hour {idx + 1}
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#667eea' }}>
{data.label}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
{formatCurrency(data.cost)}
</Typography>
</Box>
</Grid>
))}
</Grid>
</Box>
)}
{/* User Actions Tab */}
{activeTab === 1 && (
<Box>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold', color: '#ffffff' }}>
Cost Breakdown by User Action
</Typography>
<Box sx={{ height: 300, mb: 3 }}>
<ResponsiveContainer width="100%" height="100%">
<Suspense fallback={<ChartLoadingFallback />}>
<LazyPieChart>
<Pie
data={userActionData}
dataKey="cost"
nameKey="action"
cx="50%"
cy="50%"
outerRadius={100}
label={(entry: any) => {
const data = userActionData[entry.index];
return data ? `${data.action}: ${(entry.percent * 100).toFixed(0)}%` : '';
}}
>
{userActionData.map((_, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<RechartsTooltip
formatter={(value: number) => formatCurrency(value)}
contentStyle={{
backgroundColor: 'rgba(0, 0, 0, 0.8)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 8
}}
/>
</LazyPieChart>
</Suspense>
</ResponsiveContainer>
</Box>
{/* Top Actions Table */}
<Box sx={{ maxHeight: 200, overflowY: 'auto' }}>
{userActionData.map((action, idx) => (
<Box
key={action.endpoint}
sx={{
p: 1.5,
mb: 1,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 1,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{action.action}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
{action.calls} calls {formatCurrency(action.avgCostPerCall)} avg
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: COLORS[idx % COLORS.length] }}>
{formatCurrency(action.cost)}
</Typography>
</Box>
))}
</Box>
</Box>
)}
{/* Efficiency Metrics Tab */}
{activeTab === 2 && (
<Box>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold', color: '#ffffff' }}>
Cost Efficiency Metrics
</Typography>
<Grid container spacing={2}>
{efficiencyMetrics.map((metric, idx) => (
<Grid item xs={6} sm={4} key={metric.label}>
<Tooltip title={metric.description} arrow>
<Box
sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)',
textAlign: 'center',
cursor: 'help'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, mb: 1 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
{metric.label}
</Typography>
{metric.trend === 'up' && <TrendingUp size={14} color="#22c55e" />}
{metric.trend === 'down' && <TrendingUp size={14} color="#ef4444" style={{ transform: 'rotate(180deg)' }} />}
</Box>
<Typography
variant="h5"
sx={{
fontWeight: 'bold',
color: metric.trend === 'up' ? '#22c55e' :
metric.trend === 'down' ? '#ef4444' : '#ffffff'
}}
>
{metric.unit === '$' ? formatCurrency(metric.value) :
metric.unit === '%' ? `${metric.value.toFixed(1)}%` :
metric.unit === 's' ? `${metric.value.toFixed(2)}s` :
formatNumber(metric.value)}
</Typography>
</Box>
</Tooltip>
</Grid>
))}
</Grid>
</Box>
)}
{/* Daily Heatmap Tab */}
{activeTab === 3 && (
<Box>
<DailyCostHeatmap
usageLogs={usageLogs}
currentMonth={new Date().getMonth()}
currentYear={new Date().getFullYear()}
/>
</Box>
)}
</CardContent>
</Card>
</motion.div>
);
};
export default AdvancedCostAnalytics;

View File

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

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import {
Card,
CardContent,
@@ -28,6 +28,11 @@ import {
calculateUsagePercentage
} from '../../services/billingService';
// Shared Components
import { AnimatedNumber } from '../shared/AnimatedNumber';
import { AnimatedProgressBar } from '../shared/AnimatedProgressBar';
import LiveCostCounter from './LiveCostCounter';
// Terminal Theme
import {
TerminalCard,
@@ -55,14 +60,20 @@ const BillingOverview: React.FC<BillingOverviewProps> = ({
const CardComponent = terminalTheme ? TerminalCard : Card;
const CardContentComponent = terminalTheme ? TerminalCardContent : CardContent;
const TypographyComponent = terminalTheme ? TerminalTypography : Typography;
// Debug logs removed to reduce console noise
const [previousCost, setPreviousCost] = useState<number | undefined>(undefined);
// Track previous cost for velocity calculation
useEffect(() => {
if (usageStats.total_cost !== undefined) {
setPreviousCost(usageStats.total_cost);
}
}, [usageStats.total_cost]);
const costUsagePercentage = calculateUsagePercentage(
usageStats.total_cost,
usageStats.limits.limits.monthly_cost || 1
);
// Debug logs removed to reduce console noise
const getStatusChip = () => {
const status: string = usageStats.usage_status;
@@ -186,28 +197,16 @@ const BillingOverview: React.FC<BillingOverviewProps> = ({
</CardContentComponent>
<CardContentComponent 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 }}
>
<TypographyComponent
variant="h3"
sx={{
fontWeight: 'bold',
color: terminalTheme ? terminalColors.text : '#ffffff',
textShadow: terminalTheme ? 'none' : '0 2px 4px rgba(0,0,0,0.3)',
mb: 1
}}
>
{formatCurrency(usageStats.total_cost)}
</TypographyComponent>
<TypographyComponent variant="body2" sx={{ color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.8)' }}>
Total Cost This Month
</TypographyComponent>
</motion.div>
{/* Live Cost Counter */}
<Box sx={{ mb: 3 }}>
<LiveCostCounter
currentCost={usageStats.total_cost}
previousCost={previousCost}
velocity={0} // TODO: Calculate from trends if available
terminalTheme={terminalTheme}
terminalColors={terminalColors}
TypographyComponent={TypographyComponent}
/>
</Box>
{/* Usage Metrics */}
@@ -218,7 +217,10 @@ const BillingOverview: React.FC<BillingOverviewProps> = ({
API Calls
</TypographyComponent>
<TypographyComponent variant="body2" sx={{ fontWeight: 'bold', color: terminalTheme ? terminalColors.text : '#ffffff' }}>
{formatNumber(usageStats.total_calls)}
<AnimatedNumber
value={usageStats.total_calls}
format={(n) => formatNumber(n)}
/>
</TypographyComponent>
</Box>
</Tooltip>
@@ -229,7 +231,10 @@ const BillingOverview: React.FC<BillingOverviewProps> = ({
Tokens Used
</TypographyComponent>
<TypographyComponent variant="body2" sx={{ fontWeight: 'bold', color: terminalTheme ? terminalColors.text : '#ffffff' }}>
{formatNumber(usageStats.total_tokens)}
<AnimatedNumber
value={usageStats.total_tokens}
format={(n) => formatNumber(n)}
/>
</TypographyComponent>
</Box>
</Tooltip>
@@ -257,22 +262,16 @@ const BillingOverview: React.FC<BillingOverviewProps> = ({
{formatPercentage(costUsagePercentage)}
</TypographyComponent>
</Box>
<LinearProgress
variant="determinate"
<AnimatedProgressBar
value={Math.min(costUsagePercentage, 100)}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: terminalTheme ? terminalColors.backgroundLight : 'rgba(255,255,255,0.1)',
'& .MuiLinearProgress-bar': {
backgroundColor: terminalTheme
? (costUsagePercentage > 80 ? terminalColors.error :
costUsagePercentage > 60 ? terminalColors.warning : terminalColors.success)
: (costUsagePercentage > 80 ? '#ef4444' :
costUsagePercentage > 60 ? '#f59e0b' : '#22c55e'),
borderRadius: 4,
}
}}
height={8}
color={terminalTheme
? (costUsagePercentage > 80 ? terminalColors.error :
costUsagePercentage > 60 ? terminalColors.warning : terminalColors.success)
: (costUsagePercentage > 80 ? '#ef4444' :
costUsagePercentage > 60 ? '#f59e0b' : '#22c55e')
}
showPercentage={false}
/>
<TypographyComponent variant="caption" sx={{ mt: 0.5, display: 'block', color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)' }}>
{formatCurrency(usageStats.total_cost)} of {formatCurrency(usageStats.limits.limits.monthly_cost)} limit

View File

@@ -1,837 +1,6 @@
import React, { useState, useEffect } 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';
import { showToastNotification } from '../../utils/toastNotifications';
// Terminal Theme
import {
TerminalCard,
TerminalCardContent,
TerminalTypography,
TerminalChip,
TerminalChipError,
TerminalChipWarning,
terminalColors
} from '../SchedulerDashboard/terminalTheme';
interface CompactBillingDashboardProps {
userId?: string;
terminalTheme?: boolean;
}
const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userId, terminalTheme = false }) => {
// Conditional component selection based on terminal theme
const CardComponent = terminalTheme ? TerminalCard : Card;
const CardContentComponent = terminalTheme ? TerminalCardContent : CardContent;
const TypographyComponent = terminalTheme ? TerminalTypography : Typography;
const ChipComponent = terminalTheme ? TerminalChip : Chip;
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 fetchData = async (showSuccessToast: boolean = false) => {
try {
setLoading(true);
setError(null);
const [billingData, healthData] = await Promise.all([
billingService.getDashboardData(userId),
monitoringService.getSystemHealth()
]);
setDashboardData(billingData);
setSystemHealth(healthData);
// Show success toast only if explicitly requested (user-initiated refresh)
if (showSuccessToast && billingData && healthData) {
showToastNotification(
'Billing data refreshed successfully',
'success',
{ duration: 3000 }
);
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch data';
setError(errorMessage);
// Always show error toast for failures
showToastNotification(errorMessage, 'error', { duration: 5000 });
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const formatCurrency = (amount: number) => `$${amount.toFixed(4)}`;
const formatNumber = (num: number) => num.toLocaleString();
if (loading && !dashboardData) {
const loadingCardStyles = terminalTheme
? {
backgroundColor: terminalColors.background,
border: `1px solid ${terminalColors.border}`,
borderRadius: 3
}
: {
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
};
return (
<CardComponent sx={loadingCardStyles}>
<CardContentComponent sx={{ textAlign: 'center', py: 4 }}>
<TypographyComponent sx={{ color: terminalTheme ? terminalColors.text : 'rgba(255,255,255,0.8)' }}>
Loading billing data...
</TypographyComponent>
</CardContentComponent>
</CardComponent>
);
}
if (error) {
const errorCardStyles = terminalTheme
? {
backgroundColor: terminalColors.background,
border: `1px solid ${terminalColors.error}`,
borderRadius: 3
}
: {
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
};
return (
<CardComponent sx={errorCardStyles}>
<CardContentComponent sx={{ textAlign: 'center', py: 4 }}>
<TypographyComponent sx={{ color: terminalTheme ? terminalColors.error : '#ff6b6b' }}>
Error: {error}
</TypographyComponent>
<IconButton onClick={() => fetchData(true)} sx={{ mt: 1, color: terminalTheme ? terminalColors.text : 'inherit' }}>
<RefreshCw size={16} />
</IconButton>
</CardContentComponent>
</CardComponent>
);
}
if (!dashboardData) return null;
const { current_usage, limits, alerts } = dashboardData;
const mainCardStyles = terminalTheme
? {
backgroundColor: terminalColors.background,
border: `1px solid ${terminalColors.border}`,
borderRadius: 4,
position: 'relative' as const,
overflow: 'hidden' as const,
boxShadow: '0 0 15px rgba(0, 255, 0, 0.2)',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '1px',
background: `linear-gradient(90deg, transparent, ${terminalColors.border}, transparent)`,
zIndex: 1
}
}
: {
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' as const,
overflow: 'hidden' as const,
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
}
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<CardComponent sx={mainCardStyles}>
{/* Header - Removed to save space */}
<CardContentComponent 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,
...(terminalTheme ? {
backgroundColor: terminalColors.backgroundLight,
borderRadius: 3,
border: `1px solid ${terminalColors.border}`,
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: `0 0 15px ${terminalColors.border}40`,
borderColor: terminalColors.secondary
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: terminalColors.border,
zIndex: 1
}
} : {
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
}
})
}}>
<TypographyComponent variant="h5" sx={{
fontWeight: 800,
color: terminalTheme ? terminalColors.text : '#ffffff',
textShadow: terminalTheme ? 'none' : '0 2px 8px rgba(0,0,0,0.4)',
mb: 0.5
}}>
{formatCurrency(current_usage.total_cost)}
</TypographyComponent>
<TypographyComponent variant="body2" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.75rem'
}}>
Total Cost
</TypographyComponent>
</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,
...(terminalTheme ? {
backgroundColor: terminalColors.backgroundLight,
borderRadius: 3,
border: `1px solid ${terminalColors.border}`,
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: `0 0 15px ${terminalColors.border}40`,
borderColor: terminalColors.secondary
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: terminalColors.border,
zIndex: 1
}
} : {
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
}
})
}}>
<TypographyComponent variant="h5" sx={{
fontWeight: 800,
color: terminalTheme ? terminalColors.text : '#ffffff',
textShadow: terminalTheme ? 'none' : '0 2px 8px rgba(0,0,0,0.4)',
mb: 0.5
}}>
{formatNumber(current_usage.total_calls)}
</TypographyComponent>
<TypographyComponent variant="body2" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.75rem'
}}>
API Calls
</TypographyComponent>
</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,
...(terminalTheme ? {
backgroundColor: terminalColors.backgroundLight,
borderRadius: 3,
border: `1px solid ${terminalColors.border}`,
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: `0 0 15px ${terminalColors.border}40`,
borderColor: terminalColors.secondary
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: terminalColors.border,
zIndex: 1
}
} : {
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
}
})
}}>
<TypographyComponent variant="h5" sx={{
fontWeight: 800,
color: terminalTheme ? terminalColors.text : '#ffffff',
textShadow: terminalTheme ? 'none' : '0 2px 8px rgba(0,0,0,0.4)',
mb: 0.5
}}>
{(current_usage.total_tokens / 1000).toFixed(1)}k
</TypographyComponent>
<TypographyComponent variant="body2" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.75rem'
}}>
Tokens
</TypographyComponent>
</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,
...(terminalTheme ? {
backgroundColor: terminalColors.backgroundLight,
borderRadius: 3,
border: `1px solid ${systemHealth?.status === 'healthy' ? terminalColors.success : terminalColors.error}`,
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: `0 0 15px ${systemHealth?.status === 'healthy' ? terminalColors.success : terminalColors.error}40`,
borderColor: systemHealth?.status === 'healthy' ? terminalColors.secondary : terminalColors.error
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: systemHealth?.status === 'healthy' ? terminalColors.success : terminalColors.error,
zIndex: 1
}
} : {
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={terminalTheme
? (systemHealth?.status === 'healthy' ? terminalColors.success : terminalColors.error)
: (systemHealth?.status === 'healthy' ? '#4ade80' : '#ff6b6b')
} />
<TypographyComponent variant="body1" sx={{
color: terminalTheme
? (systemHealth?.status === 'healthy' ? terminalColors.success : terminalColors.error)
: (systemHealth?.status === 'healthy' ? '#4ade80' : '#ff6b6b'),
fontWeight: 700,
textTransform: 'capitalize',
textShadow: terminalTheme ? 'none' : '0 2px 4px rgba(0,0,0,0.3)'
}}>
{systemHealth?.status || 'Unknown'}
</TypographyComponent>
</Box>
<TypographyComponent variant="body2" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.75rem'
}}>
System Health
</TypographyComponent>
</Box>
</Tooltip>
</Grid>
</Grid>
{/* Usage Progress */}
{limits.limits.monthly_cost > 0 && (
<Box sx={{
mb: 3,
p: 2.5,
...(terminalTheme ? {
backgroundColor: terminalColors.backgroundLight,
borderRadius: 3,
border: `1px solid ${terminalColors.border}`
} : {
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>
<TypographyComponent variant="subtitle2" sx={{
color: terminalTheme ? terminalColors.text : '#ffffff',
fontWeight: 600,
mb: 0.5
}}>
Monthly Budget Usage
</TypographyComponent>
<TypographyComponent variant="caption" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)',
display: 'block'
}}>
Track your AI spending against monthly limits
</TypographyComponent>
</Box>
<Box sx={{ textAlign: 'right' }}>
<TypographyComponent variant="h6" sx={{
color: terminalTheme ? terminalColors.text : '#ffffff',
fontWeight: 'bold',
textShadow: terminalTheme ? 'none' : '0 2px 4px rgba(0,0,0,0.3)'
}}>
{formatCurrency(current_usage.total_cost)}
</TypographyComponent>
<TypographyComponent variant="caption" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)',
display: 'block'
}}>
of {formatCurrency(limits.limits.monthly_cost)}
</TypographyComponent>
</Box>
</Box>
<LinearProgress
variant="determinate"
value={(current_usage.total_cost / limits.limits.monthly_cost) * 100}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: terminalTheme ? terminalColors.backgroundLight : 'rgba(255,255,255,0.1)',
'& .MuiLinearProgress-bar': {
background: terminalTheme
? (current_usage.total_cost / limits.limits.monthly_cost > 0.8
? terminalColors.error
: current_usage.total_cost / limits.limits.monthly_cost > 0.6
? terminalColors.warning
: terminalColors.success)
: (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: terminalTheme ? 'none' : '0 2px 8px rgba(0,0,0,0.2)'
}
}}
/>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 1 }}>
<TypographyComponent variant="caption" sx={{
color: terminalTheme
? (current_usage.total_cost / limits.limits.monthly_cost > 0.8 ? terminalColors.error : terminalColors.textSecondary)
: (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'
}
</TypographyComponent>
<TypographyComponent variant="caption" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)',
fontWeight: 500
}}>
{((current_usage.total_cost / limits.limits.monthly_cost) * 100).toFixed(1)}% used
</TypographyComponent>
</Box>
</Box>
)}
{/* Alerts */}
{alerts.length > 0 && (
<Box sx={{
mb: 3,
p: 2.5,
...(terminalTheme ? {
backgroundColor: terminalColors.backgroundLight,
borderRadius: 3,
border: `1px solid ${terminalColors.error}`,
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: terminalColors.error,
borderRadius: '3px 3px 0 0'
}
} : {
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={terminalTheme ? terminalColors.error : "#ff6b6b"} />
<TypographyComponent variant="subtitle2" sx={{
fontWeight: 700,
color: terminalTheme ? terminalColors.error : '#ff6b6b',
textShadow: terminalTheme ? 'none' : '0 1px 2px rgba(0,0,0,0.3)'
}}>
System Alerts ({alerts.length})
</TypographyComponent>
</Box>
<TypographyComponent variant="caption" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.8)',
display: 'block',
mb: 2
}}>
Important notifications requiring your attention
</TypographyComponent>
<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"
>
{terminalTheme ? (
<TerminalChipError
label={alert.title}
size="small"
icon={<AlertTriangle size={14} />}
sx={{
fontWeight: 500,
'&:hover': {
transform: 'translateY(-1px)'
},
transition: 'all 0.2s ease'
}}
/>
) : (
<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 && (
terminalTheme ? (
<TerminalChip
label={`+${alerts.length - 3} more`}
size="small"
sx={{ fontWeight: 500 }}
/>
) : (
<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>
)}
</CardContentComponent>
</CardComponent>
</motion.div>
);
};
export default CompactBillingDashboard;
/**
* @deprecated This file has been refactored into a modular structure.
* Please import from './CompactBillingDashboard/index' instead.
* This file is kept for backward compatibility and will be removed in a future version.
*/
export { default } from './CompactBillingDashboard/index';

View File

@@ -0,0 +1,153 @@
import React from 'react';
import { Box, Tooltip } from '@mui/material';
import { AlertTriangle } from 'lucide-react';
import { Chip } from '@mui/material';
import { TerminalTypography, TerminalChip, TerminalChipError } from '../../../SchedulerDashboard/terminalTheme';
import { terminalColors } from '../../../SchedulerDashboard/terminalTheme';
import { DashboardData } from '../../../../types/billing';
interface AlertsSectionProps {
alerts: DashboardData['alerts'];
terminalTheme?: boolean;
TypographyComponent: typeof TerminalTypography | React.ComponentType<any>;
ChipComponent: typeof TerminalChip | typeof Chip;
}
/**
* AlertsSection - Displays system alerts
*/
export const AlertsSection: React.FC<AlertsSectionProps> = ({
alerts,
terminalTheme = false,
TypographyComponent,
ChipComponent
}) => {
if (alerts.length === 0) return null;
return (
<Box sx={{
mb: 3,
p: 2.5,
...(terminalTheme ? {
backgroundColor: terminalColors.backgroundLight,
borderRadius: 3,
border: `1px solid ${terminalColors.error}`,
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: terminalColors.error,
borderRadius: '3px 3px 0 0'
}
} : {
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={terminalTheme ? terminalColors.error : "#ff6b6b"} />
<TypographyComponent variant="subtitle2" sx={{
fontWeight: 700,
color: terminalTheme ? terminalColors.error : '#ff6b6b',
textShadow: terminalTheme ? 'none' : '0 1px 2px rgba(0,0,0,0.3)'
}}>
System Alerts ({alerts.length})
</TypographyComponent>
</Box>
<TypographyComponent variant="caption" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.8)',
display: 'block',
mb: 2
}}>
Important notifications requiring your attention
</TypographyComponent>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{alerts.slice(0, 3).map((alert) => (
<Tooltip
key={alert.id}
title={
<Box>
<TypographyComponent variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
{alert.title}
</TypographyComponent>
<TypographyComponent variant="body2" sx={{ opacity: 0.9 }}>
{alert.message}
</TypographyComponent>
</Box>
}
arrow
placement="top"
>
{terminalTheme ? (
<TerminalChipError
label={alert.title}
size="small"
icon={<AlertTriangle size={14} />}
sx={{
fontWeight: 500,
'&:hover': {
transform: 'translateY(-1px)'
},
transition: 'all 0.2s ease'
}}
/>
) : (
<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 && (
terminalTheme ? (
<TerminalChip
label={`+${alerts.length - 3} more`}
size="small"
sx={{ fontWeight: 500 }}
/>
) : (
<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>
);
};

View File

@@ -0,0 +1,254 @@
import React from 'react';
import { Grid, Box, Tooltip } from '@mui/material';
import { TerminalTypography } from '../../../SchedulerDashboard/terminalTheme';
import { terminalColors } from '../../../SchedulerDashboard/terminalTheme';
import { formatCurrency } from '../utils/formatting';
import { DashboardData } from '../../../../types/billing';
interface CostEfficiencyMetricsProps {
currentUsage: DashboardData['current_usage'];
terminalTheme?: boolean;
TypographyComponent: typeof TerminalTypography | React.ComponentType<any>;
}
/**
* CostEfficiencyMetrics - Displays cost efficiency metrics (Avg Cost/Call, Cost/1K Tokens, Efficiency Score)
*/
export const CostEfficiencyMetrics: React.FC<CostEfficiencyMetricsProps> = ({
currentUsage,
terminalTheme = false,
TypographyComponent
}) => {
if (currentUsage.total_calls === 0) return null;
const avgCostPerCall = currentUsage.total_cost / currentUsage.total_calls;
const costPer1KTokens = currentUsage.total_tokens > 0
? (currentUsage.total_cost / currentUsage.total_tokens) * 1000
: 0;
const getEfficiencyScore = () => {
if (avgCostPerCall < 0.01) return '⭐ Excellent';
if (avgCostPerCall < 0.05) return '✅ Good';
if (avgCostPerCall < 0.10) return '⚡ Fair';
return '⚠️ High';
};
return (
<Grid container spacing={2} sx={{ mb: 2 }}>
{/* Average Cost Per Call */}
<Grid item xs={6} sm={4}>
<Tooltip
title={
<Box>
<TypographyComponent variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
Average Cost Per API Call
</TypographyComponent>
<TypographyComponent variant="body2" sx={{ opacity: 0.9 }}>
Calculated as total cost divided by total API calls this month
</TypographyComponent>
<TypographyComponent variant="caption" sx={{ opacity: 0.7, mt: 1, display: 'block' }}>
Lower values indicate more cost-efficient usage
</TypographyComponent>
</Box>
}
arrow
placement="top"
>
<Box sx={{
textAlign: 'center',
p: 2,
...(terminalTheme ? {
backgroundColor: terminalColors.backgroundLight,
borderRadius: 3,
border: `1px solid ${terminalColors.border}`,
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: `0 0 10px ${terminalColors.border}30`,
borderColor: terminalColors.secondary
}
} : {
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.12) 0%, rgba(79, 70, 229, 0.08) 100%)',
borderRadius: 3,
border: '1px solid rgba(99, 102, 241, 0.25)',
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: '0 6px 20px rgba(99, 102, 241, 0.2)',
border: '1px solid rgba(99, 102, 241, 0.4)'
}
})
}}>
<TypographyComponent variant="h6" sx={{
fontWeight: 700,
color: terminalTheme ? terminalColors.text : '#ffffff',
textShadow: terminalTheme ? 'none' : '0 2px 4px rgba(0,0,0,0.3)',
mb: 0.5
}}>
{formatCurrency(avgCostPerCall)}
</TypographyComponent>
<TypographyComponent variant="body2" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.7rem'
}}>
Avg Cost/Call
</TypographyComponent>
</Box>
</Tooltip>
</Grid>
{/* Cost Per 1K Tokens */}
{currentUsage.total_tokens > 0 && (
<Grid item xs={6} sm={4}>
<Tooltip
title={
<Box>
<TypographyComponent variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
Cost Per 1,000 Tokens
</TypographyComponent>
<TypographyComponent variant="body2" sx={{ opacity: 0.9 }}>
Average cost for processing 1,000 tokens (input + output)
</TypographyComponent>
<TypographyComponent variant="caption" sx={{ opacity: 0.7, mt: 1, display: 'block' }}>
Useful for estimating costs of future operations
</TypographyComponent>
</Box>
}
arrow
placement="top"
>
<Box sx={{
textAlign: 'center',
p: 2,
...(terminalTheme ? {
backgroundColor: terminalColors.backgroundLight,
borderRadius: 3,
border: `1px solid ${terminalColors.border}`,
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: `0 0 10px ${terminalColors.border}30`,
borderColor: terminalColors.secondary
}
} : {
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 6px 20px rgba(168, 85, 247, 0.2)',
border: '1px solid rgba(168, 85, 247, 0.4)'
}
})
}}>
<TypographyComponent variant="h6" sx={{
fontWeight: 700,
color: terminalTheme ? terminalColors.text : '#ffffff',
textShadow: terminalTheme ? 'none' : '0 2px 4px rgba(0,0,0,0.3)',
mb: 0.5
}}>
{formatCurrency(costPer1KTokens)}
</TypographyComponent>
<TypographyComponent variant="body2" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.7rem'
}}>
Cost/1K Tokens
</TypographyComponent>
</Box>
</Tooltip>
</Grid>
)}
{/* Cost Efficiency Score */}
<Grid item xs={6} sm={4}>
<Tooltip
title={
<Box>
<TypographyComponent variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
Cost Efficiency Indicator
</TypographyComponent>
<TypographyComponent variant="body2" sx={{ opacity: 0.9 }}>
Based on average cost per call and token usage
</TypographyComponent>
<TypographyComponent variant="caption" sx={{ opacity: 0.7, mt: 1, display: 'block' }}>
Lower cost per call = Higher efficiency
</TypographyComponent>
</Box>
}
arrow
placement="top"
>
<Box sx={{
textAlign: 'center',
p: 2,
...(terminalTheme ? {
backgroundColor: terminalColors.backgroundLight,
borderRadius: 3,
border: `1px solid ${terminalColors.border}`,
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: `0 0 10px ${terminalColors.border}30`,
borderColor: terminalColors.secondary
}
} : {
background: 'linear-gradient(135deg, rgba(34, 197, 94, 0.12) 0%, rgba(22, 163, 74, 0.08) 100%)',
borderRadius: 3,
border: '1px solid rgba(34, 197, 94, 0.25)',
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: '0 6px 20px rgba(34, 197, 94, 0.2)',
border: '1px solid rgba(34, 197, 94, 0.4)'
}
})
}}>
<TypographyComponent variant="h6" sx={{
fontWeight: 700,
color: terminalTheme ? terminalColors.text : '#ffffff',
textShadow: terminalTheme ? 'none' : '0 2px 4px rgba(0,0,0,0.3)',
mb: 0.5
}}>
{getEfficiencyScore()}
</TypographyComponent>
<TypographyComponent variant="body2" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.7rem'
}}>
Efficiency
</TypographyComponent>
</Box>
</Tooltip>
</Grid>
</Grid>
);
};

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { Box, Tooltip, IconButton } from '@mui/material';
import { RefreshCw } from 'lucide-react';
import { TerminalTypography } from '../../../SchedulerDashboard/terminalTheme';
import { terminalColors } from '../../../SchedulerDashboard/terminalTheme';
interface DashboardHeaderProps {
lastRefreshTime: Date | null;
onRefresh: () => void;
loading: boolean;
terminalTheme?: boolean;
TypographyComponent: typeof TerminalTypography | React.ComponentType<any>;
}
/**
* DashboardHeader - Displays last refresh time and refresh button
*/
export const DashboardHeader: React.FC<DashboardHeaderProps> = ({
lastRefreshTime,
onRefresh,
loading,
terminalTheme = false,
TypographyComponent
}) => {
return (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
{lastRefreshTime && (
<Tooltip title={`Data last refreshed at ${lastRefreshTime.toLocaleTimeString()}`}>
<TypographyComponent
variant="caption"
sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.6)',
fontSize: '0.7rem',
fontStyle: 'italic'
}}
>
Last updated: {lastRefreshTime.toLocaleTimeString()}
</TypographyComponent>
</Tooltip>
)}
<Tooltip title="Refresh billing data">
<IconButton
size="small"
onClick={onRefresh}
disabled={loading}
sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)',
'&:hover': {
color: terminalTheme ? terminalColors.text : '#ffffff',
backgroundColor: terminalTheme ? terminalColors.backgroundLight : 'rgba(255,255,255,0.1)'
}
}}
>
<RefreshCw size={14} className={loading ? 'animate-spin' : ''} />
</IconButton>
</Tooltip>
</Box>
);
};

View File

@@ -0,0 +1,281 @@
import React from 'react';
import { Grid, Box, Tooltip, Typography } from '@mui/material';
import { motion } from 'framer-motion';
import { CheckCircle } from 'lucide-react';
import { TerminalTypography } from '../../../SchedulerDashboard/terminalTheme';
import { terminalColors } from '../../../SchedulerDashboard/terminalTheme';
import { MetricCard } from './MetricCard';
import { formatCurrency, formatNumber } from '../utils/formatting';
import { DashboardData } from '../../../../types/billing';
import { SystemHealth } from '../../../../types/monitoring';
interface MainMetricsGridProps {
currentUsage: DashboardData['current_usage'];
systemHealth: SystemHealth | null;
healthError: string | null;
sparklineData: {
cost: Array<{ date: string; value: number }>;
calls: Array<{ date: string; value: number }>;
tokens: Array<{ date: string; value: number }>;
};
terminalTheme?: boolean;
TypographyComponent: typeof TerminalTypography | React.ComponentType<any>;
}
/**
* MainMetricsGrid - Displays the 4 main metric cards (Cost, Calls, Tokens, System Health)
*/
export const MainMetricsGrid: React.FC<MainMetricsGridProps> = ({
currentUsage,
systemHealth,
healthError,
sparklineData,
terminalTheme = false,
TypographyComponent
}) => {
return (
<Grid container spacing={2} sx={{ mb: 2 }}>
{/* Total Cost */}
<Grid item xs={6} sm={3}>
<MetricCard
title="Total Cost"
value={currentUsage.total_cost}
formatValue={formatCurrency}
decimals={4}
tooltip={
<Box>
<TypographyComponent variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
Monthly API Usage Cost
</TypographyComponent>
<TypographyComponent variant="body2" sx={{ opacity: 0.9 }}>
Total spending across all AI providers this month
</TypographyComponent>
<TypographyComponent variant="caption" sx={{ opacity: 0.7, mt: 1, display: 'block' }}>
Includes: Gemini, OpenAI, Anthropic, Mistral
</TypographyComponent>
</Box>
}
sparklineData={sparklineData.cost}
sparklineColor={terminalTheme ? terminalColors.success : '#4ade80'}
sparklineFormatValue={formatCurrency}
sparklineLabel="Cost"
gradientColors={{
start: 'rgba(74, 222, 128, 0.12)',
end: 'rgba(34, 197, 94, 0.08)',
border: 'rgba(74, 222, 128, 0.25)',
hoverBorder: 'rgba(74, 222, 128, 0.4)',
topBar: 'linear-gradient(90deg, #4ade80, #22c55e)'
}}
animationDelay={0}
terminalTheme={terminalTheme}
TypographyComponent={TypographyComponent}
/>
</Grid>
{/* API Calls */}
<Grid item xs={6} sm={3}>
<MetricCard
title="API Calls"
value={currentUsage.total_calls}
formatValue={formatNumber}
tooltip={
<Box>
<TypographyComponent variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
API Request Volume
</TypographyComponent>
<TypographyComponent variant="body2" sx={{ opacity: 0.9 }}>
Total number of AI API requests made this month
</TypographyComponent>
<TypographyComponent variant="caption" sx={{ opacity: 0.7, mt: 1, display: 'block' }}>
Each request generates content, analyzes data, or processes information
</TypographyComponent>
</Box>
}
sparklineData={sparklineData.calls}
sparklineColor={terminalTheme ? terminalColors.secondary : '#3b82f6'}
sparklineFormatValue={formatNumber}
sparklineLabel="API Calls"
gradientColors={{
start: 'rgba(59, 130, 246, 0.12)',
end: 'rgba(37, 99, 235, 0.08)',
border: 'rgba(59, 130, 246, 0.25)',
hoverBorder: 'rgba(59, 130, 246, 0.4)',
topBar: 'linear-gradient(90deg, #3b82f6, #2563eb)'
}}
animationDelay={0.1}
terminalTheme={terminalTheme}
TypographyComponent={TypographyComponent}
/>
</Grid>
{/* Tokens */}
<Grid item xs={6} sm={3}>
<MetricCard
title="Tokens"
value={currentUsage.total_tokens / 1000}
formatValue={(n) => `${n.toFixed(1)}k`}
decimals={1}
tooltip={
<Box>
<TypographyComponent variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
AI Processing Units
</TypographyComponent>
<TypographyComponent variant="body2" sx={{ opacity: 0.9 }}>
Total tokens processed by AI models this month
</TypographyComponent>
<TypographyComponent variant="caption" sx={{ opacity: 0.7, mt: 1, display: 'block' }}>
Tokens represent words, characters, and data processed by AI
</TypographyComponent>
</Box>
}
sparklineData={sparklineData.tokens}
sparklineColor={terminalTheme ? terminalColors.warning : '#a855f7'}
sparklineFormatValue={(n) => `${n.toFixed(1)}k`}
sparklineLabel="Tokens (k)"
gradientColors={{
start: 'rgba(168, 85, 247, 0.12)',
end: 'rgba(147, 51, 234, 0.08)',
border: 'rgba(168, 85, 247, 0.25)',
hoverBorder: 'rgba(168, 85, 247, 0.4)',
topBar: 'linear-gradient(90deg, #a855f7, #9333ea)'
}}
animationDelay={0.2}
terminalTheme={terminalTheme}
TypographyComponent={TypographyComponent}
/>
</Grid>
{/* System Health */}
<Grid item xs={6} sm={3}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.4, ease: "easeOut" }}
>
<Tooltip
title={
<Box>
<TypographyComponent variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
System Performance Status
</TypographyComponent>
<TypographyComponent variant="body2" sx={{ opacity: 0.9 }}>
Real-time monitoring of API services and system performance
</TypographyComponent>
{healthError && (
<TypographyComponent variant="caption" sx={{ color: '#ff9800', mt: 1, display: 'block', fontWeight: 'bold' }}>
Showing last known values - Unable to fetch latest data
</TypographyComponent>
)}
<TypographyComponent variant="caption" sx={{ opacity: 0.7, mt: 1, display: 'block' }}>
{systemHealth?.status === 'healthy'
? 'All systems operational and responding normally'
: systemHealth?.status === 'warning'
? 'Some services may be experiencing issues'
: systemHealth?.status === 'critical'
? 'Critical issues detected'
: 'Status unknown'
}
</TypographyComponent>
{systemHealth?.timestamp && (
<TypographyComponent variant="caption" sx={{ opacity: 0.6, mt: 0.5, display: 'block', fontSize: '0.65rem' }}>
Last updated: {new Date(systemHealth.timestamp).toLocaleTimeString()}
</TypographyComponent>
)}
</Box>
}
arrow
placement="top"
>
<Box sx={{
textAlign: 'center',
p: 2.5,
...(terminalTheme ? {
backgroundColor: terminalColors.backgroundLight,
borderRadius: 3,
border: `1px solid ${systemHealth?.status === 'healthy' ? terminalColors.success : terminalColors.error}`,
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: `0 0 15px ${systemHealth?.status === 'healthy' ? terminalColors.success : terminalColors.error}40`,
borderColor: systemHealth?.status === 'healthy' ? terminalColors.secondary : terminalColors.error
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: systemHealth?.status === 'healthy' ? terminalColors.success : terminalColors.error,
zIndex: 1
}
} : {
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={terminalTheme
? (systemHealth?.status === 'healthy' ? terminalColors.success : terminalColors.error)
: (systemHealth?.status === 'healthy' ? '#4ade80' : '#ff6b6b')
} />
<TypographyComponent variant="body1" sx={{
color: terminalTheme
? (systemHealth?.status === 'healthy' ? terminalColors.success : terminalColors.error)
: (systemHealth?.status === 'healthy' ? '#4ade80' : '#ff6b6b'),
fontWeight: 700,
textTransform: 'capitalize',
textShadow: terminalTheme ? 'none' : '0 2px 4px rgba(0,0,0,0.3)'
}}>
{systemHealth?.status || 'Unknown'}
</TypographyComponent>
</Box>
<TypographyComponent variant="body2" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.75rem'
}}>
System Health
</TypographyComponent>
</Box>
</Tooltip>
</motion.div>
</Grid>
</Grid>
);
};

View File

@@ -0,0 +1,145 @@
import React from 'react';
import { Box, Tooltip } from '@mui/material';
import { motion } from 'framer-motion';
import { TerminalTypography } from '../../../SchedulerDashboard/terminalTheme';
import { terminalColors } from '../../../SchedulerDashboard/terminalTheme';
import { AnimatedNumber } from '../../../shared/AnimatedNumber';
import { MiniSparkline } from '../../../shared/MiniSparkline';
interface MetricCardProps {
title: string;
value: number;
formatValue: (n: number) => string;
decimals?: number;
tooltip: React.ReactNode;
sparklineData?: Array<{ date: string; value: number }>;
sparklineColor?: string;
sparklineFormatValue?: (n: number) => string;
sparklineLabel?: string;
gradientColors: {
start: string;
end: string;
border: string;
hoverBorder: string;
topBar: string;
};
animationDelay?: number;
terminalTheme?: boolean;
TypographyComponent: typeof TerminalTypography | React.ComponentType<any>;
}
/**
* MetricCard - Reusable metric card component for displaying key metrics
*/
export const MetricCard: React.FC<MetricCardProps> = ({
title,
value,
formatValue,
decimals = 0,
tooltip,
sparklineData,
sparklineColor,
sparklineFormatValue,
sparklineLabel,
gradientColors,
animationDelay = 0,
terminalTheme = false,
TypographyComponent
}) => {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: animationDelay, duration: 0.4, ease: "easeOut" }}
>
<Tooltip
title={tooltip}
arrow
placement="top"
>
<Box sx={{
textAlign: 'center',
p: 2.5,
...(terminalTheme ? {
backgroundColor: terminalColors.backgroundLight,
borderRadius: 3,
border: `1px solid ${terminalColors.border}`,
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: `0 0 15px ${terminalColors.border}40`,
borderColor: terminalColors.secondary
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: terminalColors.border,
zIndex: 1
}
} : {
background: `linear-gradient(135deg, ${gradientColors.start} 0%, ${gradientColors.end} 100%)`,
borderRadius: 3,
border: `1px solid ${gradientColors.border}`,
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: `0 8px 25px ${gradientColors.start.replace('0.12', '0.2')}`,
border: `1px solid ${gradientColors.hoverBorder}`
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: gradientColors.topBar,
zIndex: 1
}
})
}}>
<TypographyComponent variant="h5" sx={{
fontWeight: 800,
color: terminalTheme ? terminalColors.text : '#ffffff',
textShadow: terminalTheme ? 'none' : '0 2px 8px rgba(0,0,0,0.4)',
mb: 0.5
}}>
<AnimatedNumber
value={value}
format={formatValue}
decimals={decimals}
/>
</TypographyComponent>
<TypographyComponent variant="body2" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.75rem'
}}>
{title}
</TypographyComponent>
{sparklineData && sparklineData.length > 0 && sparklineColor && (
<MiniSparkline
data={sparklineData}
color={sparklineColor}
height={50}
formatValue={sparklineFormatValue || formatValue}
label={sparklineLabel || title}
/>
)}
</Box>
</Tooltip>
</motion.div>
);
};

View File

@@ -0,0 +1,260 @@
import React from 'react';
import { Box, Tooltip } from '@mui/material';
import { AlertTriangle } from 'lucide-react';
import { TerminalTypography } from '../../../SchedulerDashboard/terminalTheme';
import { terminalColors } from '../../../SchedulerDashboard/terminalTheme';
import { AnimatedProgressBar } from '../../../shared/AnimatedProgressBar';
import ProviderCostComparison from '../../ProviderCostComparison';
import { formatCurrency } from '../utils/formatting';
import { DashboardData } from '../../../../types/billing';
interface MonthlyBudgetUsageProps {
currentUsage: DashboardData['current_usage'];
limits: DashboardData['limits'];
terminalTheme?: boolean;
TypographyComponent: typeof TerminalTypography | React.ComponentType<any>;
}
/**
* MonthlyBudgetUsage - Displays monthly budget usage with progress bar and provider breakdown
*/
export const MonthlyBudgetUsage: React.FC<MonthlyBudgetUsageProps> = ({
currentUsage,
limits,
terminalTheme = false,
TypographyComponent
}) => {
if (limits.limits.monthly_cost <= 0) return null;
const usagePercentage = (currentUsage.total_cost / limits.limits.monthly_cost) * 100;
const remainingBudget = limits.limits.monthly_cost - currentUsage.total_cost;
const daysInMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate();
const currentDay = new Date().getDate();
const daysRemaining = daysInMonth - currentDay;
const avgDailyCost = currentUsage.total_cost / currentDay;
const estimatedDaysUntilExhaustion = avgDailyCost > 0 ? Math.ceil(remainingBudget / avgDailyCost) : daysRemaining;
// Determine warning level
const isCritical = usagePercentage >= 95;
const isWarning = usagePercentage >= 80;
const isModerate = usagePercentage >= 50;
return (
<Box sx={{ mb: 3 }}>
{/* Enhanced Warning Banner */}
{(isCritical || isWarning || isModerate) && (
<Box sx={{
mb: 2,
p: 2.5,
...(terminalTheme ? {
backgroundColor: isCritical
? terminalColors.error + '20'
: isWarning
? terminalColors.warning + '20'
: terminalColors.secondary + '20',
borderRadius: 3,
border: `2px solid ${isCritical
? terminalColors.error
: isWarning
? terminalColors.warning
: terminalColors.secondary}`,
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '4px',
background: isCritical
? terminalColors.error
: isWarning
? terminalColors.warning
: terminalColors.secondary,
zIndex: 1
}
} : {
background: isCritical
? 'linear-gradient(135deg, rgba(239, 68, 68, 0.2) 0%, rgba(220, 38, 38, 0.15) 100%)'
: isWarning
? 'linear-gradient(135deg, rgba(245, 158, 11, 0.2) 0%, rgba(217, 119, 6, 0.15) 100%)'
: 'linear-gradient(135deg, rgba(59, 130, 246, 0.2) 0%, rgba(37, 99, 235, 0.15) 100%)',
borderRadius: 3,
border: `2px solid ${isCritical
? 'rgba(239, 68, 68, 0.5)'
: isWarning
? 'rgba(245, 158, 11, 0.5)'
: 'rgba(59, 130, 246, 0.5)'}`,
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '4px',
background: isCritical
? 'linear-gradient(90deg, #ef4444, #dc2626)'
: isWarning
? 'linear-gradient(90deg, #f59e0b, #d97706)'
: 'linear-gradient(90deg, #3b82f6, #2563eb)',
zIndex: 1
}
})
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 1 }}>
<AlertTriangle
size={24}
color={terminalTheme
? (isCritical ? terminalColors.error : isWarning ? terminalColors.warning : terminalColors.secondary)
: (isCritical ? '#ef4444' : isWarning ? '#f59e0b' : '#3b82f6')
}
/>
<Box sx={{ flex: 1 }}>
<TypographyComponent variant="subtitle1" sx={{
fontWeight: 700,
color: terminalTheme
? (isCritical ? terminalColors.error : isWarning ? terminalColors.warning : terminalColors.text)
: (isCritical ? '#ef4444' : isWarning ? '#f59e0b' : '#ffffff'),
mb: 0.5
}}>
{isCritical
? '🚨 Critical: Approaching Budget Limit'
: isWarning
? '⚠️ Warning: High Budget Usage'
: ' Notice: 50% of Budget Used'
}
</TypographyComponent>
<TypographyComponent variant="body2" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.9)',
mb: 1
}}>
{isCritical
? `You've used ${usagePercentage.toFixed(1)}% of your monthly budget. Only ${formatCurrency(remainingBudget)} remaining.`
: isWarning
? `You've used ${usagePercentage.toFixed(1)}% of your monthly budget. ${formatCurrency(remainingBudget)} remaining.`
: `You've used ${usagePercentage.toFixed(1)}% of your monthly budget. ${formatCurrency(remainingBudget)} remaining.`
}
</TypographyComponent>
{avgDailyCost > 0 && estimatedDaysUntilExhaustion > 0 && (
<TypographyComponent variant="caption" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.8)',
display: 'block',
fontStyle: 'italic'
}}>
{estimatedDaysUntilExhaustion <= daysRemaining
? `At current spending rate, budget will be exhausted in ~${estimatedDaysUntilExhaustion} day${estimatedDaysUntilExhaustion !== 1 ? 's' : ''}.`
: `Current spending rate is sustainable for the remainder of the month.`
}
</TypographyComponent>
)}
</Box>
</Box>
</Box>
)}
{/* Budget Progress Bar */}
<Box sx={{
p: 2.5,
...(terminalTheme ? {
backgroundColor: terminalColors.backgroundLight,
borderRadius: 3,
border: `1px solid ${terminalColors.border}`
} : {
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>
<TypographyComponent variant="subtitle2" sx={{
color: terminalTheme ? terminalColors.text : '#ffffff',
fontWeight: 600,
mb: 0.5
}}>
Monthly Budget Usage
</TypographyComponent>
<TypographyComponent variant="caption" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)',
display: 'block'
}}>
Track your AI spending against monthly limits
</TypographyComponent>
</Box>
<Box sx={{ textAlign: 'right' }}>
<TypographyComponent variant="h6" sx={{
color: terminalTheme ? terminalColors.text : '#ffffff',
fontWeight: 'bold',
textShadow: terminalTheme ? 'none' : '0 2px 4px rgba(0,0,0,0.3)'
}}>
{formatCurrency(currentUsage.total_cost)}
</TypographyComponent>
<TypographyComponent variant="caption" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)',
display: 'block'
}}>
of {formatCurrency(limits.limits.monthly_cost)}
</TypographyComponent>
</Box>
</Box>
<AnimatedProgressBar
value={Math.min(usagePercentage, 100)}
height={10}
color={terminalTheme
? (isCritical
? terminalColors.error
: isWarning
? terminalColors.warning
: isModerate
? terminalColors.secondary
: terminalColors.success)
: (isCritical
? '#ef4444'
: isWarning
? '#f59e0b'
: isModerate
? '#3b82f6'
: '#4ade80')
}
showPercentage={false}
/>
{/* Provider Cost Comparison Chart */}
{currentUsage.provider_breakdown && Object.keys(currentUsage.provider_breakdown).length > 0 && (
<ProviderCostComparison
providerBreakdown={currentUsage.provider_breakdown}
terminalTheme={terminalTheme}
terminalColors={terminalColors}
/>
)}
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 1.5 }}>
<TypographyComponent variant="caption" sx={{
color: terminalTheme
? (isCritical ? terminalColors.error : isWarning ? terminalColors.warning : terminalColors.textSecondary)
: (isCritical ? '#ef4444' : isWarning ? '#f59e0b' : 'rgba(255,255,255,0.7)'),
fontWeight: (isCritical || isWarning) ? 600 : 400
}}>
{isCritical
? '🚨 Critical: Approaching limit'
: isWarning
? '⚠️ Warning: High usage'
: isModerate
? ' Moderate usage'
: '✅ Within budget'
}
</TypographyComponent>
<TypographyComponent variant="caption" sx={{
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)',
fontWeight: 600
}}>
{usagePercentage.toFixed(1)}% used {formatCurrency(remainingBudget)} remaining
</TypographyComponent>
</Box>
</Box>
</Box>
);
};

View File

@@ -0,0 +1,121 @@
import React, { useMemo } from 'react';
import { Box } from '@mui/material';
import { motion } from 'framer-motion';
import { TerminalTypography } from '../../../SchedulerDashboard/terminalTheme';
import { terminalColors } from '../../../SchedulerDashboard/terminalTheme';
import { UsageLimitRing } from '../../../shared/UsageLimitRing';
import { DashboardData } from '../../../../types/billing';
interface UsageLimitRingsProps {
currentUsage: DashboardData['current_usage'];
limits: DashboardData['limits'];
terminalTheme?: boolean;
TypographyComponent: typeof TerminalTypography | React.ComponentType<any>;
}
/**
* UsageLimitRings - Displays circular progress rings for key usage limits
*/
export const UsageLimitRings: React.FC<UsageLimitRingsProps> = ({
currentUsage,
limits,
terminalTheme = false,
TypographyComponent
}) => {
// Calculate image calls - check multiple possible sources
const imageCalls = useMemo(() => {
// Primary: provider_breakdown.image
const imageFromBreakdown = currentUsage.provider_breakdown?.image?.calls ?? 0;
const imageEditFromBreakdown = currentUsage.provider_breakdown?.image_edit?.calls ?? 0;
// Fallback: Check if there's a stability key (legacy)
const stabilityFromBreakdown = currentUsage.provider_breakdown?.stability?.calls ?? 0;
// Sum all image-related calls
const total = imageFromBreakdown + imageEditFromBreakdown + stabilityFromBreakdown;
// Debug logging (can be removed in production)
if (total > 0 || imageFromBreakdown > 0 || stabilityFromBreakdown > 0) {
console.log('[UsageLimitRings] Image calls calculation:', {
image: imageFromBreakdown,
image_edit: imageEditFromBreakdown,
stability: stabilityFromBreakdown,
total,
provider_breakdown_keys: Object.keys(currentUsage.provider_breakdown || {})
});
}
return total;
}, [currentUsage.provider_breakdown]);
// Calculate video calls - check multiple possible sources
const videoCalls = useMemo(() => {
// Primary: provider_breakdown.video
const videoFromBreakdown = currentUsage.provider_breakdown?.video?.calls ?? 0;
// Debug logging (can be removed in production)
if (videoFromBreakdown > 0) {
console.log('[UsageLimitRings] Video calls calculation:', {
video: videoFromBreakdown,
provider_breakdown_keys: Object.keys(currentUsage.provider_breakdown || {})
});
}
return videoFromBreakdown;
}, [currentUsage.provider_breakdown]);
const keyLimits = [
{
label: 'AI Calls',
used: currentUsage.total_calls,
limit: limits.limits.gemini_calls || limits.limits.openai_calls || 50,
color: '#3b82f6'
},
{
label: 'Images',
used: imageCalls,
limit: limits.limits.stability_calls || 50,
color: '#a855f7'
},
{
label: 'Videos',
used: videoCalls,
limit: limits.limits.video_calls || 30,
color: '#ec4899'
}
].filter(item => item.limit > 0);
if (keyLimits.length === 0) return null;
return (
<Box sx={{ mb: 3 }}>
<TypographyComponent variant="subtitle2" sx={{
fontWeight: 600,
mb: 2,
color: terminalTheme ? terminalColors.text : 'rgba(255,255,255,0.9)'
}}>
Usage Limits Overview
</TypographyComponent>
<Box sx={{ display: 'flex', justifyContent: 'space-around', flexWrap: 'wrap', gap: 2 }}>
{keyLimits.map((item, index) => (
<motion.div
key={item.label}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: index * 0.1, duration: 0.4 }}
>
<UsageLimitRing
used={item.used}
limit={item.limit}
label={item.label}
color={item.color}
size={100}
terminalTheme={terminalTheme}
terminalColors={terminalColors}
/>
</motion.div>
))}
</Box>
</Box>
);
};

View File

@@ -0,0 +1,197 @@
import { useState, useEffect, useMemo } from 'react';
import { DashboardData } from '../../../../types/billing';
import { SystemHealth } from '../../../../types/monitoring';
import { billingService } from '../../../../services/billingService';
import { monitoringService } from '../../../../services/monitoringService';
import { onApiEvent } from '../../../../utils/apiEvents';
import { showToastNotification } from '../../../../utils/toastNotifications';
interface UseCompactBillingDataReturn {
dashboardData: DashboardData | null;
systemHealth: SystemHealth | null;
loading: boolean;
error: string | null;
lastRefreshTime: Date | null;
healthError: string | null;
sparklineData: {
cost: Array<{ date: string; value: number }>;
calls: Array<{ date: string; value: number }>;
tokens: Array<{ date: string; value: number }>;
};
refresh: (showSuccessToast?: boolean) => Promise<void>;
}
/**
* Custom hook for managing CompactBillingDashboard data fetching and state
*/
export const useCompactBillingData = (userId?: string): UseCompactBillingDataReturn => {
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 [lastRefreshTime, setLastRefreshTime] = useState<Date | null>(null);
const [healthError, setHealthError] = useState<string | null>(null);
const fetchData = async (showSuccessToast: boolean = false) => {
try {
setLoading(true);
setError(null);
// Use Promise.allSettled to prevent health check timeout from blocking dashboard
const results = await Promise.allSettled([
billingService.getDashboardData(userId),
monitoringService.getSystemHealth()
]);
// Handle billing data (required)
if (results[0].status === 'fulfilled') {
setDashboardData(results[0].value);
setLastRefreshTime(new Date());
setError(null); // Clear any previous errors
} else {
// Billing data is critical - show error
const errorMessage = results[0].reason instanceof Error
? results[0].reason.message
: 'Failed to fetch data';
setError(errorMessage);
showToastNotification(
`Unable to fetch latest billing data: ${errorMessage}. Showing last known values.`,
'error',
{ duration: 7000 }
);
setLoading(false);
return;
}
// Handle health data (optional - don't block dashboard if it fails)
if (results[1].status === 'fulfilled') {
setSystemHealth(results[1].value);
setHealthError(null); // Clear health error on success
} else {
// Health check failed - keep last successful value, show error toast
const healthErrorMessage = results[1].reason instanceof Error
? results[1].reason.message
: 'System health check timed out or failed';
setHealthError(healthErrorMessage);
showToastNotification(
`Unable to fetch latest system health data: ${healthErrorMessage}. Showing last known values.`,
'warning',
{ duration: 6000 }
);
// Don't update systemHealth - keep last successful value
// Only set to null if we never had a successful fetch
if (!systemHealth) {
setSystemHealth(null);
}
}
// Show success toast only if explicitly requested (user-initiated refresh)
if (showSuccessToast && results[0].status === 'fulfilled' && results[1].status === 'fulfilled') {
showToastNotification(
'Billing data refreshed successfully',
'success',
{ duration: 3000 }
);
} else if (showSuccessToast && results[0].status === 'fulfilled' && results[1].status === 'rejected') {
showToastNotification(
'Billing data refreshed, but system health check failed',
'warning',
{ duration: 4000 }
);
}
} catch (err) {
// Fallback error handling (shouldn't reach here with allSettled)
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch data';
setError(errorMessage);
showToastNotification(errorMessage, 'error', { duration: 5000 });
} finally {
setLoading(false);
}
};
// Initial fetch
useEffect(() => {
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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;
Promise.allSettled([billingService.getDashboardData(userId), monitoringService.getSystemHealth()])
.then((results) => {
if (results[0].status === 'fulfilled') {
setDashboardData(results[0].value);
setLastRefreshTime(new Date());
setError(null);
}
if (results[1].status === 'fulfilled') {
setSystemHealth(results[1].value);
setHealthError(null);
} else {
// Keep last successful health value, don't set fake defaults
const healthErrorMessage = results[1].reason instanceof Error
? results[1].reason.message
: 'System health check failed';
setHealthError(healthErrorMessage);
// Don't update systemHealth - keep last successful value
}
})
.catch(() => {/* ignore */});
});
return unsubscribe;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Prepare sparkline data for last 7 days (or available data)
const sparklineData = useMemo(() => {
if (!dashboardData || !dashboardData.trends || !dashboardData.trends.periods || dashboardData.trends.periods.length === 0) {
return {
cost: [],
calls: [],
tokens: []
};
}
const { trends } = dashboardData;
// Get last 7 periods (or all if less than 7)
const last7Periods = trends.periods.slice(-7);
const last7Costs = trends.total_cost.slice(-7);
const last7Calls = trends.total_calls.slice(-7);
const last7Tokens = trends.total_tokens.slice(-7);
return {
cost: last7Periods.map((period, index) => ({
date: period,
value: last7Costs[index] || 0
})),
calls: last7Periods.map((period, index) => ({
date: period,
value: last7Calls[index] || 0
})),
tokens: last7Periods.map((period, index) => ({
date: period,
value: (last7Tokens[index] || 0) / 1000 // Convert to thousands
}))
};
}, [dashboardData]);
return {
dashboardData,
systemHealth,
loading,
error,
lastRefreshTime,
healthError,
sparklineData,
refresh: fetchData
};
};

View File

@@ -0,0 +1,216 @@
import React from 'react';
import { Card, CardContent, Typography, Chip } from '@mui/material';
import { motion } from 'framer-motion';
import {
TerminalCard,
TerminalCardContent,
TerminalTypography,
TerminalChip,
terminalColors
} from '../../SchedulerDashboard/terminalTheme';
// Hooks
import { useCompactBillingData } from './hooks/useCompactBillingData';
// Components
import { DashboardHeader } from './components/DashboardHeader';
import { MainMetricsGrid } from './components/MainMetricsGrid';
import { CostEfficiencyMetrics } from './components/CostEfficiencyMetrics';
import { UsageLimitRings } from './components/UsageLimitRings';
import { MonthlyBudgetUsage } from './components/MonthlyBudgetUsage';
import { AlertsSection } from './components/AlertsSection';
interface CompactBillingDashboardProps {
userId?: string;
terminalTheme?: boolean;
}
/**
* CompactBillingDashboard - Main orchestrator component
*
* Refactored from monolithic component into modular structure:
* - Data fetching: useCompactBillingData hook
* - UI Components: Separated into focused, reusable components
* - Utils: Formatting utilities extracted
*/
const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({
userId,
terminalTheme = false
}) => {
// Conditional component selection based on terminal theme
const CardComponent = terminalTheme ? TerminalCard : Card;
const CardContentComponent = terminalTheme ? TerminalCardContent : CardContent;
const TypographyComponent = terminalTheme ? TerminalTypography : Typography;
const ChipComponent = terminalTheme ? TerminalChip : Chip;
// Data fetching hook
const {
dashboardData,
systemHealth,
loading,
error,
lastRefreshTime,
healthError,
sparklineData,
refresh
} = useCompactBillingData(userId);
// Loading state
if (loading && !dashboardData) {
const loadingCardStyles = terminalTheme
? {
backgroundColor: terminalColors.background,
border: `1px solid ${terminalColors.border}`,
borderRadius: 3
}
: {
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
};
return (
<CardComponent sx={loadingCardStyles}>
<CardContentComponent sx={{ textAlign: 'center', py: 4 }}>
<TypographyComponent sx={{ color: terminalTheme ? terminalColors.text : 'rgba(255,255,255,0.8)' }}>
Loading billing data...
</TypographyComponent>
</CardContentComponent>
</CardComponent>
);
}
// Error state
if (error) {
const errorCardStyles = terminalTheme
? {
backgroundColor: terminalColors.background,
border: `1px solid ${terminalColors.error}`,
borderRadius: 3
}
: {
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
};
return (
<CardComponent sx={errorCardStyles}>
<CardContentComponent sx={{ textAlign: 'center', py: 4 }}>
<TypographyComponent sx={{ color: terminalTheme ? terminalColors.error : '#ff6b6b' }}>
Error: {error}
</TypographyComponent>
</CardContentComponent>
</CardComponent>
);
}
if (!dashboardData) return null;
const { current_usage, limits, alerts } = dashboardData;
const mainCardStyles = terminalTheme
? {
backgroundColor: terminalColors.background,
border: `1px solid ${terminalColors.border}`,
borderRadius: 4,
position: 'relative' as const,
overflow: 'hidden' as const,
boxShadow: '0 0 15px rgba(0, 255, 0, 0.2)',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '1px',
background: `linear-gradient(90deg, transparent, ${terminalColors.border}, transparent)`,
zIndex: 1
}
}
: {
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' as const,
overflow: 'hidden' as const,
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
}
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<CardComponent sx={mainCardStyles}>
<CardContentComponent sx={{ pt: 2 }}>
{/* Header */}
<DashboardHeader
lastRefreshTime={lastRefreshTime}
onRefresh={() => refresh(true)}
loading={loading}
terminalTheme={terminalTheme}
TypographyComponent={TypographyComponent}
/>
{/* Main Metrics Grid */}
<MainMetricsGrid
currentUsage={current_usage}
systemHealth={systemHealth}
healthError={healthError}
sparklineData={sparklineData}
terminalTheme={terminalTheme}
TypographyComponent={TypographyComponent}
/>
{/* Cost Efficiency Metrics */}
<CostEfficiencyMetrics
currentUsage={current_usage}
terminalTheme={terminalTheme}
TypographyComponent={TypographyComponent}
/>
{/* Usage Limit Rings */}
<UsageLimitRings
currentUsage={current_usage}
limits={limits}
terminalTheme={terminalTheme}
TypographyComponent={TypographyComponent}
/>
{/* Monthly Budget Usage */}
<MonthlyBudgetUsage
currentUsage={current_usage}
limits={limits}
terminalTheme={terminalTheme}
TypographyComponent={TypographyComponent}
/>
{/* Alerts Section */}
<AlertsSection
alerts={alerts}
terminalTheme={terminalTheme}
TypographyComponent={TypographyComponent}
ChipComponent={ChipComponent}
/>
</CardContentComponent>
</CardComponent>
</motion.div>
);
};
export default CompactBillingDashboard;

View File

@@ -0,0 +1,7 @@
/**
* Formatting utilities for CompactBillingDashboard
*/
export const formatCurrency = (amount: number): string => `$${amount.toFixed(4)}`;
export const formatNumber = (num: number): string => num.toLocaleString();

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import {
Card,
CardContent,
@@ -10,6 +10,7 @@ import {
Accordion,
AccordionSummary,
AccordionDetails,
CircularProgress,
} from '@mui/material';
import { ExpandMore } from '@mui/icons-material';
import { motion } from 'framer-motion';
@@ -20,11 +21,15 @@ import {
Code,
Database,
FileText,
BarChart3
BarChart3,
RefreshCw
} from 'lucide-react';
// Types
import { ProviderBreakdown } from '../../types/billing';
import { ProviderBreakdown, APIPricing } from '../../types/billing';
// Services
import { billingService } from '../../services/billingService';
interface ComprehensiveAPIBreakdownProps {
providerBreakdown: ProviderBreakdown;
@@ -151,10 +156,96 @@ const ComprehensiveAPIBreakdown: React.FC<ComprehensiveAPIBreakdownProps> = ({
providerBreakdown,
totalCost
}) => {
const [pricing, setPricing] = useState<APIPricing[]>([]);
const [loadingPricing, setLoadingPricing] = useState(true);
const [pricingError, setPricingError] = useState<string | null>(null);
// Fetch dynamic pricing on mount
useEffect(() => {
const fetchPricing = async () => {
try {
setLoadingPricing(true);
setPricingError(null);
const pricingData = await billingService.getAPIPricing();
setPricing(pricingData);
} catch (err) {
console.error('[ComprehensiveAPIBreakdown] Error fetching pricing:', err);
setPricingError(err instanceof Error ? err.message : 'Failed to fetch pricing');
} finally {
setLoadingPricing(false);
}
};
fetchPricing();
}, []);
// Get active providers from breakdown
const activeProviders = Object.entries(providerBreakdown)
.filter(([_, data]) => data.cost > 0)
.map(([provider, data]) => ({ provider, ...data }));
.filter(([_, data]) => data && data.cost > 0)
.map(([provider, data]) => ({
provider,
cost: data?.cost ?? 0,
calls: data?.calls ?? 0,
tokens: data?.tokens ?? 0
}));
// Helper function to format pricing from API or fallback to static
const getPricingDisplay = (apiName: string, fallbackPricing: string): string => {
if (loadingPricing) {
return 'Loading pricing...';
}
if (pricingError) {
return fallbackPricing; // Fallback to static pricing on error
}
// Find matching pricing by provider name
const apiPricing = pricing.find(p =>
p.provider.toLowerCase() === apiName.toLowerCase() ||
p.model_name.toLowerCase().includes(apiName.toLowerCase())
);
if (apiPricing) {
// Format pricing based on what's available
const parts: string[] = [];
if (apiPricing.cost_per_input_token > 0 || apiPricing.cost_per_output_token > 0) {
const inputCost = apiPricing.cost_per_input_token > 0
? `$${(apiPricing.cost_per_input_token * 1000000).toFixed(2)}/1M input`
: '';
const outputCost = apiPricing.cost_per_output_token > 0
? `$${(apiPricing.cost_per_output_token * 1000000).toFixed(2)}/1M output`
: '';
if (inputCost && outputCost) {
parts.push(`${inputCost}, ${outputCost}`);
} else if (inputCost) {
parts.push(inputCost);
} else if (outputCost) {
parts.push(outputCost);
}
}
if (apiPricing.cost_per_request > 0) {
parts.push(`$${apiPricing.cost_per_request.toFixed(4)} per request`);
}
if (apiPricing.cost_per_search > 0) {
parts.push(`$${apiPricing.cost_per_search.toFixed(4)} per search`);
}
if (apiPricing.cost_per_image > 0) {
parts.push(`$${apiPricing.cost_per_image.toFixed(2)} per image`);
}
if (apiPricing.cost_per_page > 0) {
parts.push(`$${apiPricing.cost_per_page.toFixed(4)} per page`);
}
return parts.length > 0 ? parts.join(', ') : fallbackPricing;
}
return fallbackPricing; // Fallback to static pricing if not found
};
const getProviderCategory = (providerName: string) => {
const provider = providerName.toLowerCase();
@@ -180,9 +271,9 @@ const ComprehensiveAPIBreakdown: React.FC<ComprehensiveAPIBreakdownProps> = ({
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)
totalCost: categoryProviders.reduce((sum, p) => sum + (p.cost ?? 0), 0),
totalCalls: categoryProviders.reduce((sum, p) => sum + (p.calls ?? 0), 0),
totalTokens: categoryProviders.reduce((sum, p) => sum + (p.tokens ?? 0), 0)
};
};
@@ -210,9 +301,35 @@ const ComprehensiveAPIBreakdown: React.FC<ComprehensiveAPIBreakdownProps> = ({
<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 sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{loadingPricing && (
<CircularProgress size={16} sx={{ color: 'rgba(255,255,255,0.7)' }} />
)}
<Tooltip title={pricingError ? `Pricing error: ${pricingError}. Showing static pricing.` : "Detailed breakdown with real-time pricing from API"}>
<Info size={16} color="rgba(255,255,255,0.7)" />
</Tooltip>
{!loadingPricing && !pricingError && (
<Tooltip title="Refresh pricing">
<RefreshCw
size={16}
color="rgba(255,255,255,0.7)"
style={{ cursor: 'pointer' }}
onClick={async () => {
try {
setLoadingPricing(true);
const pricingData = await billingService.getAPIPricing();
setPricing(pricingData);
setPricingError(null);
} catch (err) {
setPricingError(err instanceof Error ? err.message : 'Failed to refresh pricing');
} finally {
setLoadingPricing(false);
}
}}
/>
</Tooltip>
)}
</Box>
</Box>
</CardContent>
@@ -253,7 +370,7 @@ const ComprehensiveAPIBreakdown: React.FC<ComprehensiveAPIBreakdownProps> = ({
<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)}
{activeProviders.reduce((sum, p) => sum + (p.calls ?? 0), 0)}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Total Calls
@@ -352,9 +469,50 @@ const ComprehensiveAPIBreakdown: React.FC<ComprehensiveAPIBreakdownProps> = ({
{api.description}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)', display: 'block', mb: 1 }}>
Pricing: {api.pricing}
</Typography>
<Tooltip
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
Current Pricing
</Typography>
<Typography variant="body2">
{loadingPricing
? 'Loading...'
: pricingError
? `Using fallback pricing. Error: ${pricingError}`
: 'Real-time pricing from API'
}
</Typography>
{!loadingPricing && !pricingError && (
<Typography variant="caption" sx={{ mt: 1, display: 'block', opacity: 0.8 }}>
Last updated: {pricing.find(p =>
p.provider.toLowerCase() === api.name.toLowerCase()
)?.effective_date || 'N/A'}
</Typography>
)}
</Box>
}
arrow
placement="top"
>
<Typography
variant="caption"
sx={{
color: loadingPricing
? 'rgba(255,255,255,0.5)'
: pricingError
? 'rgba(255,193,7,0.8)'
: 'rgba(74, 222, 128, 0.9)',
display: 'block',
mb: 1,
fontWeight: !loadingPricing && !pricingError ? 500 : 400
}}
>
Pricing: {getPricingDisplay(api.name, api.pricing)}
{!loadingPricing && !pricingError && ' ✓'}
{pricingError && ' (static)'}
</Typography>
</Tooltip>
{providerData && (
<Box sx={{ mt: 2, p: 1, backgroundColor: 'rgba(0,0,0,0.2)', borderRadius: 1 }}>
@@ -364,7 +522,7 @@ const ComprehensiveAPIBreakdown: React.FC<ComprehensiveAPIBreakdownProps> = ({
Cost
</Typography>
<Typography variant="caption" sx={{ display: 'block', color: '#4ade80', fontWeight: 'bold' }}>
${providerData.cost.toFixed(4)}
${(providerData.cost ?? 0).toFixed(4)}
</Typography>
</Grid>
<Grid item xs={4}>
@@ -372,7 +530,7 @@ const ComprehensiveAPIBreakdown: React.FC<ComprehensiveAPIBreakdownProps> = ({
Calls
</Typography>
<Typography variant="caption" sx={{ display: 'block', color: '#ffffff', fontWeight: 'bold' }}>
{providerData.calls}
{providerData.calls ?? 0}
</Typography>
</Grid>
<Grid item xs={4}>
@@ -380,7 +538,7 @@ const ComprehensiveAPIBreakdown: React.FC<ComprehensiveAPIBreakdownProps> = ({
Tokens
</Typography>
<Typography variant="caption" sx={{ display: 'block', color: '#ffffff', fontWeight: 'bold' }}>
{providerData.tokens.toLocaleString()}
{(providerData.tokens ?? 0).toLocaleString()}
</Typography>
</Grid>
</Grid>

View File

@@ -35,12 +35,12 @@ const CostBreakdown: React.FC<CostBreakdownProps> = ({
}) => {
// Transform data for pie chart
const chartData = Object.entries(providerBreakdown)
.filter(([_, data]) => data.cost > 0)
.filter(([_, data]) => data && data.cost > 0)
.map(([provider, data]) => ({
name: provider.charAt(0).toUpperCase() + provider.slice(1),
value: data.cost,
calls: data.calls,
tokens: data.tokens,
value: data?.cost ?? 0,
calls: data?.calls ?? 0,
tokens: data?.tokens ?? 0,
color: getProviderColor(provider),
icon: getProviderIcon(provider)
}))
@@ -124,9 +124,14 @@ const CostBreakdown: React.FC<CostBreakdownProps> = ({
outerRadius={80}
fill="#8884d8"
dataKey="value"
animationBegin={0}
animationDuration={1000}
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
<Cell
key={`cell-${index}`}
fill={entry.color}
/>
))}
</Pie>
<Tooltip content={<CustomTooltip />} />

View File

@@ -0,0 +1,308 @@
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
Box,
Grid,
Chip,
CircularProgress,
Alert,
Divider,
} from '@mui/material';
import {
DollarSign,
Info,
CheckCircle,
AlertTriangle
} from 'lucide-react';
// Types
import { PreflightCheckResponse, PreflightOperation } from '../../services/billingService';
// Services
import { billingService, formatCurrency, checkPreflightBatch } from '../../services/billingService';
interface CostEstimationModalProps {
open: boolean;
onClose: () => void;
onConfirm: () => void;
operations: PreflightOperation[];
userId?: string;
}
const CostEstimationModal: React.FC<CostEstimationModalProps> = ({
open,
onClose,
onConfirm,
operations,
userId
}) => {
const [estimation, setEstimation] = useState<PreflightCheckResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (open && operations.length > 0) {
fetchEstimation();
} else {
setEstimation(null);
setError(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, JSON.stringify(operations)]);
const fetchEstimation = async () => {
try {
setLoading(true);
setError(null);
const result = await checkPreflightBatch(operations);
setEstimation(result);
} catch (err) {
console.error('[CostEstimationModal] Error fetching estimation:', err);
setError(err instanceof Error ? err.message : 'Failed to estimate costs');
} finally {
setLoading(false);
}
};
const handleConfirm = () => {
if (estimation?.can_proceed) {
onConfirm();
onClose();
}
};
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
background: 'linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.98) 100%)',
borderRadius: 3,
boxShadow: '0 20px 60px rgba(0,0,0,0.3)'
}
}}
>
<DialogTitle sx={{
display: 'flex',
alignItems: 'center',
gap: 1.5,
pb: 1,
borderBottom: '1px solid rgba(0,0,0,0.1)'
}}>
<DollarSign size={24} color="#667eea" />
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#1e293b' }}>
Cost Estimation
</Typography>
<Typography variant="caption" sx={{ color: '#64748b' }}>
Estimated cost for this operation
</Typography>
</Box>
</DialogTitle>
<DialogContent sx={{ pt: 3 }}>
{loading && (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', py: 4 }}>
<CircularProgress size={40} sx={{ color: '#667eea', mb: 2 }} />
<Typography variant="body2" sx={{ color: '#64748b' }}>
Calculating estimated costs...
</Typography>
</Box>
)}
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{estimation && !loading && (
<>
{/* Overall Estimation */}
<Box
sx={{
p: 2.5,
mb: 3,
background: estimation.can_proceed
? 'linear-gradient(135deg, rgba(34, 197, 94, 0.1) 0%, rgba(22, 163, 74, 0.05) 100%)'
: 'linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.05) 100%)',
borderRadius: 2,
border: `2px solid ${estimation.can_proceed ? '#22c55e' : '#ef4444'}`,
textAlign: 'center'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, mb: 1 }}>
{estimation.can_proceed ? (
<CheckCircle size={24} color="#22c55e" />
) : (
<AlertTriangle size={24} color="#ef4444" />
)}
<Typography variant="h5" sx={{ fontWeight: 'bold', color: estimation.can_proceed ? '#22c55e' : '#ef4444' }}>
{formatCurrency(estimation.total_cost)}
</Typography>
</Box>
<Typography variant="body2" sx={{ color: '#64748b', mb: 1 }}>
Estimated Total Cost
</Typography>
{!estimation.can_proceed && (
<Alert severity="error" sx={{ mt: 1 }}>
This operation cannot proceed. {estimation.operations.find(op => !op.allowed)?.message || 'Limit exceeded'}
</Alert>
)}
</Box>
{/* Operation Breakdown */}
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 2, color: '#1e293b' }}>
Operation Breakdown
</Typography>
<Grid container spacing={2} sx={{ mb: 2 }}>
{estimation.operations.map((op, index) => (
<Grid item xs={12} key={index}>
<Box
sx={{
p: 2,
backgroundColor: op.allowed ? 'rgba(34, 197, 94, 0.05)' : 'rgba(239, 68, 68, 0.05)',
borderRadius: 2,
border: `1px solid ${op.allowed ? 'rgba(34, 197, 94, 0.2)' : 'rgba(239, 68, 68, 0.2)'}`
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#1e293b' }}>
{op.provider}
</Typography>
<Chip
label={op.allowed ? 'Allowed' : 'Blocked'}
size="small"
sx={{
backgroundColor: op.allowed ? 'rgba(34, 197, 94, 0.2)' : 'rgba(239, 68, 68, 0.2)',
color: op.allowed ? '#22c55e' : '#ef4444',
fontWeight: 'bold'
}}
/>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" sx={{ color: '#64748b' }}>
Operation:
</Typography>
<Typography variant="caption" sx={{ fontWeight: 500, color: '#1e293b' }}>
{op.operation_type}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" sx={{ color: '#64748b' }}>
Estimated Cost:
</Typography>
<Typography variant="caption" sx={{ fontWeight: 'bold', color: '#667eea' }}>
{formatCurrency(op.cost)}
</Typography>
</Box>
{op.limit_info && (
<Box sx={{ mt: 1, pt: 1, borderTop: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="caption" sx={{ color: '#64748b' }}>
Usage: {op.limit_info.current_usage} / {op.limit_info.limit}
({((op.limit_info.current_usage / op.limit_info.limit) * 100).toFixed(1)}%)
</Typography>
</Box>
)}
{op.message && (
<Typography variant="caption" sx={{ color: op.allowed ? '#22c55e' : '#ef4444', display: 'block', mt: 0.5 }}>
{op.message}
</Typography>
)}
</Box>
</Grid>
))}
</Grid>
{/* Usage Summary */}
{estimation.usage_summary && (
<>
<Divider sx={{ my: 2 }} />
<Box sx={{ p: 2, backgroundColor: 'rgba(102, 126, 234, 0.05)', borderRadius: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 1, color: '#1e293b' }}>
Current Usage Summary
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="body2" sx={{ color: '#64748b' }}>
Current Calls:
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#1e293b' }}>
{estimation.usage_summary.current_calls}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="body2" sx={{ color: '#64748b' }}>
Limit:
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#1e293b' }}>
{estimation.usage_summary.limit}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" sx={{ color: '#64748b' }}>
Remaining:
</Typography>
<Typography
variant="body2"
sx={{
fontWeight: 'bold',
color: estimation.usage_summary.remaining > 0 ? '#22c55e' : '#ef4444'
}}
>
{estimation.usage_summary.remaining}
</Typography>
</Box>
</Box>
</>
)}
{/* Info Note */}
<Box sx={{ mt: 2, p: 1.5, backgroundColor: 'rgba(59, 130, 246, 0.05)', borderRadius: 1 }}>
<Box sx={{ display: 'flex', gap: 1 }}>
<Info size={16} color="#3b82f6" style={{ marginTop: 2 }} />
<Typography variant="caption" sx={{ color: '#64748b' }}>
This is an estimate. Actual costs may vary based on token usage and API response length.
</Typography>
</Box>
</Box>
</>
)}
</DialogContent>
<DialogActions sx={{ px: 3, py: 2, borderTop: '1px solid rgba(0,0,0,0.1)' }}>
<Button onClick={onClose} sx={{ color: '#64748b' }}>
Cancel
</Button>
<Button
onClick={handleConfirm}
variant="contained"
disabled={loading || !estimation?.can_proceed}
sx={{
background: estimation?.can_proceed
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
: 'rgba(100, 116, 139, 0.3)',
'&:hover': {
background: estimation?.can_proceed
? 'linear-gradient(135deg, #5568d3 0%, #6b3d91 100%)'
: 'rgba(100, 116, 139, 0.3)'
}
}}
>
{loading ? 'Calculating...' : estimation?.can_proceed ? 'Proceed' : 'Cannot Proceed'}
</Button>
</DialogActions>
</Dialog>
);
};
export default CostEstimationModal;

View File

@@ -0,0 +1,443 @@
import React, { useState, useEffect, useMemo } from 'react';
import {
Card,
CardContent,
Typography,
Box,
Grid,
Chip,
Tooltip,
Button,
Alert,
CircularProgress,
Accordion,
AccordionSummary,
AccordionDetails,
} from '@mui/material';
import { ExpandMore } from '@mui/icons-material';
import { motion } from 'framer-motion';
import {
Lightbulb,
TrendingDown,
DollarSign,
ArrowRight,
Info,
Sparkles
} from 'lucide-react';
// Types
import { UsageLog } from '../../types/billing';
// Services
import { billingService, formatCurrency } from '../../services/billingService';
interface CostOptimizationRecommendationsProps {
userId?: string;
terminalTheme?: boolean;
}
interface OptimizationRecommendation {
id: string;
title: string;
description: string;
potentialSavings: number;
savingsPercentage: number;
category: 'model_switch' | 'provider_switch' | 'usage_pattern' | 'efficiency';
priority: 'high' | 'medium' | 'low';
actionItems: string[];
currentCost: number;
recommendedCost: number;
}
const CostOptimizationRecommendations: React.FC<CostOptimizationRecommendationsProps> = ({
userId,
terminalTheme = false
}) => {
const [usageLogs, setUsageLogs] = useState<UsageLog[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchUsageLogs = async () => {
try {
setLoading(true);
setError(null);
const currentDate = new Date();
const billingPeriod = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`;
const response = await billingService.getUsageLogs(1000, 0, undefined, undefined, billingPeriod);
setUsageLogs(response.logs || []);
} catch (err) {
console.error('[CostOptimizationRecommendations] Error fetching usage logs:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch usage logs');
} finally {
setLoading(false);
}
};
fetchUsageLogs();
}, [userId]);
// Analyze usage patterns and generate recommendations
const recommendations = useMemo(() => {
if (usageLogs.length === 0) return [];
const recs: OptimizationRecommendation[] = [];
// 1. Model Switch Recommendations
// Analyze if user is using expensive models when cheaper alternatives exist
const modelUsage = usageLogs.reduce((acc, log) => {
if (!log.model_used || log.cost_total === 0) return acc;
const key = `${log.provider}:${log.model_used}`;
if (!acc[key]) {
acc[key] = { cost: 0, calls: 0, tokens: 0 };
}
acc[key].cost += log.cost_total;
acc[key].calls += 1;
acc[key].tokens += log.tokens_total;
return acc;
}, {} as Record<string, { cost: number; calls: number; tokens: number }>);
// Check for Gemini Pro usage when Flash could work
const geminiProUsage = Object.entries(modelUsage).find(([key]) =>
key.includes('gemini') && key.includes('pro') && !key.includes('flash')
);
if (geminiProUsage) {
const [_, data] = geminiProUsage;
// Estimate Flash cost (typically 10-20x cheaper)
const estimatedFlashCost = data.cost * 0.1; // Conservative estimate
const savings = data.cost - estimatedFlashCost;
if (savings > 0.01) { // Only show if savings > $0.01
recs.push({
id: 'gemini-pro-to-flash',
title: 'Switch Gemini Pro to Flash for Simple Tasks',
description: `You're using Gemini Pro for ${data.calls} calls. Consider using Gemini Flash for simpler tasks - it's 10x cheaper with similar quality for most use cases.`,
potentialSavings: savings,
savingsPercentage: (savings / data.cost) * 100,
category: 'model_switch',
priority: 'high',
actionItems: [
'Use Gemini Flash for content generation',
'Use Gemini Flash for simple Q&A',
'Reserve Gemini Pro for complex reasoning tasks only'
],
currentCost: data.cost,
recommendedCost: estimatedFlashCost
});
}
}
// 2. Provider Cost Analysis
const providerCosts = usageLogs.reduce((acc, log) => {
if (log.cost_total === 0) return acc;
if (!acc[log.provider]) {
acc[log.provider] = { cost: 0, calls: 0, avgCostPerCall: 0 };
}
acc[log.provider].cost += log.cost_total;
acc[log.provider].calls += 1;
acc[log.provider].avgCostPerCall = acc[log.provider].cost / acc[log.provider].calls;
return acc;
}, {} as Record<string, { cost: number; calls: number; avgCostPerCall: number }>);
// Find expensive providers
const sortedProviders = Object.entries(providerCosts)
.sort(([, a], [, b]) => b.avgCostPerCall - a.avgCostPerCall);
if (sortedProviders.length > 1) {
const [mostExpensive, secondMost] = sortedProviders;
const [expensiveProvider, expensiveData] = mostExpensive;
const [alternativeProvider, alternativeData] = secondMost;
// If expensive provider has significantly higher cost per call
if (expensiveData.avgCostPerCall > alternativeData.avgCostPerCall * 1.5 && expensiveData.calls > 10) {
const estimatedAlternativeCost = expensiveData.calls * alternativeData.avgCostPerCall;
const savings = expensiveData.cost - estimatedAlternativeCost;
if (savings > 0.01) {
recs.push({
id: `provider-switch-${expensiveProvider}`,
title: `Consider ${alternativeProvider} for Some Operations`,
description: `${expensiveProvider} costs $${expensiveData.avgCostPerCall.toFixed(4)} per call on average, while ${alternativeProvider} costs $${alternativeData.avgCostPerCall.toFixed(4)}. Consider switching for non-critical operations.`,
potentialSavings: savings,
savingsPercentage: (savings / expensiveData.cost) * 100,
category: 'provider_switch',
priority: 'medium',
actionItems: [
`Use ${alternativeProvider} for batch operations`,
`Reserve ${expensiveProvider} for high-priority tasks only`,
'Review operation requirements to identify switchable tasks'
],
currentCost: expensiveData.cost,
recommendedCost: estimatedAlternativeCost
});
}
}
}
// 3. Usage Pattern Analysis - High Token Usage
const highTokenUsage = usageLogs.filter(log =>
log.tokens_total > 10000 && log.cost_total > 0.01
);
if (highTokenUsage.length > 5) {
const totalHighTokenCost = highTokenUsage.reduce((sum, log) => sum + log.cost_total, 0);
const avgTokensPerCall = highTokenUsage.reduce((sum, log) => sum + log.tokens_total, 0) / highTokenUsage.length;
recs.push({
id: 'optimize-high-token-usage',
title: 'Optimize High Token Usage Operations',
description: `You have ${highTokenUsage.length} operations using >10K tokens each. Consider breaking down large requests or using more efficient prompts.`,
potentialSavings: totalHighTokenCost * 0.15, // Estimate 15% savings
savingsPercentage: 15,
category: 'usage_pattern',
priority: 'medium',
actionItems: [
'Break down large requests into smaller chunks',
'Use more concise prompts',
'Implement result caching for repeated queries',
`Average tokens per call: ${Math.round(avgTokensPerCall).toLocaleString()}`
],
currentCost: totalHighTokenCost,
recommendedCost: totalHighTokenCost * 0.85
});
}
// 4. Efficiency - Failed Requests
const failedRequests = usageLogs.filter(log => log.status === 'failed' && log.cost_total > 0);
if (failedRequests.length > 0) {
const failedCost = failedRequests.reduce((sum, log) => sum + log.cost_total, 0);
const failureRate = (failedRequests.length / usageLogs.length) * 100;
if (failureRate > 5) { // More than 5% failure rate
recs.push({
id: 'reduce-failed-requests',
title: 'Reduce Failed API Requests',
description: `${failedRequests.length} requests failed (${failureRate.toFixed(1)}% failure rate), costing $${failedCost.toFixed(4)}. Improve error handling and retry logic.`,
potentialSavings: failedCost * 0.8, // Can save 80% by preventing failures
savingsPercentage: 80,
category: 'efficiency',
priority: 'high',
actionItems: [
'Review and fix error-prone operations',
'Implement better retry logic',
'Add input validation before API calls',
'Monitor error patterns and address root causes'
],
currentCost: failedCost,
recommendedCost: failedCost * 0.2
});
}
}
// Sort by priority and potential savings
return recs.sort((a, b) => {
const priorityOrder = { high: 3, medium: 2, low: 1 };
if (priorityOrder[a.priority] !== priorityOrder[b.priority]) {
return priorityOrder[b.priority] - priorityOrder[a.priority];
}
return b.potentialSavings - a.potentialSavings;
});
}, [usageLogs]);
const totalPotentialSavings = recommendations.reduce((sum, rec) => sum + rec.potentialSavings, 0);
const totalCurrentCost = usageLogs.reduce((sum, log) => sum + log.cost_total, 0);
if (loading) {
return (
<Card sx={{ p: 3, textAlign: 'center' }}>
<CircularProgress />
<Typography sx={{ mt: 2, color: 'rgba(255,255,255,0.7)' }}>
Analyzing usage patterns...
</Typography>
</Card>
);
}
if (error) {
return (
<Card sx={{ p: 3 }}>
<Alert severity="error">{error}</Alert>
</Card>
);
}
if (recommendations.length === 0) {
return (
<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,
}}
>
<CardContent sx={{ textAlign: 'center', py: 4 }}>
<Sparkles size={48} color="#22c55e" style={{ marginBottom: 16 }} />
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#ffffff', mb: 1 }}>
Great Job!
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Your usage patterns are already optimized. No recommendations at this time.
</Typography>
</CardContent>
</Card>
);
}
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,
}}
>
<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' }}>
<Lightbulb size={20} />
Cost Optimization Recommendations
</Typography>
<Tooltip title="AI-powered suggestions to reduce your API costs">
<Info size={16} color="rgba(255,255,255,0.7)" />
</Tooltip>
</Box>
{/* Summary */}
<Box
sx={{
p: 2.5,
mb: 3,
background: 'linear-gradient(135deg, rgba(34, 197, 94, 0.15) 0%, rgba(22, 163, 74, 0.1) 100%)',
borderRadius: 2,
border: '1px solid rgba(34, 197, 94, 0.3)',
textAlign: 'center'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, mb: 1 }}>
<TrendingDown size={24} color="#22c55e" />
<Typography variant="h5" sx={{ fontWeight: 'bold', color: '#22c55e' }}>
{formatCurrency(totalPotentialSavings)}
</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.9)', mb: 0.5 }}>
Potential Monthly Savings
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
{recommendations.length} recommendation{recommendations.length !== 1 ? 's' : ''}
{totalCurrentCost > 0 ? ` ${((totalPotentialSavings / totalCurrentCost) * 100).toFixed(1)}% of current spending` : ''}
</Typography>
</Box>
</CardContent>
<CardContent sx={{ pt: 0 }}>
{/* Recommendations List */}
{recommendations.map((rec, index) => (
<Accordion
key={rec.id}
sx={{
mb: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 2,
'&:before': { display: 'none' },
boxShadow: 'none'
}}
>
<AccordionSummary
expandIcon={<ExpandMore sx={{ color: 'rgba(255,255,255,0.7)' }} />}
sx={{ px: 2, py: 1.5 }}
>
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', gap: 2 }}>
<Box sx={{ flex: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{rec.title}
</Typography>
<Chip
label={rec.priority}
size="small"
sx={{
backgroundColor: rec.priority === 'high' ? 'rgba(239, 68, 68, 0.2)' :
rec.priority === 'medium' ? 'rgba(245, 158, 11, 0.2)' :
'rgba(100, 116, 139, 0.2)',
color: rec.priority === 'high' ? '#ef4444' :
rec.priority === 'medium' ? '#f59e0b' :
'#64748b',
fontWeight: 'bold',
fontSize: '0.7rem',
height: 20
}}
/>
</Box>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)', display: 'block' }}>
{rec.description}
</Typography>
</Box>
<Box sx={{ textAlign: 'right', minWidth: 120 }}>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#22c55e' }}>
{formatCurrency(rec.potentialSavings)}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
{rec.savingsPercentage.toFixed(1)}% savings
</Typography>
</Box>
</Box>
</AccordionSummary>
<AccordionDetails sx={{ px: 2, pb: 2 }}>
<Box sx={{ mb: 2 }}>
<Grid container spacing={2}>
<Grid item xs={6}>
<Box sx={{ p: 1.5, backgroundColor: 'rgba(239, 68, 68, 0.1)', borderRadius: 1 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Current Cost
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#ef4444' }}>
{formatCurrency(rec.currentCost)}
</Typography>
</Box>
</Grid>
<Grid item xs={6}>
<Box sx={{ p: 1.5, backgroundColor: 'rgba(34, 197, 94, 0.1)', borderRadius: 1 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Recommended Cost
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#22c55e' }}>
{formatCurrency(rec.recommendedCost)}
</Typography>
</Box>
</Grid>
</Grid>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 1, color: '#ffffff' }}>
Action Items:
</Typography>
<Box component="ul" sx={{ m: 0, pl: 2 }}>
{rec.actionItems.map((item, idx) => (
<li key={idx}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)', mb: 0.5 }}>
{item}
</Typography>
</li>
))}
</Box>
</AccordionDetails>
</Accordion>
))}
</CardContent>
</Card>
</motion.div>
);
};
export default CostOptimizationRecommendations;

View File

@@ -0,0 +1,243 @@
import React, { Suspense, useMemo } from 'react';
import {
Card,
CardContent,
Typography,
Box,
Chip,
Tooltip as MuiTooltip,
} from '@mui/material';
import { motion } from 'framer-motion';
import {
TrendingUp,
TrendingDown,
AlertTriangle
} from 'lucide-react';
import {
LazyLineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip as RechartsTooltip,
ResponsiveContainer,
ReferenceLine,
ChartLoadingFallback
} from '../../utils/lazyRecharts';
// Types
import { UsageTrends as UsageTrendsType, CostProjections } from '../../types/billing';
// Utils
import { formatCurrency } from '../../services/billingService';
interface CostVelocityChartProps {
trends: UsageTrendsType;
projections: CostProjections;
monthlyLimit: number;
}
/**
* CostVelocityChart - Shows daily spending rate (cost velocity) over time
* with projected monthly cost and budget limit annotations
*/
const CostVelocityChart: React.FC<CostVelocityChartProps> = ({
trends,
projections,
monthlyLimit
}) => {
// Calculate daily spending rate for each period
const velocityData = useMemo(() => {
if (!trends.periods || trends.periods.length === 0) {
return [];
}
const data = [];
const currentDate = new Date();
const daysInMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate();
for (let i = 0; i < trends.periods.length; i++) {
const period = trends.periods[i];
const cost = trends.total_cost[i] || 0;
// Parse period (assuming format like "2025-01" or day number)
// For monthly periods, calculate daily average
const dayNumber = i + 1; // Approximate day in month
const dailyRate = dayNumber > 0 ? cost / dayNumber : 0;
const projectedMonthly = dailyRate * daysInMonth;
data.push({
period,
day: dayNumber,
dailyRate,
projectedMonthly,
actualCost: cost
});
}
return data;
}, [trends]);
// Calculate 7-day moving average
const movingAverageData = useMemo(() => {
if (velocityData.length === 0) return [];
const windowSize = Math.min(7, velocityData.length);
return velocityData.map((point, index) => {
const start = Math.max(0, index - windowSize + 1);
const window = velocityData.slice(start, index + 1);
const avg = window.reduce((sum, p) => sum + p.dailyRate, 0) / window.length;
return { ...point, movingAvg: avg };
});
}, [velocityData]);
// Current velocity metrics
const currentVelocity = velocityData.length > 0
? velocityData[velocityData.length - 1].dailyRate
: 0;
const projectedCost = projections.projected_monthly_cost || 0;
const isOverBudget = projectedCost > monthlyLimit;
// Custom tooltip
const CustomTooltip = ({ active, payload, label }: 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 }}>
Day {data.day}
</Typography>
<Typography variant="body2">
Daily Rate: {formatCurrency(data.dailyRate)}
</Typography>
<Typography variant="body2">
7-Day Avg: {formatCurrency(data.movingAvg || 0)}
</Typography>
<Typography variant="body2">
Projected Monthly: {formatCurrency(data.projectedMonthly)}
</Typography>
</Box>
);
}
return null;
};
if (velocityData.length === 0) {
return null;
}
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,
}}
>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
Cost Velocity Trend
</Typography>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
{isOverBudget && (
<Chip
icon={<AlertTriangle size={14} />}
label="Over Budget"
color="error"
size="small"
/>
)}
<Chip
icon={currentVelocity > (monthlyLimit / 30) ? <TrendingUp size={14} /> : <TrendingDown size={14} />}
label={`${formatCurrency(currentVelocity)}/day`}
color={isOverBudget ? 'error' : 'default'}
size="small"
/>
</Box>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)', mb: 0.5 }}>
Projected Monthly Cost: <strong style={{ color: isOverBudget ? '#ef4444' : '#4ade80' }}>
{formatCurrency(projectedCost)}
</strong>
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
Based on current daily spending rate
</Typography>
</Box>
<Suspense fallback={<ChartLoadingFallback />}>
<ResponsiveContainer width="100%" height={300}>
<LazyLineChart data={movingAverageData} margin={{ top: 5, right: 20, bottom: 5, left: 10 }}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
<XAxis
dataKey="day"
stroke="rgba(255,255,255,0.7)"
tick={{ fill: 'rgba(255,255,255,0.7)', fontSize: 12 }}
/>
<YAxis
stroke="rgba(255,255,255,0.7)"
tick={{ fill: 'rgba(255,255,255,0.7)', fontSize: 12 }}
tickFormatter={(value) => formatCurrency(value)}
/>
<RechartsTooltip content={<CustomTooltip />} />
{/* Daily Rate Line */}
<Line
type="monotone"
dataKey="dailyRate"
stroke="#3b82f6"
strokeWidth={2}
dot={{ fill: '#3b82f6', r: 4 }}
name="Daily Rate"
animationDuration={1000}
animationBegin={0}
/>
{/* 7-Day Moving Average */}
<Line
type="monotone"
dataKey="movingAvg"
stroke="#4ade80"
strokeWidth={2}
strokeDasharray="5 5"
dot={false}
name="7-Day Avg"
animationDuration={1000}
animationBegin={200}
/>
{/* Budget Limit Reference Line */}
<ReferenceLine
y={monthlyLimit / 30}
stroke="#ef4444"
strokeDasharray="3 3"
label={{ value: "Budget Limit", position: "right", fill: "#ef4444" }}
/>
</LazyLineChart>
</ResponsiveContainer>
</Suspense>
</CardContent>
</Card>
</motion.div>
);
};
export default CostVelocityChart;

View File

@@ -0,0 +1,231 @@
import React, { Suspense, useMemo } from 'react';
import {
Card,
CardContent,
Typography,
Box,
Tooltip as MuiTooltip,
} from '@mui/material';
import { motion } from 'framer-motion';
import { Calendar } from 'lucide-react';
import { formatCurrency } from '../../services/billingService';
import { UsageLog } from '../../types/billing';
interface DailyCostHeatmapProps {
usageLogs: UsageLog[];
currentMonth: number;
currentYear: number;
}
/**
* DailyCostHeatmap - Calendar-style heatmap showing cost patterns by day
*
* Visualizes daily spending patterns to identify high-cost days
*/
const DailyCostHeatmap: React.FC<DailyCostHeatmapProps> = ({
usageLogs,
currentMonth,
currentYear
}) => {
// Aggregate logs by day
const dailyCosts = useMemo(() => {
const costs: Record<number, number> = {};
usageLogs.forEach(log => {
const logDate = new Date(log.timestamp);
if (logDate.getMonth() === currentMonth && logDate.getFullYear() === currentYear) {
const day = logDate.getDate();
costs[day] = (costs[day] || 0) + (log.cost_total || 0);
}
});
return costs;
}, [usageLogs, currentMonth, currentYear]);
// Get days in month
const daysInMonth = useMemo(() => {
return new Date(currentYear, currentMonth + 1, 0).getDate();
}, [currentMonth, currentYear]);
// Calculate max cost for color scaling
const maxCost = useMemo(() => {
return Math.max(...Object.values(dailyCosts), 0.01);
}, [dailyCosts]);
// Get color intensity based on cost
const getColorIntensity = (cost: number) => {
if (cost === 0) return 'rgba(255,255,255,0.05)';
const intensity = Math.min(cost / maxCost, 1);
// Green (low) to Red (high)
if (intensity < 0.3) {
return `rgba(34, 197, 94, ${0.3 + intensity * 0.4})`; // Green
} else if (intensity < 0.7) {
return `rgba(234, 179, 8, ${0.5 + (intensity - 0.3) * 0.3})`; // Yellow
} else {
return `rgba(239, 68, 68, ${0.6 + (intensity - 0.7) * 0.4})`; // Red
}
};
// Generate calendar grid
const calendarDays = useMemo(() => {
const firstDay = new Date(currentYear, currentMonth, 1).getDay();
const days: Array<{ day: number; cost: number; date: Date | null }> = [];
// Add empty cells for days before month starts
for (let i = 0; i < firstDay; i++) {
days.push({ day: 0, cost: 0, date: null });
}
// Add days of month
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(currentYear, currentMonth, day);
days.push({
day,
cost: dailyCosts[day] || 0,
date
});
}
return days;
}, [currentMonth, currentYear, daysInMonth, dailyCosts]);
if (usageLogs.length === 0) {
return null;
}
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,
}}
>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<Calendar size={20} color="#4ade80" />
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
Daily Cost Heatmap
</Typography>
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)', mb: 1 }}>
Cost intensity by day of month
</Typography>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', fontSize: '0.75rem' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Box sx={{ width: 12, height: 12, backgroundColor: 'rgba(34, 197, 94, 0.5)', borderRadius: 1 }} />
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>Low</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Box sx={{ width: 12, height: 12, backgroundColor: 'rgba(234, 179, 8, 0.7)', borderRadius: 1 }} />
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>Medium</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Box sx={{ width: 12, height: 12, backgroundColor: 'rgba(239, 68, 68, 0.8)', borderRadius: 1 }} />
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>High</Typography>
</Box>
</Box>
</Box>
<Box
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: 1,
mb: 1
}}
>
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
<Typography
key={day}
variant="caption"
sx={{
textAlign: 'center',
color: 'rgba(255,255,255,0.6)',
fontWeight: 600,
fontSize: '0.7rem'
}}
>
{day}
</Typography>
))}
</Box>
<Box
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: 1
}}
>
{calendarDays.map((item, index) => (
<MuiTooltip
key={index}
title={
item.date ? (
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
{item.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</Typography>
<Typography variant="body2">
Cost: {formatCurrency(item.cost)}
</Typography>
</Box>
) : (
'No data'
)
}
arrow
placement="top"
>
<Box
sx={{
aspectRatio: '1',
backgroundColor: getColorIntensity(item.cost),
borderRadius: 1,
border: item.cost > 0 ? '1px solid rgba(255,255,255,0.2)' : '1px solid rgba(255,255,255,0.05)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: item.date ? 'pointer' : 'default',
transition: 'all 0.2s ease',
'&:hover': item.date ? {
transform: 'scale(1.1)',
zIndex: 1,
boxShadow: '0 4px 12px rgba(0,0,0,0.3)'
} : {},
position: 'relative'
}}
>
{item.day > 0 && (
<Typography
variant="caption"
sx={{
color: item.cost > maxCost * 0.5 ? '#ffffff' : 'rgba(255,255,255,0.8)',
fontSize: '0.65rem',
fontWeight: item.cost > 0 ? 600 : 400
}}
>
{item.day}
</Typography>
)}
</Box>
</MuiTooltip>
))}
</Box>
</CardContent>
</Card>
</motion.div>
);
};
export default DailyCostHeatmap;

View File

@@ -38,6 +38,13 @@ import CostBreakdown from './CostBreakdown';
import UsageTrends from './UsageTrends';
import UsageAlerts from './UsageAlerts';
import ComprehensiveAPIBreakdown from './ComprehensiveAPIBreakdown';
import ToolCostBreakdown from './ToolCostBreakdown';
import CostOptimizationRecommendations from './CostOptimizationRecommendations';
import AdvancedCostAnalytics from './AdvancedCostAnalytics';
import DailyCostHeatmap from './DailyCostHeatmap';
import LiveCostCounter from './LiveCostCounter';
import ErrorRateGauge from './ErrorRateGauge';
import MultiSeriesCostChart from './MultiSeriesCostChart';
// Terminal Theme
import {
@@ -62,29 +69,77 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<ViewMode>('compact');
const [lastRefreshTime, setLastRefreshTime] = useState<Date | null>(null);
const [healthError, setHealthError] = useState<string | null>(null);
const fetchDashboardData = async (showSuccessToast: boolean = false) => {
try {
const [billingData, healthData] = await Promise.all([
// Use Promise.allSettled to prevent health check timeout from blocking dashboard
const results = await Promise.allSettled([
billingService.getDashboardData(),
monitoringService.getSystemHealth()
]);
setDashboardData(billingData);
setSystemHealth(healthData);
// Handle billing data (required)
if (results[0].status === 'fulfilled') {
setDashboardData(results[0].value);
setLastRefreshTime(new Date());
setError(null); // Clear any previous errors
} else {
// Billing data is critical - show error
const errorMessage = results[0].reason instanceof Error
? results[0].reason.message
: 'Failed to fetch dashboard data';
setError(errorMessage);
showToastNotification(
`Unable to fetch latest billing data: ${errorMessage}. Showing last known values.`,
'error',
{ duration: 7000 }
);
setLoading(false);
return;
}
// Handle health data (optional - don't block dashboard if it fails)
if (results[1].status === 'fulfilled') {
setSystemHealth(results[1].value);
setHealthError(null); // Clear health error on success
} else {
// Health check failed - keep last successful value, show error toast
const healthErrorMessage = results[1].reason instanceof Error
? results[1].reason.message
: 'System health check timed out or failed';
setHealthError(healthErrorMessage);
showToastNotification(
`Unable to fetch latest system health data: ${healthErrorMessage}. Showing last known values.`,
'warning',
{ duration: 6000 }
);
// Don't update systemHealth - keep last successful value
// Only set to null if we never had a successful fetch
if (!systemHealth) {
setSystemHealth(null);
}
}
// Show success toast only if explicitly requested (user-initiated refresh)
if (showSuccessToast && billingData && healthData) {
if (showSuccessToast && results[0].status === 'fulfilled' && results[1].status === 'fulfilled') {
showToastNotification(
'Billing data refreshed successfully',
'success',
{ duration: 3000 }
);
} else if (showSuccessToast && results[0].status === 'fulfilled' && results[1].status === 'rejected') {
showToastNotification(
'Billing data refreshed, but system health check failed',
'warning',
{ duration: 4000 }
);
}
} catch (error) {
// Fallback error handling (shouldn't reach here with allSettled)
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch dashboard data';
setError(errorMessage);
// Always show error toast for failures
showToastNotification(errorMessage, 'error', { duration: 5000 });
} finally {
setLoading(false);
@@ -99,10 +154,24 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
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);
Promise.allSettled([billingService.getDashboardData(), monitoringService.getSystemHealth()])
.then((results) => {
if (results[0].status === 'fulfilled') {
setDashboardData(results[0].value);
setLastRefreshTime(new Date());
setError(null);
}
if (results[1].status === 'fulfilled') {
setSystemHealth(results[1].value);
setHealthError(null);
} else {
// Keep last successful health value, don't set fake defaults
const healthErrorMessage = results[1].reason instanceof Error
? results[1].reason.message
: 'System health check failed';
setHealthError(healthErrorMessage);
// Don't update systemHealth - keep last successful value
}
})
.catch(() => {/* ignore */});
});
@@ -124,16 +193,39 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
useEffect(() => {
const handleBillingRefresh = () => {
console.log('EnhancedBillingDashboard: Billing refresh requested, refreshing data...');
// Use a fresh call to fetchDashboardData to ensure we get latest data
Promise.all([billingService.getDashboardData(), monitoringService.getSystemHealth()])
.then(([billingData, healthData]) => {
setDashboardData(billingData);
setSystemHealth(healthData);
// Use allSettled to prevent health check from blocking refresh
Promise.allSettled([billingService.getDashboardData(), monitoringService.getSystemHealth()])
.then((results) => {
if (results[0].status === 'fulfilled') {
setDashboardData(results[0].value);
setLastRefreshTime(new Date());
setError(null);
} else {
const errorMessage = results[0].reason instanceof Error
? results[0].reason.message
: 'Failed to refresh billing data';
setError(errorMessage);
showToastNotification(
`Unable to refresh billing data: ${errorMessage}. Showing last known values.`,
'error',
{ duration: 6000 }
);
console.error('Error refreshing billing data:', results[0].reason);
}
if (results[1].status === 'fulfilled') {
setSystemHealth(results[1].value);
setHealthError(null);
} else {
// Keep last successful health value, don't set fake defaults
const healthErrorMessage = results[1].reason instanceof Error
? results[1].reason.message
: 'System health check failed';
setHealthError(healthErrorMessage);
// Don't update systemHealth - keep last successful value
}
})
.catch((error) => {
const errorMessage = error instanceof Error ? error.message : 'Failed to refresh billing data';
setError(errorMessage);
console.error('Error refreshing billing data:', error);
console.error('Unexpected error in billing refresh:', error);
});
};
@@ -227,55 +319,73 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
{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>
))}
.filter(([_, data]) => data && data.cost > 0)
.map(([provider, data]) => {
const providerData = data!; // Safe after filter
return (
<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: ${(providerData.cost ?? 0).toFixed(4)}
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Calls: {(providerData.calls ?? 0).toLocaleString()}
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Tokens: {(providerData.tokens ?? 0).toLocaleString()}
</Typography>
</Box>
}
arrow
placement="top"
>
<Chip
label={`${provider}: $${(providerData.cost ?? 0).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 }}>
{/* Last Refresh Timestamp */}
{lastRefreshTime && (
<Tooltip title={`Data last refreshed at ${lastRefreshTime.toLocaleTimeString()}`}>
<TypographyComponent
variant="caption"
sx={{
color: 'rgba(255,255,255,0.6)',
fontSize: '0.7rem',
fontStyle: 'italic'
}}
>
Last updated: {lastRefreshTime.toLocaleTimeString()}
</TypographyComponent>
</Tooltip>
)}
<Tooltip title="Refresh billing data">
<IconButton
size="small"
@@ -405,13 +515,54 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
/>
</Grid>
{/* Tool-Level Cost Breakdown */}
<Grid item xs={12} md={6}>
<ToolCostBreakdown
userId={userId}
terminalTheme={terminalTheme}
/>
</Grid>
{/* Bottom Row - Comprehensive API Breakdown */}
<Grid item xs={12}>
<Grid item xs={12} md={6}>
<ComprehensiveAPIBreakdown
providerBreakdown={dashboardData.current_usage.provider_breakdown}
totalCost={dashboardData.current_usage.total_cost}
/>
</Grid>
{/* Priority 3: Cost Optimization Recommendations */}
<Grid item xs={12} md={6}>
<CostOptimizationRecommendations
userId={userId}
terminalTheme={terminalTheme}
/>
</Grid>
{/* Priority 3: Advanced Cost Analytics */}
<Grid item xs={12}>
<AdvancedCostAnalytics
userId={userId}
terminalTheme={terminalTheme}
/>
</Grid>
{/* Phase 3: Multi-Series Cost Chart */}
<Grid item xs={12} md={6}>
<MultiSeriesCostChart
trends={dashboardData.trends}
monthlyLimit={dashboardData.projections.cost_limit || 0}
/>
</Grid>
{/* Phase 3: Error Rate Gauge */}
<Grid item xs={12} md={6}>
<ErrorRateGauge
systemHealth={systemHealth}
terminalTheme={terminalTheme}
terminalColors={terminalColors}
/>
</Grid>
</Grid>
</motion.div>
)}

View File

@@ -0,0 +1,244 @@
import React, { Suspense, useMemo } from 'react';
import {
Card,
CardContent,
Typography,
Box,
Tooltip as MuiTooltip,
} from '@mui/material';
import { motion } from 'framer-motion';
import { AlertTriangle, CheckCircle, AlertCircle } from 'lucide-react';
import {
LazyPieChart,
Pie,
Cell,
ResponsiveContainer,
ChartLoadingFallback
} from '../../utils/lazyRecharts';
import { SystemHealth } from '../../types/monitoring';
interface ErrorRateGaugeProps {
systemHealth: SystemHealth | null;
terminalTheme?: boolean;
terminalColors?: any;
}
/**
* ErrorRateGauge - Semi-circular gauge showing API error rate
*
* Visual gauge with:
* - Green (0-5%): Healthy
* - Yellow (5-10%): Warning
* - Red (>10%): Critical
*/
const ErrorRateGauge: React.FC<ErrorRateGaugeProps> = ({
systemHealth,
terminalTheme = false,
terminalColors
}) => {
const errorRate = systemHealth?.error_rate || 0;
const recentRequests = systemHealth?.recent_requests || 0;
const recentErrors = systemHealth?.recent_errors || 0;
// Determine status and color
const { status, color, icon } = useMemo(() => {
if (errorRate <= 5) {
return {
status: 'Healthy',
color: terminalTheme ? (terminalColors?.success || '#22c55e') : '#22c55e',
icon: <CheckCircle size={20} color="#22c55e" />
};
} else if (errorRate <= 10) {
return {
status: 'Warning',
color: terminalTheme ? (terminalColors?.warning || '#f59e0b') : '#f59e0b',
icon: <AlertCircle size={20} color="#f59e0b" />
};
} else {
return {
status: 'Critical',
color: terminalTheme ? (terminalColors?.error || '#ef4444') : '#ef4444',
icon: <AlertTriangle size={20} color="#ef4444" />
};
}
}, [errorRate, terminalTheme, terminalColors]);
// Data for semi-circular gauge (using Pie chart)
const gaugeData = [
{ name: 'Errors', value: errorRate, fill: color },
{ name: 'Success', value: Math.max(0, 100 - errorRate), fill: 'rgba(255,255,255,0.1)' }
];
return (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4 }}
>
<Card
sx={{
height: '100%',
background: terminalTheme
? (terminalColors?.background || 'rgba(0,0,0,0.8)')
: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: `1px solid ${terminalTheme
? (terminalColors?.border || 'rgba(255,255,255,0.1)')
: 'rgba(255,255,255,0.1)'}`,
borderRadius: 3,
}}
>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
{icon}
<Typography variant="h6" sx={{ fontWeight: 'bold', color: terminalTheme ? terminalColors?.text : '#ffffff' }}>
Error Rate Gauge
</Typography>
</Box>
<Box sx={{ position: 'relative', height: 200, mb: 2 }}>
<Suspense fallback={<ChartLoadingFallback />}>
<ResponsiveContainer width="100%" height="100%">
<LazyPieChart>
<Pie
data={gaugeData}
cx="50%"
cy="80%"
startAngle={180}
endAngle={0}
innerRadius={60}
outerRadius={80}
dataKey="value"
animationDuration={1000}
animationBegin={0}
>
<Cell fill={color} />
<Cell fill={terminalTheme
? (terminalColors?.backgroundLight || 'rgba(255,255,255,0.1)')
: 'rgba(255,255,255,0.1)'}
/>
</Pie>
</LazyPieChart>
</ResponsiveContainer>
</Suspense>
{/* Center value display */}
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
textAlign: 'center',
mt: 2
}}
>
<Typography
variant="h3"
sx={{
fontWeight: 800,
color: color,
textShadow: terminalTheme ? 'none' : '0 2px 8px rgba(0,0,0,0.3)'
}}
>
{errorRate.toFixed(1)}%
</Typography>
<Typography
variant="caption"
sx={{
color: terminalTheme
? (terminalColors?.textSecondary || 'rgba(255,255,255,0.7)')
: 'rgba(255,255,255,0.7)',
fontSize: '0.7rem',
textTransform: 'uppercase',
letterSpacing: '1px'
}}
>
Error Rate
</Typography>
</Box>
</Box>
{/* Stats */}
<Box sx={{ display: 'flex', justifyContent: 'space-around', gap: 2 }}>
<MuiTooltip title="Total API requests in the last 5 minutes">
<Box sx={{ textAlign: 'center' }}>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: terminalTheme ? terminalColors?.text : '#ffffff'
}}
>
{recentRequests.toLocaleString()}
</Typography>
<Typography
variant="caption"
sx={{
color: terminalTheme
? (terminalColors?.textSecondary || 'rgba(255,255,255,0.7)')
: 'rgba(255,255,255,0.7)',
fontSize: '0.7rem'
}}
>
Requests
</Typography>
</Box>
</MuiTooltip>
<MuiTooltip title="Failed requests in the last 5 minutes">
<Box sx={{ textAlign: 'center' }}>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: color
}}
>
{recentErrors.toLocaleString()}
</Typography>
<Typography
variant="caption"
sx={{
color: terminalTheme
? (terminalColors?.textSecondary || 'rgba(255,255,255,0.7)')
: 'rgba(255,255,255,0.7)',
fontSize: '0.7rem'
}}
>
Errors
</Typography>
</Box>
</MuiTooltip>
<MuiTooltip title="System health status">
<Box sx={{ textAlign: 'center' }}>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: color,
textTransform: 'capitalize'
}}
>
{status}
</Typography>
<Typography
variant="caption"
sx={{
color: terminalTheme
? (terminalColors?.textSecondary || 'rgba(255,255,255,0.7)')
: 'rgba(255,255,255,0.7)',
fontSize: '0.7rem'
}}
>
Status
</Typography>
</Box>
</MuiTooltip>
</Box>
</CardContent>
</Card>
</motion.div>
);
};
export default ErrorRateGauge;

View File

@@ -0,0 +1,154 @@
import React, { useEffect, useState } from 'react';
import { Box, Typography } from '@mui/material';
import { motion, useSpring, useTransform } from 'framer-motion';
import { DollarSign, TrendingUp, TrendingDown } from 'lucide-react';
import { AnimatedNumber } from '../shared/AnimatedNumber';
import { formatCurrency } from '../../services/billingService';
interface LiveCostCounterProps {
currentCost: number;
previousCost?: number;
velocity?: number; // Daily spending rate
terminalTheme?: boolean;
terminalColors?: any;
TypographyComponent?: React.ComponentType<any>;
}
/**
* LiveCostCounter - Animated counter showing real-time cost accumulation
*
* Features:
* - Smooth number animation
* - Velocity indicator (trending up/down)
* - Pulse animation on cost increase
* - Color changes based on velocity
*/
const LiveCostCounter: React.FC<LiveCostCounterProps> = ({
currentCost,
previousCost,
velocity = 0,
terminalTheme = false,
terminalColors,
TypographyComponent = Typography
}) => {
const [hasIncreased, setHasIncreased] = useState(false);
const [pulseKey, setPulseKey] = useState(0);
// Detect cost increase
useEffect(() => {
if (previousCost !== undefined && currentCost > previousCost) {
setHasIncreased(true);
setPulseKey(prev => prev + 1);
const timer = setTimeout(() => setHasIncreased(false), 1000);
return () => clearTimeout(timer);
}
}, [currentCost, previousCost]);
// Calculate velocity trend
const isIncreasing = velocity > 0;
const velocityPercent = Math.abs(velocity);
// Color based on velocity
const getColor = () => {
if (terminalTheme) {
if (velocityPercent > 20) return terminalColors?.error || '#ef4444';
if (velocityPercent > 10) return terminalColors?.warning || '#f59e0b';
return terminalColors?.success || '#22c55e';
}
if (velocityPercent > 20) return '#ef4444';
if (velocityPercent > 10) return '#f59e0b';
return '#22c55e';
};
return (
<motion.div
key={pulseKey}
animate={hasIncreased ? {
scale: [1, 1.05, 1],
} : {}}
transition={{ duration: 0.5 }}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
p: 3,
...(terminalTheme ? {
backgroundColor: terminalColors?.backgroundLight,
borderRadius: 3,
border: `1px solid ${terminalColors?.border || 'rgba(255,255,255,0.1)'}`
} : {
background: 'linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(79, 70, 229, 0.1) 100%)',
borderRadius: 3,
border: '1px solid rgba(102, 126, 234, 0.3)'
})
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<DollarSign
size={24}
color={terminalTheme ? (terminalColors?.text || '#ffffff') : '#667eea'}
/>
<TypographyComponent
variant="caption"
sx={{
color: terminalTheme
? (terminalColors?.textSecondary || 'rgba(255,255,255,0.7)')
: 'rgba(255,255,255,0.7)',
textTransform: 'uppercase',
letterSpacing: '1px',
fontSize: '0.7rem',
fontWeight: 600
}}
>
Live Cost
</TypographyComponent>
</Box>
<TypographyComponent
variant="h3"
sx={{
fontWeight: 800,
color: getColor(),
mb: 1,
textShadow: terminalTheme ? 'none' : '0 2px 8px rgba(0,0,0,0.3)',
display: 'flex',
alignItems: 'baseline',
gap: 0.5
}}
>
<AnimatedNumber
value={currentCost}
format={formatCurrency}
decimals={4}
duration={0.8}
/>
</TypographyComponent>
{previousCost !== undefined && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
{isIncreasing ? (
<TrendingUp size={14} color={getColor()} />
) : (
<TrendingDown size={14} color={getColor()} />
)}
<TypographyComponent
variant="caption"
sx={{
color: getColor(),
fontWeight: 600,
fontSize: '0.75rem'
}}
>
{isIncreasing ? '+' : ''}{velocityPercent.toFixed(1)}% daily rate
</TypographyComponent>
</Box>
)}
</Box>
</motion.div>
);
};
export default LiveCostCounter;

View File

@@ -0,0 +1,210 @@
import React, { Suspense, useMemo } from 'react';
import {
Card,
CardContent,
Typography,
Box,
Chip,
Tooltip as MuiTooltip,
} from '@mui/material';
import { motion } from 'framer-motion';
import {
TrendingUp,
TrendingDown,
DollarSign
} from 'lucide-react';
import {
LazyComposedChart,
Area,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip as RechartsTooltip,
ResponsiveContainer,
Legend,
ChartLoadingFallback
} from '../../utils/lazyRecharts';
// Types
import { UsageTrends as UsageTrendsType } from '../../types/billing';
// Utils
import { formatCurrency } from '../../services/billingService';
interface MultiSeriesCostChartProps {
trends: UsageTrendsType;
monthlyLimit?: number;
}
/**
* MultiSeriesCostChart - Multi-series area chart showing cost breakdown over time
* Displays total cost, provider-specific costs, and budget limit
*/
const MultiSeriesCostChart: React.FC<MultiSeriesCostChartProps> = ({
trends,
monthlyLimit
}) => {
// Transform data for multi-series chart
const chartData = useMemo(() => {
if (!trends.periods || trends.periods.length === 0) {
return [];
}
return trends.periods.map((period, index) => ({
period,
totalCost: trends.total_cost[index] || 0,
calls: trends.total_calls[index] || 0,
tokens: trends.total_tokens[index] || 0,
}));
}, [trends]);
// Calculate trend
const costTrend = useMemo(() => {
if (chartData.length < 2) return 0;
const first = chartData[0].totalCost;
const last = chartData[chartData.length - 1].totalCost;
if (first === 0) return last > 0 ? 100 : 0;
return ((last - first) / first) * 100;
}, [chartData]);
// Custom tooltip
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<Box
sx={{
backgroundColor: 'rgba(0, 0, 0, 0.9)',
color: 'white',
padding: 2,
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)',
minWidth: 200
}}
>
<Typography variant="body2" sx={{ fontWeight: 'bold', mb: 1 }}>
{label}
</Typography>
{payload.map((entry: any, index: number) => (
<Box key={index} sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Box
sx={{
width: 12,
height: 12,
backgroundColor: entry.color,
borderRadius: '50%'
}}
/>
<Typography variant="body2" sx={{ flex: 1 }}>
{entry.name}:
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>
{formatCurrency(entry.value)}
</Typography>
</Box>
))}
</Box>
);
}
return null;
};
if (chartData.length === 0) {
return null;
}
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,
}}
>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<DollarSign size={20} color="#4ade80" />
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
Cost Over Time
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<Chip
icon={costTrend >= 0 ? <TrendingUp size={14} /> : <TrendingDown size={14} />}
label={`${costTrend >= 0 ? '+' : ''}${costTrend.toFixed(1)}%`}
color={costTrend >= 0 ? 'error' : 'success'}
size="small"
/>
</Box>
</Box>
<Suspense fallback={<ChartLoadingFallback />}>
<ResponsiveContainer width="100%" height={350}>
<LazyComposedChart data={chartData} margin={{ top: 5, right: 20, bottom: 5, left: 10 }}>
<defs>
<linearGradient id="colorTotalCost" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#667eea" stopOpacity={0.8}/>
<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.7)"
tick={{ fill: 'rgba(255,255,255,0.7)', fontSize: 12 }}
/>
<YAxis
stroke="rgba(255,255,255,0.7)"
tick={{ fill: 'rgba(255,255,255,0.7)', fontSize: 12 }}
tickFormatter={(value) => formatCurrency(value)}
/>
<RechartsTooltip content={<CustomTooltip />} />
<Legend
wrapperStyle={{ paddingTop: '20px' }}
iconType="line"
/>
{/* Total Cost Area */}
<Area
type="monotone"
dataKey="totalCost"
stroke="#667eea"
strokeWidth={3}
fillOpacity={1}
fill="url(#colorTotalCost)"
name="Total Cost"
animationDuration={1000}
animationBegin={0}
/>
{/* Budget Limit Reference Line */}
{monthlyLimit && monthlyLimit > 0 && (
<Line
type="monotone"
dataKey={() => monthlyLimit}
stroke="#ef4444"
strokeDasharray="5 5"
strokeWidth={2}
dot={false}
name="Budget Limit"
legendType="line"
/>
)}
</LazyComposedChart>
</ResponsiveContainer>
</Suspense>
</CardContent>
</Card>
</motion.div>
);
};
export default MultiSeriesCostChart;

View File

@@ -0,0 +1,171 @@
import React, { Suspense } from 'react';
import {
Box,
Typography,
Tooltip as MuiTooltip,
} from '@mui/material';
import { motion } from 'framer-motion';
import {
LazyBarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip as RechartsTooltip,
ResponsiveContainer,
Cell,
ChartLoadingFallback
} from '../../utils/lazyRecharts';
// Types
import { ProviderBreakdown } from '../../types/billing';
// Utils
import {
formatCurrency,
getProviderColor,
getProviderIcon
} from '../../services/billingService';
interface ProviderCostComparisonProps {
providerBreakdown: ProviderBreakdown;
terminalTheme?: boolean;
terminalColors?: any;
}
/**
* ProviderCostComparison - Horizontal bar chart comparing costs across providers
*
* Usage:
* <ProviderCostComparison providerBreakdown={breakdown} />
*/
const ProviderCostComparison: React.FC<ProviderCostComparisonProps> = ({
providerBreakdown,
terminalTheme = false,
terminalColors
}) => {
// Transform data for chart
const chartData = Object.entries(providerBreakdown)
.filter(([_, data]) => data && (data.cost ?? 0) > 0)
.map(([provider, data]) => ({
name: provider.charAt(0).toUpperCase() + provider.slice(1),
cost: data?.cost ?? 0,
calls: data?.calls ?? 0,
tokens: data?.tokens ?? 0,
color: getProviderColor(provider),
icon: getProviderIcon(provider)
}))
.sort((a, b) => b.cost - a.cost)
.slice(0, 5); // Top 5 providers
if (chartData.length === 0) {
return null;
}
// Custom tooltip
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.cost)}
</Typography>
<Typography variant="body2">
Calls: {data.calls.toLocaleString()}
</Typography>
<Typography variant="body2">
Tokens: {data.tokens.toLocaleString()}
</Typography>
</Box>
);
}
return null;
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.2 }}
>
<Box sx={{ mt: 2 }}>
<Typography
variant="subtitle2"
sx={{
fontWeight: 600,
mb: 1.5,
color: terminalTheme
? (terminalColors?.text || '#ffffff')
: 'rgba(255,255,255,0.9)'
}}
>
Provider Cost Comparison
</Typography>
<Suspense fallback={<ChartLoadingFallback />}>
<ResponsiveContainer width="100%" height={200}>
<LazyBarChart
data={chartData}
layout="vertical"
margin={{ top: 5, right: 20, bottom: 5, left: 60 }}
>
<CartesianGrid strokeDasharray="3 3" stroke={terminalTheme
? (terminalColors?.border || 'rgba(255,255,255,0.1)')
: 'rgba(255,255,255,0.1)'}
/>
<XAxis
type="number"
stroke={terminalTheme
? (terminalColors?.textSecondary || 'rgba(255,255,255,0.7)')
: 'rgba(255,255,255,0.7)'}
tick={{ fill: terminalTheme
? (terminalColors?.textSecondary || 'rgba(255,255,255,0.7)')
: 'rgba(255,255,255,0.7)', fontSize: 11 }}
tickFormatter={(value) => formatCurrency(value)}
/>
<YAxis
type="category"
dataKey="name"
stroke={terminalTheme
? (terminalColors?.textSecondary || 'rgba(255,255,255,0.7)')
: 'rgba(255,255,255,0.7)'}
tick={{ fill: terminalTheme
? (terminalColors?.textSecondary || 'rgba(255,255,255,0.7)')
: 'rgba(255,255,255,0.7)', fontSize: 11 }}
width={55}
/>
<RechartsTooltip content={<CustomTooltip />} />
<Bar
dataKey="cost"
radius={[0, 4, 4, 0]}
animationDuration={800}
animationBegin={0}
>
{chartData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.color}
/>
))}
</Bar>
</LazyBarChart>
</ResponsiveContainer>
</Suspense>
</Box>
</motion.div>
);
};
export default ProviderCostComparison;

View File

@@ -0,0 +1,397 @@
import React, { useState, useEffect, useMemo } from 'react';
import {
Card,
CardContent,
Typography,
Box,
Grid,
Chip,
Tooltip,
CircularProgress,
} from '@mui/material';
import { motion } from 'framer-motion';
import {
FileText,
Image as ImageIcon,
Video,
Search,
Mic,
Code
} from 'lucide-react';
// Types
import { UsageLog } from '../../types/billing';
// Services
import { billingService } from '../../services/billingService';
import { formatCurrency, formatNumber } from '../../services/billingService';
interface ToolCostBreakdownProps {
userId?: string;
terminalTheme?: boolean;
}
// Map endpoints to tool names
const endpointToTool = (endpoint: string): string => {
const endpointLower = endpoint.toLowerCase();
if (endpointLower.includes('blog') || endpointLower.includes('blog-writer')) {
return 'Blog Writer';
}
if (endpointLower.includes('story') || endpointLower.includes('story-writer')) {
return 'Story Writer';
}
if (endpointLower.includes('podcast') || endpointLower.includes('podcast-maker')) {
return 'Podcast Maker';
}
if (endpointLower.includes('image') || endpointLower.includes('image-studio')) {
return 'Image Studio';
}
if (endpointLower.includes('video') || endpointLower.includes('video-studio')) {
return 'Video Studio';
}
if (endpointLower.includes('research') || endpointLower.includes('researcher')) {
return 'Research Tools';
}
if (endpointLower.includes('linkedin')) {
return 'LinkedIn Writer';
}
if (endpointLower.includes('facebook')) {
return 'Facebook Writer';
}
if (endpointLower.includes('seo')) {
return 'SEO Tools';
}
if (endpointLower.includes('audio') || endpointLower.includes('tts')) {
return 'Audio Generation';
}
return 'Other';
};
const getToolIcon = (tool: string) => {
const toolLower = tool.toLowerCase();
if (toolLower.includes('blog')) return <FileText size={18} />;
if (toolLower.includes('story')) return <FileText size={18} />;
if (toolLower.includes('podcast')) return <Mic size={18} />;
if (toolLower.includes('image')) return <ImageIcon size={18} />;
if (toolLower.includes('video')) return <Video size={18} />;
if (toolLower.includes('research')) return <Search size={18} />;
if (toolLower.includes('audio')) return <Mic size={18} />;
return <Code size={18} />;
};
const ToolCostBreakdown: React.FC<ToolCostBreakdownProps> = ({ userId, terminalTheme = false }) => {
const [usageLogs, setUsageLogs] = useState<UsageLog[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchUsageLogs = async () => {
try {
setLoading(true);
setError(null);
// Get current billing period
const currentDate = new Date();
const billingPeriod = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`;
// Fetch usage logs with high limit to get comprehensive data
const response = await billingService.getUsageLogs(1000, 0, undefined, undefined, billingPeriod);
setUsageLogs(response.logs || []);
} catch (err) {
console.error('[ToolCostBreakdown] Error fetching usage logs:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch usage logs');
} finally {
setLoading(false);
}
};
fetchUsageLogs();
}, [userId]);
const toolCosts = useMemo(() => {
const grouped = usageLogs.reduce((acc, log) => {
const tool = endpointToTool(log.endpoint || '');
if (!acc[tool]) {
acc[tool] = { cost: 0, calls: 0, tokens: 0 };
}
acc[tool].cost += log.cost_total || 0;
acc[tool].calls += 1;
acc[tool].tokens += log.tokens_total || 0;
return acc;
}, {} as Record<string, { cost: number; calls: number; tokens: number }>);
return Object.entries(grouped)
.map(([tool, data]) => ({
tool,
...data
}))
.sort((a, b) => b.cost - a.cost)
.filter(item => item.cost > 0); // Only show tools with costs
}, [usageLogs]);
const totalCost = toolCosts.reduce((sum, item) => sum + item.cost, 0);
if (loading) {
return (
<Card sx={{
background: terminalTheme
? 'transparent'
: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
border: terminalTheme ? '1px solid #00ff00' : '1px solid rgba(255,255,255,0.1)',
borderRadius: 3
}}>
<CardContent sx={{ textAlign: 'center', py: 4 }}>
<CircularProgress size={24} sx={{ color: terminalTheme ? '#00ff00' : undefined }} />
<Typography sx={{ mt: 2, color: terminalTheme ? '#00ff00' : 'rgba(255,255,255,0.8)' }}>
Loading tool costs...
</Typography>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card sx={{
background: terminalTheme
? 'transparent'
: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
border: terminalTheme ? '1px solid #ff0000' : '1px solid rgba(255,107,107,0.3)',
borderRadius: 3
}}>
<CardContent sx={{ textAlign: 'center', py: 2 }}>
<Typography sx={{ color: terminalTheme ? '#ff0000' : '#ff6b6b', fontSize: '0.875rem' }}>
{error}
</Typography>
</CardContent>
</Card>
);
}
if (toolCosts.length === 0) {
return (
<Card sx={{
background: terminalTheme
? 'transparent'
: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
border: terminalTheme ? `1px solid ${terminalTheme ? '#00ff00' : 'rgba(255,255,255,0.1)'}` : '1px solid rgba(255,255,255,0.1)',
borderRadius: 3
}}>
<CardContent>
<Typography variant="h6" sx={{ mb: 2, color: terminalTheme ? '#00ff00' : '#ffffff', fontWeight: 'bold' }}>
Cost by Tool
</Typography>
<Typography sx={{ color: terminalTheme ? '#00ff00' : 'rgba(255,255,255,0.7)', fontSize: '0.875rem' }}>
No usage data available yet. Costs will appear here as you use different tools.
</Typography>
</CardContent>
</Card>
);
}
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<Card sx={{
height: '100%',
background: terminalTheme
? 'transparent'
: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: terminalTheme ? 'none' : 'blur(10px)',
border: terminalTheme ? '1px solid #00ff00' : '1px solid rgba(255,255,255,0.1)',
borderRadius: 3,
position: 'relative',
overflow: 'hidden'
}}>
<CardContent>
<Typography variant="h6" sx={{
mb: 3,
color: terminalTheme ? '#00ff00' : '#ffffff',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
gap: 1
}}>
<Code size={20} />
Cost by Tool
</Typography>
<Grid container spacing={2}>
{toolCosts.map((item, index) => {
const percentage = totalCost > 0 ? ((item.cost / totalCost) * 100).toFixed(1) : '0';
return (
<Grid item xs={12} sm={6} key={item.tool}>
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
>
<Tooltip
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
{item.tool} Usage
</Typography>
<Typography variant="body2">
Cost: {formatCurrency(item.cost)} ({percentage}%)
</Typography>
<Typography variant="body2">
Calls: {formatNumber(item.calls)}
</Typography>
<Typography variant="body2">
Tokens: {formatNumber(item.tokens)}
</Typography>
<Typography variant="caption" sx={{ mt: 1, display: 'block', opacity: 0.8 }}>
Avg cost per call: {formatCurrency(item.cost / item.calls)}
</Typography>
</Box>
}
arrow
placement="top"
>
<Box
sx={{
p: 2,
backgroundColor: terminalTheme
? 'rgba(0, 255, 0, 0.05)'
: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: terminalTheme
? '1px solid rgba(0, 255, 0, 0.2)'
: '1px solid rgba(255,255,255,0.1)',
position: 'relative',
cursor: 'help',
transition: 'all 0.2s ease',
'&:hover': {
transform: 'translateY(-2px)',
backgroundColor: terminalTheme
? 'rgba(0, 255, 0, 0.1)'
: 'rgba(255,255,255,0.08)',
border: terminalTheme
? '1px solid rgba(0, 255, 0, 0.4)'
: '1px solid rgba(255,255,255,0.2)'
}
}}
>
{/* Tool Header */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{ color: terminalTheme ? '#00ff00' : '#ffffff' }}>
{getToolIcon(item.tool)}
</Box>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: terminalTheme ? '#00ff00' : '#ffffff' }}>
{item.tool}
</Typography>
</Box>
<Chip
label={`${percentage}%`}
size="small"
sx={{
backgroundColor: terminalTheme
? 'rgba(0, 255, 0, 0.2)'
: 'rgba(74, 222, 128, 0.2)',
color: terminalTheme ? '#00ff00' : '#4ade80',
fontWeight: 'bold',
border: terminalTheme
? '1px solid rgba(0, 255, 0, 0.3)'
: '1px solid rgba(74, 222, 128, 0.3)'
}}
/>
</Box>
{/* Metrics */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" sx={{ color: terminalTheme ? '#00ff00' : 'rgba(255,255,255,0.7)' }}>
Cost:
</Typography>
<Typography variant="caption" sx={{ fontWeight: 'bold', color: terminalTheme ? '#00ff00' : '#ffffff' }}>
{formatCurrency(item.cost)}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" sx={{ color: terminalTheme ? '#00ff00' : 'rgba(255,255,255,0.7)' }}>
Calls:
</Typography>
<Typography variant="caption" sx={{ fontWeight: 'bold', color: terminalTheme ? '#00ff00' : '#ffffff' }}>
{formatNumber(item.calls)}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="caption" sx={{ color: terminalTheme ? '#00ff00' : 'rgba(255,255,255,0.7)' }}>
Tokens:
</Typography>
<Typography variant="caption" sx={{ fontWeight: 'bold', color: terminalTheme ? '#00ff00' : '#ffffff' }}>
{formatNumber(item.tokens)}
</Typography>
</Box>
{/* Progress bar */}
<Box sx={{ mt: 1 }}>
<Box
sx={{
height: 4,
backgroundColor: terminalTheme
? 'rgba(0, 255, 0, 0.1)'
: '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: terminalTheme ? '#00ff00' : '#4ade80',
borderRadius: 2
}}
/>
</Box>
</Box>
</Box>
</Tooltip>
</motion.div>
</Grid>
);
})}
</Grid>
{/* Summary */}
<Box
sx={{
mt: 3,
p: 2,
backgroundColor: terminalTheme
? 'rgba(0, 255, 0, 0.05)'
: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: terminalTheme
? '1px solid rgba(0, 255, 0, 0.2)'
: '1px solid rgba(255,255,255,0.1)'
}}
>
<Typography variant="body2" sx={{ mb: 1, color: terminalTheme ? '#00ff00' : 'rgba(255,255,255,0.8)' }}>
Total Tool Costs
</Typography>
<Typography variant="h5" sx={{ fontWeight: 'bold', color: terminalTheme ? '#00ff00' : '#ffffff' }}>
{formatCurrency(totalCost)}
</Typography>
<Typography variant="caption" sx={{ color: terminalTheme ? '#00ff00' : 'rgba(255,255,255,0.7)' }}>
Across {toolCosts.length} active tool{toolCosts.length !== 1 ? 's' : ''}
</Typography>
</Box>
</CardContent>
</Card>
</motion.div>
);
};
export default ToolCostBreakdown;

View File

@@ -324,7 +324,7 @@ const UsageLogsTable: React.FC<UsageLogsTableProps> = ({ initialLimit = 50 }) =>
</TerminalTypography>
</TerminalTableCell>
<TerminalTableCell>
<TerminalTypography variant="body2" sx={{ textTransform: 'capitalize' }}>
<TerminalTypography variant="body2" sx={{ textTransform: 'capitalize', fontWeight: 'bold' }}>
{log.provider}
</TerminalTypography>
</TerminalTableCell>

View File

@@ -7,6 +7,7 @@ import {
Grid,
Chip,
CircularProgress,
Tooltip as MuiTooltip,
} from '@mui/material';
import {
TrendingUp as TrendingUpIcon,
@@ -21,7 +22,7 @@ import {
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Tooltip as RechartsTooltip,
ResponsiveContainer,
Area,
ChartLoadingFallback
@@ -37,6 +38,10 @@ import {
formatPercentage
} from '../../services/billingService';
// Components
import CostVelocityChart from './CostVelocityChart';
import MultiSeriesCostChart from './MultiSeriesCostChart';
interface UsageTrendsProps {
trends: UsageTrendsType;
projections: CostProjections;
@@ -67,6 +72,28 @@ const UsageTrends: React.FC<UsageTrendsProps> = ({
: chartData[chartData.length - 1].calls > 0 ? 100 : 0
: 0;
// Calculate cost velocity (daily spending rate)
const currentDate = new Date();
const daysInMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate();
const currentDay = currentDate.getDate();
const currentMonthCost = chartData.length > 0 ? chartData[chartData.length - 1].cost : 0;
const avgDailyCost = currentDay > 0 ? currentMonthCost / currentDay : 0;
const projectedMonthlyCost = avgDailyCost * daysInMonth;
const daysRemaining = daysInMonth - currentDay;
// Calculate cost velocity trend (comparing recent vs earlier period)
const recentPeriods = chartData.slice(-3); // Last 3 periods
const earlierPeriods = chartData.slice(0, Math.min(3, chartData.length - 3));
const recentAvgCost = recentPeriods.length > 0
? recentPeriods.reduce((sum, p) => sum + p.cost, 0) / recentPeriods.length
: 0;
const earlierAvgCost = earlierPeriods.length > 0
? earlierPeriods.reduce((sum, p) => sum + p.cost, 0) / earlierPeriods.length
: 0;
const velocityTrend = earlierAvgCost > 0
? ((recentAvgCost - earlierAvgCost) / earlierAvgCost) * 100
: 0;
// Custom tooltip for charts
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
@@ -121,126 +148,163 @@ const UsageTrends: React.FC<UsageTrendsProps> = ({
</CardContent>
<CardContent sx={{ pt: 0 }}>
{/* Growth Indicators */}
{/* Growth Indicators & Cost Velocity */}
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={6}>
<Grid item xs={6} sm={4}>
<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 ? (
<TrendingUpIcon sx={{ fontSize: 16, color: '#22c55e' }} />
) : (
<TrendingDownIcon sx={{ fontSize: 16, color: '#ef4444' }} />
)}
<Typography variant="body2" color="text.secondary">
Cost Growth
</Typography>
</Box>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: costGrowth >= 0 ? '#22c55e' : '#ef4444'
<MuiTooltip title="Percentage change in cost compared to first period" arrow>
<Box component="span"
sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)',
textAlign: 'center',
cursor: 'help'
}}
>
{costGrowth >= 0 ? '+' : ''}{costGrowth.toFixed(1)}%
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, mb: 1 }}>
{costGrowth >= 0 ? (
<TrendingUpIcon sx={{ fontSize: 16, color: '#22c55e' }} />
) : (
<TrendingDownIcon sx={{ fontSize: 16, color: '#ef4444' }} />
)}
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Cost Growth
</Typography>
</Box>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: costGrowth >= 0 ? '#22c55e' : '#ef4444'
}}
>
{costGrowth >= 0 ? '+' : ''}{costGrowth.toFixed(1)}%
</Typography>
</Box>
</MuiTooltip>
</motion.div>
</Grid>
<Grid item xs={6}>
<Grid item xs={6} sm={4}>
<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 ? (
<TrendingUpIcon sx={{ fontSize: 16, color: '#22c55e' }} />
) : (
<TrendingDownIcon sx={{ fontSize: 16, color: '#ef4444' }} />
)}
<Typography variant="body2" color="text.secondary">
Calls Growth
</Typography>
</Box>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: callsGrowth >= 0 ? '#22c55e' : '#ef4444'
<MuiTooltip title="Percentage change in API calls compared to first period" arrow>
<Box component="span"
sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)',
textAlign: 'center',
cursor: 'help'
}}
>
{callsGrowth >= 0 ? '+' : ''}{callsGrowth.toFixed(1)}%
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, mb: 1 }}>
{callsGrowth >= 0 ? (
<TrendingUpIcon sx={{ fontSize: 16, color: '#22c55e' }} />
) : (
<TrendingDownIcon sx={{ fontSize: 16, color: '#ef4444' }} />
)}
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Calls Growth
</Typography>
</Box>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: callsGrowth >= 0 ? '#22c55e' : '#ef4444'
}}
>
{callsGrowth >= 0 ? '+' : ''}{callsGrowth.toFixed(1)}%
</Typography>
</Box>
</MuiTooltip>
</motion.div>
</Grid>
<Grid item xs={12} sm={4}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.2 }}
>
<MuiTooltip
title={
<React.Fragment>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5, display: 'block' }}>
Daily Spending Rate
</Typography>
<Typography variant="body2" sx={{ display: 'block' }}>
Average: {formatCurrency(avgDailyCost)}/day
</Typography>
<Typography variant="body2" sx={{ display: 'block' }}>
Velocity trend: {velocityTrend >= 0 ? '+' : ''}{velocityTrend.toFixed(1)}%
</Typography>
<Typography variant="caption" sx={{ mt: 1, display: 'block', opacity: 0.8 }}>
Based on current month's spending pattern
</Typography>
</React.Fragment>
}
arrow
placement="top"
>
<Box component="span"
sx={{
p: 2,
backgroundColor: 'rgba(102, 126, 234, 0.1)',
borderRadius: 2,
border: '1px solid rgba(102, 126, 234, 0.3)',
textAlign: 'center',
cursor: 'help'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, mb: 1 }}>
<CalendarIcon sx={{ fontSize: 16, color: '#667eea' }} />
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.9)', fontWeight: 500 }}>
Cost Velocity
</Typography>
</Box>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: '#667eea'
}}
>
{formatCurrency(avgDailyCost)}/day
</Typography>
<Typography
variant="caption"
sx={{
color: velocityTrend >= 0 ? '#ef4444' : '#22c55e',
display: 'block',
mt: 0.5
}}
>
{velocityTrend >= 0 ? '' : ''} {Math.abs(velocityTrend).toFixed(1)}% trend
</Typography>
</Box>
</MuiTooltip>
</motion.div>
</Grid>
</Grid>
{/* Cost Trend Chart */}
{/* Multi-Series Cost 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%">
<Suspense fallback={<ChartLoadingFallback />}>
<LazyAreaChart 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)"
/>
</LazyAreaChart>
</Suspense>
</ResponsiveContainer>
</Box>
<MultiSeriesCostChart
trends={trends}
monthlyLimit={projections.cost_limit || 0}
/>
</Box>
{/* API Calls Trend Chart */}
@@ -263,7 +327,7 @@ const UsageTrends: React.FC<UsageTrendsProps> = ({
fontSize={12}
tickFormatter={(value) => formatNumber(value)}
/>
<Tooltip content={<CustomTooltip />} />
<RechartsTooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="calls"
@@ -271,6 +335,8 @@ const UsageTrends: React.FC<UsageTrendsProps> = ({
strokeWidth={2}
dot={{ fill: '#764ba2', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: '#764ba2', strokeWidth: 2 }}
animationDuration={1000}
animationBegin={200}
/>
</LazyLineChart>
</Suspense>
@@ -278,62 +344,167 @@ const UsageTrends: React.FC<UsageTrendsProps> = ({
</Box>
</Box>
{/* Projections */}
{/* Enhanced Projections */}
<Box
sx={{
p: 2,
p: 2.5,
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 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold', display: 'flex', alignItems: 'center', gap: 1, color: '#ffffff' }}>
<CalendarIcon fontSize="small" />
Monthly Projections
Monthly Projections & Forecast
</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 container spacing={2} sx={{ mb: 2 }}>
<Grid item xs={6} sm={3}>
<MuiTooltip
title={
<React.Fragment>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5, display: 'block' }}>
Projected Monthly Cost
</Typography>
<Typography variant="body2" sx={{ display: 'block' }}>
Based on current spending rate: {formatCurrency(avgDailyCost)}/day
</Typography>
<Typography variant="caption" sx={{ mt: 1, display: 'block', opacity: 0.8 }}>
{daysRemaining} days remaining in month
</Typography>
</React.Fragment>
}
arrow
placement="top"
>
<Box component="span" sx={{ textAlign: 'center', cursor: 'help' }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)', mb: 0.5 }}>
Projected Cost
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#667eea' }}>
{formatCurrency(Math.max(projectedMonthlyCost, projections.projected_monthly_cost))}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)', display: 'block', mt: 0.5 }}>
{projectedMonthlyCost > projections.projected_monthly_cost ? '(velocity-based)' : '(trend-based)'}
</Typography>
</Box>
</MuiTooltip>
</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 item xs={6} sm={3}>
<MuiTooltip
title={
<React.Fragment>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5, display: 'block' }}>
Projected Usage Percentage
</Typography>
<Typography variant="body2" sx={{ display: 'block' }}>
Based on projected cost vs monthly limit
</Typography>
</React.Fragment>
}
arrow
placement="top"
>
<Box component="span" sx={{ textAlign: 'center', cursor: 'help' }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)', mb: 0.5 }}>
Usage %
</Typography>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: projections.projected_usage_percentage > 80 ? '#ef4444' :
projections.projected_usage_percentage > 60 ? '#f59e0b' : '#22c55e'
}}
>
{formatPercentage(projections.projected_usage_percentage)}
</Typography>
</Box>
</MuiTooltip>
</Grid>
<Grid item xs={6} sm={3}>
<MuiTooltip
title={
<React.Fragment>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5, display: 'block' }}>
Estimated Days Until Budget Exhaustion
</Typography>
<Typography variant="body2" sx={{ display: 'block' }}>
Based on current daily spending rate
</Typography>
</React.Fragment>
}
arrow
placement="top"
>
<Box component="span" sx={{ textAlign: 'center', cursor: 'help' }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)', mb: 0.5 }}>
Days Remaining
</Typography>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: avgDailyCost > 0 && projections.cost_limit > 0
? (projections.cost_limit / avgDailyCost <= daysRemaining ? '#ef4444' : '#22c55e')
: '#ffffff'
}}
>
{avgDailyCost > 0 && projections.cost_limit > 0
? Math.min(Math.ceil((projections.cost_limit - currentMonthCost) / avgDailyCost), daysRemaining)
: daysRemaining
}
</Typography>
</Box>
</MuiTooltip>
</Grid>
<Grid item xs={6} sm={3}>
<MuiTooltip
title={
<React.Fragment>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5, display: 'block' }}>
Monthly Budget Limit
</Typography>
<Typography variant="body2" sx={{ display: 'block' }}>
Maximum spending allowed this month
</Typography>
</React.Fragment>
}
arrow
placement="top"
>
<Box component="span" sx={{ textAlign: 'center', cursor: 'help' }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)', mb: 0.5 }}>
Budget Limit
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{formatCurrency(projections.cost_limit)}
</Typography>
</Box>
</MuiTooltip>
</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'
{/* Cost Velocity Warning */}
{avgDailyCost > 0 && projectedMonthlyCost > projections.cost_limit && (
<Box
sx={{
mt: 2,
p: 1.5,
backgroundColor: 'rgba(239, 68, 68, 0.1)',
borderRadius: 1,
border: '1px solid rgba(239, 68, 68, 0.3)'
}}
/>
</Box>
>
<Typography variant="caption" sx={{ color: '#ef4444', fontWeight: 500 }}>
⚠️ At current spending rate ({formatCurrency(avgDailyCost)}/day), you'll exceed your monthly budget
in ~{Math.ceil((projections.cost_limit - currentMonthCost) / avgDailyCost)} days.
</Typography>
</Box>
)}
</Box>
</CardContent>
@@ -362,6 +533,15 @@ const UsageTrends: React.FC<UsageTrendsProps> = ({
pointerEvents: 'none'
}}
/>
{/* Cost Velocity Chart */}
<Box sx={{ mt: 3 }}>
<CostVelocityChart
trends={trends}
projections={projections}
monthlyLimit={projections.cost_limit || 0}
/>
</Box>
</Card>
</motion.div>
);