520 lines
20 KiB
TypeScript
520 lines
20 KiB
TypeScript
/**
|
||
* Actionable Insights & Recommendations Display Component
|
||
* Shows AI-powered, traffic-focused insights with implementation steps
|
||
*/
|
||
|
||
import React, { useState } from 'react';
|
||
import {
|
||
Box,
|
||
Card,
|
||
CardContent,
|
||
CardActions,
|
||
Typography,
|
||
Chip,
|
||
Button,
|
||
Stack,
|
||
Grid,
|
||
LinearProgress,
|
||
Accordion,
|
||
AccordionSummary,
|
||
AccordionDetails,
|
||
Alert,
|
||
Badge,
|
||
Tooltip,
|
||
IconButton,
|
||
Divider,
|
||
List,
|
||
ListItem,
|
||
ListItemIcon,
|
||
ListItemText,
|
||
} from '@mui/material';
|
||
import {
|
||
ExpandMore as ExpandMoreIcon,
|
||
TrendingUp as TrendingUpIcon,
|
||
Lightbulb as LightbulbIcon,
|
||
CheckCircle as CheckCircleIcon,
|
||
Schedule as ScheduleIcon,
|
||
Flag as FlagIcon,
|
||
BookmarkAdd as BookmarkAddIcon,
|
||
Share as ShareIcon,
|
||
OpenInNew as OpenInNewIcon,
|
||
ArrowRight as ArrowRightIcon,
|
||
} from '@mui/icons-material';
|
||
import { motion, AnimatePresence } from 'framer-motion';
|
||
import { ActionableInsight, TrafficImprovementStrategy } from '../../../api/llmInsightsGenerator';
|
||
|
||
interface ActionableInsightsDisplayProps {
|
||
insights: ActionableInsight[];
|
||
strategies?: TrafficImprovementStrategy[];
|
||
onSaveInsight?: (insight: ActionableInsight) => void;
|
||
onShareInsight?: (insight: ActionableInsight) => void;
|
||
loading?: boolean;
|
||
empty?: boolean;
|
||
}
|
||
|
||
const getEffortColor = (effort: 'easy' | 'medium' | 'complex'): string => {
|
||
const colors: Record<string, string> = {
|
||
easy: '#4caf50',
|
||
medium: '#ff9800',
|
||
complex: '#f44336',
|
||
};
|
||
return colors[effort];
|
||
};
|
||
|
||
const getEffortLabel = (effort: 'easy' | 'medium' | 'complex'): string => {
|
||
const labels: Record<string, string> = {
|
||
easy: 'Easy',
|
||
medium: 'Medium',
|
||
complex: 'Complex',
|
||
};
|
||
return labels[effort];
|
||
};
|
||
|
||
const getImpactColor = (impact: 'high' | 'medium' | 'low'): string => {
|
||
const colors: Record<string, string> = {
|
||
high: '#d32f2f',
|
||
medium: '#f57c00',
|
||
low: '#388e3c',
|
||
};
|
||
return colors[impact];
|
||
};
|
||
|
||
export const ActionableInsightsDisplay: React.FC<ActionableInsightsDisplayProps> = ({
|
||
insights,
|
||
strategies,
|
||
onSaveInsight,
|
||
onShareInsight,
|
||
loading = false,
|
||
empty = false,
|
||
}) => {
|
||
const [savedInsights, setSavedInsights] = useState<Set<string>>(new Set());
|
||
const [expandedInsight, setExpandedInsight] = useState<string | null>(null);
|
||
const [filterImpact, setFilterImpact] = useState<'all' | 'high' | 'medium' | 'low'>('all');
|
||
const [filterEffort, setFilterEffort] = useState<'all' | 'easy' | 'medium' | 'complex'>('all');
|
||
|
||
const handleSaveInsight = (insight: ActionableInsight) => {
|
||
const id = `${insight.title}-${insight.priority}`;
|
||
setSavedInsights(prev => {
|
||
const newSet = new Set(prev);
|
||
if (newSet.has(id)) {
|
||
newSet.delete(id);
|
||
} else {
|
||
newSet.add(id);
|
||
}
|
||
return newSet;
|
||
});
|
||
onSaveInsight?.(insight);
|
||
};
|
||
|
||
const handleShareInsight = (insight: ActionableInsight) => {
|
||
const text = `🎯 ${insight.title}\n\n📊 Impact: ${insight.impact}\n⚙️ Effort: ${insight.effort}\n⏱️ Time: ${insight.timeToImplement}\n\n💡 ${insight.description}`;
|
||
if (navigator.share) {
|
||
navigator.share({
|
||
title: 'SEO Insight',
|
||
text,
|
||
});
|
||
} else {
|
||
// Fallback: copy to clipboard
|
||
navigator.clipboard.writeText(text);
|
||
}
|
||
onShareInsight?.(insight);
|
||
};
|
||
|
||
const filteredInsights = insights.filter(insight => {
|
||
if (filterImpact !== 'all' && insight.impact !== filterImpact) return false;
|
||
if (filterEffort !== 'all' && insight.effort !== filterEffort) return false;
|
||
return true;
|
||
});
|
||
|
||
// Sort by priority (highest first)
|
||
const sortedInsights = [...filteredInsights].sort((a, b) => b.priority - a.priority);
|
||
|
||
if (loading) {
|
||
return (
|
||
<Box sx={{ py: 4, textAlign: 'center' }}>
|
||
<Typography variant="body2" color="textSecondary">
|
||
Generating insights...
|
||
</Typography>
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
if (empty || insights.length === 0) {
|
||
return (
|
||
<Alert severity="info">
|
||
No insights generated yet. Run an audit or analysis to get personalized recommendations.
|
||
</Alert>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.3 }}
|
||
>
|
||
<Box sx={{ py: 3 }}>
|
||
{/* Header */}
|
||
<Box sx={{ mb: 4 }}>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||
<LightbulbIcon sx={{ fontSize: 32, color: '#fbc02d' }} />
|
||
<Typography variant="h5" sx={{ fontWeight: 600 }}>
|
||
Actionable Insights & Recommendations
|
||
</Typography>
|
||
<Badge
|
||
badgeContent={filteredInsights.length}
|
||
color="primary"
|
||
sx={{ ml: 'auto' }}
|
||
/>
|
||
</Box>
|
||
<Typography variant="body2" color="textSecondary">
|
||
{sortedInsights.length} prioritized recommendations to improve your organic traffic
|
||
</Typography>
|
||
</Box>
|
||
|
||
{/* Traffic Impact Summary */}
|
||
<Card sx={{ mb: 4, bgcolor: 'success.lighter', border: '1px solid rgba(76, 175, 80, 0.3)' }}>
|
||
<CardContent>
|
||
<Grid container spacing={2}>
|
||
<Grid item xs={12} sm={6}>
|
||
<Box>
|
||
<Typography variant="body2" color="textSecondary" sx={{ mb: 0.5 }}>
|
||
Estimated Total Traffic Gain
|
||
</Typography>
|
||
<Typography variant="h4" sx={{ color: '#4caf50', fontWeight: 600 }}>
|
||
+{sortedInsights.reduce((sum, i) => sum + i.estimatedTrafficGain, 0).toLocaleString()} visits/month
|
||
</Typography>
|
||
</Box>
|
||
</Grid>
|
||
<Grid item xs={12} sm={6}>
|
||
<Box>
|
||
<Typography variant="body2" color="textSecondary" sx={{ mb: 0.5 }}>
|
||
Quick Wins Available
|
||
</Typography>
|
||
<Typography variant="h4" sx={{ color: '#2196f3', fontWeight: 600 }}>
|
||
{sortedInsights.filter(i => i.effort === 'easy').length} easy implementations
|
||
</Typography>
|
||
</Box>
|
||
</Grid>
|
||
</Grid>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Filters */}
|
||
<Box sx={{ mb: 3, display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||
Filter by:
|
||
</Typography>
|
||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||
<Chip
|
||
label="All"
|
||
size="small"
|
||
variant={filterImpact === 'all' && filterEffort === 'all' ? 'filled' : 'outlined'}
|
||
onClick={() => {
|
||
setFilterImpact('all');
|
||
setFilterEffort('all');
|
||
}}
|
||
/>
|
||
<Chip
|
||
label="High Impact"
|
||
size="small"
|
||
variant={filterImpact === 'high' ? 'filled' : 'outlined'}
|
||
color={filterImpact === 'high' ? 'error' : 'default'}
|
||
onClick={() => setFilterImpact('high')}
|
||
/>
|
||
<Chip
|
||
label="Easy to Implement"
|
||
size="small"
|
||
variant={filterEffort === 'easy' ? 'filled' : 'outlined'}
|
||
color={filterEffort === 'easy' ? 'success' : 'default'}
|
||
onClick={() => setFilterEffort('easy')}
|
||
/>
|
||
<Chip
|
||
label="Quick Wins"
|
||
size="small"
|
||
variant={filterImpact === 'high' && filterEffort === 'easy' ? 'filled' : 'outlined'}
|
||
color={filterImpact === 'high' && filterEffort === 'easy' ? 'primary' : 'default'}
|
||
onClick={() => {
|
||
setFilterImpact('high');
|
||
setFilterEffort('easy');
|
||
}}
|
||
/>
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* Insights Grid */}
|
||
<Grid container spacing={2} sx={{ mb: 4 }}>
|
||
<AnimatePresence>
|
||
{sortedInsights.map((insight, idx) => {
|
||
const insightId = `${insight.title}-${insight.priority}`;
|
||
const isSaved = savedInsights.has(insightId);
|
||
const effortScore = (insight.effort === 'easy' ? 30 : insight.effort === 'medium' ? 60 : 90);
|
||
const impactScore = insight.priority * 10; // priority is 1-10
|
||
|
||
return (
|
||
<Grid item xs={12} md={6} key={idx}>
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -10 }}
|
||
transition={{ delay: idx * 0.05 }}
|
||
>
|
||
<Card
|
||
sx={{
|
||
height: '100%',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
border: `2px solid ${getImpactColor(insight.impact)}`,
|
||
bgcolor: insight.impact === 'high' ? 'error.lighter' : 'background.paper',
|
||
transition: 'all 0.3s ease',
|
||
'&:hover': {
|
||
boxShadow: 3,
|
||
transform: 'translateY(-2px)',
|
||
},
|
||
}}
|
||
>
|
||
<CardContent sx={{ flexGrow: 1 }}>
|
||
{/* Header */}
|
||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, mb: 2 }}>
|
||
<Box sx={{ flex: 1 }}>
|
||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 0.5 }}>
|
||
{insight.title}
|
||
</Typography>
|
||
<Typography variant="body2" color="textSecondary">
|
||
{insight.description}
|
||
</Typography>
|
||
</Box>
|
||
<Tooltip title={isSaved ? 'Remove bookmark' : 'Save insight'}>
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => handleSaveInsight(insight)}
|
||
sx={{
|
||
color: isSaved ? '#fbc02d' : 'action.disabled',
|
||
}}
|
||
>
|
||
<BookmarkAddIcon />
|
||
</IconButton>
|
||
</Tooltip>
|
||
</Box>
|
||
|
||
{/* Metrics */}
|
||
<Grid container spacing={1} sx={{ mb: 2 }}>
|
||
<Grid item xs={6}>
|
||
<Box>
|
||
<Typography variant="caption" color="textSecondary">
|
||
Impact
|
||
</Typography>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.5 }}>
|
||
<TrendingUpIcon
|
||
sx={{
|
||
fontSize: 16,
|
||
color: getImpactColor(insight.impact),
|
||
}}
|
||
/>
|
||
<Chip
|
||
label={insight.impact.toUpperCase()}
|
||
size="small"
|
||
sx={{
|
||
bgcolor: getImpactColor(insight.impact),
|
||
color: 'white',
|
||
}}
|
||
/>
|
||
</Box>
|
||
</Box>
|
||
</Grid>
|
||
<Grid item xs={6}>
|
||
<Box>
|
||
<Typography variant="caption" color="textSecondary">
|
||
Effort
|
||
</Typography>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.5 }}>
|
||
<Chip
|
||
label={getEffortLabel(insight.effort)}
|
||
size="small"
|
||
sx={{
|
||
bgcolor: getEffortColor(insight.effort),
|
||
color: 'white',
|
||
}}
|
||
/>
|
||
</Box>
|
||
</Box>
|
||
</Grid>
|
||
</Grid>
|
||
|
||
{/* Traffic Gain */}
|
||
<Box sx={{ mb: 2, p: 1.5, bgcolor: 'success.lighter', borderRadius: 1 }}>
|
||
<Typography variant="caption" color="textSecondary" display="block">
|
||
Estimated Monthly Traffic Gain
|
||
</Typography>
|
||
<Typography variant="h6" sx={{ color: '#4caf50', fontWeight: 600 }}>
|
||
+{insight.estimatedTrafficGain.toLocaleString()} visits/month
|
||
</Typography>
|
||
</Box>
|
||
|
||
{/* Time to Implement */}
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||
<ScheduleIcon sx={{ fontSize: 18, color: 'action.disabled' }} />
|
||
<Typography variant="body2">
|
||
<strong>Implementation:</strong> {insight.timeToImplement}
|
||
</Typography>
|
||
</Box>
|
||
|
||
{/* Implementation Steps (Expandable) */}
|
||
<Accordion
|
||
onChange={() =>
|
||
setExpandedInsight(
|
||
expandedInsight === insightId ? null : insightId
|
||
)
|
||
}
|
||
sx={{
|
||
boxShadow: 'none',
|
||
border: '1px solid',
|
||
borderColor: 'divider',
|
||
bgcolor: 'transparent',
|
||
}}
|
||
>
|
||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||
<FlagIcon sx={{ mr: 1, fontSize: 18 }} />
|
||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||
Implementation Steps
|
||
</Typography>
|
||
</AccordionSummary>
|
||
<AccordionDetails>
|
||
<List sx={{ py: 0 }}>
|
||
{insight.steps.map((step: string, stepIdx: number) => (
|
||
<ListItem key={stepIdx} sx={{ py: 1, px: 0 }}>
|
||
<ListItemIcon sx={{ minWidth: 32 }}>
|
||
<CheckCircleIcon
|
||
sx={{ fontSize: 18, color: '#4caf50' }}
|
||
/>
|
||
</ListItemIcon>
|
||
<ListItemText
|
||
primary={step}
|
||
primaryTypographyProps={{ variant: 'body2' }}
|
||
/>
|
||
</ListItem>
|
||
))}
|
||
</List>
|
||
</AccordionDetails>
|
||
</Accordion>
|
||
|
||
{/* Tools/Resources */}
|
||
{insight.tools && insight.tools.length > 0 && (
|
||
<Box sx={{ mt: 2 }}>
|
||
<Typography variant="caption" color="textSecondary" display="block" sx={{ mb: 0.5 }}>
|
||
Recommended Tools:
|
||
</Typography>
|
||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||
{insight.tools.map((tool: string, toolIdx: number) => (
|
||
<Chip key={toolIdx} label={tool} size="small" variant="outlined" />
|
||
))}
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
|
||
{/* Priority Badge */}
|
||
<Box sx={{ mt: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||
<Typography variant="caption" color="textSecondary">
|
||
Priority Score:
|
||
</Typography>
|
||
<LinearProgress
|
||
variant="determinate"
|
||
value={Math.min(insight.priority * 10, 100)}
|
||
sx={{ flex: 1 }}
|
||
/>
|
||
<Typography variant="caption" sx={{ fontWeight: 600 }}>
|
||
{insight.priority}/10
|
||
</Typography>
|
||
</Box>
|
||
</CardContent>
|
||
|
||
<Divider />
|
||
|
||
<CardActions>
|
||
<Button
|
||
size="small"
|
||
startIcon={<ShareIcon />}
|
||
onClick={() => handleShareInsight(insight)}
|
||
>
|
||
Share
|
||
</Button>
|
||
<Button
|
||
size="small"
|
||
startIcon={<OpenInNewIcon />}
|
||
href="#"
|
||
target="_blank"
|
||
>
|
||
Learn More
|
||
</Button>
|
||
</CardActions>
|
||
</Card>
|
||
</motion.div>
|
||
</Grid>
|
||
);
|
||
})}
|
||
</AnimatePresence>
|
||
</Grid>
|
||
|
||
{/* Traffic Improvement Strategies */}
|
||
{strategies && strategies.length > 0 && (
|
||
<Box sx={{ mt: 6 }}>
|
||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3 }}>
|
||
🚀 Traffic Improvement Strategies
|
||
</Typography>
|
||
|
||
<Grid container spacing={2}>
|
||
{strategies.map((strategy, idx) => (
|
||
<Grid item xs={12} md={6} key={idx}>
|
||
<Card
|
||
sx={{
|
||
border: `2px solid ${strategy.phase === 'quick_wins' ? '#4caf50' : strategy.phase === 'medium_term' ? '#2196f3' : '#ff9800'}`,
|
||
}}
|
||
>
|
||
<CardContent>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||
{strategy.phase === 'quick_wins' && <FlagIcon sx={{ color: '#4caf50' }} />}
|
||
{strategy.phase === 'medium_term' && <ScheduleIcon sx={{ color: '#2196f3' }} />}
|
||
{strategy.phase === 'long_term' && <TrendingUpIcon sx={{ color: '#ff9800' }} />}
|
||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||
{strategy.title}
|
||
</Typography>
|
||
</Box>
|
||
<Typography variant="body2" sx={{ mb: 2 }}>
|
||
{strategy.description}
|
||
</Typography>
|
||
<Divider sx={{ my: 1 }} />
|
||
<Box sx={{ mt: 2 }}>
|
||
<Typography variant="caption" color="textSecondary" display="block" sx={{ mb: 0.5 }}>
|
||
Key Actions:
|
||
</Typography>
|
||
<Stack spacing={0.5}>
|
||
{strategy.keyActions.map((action: string, actionIdx: number) => (
|
||
<Box key={actionIdx} sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
|
||
<ArrowRightIcon sx={{ fontSize: 16, mt: 0.3, flexShrink: 0 }} />
|
||
<Typography variant="body2">{action}</Typography>
|
||
</Box>
|
||
))}
|
||
</Stack>
|
||
</Box>
|
||
<Box sx={{ mt: 2, p: 1, bgcolor: 'primary.lighter', borderRadius: 1 }}>
|
||
<Typography variant="caption" color="textSecondary" display="block">
|
||
Timeframe: {strategy.timeframe}
|
||
</Typography>
|
||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||
Expected ROI: {strategy.expectedROI}
|
||
</Typography>
|
||
</Box>
|
||
</CardContent>
|
||
</Card>
|
||
</Grid>
|
||
))}
|
||
</Grid>
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
</motion.div>
|
||
);
|
||
};
|
||
|
||
export default ActionableInsightsDisplay;
|