AI Analysis and Content Strategy fixes. Enhanced Strategy Routes refactoring.
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user