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,50 @@
import React, { useEffect } from 'react';
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion';
interface AnimatedNumberProps {
value: number;
format?: (n: number) => string;
duration?: number;
decimals?: number;
prefix?: string;
suffix?: string;
}
/**
* AnimatedNumber - Smoothly animates number changes
*
* Usage:
* <AnimatedNumber value={1234.56} format={(n) => `$${n.toFixed(2)}`} />
* <AnimatedNumber value={1000} prefix="$" decimals={2} />
*/
export const AnimatedNumber: React.FC<AnimatedNumberProps> = ({
value,
format,
duration = 1,
decimals = 0,
prefix = '',
suffix = ''
}) => {
const motionValue = useMotionValue(0);
const spring = useSpring(motionValue, {
stiffness: 50,
damping: 30,
duration: duration * 1000
});
const display = useTransform(spring, (latest) => {
const rounded = decimals > 0 ? Number(latest.toFixed(decimals)) : Math.round(latest);
if (format) {
return format(rounded);
}
return `${prefix}${rounded.toLocaleString()}${suffix}`;
});
useEffect(() => {
motionValue.set(value);
}, [value, motionValue]);
return <motion.span>{display}</motion.span>;
};
export default AnimatedNumber;

View File

@@ -0,0 +1,106 @@
import React from 'react';
import { Box, LinearProgress, Typography } from '@mui/material';
import { motion } from 'framer-motion';
interface AnimatedProgressBarProps {
value: number; // 0-100
maxValue?: number; // Optional max value for display
label?: string;
color?: string;
height?: number;
showLabel?: boolean;
showPercentage?: boolean;
variant?: 'determinate' | 'indeterminate' | 'buffer' | 'query';
}
/**
* AnimatedProgressBar - Progress bar with smooth fill animation
*
* Usage:
* <AnimatedProgressBar value={75} label="Usage" showPercentage />
* <AnimatedProgressBar value={50} color="#4ade80" height={8} />
*/
export const AnimatedProgressBar: React.FC<AnimatedProgressBarProps> = ({
value,
maxValue,
label,
color,
height = 8,
showLabel = false,
showPercentage = false,
variant = 'determinate'
}) => {
// Clamp value between 0 and 100
const clampedValue = Math.min(Math.max(value, 0), 100);
// Get color based on value if not provided
const getColor = () => {
if (color) return color;
if (clampedValue >= 90) return '#ef4444'; // Red
if (clampedValue >= 75) return '#f59e0b'; // Orange
if (clampedValue >= 50) return '#eab308'; // Yellow
return '#22c55e'; // Green
};
const displayValue = maxValue
? `${Math.round(clampedValue)} / ${maxValue}`
: `${Math.round(clampedValue)}%`;
return (
<Box sx={{ width: '100%' }}>
{(showLabel || showPercentage) && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
{showLabel && label && (
<Typography variant="caption" sx={{ fontSize: '0.75rem', opacity: 0.8 }}>
{label}
</Typography>
)}
{showPercentage && (
<Typography variant="caption" sx={{ fontSize: '0.75rem', fontWeight: 'bold' }}>
{displayValue}
</Typography>
)}
</Box>
)}
<Box sx={{ position: 'relative', width: '100%', height }}>
<LinearProgress
variant={variant}
value={clampedValue}
sx={{
height,
borderRadius: height / 2,
backgroundColor: 'rgba(255,255,255,0.1)',
'& .MuiLinearProgress-bar': {
borderRadius: height / 2,
backgroundColor: getColor(),
}
}}
/>
{/* Animated overlay for smoother animation */}
<motion.div
initial={{ scaleX: 0 }}
animate={{ scaleX: clampedValue / 100 }}
transition={{
duration: 1,
ease: "easeOut",
delay: 0.2
}}
style={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
transformOrigin: 'left',
backgroundColor: getColor(),
borderRadius: height / 2,
opacity: 0.3,
pointerEvents: 'none'
}}
/>
</Box>
</Box>
);
};
export default AnimatedProgressBar;

View File

@@ -0,0 +1,148 @@
import React, { Suspense } from 'react';
import { Box, Typography } from '@mui/material';
import {
LazyLineChart,
Line,
XAxis,
YAxis,
Tooltip as RechartsTooltip,
ResponsiveContainer,
ChartLoadingFallback
} from '../../utils/lazyRecharts';
interface MiniSparklineProps {
data: Array<{ date: string; value: number }>;
color: string;
height?: number;
showArea?: boolean;
formatValue?: (value: number) => string;
label?: string;
}
/**
* MiniSparkline - Enhanced trend line chart for metric cards with axes and tooltips
*
* Usage:
* <MiniSparkline
* data={last7DaysData}
* color="#4ade80"
* height={60}
* formatValue={(v) => `$${v.toFixed(2)}`}
* label="Cost"
* />
*/
export const MiniSparkline: React.FC<MiniSparklineProps> = ({
data,
color,
height = 60,
showArea = false,
formatValue = (v) => v.toLocaleString(),
label = 'Value'
}) => {
// Ensure we have data
if (!data || data.length === 0) {
return (
<Box sx={{ height, width: '100%', mt: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.5)', fontSize: '0.7rem' }}>
No data available
</Typography>
</Box>
);
}
// If only one data point, duplicate it for visual consistency
const chartData = data.length === 1
? [data[0], { ...data[0], value: data[0].value }]
: data;
// Calculate min/max for Y-axis domain
const values = chartData.map(d => d.value);
const minValue = Math.min(...values);
const maxValue = Math.max(...values);
const padding = (maxValue - minValue) * 0.1 || 0.1;
// Format date for X-axis
const formatDate = (dateStr: string) => {
try {
const date = new Date(dateStr);
if (isNaN(date.getTime())) return dateStr;
// Show day of month for daily data
return date.getDate().toString();
} catch {
return dateStr;
}
};
// 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.9)',
color: 'white',
padding: 1,
borderRadius: 1,
border: `1px solid ${color}`,
fontSize: '0.75rem'
}}
>
<Typography variant="caption" sx={{ display: 'block', fontWeight: 'bold', mb: 0.5 }}>
{new Date(data.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</Typography>
<Typography variant="caption" sx={{ color: color }}>
{label}: {formatValue(data.value)}
</Typography>
</Box>
);
}
return null;
};
return (
<Box sx={{ height, width: '100%', mt: 1 }}>
<Suspense fallback={<ChartLoadingFallback />}>
<ResponsiveContainer width="100%" height="100%">
<LazyLineChart
data={chartData}
margin={{ top: 5, right: 5, bottom: 20, left: 5 }}
>
<XAxis
dataKey="date"
tickFormatter={formatDate}
stroke="rgba(255,255,255,0.5)"
tick={{ fill: 'rgba(255,255,255,0.5)', fontSize: 10 }}
height={20}
/>
<YAxis
domain={[minValue - padding, maxValue + padding]}
tickFormatter={(value) => {
if (value >= 1000) return `${(value / 1000).toFixed(1)}k`;
if (value >= 1) return value.toFixed(0);
return value.toFixed(2);
}}
stroke="rgba(255,255,255,0.5)"
tick={{ fill: 'rgba(255,255,255,0.5)', fontSize: 10 }}
width={40}
/>
<RechartsTooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="value"
stroke={color}
strokeWidth={2}
dot={{ fill: color, r: 3 }}
activeDot={{ r: 5, fill: color }}
isAnimationActive={true}
animationDuration={1000}
animationBegin={0}
/>
</LazyLineChart>
</ResponsiveContainer>
</Suspense>
</Box>
);
};
export default MiniSparkline;

View File

@@ -0,0 +1,166 @@
/**
* Priority 2 Alert Banner Component
*
* Displays Priority 2 alerts (cost trends, pricing changes, OSS recommendations)
* in a prominent banner format for the main dashboard.
*/
import React from 'react';
import {
Alert,
AlertTitle,
Box,
Button,
IconButton,
Collapse,
Stack,
Chip,
Typography,
} from '@mui/material';
import {
AlertTriangle,
Info,
XCircle,
TrendingUp,
DollarSign,
X,
Lightbulb,
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Priority2Alert } from '../../hooks/usePriority2Alerts';
interface Priority2AlertBannerProps {
alerts: Priority2Alert[];
onDismiss: (alertId: string) => void;
maxAlerts?: number;
}
const Priority2AlertBanner: React.FC<Priority2AlertBannerProps> = ({
alerts,
onDismiss,
maxAlerts = 3
}) => {
const [expanded, setExpanded] = React.useState(true);
const [dismissedIds, setDismissedIds] = React.useState<Set<string>>(new Set());
// Filter out dismissed alerts
const visibleAlerts = alerts.filter(alert => !dismissedIds.has(alert.id));
const displayAlerts = visibleAlerts.slice(0, maxAlerts);
const remainingCount = visibleAlerts.length - maxAlerts;
const getSeverityIcon = (severity: string, type: string) => {
if (type === 'cost_trend') return <TrendingUp size={20} />;
if (type === 'oss_recommendation') return <Lightbulb size={20} />;
if (type === 'pricing_change') return <DollarSign size={20} />;
switch (severity) {
case 'error':
return <XCircle size={20} />;
case 'warning':
return <AlertTriangle size={20} />;
default:
return <Info size={20} />;
}
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'error':
return 'error';
case 'warning':
return 'warning';
default:
return 'info';
}
};
const handleDismiss = (alertId: string) => {
setDismissedIds(prev => new Set([...prev, alertId]));
onDismiss(alertId);
};
if (displayAlerts.length === 0) {
return null;
}
return (
<Box sx={{ mb: 3 }}>
<AnimatePresence>
{displayAlerts.map((alert, index) => (
<motion.div
key={alert.id}
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
>
<Alert
severity={getSeverityColor(alert.severity)}
icon={getSeverityIcon(alert.severity, alert.type)}
action={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{alert.action && (
<Button
size="small"
onClick={alert.action.onClick}
sx={{ textTransform: 'none' }}
>
{alert.action.label}
</Button>
)}
{alert.dismissible && (
<IconButton
size="small"
onClick={() => handleDismiss(alert.id)}
sx={{ ml: 1 }}
>
<X size={16} />
</IconButton>
)}
</Box>
}
sx={{
mb: 2,
'& .MuiAlert-icon': {
alignItems: 'center'
}
}}
>
<AlertTitle sx={{ fontWeight: 'bold', mb: 0.5 }}>
{alert.title}
</AlertTitle>
{alert.message}
{alert.type && (
<Chip
label={alert.type.replace('_', ' ')}
size="small"
sx={{ mt: 1, ml: 1 }}
/>
)}
</Alert>
</motion.div>
))}
</AnimatePresence>
{remainingCount > 0 && (
<Collapse in={expanded}>
<Alert severity="info" sx={{ mt: 1 }}>
<AlertTitle>
{remainingCount} more alert{remainingCount > 1 ? 's' : ''} available
</AlertTitle>
View all alerts in the{' '}
<Button
size="small"
onClick={() => window.location.href = '/billing'}
sx={{ textTransform: 'none' }}
>
Billing Dashboard
</Button>
</Alert>
</Collapse>
)}
</Box>
);
};
export default Priority2AlertBanner;

View File

@@ -111,7 +111,6 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
}
// All checks passed - render protected component
console.log('ProtectedRoute: Access granted (context/local), rendering component');
return <>{children}</>;
};

View File

@@ -23,6 +23,8 @@ import {
} from '@mui/icons-material';
import { apiClient } from '../../api/client';
import { useSubscription } from '../../contexts/SubscriptionContext';
import { usePriority2Alerts } from '../../hooks/usePriority2Alerts';
import Priority2AlertBanner from './Priority2AlertBanner';
interface UsageStats {
total_calls: number;
@@ -84,6 +86,13 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
const userId = localStorage.getItem('user_id');
// Priority 2 Alerts - automatically appears in all tool headers
const { alerts: priority2Alerts, dismissAlert: dismissPriority2Alert } = usePriority2Alerts({
userId: userId || undefined,
enabled: !!userId && subscription?.active,
checkInterval: 120000, // Check every 2 minutes
});
const fetchUsageData = async () => {
if (!userId) return;
@@ -175,19 +184,34 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
);
}
return null;
return <Box />; // Return empty box instead of null
}
if (compact) {
// Compact view - show key metrics as chips
const totalCalls = dashboardData.summary.total_api_calls_this_month;
const totalCost = dashboardData.summary.total_cost_this_month;
// Use current_usage for accurate cost (properly coerced from provider breakdown)
// Fallback to summary if current_usage is not available
const totalCalls = dashboardData.current_usage?.total_calls ?? dashboardData.summary.total_api_calls_this_month;
const totalCost = dashboardData.current_usage?.total_cost ?? dashboardData.summary.total_cost_this_month ?? 0;
const monthlyLimit = dashboardData.limits.limits.monthly_cost;
const usagePercentage = (totalCost / monthlyLimit) * 100;
const usagePercentage = monthlyLimit > 0 ? (totalCost / monthlyLimit) * 100 : 0;
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{/* Total API Calls */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{/* Priority 2 Alerts - Shows cost trends, OSS recommendations, spending velocity */}
{priority2Alerts.length > 0 && (
<Box sx={{ mb: 0.5 }}>
<Priority2AlertBanner
alerts={priority2Alerts}
onDismiss={dismissPriority2Alert}
maxAlerts={1} // Show only 1 alert in compact view
/>
</Box>
)}
{/* Usage Statistics */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{/* Total API Calls */}
<Tooltip title={`${totalCalls.toLocaleString()} API calls this month`}>
<Chip
icon={getUsageStatusIcon(dashboardData.summary.usage_status)}
@@ -298,6 +322,7 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
</Box>
)}
</Menu>
</Box>
</Box>
);
}
@@ -316,7 +341,7 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
Total API Calls
</Typography>
<Typography variant="h4" color="primary">
{dashboardData.summary.total_api_calls_this_month.toLocaleString()}
{(dashboardData.current_usage?.total_calls ?? dashboardData.summary.total_api_calls_this_month).toLocaleString()}
</Typography>
</Box>
@@ -326,7 +351,7 @@ const UsageDashboard: React.FC<UsageDashboardProps> = ({
Monthly Cost
</Typography>
<Typography variant="h4" color="secondary">
${dashboardData.summary.total_cost_this_month.toFixed(2)}
${(dashboardData.current_usage?.total_cost ?? dashboardData.summary.total_cost_this_month ?? 0).toFixed(2)}
</Typography>
<Typography variant="caption" color="text.secondary">
of ${dashboardData.limits.limits.monthly_cost} limit

View File

@@ -0,0 +1,131 @@
import React, { Suspense } from 'react';
import { Box, Typography } from '@mui/material';
import { LazyPieChart, Pie, Cell, ResponsiveContainer, ChartLoadingFallback } from '../../utils/lazyRecharts';
import { motion } from 'framer-motion';
interface UsageLimitRingProps {
used: number;
limit: number;
label: string;
color: string;
size?: number;
showValue?: boolean;
terminalTheme?: boolean;
terminalColors?: any;
}
/**
* UsageLimitRing - Circular progress ring showing usage vs limit
*
* Usage:
* <UsageLimitRing
* used={75}
* limit={100}
* label="Calls"
* color="#4ade80"
* />
*/
export const UsageLimitRing: React.FC<UsageLimitRingProps> = ({
used,
limit,
label,
color,
size = 120,
showValue = true,
terminalTheme = false,
terminalColors
}) => {
const percentage = limit > 0 ? Math.min((used / limit) * 100, 100) : 0;
const remaining = Math.max(0, limit - used);
const data = [
{ name: 'Used', value: used },
{ name: 'Remaining', value: remaining }
];
// Determine color based on percentage
const getRingColor = () => {
if (percentage >= 90) return '#ef4444'; // Red
if (percentage >= 75) return '#f59e0b'; // Orange
if (percentage >= 50) return '#eab308'; // Yellow
return color || '#22c55e'; // Green or provided color
};
const ringColor = getRingColor();
return (
<Box sx={{ position: 'relative', width: size, height: size }}>
<Suspense fallback={<ChartLoadingFallback />}>
<ResponsiveContainer width="100%" height="100%">
<LazyPieChart>
<Pie
data={data}
cx="50%"
cy="50%"
innerRadius={size * 0.35}
outerRadius={size * 0.45}
startAngle={90}
endAngle={-270}
dataKey="value"
animationBegin={0}
animationDuration={1000}
>
<Cell fill={ringColor} />
<Cell fill={terminalTheme
? (terminalColors?.backgroundLight || 'rgba(255,255,255,0.1)')
: 'rgba(255,255,255,0.1)'}
/>
</Pie>
</LazyPieChart>
</ResponsiveContainer>
</Suspense>
<Box sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
textAlign: 'center',
pointerEvents: 'none'
}}>
{showValue && (
<>
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.3, duration: 0.4 }}
>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: terminalTheme
? (terminalColors?.text || '#ffffff')
: '#ffffff',
fontSize: size * 0.15,
lineHeight: 1.2
}}
>
{Math.round(percentage)}%
</Typography>
</motion.div>
<Typography
variant="caption"
sx={{
fontSize: size * 0.08,
color: terminalTheme
? (terminalColors?.textSecondary || 'rgba(255,255,255,0.7)')
: 'rgba(255,255,255,0.7)',
display: 'block',
mt: 0.5
}}
>
{label}
</Typography>
</>
)}
</Box>
</Box>
);
};
export default UsageLimitRing;