ALwrity version 0.5.5
This commit is contained in:
@@ -38,6 +38,7 @@ import {
|
||||
ServiceStatus,
|
||||
DashboardData
|
||||
} from '../../services/contentPlanningOrchestrator';
|
||||
import { StrategyCalendarProvider } from '../../contexts/StrategyCalendarContext';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -188,7 +189,8 @@ const ContentPlanningDashboard: React.FC = () => {
|
||||
const totalAIItems = (dashboardData.aiInsights?.length || 0) + (dashboardData.aiRecommendations?.length || 0);
|
||||
|
||||
return (
|
||||
<Container maxWidth={false} sx={{ height: '100vh', p: 0 }}>
|
||||
<StrategyCalendarProvider>
|
||||
<Container maxWidth={false} sx={{ height: '100vh', p: 0 }}>
|
||||
<AppBar position="static" color="default" elevation={1}>
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
@@ -325,7 +327,8 @@ const ContentPlanningDashboard: React.FC = () => {
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
</Drawer>
|
||||
</Container>
|
||||
</Container>
|
||||
</StrategyCalendarProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,493 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Slider,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Alert,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Chip,
|
||||
Switch,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import {
|
||||
AutoAwesome as AIIcon,
|
||||
Speed as SpeedIcon,
|
||||
Analytics as AnalyticsIcon,
|
||||
TrendingUp as TrendingIcon,
|
||||
Psychology as PsychologyIcon,
|
||||
Info as InfoIcon,
|
||||
Settings as SettingsIcon,
|
||||
Assessment as AssessmentIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface AdvancedOptionsStepProps {
|
||||
calendarConfig: any;
|
||||
onConfigUpdate: (updates: any) => void;
|
||||
strategyContext?: any;
|
||||
}
|
||||
|
||||
const AdvancedOptionsStep: React.FC<AdvancedOptionsStepProps> = ({
|
||||
calendarConfig,
|
||||
onConfigUpdate,
|
||||
strategyContext
|
||||
}) => {
|
||||
const handlePerformancePredictionChange = (metric: string, value: number) => {
|
||||
const newPredictions = { ...calendarConfig.performancePredictions, [metric]: value };
|
||||
onConfigUpdate({ performancePredictions: newPredictions });
|
||||
};
|
||||
|
||||
const handleAdvancedSettingChange = (setting: string, value: any) => {
|
||||
const newAdvancedSettings = { ...calendarConfig.advancedSettings, [setting]: value };
|
||||
onConfigUpdate({ advancedSettings: newAdvancedSettings });
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Advanced Options & Optimization
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Configure advanced settings for AI-powered optimization, performance predictions, and content strategy enhancement.
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{/* AI Optimization Settings */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={<AIIcon color="primary" />}
|
||||
title="AI Optimization Settings"
|
||||
action={
|
||||
<Tooltip title="Configure AI-powered optimization features">
|
||||
<IconButton size="small">
|
||||
<InfoIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={calendarConfig.advancedSettings?.aiOptimization || false}
|
||||
onChange={(e) => handleAdvancedSettingChange('aiOptimization', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Enable AI Content Optimization"
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
AI will automatically optimize content titles, descriptions, and timing for maximum engagement
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={calendarConfig.advancedSettings?.smartScheduling || false}
|
||||
onChange={(e) => handleAdvancedSettingChange('smartScheduling', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Smart Scheduling"
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Automatically adjust posting times based on audience behavior and engagement patterns
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={calendarConfig.advancedSettings?.trendIntegration || false}
|
||||
onChange={(e) => handleAdvancedSettingChange('trendIntegration', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Trend Integration"
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Incorporate trending topics and hashtags into your content calendar
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={calendarConfig.advancedSettings?.competitiveAnalysis || false}
|
||||
onChange={(e) => handleAdvancedSettingChange('competitiveAnalysis', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Competitive Analysis"
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Monitor competitor content and adjust strategy based on their performance
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Performance Predictions */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={<AnalyticsIcon color="primary" />}
|
||||
title="Performance Predictions"
|
||||
action={
|
||||
<Tooltip title="Set performance targets and predictions">
|
||||
<IconButton size="small">
|
||||
<InfoIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Expected Traffic Growth
|
||||
</Typography>
|
||||
<Slider
|
||||
value={calendarConfig.performancePredictions?.trafficGrowth || 25}
|
||||
onChange={(_, value) => handlePerformancePredictionChange('trafficGrowth', value as number)}
|
||||
min={0}
|
||||
max={100}
|
||||
valueLabelDisplay="auto"
|
||||
marks={[
|
||||
{ value: 0, label: '0%' },
|
||||
{ value: 25, label: '25%' },
|
||||
{ value: 50, label: '50%' },
|
||||
{ value: 75, label: '75%' },
|
||||
{ value: 100, label: '100%' }
|
||||
]}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Predicted increase in website traffic from content calendar
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Engagement Rate Target
|
||||
</Typography>
|
||||
<Slider
|
||||
value={calendarConfig.performancePredictions?.engagementRate || 15}
|
||||
onChange={(_, value) => handlePerformancePredictionChange('engagementRate', value as number)}
|
||||
min={0}
|
||||
max={50}
|
||||
valueLabelDisplay="auto"
|
||||
marks={[
|
||||
{ value: 0, label: '0%' },
|
||||
{ value: 10, label: '10%' },
|
||||
{ value: 20, label: '20%' },
|
||||
{ value: 30, label: '30%' },
|
||||
{ value: 40, label: '40%' },
|
||||
{ value: 50, label: '50%' }
|
||||
]}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Target engagement rate for social media content
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Conversion Rate Target
|
||||
</Typography>
|
||||
<Slider
|
||||
value={calendarConfig.performancePredictions?.conversionRate || 10}
|
||||
onChange={(_, value) => handlePerformancePredictionChange('conversionRate', value as number)}
|
||||
min={0}
|
||||
max={25}
|
||||
valueLabelDisplay="auto"
|
||||
marks={[
|
||||
{ value: 0, label: '0%' },
|
||||
{ value: 5, label: '5%' },
|
||||
{ value: 10, label: '10%' },
|
||||
{ value: 15, label: '15%' },
|
||||
{ value: 20, label: '20%' },
|
||||
{ value: 25, label: '25%' }
|
||||
]}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Target conversion rate from content to leads/sales
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Content Strategy Enhancement */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={<TrendingIcon color="primary" />}
|
||||
title="Content Strategy Enhancement"
|
||||
action={
|
||||
<Tooltip title="Advanced content strategy settings">
|
||||
<IconButton size="small">
|
||||
<InfoIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||
<InputLabel>Content Repurposing Strategy</InputLabel>
|
||||
<Select
|
||||
value={calendarConfig.advancedSettings?.repurposingStrategy || 'moderate'}
|
||||
label="Content Repurposing Strategy"
|
||||
onChange={(e) => handleAdvancedSettingChange('repurposingStrategy', e.target.value)}
|
||||
>
|
||||
<MenuItem value="minimal">Minimal (Original content only)</MenuItem>
|
||||
<MenuItem value="moderate">Moderate (Some repurposing)</MenuItem>
|
||||
<MenuItem value="aggressive">Aggressive (Maximum repurposing)</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
How much content should be repurposed across different formats and platforms
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||
<InputLabel>Content Personalization Level</InputLabel>
|
||||
<Select
|
||||
value={calendarConfig.advancedSettings?.personalizationLevel || 'medium'}
|
||||
label="Content Personalization Level"
|
||||
onChange={(e) => handleAdvancedSettingChange('personalizationLevel', e.target.value)}
|
||||
>
|
||||
<MenuItem value="low">Low (Generic content)</MenuItem>
|
||||
<MenuItem value="medium">Medium (Some personalization)</MenuItem>
|
||||
<MenuItem value="high">High (Highly personalized)</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Level of audience personalization in content creation
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||
<InputLabel>Content Innovation Level</InputLabel>
|
||||
<Select
|
||||
value={calendarConfig.advancedSettings?.innovationLevel || 'balanced'}
|
||||
label="Content Innovation Level"
|
||||
onChange={(e) => handleAdvancedSettingChange('innovationLevel', e.target.value)}
|
||||
>
|
||||
<MenuItem value="conservative">Conservative (Proven formats)</MenuItem>
|
||||
<MenuItem value="balanced">Balanced (Mix of proven and new)</MenuItem>
|
||||
<MenuItem value="innovative">Innovative (Experimental content)</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Balance between proven content formats and experimental approaches
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Audience Behavior Optimization */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={<PsychologyIcon color="primary" />}
|
||||
title="Audience Behavior Optimization"
|
||||
action={
|
||||
<Tooltip title="Optimize for audience behavior patterns">
|
||||
<IconButton size="small">
|
||||
<InfoIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={calendarConfig.advancedSettings?.audienceSegmentation || false}
|
||||
onChange={(e) => handleAdvancedSettingChange('audienceSegmentation', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Audience Segmentation"
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Create different content for different audience segments
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={calendarConfig.advancedSettings?.behavioralTargeting || false}
|
||||
onChange={(e) => handleAdvancedSettingChange('behavioralTargeting', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Behavioral Targeting"
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Target content based on user behavior and preferences
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={calendarConfig.advancedSettings?.journeyMapping || false}
|
||||
onChange={(e) => handleAdvancedSettingChange('journeyMapping', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Customer Journey Mapping"
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Align content with different stages of the customer journey
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={calendarConfig.advancedSettings?.sentimentAnalysis || false}
|
||||
onChange={(e) => handleAdvancedSettingChange('sentimentAnalysis', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Sentiment Analysis"
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Monitor and respond to audience sentiment in content planning
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Performance Monitoring */}
|
||||
<Grid item xs={12}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={<AssessmentIcon color="primary" />}
|
||||
title="Performance Monitoring & Analytics"
|
||||
action={
|
||||
<Tooltip title="Configure performance monitoring settings">
|
||||
<IconButton size="small">
|
||||
<InfoIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Monitoring Frequency</InputLabel>
|
||||
<Select
|
||||
value={calendarConfig.advancedSettings?.monitoringFrequency || 'weekly'}
|
||||
label="Monitoring Frequency"
|
||||
onChange={(e) => handleAdvancedSettingChange('monitoringFrequency', e.target.value)}
|
||||
>
|
||||
<MenuItem value="daily">Daily</MenuItem>
|
||||
<MenuItem value="weekly">Weekly</MenuItem>
|
||||
<MenuItem value="monthly">Monthly</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Alert Threshold</InputLabel>
|
||||
<Select
|
||||
value={calendarConfig.advancedSettings?.alertThreshold || 'medium'}
|
||||
label="Alert Threshold"
|
||||
onChange={(e) => handleAdvancedSettingChange('alertThreshold', e.target.value)}
|
||||
>
|
||||
<MenuItem value="low">Low (Minimal alerts)</MenuItem>
|
||||
<MenuItem value="medium">Medium (Balanced alerts)</MenuItem>
|
||||
<MenuItem value="high">High (Frequent alerts)</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Optimization Frequency</InputLabel>
|
||||
<Select
|
||||
value={calendarConfig.advancedSettings?.optimizationFrequency || 'bi-weekly'}
|
||||
label="Optimization Frequency"
|
||||
onChange={(e) => handleAdvancedSettingChange('optimizationFrequency', e.target.value)}
|
||||
>
|
||||
<MenuItem value="weekly">Weekly</MenuItem>
|
||||
<MenuItem value="bi-weekly">Bi-weekly</MenuItem>
|
||||
<MenuItem value="monthly">Monthly</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<Box>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Key Performance Indicators (KPIs)
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{[
|
||||
'Traffic Growth',
|
||||
'Engagement Rate',
|
||||
'Conversion Rate',
|
||||
'Brand Awareness',
|
||||
'Lead Generation',
|
||||
'Social Reach',
|
||||
'Content Quality Score',
|
||||
'ROI'
|
||||
].map((kpi) => (
|
||||
<Chip
|
||||
key={kpi}
|
||||
label={kpi}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Advanced Settings Summary */}
|
||||
<Alert severity="info" sx={{ mt: 3 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Advanced Configuration Summary
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
AI optimization is {calendarConfig.advancedSettings?.aiOptimization ? 'enabled' : 'disabled'},
|
||||
smart scheduling is {calendarConfig.advancedSettings?.smartScheduling ? 'enabled' : 'disabled'},
|
||||
and performance monitoring is set to {calendarConfig.advancedSettings?.monitoringFrequency || 'weekly'} frequency.
|
||||
Expected traffic growth: {calendarConfig.performancePredictions?.trafficGrowth || 25}%.
|
||||
</Typography>
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedOptionsStep;
|
||||
@@ -0,0 +1,547 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
TextField,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
Alert,
|
||||
FormHelperText
|
||||
} from '@mui/material';
|
||||
import {
|
||||
CalendarToday as CalendarIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
Help as HelpIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
Public as PublicIcon,
|
||||
AccessTime as AccessTimeIcon,
|
||||
ContentPaste as ContentPasteIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
// Import calendar-specific types
|
||||
import { type CalendarConfig } from './types';
|
||||
|
||||
interface CalendarConfigurationStepProps {
|
||||
calendarConfig: CalendarConfig;
|
||||
onConfigUpdate: (updates: Partial<CalendarConfig>) => void;
|
||||
strategyContext?: any;
|
||||
isFromStrategyActivation?: boolean; // Strategy context available for generation
|
||||
}
|
||||
|
||||
// Enhanced styling with better input prominence and readability
|
||||
const ENHANCED_STYLES = {
|
||||
card: {
|
||||
borderRadius: 2,
|
||||
background: 'rgba(255, 255, 255, 0.95)',
|
||||
color: '#333',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.1)',
|
||||
border: '1px solid rgba(0, 0, 0, 0.1)',
|
||||
position: 'relative' as const,
|
||||
overflow: 'hidden',
|
||||
'&:hover': {
|
||||
boxShadow: '0 6px 25px rgba(0, 0, 0, 0.15)',
|
||||
transform: 'translateY(-2px)'
|
||||
},
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
|
||||
},
|
||||
cardContent: {
|
||||
p: 3,
|
||||
position: 'relative' as const,
|
||||
zIndex: 1
|
||||
},
|
||||
sectionHeader: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
mb: 3,
|
||||
'& .MuiTypography-root': {
|
||||
fontWeight: 600,
|
||||
color: '#2c3e50'
|
||||
}
|
||||
},
|
||||
iconContainer: {
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
mr: 2,
|
||||
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
formControl: {
|
||||
'& .MuiInputLabel-root': {
|
||||
color: '#555',
|
||||
fontWeight: 500,
|
||||
'&.Mui-focused': {
|
||||
color: '#667eea'
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
color: '#333',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
'& fieldset': {
|
||||
borderColor: 'rgba(0, 0, 0, 0.2)',
|
||||
borderWidth: '2px'
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: 'rgba(102, 126, 234, 0.5)'
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#667eea',
|
||||
borderWidth: '2px'
|
||||
}
|
||||
},
|
||||
'& .MuiSelect-icon': {
|
||||
color: '#555'
|
||||
}
|
||||
},
|
||||
textField: {
|
||||
'& .MuiInputLabel-root': {
|
||||
color: '#555',
|
||||
fontWeight: 500,
|
||||
'&.Mui-focused': {
|
||||
color: '#667eea'
|
||||
}
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
color: '#333',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
'& fieldset': {
|
||||
borderColor: 'rgba(0, 0, 0, 0.2)',
|
||||
borderWidth: '2px'
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: 'rgba(102, 126, 234, 0.5)'
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#667eea',
|
||||
borderWidth: '2px'
|
||||
}
|
||||
}
|
||||
},
|
||||
platformCard: {
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
border: '2px solid rgba(0, 0, 0, 0.1)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
background: 'rgba(102, 126, 234, 0.05)',
|
||||
border: '2px solid rgba(102, 126, 234, 0.3)',
|
||||
transform: 'translateY(-2px)'
|
||||
},
|
||||
'&.selected': {
|
||||
background: 'linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%)',
|
||||
border: '2px solid rgba(102, 126, 234, 0.5)',
|
||||
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.2)'
|
||||
}
|
||||
},
|
||||
checkbox: {
|
||||
color: '#b0b0b0',
|
||||
'&.Mui-checked': {
|
||||
color: '#667eea'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const CalendarConfigurationStep: React.FC<CalendarConfigurationStepProps> = ({
|
||||
calendarConfig,
|
||||
onConfigUpdate,
|
||||
strategyContext,
|
||||
isFromStrategyActivation = false
|
||||
}) => {
|
||||
// Enhanced calendar-specific handlers
|
||||
const handleCalendarTypeChange = (type: 'weekly' | 'monthly' | 'quarterly') => {
|
||||
onConfigUpdate({ calendarType: type });
|
||||
};
|
||||
|
||||
const handleDurationChange = (duration: number) => {
|
||||
onConfigUpdate({ calendarDuration: duration });
|
||||
};
|
||||
|
||||
const handleStartDateChange = (date: string) => {
|
||||
onConfigUpdate({ startDate: date });
|
||||
};
|
||||
|
||||
const handlePostingFrequencyChange = (frequency: number) => {
|
||||
onConfigUpdate({ postingFrequency: frequency });
|
||||
};
|
||||
|
||||
const handleContentVolumeChange = (volume: number) => {
|
||||
onConfigUpdate({ contentVolume: volume });
|
||||
};
|
||||
|
||||
const handlePlatformChange = (platform: string, checked: boolean) => {
|
||||
let newPlatforms = [...(calendarConfig.priorityPlatforms || [])];
|
||||
if (checked) {
|
||||
newPlatforms.push(platform);
|
||||
} else {
|
||||
newPlatforms = newPlatforms.filter(p => p !== platform);
|
||||
}
|
||||
onConfigUpdate({ priorityPlatforms: newPlatforms });
|
||||
};
|
||||
|
||||
const handleTimeZoneChange = (timezone: string) => {
|
||||
onConfigUpdate({ timeZone: timezone });
|
||||
};
|
||||
|
||||
const handleContentDistributionChange = (distribution: 'even' | 'frontloaded' | 'backloaded') => {
|
||||
onConfigUpdate({ contentDistribution: distribution });
|
||||
};
|
||||
|
||||
const handleReviewCycleChange = (cycle: 'weekly' | 'monthly' | 'quarterly') => {
|
||||
onConfigUpdate({ reviewCycle: cycle });
|
||||
};
|
||||
|
||||
const availablePlatforms = [
|
||||
{ value: 'LinkedIn', label: 'LinkedIn', icon: '💼', description: 'Professional networking and B2B content' },
|
||||
{ value: 'Twitter', label: 'Twitter/X', icon: '🐦', description: 'Real-time updates and engagement' },
|
||||
{ value: 'Facebook', label: 'Facebook', icon: '📘', description: 'Community building and brand awareness' },
|
||||
{ value: 'Instagram', label: 'Instagram', icon: '📸', description: 'Visual content and storytelling' },
|
||||
{ value: 'YouTube', label: 'YouTube', icon: '📺', description: 'Video content and tutorials' },
|
||||
{ value: 'Blog', label: 'Blog/Website', icon: '📝', description: 'Long-form content and SEO' },
|
||||
{ value: 'Email', label: 'Email Newsletter', icon: '📧', description: 'Direct communication and nurturing' }
|
||||
];
|
||||
|
||||
const timeZones = [
|
||||
{ value: 'America/New_York', label: 'Eastern Time (ET)' },
|
||||
{ value: 'America/Chicago', label: 'Central Time (CT)' },
|
||||
{ value: 'America/Denver', label: 'Mountain Time (MT)' },
|
||||
{ value: 'America/Los_Angeles', label: 'Pacific Time (PT)' },
|
||||
{ value: 'Europe/London', label: 'London (GMT/BST)' },
|
||||
{ value: 'Europe/Paris', label: 'Paris (CET/CEST)' },
|
||||
{ value: 'Asia/Tokyo', label: 'Tokyo (JST)' },
|
||||
{ value: 'Asia/Shanghai', label: 'Shanghai (CST)' },
|
||||
{ value: 'Australia/Sydney', label: 'Sydney (AEST/AEDT)' }
|
||||
];
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
{/* Header with Strategy Context */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h4" gutterBottom sx={{
|
||||
fontWeight: 700,
|
||||
color: '#2c3e50',
|
||||
mb: 1
|
||||
}}>
|
||||
Calendar Configuration
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" color="#555" sx={{ mb: 2 }}>
|
||||
Configure your content calendar settings to create an optimized publishing schedule.
|
||||
</Typography>
|
||||
|
||||
{isFromStrategyActivation && (
|
||||
<Alert severity="success" sx={{
|
||||
background: 'rgba(76, 175, 80, 0.1)',
|
||||
border: '1px solid rgba(76, 175, 80, 0.3)',
|
||||
color: '#2e7d32'
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TrendingUpIcon />
|
||||
<Typography variant="body2">
|
||||
Strategy context available - your activated strategy will enhance calendar generation
|
||||
</Typography>
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Basic Calendar Setup */}
|
||||
<Card sx={{ ...ENHANCED_STYLES.card, mb: 3 }}>
|
||||
<CardContent sx={ENHANCED_STYLES.cardContent}>
|
||||
<Box sx={ENHANCED_STYLES.sectionHeader}>
|
||||
<Box sx={ENHANCED_STYLES.iconContainer}>
|
||||
<CalendarIcon sx={{ color: 'white', fontSize: 24 }} />
|
||||
</Box>
|
||||
<Typography variant="h5">
|
||||
Basic Calendar Setup
|
||||
</Typography>
|
||||
<Tooltip title="Configure the fundamental structure of your content calendar">
|
||||
<IconButton size="small" sx={{ ml: 1, color: '#555' }}>
|
||||
<HelpIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<FormControl fullWidth sx={ENHANCED_STYLES.formControl}>
|
||||
<InputLabel>Calendar Type</InputLabel>
|
||||
<Select
|
||||
value={calendarConfig.calendarType}
|
||||
onChange={(e) => handleCalendarTypeChange(e.target.value as 'weekly' | 'monthly' | 'quarterly')}
|
||||
label="Calendar Type"
|
||||
>
|
||||
<MenuItem value="weekly">
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<span>📅</span>
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight={600}>Weekly</Typography>
|
||||
<Typography variant="caption" color="#666">7-day planning cycles</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
<MenuItem value="monthly">
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<span>📆</span>
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight={600}>Monthly</Typography>
|
||||
<Typography variant="caption" color="#666">30-day planning cycles</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
<MenuItem value="quarterly">
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<span>📊</span>
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight={600}>Quarterly</Typography>
|
||||
<Typography variant="caption" color="#666">90-day strategic planning</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
<FormHelperText sx={{ color: '#666' }}>
|
||||
Choose your planning cycle - affects content volume and scheduling
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Start Date"
|
||||
type="date"
|
||||
value={calendarConfig.startDate}
|
||||
onChange={(e) => handleStartDateChange(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={ENHANCED_STYLES.textField}
|
||||
InputProps={{
|
||||
startAdornment: <AccessTimeIcon sx={{ mr: 1, color: '#666' }} />
|
||||
}}
|
||||
/>
|
||||
<FormHelperText sx={{ color: '#666' }}>
|
||||
When should your calendar begin?
|
||||
</FormHelperText>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Duration (periods)"
|
||||
type="number"
|
||||
value={calendarConfig.calendarDuration}
|
||||
onChange={(e) => handleDurationChange(parseInt(e.target.value) || 1)}
|
||||
inputProps={{ min: 1, max: 52 }}
|
||||
sx={ENHANCED_STYLES.textField}
|
||||
InputProps={{
|
||||
startAdornment: <ContentPasteIcon sx={{ mr: 1, color: '#666' }} />
|
||||
}}
|
||||
/>
|
||||
<FormHelperText sx={{ color: '#666' }}>
|
||||
Number of {calendarConfig.calendarType === 'weekly' ? 'weeks' :
|
||||
calendarConfig.calendarType === 'monthly' ? 'months' : 'quarters'} to generate
|
||||
</FormHelperText>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Posts per Week"
|
||||
type="number"
|
||||
value={calendarConfig.postingFrequency}
|
||||
onChange={(e) => handlePostingFrequencyChange(parseInt(e.target.value) || 1)}
|
||||
inputProps={{ min: 1, max: 7 }}
|
||||
sx={ENHANCED_STYLES.textField}
|
||||
InputProps={{
|
||||
startAdornment: <ScheduleIcon sx={{ mr: 1, color: '#666' }} />
|
||||
}}
|
||||
/>
|
||||
<FormHelperText sx={{ color: '#666' }}>
|
||||
How many posts should be published weekly?
|
||||
</FormHelperText>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Content Volume per Period"
|
||||
type="number"
|
||||
value={calendarConfig.contentVolume}
|
||||
onChange={(e) => handleContentVolumeChange(parseInt(e.target.value) || 1)}
|
||||
inputProps={{ min: 1 }}
|
||||
sx={ENHANCED_STYLES.textField}
|
||||
InputProps={{
|
||||
startAdornment: <ContentPasteIcon sx={{ mr: 1, color: '#666' }} />
|
||||
}}
|
||||
/>
|
||||
<FormHelperText sx={{ color: '#666' }}>
|
||||
Total content pieces per {calendarConfig.calendarType === 'weekly' ? 'week' :
|
||||
calendarConfig.calendarType === 'monthly' ? 'month' : 'quarter'}
|
||||
</FormHelperText>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Platform & Scheduling Preferences */}
|
||||
<Card sx={{ ...ENHANCED_STYLES.card, mb: 3 }}>
|
||||
<CardContent sx={ENHANCED_STYLES.cardContent}>
|
||||
<Box sx={ENHANCED_STYLES.sectionHeader}>
|
||||
<Box sx={ENHANCED_STYLES.iconContainer}>
|
||||
<PublicIcon sx={{ color: 'white', fontSize: 24 }} />
|
||||
</Box>
|
||||
<Typography variant="h5">
|
||||
Platform & Scheduling Preferences
|
||||
</Typography>
|
||||
<Tooltip title="Select your content distribution platforms and scheduling preferences">
|
||||
<IconButton size="small" sx={{ ml: 1, color: '#555' }}>
|
||||
<HelpIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6" gutterBottom sx={{ color: '#2c3e50', mb: 2 }}>
|
||||
Priority Platforms
|
||||
</Typography>
|
||||
<Typography variant="body2" color="#666" sx={{ mb: 2 }}>
|
||||
Select the platforms where you'll publish your content. Choose platforms that align with your audience and content strategy.
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{availablePlatforms.map((platform) => (
|
||||
<Grid item xs={12} sm={6} md={4} key={platform.value}>
|
||||
<Card
|
||||
sx={{
|
||||
...ENHANCED_STYLES.platformCard,
|
||||
...(calendarConfig.priorityPlatforms.includes(platform.value) && { className: 'selected' })
|
||||
}}
|
||||
onClick={() => handlePlatformChange(platform.value, !calendarConfig.priorityPlatforms.includes(platform.value))}
|
||||
>
|
||||
<CardContent sx={{ p: 2 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={calendarConfig.priorityPlatforms.includes(platform.value)}
|
||||
onChange={(e) => handlePlatformChange(platform.value, e.target.checked)}
|
||||
sx={ENHANCED_STYLES.checkbox}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
|
||||
<span style={{ fontSize: '1.2rem' }}>{platform.icon}</span>
|
||||
<Typography variant="body2" fontWeight={600} color="#2c3e50">
|
||||
{platform.label}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="caption" color="#666">
|
||||
{platform.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
sx={{ m: 0, width: '100%' }}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<FormControl fullWidth sx={ENHANCED_STYLES.formControl}>
|
||||
<InputLabel>Time Zone</InputLabel>
|
||||
<Select
|
||||
value={calendarConfig.timeZone}
|
||||
onChange={(e) => handleTimeZoneChange(e.target.value)}
|
||||
label="Time Zone"
|
||||
>
|
||||
{timeZones.map((tz) => (
|
||||
<MenuItem key={tz.value} value={tz.value}>
|
||||
{tz.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText sx={{ color: '#666' }}>Your local timezone for accurate scheduling</FormHelperText>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<FormControl fullWidth sx={ENHANCED_STYLES.formControl}>
|
||||
<InputLabel>Content Distribution</InputLabel>
|
||||
<Select
|
||||
value={calendarConfig.contentDistribution}
|
||||
onChange={(e) => handleContentDistributionChange(e.target.value as 'even' | 'frontloaded' | 'backloaded')}
|
||||
label="Content Distribution"
|
||||
>
|
||||
<MenuItem value="even">
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight={600}>Even Distribution</Typography>
|
||||
<Typography variant="caption" color="#666">Consistent posting throughout the period</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
<MenuItem value="frontloaded">
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight={600}>Front-loaded</Typography>
|
||||
<Typography variant="caption" color="#666">More content at the beginning</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
<MenuItem value="backloaded">
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight={600}>Back-loaded</Typography>
|
||||
<Typography variant="caption" color="#666">More content towards the end</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
<FormHelperText sx={{ color: '#666' }}>How should content be distributed across the period?</FormHelperText>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<FormControl fullWidth sx={ENHANCED_STYLES.formControl}>
|
||||
<InputLabel>Review Cycle</InputLabel>
|
||||
<Select
|
||||
value={calendarConfig.reviewCycle}
|
||||
onChange={(e) => handleReviewCycleChange(e.target.value as 'weekly' | 'monthly' | 'quarterly')}
|
||||
label="Review Cycle"
|
||||
>
|
||||
<MenuItem value="weekly">
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight={600}>Weekly</Typography>
|
||||
<Typography variant="caption" color="#666">Review and adjust every week</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
<MenuItem value="monthly">
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight={600}>Monthly</Typography>
|
||||
<Typography variant="caption" color="#666">Review and adjust every month</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
<MenuItem value="quarterly">
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight={600}>Quarterly</Typography>
|
||||
<Typography variant="caption" color="#666">Review and adjust every quarter</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
<FormHelperText sx={{ color: '#666' }}>How often should you review and adjust your calendar?</FormHelperText>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalendarConfigurationStep;
|
||||
@@ -0,0 +1,216 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import {
|
||||
CalendarToday as CalendarIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
Settings as SettingsIcon,
|
||||
CheckCircle as CheckCircleIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
// Import calendar-specific types
|
||||
import { type CalendarConfig } from './types';
|
||||
|
||||
interface DataReviewStepProps {
|
||||
calendarConfig: CalendarConfig;
|
||||
userData: any;
|
||||
strategyContext?: any;
|
||||
onConfigUpdate: (updates: Partial<CalendarConfig>) => void;
|
||||
}
|
||||
|
||||
const DataReviewStep: React.FC<DataReviewStepProps> = ({
|
||||
calendarConfig,
|
||||
userData,
|
||||
strategyContext,
|
||||
onConfigUpdate
|
||||
}) => {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Calendar Setup Review
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Review your calendar configuration before proceeding to generation.
|
||||
</Typography>
|
||||
|
||||
{/* Strategy Context Status */}
|
||||
{strategyContext && (
|
||||
<Paper sx={{ p: 3, mb: 3, bgcolor: 'success.light', color: 'success.contrastText' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<CheckCircleIcon />
|
||||
<Typography variant="h6">Strategy Context Available</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||
Your activated strategy will be used internally during calendar generation for enhanced results.
|
||||
</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Calendar Configuration Summary */}
|
||||
<Grid container spacing={3}>
|
||||
{/* Basic Setup */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<CalendarIcon />
|
||||
Basic Setup
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">Calendar Type:</Typography>
|
||||
<Chip label={calendarConfig.calendarType} size="small" color="primary" />
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">Duration:</Typography>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{calendarConfig.calendarDuration} {calendarConfig.calendarType === 'weekly' ? 'weeks' :
|
||||
calendarConfig.calendarType === 'monthly' ? 'months' : 'quarters'}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">Start Date:</Typography>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{new Date(calendarConfig.startDate).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">Posts per Week:</Typography>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{calendarConfig.postingFrequency}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">Content Volume:</Typography>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{calendarConfig.contentVolume} pieces per period
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Platform & Scheduling */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<ScheduleIcon />
|
||||
Platform & Scheduling
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="body2">Priority Platforms:</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||
{calendarConfig.priorityPlatforms.map((platform) => (
|
||||
<Chip key={platform} label={platform} size="small" variant="outlined" />
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">Time Zone:</Typography>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{calendarConfig.timeZone}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">Content Distribution:</Typography>
|
||||
<Chip label={calendarConfig.contentDistribution} size="small" color="secondary" />
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">Review Cycle:</Typography>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{calendarConfig.reviewCycle}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Generation Options */}
|
||||
<Grid item xs={12}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<SettingsIcon />
|
||||
Generation Options
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="body2">Include Weekends:</Typography>
|
||||
<Chip
|
||||
label={calendarConfig.includeWeekends ? 'Yes' : 'No'}
|
||||
size="small"
|
||||
color={calendarConfig.includeWeekends ? 'success' : 'default'}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="body2">Auto-Schedule:</Typography>
|
||||
<Chip
|
||||
label={calendarConfig.autoSchedule ? 'Yes' : 'No'}
|
||||
size="small"
|
||||
color={calendarConfig.autoSchedule ? 'success' : 'default'}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="body2">Generate Topics:</Typography>
|
||||
<Chip
|
||||
label={calendarConfig.generateTopics ? 'Yes' : 'No'}
|
||||
size="small"
|
||||
color={calendarConfig.generateTopics ? 'success' : 'default'}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Summary */}
|
||||
<Paper sx={{ p: 3, mt: 3, bgcolor: 'info.light', color: 'info.contrastText' }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Ready to Generate
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Your calendar will be generated with {calendarConfig.contentVolume} pieces of content over{' '}
|
||||
{calendarConfig.calendarDuration} {calendarConfig.calendarType === 'weekly' ? 'weeks' :
|
||||
calendarConfig.calendarType === 'monthly' ? 'months' : 'quarters'} with{' '}
|
||||
{calendarConfig.postingFrequency} posts per week on {calendarConfig.priorityPlatforms.length} platforms.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataReviewStep;
|
||||
@@ -0,0 +1,471 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
Alert,
|
||||
Chip,
|
||||
Grid,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
LinearProgress
|
||||
} from '@mui/material';
|
||||
import {
|
||||
PlayArrow as PlayIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Warning as WarningIcon,
|
||||
Info as InfoIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
// Import calendar-specific types
|
||||
import { type CalendarConfig } from './types';
|
||||
|
||||
interface GenerateCalendarStepProps {
|
||||
calendarConfig: CalendarConfig;
|
||||
onGenerateCalendar: (config: CalendarConfig) => void;
|
||||
loading?: boolean;
|
||||
strategyContext?: any;
|
||||
isFromStrategyActivation?: boolean; // Strategy context available for generation
|
||||
}
|
||||
|
||||
const GenerateCalendarStep: React.FC<GenerateCalendarStepProps> = ({
|
||||
calendarConfig,
|
||||
onGenerateCalendar,
|
||||
loading = false,
|
||||
strategyContext,
|
||||
isFromStrategyActivation = false
|
||||
}) => {
|
||||
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [generationOptions, setGenerationOptions] = useState({
|
||||
includeAIOptimization: true,
|
||||
includeSmartScheduling: true,
|
||||
includeTrendIntegration: true,
|
||||
includeCompetitiveAnalysis: true,
|
||||
includePerformanceTracking: true,
|
||||
// Calendar-specific options moved from Step 1
|
||||
includeWeekends: calendarConfig.includeWeekends || false,
|
||||
autoSchedule: calendarConfig.autoSchedule || false,
|
||||
generateTopics: calendarConfig.generateTopics || false
|
||||
});
|
||||
|
||||
// Validate calendar configuration
|
||||
useEffect(() => {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Validate essential calendar configuration
|
||||
if (!calendarConfig.calendarType) {
|
||||
errors.push('Calendar type is required');
|
||||
}
|
||||
|
||||
if (!calendarConfig.startDate) {
|
||||
errors.push('Start date is required');
|
||||
}
|
||||
|
||||
if (calendarConfig.calendarDuration <= 0) {
|
||||
errors.push('Calendar duration must be greater than 0');
|
||||
}
|
||||
|
||||
if (calendarConfig.postingFrequency <= 0) {
|
||||
errors.push('Posting frequency must be greater than 0');
|
||||
}
|
||||
|
||||
if (!calendarConfig.priorityPlatforms || calendarConfig.priorityPlatforms.length === 0) {
|
||||
errors.push('At least one platform must be selected');
|
||||
}
|
||||
|
||||
if (!calendarConfig.timeZone) {
|
||||
errors.push('Time zone is required');
|
||||
}
|
||||
|
||||
setValidationErrors(errors);
|
||||
}, [calendarConfig]);
|
||||
|
||||
const handleGenerate = () => {
|
||||
if (validationErrors.length > 0) {
|
||||
return; // Don't proceed if there are validation errors
|
||||
}
|
||||
|
||||
// Enhanced calendar config with strategy context and generation options
|
||||
const enhancedConfig = {
|
||||
...calendarConfig,
|
||||
// Include calendar-specific options from generation options
|
||||
includeWeekends: generationOptions.includeWeekends,
|
||||
autoSchedule: generationOptions.autoSchedule,
|
||||
generateTopics: generationOptions.generateTopics,
|
||||
strategyContext: isFromStrategyActivation ? {
|
||||
strategyId: strategyContext?.strategyId,
|
||||
strategyData: strategyContext?.strategyData,
|
||||
available: true
|
||||
} : undefined,
|
||||
generationOptions,
|
||||
metadata: {
|
||||
generatedFrom: isFromStrategyActivation ? 'strategy_activation' : 'manual_config',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '3.0'
|
||||
}
|
||||
};
|
||||
|
||||
onGenerateCalendar(enhancedConfig);
|
||||
};
|
||||
|
||||
const canGenerate = validationErrors.length === 0 && !loading;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Generate Your Content Calendar
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Review your configuration and generate your optimized content calendar.
|
||||
</Typography>
|
||||
|
||||
{/* Strategy Context Status */}
|
||||
{isFromStrategyActivation && (
|
||||
<Alert severity="success" sx={{ mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<CheckCircleIcon />
|
||||
<Box>
|
||||
<Typography variant="subtitle2">Strategy Context Available</Typography>
|
||||
<Typography variant="body2">
|
||||
Your activated strategy will be used internally during generation for enhanced results.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Validation Errors */}
|
||||
{validationErrors.length > 0 && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Please fix the following issues:
|
||||
</Typography>
|
||||
<ul style={{ margin: 0, paddingLeft: '20px' }}>
|
||||
{validationErrors.map((error, index) => (
|
||||
<li key={index}>{error}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Configuration Summary */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Configuration Summary
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">Calendar Type:</Typography>
|
||||
<Chip label={calendarConfig.calendarType} size="small" color="primary" />
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">Duration:</Typography>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{calendarConfig.calendarDuration} {calendarConfig.calendarType === 'weekly' ? 'weeks' :
|
||||
calendarConfig.calendarType === 'monthly' ? 'months' : 'quarters'}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">Posts per Week:</Typography>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{calendarConfig.postingFrequency}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">Content Volume:</Typography>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{calendarConfig.contentVolume} pieces per period
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="body2">Platforms:</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||
{calendarConfig.priorityPlatforms.map((platform) => (
|
||||
<Chip key={platform} label={platform} size="small" variant="outlined" />
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">Time Zone:</Typography>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{calendarConfig.timeZone}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">Distribution:</Typography>
|
||||
<Chip label={calendarConfig.contentDistribution} size="small" color="secondary" />
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">Review Cycle:</Typography>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{calendarConfig.reviewCycle}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Generation Options */}
|
||||
<Accordion expanded={showAdvanced} onChange={() => setShowAdvanced(!showAdvanced)}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<InfoIcon />
|
||||
Generation Options
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={generationOptions.includeAIOptimization}
|
||||
onChange={(e) => setGenerationOptions(prev => ({
|
||||
...prev,
|
||||
includeAIOptimization: e.target.checked
|
||||
}))}
|
||||
/>
|
||||
}
|
||||
label="AI Content Optimization"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={generationOptions.includeSmartScheduling}
|
||||
onChange={(e) => setGenerationOptions(prev => ({
|
||||
...prev,
|
||||
includeSmartScheduling: e.target.checked
|
||||
}))}
|
||||
/>
|
||||
}
|
||||
label="Smart Scheduling"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={generationOptions.includeTrendIntegration}
|
||||
onChange={(e) => setGenerationOptions(prev => ({
|
||||
...prev,
|
||||
includeTrendIntegration: e.target.checked
|
||||
}))}
|
||||
/>
|
||||
}
|
||||
label="Trend Integration"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={generationOptions.includeCompetitiveAnalysis}
|
||||
onChange={(e) => setGenerationOptions(prev => ({
|
||||
...prev,
|
||||
includeCompetitiveAnalysis: e.target.checked
|
||||
}))}
|
||||
/>
|
||||
}
|
||||
label="Competitive Analysis"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={generationOptions.includePerformanceTracking}
|
||||
onChange={(e) => setGenerationOptions(prev => ({
|
||||
...prev,
|
||||
includePerformanceTracking: e.target.checked
|
||||
}))}
|
||||
/>
|
||||
}
|
||||
label="Performance Tracking"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Calendar-specific generation options moved from Step 1 */}
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 2, color: 'text.secondary', borderBottom: '1px solid #e0e0e0', pb: 1 }}>
|
||||
Calendar-Specific Options
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={generationOptions.includeWeekends}
|
||||
onChange={(e) => setGenerationOptions(prev => ({
|
||||
...prev,
|
||||
includeWeekends: e.target.checked
|
||||
}))}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight={600}>Include Weekends</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Schedule content on weekends for better engagement
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={generationOptions.autoSchedule}
|
||||
onChange={(e) => setGenerationOptions(prev => ({
|
||||
...prev,
|
||||
autoSchedule: e.target.checked
|
||||
}))}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight={600}>Auto-Schedule Posts</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Automatically assign optimal posting times
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={generationOptions.generateTopics}
|
||||
onChange={(e) => setGenerationOptions(prev => ({
|
||||
...prev,
|
||||
generateTopics: e.target.checked
|
||||
}))}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight={600}>Generate Topics</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
AI-powered topic suggestions for each post
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
{/* What You'll Get */}
|
||||
<Card sx={{ mt: 3, mb: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
What You'll Get
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Box sx={{ textAlign: 'center', p: 2 }}>
|
||||
<Typography variant="h4" color="primary" gutterBottom>
|
||||
{calendarConfig.contentVolume}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Content Pieces
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<Box sx={{ textAlign: 'center', p: 2 }}>
|
||||
<Typography variant="h4" color="primary" gutterBottom>
|
||||
{calendarConfig.calendarDuration}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{calendarConfig.calendarType === 'weekly' ? 'Weeks' :
|
||||
calendarConfig.calendarType === 'monthly' ? 'Months' : 'Quarters'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<Box sx={{ textAlign: 'center', p: 2 }}>
|
||||
<Typography variant="h4" color="primary" gutterBottom>
|
||||
{calendarConfig.priorityPlatforms.length}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Platforms
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
|
||||
Your calendar will include optimized content scheduling, AI-powered topic suggestions,
|
||||
and performance predictions based on your configuration.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Generate Button */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
startIcon={<PlayIcon />}
|
||||
onClick={handleGenerate}
|
||||
disabled={!canGenerate}
|
||||
sx={{ minWidth: 200 }}
|
||||
>
|
||||
{loading ? 'Generating...' : 'Generate Calendar'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Loading Progress */}
|
||||
{loading && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<LinearProgress />
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1, textAlign: 'center' }}>
|
||||
Generating your optimized content calendar...
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateCalendarStep;
|
||||
@@ -0,0 +1,217 @@
|
||||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Button,
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Divider,
|
||||
Chip
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Error as ErrorIcon,
|
||||
Refresh as RefreshIcon,
|
||||
BugReport as BugReportIcon,
|
||||
Home as HomeIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
onReset?: () => void;
|
||||
onGoHome?: () => void;
|
||||
stepName?: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
errorInfo: ErrorInfo | null;
|
||||
errorId: string;
|
||||
}
|
||||
|
||||
export class WizardErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
errorId: ''
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Partial<State> {
|
||||
// Generate a unique error ID for tracking
|
||||
const errorId = `wizard-error-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
return {
|
||||
hasError: true,
|
||||
error,
|
||||
errorId
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
// Log error to console in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Wizard Error Boundary caught an error:', error, errorInfo);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
errorInfo
|
||||
});
|
||||
|
||||
// In a real application, you would send this to your error reporting service
|
||||
// Example: Sentry.captureException(error, { extra: errorInfo });
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
this.setState({
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
errorId: ''
|
||||
});
|
||||
|
||||
if (this.props.onReset) {
|
||||
this.props.onReset();
|
||||
}
|
||||
};
|
||||
|
||||
handleGoHome = () => {
|
||||
if (this.props.onGoHome) {
|
||||
this.props.onGoHome();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
const { error, errorInfo, errorId } = this.state;
|
||||
const { stepName } = this.props;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3, maxWidth: 800, mx: 'auto' }}>
|
||||
<Card sx={{
|
||||
border: '2px solid #f44336',
|
||||
boxShadow: '0 8px 32px rgba(244, 67, 54, 0.2)'
|
||||
}}>
|
||||
<CardContent sx={{ p: 4 }}>
|
||||
{/* Error Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
<ErrorIcon color="error" sx={{ fontSize: 40 }} />
|
||||
<Box>
|
||||
<Typography variant="h5" color="error" gutterBottom>
|
||||
Something went wrong
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{stepName ? `Error in ${stepName} step` : 'Error in Calendar Wizard'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
{/* Error Details */}
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
<AlertTitle>Error Details</AlertTitle>
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
{error?.message || 'An unexpected error occurred'}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`Error ID: ${errorId}`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
icon={<BugReportIcon />}
|
||||
/>
|
||||
</Alert>
|
||||
|
||||
{/* Recovery Options */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
What you can do:
|
||||
</Typography>
|
||||
<Box component="ul" sx={{ pl: 2 }}>
|
||||
<Typography component="li" variant="body2" sx={{ mb: 1 }}>
|
||||
Try refreshing the wizard to start over
|
||||
</Typography>
|
||||
<Typography component="li" variant="body2" sx={{ mb: 1 }}>
|
||||
Go back to the main dashboard
|
||||
</Typography>
|
||||
<Typography component="li" variant="body2" sx={{ mb: 1 }}>
|
||||
Check your internet connection
|
||||
</Typography>
|
||||
<Typography component="li" variant="body2">
|
||||
Contact support if the problem persists
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={this.handleReset}
|
||||
sx={{ minWidth: 140 }}
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<HomeIcon />}
|
||||
onClick={this.handleGoHome}
|
||||
sx={{ minWidth: 140 }}
|
||||
>
|
||||
Go to Dashboard
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Development Error Stack */}
|
||||
{process.env.NODE_ENV === 'development' && errorInfo && (
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Error Stack (Development Only)
|
||||
</Typography>
|
||||
<Box
|
||||
component="pre"
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: 'grey.100',
|
||||
borderRadius: 1,
|
||||
fontSize: '0.75rem',
|
||||
overflow: 'auto',
|
||||
maxHeight: 200
|
||||
}}
|
||||
>
|
||||
{errorInfo.componentStack}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
// Higher-order component for wrapping individual steps
|
||||
export const withErrorBoundary = <P extends object>(
|
||||
WrappedComponent: React.ComponentType<P>,
|
||||
stepName?: string
|
||||
) => {
|
||||
return class WithErrorBoundary extends Component<P> {
|
||||
render() {
|
||||
return (
|
||||
<WizardErrorBoundary stepName={stepName}>
|
||||
<WrappedComponent {...this.props} />
|
||||
</WizardErrorBoundary>
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,294 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
CircularProgress,
|
||||
Chip,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import {
|
||||
HourglassEmpty as HourglassIcon,
|
||||
TrendingUp as TrendingIcon,
|
||||
CheckCircle as CheckIcon,
|
||||
Error as ErrorIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface LoadingStep {
|
||||
id: string;
|
||||
label: string;
|
||||
status: 'pending' | 'loading' | 'completed' | 'error';
|
||||
progress?: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface WizardLoadingStateProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
steps?: LoadingStep[];
|
||||
overallProgress?: number;
|
||||
isGenerating?: boolean;
|
||||
error?: string | null;
|
||||
onRetry?: () => void;
|
||||
}
|
||||
|
||||
const LoadingStepItem: React.FC<{ step: LoadingStep }> = ({ step }) => {
|
||||
const getStatusIcon = () => {
|
||||
switch (step.status) {
|
||||
case 'completed':
|
||||
return <CheckIcon color="success" fontSize="small" />;
|
||||
case 'loading':
|
||||
return <CircularProgress size={16} />;
|
||||
case 'error':
|
||||
return <ErrorIcon color="error" fontSize="small" />;
|
||||
default:
|
||||
return <HourglassIcon color="disabled" fontSize="small" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (step.status) {
|
||||
case 'completed':
|
||||
return 'success';
|
||||
case 'loading':
|
||||
return 'primary';
|
||||
case 'error':
|
||||
return 'error';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
bgcolor: step.status === 'loading' ? 'action.hover' : 'transparent',
|
||||
border: step.status === 'loading' ? '1px solid' : 'none',
|
||||
borderColor: 'primary.main'
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{getStatusIcon()}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||
{step.label}
|
||||
</Typography>
|
||||
{step.message && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{step.message}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Chip
|
||||
label={step.status}
|
||||
size="small"
|
||||
color={getStatusColor() as any}
|
||||
variant={step.status === 'pending' ? 'outlined' : 'filled'}
|
||||
/>
|
||||
|
||||
{step.progress !== undefined && step.status === 'loading' && (
|
||||
<Box sx={{ width: 100 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={step.progress}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const WizardLoadingState: React.FC<WizardLoadingStateProps> = ({
|
||||
title,
|
||||
subtitle,
|
||||
steps = [],
|
||||
overallProgress,
|
||||
isGenerating = false,
|
||||
error,
|
||||
onRetry
|
||||
}) => {
|
||||
const defaultSteps: LoadingStep[] = [
|
||||
{
|
||||
id: 'analyzing',
|
||||
label: 'Analyzing your strategy data',
|
||||
status: 'loading',
|
||||
progress: 25,
|
||||
message: 'Processing content pillars and target audience'
|
||||
},
|
||||
{
|
||||
id: 'configuring',
|
||||
label: 'Configuring calendar settings',
|
||||
status: 'pending',
|
||||
message: 'Setting up content mix and timing preferences'
|
||||
},
|
||||
{
|
||||
id: 'generating',
|
||||
label: 'Generating content calendar',
|
||||
status: 'pending',
|
||||
message: 'Creating optimized content schedule'
|
||||
},
|
||||
{
|
||||
id: 'optimizing',
|
||||
label: 'Optimizing for performance',
|
||||
status: 'pending',
|
||||
message: 'Applying AI-driven optimization'
|
||||
}
|
||||
];
|
||||
|
||||
const displaySteps = steps.length > 0 ? steps : defaultSteps;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3, maxWidth: 800, mx: 'auto' }}>
|
||||
<Card sx={{
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.1)',
|
||||
border: '1px solid',
|
||||
borderColor: 'divider'
|
||||
}}>
|
||||
<CardContent sx={{ p: 4 }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ textAlign: 'center', mb: 4 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mb: 2 }}>
|
||||
{isGenerating ? (
|
||||
<CircularProgress size={60} thickness={4} />
|
||||
) : (
|
||||
<TrendingIcon sx={{ fontSize: 60, color: 'primary.main' }} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Typography variant="h5" gutterBottom>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
{subtitle && (
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{subtitle}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{ mb: 3 }}
|
||||
action={
|
||||
onRetry && (
|
||||
<Box component="button" onClick={onRetry} sx={{
|
||||
border: 'none',
|
||||
bgcolor: 'transparent',
|
||||
color: 'inherit',
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'underline'
|
||||
}}>
|
||||
Retry
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Overall Progress */}
|
||||
{overallProgress !== undefined && (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Overall Progress
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{Math.round(overallProgress)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={overallProgress}
|
||||
sx={{ height: 8, borderRadius: 4 }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Loading Steps */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{displaySteps.map((step, index) => (
|
||||
<LoadingStepItem key={step.id} step={step} />
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Additional Info */}
|
||||
<Box sx={{ mt: 4, p: 2, bgcolor: 'grey.50', borderRadius: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center' }}>
|
||||
{isGenerating
|
||||
? 'Please wait while we generate your content calendar. This may take a few moments.'
|
||||
: 'We\'re preparing your content calendar with the latest AI-powered optimizations.'
|
||||
}
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// Specialized loading states for different wizard operations
|
||||
export const CalendarGenerationLoading: React.FC<{ progress?: number; error?: string }> = ({
|
||||
progress,
|
||||
error
|
||||
}) => (
|
||||
<WizardLoadingState
|
||||
title="Generating Your Content Calendar"
|
||||
subtitle="AI is creating an optimized content schedule based on your strategy"
|
||||
overallProgress={progress}
|
||||
isGenerating={true}
|
||||
error={error}
|
||||
steps={[
|
||||
{
|
||||
id: 'validating',
|
||||
label: 'Validating configuration',
|
||||
status: progress && progress > 0 ? 'completed' : 'loading',
|
||||
progress: progress && progress > 0 ? 100 : 50
|
||||
},
|
||||
{
|
||||
id: 'processing',
|
||||
label: 'Processing strategy data',
|
||||
status: progress && progress > 20 ? 'completed' : progress && progress > 10 ? 'loading' : 'pending',
|
||||
progress: progress && progress > 10 ? Math.min(100, (progress - 10) * 5) : 0
|
||||
},
|
||||
{
|
||||
id: 'generating',
|
||||
label: 'Generating content schedule',
|
||||
status: progress && progress > 50 ? 'completed' : progress && progress > 30 ? 'loading' : 'pending',
|
||||
progress: progress && progress > 30 ? Math.min(100, (progress - 30) * 5) : 0
|
||||
},
|
||||
{
|
||||
id: 'optimizing',
|
||||
label: 'Optimizing for performance',
|
||||
status: progress && progress > 80 ? 'completed' : progress && progress > 60 ? 'loading' : 'pending',
|
||||
progress: progress && progress > 60 ? Math.min(100, (progress - 60) * 5) : 0
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
export const DataProcessingLoading: React.FC<{ message?: string }> = ({ message }) => (
|
||||
<WizardLoadingState
|
||||
title="Processing Your Data"
|
||||
subtitle="Analyzing your strategy and preparing calendar configuration"
|
||||
steps={[
|
||||
{
|
||||
id: 'loading',
|
||||
label: message || 'Loading and validating data',
|
||||
status: 'loading',
|
||||
progress: 75
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,289 @@
|
||||
import { useState, useCallback, useMemo, useRef } from 'react';
|
||||
import { CalendarConfig, WizardStep, ValidationError, WizardState, WizardActions } from '../types';
|
||||
|
||||
// All interfaces are now imported from types.ts
|
||||
|
||||
export const useCalendarWizardState = (
|
||||
onGenerateCalendar: (calendarConfig: CalendarConfig) => void
|
||||
): [WizardState, WizardActions] => {
|
||||
// Store the callback in a ref to prevent it from causing re-renders
|
||||
const onGenerateCalendarRef = useRef(onGenerateCalendar);
|
||||
onGenerateCalendarRef.current = onGenerateCalendar;
|
||||
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [calendarConfig, setCalendarConfig] = useState<CalendarConfig>(() => createDefaultConfig());
|
||||
const [validationErrors, setValidationErrors] = useState<ValidationError[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [generationProgress, setGenerationProgress] = useState(0);
|
||||
|
||||
// Define steps with validation - streamlined 2-step wizard
|
||||
const steps: WizardStep[] = useMemo(() => [
|
||||
{
|
||||
label: 'Calendar Configuration',
|
||||
icon: null, // Will be set by parent component
|
||||
description: 'Configure your calendar settings and preferences',
|
||||
isCompleted: false,
|
||||
hasErrors: false
|
||||
},
|
||||
{
|
||||
label: 'Generate Calendar',
|
||||
icon: null,
|
||||
description: 'Review and generate your optimized content calendar',
|
||||
isCompleted: false,
|
||||
hasErrors: false
|
||||
}
|
||||
], []);
|
||||
|
||||
// Validation rules for each step - streamlined 2-step validation
|
||||
const validationRules = useMemo(() => ({
|
||||
0: (config: CalendarConfig): ValidationError[] => {
|
||||
const errors: ValidationError[] = [];
|
||||
// Basic calendar setup validation
|
||||
if (!config.calendarType) {
|
||||
errors.push({ field: 'calendarType', message: 'Calendar type is required', step: 0 });
|
||||
}
|
||||
if (!config.startDate) {
|
||||
errors.push({ field: 'startDate', message: 'Start date is required', step: 0 });
|
||||
}
|
||||
if (config.calendarDuration <= 0) {
|
||||
errors.push({ field: 'calendarDuration', message: 'Calendar duration must be greater than 0', step: 0 });
|
||||
}
|
||||
if (config.postingFrequency <= 0) {
|
||||
errors.push({ field: 'postingFrequency', message: 'Posting frequency must be greater than 0', step: 0 });
|
||||
}
|
||||
if (config.contentVolume <= 0) {
|
||||
errors.push({ field: 'contentVolume', message: 'Content volume must be greater than 0', step: 0 });
|
||||
}
|
||||
// Platform and scheduling validation
|
||||
if (config.priorityPlatforms.length === 0) {
|
||||
errors.push({ field: 'priorityPlatforms', message: 'At least one platform is required', step: 0 });
|
||||
}
|
||||
if (!config.timeZone) {
|
||||
errors.push({ field: 'timeZone', message: 'Time zone is required', step: 0 });
|
||||
}
|
||||
if (!config.contentDistribution) {
|
||||
errors.push({ field: 'contentDistribution', message: 'Content distribution is required', step: 0 });
|
||||
}
|
||||
if (!config.reviewCycle) {
|
||||
errors.push({ field: 'reviewCycle', message: 'Review cycle is required', step: 0 });
|
||||
}
|
||||
return errors;
|
||||
},
|
||||
1: (config: CalendarConfig): ValidationError[] => {
|
||||
// Step 1 is the generation step, no additional validation needed
|
||||
return [];
|
||||
}
|
||||
}), []);
|
||||
|
||||
// Update calendar configuration
|
||||
const updateCalendarConfig = useCallback((updates: Partial<CalendarConfig>) => {
|
||||
setCalendarConfig(prev => ({ ...prev, ...updates }));
|
||||
// Clear validation errors for updated fields
|
||||
setValidationErrors(prev => prev.filter(error =>
|
||||
!Object.keys(updates).some(key => error.field.startsWith(key))
|
||||
));
|
||||
}, []);
|
||||
|
||||
// Validate a specific step
|
||||
const validateStep = useCallback((step: number): boolean => {
|
||||
const validator = validationRules[step as keyof typeof validationRules];
|
||||
if (!validator) return true;
|
||||
|
||||
const errors = validator(calendarConfig);
|
||||
setValidationErrors(prev => {
|
||||
const filtered = prev.filter(error => error.step !== step);
|
||||
return [...filtered, ...errors];
|
||||
});
|
||||
|
||||
return errors.length === 0;
|
||||
}, [calendarConfig, validationRules]);
|
||||
|
||||
// Validate all steps
|
||||
const validateAllSteps = useCallback((): boolean => {
|
||||
const allErrors: ValidationError[] = [];
|
||||
Object.keys(validationRules).forEach(stepKey => {
|
||||
const step = parseInt(stepKey);
|
||||
const validator = validationRules[step as keyof typeof validationRules];
|
||||
if (validator) {
|
||||
allErrors.push(...validator(calendarConfig));
|
||||
}
|
||||
});
|
||||
setValidationErrors(allErrors);
|
||||
return allErrors.length === 0;
|
||||
}, [calendarConfig, validationRules]);
|
||||
|
||||
// Clear all errors
|
||||
const clearErrors = useCallback(() => {
|
||||
setValidationErrors([]);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
// Check if user can proceed to a specific step
|
||||
const canProceedToStep = useCallback((step: number): boolean => {
|
||||
// Can always go back
|
||||
if (step < activeStep) return true;
|
||||
|
||||
// Can't skip steps
|
||||
if (step > activeStep + 1) return false;
|
||||
|
||||
// Validate current step before proceeding
|
||||
if (step === activeStep + 1) {
|
||||
// Inline validation to avoid circular dependency
|
||||
const validator = validationRules[activeStep as keyof typeof validationRules];
|
||||
if (!validator) return true;
|
||||
|
||||
const errors = validator(calendarConfig);
|
||||
return errors.length === 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [activeStep, calendarConfig, validationRules]);
|
||||
|
||||
// Get step status
|
||||
const getStepStatus = useCallback((step: number): 'completed' | 'current' | 'pending' | 'error' => {
|
||||
if (step === activeStep) return 'current';
|
||||
if (step < activeStep) return 'completed';
|
||||
if (validationErrors.some(error => error.step === step)) return 'error';
|
||||
return 'pending';
|
||||
}, [activeStep, validationErrors]);
|
||||
|
||||
// Reset wizard to initial state
|
||||
const resetWizard = useCallback(() => {
|
||||
setActiveStep(0);
|
||||
setCalendarConfig(createDefaultConfig());
|
||||
setValidationErrors([]);
|
||||
setIsLoading(false);
|
||||
setError(null);
|
||||
setIsGenerating(false);
|
||||
setGenerationProgress(0);
|
||||
}, []); // Remove initialConfig from dependencies to prevent infinite loop
|
||||
|
||||
// Enhanced step navigation with validation
|
||||
const setActiveStepWithValidation = useCallback((step: number) => {
|
||||
if (canProceedToStep(step)) {
|
||||
setActiveStep(step);
|
||||
clearErrors();
|
||||
}
|
||||
}, [canProceedToStep, clearErrors]);
|
||||
|
||||
// Generate calendar with progress tracking
|
||||
const generateCalendar = useCallback(async () => {
|
||||
if (!validateAllSteps()) {
|
||||
setError('Please fix validation errors before generating calendar');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGenerating(true);
|
||||
setGenerationProgress(0);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Simulate progress updates
|
||||
const progressInterval = setInterval(() => {
|
||||
setGenerationProgress(prev => {
|
||||
if (prev >= 90) {
|
||||
clearInterval(progressInterval);
|
||||
return 90;
|
||||
}
|
||||
return prev + 10;
|
||||
});
|
||||
}, 200);
|
||||
|
||||
await onGenerateCalendarRef.current(calendarConfig);
|
||||
|
||||
clearInterval(progressInterval);
|
||||
setGenerationProgress(100);
|
||||
|
||||
// Reset after successful generation
|
||||
setTimeout(() => {
|
||||
setIsGenerating(false);
|
||||
setGenerationProgress(0);
|
||||
}, 1000);
|
||||
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to generate calendar');
|
||||
setIsGenerating(false);
|
||||
setGenerationProgress(0);
|
||||
}
|
||||
}, [calendarConfig, validateAllSteps]); // Remove onGenerateCalendar dependency
|
||||
|
||||
const state: WizardState = useMemo(() => ({
|
||||
activeStep,
|
||||
calendarConfig,
|
||||
steps,
|
||||
validationErrors,
|
||||
isLoading,
|
||||
error,
|
||||
isGenerating,
|
||||
generationProgress
|
||||
}), [
|
||||
activeStep,
|
||||
calendarConfig,
|
||||
steps,
|
||||
validationErrors,
|
||||
isLoading,
|
||||
error,
|
||||
isGenerating,
|
||||
generationProgress
|
||||
]);
|
||||
|
||||
const actions: WizardActions = useMemo(() => ({
|
||||
setActiveStep: setActiveStepWithValidation,
|
||||
updateCalendarConfig,
|
||||
validateStep,
|
||||
validateAllSteps,
|
||||
clearErrors,
|
||||
setError,
|
||||
setLoading: setIsLoading,
|
||||
setGenerating: setIsGenerating,
|
||||
setGenerationProgress,
|
||||
resetWizard,
|
||||
canProceedToStep,
|
||||
getStepStatus
|
||||
}), [
|
||||
setActiveStepWithValidation,
|
||||
updateCalendarConfig,
|
||||
validateStep,
|
||||
validateAllSteps,
|
||||
clearErrors,
|
||||
setError,
|
||||
setIsLoading,
|
||||
setIsGenerating,
|
||||
setGenerationProgress,
|
||||
resetWizard,
|
||||
canProceedToStep,
|
||||
getStepStatus
|
||||
]);
|
||||
|
||||
return [state, actions];
|
||||
};
|
||||
|
||||
// Helper function to create default calendar config
|
||||
const createDefaultConfig = (): CalendarConfig => {
|
||||
return {
|
||||
// Calendar Structure
|
||||
calendarType: 'monthly',
|
||||
calendarDuration: 4, // 4 weeks/months
|
||||
startDate: new Date().toISOString().split('T')[0], // Today's date
|
||||
|
||||
// Posting Configuration
|
||||
postingFrequency: 3, // 3 posts per week
|
||||
contentVolume: 12, // 12 pieces per period
|
||||
|
||||
// Platform Scheduling
|
||||
priorityPlatforms: ['LinkedIn', 'Twitter'],
|
||||
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, // User's timezone
|
||||
|
||||
// Calendar Preferences
|
||||
excludeDates: [],
|
||||
contentDistribution: 'even',
|
||||
reviewCycle: 'weekly',
|
||||
|
||||
// Generation Options
|
||||
includeWeekends: false,
|
||||
autoSchedule: true,
|
||||
generateTopics: true
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
export { default as DataReviewStep } from './DataReviewStep';
|
||||
export { default as CalendarConfigurationStep } from './CalendarConfigurationStep';
|
||||
export { default as AdvancedOptionsStep } from './AdvancedOptionsStep';
|
||||
export { default as GenerateCalendarStep } from './GenerateCalendarStep';
|
||||
|
||||
// State management and utilities
|
||||
export { useCalendarWizardState } from './hooks/useCalendarWizardState';
|
||||
export type { CalendarConfig, WizardState, WizardActions, ValidationError } from './types';
|
||||
|
||||
// Error handling
|
||||
export { WizardErrorBoundary, withErrorBoundary } from './components/WizardErrorBoundary';
|
||||
|
||||
// Loading states
|
||||
export {
|
||||
WizardLoadingState,
|
||||
CalendarGenerationLoading,
|
||||
DataProcessingLoading
|
||||
} from './components/WizardLoadingState';
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Calendar Wizard Types
|
||||
* Phase 3A: Simplified Calendar-Specific Configuration
|
||||
*/
|
||||
|
||||
export interface CalendarConfig {
|
||||
// Calendar Structure
|
||||
calendarType: 'weekly' | 'monthly' | 'quarterly';
|
||||
calendarDuration: number; // Number of periods to generate
|
||||
startDate: string; // ISO date string
|
||||
|
||||
// Posting Configuration
|
||||
postingFrequency: number; // Posts per week
|
||||
contentVolume: number; // Total pieces per period
|
||||
|
||||
// Platform Scheduling
|
||||
priorityPlatforms: string[]; // ["LinkedIn", "Twitter", "Blog"]
|
||||
timeZone: string; // User's timezone
|
||||
|
||||
// Calendar Preferences
|
||||
excludeDates: string[]; // Holiday/blackout dates
|
||||
contentDistribution: 'even' | 'frontloaded' | 'backloaded';
|
||||
reviewCycle: 'weekly' | 'monthly' | 'quarterly';
|
||||
|
||||
// Generation Options
|
||||
includeWeekends: boolean;
|
||||
autoSchedule: boolean;
|
||||
generateTopics: boolean;
|
||||
}
|
||||
|
||||
export interface WizardStep {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
description: string;
|
||||
isCompleted: boolean;
|
||||
hasErrors: boolean;
|
||||
}
|
||||
|
||||
export interface ValidationError {
|
||||
field: string;
|
||||
message: string;
|
||||
step: number;
|
||||
}
|
||||
|
||||
export interface WizardState {
|
||||
activeStep: number;
|
||||
calendarConfig: CalendarConfig;
|
||||
steps: WizardStep[];
|
||||
validationErrors: ValidationError[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
isGenerating: boolean;
|
||||
generationProgress: number;
|
||||
}
|
||||
|
||||
export interface WizardActions {
|
||||
setActiveStep: (step: number) => void;
|
||||
updateCalendarConfig: (updates: Partial<CalendarConfig>) => void;
|
||||
validateStep: (step: number) => boolean;
|
||||
validateAllSteps: () => boolean;
|
||||
clearErrors: () => void;
|
||||
setError: (error: string | null) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setGenerating: (generating: boolean) => void;
|
||||
setGenerationProgress: (progress: number) => void;
|
||||
resetWizard: () => void;
|
||||
canProceedToStep: (step: number) => boolean;
|
||||
getStepStatus: (step: number) => 'completed' | 'current' | 'pending' | 'error';
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
@@ -55,7 +55,8 @@ import {
|
||||
Schedule as ScheduleIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useEnhancedStrategyStore, STRATEGIC_INPUT_FIELDS } from '../../../stores/enhancedStrategyStore';
|
||||
import { useStrategyBuilderStore, STRATEGIC_INPUT_FIELDS } from '../../../stores/strategyBuilderStore';
|
||||
import { useEnhancedStrategyStore } from '../../../stores/enhancedStrategyStore';
|
||||
import StrategicInputField from './ContentStrategyBuilder/StrategicInputField';
|
||||
import EnhancedTooltip from './ContentStrategyBuilder/EnhancedTooltip';
|
||||
import AIRecommendationsPanel from './AIRecommendationsPanel';
|
||||
@@ -90,53 +91,58 @@ import CategoryDetailView from './ContentStrategyBuilder/components/CategoryDeta
|
||||
|
||||
const ContentStrategyBuilder: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Strategy Builder Store (for form data, validation, auto-population)
|
||||
const {
|
||||
formData,
|
||||
formErrors,
|
||||
autoPopulatedFields,
|
||||
dataSources,
|
||||
inputDataPoints, // Add inputDataPoints from store
|
||||
inputDataPoints,
|
||||
personalizationData,
|
||||
loading,
|
||||
error,
|
||||
saving,
|
||||
aiGenerating,
|
||||
currentStep,
|
||||
completedSteps,
|
||||
disclosureSteps,
|
||||
currentStrategy,
|
||||
updateFormField,
|
||||
// Transparency state
|
||||
transparencyModalOpen,
|
||||
generationProgress: storeGenerationProgress,
|
||||
currentPhase,
|
||||
educationalContent: storeEducationalContent,
|
||||
transparencyMessages,
|
||||
isGenerating,
|
||||
setTransparencyModalOpen,
|
||||
setGenerationProgress: setStoreGenerationProgress,
|
||||
setCurrentPhase,
|
||||
setEducationalContent: setStoreEducationalContent,
|
||||
addTransparencyMessage,
|
||||
clearTransparencyMessages,
|
||||
setIsGenerating,
|
||||
validateFormField,
|
||||
validateAllFields,
|
||||
completeStep,
|
||||
getNextStep,
|
||||
getPreviousStep,
|
||||
setCurrentStep,
|
||||
canProceedToStep,
|
||||
resetForm,
|
||||
autoPopulateFromOnboarding,
|
||||
generateAIRecommendations,
|
||||
createEnhancedStrategy,
|
||||
createStrategy: createEnhancedStrategy,
|
||||
calculateCompletionPercentage,
|
||||
getCompletionStats,
|
||||
setError,
|
||||
setCurrentStrategy,
|
||||
setAIGenerating,
|
||||
setSaving,
|
||||
personalizationData
|
||||
setSaving
|
||||
} = useStrategyBuilderStore();
|
||||
|
||||
// Enhanced Strategy Store (for AI analysis, progressive disclosure, transparency)
|
||||
const {
|
||||
aiGenerating,
|
||||
currentStep,
|
||||
completedSteps,
|
||||
disclosureSteps,
|
||||
transparencyModalOpen,
|
||||
transparencyGenerationProgress: storeGenerationProgress,
|
||||
currentPhase,
|
||||
educationalContent: storeEducationalContent,
|
||||
transparencyMessages,
|
||||
transparencyGenerating: isGenerating,
|
||||
setTransparencyModalOpen,
|
||||
setTransparencyGenerationProgress: setStoreGenerationProgress,
|
||||
setCurrentPhase,
|
||||
setEducationalContent: setStoreEducationalContent,
|
||||
addTransparencyMessage,
|
||||
clearTransparencyMessages,
|
||||
setTransparencyGenerating: setIsGenerating,
|
||||
completeStep,
|
||||
getNextStep,
|
||||
getPreviousStep,
|
||||
setCurrentStep,
|
||||
canProceedToDisclosureStep: canProceedToStep,
|
||||
generateAIRecommendations,
|
||||
setAIGenerating
|
||||
} = useEnhancedStrategyStore();
|
||||
|
||||
const [showAIRecommendations, setShowAIRecommendations] = useState(false);
|
||||
@@ -237,8 +243,8 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
setError
|
||||
});
|
||||
|
||||
const completionStats = getCompletionStats();
|
||||
const completionPercentage = calculateCompletionPercentage();
|
||||
const completionStats = useMemo(() => getCompletionStats(), [formData]);
|
||||
const completionPercentage = useMemo(() => calculateCompletionPercentage(), [formData]);
|
||||
|
||||
// Use extracted hooks
|
||||
const {
|
||||
@@ -314,7 +320,7 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
if (!autoPopulateAttempted) {
|
||||
autoPopulateFromOnboarding();
|
||||
}
|
||||
}, [autoPopulateAttempted, autoPopulateFromOnboarding]);
|
||||
}, [autoPopulateAttempted]); // Removed autoPopulateFromOnboarding from dependencies
|
||||
|
||||
// Set default category selection
|
||||
useEffect(() => {
|
||||
@@ -362,15 +368,7 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
}
|
||||
}, [showEnterpriseModal, aiGenerating]);
|
||||
|
||||
// Monitor store data changes for debugging
|
||||
useEffect(() => {
|
||||
console.log('🎯 Store data changed:', {
|
||||
autoPopulatedFieldsCount: Object.keys(autoPopulatedFields || {}).length,
|
||||
dataSourcesCount: Object.keys(dataSources || {}).length,
|
||||
inputDataPointsCount: Object.keys(inputDataPoints || {}).length,
|
||||
transparencyMessagesCount: transparencyMessages?.length || 0
|
||||
});
|
||||
}, [autoPopulatedFields, dataSources, inputDataPoints, transparencyMessages]);
|
||||
// Note: Removed store monitoring useEffect to prevent infinite re-renders
|
||||
|
||||
// Add CSS keyframes for pulse animation
|
||||
useEffect(() => {
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
DataUsage as DataUsageIcon,
|
||||
Close as CloseIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useEnhancedStrategyStore } from '../../../../stores/enhancedStrategyStore';
|
||||
import { useStrategyBuilderStore } from '../../../../stores/strategyBuilderStore';
|
||||
|
||||
interface EnhancedTooltipProps {
|
||||
fieldId: string;
|
||||
@@ -40,7 +40,19 @@ const EnhancedTooltip: React.FC<EnhancedTooltipProps> = ({
|
||||
open,
|
||||
onClose
|
||||
}) => {
|
||||
const { getTooltipData, autoPopulatedFields, dataSources } = useEnhancedStrategyStore();
|
||||
const { autoPopulatedFields, dataSources, confidenceScores } = useStrategyBuilderStore();
|
||||
|
||||
// Since getTooltipData is not in strategyBuilderStore, we'll create a simple implementation
|
||||
const getTooltipData = (fieldId: string) => {
|
||||
// This is a simplified tooltip data implementation
|
||||
// In a real scenario, you might want to move this to the strategyBuilderStore
|
||||
return {
|
||||
title: `About ${fieldId.replace(/_/g, ' ')}`,
|
||||
description: `Information about ${fieldId.replace(/_/g, ' ')}`,
|
||||
tips: [`Tip for ${fieldId}`],
|
||||
confidence_level: confidenceScores?.[fieldId] || 0.8
|
||||
};
|
||||
};
|
||||
|
||||
const tooltipData = getTooltipData(fieldId);
|
||||
const isAutoPopulated = !!(autoPopulatedFields && autoPopulatedFields[fieldId]);
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
ExpandLess as ExpandLessIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useEnhancedStrategyStore } from '../../../../stores/enhancedStrategyStore';
|
||||
import { useStrategyBuilderStore } from '../../../../stores/strategyBuilderStore';
|
||||
|
||||
interface StrategicInputFieldProps {
|
||||
fieldId: string;
|
||||
@@ -108,7 +108,15 @@ const StrategicInputField: React.FC<StrategicInputFieldProps> = ({
|
||||
accentColorKey = 'primary',
|
||||
isCompact = false
|
||||
}) => {
|
||||
const { getTooltipData } = useEnhancedStrategyStore();
|
||||
// Since getTooltipData is not in strategyBuilderStore, we'll create a simple implementation
|
||||
const getTooltipData = (fieldId: string) => {
|
||||
// This is a simplified tooltip data implementation
|
||||
return {
|
||||
title: `About ${fieldId.replace(/_/g, ' ')}`,
|
||||
description: `Information about ${fieldId.replace(/_/g, ' ')}`,
|
||||
tips: [`Tip for ${fieldId}`]
|
||||
};
|
||||
};
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [showPersonalization, setShowPersonalization] = useState(false);
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import StrategicInputField from '../StrategicInputField';
|
||||
import { CategoryDetailViewProps, EducationalInfoDialogProps } from '../types/contentStrategy.types';
|
||||
import { useEnhancedStrategyStore } from '../../../../../stores/enhancedStrategyStore';
|
||||
import { useStrategyBuilderStore } from '../../../../../stores/strategyBuilderStore';
|
||||
|
||||
const EducationalInfoDialog: React.FC<EducationalInfoDialogProps> = ({
|
||||
open,
|
||||
@@ -121,7 +121,7 @@ const CategoryDetailView: React.FC<CategoryDetailViewProps> = ({
|
||||
const [expandedCard, setExpandedCard] = useState<string | null>(null);
|
||||
|
||||
// Get confidence scores from store
|
||||
const { confidenceScores } = useEnhancedStrategyStore();
|
||||
const { confidenceScores } = useStrategyBuilderStore();
|
||||
|
||||
if (!activeCategory) {
|
||||
return (
|
||||
|
||||
@@ -34,8 +34,7 @@ const EducationalModal: React.FC<EducationalModalProps> = ({
|
||||
generationProgress,
|
||||
onReviewStrategy
|
||||
}) => {
|
||||
// Debug: Log progress and button state
|
||||
console.log('🎯 EducationalModal - Progress:', generationProgress, 'Show Next Button:', generationProgress >= 100);
|
||||
// Note: Removed debug logging to prevent infinite re-renders
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { contentPlanningApi } from '../../../../../services/contentPlanningApi';
|
||||
import { useEnhancedStrategyStore } from '../../../../../stores/enhancedStrategyStore';
|
||||
import { useStrategyBuilderStore } from '../../../../../stores/strategyBuilderStore';
|
||||
|
||||
interface UseAIRefreshProps {
|
||||
setTransparencyModalOpen: (open: boolean) => void;
|
||||
@@ -235,7 +235,7 @@ export const useAIRefresh = ({
|
||||
console.log('🎯 Field values details:', fieldValues);
|
||||
|
||||
// Update the store with the new data
|
||||
useEnhancedStrategyStore.setState((state) => {
|
||||
useStrategyBuilderStore.setState((state) => {
|
||||
const newState = {
|
||||
autoPopulatedFields: { ...state.autoPopulatedFields, ...fieldValues },
|
||||
dataSources: { ...state.dataSources, ...sources },
|
||||
|
||||
@@ -10,14 +10,17 @@ export const useAutoPopulation = ({
|
||||
completionStats
|
||||
}: UseAutoPopulationProps) => {
|
||||
const [autoPopulateAttempted, setAutoPopulateAttempted] = useState(false);
|
||||
const [isAutoPopulating, setIsAutoPopulating] = useState(false);
|
||||
|
||||
// Auto-populate from onboarding on first load
|
||||
useEffect(() => {
|
||||
if (!autoPopulateAttempted) {
|
||||
if (!autoPopulateAttempted && !isAutoPopulating) {
|
||||
setIsAutoPopulating(true);
|
||||
autoPopulateFromOnboarding();
|
||||
setAutoPopulateAttempted(true);
|
||||
setIsAutoPopulating(false);
|
||||
}
|
||||
}, [autoPopulateAttempted, autoPopulateFromOnboarding]);
|
||||
}, [autoPopulateAttempted, isAutoPopulating]); // Removed autoPopulateFromOnboarding from dependencies
|
||||
|
||||
return {
|
||||
autoPopulateAttempted,
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Alert,
|
||||
AlertTitle,
|
||||
CircularProgress,
|
||||
LinearProgress,
|
||||
Card,
|
||||
CardContent,
|
||||
Grid,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Chip
|
||||
} from '@mui/material';
|
||||
import {
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
PlayArrow as PlayArrowIcon,
|
||||
SmartToy as SmartToyIcon,
|
||||
Person as PersonIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
ThumbUp as ThumbUpIcon,
|
||||
MonetizationOn as MonetizationOnIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Star as StarIcon,
|
||||
EmojiEvents as EmojiEventsIcon,
|
||||
People as PeopleIcon
|
||||
} from '@mui/icons-material';
|
||||
import { strategyMonitoringApi, MonitoringPlan } from '../../../services/strategyMonitoringApi';
|
||||
|
||||
interface StrategyActivationModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
strategyId: number;
|
||||
strategyData: any;
|
||||
onSetupMonitoring: (monitoringPlan: any) => Promise<void>;
|
||||
}
|
||||
|
||||
interface MonitoringTask {
|
||||
title: string;
|
||||
description: string;
|
||||
assignee: 'ALwrity' | 'Human';
|
||||
frequency: string;
|
||||
metric: string;
|
||||
measurementMethod: string;
|
||||
successCriteria: string;
|
||||
alertThreshold: string;
|
||||
actionableInsights?: string;
|
||||
}
|
||||
|
||||
interface MonitoringComponent {
|
||||
name: string;
|
||||
icon: string;
|
||||
tasks: MonitoringTask[];
|
||||
}
|
||||
|
||||
|
||||
|
||||
const StrategyActivationModal: React.FC<StrategyActivationModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
strategyId,
|
||||
strategyData,
|
||||
onSetupMonitoring
|
||||
}) => {
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [monitoringPlan, setMonitoringPlan] = useState<MonitoringPlan | null>(null);
|
||||
const [showMonitoringPlan, setShowMonitoringPlan] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSetupMonitoring = async () => {
|
||||
setIsGenerating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Call the API to generate monitoring plan
|
||||
const response = await strategyMonitoringApi.generateMonitoringPlan(strategyId);
|
||||
setMonitoringPlan(response.data);
|
||||
setShowMonitoringPlan(true);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to generate monitoring plan');
|
||||
console.error('Error generating monitoring plan:', err);
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActivateStrategy = async () => {
|
||||
if (!monitoringPlan) return;
|
||||
|
||||
try {
|
||||
await onSetupMonitoring(monitoringPlan);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error activating strategy:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getComponentIcon = (iconName: string) => {
|
||||
const iconMap: { [key: string]: React.ReactElement } = {
|
||||
'TrendingUpIcon': <TrendingUpIcon />,
|
||||
'ThumbUpIcon': <ThumbUpIcon />,
|
||||
'MonetizationOnIcon': <MonetizationOnIcon />,
|
||||
'CheckCircleIcon': <CheckCircleIcon />,
|
||||
'StarIcon': <StarIcon />,
|
||||
'EmojiEventsIcon': <EmojiEventsIcon />,
|
||||
'PeopleIcon': <PeopleIcon />,
|
||||
'AssessmentIcon': <AssessmentIcon />
|
||||
};
|
||||
return iconMap[iconName] || <AssessmentIcon />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
|
||||
<DialogTitle>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<AutoAwesomeIcon color="primary" />
|
||||
Activate Content Strategy & Setup Monitoring
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!showMonitoringPlan ? (
|
||||
// Initial Setup View
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
ALwrity will create Quality & Performance Metrics
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" paragraph>
|
||||
Your content strategy will be continuously monitored and optimized based on comprehensive metrics and AI-powered analysis.
|
||||
</Typography>
|
||||
|
||||
<Alert severity="info" sx={{ mb: 3 }}>
|
||||
<AlertTitle>What happens next?</AlertTitle>
|
||||
ALwrity AI will analyze your strategy components and create a customized monitoring plan with specific tasks, metrics, and schedules to keep your strategy performing optimally.
|
||||
</Alert>
|
||||
|
||||
{isGenerating && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom color="primary">
|
||||
🎯 Creating Your Custom Monitoring Plan
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Step 1: Analyzing Strategy Components
|
||||
</Typography>
|
||||
<LinearProgress variant="indeterminate" sx={{ mb: 1 }} />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Reviewing your content pillars, target audience, and business goals
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Step 2: Generating Monitoring Tasks
|
||||
</Typography>
|
||||
<LinearProgress variant="indeterminate" sx={{ mb: 1 }} />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Creating automated ALwrity tasks and manual human review tasks
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Step 3: Setting Up Metrics & Alerts
|
||||
</Typography>
|
||||
<LinearProgress variant="indeterminate" sx={{ mb: 1 }} />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Configuring success criteria, alert thresholds, and measurement methods
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Step 4: Finalizing Your Plan
|
||||
</Typography>
|
||||
<LinearProgress variant="indeterminate" sx={{ mb: 1 }} />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Validating plan structure and preparing for activation
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Alert severity="info" sx={{ mt: 2 }}>
|
||||
<AlertTitle>What You'll Get</AlertTitle>
|
||||
<Typography variant="body2" component="div">
|
||||
• <strong>Automated Tasks:</strong> ALwrity will handle daily monitoring, analytics, and alerting
|
||||
<br />
|
||||
• <strong>Manual Tasks:</strong> You'll review strategic decisions and creative direction
|
||||
<br />
|
||||
• <strong>Performance Metrics:</strong> Track traffic growth, engagement, conversions, and ROI
|
||||
<br />
|
||||
• <strong>Smart Alerts:</strong> Get notified when metrics need attention
|
||||
<br />
|
||||
• <strong>Actionable Insights:</strong> Clear recommendations for optimization
|
||||
</Typography>
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={handleSetupMonitoring}
|
||||
disabled={isGenerating || !!error}
|
||||
startIcon={isGenerating ? <CircularProgress size={20} /> : <AssessmentIcon />}
|
||||
fullWidth
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{isGenerating ? 'Creating Your Monitoring Plan...' : 'Setup Audit & Adaptive Monitoring'}
|
||||
</Button>
|
||||
|
||||
{error && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
setError(null);
|
||||
handleSetupMonitoring();
|
||||
}}
|
||||
disabled={isGenerating}
|
||||
fullWidth
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
Retry Setup
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
// Monitoring Plan Display View
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Generated Monitoring Plan
|
||||
</Typography>
|
||||
|
||||
{monitoringPlan && (
|
||||
<MonitoringPlanDisplay
|
||||
plan={monitoringPlan}
|
||||
strategyData={strategyData}
|
||||
getComponentIcon={getComponentIcon}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Alert severity="success" sx={{ mt: 2 }}>
|
||||
<AlertTitle>Monitoring Plan Ready!</AlertTitle>
|
||||
Your strategy will now be continuously monitored with AI-powered analysis and adaptive recommendations.
|
||||
</Alert>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={handleActivateStrategy}
|
||||
startIcon={<PlayArrowIcon />}
|
||||
fullWidth
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Activate Strategy & Start Monitoring
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>
|
||||
{showMonitoringPlan ? 'Cancel' : 'Close'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
// Monitoring Plan Display Component
|
||||
interface MonitoringPlanDisplayProps {
|
||||
plan: MonitoringPlan;
|
||||
strategyData: any;
|
||||
getComponentIcon: (iconName: string) => React.ReactElement;
|
||||
}
|
||||
|
||||
const MonitoringPlanDisplay: React.FC<MonitoringPlanDisplayProps> = ({
|
||||
plan,
|
||||
strategyData,
|
||||
getComponentIcon
|
||||
}) => {
|
||||
// Helper function to get icon name from component name
|
||||
const getComponentIconName = (componentName: string): string => {
|
||||
const iconMap: Record<string, string> = {
|
||||
'Strategic Insights': 'TrendingUpIcon',
|
||||
'Competitive Analysis': 'EmojiEventsIcon',
|
||||
'Performance Predictions': 'AssessmentIcon',
|
||||
'Implementation Roadmap': 'CheckCircleIcon',
|
||||
'Risk Assessment': 'StarIcon'
|
||||
};
|
||||
return iconMap[componentName] || 'TrendingUpIcon';
|
||||
};
|
||||
return (
|
||||
<Box>
|
||||
{/* Enhanced Summary Section */}
|
||||
<Card sx={{
|
||||
mb: 3,
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white',
|
||||
boxShadow: '0 8px 32px rgba(102, 126, 234, 0.3)'
|
||||
}}>
|
||||
<CardContent>
|
||||
<Typography variant="h5" gutterBottom sx={{ fontWeight: 700 }}>
|
||||
🎯 AI-Powered Monitoring Plan
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 3, opacity: 0.9 }}>
|
||||
Your content strategy will be continuously monitored with comprehensive metrics and actionable insights
|
||||
</Typography>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={6} md={3}>
|
||||
<Box sx={{ textAlign: 'center', p: 2, background: 'rgba(255,255,255,0.1)', borderRadius: 2 }}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, mb: 1 }}>
|
||||
{plan.totalTasks}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>Total Tasks</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={6} md={3}>
|
||||
<Box sx={{ textAlign: 'center', p: 2, background: 'rgba(255,255,255,0.1)', borderRadius: 2 }}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, mb: 1, color: '#4caf50' }}>
|
||||
{plan.alwrityTasks}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>AI Automated</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={6} md={3}>
|
||||
<Box sx={{ textAlign: 'center', p: 2, background: 'rgba(255,255,255,0.1)', borderRadius: 2 }}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, mb: 1, color: '#ff9800' }}>
|
||||
{plan.humanTasks}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>Human Tasks</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={6} md={3}>
|
||||
<Box sx={{ textAlign: 'center', p: 2, background: 'rgba(255,255,255,0.1)', borderRadius: 2 }}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, mb: 1, color: '#2196f3' }}>
|
||||
{plan.metricsCount}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>Metrics Tracked</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Enhanced Component-wise Monitoring Tasks */}
|
||||
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, mb: 3 }}>
|
||||
📊 Monitoring Tasks by Strategy Component
|
||||
</Typography>
|
||||
|
||||
{/* Group tasks by component */}
|
||||
{(() => {
|
||||
const tasksByComponent = plan.monitoringTasks.reduce((acc: Record<string, typeof plan.monitoringTasks>, task) => {
|
||||
if (!acc[task.component]) {
|
||||
acc[task.component] = [];
|
||||
}
|
||||
acc[task.component].push(task);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return Object.entries(tasksByComponent).map(([componentName, tasks], index) => (
|
||||
<Card key={index} sx={{
|
||||
mb: 3,
|
||||
border: '1px solid rgba(102, 126, 234, 0.2)',
|
||||
boxShadow: '0 4px 20px rgba(0,0,0,0.1)',
|
||||
'&:hover': {
|
||||
boxShadow: '0 8px 32px rgba(102, 126, 234, 0.2)',
|
||||
transform: 'translateY(-2px)'
|
||||
},
|
||||
transition: 'all 0.3s ease'
|
||||
}}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
{/* Component Header */}
|
||||
<Box display="flex" alignItems="center" gap={2} sx={{ mb: 3 }}>
|
||||
<Box sx={{
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white'
|
||||
}}>
|
||||
{getComponentIcon(getComponentIconName(componentName))}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
{componentName}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{tasks.length} comprehensive monitoring tasks
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Enhanced Task List */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{(tasks as typeof plan.monitoringTasks).map((task, taskIndex) => (
|
||||
<Card key={taskIndex} sx={{
|
||||
border: '1px solid rgba(0,0,0,0.1)',
|
||||
background: task.assignee === 'ALwrity' ? 'rgba(76, 175, 80, 0.05)' : 'rgba(255, 152, 0, 0.05)'
|
||||
}}>
|
||||
<CardContent sx={{ p: 2.5 }}>
|
||||
{/* Task Header */}
|
||||
<Box display="flex" alignItems="center" gap={2} sx={{ mb: 2 }}>
|
||||
<Box sx={{
|
||||
p: 1,
|
||||
borderRadius: 1,
|
||||
background: task.assignee === 'ALwrity' ? 'rgba(76, 175, 80, 0.2)' : 'rgba(255, 152, 0, 0.2)',
|
||||
color: task.assignee === 'ALwrity' ? '#4caf50' : '#ff9800'
|
||||
}}>
|
||||
{task.assignee === 'ALwrity' ?
|
||||
<SmartToyIcon fontSize="small" /> :
|
||||
<PersonIcon fontSize="small" />
|
||||
}
|
||||
</Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 0.5 }}>
|
||||
{task.title}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Chip
|
||||
label={task.assignee}
|
||||
size="small"
|
||||
color={task.assignee === 'ALwrity' ? 'success' : 'warning'}
|
||||
sx={{ fontWeight: 600 }}
|
||||
/>
|
||||
<Chip
|
||||
label={task.frequency}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ fontWeight: 500 }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Task Description */}
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2, lineHeight: 1.6 }}>
|
||||
{task.description}
|
||||
</Typography>
|
||||
|
||||
{/* Task Details Grid */}
|
||||
<Grid container spacing={2} sx={{ mb: 2 }}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ p: 2, background: 'rgba(33, 150, 243, 0.05)', borderRadius: 1 }}>
|
||||
<Typography variant="caption" color="primary" sx={{ fontWeight: 600, textTransform: 'uppercase' }}>
|
||||
Metric to Track
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 0.5, fontWeight: 500 }}>
|
||||
{task.metric}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ p: 2, background: 'rgba(156, 39, 176, 0.05)', borderRadius: 1 }}>
|
||||
<Typography variant="caption" color="secondary" sx={{ fontWeight: 600, textTransform: 'uppercase' }}>
|
||||
Success Criteria
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 0.5, fontWeight: 500 }}>
|
||||
{task.successCriteria}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Measurement Method */}
|
||||
<Box sx={{ p: 2, background: 'rgba(255, 193, 7, 0.05)', borderRadius: 1, mb: 2 }}>
|
||||
<Typography variant="caption" color="warning.main" sx={{ fontWeight: 600, textTransform: 'uppercase' }}>
|
||||
📏 Measurement Method
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 0.5, lineHeight: 1.6 }}>
|
||||
{task.measurementMethod}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Alert Threshold and Actionable Insights */}
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ p: 2, background: 'rgba(244, 67, 54, 0.05)', borderRadius: 1 }}>
|
||||
<Typography variant="caption" color="error" sx={{ fontWeight: 600, textTransform: 'uppercase' }}>
|
||||
🚨 Alert Threshold
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 0.5, fontWeight: 500 }}>
|
||||
{task.alertThreshold}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ p: 2, background: 'rgba(76, 175, 80, 0.05)', borderRadius: 1 }}>
|
||||
<Typography variant="caption" color="success.main" sx={{ fontWeight: 600, textTransform: 'uppercase' }}>
|
||||
💡 Actionable Insights
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 0.5, fontWeight: 500 }}>
|
||||
{task.actionableInsights || "Review data and adjust strategy based on performance trends"}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
));
|
||||
})()}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default StrategyActivationModal;
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Box, CircularProgress, Alert, Typography } from '@mui/material';
|
||||
import { useStrategyData } from './hooks/useStrategyData';
|
||||
import StrategyHeader from './components/StrategyHeader';
|
||||
import StrategicInsightsCard from './components/StrategicInsightsCard';
|
||||
import CompetitiveAnalysisCard from './components/CompetitiveAnalysisCard';
|
||||
@@ -8,15 +7,60 @@ import PerformancePredictionsCard from './components/PerformancePredictionsCard'
|
||||
import ImplementationRoadmapCard from './components/ImplementationRoadmapCard';
|
||||
import RiskAssessmentCard from './components/RiskAssessmentCard';
|
||||
import ReviewProgressHeader from './components/ReviewProgressHeader';
|
||||
import { StrategyData } from './types/strategy.types';
|
||||
import { useStrategyReviewStore } from '../../../../stores/strategyReviewStore';
|
||||
|
||||
const StrategyIntelligenceTab: React.FC = () => {
|
||||
const { strategyData, loading, error } = useStrategyData();
|
||||
|
||||
// State to control review progress visibility
|
||||
const [showReviewProgress, setShowReviewProgress] = useState(false);
|
||||
interface StrategyIntelligenceTabProps {
|
||||
strategyData?: StrategyData | null;
|
||||
loading?: boolean;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
const StrategyIntelligenceTab: React.FC<StrategyIntelligenceTabProps> = ({
|
||||
strategyData,
|
||||
loading = false,
|
||||
error = null
|
||||
}) => {
|
||||
// Get review process state from store
|
||||
const { reviewProcessStarted, startReviewProcess, components, initializeComponents } = useStrategyReviewStore();
|
||||
|
||||
// Initialize components if they don't exist
|
||||
useEffect(() => {
|
||||
if (components.length === 0) {
|
||||
console.log('🔧 StrategyIntelligenceTab: Initializing components');
|
||||
const STRATEGY_COMPONENTS = [
|
||||
{
|
||||
id: 'strategic_insights',
|
||||
title: 'Strategic Insights',
|
||||
subtitle: 'AI-powered market analysis'
|
||||
},
|
||||
{
|
||||
id: 'competitive_analysis',
|
||||
title: 'Competitive Analysis',
|
||||
subtitle: 'Market positioning insights'
|
||||
},
|
||||
{
|
||||
id: 'performance_predictions',
|
||||
title: 'Performance Predictions',
|
||||
subtitle: 'ROI and success metrics'
|
||||
},
|
||||
{
|
||||
id: 'implementation_roadmap',
|
||||
title: 'Implementation Roadmap',
|
||||
subtitle: 'Project timeline and phases'
|
||||
},
|
||||
{
|
||||
id: 'risk_assessment',
|
||||
title: 'Risk Assessment',
|
||||
subtitle: 'Risk analysis and mitigation'
|
||||
}
|
||||
];
|
||||
initializeComponents(STRATEGY_COMPONENTS);
|
||||
}
|
||||
}, [components.length, initializeComponents]);
|
||||
|
||||
const handleStartReviewProcess = () => {
|
||||
setShowReviewProgress(true);
|
||||
startReviewProcess();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@@ -58,9 +102,9 @@ const StrategyIntelligenceTab: React.FC = () => {
|
||||
/>
|
||||
|
||||
{/* Review Progress Header - Only shown when review process is started */}
|
||||
{showReviewProgress && <ReviewProgressHeader />}
|
||||
{reviewProcessStarted && <ReviewProgressHeader strategyData={strategyData} />}
|
||||
|
||||
{/* Strategy Components Grid */}
|
||||
{/* Strategy Intelligence Cards */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
@@ -93,10 +137,6 @@ const StrategyIntelligenceTab: React.FC = () => {
|
||||
<ImplementationRoadmapCard strategyData={strategyData} />
|
||||
<RiskAssessmentCard strategyData={strategyData} />
|
||||
</Box>
|
||||
|
||||
{/* Action Buttons - Removed, functionality moved to "Confirm & Activate Strategy" button in ReviewProgressHeader */}
|
||||
|
||||
{/* Confirmation Dialog - Removed, functionality moved to "Confirm & Activate Strategy" button */}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Alert,
|
||||
AlertTitle,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
Switch,
|
||||
FormControlLabel
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Visibility as VisibilityIcon,
|
||||
Refresh as RefreshIcon,
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
People as PeopleIcon,
|
||||
MonetizationOn as MonetizationOnIcon,
|
||||
Speed as SpeedIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Schedule as ScheduleIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion } from 'framer-motion';
|
||||
import MetricTransparencyCard from './MetricTransparencyCard';
|
||||
import { strategyMonitoringApi } from '../../../../../services/strategyMonitoringApi';
|
||||
|
||||
interface DataTransparencyPanelProps {
|
||||
strategyId: number;
|
||||
strategyData?: any;
|
||||
}
|
||||
|
||||
const DataTransparencyPanel: React.FC<DataTransparencyPanelProps> = ({
|
||||
strategyId,
|
||||
strategyData
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [transparencyData, setTransparencyData] = useState<any[]>([]);
|
||||
const [lastRefresh, setLastRefresh] = useState<Date>(new Date());
|
||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||
const [refreshInterval, setRefreshInterval] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadTransparencyData();
|
||||
|
||||
// Set up auto-refresh every 5 minutes if enabled
|
||||
if (autoRefresh) {
|
||||
const interval = setInterval(() => {
|
||||
loadTransparencyData();
|
||||
setLastRefresh(new Date());
|
||||
}, 5 * 60 * 1000); // 5 minutes
|
||||
|
||||
setRefreshInterval(interval);
|
||||
|
||||
return () => {
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}
|
||||
}, [strategyId, autoRefresh]);
|
||||
|
||||
// Cleanup interval on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
};
|
||||
}, [refreshInterval]);
|
||||
|
||||
const loadTransparencyData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Try to get real data from API first
|
||||
try {
|
||||
const response = await strategyMonitoringApi.getTransparencyData(strategyId);
|
||||
if (response.success && response.data) {
|
||||
setTransparencyData(response.data);
|
||||
return;
|
||||
}
|
||||
} catch (apiError) {
|
||||
console.warn('API call failed, falling back to mock data:', apiError);
|
||||
// Continue to mock data as fallback
|
||||
}
|
||||
|
||||
// Fallback to mock data if API fails
|
||||
const mockTransparencyData = [
|
||||
{
|
||||
metricName: "Traffic Growth",
|
||||
currentValue: 15.7,
|
||||
unit: "%",
|
||||
dataFreshness: {
|
||||
lastUpdated: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), // 2 hours ago
|
||||
updateFrequency: "Every 4 hours",
|
||||
dataSource: "Google Analytics + AI Analysis",
|
||||
confidence: 92
|
||||
},
|
||||
measurementMethodology: {
|
||||
description: "Organic traffic growth compared to previous period",
|
||||
calculationMethod: "Percentage change in organic sessions over 30-day rolling period, weighted by content performance and user engagement",
|
||||
dataPoints: ["Organic Sessions", "Page Views", "Bounce Rate", "Time on Site", "Content Performance"],
|
||||
validationProcess: "Cross-validated with Google Search Console data and AI-powered content performance analysis"
|
||||
},
|
||||
monitoringTasks: [
|
||||
{
|
||||
title: "Monitor Organic Traffic Trends",
|
||||
description: "Track daily organic traffic patterns and identify growth opportunities",
|
||||
assignee: "ALwrity",
|
||||
frequency: "Daily",
|
||||
metric: "Organic Sessions",
|
||||
measurementMethod: "Automated Google Analytics API integration with real-time data processing",
|
||||
successCriteria: "Maintain 10%+ monthly growth rate with <5% variance",
|
||||
alertThreshold: "Drop below 5% growth for 3 consecutive days",
|
||||
actionableInsights: "Optimize content based on top-performing pages and keywords",
|
||||
status: "active",
|
||||
lastExecuted: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
title: "Content Performance Analysis",
|
||||
description: "Analyze which content pieces drive the most traffic and engagement",
|
||||
assignee: "ALwrity",
|
||||
frequency: "Weekly",
|
||||
metric: "Content Performance Score",
|
||||
measurementMethod: "AI-powered content analysis using engagement metrics and conversion data",
|
||||
successCriteria: "Identify top 20% performing content pieces",
|
||||
alertThreshold: "Performance score drops below 70%",
|
||||
actionableInsights: "Replicate successful content patterns and optimize underperforming pieces",
|
||||
status: "completed",
|
||||
lastExecuted: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()
|
||||
}
|
||||
],
|
||||
strategyMapping: {
|
||||
relatedComponents: ["Strategic Insights", "Content Strategy", "Audience Analysis"],
|
||||
impactAreas: ["Brand Awareness", "Lead Generation", "Market Reach"],
|
||||
dependencies: ["SEO Optimization", "Content Quality", "User Experience"]
|
||||
},
|
||||
aiInsights: {
|
||||
trendAnalysis: "Traffic growth shows strong upward trend with 15.7% increase. Top-performing content categories are educational blog posts and case studies. Seasonal patterns indicate peak engagement during business hours.",
|
||||
recommendations: [
|
||||
"Increase content production in educational blog category by 25%",
|
||||
"Optimize case study content for better search visibility",
|
||||
"Implement A/B testing for content headlines",
|
||||
"Focus on long-form content (2000+ words) which shows 40% higher engagement"
|
||||
],
|
||||
riskFactors: ["Seasonal traffic fluctuations", "Competitor content strategy changes", "Algorithm updates"],
|
||||
opportunities: ["Video content expansion", "Guest posting opportunities", "Social media amplification"]
|
||||
}
|
||||
},
|
||||
{
|
||||
metricName: "Engagement Rate",
|
||||
currentValue: 8.3,
|
||||
unit: "%",
|
||||
dataFreshness: {
|
||||
lastUpdated: new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString(), // 1 hour ago
|
||||
updateFrequency: "Every 2 hours",
|
||||
dataSource: "Social Media Analytics + Website Analytics",
|
||||
confidence: 88
|
||||
},
|
||||
measurementMethodology: {
|
||||
description: "Average engagement rate across all content and social media",
|
||||
calculationMethod: "Weighted average of likes, shares, comments, and time spent across all platforms",
|
||||
dataPoints: ["Social Media Engagement", "Website Comments", "Time on Page", "Social Shares", "Email Engagement"],
|
||||
validationProcess: "Cross-platform validation using multiple analytics tools and AI sentiment analysis"
|
||||
},
|
||||
monitoringTasks: [
|
||||
{
|
||||
title: "Social Media Engagement Tracking",
|
||||
description: "Monitor engagement across all social media platforms",
|
||||
assignee: "ALwrity",
|
||||
frequency: "Real-time",
|
||||
metric: "Engagement Rate",
|
||||
measurementMethod: "Automated social media API integration with sentiment analysis",
|
||||
successCriteria: "Maintain 8%+ average engagement rate",
|
||||
alertThreshold: "Engagement drops below 5% for 24 hours",
|
||||
actionableInsights: "Adjust content timing and messaging based on engagement patterns",
|
||||
status: "active",
|
||||
lastExecuted: new Date(Date.now() - 30 * 60 * 1000).toISOString()
|
||||
}
|
||||
],
|
||||
strategyMapping: {
|
||||
relatedComponents: ["Audience Analysis", "Content Strategy", "Social Media Strategy"],
|
||||
impactAreas: ["Brand Engagement", "Community Building", "Customer Loyalty"],
|
||||
dependencies: ["Content Quality", "Social Media Presence", "Community Management"]
|
||||
},
|
||||
aiInsights: {
|
||||
trendAnalysis: "Engagement rate is stable at 8.3% with peak engagement during lunch hours and early evenings. Video content shows 2.5x higher engagement than text-only posts.",
|
||||
recommendations: [
|
||||
"Increase video content production by 50%",
|
||||
"Optimize posting times for peak engagement hours",
|
||||
"Implement interactive content elements",
|
||||
"Focus on community-building content"
|
||||
],
|
||||
riskFactors: ["Platform algorithm changes", "Content fatigue", "Competition for attention"],
|
||||
opportunities: ["Live streaming opportunities", "User-generated content campaigns", "Influencer collaborations"]
|
||||
}
|
||||
},
|
||||
{
|
||||
metricName: "Conversion Rate",
|
||||
currentValue: 2.1,
|
||||
unit: "%",
|
||||
dataFreshness: {
|
||||
lastUpdated: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(), // 6 hours ago
|
||||
updateFrequency: "Every 6 hours",
|
||||
dataSource: "Google Analytics + CRM Data",
|
||||
confidence: 85
|
||||
},
|
||||
measurementMethodology: {
|
||||
description: "Content-driven conversion rate across all touchpoints",
|
||||
calculationMethod: "Conversions divided by total visitors, weighted by content attribution and customer journey analysis",
|
||||
dataPoints: ["Website Conversions", "Email Signups", "Lead Form Submissions", "Content Downloads", "Sales Attribution"],
|
||||
validationProcess: "CRM integration validation and conversion funnel analysis"
|
||||
},
|
||||
monitoringTasks: [
|
||||
{
|
||||
title: "Conversion Funnel Analysis",
|
||||
description: "Track conversion rates at each stage of the customer journey",
|
||||
assignee: "ALwrity",
|
||||
frequency: "Daily",
|
||||
metric: "Conversion Rate",
|
||||
measurementMethod: "Automated funnel analysis using Google Analytics and CRM data",
|
||||
successCriteria: "Maintain 2%+ overall conversion rate",
|
||||
alertThreshold: "Conversion rate drops below 1.5%",
|
||||
actionableInsights: "Optimize conversion points and remove friction from customer journey",
|
||||
status: "active",
|
||||
lastExecuted: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString()
|
||||
}
|
||||
],
|
||||
strategyMapping: {
|
||||
relatedComponents: ["Performance Predictions", "Implementation Roadmap", "Risk Assessment"],
|
||||
impactAreas: ["Revenue Generation", "Lead Quality", "Customer Acquisition"],
|
||||
dependencies: ["Content Quality", "User Experience", "Lead Nurturing"]
|
||||
},
|
||||
aiInsights: {
|
||||
trendAnalysis: "Conversion rate is improving steadily with 2.1% current rate. Top-converting content includes case studies and product demos. Mobile conversions show 30% improvement after UX optimization.",
|
||||
recommendations: [
|
||||
"Increase case study and demo content production",
|
||||
"Optimize mobile user experience further",
|
||||
"Implement personalized content recommendations",
|
||||
"A/B test call-to-action buttons and forms"
|
||||
],
|
||||
riskFactors: ["Market competition", "Economic factors", "Technology changes"],
|
||||
opportunities: ["Personalization opportunities", "Automation implementation", "Cross-selling strategies"]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
setTransparencyData(mockTransparencyData);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load transparency data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" minHeight={400}>
|
||||
<CircularProgress size={60} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
<AlertTitle>Error Loading Transparency Data</AlertTitle>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<Box>
|
||||
<Typography variant="h4" sx={{
|
||||
fontWeight: 700,
|
||||
background: 'linear-gradient(45deg, #667eea, #764ba2)',
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
mb: 1
|
||||
}}>
|
||||
🔍 Data Transparency & Methodology
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: 'text.secondary', mb: 1 }}>
|
||||
Detailed insights into how each metric is measured, data freshness, and AI monitoring tasks
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary', display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<ScheduleIcon sx={{ fontSize: 14 }} />
|
||||
Last updated: {lastRefresh.toLocaleTimeString()}
|
||||
</Typography>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={autoRefresh}
|
||||
onChange={(e) => setAutoRefresh(e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label="Auto-refresh"
|
||||
sx={{ '& .MuiFormControlLabel-label': { fontSize: '0.75rem' } }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Tooltip title="Refresh transparency data">
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
loadTransparencyData();
|
||||
setLastRefresh(new Date());
|
||||
}}
|
||||
sx={{ color: 'primary.main' }}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
</Box>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<Grid container spacing={3} sx={{ mb: 4 }}>
|
||||
<Grid item xs={12} md={3}>
|
||||
<Card sx={{ background: 'linear-gradient(135deg, #4caf50 0%, #45a049 100%)', color: 'white' }}>
|
||||
<CardContent sx={{ p: 2, textAlign: 'center' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700 }}>
|
||||
{transparencyData.length}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>
|
||||
Metrics Tracked
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={3}>
|
||||
<Card sx={{ background: 'linear-gradient(135deg, #2196f3 0%, #1976d2 100%)', color: 'white' }}>
|
||||
<CardContent sx={{ p: 2, textAlign: 'center' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700 }}>
|
||||
{transparencyData.reduce((acc, metric) => acc + metric.monitoringTasks.length, 0)}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>
|
||||
AI Monitoring Tasks
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={3}>
|
||||
<Card sx={{ background: 'linear-gradient(135deg, #ff9800 0%, #f57c00 100%)', color: 'white' }}>
|
||||
<CardContent sx={{ p: 2, textAlign: 'center' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700 }}>
|
||||
{Math.round(transparencyData.reduce((acc, metric) => acc + metric.dataFreshness.confidence, 0) / transparencyData.length)}%
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>
|
||||
Avg. Data Confidence
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={3}>
|
||||
<Card sx={{ background: 'linear-gradient(135deg, #9c27b0 0%, #7b1fa2 100%)', color: 'white' }}>
|
||||
<CardContent sx={{ p: 2, textAlign: 'center' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700 }}>
|
||||
Real-time
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>
|
||||
Data Updates
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Transparency Cards */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{transparencyData.map((metricData, index) => (
|
||||
<MetricTransparencyCard
|
||||
key={index}
|
||||
metricData={metricData}
|
||||
isExpanded={index === 0} // First card expanded by default
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Footer Information */}
|
||||
<Box sx={{ mt: 4, p: 3, background: 'rgba(102, 126, 234, 0.1)', borderRadius: 2 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<AutoAwesomeIcon />
|
||||
How This Data Helps Your Strategy
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
📊 Data-Driven Decisions
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.8 }}>
|
||||
Understand exactly how each metric is calculated and what data sources are used, ensuring confidence in your strategic decisions.
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
🤖 AI-Powered Monitoring
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.8 }}>
|
||||
See how AI tasks are monitoring your strategy performance and get actionable insights for optimization.
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
🎯 Strategy Alignment
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.8 }}>
|
||||
Understand how each metric maps to your strategy components and identify areas for improvement.
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataTransparencyPanel;
|
||||
@@ -0,0 +1,497 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Alert,
|
||||
AlertTitle,
|
||||
CircularProgress,
|
||||
LinearProgress,
|
||||
Divider,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon
|
||||
} from '@mui/material';
|
||||
import {
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
Speed as SpeedIcon,
|
||||
Visibility as VisibilityIcon,
|
||||
People as EngagementIcon,
|
||||
MonetizationOn as MonetizationOnIcon,
|
||||
Refresh as RefreshIcon,
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Warning as WarningIcon,
|
||||
Error as ErrorIcon,
|
||||
Analytics as AnalyticsIcon,
|
||||
Lightbulb as LightbulbIcon,
|
||||
Timeline as TimelineIcon,
|
||||
Close as CloseIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
// Import our advanced chart components
|
||||
import {
|
||||
PerformanceTrendChart,
|
||||
QualityMetricsRadar,
|
||||
PerformanceMetricsBar,
|
||||
ContentDistributionPie,
|
||||
PerformanceGauge
|
||||
} from '../../../../shared/charts/AdvancedChartComponents';
|
||||
|
||||
// Import real-time data hook
|
||||
import { useMockRealTimeData } from '../../../../../hooks/useRealTimeData';
|
||||
|
||||
// Import API services
|
||||
import { strategyMonitoringApi } from '../../../../../services/strategyMonitoringApi';
|
||||
|
||||
interface EnhancedPerformanceVisualizationProps {
|
||||
strategyId: number;
|
||||
strategyData: any;
|
||||
}
|
||||
|
||||
interface QualityAnalysisData {
|
||||
overall_score: number;
|
||||
overall_status: string;
|
||||
metrics: Array<{
|
||||
name: string;
|
||||
score: number;
|
||||
status: string;
|
||||
description: string;
|
||||
recommendations: string[];
|
||||
}>;
|
||||
recommendations: string[];
|
||||
confidence_score: number;
|
||||
}
|
||||
|
||||
const EnhancedPerformanceVisualization: React.FC<EnhancedPerformanceVisualizationProps> = ({
|
||||
strategyId,
|
||||
strategyData
|
||||
}) => {
|
||||
const [qualityAnalysis, setQualityAnalysis] = useState<QualityAnalysisData | null>(null);
|
||||
const [loadingQuality, setLoadingQuality] = useState(false);
|
||||
const [showQualityDialog, setShowQualityDialog] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Use real-time data hook
|
||||
const { data: realTimeData, isConnected, error: realTimeError } = useMockRealTimeData(strategyId);
|
||||
|
||||
useEffect(() => {
|
||||
loadQualityAnalysis();
|
||||
}, [strategyId]);
|
||||
|
||||
const loadQualityAnalysis = async () => {
|
||||
try {
|
||||
setLoadingQuality(true);
|
||||
setError(null);
|
||||
|
||||
// Call the quality analysis API
|
||||
const response = await strategyMonitoringApi.getQualityAnalysis(strategyId);
|
||||
setQualityAnalysis(response.data);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load quality analysis');
|
||||
console.error('Error loading quality analysis:', err);
|
||||
} finally {
|
||||
setLoadingQuality(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'excellent': return 'success';
|
||||
case 'good': return 'info';
|
||||
case 'needs_attention': return 'warning';
|
||||
case 'poor': return 'error';
|
||||
default: return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'excellent': return <CheckCircleIcon />;
|
||||
case 'good': return <CheckCircleIcon />;
|
||||
case 'needs_attention': return <WarningIcon />;
|
||||
case 'poor': return <ErrorIcon />;
|
||||
default: return <AssessmentIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
// Prepare chart data from real-time data
|
||||
const trendData = realTimeData?.trends?.daily || [];
|
||||
const qualityMetricsData = qualityAnalysis?.metrics?.map(metric => ({
|
||||
metric: metric.name,
|
||||
score: metric.score,
|
||||
target: 85 // Target score
|
||||
})) || [];
|
||||
|
||||
const performanceMetricsData = realTimeData?.metrics ? [
|
||||
{
|
||||
metric: 'Traffic Growth',
|
||||
value: realTimeData.metrics.traffic_growth_percentage,
|
||||
target: 15,
|
||||
status: (realTimeData.metrics.traffic_growth_percentage >= 15 ? 'excellent' :
|
||||
realTimeData.metrics.traffic_growth_percentage >= 10 ? 'good' : 'needs_attention') as 'excellent' | 'good' | 'needs_attention'
|
||||
},
|
||||
{
|
||||
metric: 'Engagement Rate',
|
||||
value: realTimeData.metrics.engagement_rate_percentage,
|
||||
target: 8,
|
||||
status: (realTimeData.metrics.engagement_rate_percentage >= 8 ? 'excellent' :
|
||||
realTimeData.metrics.engagement_rate_percentage >= 6 ? 'good' : 'needs_attention') as 'excellent' | 'good' | 'needs_attention'
|
||||
},
|
||||
{
|
||||
metric: 'Conversion Rate',
|
||||
value: realTimeData.metrics.conversion_rate_percentage,
|
||||
target: 2.5,
|
||||
status: (realTimeData.metrics.conversion_rate_percentage >= 2.5 ? 'excellent' :
|
||||
realTimeData.metrics.conversion_rate_percentage >= 2 ? 'good' : 'needs_attention') as 'excellent' | 'good' | 'needs_attention'
|
||||
},
|
||||
{
|
||||
metric: 'Content Quality',
|
||||
value: realTimeData.metrics.content_quality_score,
|
||||
target: 90,
|
||||
status: (realTimeData.metrics.content_quality_score >= 90 ? 'excellent' :
|
||||
realTimeData.metrics.content_quality_score >= 80 ? 'good' : 'needs_attention') as 'excellent' | 'good' | 'needs_attention'
|
||||
}
|
||||
] : [];
|
||||
|
||||
const contentDistributionData = [
|
||||
{ name: 'Blog Posts', value: 40, color: '#667eea' },
|
||||
{ name: 'Social Media', value: 25, color: '#764ba2' },
|
||||
{ name: 'Video Content', value: 20, color: '#4caf50' },
|
||||
{ name: 'Infographics', value: 10, color: '#ff9800' },
|
||||
{ name: 'Newsletters', value: 5, color: '#f44336' }
|
||||
];
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
<AlertTitle>Error Loading Performance Data</AlertTitle>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
{/* Header Section */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<Box>
|
||||
<Typography variant="h4" sx={{
|
||||
fontWeight: 700,
|
||||
background: 'linear-gradient(45deg, #667eea, #764ba2)',
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
mb: 1
|
||||
}}>
|
||||
🚀 Advanced Performance Dashboard
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: 'text.secondary' }}>
|
||||
Real-time monitoring and AI-powered quality analysis for your content strategy
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Chip
|
||||
icon={isConnected ? <CheckCircleIcon /> : <ErrorIcon />}
|
||||
label={isConnected ? 'Live Data' : 'Offline'}
|
||||
color={isConnected ? 'success' : 'error'}
|
||||
size="small"
|
||||
/>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
Last updated: {realTimeData?.timestamp ? new Date(realTimeData.timestamp).toLocaleString() : 'N/A'}
|
||||
</Typography>
|
||||
<Tooltip title="Refresh data">
|
||||
<IconButton onClick={loadQualityAnalysis} sx={{ color: 'primary.main' }}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
</Box>
|
||||
|
||||
{/* Real-time Performance Gauges */}
|
||||
{realTimeData?.metrics && (
|
||||
<Grid container spacing={3} sx={{ mb: 4 }}>
|
||||
<Grid item xs={12} md={6} lg={3}>
|
||||
<PerformanceGauge
|
||||
value={realTimeData.metrics.traffic_growth_percentage}
|
||||
maxValue={25}
|
||||
title="Traffic Growth"
|
||||
color="#667eea"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6} lg={3}>
|
||||
<PerformanceGauge
|
||||
value={realTimeData.metrics.engagement_rate_percentage}
|
||||
maxValue={15}
|
||||
title="Engagement Rate"
|
||||
color="#4caf50"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6} lg={3}>
|
||||
<PerformanceGauge
|
||||
value={realTimeData.metrics.conversion_rate_percentage}
|
||||
maxValue={5}
|
||||
title="Conversion Rate"
|
||||
color="#ff9800"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6} lg={3}>
|
||||
<PerformanceGauge
|
||||
value={realTimeData.metrics.content_quality_score}
|
||||
maxValue={100}
|
||||
title="Content Quality"
|
||||
color="#2196f3"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Quality Analysis Section */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 600 }}>
|
||||
<AutoAwesomeIcon sx={{ mr: 1, color: 'primary.main' }} />
|
||||
AI Quality Analysis
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<AnalyticsIcon />}
|
||||
onClick={() => setShowQualityDialog(true)}
|
||||
disabled={loadingQuality}
|
||||
>
|
||||
{loadingQuality ? 'Analyzing...' : 'View Detailed Analysis'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{qualityAnalysis && (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card elevation={2}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Overall Quality Score
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Typography variant="h3" sx={{ fontWeight: 'bold', color: 'primary.main' }}>
|
||||
{qualityAnalysis.overall_score.toFixed(1)}
|
||||
</Typography>
|
||||
<Box>
|
||||
<Chip
|
||||
icon={getStatusIcon(qualityAnalysis.overall_status)}
|
||||
label={qualityAnalysis.overall_status.replace('_', ' ').toUpperCase()}
|
||||
color={getStatusColor(qualityAnalysis.overall_status) as any}
|
||||
size="small"
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
Confidence: {qualityAnalysis.confidence_score.toFixed(1)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card elevation={2}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Priority Areas
|
||||
</Typography>
|
||||
<Box display="flex" flexWrap="wrap" gap={1}>
|
||||
{qualityAnalysis.metrics
|
||||
.filter(metric => metric.status === 'needs_attention' || metric.status === 'poor')
|
||||
.map((metric, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={metric.name}
|
||||
color={getStatusColor(metric.status) as any}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
))}
|
||||
{qualityAnalysis.metrics.filter(m => m.status === 'needs_attention' || m.status === 'poor').length === 0 && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
All areas are performing well! 🎉
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Advanced Charts Section */}
|
||||
<Grid container spacing={3} sx={{ mb: 4 }}>
|
||||
{/* Performance Trends Chart */}
|
||||
<Grid item xs={12} lg={8}>
|
||||
<PerformanceTrendChart
|
||||
data={trendData}
|
||||
title="Performance Trends Over Time"
|
||||
height={400}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Content Distribution Chart */}
|
||||
<Grid item xs={12} lg={4}>
|
||||
<ContentDistributionPie
|
||||
data={contentDistributionData}
|
||||
title="Content Distribution"
|
||||
height={400}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Quality Metrics Radar Chart */}
|
||||
<Grid item xs={12} lg={6}>
|
||||
<QualityMetricsRadar
|
||||
data={qualityMetricsData}
|
||||
title="Quality Metrics Analysis"
|
||||
height={400}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Performance Metrics Bar Chart */}
|
||||
<Grid item xs={12} lg={6}>
|
||||
<PerformanceMetricsBar
|
||||
data={performanceMetricsData}
|
||||
title="Performance vs Targets"
|
||||
height={400}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Quality Analysis Dialog */}
|
||||
<Dialog
|
||||
open={showQualityDialog}
|
||||
onClose={() => setShowQualityDialog(false)}
|
||||
maxWidth="lg"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<AutoAwesomeIcon color="primary" />
|
||||
<Typography variant="h6">AI Quality Analysis Details</Typography>
|
||||
</Box>
|
||||
<IconButton onClick={() => setShowQualityDialog(false)}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{qualityAnalysis && (
|
||||
<Box>
|
||||
{/* Overall Score */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Overall Quality Assessment
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 'bold', color: 'primary.main' }}>
|
||||
{qualityAnalysis.overall_score.toFixed(1)}/100
|
||||
</Typography>
|
||||
<Chip
|
||||
icon={getStatusIcon(qualityAnalysis.overall_status)}
|
||||
label={qualityAnalysis.overall_status.replace('_', ' ').toUpperCase()}
|
||||
color={getStatusColor(qualityAnalysis.overall_status) as any}
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
Confidence Score: {qualityAnalysis.confidence_score.toFixed(1)}%
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Detailed Metrics */}
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Detailed Quality Metrics
|
||||
</Typography>
|
||||
<Grid container spacing={2} sx={{ mb: 3 }}>
|
||||
{qualityAnalysis.metrics.map((metric, index) => (
|
||||
<Grid item xs={12} md={6} key={index}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={1}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||
{metric.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`${metric.score.toFixed(1)}/100`}
|
||||
color={getStatusColor(metric.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
{metric.description}
|
||||
</Typography>
|
||||
<Chip
|
||||
icon={getStatusIcon(metric.status)}
|
||||
label={metric.status.replace('_', ' ').toUpperCase()}
|
||||
color={getStatusColor(metric.status) as any}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* Recommendations */}
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<LightbulbIcon sx={{ mr: 1, color: 'warning.main' }} />
|
||||
AI Recommendations
|
||||
</Typography>
|
||||
<List>
|
||||
{qualityAnalysis.recommendations.map((recommendation, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemIcon>
|
||||
<LightbulbIcon color="warning" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={recommendation} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setShowQualityDialog(false)}>Close</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={loadQualityAnalysis}
|
||||
disabled={loadingQuality}
|
||||
>
|
||||
Refresh Analysis
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnhancedPerformanceVisualization;
|
||||
@@ -0,0 +1,403 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
Alert,
|
||||
Snackbar
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Check as CheckIcon,
|
||||
PlayArrow as PlayArrowIcon,
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
Celebration as CelebrationIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion, AnimatePresence, easeOut } from 'framer-motion';
|
||||
import StrategyActivationModal from '../../StrategyActivationModal';
|
||||
import { useNavigationOrchestrator } from '../../../../../services/navigationOrchestrator';
|
||||
|
||||
interface EnhancedStrategyActivationButtonProps {
|
||||
strategyData: any;
|
||||
strategyConfirmed: boolean;
|
||||
onConfirmStrategy: () => Promise<void>;
|
||||
onGenerateContentCalendar: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const EnhancedStrategyActivationButton: React.FC<EnhancedStrategyActivationButtonProps> = ({
|
||||
strategyData,
|
||||
strategyConfirmed,
|
||||
onConfirmStrategy,
|
||||
onGenerateContentCalendar,
|
||||
disabled = false
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
const [showSuccessMessage, setShowSuccessMessage] = useState(false);
|
||||
const [activationProgress, setActivationProgress] = useState(0);
|
||||
const [showActivationModal, setShowActivationModal] = useState(false);
|
||||
|
||||
// Initialize navigation orchestrator
|
||||
const navigationOrchestrator = useNavigationOrchestrator();
|
||||
|
||||
const handleActivation = async () => {
|
||||
console.log('🎯 EnhancedStrategyActivationButton: handleActivation called');
|
||||
if (isLoading || disabled) return;
|
||||
|
||||
// For now, directly call the activation function instead of opening the modal
|
||||
console.log('🎯 EnhancedStrategyActivationButton: Directly calling onConfirmStrategy');
|
||||
try {
|
||||
await onConfirmStrategy();
|
||||
console.log('🎯 EnhancedStrategyActivationButton: onConfirmStrategy completed successfully');
|
||||
} catch (error) {
|
||||
console.error('🎯 EnhancedStrategyActivationButton: onConfirmStrategy failed:', error);
|
||||
}
|
||||
|
||||
// Open the activation modal instead of calling onConfirmStrategy directly
|
||||
console.log('🎯 EnhancedStrategyActivationButton: Opening activation modal');
|
||||
setShowActivationModal(true);
|
||||
};
|
||||
|
||||
const handleGenerateCalendar = () => {
|
||||
onGenerateContentCalendar();
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setShowActivationModal(false);
|
||||
};
|
||||
|
||||
const handleSetupMonitoring = async (monitoringPlan: any) => {
|
||||
try {
|
||||
console.log('🎯 EnhancedStrategyActivationButton: handleSetupMonitoring called');
|
||||
// Call the actual activation function
|
||||
console.log('🎯 EnhancedStrategyActivationButton: Calling onConfirmStrategy()');
|
||||
await onConfirmStrategy();
|
||||
console.log('🎯 EnhancedStrategyActivationButton: onConfirmStrategy() completed');
|
||||
|
||||
// Update strategy state to confirmed/active
|
||||
// This will trigger UI updates in parent components
|
||||
|
||||
// Show success state
|
||||
setIsSuccess(true);
|
||||
setShowSuccessMessage(true);
|
||||
|
||||
// Use navigation orchestrator to handle successful activation
|
||||
const strategyId = strategyData?.strategy_metadata?.user_id || strategyData?.metadata?.user_id || '1';
|
||||
navigationOrchestrator.handleStrategyActivationSuccess(strategyId, strategyData);
|
||||
|
||||
// Reset after success animation
|
||||
setTimeout(() => {
|
||||
setIsSuccess(false);
|
||||
setActivationProgress(0);
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Strategy activation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Success animation variants
|
||||
const successVariants = {
|
||||
initial: { scale: 0, opacity: 0 },
|
||||
animate: {
|
||||
scale: [0, 1.2, 1],
|
||||
opacity: [0, 1, 1],
|
||||
transition: { duration: 0.6, ease: easeOut }
|
||||
},
|
||||
exit: { scale: 0, opacity: 0 }
|
||||
};
|
||||
|
||||
// Confetti animation variants
|
||||
const confettiVariants = {
|
||||
initial: { y: -20, opacity: 0, rotate: 0 },
|
||||
animate: {
|
||||
y: [0, -30, 0],
|
||||
opacity: [0, 1, 0],
|
||||
rotate: [0, 360],
|
||||
transition: { duration: 1.5, ease: easeOut }
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
{/* Strategy Activation Modal */}
|
||||
<StrategyActivationModal
|
||||
open={showActivationModal}
|
||||
onClose={handleCloseModal}
|
||||
strategyId={strategyData?.id || 1} // Use actual strategy ID
|
||||
strategyData={strategyData}
|
||||
onSetupMonitoring={handleSetupMonitoring}
|
||||
/>
|
||||
|
||||
{/* Success Message Snackbar */}
|
||||
<Snackbar
|
||||
open={showSuccessMessage}
|
||||
autoHideDuration={4000}
|
||||
onClose={() => setShowSuccessMessage(false)}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
>
|
||||
<Alert
|
||||
severity="success"
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
boxShadow: '0 8px 32px rgba(76, 175, 80, 0.3)',
|
||||
border: '1px solid rgba(76, 175, 80, 0.3)'
|
||||
}}
|
||||
>
|
||||
🎉 Strategy activated successfully! Ready to generate content calendar.
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
{/* Main Button Container */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', position: 'relative' }}>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
style={{ position: 'relative' }}
|
||||
>
|
||||
{/* Enhanced Activation Button */}
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={strategyConfirmed ? handleGenerateCalendar : handleActivation}
|
||||
disabled={disabled || isLoading}
|
||||
startIcon={
|
||||
isLoading ? (
|
||||
<CircularProgress
|
||||
size={20}
|
||||
sx={{ color: 'white' }}
|
||||
/>
|
||||
) : strategyConfirmed ? (
|
||||
<AutoAwesomeIcon />
|
||||
) : (
|
||||
<PlayArrowIcon />
|
||||
)
|
||||
}
|
||||
sx={{
|
||||
background: strategyConfirmed
|
||||
? 'linear-gradient(135deg, #4caf50 0%, #45a049 100%)'
|
||||
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: 4,
|
||||
px: 6,
|
||||
py: 2,
|
||||
fontWeight: 700,
|
||||
fontSize: '1.1rem',
|
||||
textTransform: 'none',
|
||||
letterSpacing: '0.5px',
|
||||
boxShadow: strategyConfirmed
|
||||
? '0 8px 32px rgba(76, 175, 80, 0.4), 0 0 20px rgba(76, 175, 80, 0.2)'
|
||||
: '0 8px 32px rgba(102, 126, 234, 0.4), 0 0 20px rgba(102, 126, 234, 0.2)',
|
||||
border: '2px solid transparent',
|
||||
backgroundClip: 'padding-box',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
minWidth: 280,
|
||||
height: 56,
|
||||
'&:hover': {
|
||||
background: strategyConfirmed
|
||||
? 'linear-gradient(135deg, #45a049 0%, #3d8b40 100%)'
|
||||
: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
|
||||
boxShadow: strategyConfirmed
|
||||
? '0 12px 40px rgba(76, 175, 80, 0.5), 0 0 30px rgba(76, 175, 80, 0.3)'
|
||||
: '0 12px 40px rgba(102, 126, 234, 0.5), 0 0 30px rgba(102, 126, 234, 0.3)',
|
||||
transform: 'translateY(-3px)',
|
||||
'&::before': {
|
||||
opacity: 1,
|
||||
transform: 'scale(1.1)'
|
||||
}
|
||||
},
|
||||
'&:disabled': {
|
||||
background: 'linear-gradient(135deg, #9e9e9e 0%, #757575 100%)',
|
||||
boxShadow: 'none',
|
||||
transform: 'none'
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.2) 50%, transparent 70%)',
|
||||
opacity: 0,
|
||||
transform: 'scale(0.8)',
|
||||
transition: 'all 0.3s ease',
|
||||
pointerEvents: 'none'
|
||||
},
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
|
||||
}}
|
||||
>
|
||||
{/* Button Text */}
|
||||
<AnimatePresence mode="wait">
|
||||
{isLoading ? (
|
||||
<motion.div
|
||||
key="loading"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Typography variant="button" sx={{ fontWeight: 600 }}>
|
||||
Activating Strategy... {activationProgress}%
|
||||
</Typography>
|
||||
</motion.div>
|
||||
) : isSuccess ? (
|
||||
<motion.div
|
||||
key="success"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Typography variant="button" sx={{ fontWeight: 600 }}>
|
||||
Strategy Activated! 🎉
|
||||
</Typography>
|
||||
</motion.div>
|
||||
) : strategyConfirmed ? (
|
||||
<motion.div
|
||||
key="calendar"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Typography variant="button" sx={{ fontWeight: 600 }}>
|
||||
Generate Content Calendar
|
||||
</Typography>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="activate"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Typography variant="button" sx={{ fontWeight: 600 }}>
|
||||
Confirm & Activate Strategy
|
||||
</Typography>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Button>
|
||||
|
||||
{/* Progress Ring (shown during loading) */}
|
||||
<AnimatePresence>
|
||||
{isLoading && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: '50%',
|
||||
background: 'conic-gradient(from 0deg, rgba(102, 126, 234, 0.3) 0deg, rgba(102, 126, 234, 0.8) 360deg)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
animation: 'spin 2s linear infinite',
|
||||
'@keyframes spin': {
|
||||
'0%': { transform: 'translate(-50%, -50%) rotate(0deg)' },
|
||||
'100%': { transform: 'translate(-50%, -50%) rotate(360deg)' }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Success Animation Overlay */}
|
||||
<AnimatePresence>
|
||||
{isSuccess && (
|
||||
<motion.div
|
||||
variants={successVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
zIndex: 2
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #4caf50 0%, #45a049 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 8px 32px rgba(76, 175, 80, 0.4)'
|
||||
}}
|
||||
>
|
||||
<CheckIcon sx={{ color: 'white', fontSize: 32 }} />
|
||||
</Box>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Confetti Animation */}
|
||||
<AnimatePresence>
|
||||
{isSuccess && (
|
||||
<>
|
||||
{[...Array(8)].map((_, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
variants={confettiVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
zIndex: 3,
|
||||
transform: `translate(-50%, -50%) rotate(${index * 45}deg)`
|
||||
}}
|
||||
>
|
||||
<CelebrationIcon
|
||||
sx={{
|
||||
color: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57'][index % 5],
|
||||
fontSize: 16
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</Box>
|
||||
|
||||
{/* Status Text */}
|
||||
<AnimatePresence>
|
||||
{isLoading && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
style={{ marginTop: 16, textAlign: 'center' }}
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 500 }}>
|
||||
Setting up monitoring and quality gates...
|
||||
</Typography>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnhancedStrategyActivationButton;
|
||||
@@ -0,0 +1,456 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Chip,
|
||||
IconButton,
|
||||
Collapse,
|
||||
Grid,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Divider,
|
||||
Tooltip,
|
||||
Alert,
|
||||
AlertTitle
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Info as InfoIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
ExpandLess as ExpandLessIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Warning as WarningIcon,
|
||||
Error as ErrorIcon,
|
||||
DataUsage as DataUsageIcon,
|
||||
Psychology as PsychologyIcon,
|
||||
Timeline as TimelineIcon,
|
||||
Refresh as RefreshIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface MonitoringTask {
|
||||
title: string;
|
||||
description: string;
|
||||
assignee: 'ALwrity' | 'Human';
|
||||
frequency: string;
|
||||
metric: string;
|
||||
measurementMethod: string;
|
||||
successCriteria: string;
|
||||
alertThreshold: string;
|
||||
actionableInsights?: string;
|
||||
lastExecuted?: string;
|
||||
status: 'pending' | 'active' | 'completed' | 'failed';
|
||||
}
|
||||
|
||||
interface MetricTransparencyData {
|
||||
metricName: string;
|
||||
currentValue: number;
|
||||
unit: string;
|
||||
dataFreshness: {
|
||||
lastUpdated: string;
|
||||
updateFrequency: string;
|
||||
dataSource: string;
|
||||
confidence: number;
|
||||
};
|
||||
measurementMethodology: {
|
||||
description: string;
|
||||
calculationMethod: string;
|
||||
dataPoints: string[];
|
||||
validationProcess: string;
|
||||
};
|
||||
monitoringTasks: MonitoringTask[];
|
||||
strategyMapping: {
|
||||
relatedComponents: string[];
|
||||
impactAreas: string[];
|
||||
dependencies: string[];
|
||||
};
|
||||
aiInsights: {
|
||||
trendAnalysis: string;
|
||||
recommendations: string[];
|
||||
riskFactors: string[];
|
||||
opportunities: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface MetricTransparencyCardProps {
|
||||
metricData: MetricTransparencyData;
|
||||
isExpanded?: boolean;
|
||||
onToggle?: () => void;
|
||||
}
|
||||
|
||||
const MetricTransparencyCard: React.FC<MetricTransparencyCardProps> = ({
|
||||
metricData,
|
||||
isExpanded = false,
|
||||
onToggle
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(isExpanded);
|
||||
|
||||
const handleToggle = () => {
|
||||
setExpanded(!expanded);
|
||||
onToggle?.();
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active': return '#4caf50';
|
||||
case 'completed': return '#2196f3';
|
||||
case 'pending': return '#ff9800';
|
||||
case 'failed': return '#f44336';
|
||||
default: return '#9e9e9e';
|
||||
}
|
||||
};
|
||||
|
||||
const getConfidenceColor = (confidence: number) => {
|
||||
if (confidence >= 90) return '#4caf50';
|
||||
if (confidence >= 70) return '#ff9800';
|
||||
return '#f44336';
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
|
||||
|
||||
if (diffInHours < 1) return 'Just now';
|
||||
if (diffInHours < 24) return `${diffInHours} hours ago`;
|
||||
if (diffInHours < 168) return `${Math.floor(diffInHours / 24)} days ago`;
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Card sx={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
mb: 2,
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'radial-gradient(circle at 20% 80%, rgba(255,255,255,0.1) 0%, transparent 50%)',
|
||||
pointerEvents: 'none'
|
||||
}
|
||||
}}>
|
||||
<CardContent sx={{ position: 'relative', zIndex: 1, p: 3 }}>
|
||||
{/* Header */}
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<AutoAwesomeIcon sx={{ fontSize: 28, color: 'rgba(255,255,255,0.9)' }} />
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, mb: 0.5 }}>
|
||||
{metricData.metricName}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.8 }}>
|
||||
{metricData.currentValue}{metricData.unit}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Chip
|
||||
label={`${metricData.dataFreshness.confidence}% Confidence`}
|
||||
size="small"
|
||||
sx={{
|
||||
background: getConfidenceColor(metricData.dataFreshness.confidence),
|
||||
color: 'white',
|
||||
fontWeight: 600
|
||||
}}
|
||||
/>
|
||||
<Tooltip title={expanded ? "Hide details" : "Show details"}>
|
||||
<IconButton onClick={handleToggle} sx={{ color: 'white' }}>
|
||||
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Data Freshness Summary */}
|
||||
<Box sx={{ mb: 2, p: 2, background: 'rgba(255,255,255,0.1)', borderRadius: 1 }}>
|
||||
<Box display="flex" alignItems="center" gap={1} mb={1}>
|
||||
<ScheduleIcon sx={{ fontSize: 16 }} />
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
Data Freshness
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>
|
||||
Last updated: {formatDate(metricData.dataFreshness.lastUpdated)} •
|
||||
Source: {metricData.dataFreshness.dataSource} •
|
||||
Updates: {metricData.dataFreshness.updateFrequency}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Expanded Content */}
|
||||
<Collapse in={expanded}>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Divider sx={{ mb: 2, borderColor: 'rgba(255,255,255,0.2)' }} />
|
||||
|
||||
{/* Measurement Methodology */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<AssessmentIcon />
|
||||
Measurement Methodology
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Calculation Method
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9, mb: 2 }}>
|
||||
{metricData.measurementMethodology.calculationMethod}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Validation Process
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9, mb: 2 }}>
|
||||
{metricData.measurementMethodology.validationProcess}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Data Points Used
|
||||
</Typography>
|
||||
<Box display="flex" gap={1} flexWrap="wrap" mb={2}>
|
||||
{metricData.measurementMethodology.dataPoints.map((point, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={point}
|
||||
size="small"
|
||||
sx={{
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
color: 'white',
|
||||
fontSize: '0.7rem'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* AI Monitoring Tasks */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<PsychologyIcon />
|
||||
AI Monitoring Tasks ({metricData.monitoringTasks.length})
|
||||
</Typography>
|
||||
|
||||
<List sx={{ p: 0 }}>
|
||||
{metricData.monitoringTasks.map((task, index) => (
|
||||
<ListItem key={index} sx={{
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start'
|
||||
}}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" width="100%" mb={1}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{task.title}
|
||||
</Typography>
|
||||
<Box display="flex" gap={1}>
|
||||
<Chip
|
||||
label={task.assignee}
|
||||
size="small"
|
||||
sx={{
|
||||
background: task.assignee === 'ALwrity' ? '#4caf50' : '#2196f3',
|
||||
color: 'white',
|
||||
fontSize: '0.6rem'
|
||||
}}
|
||||
/>
|
||||
<Chip
|
||||
label={task.status}
|
||||
size="small"
|
||||
sx={{
|
||||
background: getStatusColor(task.status),
|
||||
color: 'white',
|
||||
fontSize: '0.6rem'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" sx={{ opacity: 0.8, mb: 1 }}>
|
||||
{task.description}
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2} sx={{ width: '100%' }}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: 'rgba(255,255,255,0.7)' }}>
|
||||
Measurement Method
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontSize: '0.75rem', opacity: 0.8 }}>
|
||||
{task.measurementMethod}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: 'rgba(255,255,255,0.7)' }}>
|
||||
Success Criteria
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontSize: '0.75rem', opacity: 0.8 }}>
|
||||
{task.successCriteria}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
{/* Strategy Mapping */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TimelineIcon />
|
||||
Strategy Component Mapping
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Related Components
|
||||
</Typography>
|
||||
<Box display="flex" gap={1} flexWrap="wrap">
|
||||
{metricData.strategyMapping.relatedComponents.map((component, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={component}
|
||||
size="small"
|
||||
sx={{
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
color: 'white',
|
||||
fontSize: '0.7rem'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Impact Areas
|
||||
</Typography>
|
||||
<Box display="flex" gap={1} flexWrap="wrap">
|
||||
{metricData.strategyMapping.impactAreas.map((area, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={area}
|
||||
size="small"
|
||||
sx={{
|
||||
background: 'rgba(76, 175, 80, 0.3)',
|
||||
color: '#4caf50',
|
||||
fontSize: '0.7rem'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Dependencies
|
||||
</Typography>
|
||||
<Box display="flex" gap={1} flexWrap="wrap">
|
||||
{metricData.strategyMapping.dependencies.map((dep, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={dep}
|
||||
size="small"
|
||||
sx={{
|
||||
background: 'rgba(255, 152, 0, 0.3)',
|
||||
color: '#ff9800',
|
||||
fontSize: '0.7rem'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* AI Insights */}
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<AutoAwesomeIcon />
|
||||
AI-Powered Insights
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Trend Analysis
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9, mb: 2 }}>
|
||||
{metricData.aiInsights.trendAnalysis}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Risk Factors
|
||||
</Typography>
|
||||
<Box display="flex" gap={1} flexWrap="wrap" mb={2}>
|
||||
{metricData.aiInsights.riskFactors.map((risk, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={risk}
|
||||
size="small"
|
||||
sx={{
|
||||
background: 'rgba(244, 67, 54, 0.3)',
|
||||
color: '#f44336',
|
||||
fontSize: '0.7rem'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Recommendations
|
||||
</Typography>
|
||||
<List sx={{ p: 0 }}>
|
||||
{metricData.aiInsights.recommendations.map((rec, index) => (
|
||||
<ListItem key={index} sx={{ p: 0, mb: 1 }}>
|
||||
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||
<CheckCircleIcon sx={{ fontSize: 16, color: '#4caf50' }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={rec}
|
||||
sx={{
|
||||
'& .MuiListItemText-primary': {
|
||||
fontSize: '0.85rem',
|
||||
opacity: 0.9
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricTransparencyCard;
|
||||
@@ -0,0 +1,439 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Grid,
|
||||
CircularProgress,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Alert,
|
||||
AlertTitle,
|
||||
LinearProgress,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import {
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
Speed as SpeedIcon,
|
||||
Visibility as VisibilityIcon,
|
||||
People as EngagementIcon,
|
||||
MonetizationOn as MonetizationOnIcon,
|
||||
Refresh as RefreshIcon,
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Warning as WarningIcon,
|
||||
Error as ErrorIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion } from 'framer-motion';
|
||||
import { strategyMonitoringApi } from '../../../../../services/strategyMonitoringApi';
|
||||
|
||||
interface PerformanceMetrics {
|
||||
traffic_growth_percentage: number;
|
||||
engagement_rate_percentage: number;
|
||||
conversion_rate_percentage: number;
|
||||
roi_ratio: number;
|
||||
strategy_adoption_rate: number;
|
||||
content_quality_score: number;
|
||||
competitive_position_rank: number;
|
||||
audience_growth_percentage: number;
|
||||
confidence_score: number;
|
||||
last_updated: string;
|
||||
}
|
||||
|
||||
interface PerformanceVisualizationProps {
|
||||
strategyId: number;
|
||||
strategyData?: any;
|
||||
}
|
||||
|
||||
const PerformanceVisualization: React.FC<PerformanceVisualizationProps> = ({
|
||||
strategyId,
|
||||
strategyData
|
||||
}) => {
|
||||
const [metrics, setMetrics] = useState<PerformanceMetrics | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [lastRefresh, setLastRefresh] = useState<Date>(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
loadPerformanceMetrics();
|
||||
}, [strategyId]);
|
||||
|
||||
const loadPerformanceMetrics = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Call the API to get performance metrics
|
||||
const response = await strategyMonitoringApi.getPerformanceMetrics(strategyId);
|
||||
setMetrics(response.data);
|
||||
setLastRefresh(new Date());
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load performance metrics');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getMetricColor = (value: number, threshold: number = 0) => {
|
||||
if (value >= threshold + 10) return '#4caf50'; // Green
|
||||
if (value >= threshold) return '#ff9800'; // Orange
|
||||
return '#f44336'; // Red
|
||||
};
|
||||
|
||||
const getMetricIcon = (value: number, threshold: number = 0) => {
|
||||
if (value >= threshold + 10) return <TrendingUpIcon />;
|
||||
if (value >= threshold) return <TrendingDownIcon />;
|
||||
return <ErrorIcon />;
|
||||
};
|
||||
|
||||
const getMetricStatus = (value: number, threshold: number = 0) => {
|
||||
if (value >= threshold + 10) return 'Excellent';
|
||||
if (value >= threshold) return 'Good';
|
||||
return 'Needs Attention';
|
||||
};
|
||||
|
||||
const MetricCard = ({
|
||||
title,
|
||||
value,
|
||||
unit = '%',
|
||||
threshold = 0,
|
||||
icon,
|
||||
description
|
||||
}: {
|
||||
title: string;
|
||||
value: number;
|
||||
unit?: string;
|
||||
threshold?: number;
|
||||
icon: React.ReactNode;
|
||||
description: string;
|
||||
}) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Card sx={{
|
||||
height: '100%',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'radial-gradient(circle at 20% 80%, rgba(255,255,255,0.1) 0%, transparent 50%)',
|
||||
pointerEvents: 'none'
|
||||
}
|
||||
}}>
|
||||
<CardContent sx={{ position: 'relative', zIndex: 1, p: 3 }}>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
{icon}
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Chip
|
||||
label={getMetricStatus(value, threshold)}
|
||||
size="small"
|
||||
sx={{
|
||||
background: getMetricColor(value, threshold),
|
||||
color: 'white',
|
||||
fontWeight: 600
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h3" sx={{
|
||||
fontWeight: 700,
|
||||
mb: 1,
|
||||
color: getMetricColor(value, threshold)
|
||||
}}>
|
||||
{value}{unit}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" sx={{
|
||||
opacity: 0.9,
|
||||
mb: 2
|
||||
}}>
|
||||
{description}
|
||||
</Typography>
|
||||
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={Math.min((value / (threshold + 20)) * 100, 100)}
|
||||
sx={{
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
background: getMetricColor(value, threshold),
|
||||
borderRadius: 3
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" minHeight={400}>
|
||||
<CircularProgress size={60} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
<AlertTitle>Error Loading Performance Data</AlertTitle>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!metrics) {
|
||||
return (
|
||||
<Alert severity="info" sx={{ mb: 3 }}>
|
||||
<AlertTitle>No Performance Data Available</AlertTitle>
|
||||
Performance metrics will appear here once your strategy is activated and monitoring begins.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
{/* Header Section */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<Box>
|
||||
<Typography variant="h4" sx={{
|
||||
fontWeight: 700,
|
||||
background: 'linear-gradient(45deg, #667eea, #764ba2)',
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
mb: 1
|
||||
}}>
|
||||
🚀 Strategy Performance Dashboard
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: 'text.secondary' }}>
|
||||
Real-time monitoring and performance analytics for your content strategy
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
Last updated: {new Date(metrics.last_updated).toLocaleString()}
|
||||
</Typography>
|
||||
<Tooltip title="Refresh metrics">
|
||||
<IconButton onClick={loadPerformanceMetrics} sx={{ color: 'primary.main' }}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
</Box>
|
||||
|
||||
{/* Performance Metrics Grid */}
|
||||
<Grid container spacing={3} sx={{ mb: 4 }}>
|
||||
<Grid item xs={12} md={6} lg={3}>
|
||||
<MetricCard
|
||||
title="Traffic Growth"
|
||||
value={metrics.traffic_growth_percentage}
|
||||
unit="%"
|
||||
threshold={5}
|
||||
icon={<TrendingUpIcon />}
|
||||
description="Organic traffic growth compared to previous period"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6} lg={3}>
|
||||
<MetricCard
|
||||
title="Engagement Rate"
|
||||
value={metrics.engagement_rate_percentage}
|
||||
unit="%"
|
||||
threshold={5}
|
||||
icon={<EngagementIcon />}
|
||||
description="Average engagement rate across all content"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6} lg={3}>
|
||||
<MetricCard
|
||||
title="Conversion Rate"
|
||||
value={metrics.conversion_rate_percentage}
|
||||
unit="%"
|
||||
threshold={1}
|
||||
icon={<MonetizationOnIcon />}
|
||||
description="Content-driven conversion rate"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6} lg={3}>
|
||||
<MetricCard
|
||||
title="ROI Ratio"
|
||||
value={metrics.roi_ratio}
|
||||
unit="x"
|
||||
threshold={2}
|
||||
icon={<SpeedIcon />}
|
||||
description="Return on investment for content strategy"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Strategy Effectiveness Metrics */}
|
||||
<Grid container spacing={3} sx={{ mb: 4 }}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card sx={{ height: '100%', background: 'linear-gradient(135deg, #4caf50 0%, #45a049 100%)', color: 'white' }}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<AutoAwesomeIcon />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
Strategy Adoption Rate
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h2" sx={{ fontWeight: 700, mb: 2 }}>
|
||||
{metrics.strategy_adoption_rate}%
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" sx={{ opacity: 0.9, mb: 2 }}>
|
||||
Percentage of strategy components successfully implemented and monitored
|
||||
</Typography>
|
||||
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={metrics.strategy_adoption_rate}
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
background: 'white',
|
||||
borderRadius: 4
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card sx={{ height: '100%', background: 'linear-gradient(135deg, #ff9800 0%, #f57c00 100%)', color: 'white' }}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<AssessmentIcon />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
Content Quality Score
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h2" sx={{ fontWeight: 700, mb: 2 }}>
|
||||
{metrics.content_quality_score}/100
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" sx={{ opacity: 0.9, mb: 2 }}>
|
||||
AI-powered quality assessment of your content strategy
|
||||
</Typography>
|
||||
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={metrics.content_quality_score}
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
background: 'white',
|
||||
borderRadius: 4
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Competitive Analysis */}
|
||||
<Card sx={{ mb: 4, background: 'linear-gradient(135deg, #9c27b0 0%, #7b1fa2 100%)', color: 'white' }}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={3}>
|
||||
<VisibilityIcon />
|
||||
<Typography variant="h5" sx={{ fontWeight: 600 }}>
|
||||
Competitive Position Analysis
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||
Market Rank
|
||||
</Typography>
|
||||
<Typography variant="h3" sx={{ fontWeight: 700 }}>
|
||||
#{metrics.competitive_position_rank}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>
|
||||
Your position among top competitors in the market
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||
Audience Growth
|
||||
</Typography>
|
||||
<Typography variant="h3" sx={{ fontWeight: 700 }}>
|
||||
{metrics.audience_growth_percentage}%
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>
|
||||
Monthly audience growth rate
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Confidence Score */}
|
||||
<Card sx={{ background: 'linear-gradient(135deg, #2196f3 0%, #1976d2 100%)', color: 'white' }}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<CheckCircleIcon />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
AI Confidence Score
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, mb: 1 }}>
|
||||
{metrics.confidence_score}%
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>
|
||||
AI confidence level in the accuracy of these performance metrics
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PerformanceVisualization;
|
||||
@@ -67,6 +67,15 @@ const ProgressiveCard: React.FC<ProgressiveCardProps> = ({
|
||||
const componentStatus = component?.status || 'not_reviewed';
|
||||
const componentReviewedAt = component?.reviewedAt;
|
||||
|
||||
// Debug logging for component status
|
||||
if (componentId) {
|
||||
console.log(`🔧 ProgressiveCard [${componentId}]:`, {
|
||||
componentStatus,
|
||||
componentReviewedAt,
|
||||
allComponents: components.map(c => ({ id: c.id, status: c.status }))
|
||||
});
|
||||
}
|
||||
|
||||
// Handle hover interactions
|
||||
const handleMouseEnter = () => {
|
||||
if (trigger === 'hover') {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@@ -16,27 +16,40 @@ import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
Warning as WarningIcon,
|
||||
PlayArrow as PlayArrowIcon
|
||||
PlayArrow as PlayArrowIcon,
|
||||
AutoAwesome as AutoAwesomeIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useStrategyReviewStore } from '../../../../../stores/strategyReviewStore';
|
||||
import { useStrategyReviewStore, StrategyComponent } from '../../../../../stores/strategyReviewStore';
|
||||
import { ANALYSIS_CARD_STYLES } from '../styles';
|
||||
import { contentPlanningApi } from '../../../../../services/contentPlanningApi';
|
||||
import EnhancedStrategyActivationButton from './EnhancedStrategyActivationButton';
|
||||
import { StrategyData } from '../types/strategy.types';
|
||||
import { useNavigationOrchestrator } from '../../../../../services/navigationOrchestrator';
|
||||
|
||||
const ReviewProgressHeader: React.FC = () => {
|
||||
interface ReviewProgressHeaderProps {
|
||||
strategyData?: StrategyData | null;
|
||||
}
|
||||
|
||||
const ReviewProgressHeader: React.FC<ReviewProgressHeaderProps> = ({ strategyData }) => {
|
||||
const {
|
||||
components,
|
||||
reviewProgress,
|
||||
isAllReviewed,
|
||||
isActivated,
|
||||
resetAllReviews,
|
||||
getUnreviewedComponents,
|
||||
getReviewedComponents
|
||||
getReviewedComponents,
|
||||
activateStrategy
|
||||
} = useStrategyReviewStore();
|
||||
|
||||
// Extract domain name from strategy data (you can pass this as prop if needed)
|
||||
// Initialize navigation orchestrator
|
||||
const navigationOrchestrator = useNavigationOrchestrator();
|
||||
|
||||
// Extract domain name from strategy data
|
||||
const getDomainName = () => {
|
||||
// For now, return a default domain - you can enhance this to get from strategy data
|
||||
return "alwrity.com";
|
||||
// Since StrategyMetadata doesn't have domain, we'll use a fallback
|
||||
return "alwrity.com"; // fallback
|
||||
};
|
||||
|
||||
const unreviewedCount = getUnreviewedComponents().length;
|
||||
@@ -50,10 +63,13 @@ const ReviewProgressHeader: React.FC = () => {
|
||||
unreviewedCount,
|
||||
reviewedCount,
|
||||
totalCount,
|
||||
isAllReviewed: isAllReviewed()
|
||||
isAllReviewed: isAllReviewed(),
|
||||
isActivated: isActivated(),
|
||||
strategyData
|
||||
});
|
||||
|
||||
const getProgressColor = () => {
|
||||
if (isActivated()) return ANALYSIS_CARD_STYLES.colors.success;
|
||||
if (reviewProgress === 100) return ANALYSIS_CARD_STYLES.colors.success;
|
||||
if (reviewProgress >= 60) return ANALYSIS_CARD_STYLES.colors.primary;
|
||||
if (reviewProgress >= 30) return ANALYSIS_CARD_STYLES.colors.warning;
|
||||
@@ -61,12 +77,55 @@ const ReviewProgressHeader: React.FC = () => {
|
||||
};
|
||||
|
||||
const getProgressText = () => {
|
||||
if (isActivated()) return 'Strategy Active & Monitored!';
|
||||
if (reviewProgress === 100) return 'All components reviewed!';
|
||||
if (reviewProgress >= 60) return 'Great progress!';
|
||||
if (reviewProgress >= 30) return 'Making good progress';
|
||||
return 'Getting started';
|
||||
};
|
||||
|
||||
// Prepare strategy data for the enhanced button
|
||||
const buttonStrategyData = strategyData ? {
|
||||
id: strategyData.strategy_metadata?.user_id || strategyData.metadata?.user_id || 1,
|
||||
business_name: strategyData.strategy_metadata?.strategy_name || strategyData.metadata?.strategy_name || "ALwrity",
|
||||
domain: getDomainName(),
|
||||
// Add other strategy data as needed
|
||||
} : {
|
||||
id: 1,
|
||||
business_name: "ALwrity",
|
||||
domain: getDomainName(),
|
||||
};
|
||||
|
||||
const handleConfirmStrategy = async () => {
|
||||
// This will be called by the enhanced button when activation is confirmed
|
||||
console.log('🎯 Strategy activation confirmed');
|
||||
|
||||
// Activate the strategy in the store
|
||||
activateStrategy();
|
||||
|
||||
// You can add additional logic here if needed
|
||||
};
|
||||
|
||||
const handleGenerateContentCalendar = () => {
|
||||
console.log('🎯 Generate content calendar clicked');
|
||||
|
||||
// Prepare strategy context for navigation
|
||||
const strategyContext = {
|
||||
strategyId: (strategyData?.strategy_metadata?.user_id || strategyData?.metadata?.user_id || '1').toString(),
|
||||
strategyData: strategyData,
|
||||
activationStatus: 'active' as const,
|
||||
activationTimestamp: new Date().toISOString(),
|
||||
userPreferences: {},
|
||||
strategicIntelligence: {}
|
||||
};
|
||||
|
||||
// Navigate to calendar wizard using navigation orchestrator
|
||||
navigationOrchestrator.navigateToCalendarWizard(
|
||||
strategyContext.strategyId,
|
||||
strategyContext
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
@@ -102,291 +161,99 @@ const ReviewProgressHeader: React.FC = () => {
|
||||
bottom: 0,
|
||||
background: 'linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.1) 50%, transparent 70%)',
|
||||
animation: 'shimmer 3s infinite',
|
||||
pointerEvents: 'none'
|
||||
},
|
||||
'@keyframes shimmer': {
|
||||
'0%': { transform: 'translateX(-100%)' },
|
||||
'100%': { transform: 'translateX(100%)' }
|
||||
'@keyframes shimmer': {
|
||||
'0%': { transform: 'translateX(-100%)' },
|
||||
'100%': { transform: 'translateX(100%)' }
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Animated Border Lights */}
|
||||
<motion.div
|
||||
animate={{
|
||||
boxShadow: [
|
||||
'0 0 20px rgba(102, 126, 234, 0.5)',
|
||||
'0 0 40px rgba(102, 126, 234, 0.8)',
|
||||
'0 0 20px rgba(102, 126, 234, 0.5)'
|
||||
]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
borderRadius: '12px',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
<CardContent sx={{ position: 'relative', zIndex: 1, p: 3 }}>
|
||||
{/* Header Section */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{
|
||||
fontWeight: 700,
|
||||
background: 'linear-gradient(45deg, #667eea, #764ba2)',
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
mb: 0.5
|
||||
}}>
|
||||
Strategy Review Progress
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.8)', fontWeight: 500 }}>
|
||||
Review all strategy components to activate your content strategy
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<CardContent sx={{ position: 'relative', zIndex: 1, p: 2 }}>
|
||||
{/* Header with Circular Progress and Status Chips */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
{/* Circular Progress */}
|
||||
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
|
||||
{/* Progress Circle */}
|
||||
<Box sx={{ position: 'relative', display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<CircularProgress
|
||||
variant="determinate"
|
||||
value={reviewProgress}
|
||||
size={50}
|
||||
size={60}
|
||||
thickness={4}
|
||||
sx={{
|
||||
color: getProgressColor(),
|
||||
'& .MuiCircularProgress-circle': {
|
||||
strokeLinecap: 'round',
|
||||
filter: 'drop-shadow(0 0 8px rgba(102, 126, 234, 0.5))'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" sx={{
|
||||
color: getProgressColor(),
|
||||
fontWeight: 700,
|
||||
fontSize: '0.7rem'
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 700,
|
||||
color: 'white',
|
||||
lineHeight: 1
|
||||
}}>
|
||||
{Math.round(reviewProgress)}%
|
||||
{reviewProgress}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Progress Text */}
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
mb: 0.25
|
||||
}}>
|
||||
Strategy Review Progress
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
fontWeight: 600,
|
||||
color: getProgressColor(),
|
||||
fontSize: '0.8rem'
|
||||
}}>
|
||||
{getProgressText()}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Status Chips */}
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Chip
|
||||
icon={<CheckCircleIcon />}
|
||||
label={`${reviewedCount} Reviewed`}
|
||||
size="small"
|
||||
sx={{
|
||||
background: ANALYSIS_CARD_STYLES.colors.success,
|
||||
color: 'white',
|
||||
fontWeight: 500,
|
||||
fontSize: '0.65rem',
|
||||
height: 24,
|
||||
'& .MuiChip-icon': {
|
||||
color: 'white',
|
||||
fontSize: 14
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{unreviewedCount > 0 && (
|
||||
<Chip
|
||||
icon={<ScheduleIcon />}
|
||||
label={`${unreviewedCount} Pending`}
|
||||
size="small"
|
||||
sx={{
|
||||
background: ANALYSIS_CARD_STYLES.colors.warning,
|
||||
color: 'white',
|
||||
fontWeight: 500,
|
||||
fontSize: '0.65rem',
|
||||
height: 24,
|
||||
'& .MuiChip-icon': {
|
||||
color: 'white',
|
||||
fontSize: 14
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Typography variant="caption" sx={{
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize: '0.7rem'
|
||||
}}>
|
||||
{reviewedCount} of {totalCount} reviewed
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Reset Button */}
|
||||
<Tooltip title="Reset all reviews">
|
||||
<IconButton
|
||||
onClick={resetAllReviews}
|
||||
size="small"
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
'&:hover': {
|
||||
color: '#f44336',
|
||||
background: 'rgba(244, 67, 54, 0.2)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RefreshIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* Progress Summary */}
|
||||
<Box sx={{ mb: 1.5 }}>
|
||||
{/* Component Status */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" sx={{
|
||||
color: 'white',
|
||||
fontWeight: 500,
|
||||
fontSize: '0.8rem'
|
||||
fontWeight: 600,
|
||||
mb: 1,
|
||||
color: 'rgba(255, 255, 255, 0.9)'
|
||||
}}>
|
||||
{reviewedCount} of {totalCount} components reviewed
|
||||
Component Status:
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Informative Text */}
|
||||
<Box sx={{ mb: 1.5 }}>
|
||||
<Typography variant="body2" sx={{
|
||||
color: 'rgba(255, 255, 255, 0.9)',
|
||||
fontSize: '0.8rem',
|
||||
lineHeight: 1.4,
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
p: 1.5,
|
||||
borderRadius: 1,
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)'
|
||||
}}>
|
||||
<strong>Complete review by clicking 'Not Reviewed' button and confirming datapoints of 5 analysis components below.</strong>
|
||||
<br />
|
||||
<span style={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Important: Content strategy for <strong>{getDomainName()}</strong> will shape content generation next.
|
||||
</span>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Individual Component Status and Activate Strategy Button */}
|
||||
<Box sx={{ mb: 1.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.75 }}>
|
||||
<Typography variant="caption" sx={{
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
fontWeight: 500,
|
||||
fontSize: '0.7rem'
|
||||
}}>
|
||||
Component Status
|
||||
</Typography>
|
||||
|
||||
{/* Confirm & Activate Strategy Button */}
|
||||
<Tooltip
|
||||
title={isAllReviewed() ? "Confirm strategy and activate content generation" : "Complete all component reviews to confirm and activate strategy"}
|
||||
arrow
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={!isAllReviewed()}
|
||||
startIcon={<PlayArrowIcon />}
|
||||
onClick={async () => {
|
||||
if (isAllReviewed()) {
|
||||
try {
|
||||
// Handle strategy confirmation and activation
|
||||
console.log('Confirming and activating strategy...');
|
||||
|
||||
// 1. Save the strategy confirmation to backend
|
||||
// Note: You'll need to get the actual strategy ID from context/props
|
||||
const strategyId = "current_strategy_id"; // Replace with actual strategy ID
|
||||
|
||||
try {
|
||||
await contentPlanningApi.updateEnhancedStrategy(
|
||||
strategyId,
|
||||
{
|
||||
confirmed: true,
|
||||
confirmed_at: new Date().toISOString(),
|
||||
review_completed: true,
|
||||
review_completed_at: new Date().toISOString()
|
||||
}
|
||||
);
|
||||
console.log('Strategy confirmation saved to backend');
|
||||
} catch (updateError) {
|
||||
console.warn('Could not save confirmation to backend:', updateError);
|
||||
}
|
||||
|
||||
// 2. Show success message
|
||||
alert('Strategy confirmed and activated! You can now proceed to create your content calendar.');
|
||||
|
||||
// 3. Navigate to content calendar creation
|
||||
// You can add navigation logic here
|
||||
// navigate('/content-calendar');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error confirming and activating strategy:', error);
|
||||
alert('Error confirming strategy. Please try again.');
|
||||
}
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
background: isAllReviewed()
|
||||
? 'linear-gradient(135deg, #4caf50 0%, #66bb6a 100%)'
|
||||
: 'rgba(255, 255, 255, 0.1)',
|
||||
color: isAllReviewed() ? 'white' : 'rgba(255, 255, 255, 0.5)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.7rem',
|
||||
px: 2,
|
||||
py: 0.5,
|
||||
borderRadius: 2,
|
||||
boxShadow: isAllReviewed()
|
||||
? '0 2px 8px rgba(76, 175, 80, 0.3)'
|
||||
: 'none',
|
||||
border: isAllReviewed()
|
||||
? '1px solid rgba(76, 175, 80, 0.4)'
|
||||
: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
textTransform: 'none',
|
||||
minWidth: 'auto',
|
||||
'&:hover': {
|
||||
background: isAllReviewed()
|
||||
? 'linear-gradient(135deg, #66bb6a 0%, #81c784 100%)'
|
||||
: 'rgba(255, 255, 255, 0.1)',
|
||||
boxShadow: isAllReviewed()
|
||||
? '0 4px 12px rgba(76, 175, 80, 0.4)'
|
||||
: 'none',
|
||||
transform: isAllReviewed() ? 'translateY(-1px)' : 'none'
|
||||
},
|
||||
'&:active': {
|
||||
transform: isAllReviewed() ? 'translateY(0)' : 'none'
|
||||
},
|
||||
'&:disabled': {
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
color: 'rgba(255, 255, 255, 0.3)',
|
||||
boxShadow: 'none',
|
||||
transform: 'none'
|
||||
},
|
||||
'& .MuiButton-startIcon': {
|
||||
marginRight: 0.5
|
||||
}
|
||||
}}
|
||||
>
|
||||
Confirm & Activate Strategy
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 0.75, flexWrap: 'wrap' }}>
|
||||
{components.map((component) => (
|
||||
<Tooltip
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
{components.map((component: StrategyComponent) => (
|
||||
<Tooltip
|
||||
key={component.id}
|
||||
title={`${component.title}: ${component.status === 'reviewed' ? 'Reviewed' : 'Pending Review'}`}
|
||||
arrow
|
||||
@@ -433,7 +300,7 @@ const ReviewProgressHeader: React.FC = () => {
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Chip
|
||||
icon={<CheckCircleIcon />}
|
||||
label="Ready for Calendar Creation"
|
||||
label={isActivated() ? "Strategy Active & Monitored" : "Ready for Calendar Creation"}
|
||||
size="small"
|
||||
sx={{
|
||||
background: ANALYSIS_CARD_STYLES.colors.success,
|
||||
@@ -466,7 +333,7 @@ const ReviewProgressHeader: React.FC = () => {
|
||||
mt: 1.5,
|
||||
p: 1.5,
|
||||
borderRadius: 1,
|
||||
background: 'rgba(76, 175, 80, 0.1)',
|
||||
background: isActivated() ? 'rgba(76, 175, 80, 0.15)' : 'rgba(76, 175, 80, 0.1)',
|
||||
border: '1px solid rgba(76, 175, 80, 0.2)',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
@@ -475,11 +342,88 @@ const ReviewProgressHeader: React.FC = () => {
|
||||
fontWeight: 600,
|
||||
fontSize: '0.8rem'
|
||||
}}>
|
||||
🎉 All strategy components have been reviewed! You can now proceed to create your content calendar.
|
||||
{isActivated()
|
||||
? '🎉 Your content strategy is now active and being monitored! AI-powered insights and performance tracking are now live.'
|
||||
: '🎉 All strategy components have been reviewed! You can now proceed to create your content calendar.'
|
||||
}
|
||||
</Typography>
|
||||
</Box>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Enhanced Strategy Activation Button - Only shown when all components are reviewed and not yet activated */}
|
||||
{isAllReviewed() && !isActivated() && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
>
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'center' }}>
|
||||
<EnhancedStrategyActivationButton
|
||||
strategyData={buttonStrategyData}
|
||||
strategyConfirmed={false}
|
||||
onConfirmStrategy={handleConfirmStrategy}
|
||||
onGenerateContentCalendar={handleGenerateContentCalendar}
|
||||
disabled={false}
|
||||
/>
|
||||
</Box>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Strategy Activated Success Message */}
|
||||
{isActivated() && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
>
|
||||
<Box sx={{
|
||||
mt: 3,
|
||||
p: 3,
|
||||
borderRadius: 2,
|
||||
background: 'linear-gradient(135deg, rgba(76, 175, 80, 0.1) 0%, rgba(76, 175, 80, 0.05) 100%)',
|
||||
border: '2px solid rgba(76, 175, 80, 0.3)',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<Typography variant="h6" sx={{
|
||||
color: ANALYSIS_CARD_STYLES.colors.success,
|
||||
fontWeight: 700,
|
||||
mb: 1
|
||||
}}>
|
||||
🚀 Strategy Successfully Activated!
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{
|
||||
color: 'text.secondary',
|
||||
mb: 2
|
||||
}}>
|
||||
Your content strategy is now live and being monitored with AI-powered analytics.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={handleGenerateContentCalendar}
|
||||
startIcon={<AutoAwesomeIcon />}
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #4caf50 0%, #45a049 100%)',
|
||||
borderRadius: 3,
|
||||
px: 4,
|
||||
py: 1.5,
|
||||
fontWeight: 600,
|
||||
textTransform: 'none',
|
||||
boxShadow: '0 8px 32px rgba(76, 175, 80, 0.3)',
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(135deg, #45a049 0%, #3d8b40 100%)',
|
||||
boxShadow: '0 12px 40px rgba(76, 175, 80, 0.4)',
|
||||
transform: 'translateY(-2px)'
|
||||
},
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
Generate Content Calendar
|
||||
</Button>
|
||||
</Box>
|
||||
</motion.div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
Schedule as ScheduleIcon,
|
||||
Warning as WarningIcon,
|
||||
Edit as EditIcon,
|
||||
Undo as UndoIcon
|
||||
Undo as UndoIcon,
|
||||
PlayArrow as PlayArrowIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ReviewStatus } from '../../../../../stores/strategyReviewStore';
|
||||
@@ -35,8 +36,19 @@ const ReviewStatusIndicator: React.FC<ReviewStatusIndicatorProps> = ({
|
||||
onResetReview,
|
||||
isReviewing = false
|
||||
}) => {
|
||||
// Debug logging for status
|
||||
console.log('🔧 ReviewStatusIndicator received status:', status);
|
||||
const getStatusConfig = () => {
|
||||
switch (status) {
|
||||
case 'activated':
|
||||
return {
|
||||
icon: <PlayArrowIcon />,
|
||||
label: 'Activated',
|
||||
color: ANALYSIS_CARD_STYLES.colors.primary,
|
||||
bgColor: 'rgba(102, 126, 234, 0.1)',
|
||||
borderColor: 'rgba(102, 126, 234, 0.3)',
|
||||
textColor: ANALYSIS_CARD_STYLES.colors.primary
|
||||
};
|
||||
case 'reviewed':
|
||||
return {
|
||||
icon: <CheckCircleIcon />,
|
||||
@@ -101,6 +113,7 @@ const ReviewStatusIndicator: React.FC<ReviewStatusIndicatorProps> = ({
|
||||
} else if (status === 'reviewed' && onResetReview) {
|
||||
onResetReview();
|
||||
}
|
||||
// Activated status is read-only, no action needed
|
||||
}}
|
||||
>
|
||||
{/* Status Icon */}
|
||||
@@ -126,7 +139,7 @@ const ReviewStatusIndicator: React.FC<ReviewStatusIndicatorProps> = ({
|
||||
</Typography>
|
||||
|
||||
{/* Review Date */}
|
||||
{status === 'reviewed' && reviewedAt && (
|
||||
{(status === 'reviewed' || status === 'activated') && reviewedAt && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
@@ -141,7 +154,10 @@ const ReviewStatusIndicator: React.FC<ReviewStatusIndicatorProps> = ({
|
||||
|
||||
{/* Action Buttons - Enhanced for better UX */}
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||
{status === 'not_reviewed' && onStartReview && (
|
||||
{/* Only show action buttons for non-activated states */}
|
||||
{status !== 'activated' && (
|
||||
<>
|
||||
{status === 'not_reviewed' && onStartReview && (
|
||||
<Tooltip title="Review Component">
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -189,51 +205,53 @@ const ReviewStatusIndicator: React.FC<ReviewStatusIndicatorProps> = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{status === 'reviewed' && onResetReview && (
|
||||
<Tooltip title="Reset Review">
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onResetReview();
|
||||
}}
|
||||
disabled={isReviewing}
|
||||
startIcon={<UndoIcon />}
|
||||
sx={{
|
||||
color: ANALYSIS_CARD_STYLES.colors.warning,
|
||||
borderColor: 'rgba(255, 152, 0, 0.5)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.7rem',
|
||||
px: 1.5,
|
||||
py: 0.5,
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
minWidth: 'auto',
|
||||
background: 'rgba(255, 152, 0, 0.05)',
|
||||
'&:hover': {
|
||||
background: 'rgba(255, 152, 0, 0.1)',
|
||||
borderColor: 'rgba(255, 152, 0, 0.7)',
|
||||
transform: 'translateY(-1px)',
|
||||
boxShadow: '0 2px 8px rgba(255, 152, 0, 0.2)'
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'translateY(0)'
|
||||
},
|
||||
'&:disabled': {
|
||||
color: 'rgba(255, 152, 0, 0.4)',
|
||||
borderColor: 'rgba(255, 152, 0, 0.2)',
|
||||
background: 'rgba(255, 152, 0, 0.02)',
|
||||
transform: 'none'
|
||||
},
|
||||
'& .MuiButton-startIcon': {
|
||||
marginRight: 0.5
|
||||
}
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{status === 'reviewed' && onResetReview && (
|
||||
<Tooltip title="Reset Review">
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onResetReview();
|
||||
}}
|
||||
disabled={isReviewing}
|
||||
startIcon={<UndoIcon />}
|
||||
sx={{
|
||||
color: ANALYSIS_CARD_STYLES.colors.warning,
|
||||
borderColor: 'rgba(255, 152, 0, 0.5)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.7rem',
|
||||
px: 1.5,
|
||||
py: 0.5,
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
minWidth: 'auto',
|
||||
background: 'rgba(255, 152, 0, 0.05)',
|
||||
'&:hover': {
|
||||
background: 'rgba(255, 152, 0, 0.1)',
|
||||
borderColor: 'rgba(255, 152, 0, 0.7)',
|
||||
transform: 'translateY(-1px)',
|
||||
boxShadow: '0 2px 8px rgba(255, 152, 0, 0.2)'
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'translateY(0)'
|
||||
},
|
||||
'&:disabled': {
|
||||
color: 'rgba(255, 152, 0, 0.4)',
|
||||
borderColor: 'rgba(255, 152, 0, 0.2)',
|
||||
background: 'rgba(255, 152, 0, 0.02)',
|
||||
transform: 'none'
|
||||
},
|
||||
'& .MuiButton-startIcon': {
|
||||
marginRight: 0.5
|
||||
}
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import { motion } from 'framer-motion';
|
||||
import { StrategyData } from '../types/strategy.types';
|
||||
import EnhancedStrategyActivationButton from './EnhancedStrategyActivationButton';
|
||||
|
||||
interface StrategyActionsProps {
|
||||
strategyData: StrategyData | null;
|
||||
@@ -27,6 +28,16 @@ const StrategyActions: React.FC<StrategyActionsProps> = ({
|
||||
onGenerateContentCalendar,
|
||||
onRefreshData
|
||||
}) => {
|
||||
// Convert onConfirmStrategy to async function for the enhanced button
|
||||
const handleConfirmStrategy = async () => {
|
||||
try {
|
||||
await onConfirmStrategy();
|
||||
} catch (error) {
|
||||
console.error('Strategy confirmation failed:', error);
|
||||
throw error; // Re-throw to let the enhanced button handle the error
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 4 }}>
|
||||
{/* Strategy Confirmation Status */}
|
||||
@@ -68,65 +79,19 @@ const StrategyActions: React.FC<StrategyActionsProps> = ({
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Enhanced Strategy Activation Button */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<EnhancedStrategyActivationButton
|
||||
strategyData={strategyData}
|
||||
strategyConfirmed={strategyConfirmed}
|
||||
onConfirmStrategy={handleConfirmStrategy}
|
||||
onGenerateContentCalendar={onGenerateContentCalendar}
|
||||
disabled={false}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center' }}>
|
||||
{!strategyConfirmed ? (
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={onConfirmStrategy}
|
||||
startIcon={<CheckIcon />}
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: 3,
|
||||
px: 4,
|
||||
py: 1.5,
|
||||
fontWeight: 600,
|
||||
boxShadow: '0 8px 32px rgba(102, 126, 234, 0.3)',
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
|
||||
boxShadow: '0 12px 40px rgba(102, 126, 234, 0.4)',
|
||||
transform: 'translateY(-2px)'
|
||||
},
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
|
||||
}}
|
||||
>
|
||||
Confirm Strategy
|
||||
</Button>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={onGenerateContentCalendar}
|
||||
startIcon={<CalendarIcon />}
|
||||
color="success"
|
||||
sx={{
|
||||
borderRadius: 3,
|
||||
px: 4,
|
||||
py: 1.5,
|
||||
fontWeight: 600,
|
||||
boxShadow: '0 8px 32px rgba(76, 175, 80, 0.3)',
|
||||
'&:hover': {
|
||||
boxShadow: '0 12px 40px rgba(76, 175, 80, 0.4)',
|
||||
transform: 'translateY(-2px)'
|
||||
},
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
|
||||
}}
|
||||
>
|
||||
Generate Content Calendar
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Grid,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Alert,
|
||||
AlertTitle
|
||||
} from '@mui/material';
|
||||
import {
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
Timeline as TimelineIcon,
|
||||
CalendarToday as CalendarIcon,
|
||||
Refresh as RefreshIcon,
|
||||
AutoAwesome as AutoAwesomeIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion } from 'framer-motion';
|
||||
import { strategyMonitoringApi } from '../../../../../services/strategyMonitoringApi';
|
||||
|
||||
interface TrendData {
|
||||
date: string;
|
||||
traffic_growth: number;
|
||||
engagement_rate: number;
|
||||
conversion_rate: number;
|
||||
content_quality_score: number;
|
||||
strategy_adoption_rate: number;
|
||||
}
|
||||
|
||||
interface TrendAnalysisProps {
|
||||
strategyId: number;
|
||||
strategyData?: any;
|
||||
}
|
||||
|
||||
const TrendAnalysis: React.FC<TrendAnalysisProps> = ({
|
||||
strategyId,
|
||||
strategyData
|
||||
}) => {
|
||||
const [selectedMetric, setSelectedMetric] = useState<string>('traffic_growth');
|
||||
const [timeRange, setTimeRange] = useState<string>('30d');
|
||||
const [trendData, setTrendData] = useState<TrendData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadTrendData();
|
||||
}, [strategyId, timeRange]);
|
||||
|
||||
const loadTrendData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Call the API to get trend data
|
||||
const response = await strategyMonitoringApi.getTrendData(strategyId, timeRange);
|
||||
setTrendData(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error loading trend data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getMetricInfo = (metric: string) => {
|
||||
const metricInfo = {
|
||||
traffic_growth: { label: 'Traffic Growth', unit: '%', color: '#4caf50', icon: <TrendingUpIcon /> },
|
||||
engagement_rate: { label: 'Engagement Rate', unit: '%', color: '#2196f3', icon: <TimelineIcon /> },
|
||||
conversion_rate: { label: 'Conversion Rate', unit: '%', color: '#ff9800', icon: <AutoAwesomeIcon /> },
|
||||
content_quality_score: { label: 'Content Quality Score', unit: '/100', color: '#9c27b0', icon: <AutoAwesomeIcon /> },
|
||||
strategy_adoption_rate: { label: 'Strategy Adoption Rate', unit: '%', color: '#4caf50', icon: <TrendingUpIcon /> }
|
||||
};
|
||||
return metricInfo[metric as keyof typeof metricInfo] || metricInfo.traffic_growth;
|
||||
};
|
||||
|
||||
const calculateTrend = (data: TrendData[], metric: string) => {
|
||||
if (data.length < 2) return { direction: 'stable', percentage: 0 };
|
||||
|
||||
const values = data.map(d => d[metric as keyof TrendData] as number);
|
||||
const firstValue = values[0];
|
||||
const lastValue = values[values.length - 1];
|
||||
const change = ((lastValue - firstValue) / firstValue) * 100;
|
||||
|
||||
return {
|
||||
direction: change > 0 ? 'up' : change < 0 ? 'down' : 'stable',
|
||||
percentage: Math.abs(change)
|
||||
};
|
||||
};
|
||||
|
||||
const renderTrendChart = (data: TrendData[], metric: string) => {
|
||||
const metricInfo = getMetricInfo(metric);
|
||||
const values = data.map(d => d[metric as keyof TrendData] as number);
|
||||
const maxValue = Math.max(...values);
|
||||
const minValue = Math.min(...values);
|
||||
const range = maxValue - minValue;
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 2, height: 200, position: 'relative' }}>
|
||||
<svg width="100%" height="100%" viewBox="0 0 400 200">
|
||||
{/* Grid lines */}
|
||||
{[0, 25, 50, 75, 100].map(y => (
|
||||
<line
|
||||
key={y}
|
||||
x1="0"
|
||||
y1={200 - (y * 200 / 100)}
|
||||
x2="400"
|
||||
y2={200 - (y * 200 / 100)}
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Trend line */}
|
||||
<polyline
|
||||
points={data.map((d, i) => {
|
||||
const value = d[metric as keyof TrendData] as number;
|
||||
const x = (i / (data.length - 1)) * 400;
|
||||
const y = 200 - ((value - minValue) / range) * 180;
|
||||
return `${x},${y}`;
|
||||
}).join(' ')}
|
||||
fill="none"
|
||||
stroke={metricInfo.color}
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
|
||||
{/* Data points */}
|
||||
{data.map((d, i) => {
|
||||
const value = d[metric as keyof TrendData] as number;
|
||||
const x = (i / (data.length - 1)) * 400;
|
||||
const y = 200 - ((value - minValue) / range) * 180;
|
||||
return (
|
||||
<circle
|
||||
key={i}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r="4"
|
||||
fill={metricInfo.color}
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
|
||||
{/* Value labels */}
|
||||
<Box sx={{ position: 'absolute', top: 0, right: 0, textAlign: 'right' }}>
|
||||
<Typography variant="h6" sx={{ color: metricInfo.color, fontWeight: 700 }}>
|
||||
{values[values.length - 1]}{metricInfo.unit}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
|
||||
Current
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const trend = calculateTrend(trendData, selectedMetric);
|
||||
const metricInfo = getMetricInfo(selectedMetric);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" minHeight={400}>
|
||||
<Typography>Loading trend analysis...</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<Card sx={{
|
||||
background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a2e 25%, #16213e 50%, #0f3460 75%, #533483 100%)',
|
||||
color: 'white',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%)',
|
||||
pointerEvents: 'none'
|
||||
}
|
||||
}}>
|
||||
<CardContent sx={{ position: 'relative', zIndex: 1, p: 3 }}>
|
||||
{/* Header */}
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{
|
||||
fontWeight: 700,
|
||||
background: 'linear-gradient(45deg, #667eea, #764ba2)',
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
mb: 1
|
||||
}}>
|
||||
📈 Performance Trend Analysis
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)' }}>
|
||||
Track your strategy performance over time with AI-powered insights
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Tooltip title="Refresh data">
|
||||
<IconButton onClick={loadTrendData} sx={{ color: 'white' }}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* Controls */}
|
||||
<Grid container spacing={2} sx={{ mb: 3 }}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<FormControl fullWidth sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
color: 'white',
|
||||
'& fieldset': { borderColor: 'rgba(255,255,255,0.3)' },
|
||||
'&:hover fieldset': { borderColor: 'rgba(255,255,255,0.5)' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#667eea' }
|
||||
},
|
||||
'& .MuiInputLabel-root': { color: 'rgba(255,255,255,0.7)' },
|
||||
'& .MuiSelect-icon': { color: 'white' }
|
||||
}}>
|
||||
<InputLabel>Metric</InputLabel>
|
||||
<Select
|
||||
value={selectedMetric}
|
||||
onChange={(e) => setSelectedMetric(e.target.value)}
|
||||
label="Metric"
|
||||
>
|
||||
<MenuItem value="traffic_growth">Traffic Growth</MenuItem>
|
||||
<MenuItem value="engagement_rate">Engagement Rate</MenuItem>
|
||||
<MenuItem value="conversion_rate">Conversion Rate</MenuItem>
|
||||
<MenuItem value="content_quality_score">Content Quality Score</MenuItem>
|
||||
<MenuItem value="strategy_adoption_rate">Strategy Adoption Rate</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<FormControl fullWidth sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
color: 'white',
|
||||
'& fieldset': { borderColor: 'rgba(255,255,255,0.3)' },
|
||||
'&:hover fieldset': { borderColor: 'rgba(255,255,255,0.5)' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#667eea' }
|
||||
},
|
||||
'& .MuiInputLabel-root': { color: 'rgba(255,255,255,0.7)' },
|
||||
'& .MuiSelect-icon': { color: 'white' }
|
||||
}}>
|
||||
<InputLabel>Time Range</InputLabel>
|
||||
<Select
|
||||
value={timeRange}
|
||||
onChange={(e) => setTimeRange(e.target.value)}
|
||||
label="Time Range"
|
||||
>
|
||||
<MenuItem value="7d">Last 7 days</MenuItem>
|
||||
<MenuItem value="30d">Last 30 days</MenuItem>
|
||||
<MenuItem value="90d">Last 90 days</MenuItem>
|
||||
<MenuItem value="1y">Last year</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Trend Summary */}
|
||||
<Box sx={{ mb: 3, p: 2, background: 'rgba(255,255,255,0.1)', borderRadius: 2 }}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
{metricInfo.icon}
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
{metricInfo.label} Trend
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`${trend.direction === 'up' ? '+' : trend.direction === 'down' ? '-' : ''}${trend.percentage.toFixed(1)}%`}
|
||||
size="small"
|
||||
sx={{
|
||||
background: trend.direction === 'up' ? '#4caf50' : trend.direction === 'down' ? '#f44336' : '#ff9800',
|
||||
color: 'white',
|
||||
fontWeight: 600
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)' }}>
|
||||
{trend.direction === 'up'
|
||||
? `Your ${metricInfo.label.toLowerCase()} has improved by ${trend.percentage.toFixed(1)}% over this period.`
|
||||
: trend.direction === 'down'
|
||||
? `Your ${metricInfo.label.toLowerCase()} has decreased by ${trend.percentage.toFixed(1)}% over this period.`
|
||||
: `Your ${metricInfo.label.toLowerCase()} has remained stable over this period.`
|
||||
}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Chart */}
|
||||
<Box sx={{
|
||||
p: 3,
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: 2,
|
||||
border: '1px solid rgba(255,255,255,0.1)'
|
||||
}}>
|
||||
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
|
||||
{metricInfo.label} Over Time
|
||||
</Typography>
|
||||
{renderTrendChart(trendData, selectedMetric)}
|
||||
</Box>
|
||||
|
||||
{/* Data Points */}
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
|
||||
Recent Data Points
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{trendData.slice(-4).map((data, index) => (
|
||||
<Grid item xs={12} sm={6} md={3} key={index}>
|
||||
<Box sx={{
|
||||
p: 2,
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
borderRadius: 1,
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)', mb: 1 }}>
|
||||
{new Date(data.date).toLocaleDateString()}
|
||||
</Typography>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, color: metricInfo.color }}>
|
||||
{(data[selectedMetric as keyof TrendData] as number).toFixed(1)}{metricInfo.unit}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrendAnalysis;
|
||||
@@ -1,20 +1,28 @@
|
||||
import { useState } from 'react';
|
||||
import { contentPlanningApi } from '../../../../../services/contentPlanningApi';
|
||||
import { StrategyData } from '../types/strategy.types';
|
||||
import { useStrategyReviewStore } from '../../../../../stores/strategyReviewStore';
|
||||
|
||||
export const useStrategyActions = () => {
|
||||
const [strategyConfirmed, setStrategyConfirmed] = useState(false);
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
const [isActivating, setIsActivating] = useState(false);
|
||||
|
||||
// Get the activateStrategy method from the review store
|
||||
const { activateStrategy } = useStrategyReviewStore();
|
||||
|
||||
const handleConfirmStrategy = () => {
|
||||
setShowConfirmDialog(true);
|
||||
};
|
||||
|
||||
const confirmStrategy = async (strategyData: StrategyData | null) => {
|
||||
const confirmStrategy = async (strategyData: StrategyData | null): Promise<void> => {
|
||||
if (isActivating) {
|
||||
throw new Error('Strategy activation already in progress');
|
||||
}
|
||||
|
||||
setIsActivating(true);
|
||||
|
||||
try {
|
||||
setStrategyConfirmed(true);
|
||||
setShowConfirmDialog(false);
|
||||
|
||||
// Save confirmation status to backend
|
||||
const userId = strategyData?.strategy_metadata?.user_id || strategyData?.metadata?.user_id;
|
||||
if (userId) {
|
||||
@@ -31,10 +39,20 @@ export const useStrategyActions = () => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Strategy confirmed! Ready to generate content calendar.');
|
||||
// Set local state
|
||||
setStrategyConfirmed(true);
|
||||
setShowConfirmDialog(false);
|
||||
|
||||
// Activate strategy in the review store
|
||||
activateStrategy();
|
||||
|
||||
console.log('Strategy confirmed and activated! Ready to generate content calendar.');
|
||||
} catch (error) {
|
||||
console.error('Error confirming strategy:', error);
|
||||
setStrategyConfirmed(false);
|
||||
throw error; // Re-throw to let the calling component handle the error
|
||||
} finally {
|
||||
setIsActivating(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -46,30 +64,22 @@ export const useStrategyActions = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate content calendar based on confirmed strategy
|
||||
const calendarRequest = {
|
||||
user_id: userId,
|
||||
strategy_id: userId, // Using user_id as strategy_id for now
|
||||
calendar_type: 'comprehensive',
|
||||
industry: strategyData.base_strategy?.industry || 'technology',
|
||||
business_size: 'medium', // TODO: Get from strategy data
|
||||
force_refresh: false
|
||||
};
|
||||
console.log('🎯 Strategy Actions: Generating content calendar for strategy:', strategyData);
|
||||
|
||||
console.log('Generating content calendar with request:', calendarRequest);
|
||||
// For now, we'll just log that the function was called
|
||||
// The actual navigation is handled by the ReviewProgressHeader component
|
||||
// which uses the NavigationOrchestrator to navigate to the calendar wizard
|
||||
|
||||
// Call the calendar generation API
|
||||
const calendarResponse = await contentPlanningApi.generateCalendar(calendarRequest);
|
||||
console.log('🎯 Strategy Actions: Calendar generation request prepared');
|
||||
|
||||
console.log('Content calendar generated successfully:', calendarResponse);
|
||||
|
||||
// TODO: Navigate to calendar tab or show success message
|
||||
// You could also store the calendar data in a global state
|
||||
// TODO: In the future, this could be enhanced to:
|
||||
// 1. Call the calendar generation API directly
|
||||
// 2. Store the generated calendar data
|
||||
// 3. Navigate to the calendar view with the generated data
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating content calendar:', error);
|
||||
// Show error message to user
|
||||
throw new Error('Failed to generate content calendar. Please try again.');
|
||||
console.error('Error in handleGenerateContentCalendar:', error);
|
||||
throw new Error('Failed to prepare calendar generation. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,6 +87,7 @@ export const useStrategyActions = () => {
|
||||
strategyConfirmed,
|
||||
showConfirmDialog,
|
||||
setShowConfirmDialog,
|
||||
isActivating,
|
||||
handleConfirmStrategy,
|
||||
confirmStrategy,
|
||||
handleGenerateContentCalendar
|
||||
|
||||
@@ -10,21 +10,52 @@ import {
|
||||
Divider,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
LinearProgress
|
||||
LinearProgress,
|
||||
Tabs,
|
||||
Tab
|
||||
} from '@mui/material';
|
||||
import {
|
||||
TrendingUp as TrendingUpIcon,
|
||||
Analytics as AnalyticsIcon,
|
||||
ShowChart as ShowChartIcon,
|
||||
Assessment as AssessmentIcon
|
||||
Assessment as AssessmentIcon,
|
||||
Visibility as VisibilityIcon,
|
||||
Timeline as TimelineIcon,
|
||||
AutoAwesome as AutoAwesomeIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||
import EnhancedPerformanceVisualization from '../components/StrategyIntelligence/components/EnhancedPerformanceVisualization';
|
||||
import TrendAnalysis from '../components/StrategyIntelligence/components/TrendAnalysis';
|
||||
import DataTransparencyPanel from '../components/StrategyIntelligence/components/DataTransparencyPanel';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`analytics-tabpanel-${index}`}
|
||||
aria-labelledby={`analytics-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && <Box sx={{ pt: 3 }}>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const AnalyticsTab: React.FC = () => {
|
||||
const {
|
||||
performanceMetrics,
|
||||
aiInsights,
|
||||
currentStrategy,
|
||||
loading,
|
||||
error,
|
||||
loadAIInsights,
|
||||
@@ -33,6 +64,7 @@ const AnalyticsTab: React.FC = () => {
|
||||
|
||||
const [analyticsData, setAnalyticsData] = useState<any>(null);
|
||||
const [dataLoading, setDataLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
loadAnalyticsData();
|
||||
@@ -74,16 +106,23 @@ const AnalyticsTab: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setActiveTab(newValue);
|
||||
};
|
||||
|
||||
const getPerformanceColor = (value: number) => {
|
||||
if (value >= 80) return 'success';
|
||||
if (value >= 60) return 'warning';
|
||||
return 'error';
|
||||
};
|
||||
|
||||
// Get strategy ID for performance components
|
||||
const strategyId = Number(currentStrategy?.id) || currentStrategy?.user_id || 1;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Performance Analytics
|
||||
Analytics Dashboard
|
||||
</Typography>
|
||||
|
||||
{error && (
|
||||
@@ -92,297 +131,351 @@ const AnalyticsTab: React.FC = () => {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{dataLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
|
||||
<CircularProgress />
|
||||
{/* Tabs Navigation */}
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
sx={{
|
||||
'& .MuiTab-root': {
|
||||
color: 'text.secondary',
|
||||
fontWeight: 600,
|
||||
textTransform: 'none',
|
||||
fontSize: '1rem',
|
||||
minHeight: 48,
|
||||
'&.Mui-selected': {
|
||||
color: 'primary.main',
|
||||
fontWeight: 700
|
||||
}
|
||||
},
|
||||
'& .MuiTabs-indicator': {
|
||||
height: 3,
|
||||
borderRadius: '3px 3px 0 0'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tab label="Performance Analytics" icon={<ShowChartIcon />} iconPosition="start" />
|
||||
<Tab label="Content Analytics" icon={<AnalyticsIcon />} iconPosition="start" />
|
||||
<Tab label="Data Transparency" icon={<VisibilityIcon />} iconPosition="start" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{/* Performance Analytics Tab */}
|
||||
<TabPanel value={activeTab} index={0}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<EnhancedPerformanceVisualization
|
||||
strategyId={strategyId}
|
||||
strategyData={currentStrategy}
|
||||
/>
|
||||
<TrendAnalysis
|
||||
strategyId={strategyId}
|
||||
strategyData={currentStrategy}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Grid container spacing={3}>
|
||||
{/* Performance Overview */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Performance Overview
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{performanceMetrics ? (
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Engagement Rate
|
||||
</Typography>
|
||||
<Typography variant="h4" color={getPerformanceColor(performanceMetrics.engagement)}>
|
||||
{performanceMetrics.engagement}%
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Reach
|
||||
</Typography>
|
||||
<Typography variant="h4" color="primary">
|
||||
{performanceMetrics.reach.toLocaleString()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Conversion Rate
|
||||
</Typography>
|
||||
<Typography variant="h4" color={getPerformanceColor(performanceMetrics.conversion)}>
|
||||
{performanceMetrics.conversion}%
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
ROI
|
||||
</Typography>
|
||||
<Typography variant="h4" color="success.main">
|
||||
${performanceMetrics.roi.toLocaleString()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No performance data available
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
|
||||
{/* AI Insights */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<AssessmentIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
AI Insights
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{aiInsights && aiInsights.length > 0 ? (
|
||||
<Box>
|
||||
{aiInsights.slice(0, 3).map((insight, index) => (
|
||||
<Box key={index} sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
{insight.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
{insight.description}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={insight.priority}
|
||||
color={insight.priority === 'high' ? 'error' : insight.priority === 'medium' ? 'warning' : 'success'}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No AI insights available
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{/* Content Evolution */}
|
||||
{analyticsData && analyticsData.content_evolution && (
|
||||
{/* Content Analytics Tab */}
|
||||
<TabPanel value={activeTab} index={1}>
|
||||
{dataLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : (
|
||||
<Grid container spacing={3}>
|
||||
{/* Performance Overview */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<ShowChartIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Content Evolution
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{analyticsData.content_evolution.content_types ? (
|
||||
<Box>
|
||||
{analyticsData.content_evolution.content_types.map((contentType: string, index: number) => {
|
||||
const performance = analyticsData.content_evolution.performance_by_type?.[contentType];
|
||||
return (
|
||||
<Box key={index} sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle1" sx={{ textTransform: 'capitalize' }}>
|
||||
{contentType.replace('_', ' ')}
|
||||
</Typography>
|
||||
{performance && (
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Growth
|
||||
</Typography>
|
||||
<Typography variant="h6" color="success.main">
|
||||
+{performance.growth}%
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Engagement
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{performance.engagement}%
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No content evolution data available
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Performance Trends */}
|
||||
{analyticsData && analyticsData.performance_trends && (
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<TrendingUpIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Performance Trends
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{analyticsData.performance_trends.engagement_trend ? (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Engagement Trend (Last 5 periods)
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
{analyticsData.performance_trends.engagement_trend.map((value: number, index: number) => (
|
||||
<Box key={index} sx={{ flex: 1, textAlign: 'center' }}>
|
||||
<Typography variant="h6" color="primary">
|
||||
{value}%
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Period {index + 1}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No trend data available
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Engagement Patterns */}
|
||||
{analyticsData && analyticsData.engagement_patterns && (
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Engagement Patterns
|
||||
Performance Overview
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{analyticsData.engagement_patterns.peak_times && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Peak Engagement Times
|
||||
{performanceMetrics ? (
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Engagement Rate
|
||||
</Typography>
|
||||
{analyticsData.engagement_patterns.peak_times.map((time: string, index: number) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={time}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
sx={{ mr: 1, mb: 1 }}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{analyticsData.engagement_patterns.best_days && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Best Performing Days
|
||||
<Typography variant="h4" color={getPerformanceColor(performanceMetrics.engagement)}>
|
||||
{performanceMetrics.engagement}%
|
||||
</Typography>
|
||||
{analyticsData.engagement_patterns.best_days.map((day: string, index: number) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={day}
|
||||
color="success"
|
||||
variant="outlined"
|
||||
sx={{ mr: 1, mb: 1 }}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{analyticsData.engagement_patterns.audience_segments && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Top Audience Segments
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Reach
|
||||
</Typography>
|
||||
<Typography variant="h4" color="primary">
|
||||
{performanceMetrics.reach.toLocaleString()}
|
||||
</Typography>
|
||||
{analyticsData.engagement_patterns.audience_segments.map((segment: string, index: number) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={segment.replace('_', ' ')}
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
sx={{ mr: 1, mb: 1 }}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Conversion Rate
|
||||
</Typography>
|
||||
<Typography variant="h4" color={getPerformanceColor(performanceMetrics.conversion)}>
|
||||
{performanceMetrics.conversion}%
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
ROI
|
||||
</Typography>
|
||||
<Typography variant="h4" color="success.main">
|
||||
${performanceMetrics.roi.toLocaleString()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No performance data available
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Recommendations */}
|
||||
{analyticsData && analyticsData.recommendations && analyticsData.recommendations.length > 0 && (
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
{/* AI Insights */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<AssessmentIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
AI Recommendations
|
||||
AI Insights
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{analyticsData.recommendations.map((recommendation: any, index: number) => (
|
||||
<Grid item xs={12} md={6} key={index}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{recommendation.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{recommendation.description}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Chip
|
||||
label={recommendation.type}
|
||||
color="primary"
|
||||
size="small"
|
||||
/>
|
||||
<Chip
|
||||
label={`${(recommendation.confidence * 100).toFixed(0)}% confidence`}
|
||||
color="success"
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
{aiInsights && aiInsights.length > 0 ? (
|
||||
<Box>
|
||||
{aiInsights.slice(0, 3).map((insight, index) => (
|
||||
<Box key={index} sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
{insight.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
{insight.description}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={insight.priority}
|
||||
color={insight.priority === 'high' ? 'error' : insight.priority === 'medium' ? 'warning' : 'success'}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No AI insights available
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Content Evolution */}
|
||||
{analyticsData && analyticsData.content_evolution && (
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<ShowChartIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Content Evolution
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{analyticsData.content_evolution.content_types ? (
|
||||
<Box>
|
||||
{analyticsData.content_evolution.content_types.map((contentType: string, index: number) => {
|
||||
const performance = analyticsData.content_evolution.performance_by_type?.[contentType];
|
||||
return (
|
||||
<Box key={index} sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle1" sx={{ textTransform: 'capitalize' }}>
|
||||
{contentType.replace('_', ' ')}
|
||||
</Typography>
|
||||
{performance && (
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Growth
|
||||
</Typography>
|
||||
<Typography variant="h6" color="success.main">
|
||||
+{performance.growth}%
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Engagement
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{performance.engagement}%
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No content evolution data available
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Performance Trends */}
|
||||
{analyticsData && analyticsData.performance_trends && (
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<TrendingUpIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Performance Trends
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{analyticsData.performance_trends.engagement_trend ? (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Engagement Trend (Last 5 periods)
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
{analyticsData.performance_trends.engagement_trend.map((value: number, index: number) => (
|
||||
<Box key={index} sx={{ flex: 1, textAlign: 'center' }}>
|
||||
<Typography variant="h6" color="primary">
|
||||
{value}%
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Period {index + 1}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No trend data available
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Engagement Patterns */}
|
||||
{analyticsData && analyticsData.engagement_patterns && (
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Engagement Patterns
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{analyticsData.engagement_patterns.peak_times && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Peak Engagement Times
|
||||
</Typography>
|
||||
{analyticsData.engagement_patterns.peak_times.map((time: string, index: number) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={time}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
sx={{ mr: 1, mb: 1 }}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{analyticsData.engagement_patterns.best_days && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Best Performing Days
|
||||
</Typography>
|
||||
{analyticsData.engagement_patterns.best_days.map((day: string, index: number) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={day}
|
||||
color="success"
|
||||
variant="outlined"
|
||||
sx={{ mr: 1, mb: 1 }}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{analyticsData.engagement_patterns.audience_segments && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Top Audience Segments
|
||||
</Typography>
|
||||
{analyticsData.engagement_patterns.audience_segments.map((segment: string, index: number) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={segment.replace('_', ' ')}
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
sx={{ mr: 1, mb: 1 }}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Recommendations */}
|
||||
{analyticsData && analyticsData.recommendations && analyticsData.recommendations.length > 0 && (
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<AssessmentIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
AI Recommendations
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{analyticsData.recommendations.map((recommendation: any, index: number) => (
|
||||
<Grid item xs={12} md={6} key={index}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{recommendation.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{recommendation.description}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Chip
|
||||
label={recommendation.type}
|
||||
color="primary"
|
||||
size="small"
|
||||
/>
|
||||
<Chip
|
||||
label={`${(recommendation.confidence * 100).toFixed(0)}% confidence`}
|
||||
color="success"
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
{/* Data Transparency Tab */}
|
||||
<TabPanel value={activeTab} index={2}>
|
||||
<DataTransparencyPanel
|
||||
strategyId={strategyId}
|
||||
strategyData={currentStrategy}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,58 +1,22 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
Paper,
|
||||
Typography,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardActions,
|
||||
Chip,
|
||||
Alert,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
LinearProgress,
|
||||
CircularProgress,
|
||||
Tabs,
|
||||
Tab,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Badge
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import {
|
||||
TrendingUp as TrendingUpIcon,
|
||||
Business as BusinessIcon,
|
||||
Lightbulb as LightbulbIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Warning as WarningIcon,
|
||||
Analytics as AnalyticsIcon,
|
||||
Timeline as TimelineIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Add as AddIcon,
|
||||
Edit as EditIcon,
|
||||
Visibility as VisibilityIcon,
|
||||
ShowChart as ShowChartIcon,
|
||||
PlayArrow as PlayArrowIcon,
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
PlayArrow as PlayArrowIcon
|
||||
Edit as EditIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||
import StrategyIntelligenceTab from '../components/StrategyIntelligence/StrategyIntelligenceTab';
|
||||
import StrategyOnboardingDialog from '../components/StrategyOnboardingDialog';
|
||||
import { StrategyData } from '../components/StrategyIntelligence/types/strategy.types';
|
||||
|
||||
const ContentStrategyTab: React.FC = () => {
|
||||
const {
|
||||
@@ -77,6 +41,9 @@ const ContentStrategyTab: React.FC = () => {
|
||||
|
||||
// Real data states
|
||||
const [strategicIntelligence, setStrategicIntelligence] = useState<any>(null);
|
||||
const [strategyData, setStrategyData] = useState<StrategyData | null>(null);
|
||||
const [strategyDataLoading, setStrategyDataLoading] = useState(false);
|
||||
const [strategyDataError, setStrategyDataError] = useState<string | null>(null);
|
||||
const [dataLoading, setDataLoading] = useState({
|
||||
strategies: false,
|
||||
insights: false,
|
||||
@@ -118,6 +85,7 @@ const ContentStrategyTab: React.FC = () => {
|
||||
if (strategiesArray.length > 0) {
|
||||
console.log('✅ Strategies found, checking status...');
|
||||
checkStrategyStatus();
|
||||
loadStrategyData();
|
||||
} else if (strategiesArray.length === 0 && hasCheckedStrategy) {
|
||||
// Only set to 'none' if we've already checked and confirmed no strategies
|
||||
console.log('❌ No strategies found, setting status to none...');
|
||||
@@ -127,6 +95,61 @@ const ContentStrategyTab: React.FC = () => {
|
||||
// If strategiesArray.length === 0 and !hasCheckedStrategy, do nothing (wait for data to load)
|
||||
}, [strategies, loadStrategies]);
|
||||
|
||||
const loadStrategyData = async () => {
|
||||
try {
|
||||
setStrategyDataLoading(true);
|
||||
setStrategyDataError(null);
|
||||
|
||||
const userId = 1; // Default user ID
|
||||
|
||||
// Try to get the latest generated strategy
|
||||
try {
|
||||
const latestStrategyResponse = await contentPlanningApi.getLatestGeneratedStrategy(userId);
|
||||
|
||||
console.log('🔍 Latest strategy response from API:', latestStrategyResponse);
|
||||
|
||||
if (latestStrategyResponse && latestStrategyResponse.strategic_insights) {
|
||||
console.log('✅ Found latest generated strategy:', latestStrategyResponse);
|
||||
setStrategyData(latestStrategyResponse);
|
||||
return;
|
||||
}
|
||||
} catch (pollingError) {
|
||||
console.log('No latest strategy found in polling system, checking database...', pollingError);
|
||||
}
|
||||
|
||||
// If no strategy found in polling system, try to get from database
|
||||
try {
|
||||
const strategiesResponse = await contentPlanningApi.getEnhancedStrategies(userId);
|
||||
|
||||
const strategies = strategiesResponse?.data?.strategies || strategiesResponse?.strategies || [];
|
||||
|
||||
if (strategies && strategies.length > 0) {
|
||||
const latestStrategy = strategies[0];
|
||||
|
||||
if (latestStrategy.comprehensive_ai_analysis) {
|
||||
console.log('✅ Found comprehensive strategy in database:', latestStrategy);
|
||||
setStrategyData(latestStrategy.comprehensive_ai_analysis);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (dbError) {
|
||||
console.log('No comprehensive strategies found in database:', dbError);
|
||||
}
|
||||
|
||||
// If no strategy data is available
|
||||
console.log('❌ No comprehensive strategy data found');
|
||||
setStrategyData(null);
|
||||
setStrategyDataError('No comprehensive strategy data available. Please generate a strategy first.');
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('Error loading strategy data:', err);
|
||||
setStrategyDataError(err.message || 'Failed to load strategy data');
|
||||
setStrategyData(null);
|
||||
} finally {
|
||||
setStrategyDataLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const checkStrategyStatus = () => {
|
||||
console.log('🔍 Checking strategy status...');
|
||||
console.log('🔍 Strategies from store:', strategies);
|
||||
@@ -178,9 +201,6 @@ const ContentStrategyTab: React.FC = () => {
|
||||
loadAIInsights(),
|
||||
loadAIRecommendations()
|
||||
]);
|
||||
|
||||
// Load strategic intelligence
|
||||
await loadStrategicIntelligence();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading initial data:', error);
|
||||
@@ -189,141 +209,13 @@ const ContentStrategyTab: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadStrategicIntelligence = async () => {
|
||||
try {
|
||||
setDataLoading(prev => ({ ...prev, strategicIntelligence: true }));
|
||||
|
||||
// Use streaming endpoint for real-time updates
|
||||
const eventSource = await contentPlanningApi.streamStrategicIntelligence(1);
|
||||
|
||||
contentPlanningApi.handleSSEData(
|
||||
eventSource,
|
||||
(data) => {
|
||||
|
||||
if (data.type === 'status') {
|
||||
// Update loading message
|
||||
} else if (data.type === 'progress') {
|
||||
// Update progress (could be used for progress bar)
|
||||
} else if (data.type === 'result' && data.status === 'success') {
|
||||
// Set the strategic intelligence data
|
||||
setStrategicIntelligence(data.data);
|
||||
setDataLoading(prev => ({ ...prev, strategicIntelligence: false }));
|
||||
} else if (data.type === 'error') {
|
||||
// Set fallback data on error
|
||||
setStrategicIntelligence({
|
||||
market_positioning: {
|
||||
score: 75,
|
||||
strengths: ['Strong brand voice', 'Consistent content quality'],
|
||||
weaknesses: ['Limited video content', 'Slow content production']
|
||||
},
|
||||
competitive_advantages: [
|
||||
{ advantage: 'AI-powered content creation', impact: 'High', implementation: 'In Progress' },
|
||||
{ advantage: 'Data-driven strategy', impact: 'Medium', implementation: 'Complete' }
|
||||
],
|
||||
strategic_risks: [
|
||||
{ risk: 'Content saturation in market', probability: 'Medium', impact: 'High' },
|
||||
{ risk: 'Algorithm changes affecting reach', probability: 'High', impact: 'Medium' }
|
||||
]
|
||||
});
|
||||
setDataLoading(prev => ({ ...prev, strategicIntelligence: false }));
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// Set fallback data on error
|
||||
setStrategicIntelligence({
|
||||
market_positioning: {
|
||||
score: 75,
|
||||
strengths: ['Strong brand voice', 'Consistent content quality'],
|
||||
weaknesses: ['Limited video content', 'Slow content production']
|
||||
},
|
||||
competitive_advantages: [
|
||||
{ advantage: 'AI-powered content creation', impact: 'High', implementation: 'In Progress' },
|
||||
{ advantage: 'Data-driven strategy', impact: 'Medium', implementation: 'Complete' }
|
||||
],
|
||||
strategic_risks: [
|
||||
{ risk: 'Content saturation in market', probability: 'Medium', impact: 'High' },
|
||||
{ risk: 'Algorithm changes affecting reach', probability: 'High', impact: 'Medium' }
|
||||
]
|
||||
});
|
||||
setDataLoading(prev => ({ ...prev, strategicIntelligence: false }));
|
||||
}
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading strategic intelligence:', error);
|
||||
// Set fallback data on error
|
||||
setStrategicIntelligence({
|
||||
market_positioning: {
|
||||
score: 75,
|
||||
strengths: ['Strong brand voice', 'Consistent content quality'],
|
||||
weaknesses: ['Limited video content', 'Slow content production']
|
||||
},
|
||||
competitive_advantages: [
|
||||
{ advantage: 'AI-powered content creation', impact: 'High', implementation: 'In Progress' },
|
||||
{ advantage: 'Data-driven strategy', impact: 'Medium', implementation: 'Complete' }
|
||||
],
|
||||
strategic_risks: [
|
||||
{ risk: 'Content saturation in market', probability: 'Medium', impact: 'High' },
|
||||
{ risk: 'Algorithm changes affecting reach', probability: 'High', impact: 'Medium' }
|
||||
]
|
||||
});
|
||||
setDataLoading(prev => ({ ...prev, strategicIntelligence: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleStrategyFormChange = (field: string, value: string) => {
|
||||
setStrategyForm(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleCreateStrategy = async () => {
|
||||
if (!strategyForm.name || !strategyForm.description) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Call backend API to create strategy
|
||||
await contentPlanningApi.createStrategy({
|
||||
name: strategyForm.name,
|
||||
description: strategyForm.description,
|
||||
industry: strategyForm.industry,
|
||||
target_audience: strategyForm.target_audience,
|
||||
content_pillars: strategyForm.content_pillars
|
||||
});
|
||||
|
||||
// Reload data after creating strategy
|
||||
await loadInitialData();
|
||||
|
||||
// Reset form
|
||||
setStrategyForm({
|
||||
name: '',
|
||||
description: '',
|
||||
industry: '',
|
||||
target_audience: '',
|
||||
content_pillars: []
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating strategy:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefreshData = async () => {
|
||||
await loadInitialData();
|
||||
};
|
||||
|
||||
// Onboarding dialog handlers
|
||||
const handleConfirmStrategy = async () => {
|
||||
try {
|
||||
if (currentStrategy) {
|
||||
// For now, we'll just close the dialog since we can't update status
|
||||
// In a real implementation, you would update the strategy status in the database
|
||||
setShowOnboarding(false);
|
||||
|
||||
// Reload strategies to get updated data
|
||||
await loadStrategies();
|
||||
}
|
||||
// In a real implementation, you would update the strategy status in the database
|
||||
setShowOnboarding(false);
|
||||
|
||||
// Reload strategies to get updated data
|
||||
await loadStrategies();
|
||||
} catch (error) {
|
||||
console.error('Error activating strategy:', error);
|
||||
}
|
||||
@@ -396,12 +288,38 @@ const ContentStrategyTab: React.FC = () => {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Active Strategy Status Banner */}
|
||||
{strategyStatus === 'active' && currentStrategy && (
|
||||
<Alert
|
||||
severity="success"
|
||||
sx={{ mb: 3 }}
|
||||
action={
|
||||
<Button
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => setShowOnboarding(true)}
|
||||
startIcon={<EditIcon />}
|
||||
>
|
||||
Edit Strategy
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Typography variant="body1">
|
||||
<strong>Strategy Active:</strong> Your AI-powered content strategy is active and being monitored. View performance analytics in the Analytics tab.
|
||||
</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
||||
{/* Strategic Intelligence */}
|
||||
<Paper sx={{ width: '100%', mb: 3 }}>
|
||||
<StrategyIntelligenceTab />
|
||||
</Paper>
|
||||
{/* Strategic Intelligence - Only show if there's an active strategy */}
|
||||
{strategyStatus === 'active' && (
|
||||
<Paper sx={{ width: '100%', mb: 3 }}>
|
||||
<StrategyIntelligenceTab
|
||||
strategyData={strategyData}
|
||||
loading={strategyDataLoading}
|
||||
error={strategyDataError}
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Strategy Onboarding Dialog */}
|
||||
<StrategyOnboardingDialog
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
Tabs,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
import ContentStrategyBuilder from '../components/ContentStrategyBuilder';
|
||||
import CalendarGenerationWizard from '../components/CalendarGenerationWizard';
|
||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||
import { useStrategyCalendarContext } from '../../../contexts/StrategyCalendarContext';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -36,28 +38,87 @@ function TabPanel(props: TabPanelProps) {
|
||||
}
|
||||
|
||||
const CreateTab: React.FC = () => {
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const [userData, setUserData] = useState<any>({});
|
||||
const location = useLocation();
|
||||
|
||||
// Get strategy context from the provider
|
||||
const { state: { strategyContext }, isFromStrategyActivation } = useStrategyCalendarContext();
|
||||
|
||||
console.log('🔍 CreateTab: Rendering', { userData, strategyContext, locationState: location.state });
|
||||
|
||||
// Memoize the strategy activation status to avoid infinite re-renders
|
||||
const fromStrategyActivation = useMemo(() => {
|
||||
return isFromStrategyActivation();
|
||||
}, [isFromStrategyActivation]);
|
||||
|
||||
// Set initial tab value based on strategy activation
|
||||
const [tabValue, setTabValue] = useState(0); // Always start with Strategy Builder tab
|
||||
|
||||
useEffect(() => {
|
||||
loadUserData();
|
||||
}, []);
|
||||
// Only load user data once on mount
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const comprehensiveData = await contentPlanningApi.getComprehensiveUserData(1);
|
||||
setUserData(comprehensiveData.data);
|
||||
} catch (error) {
|
||||
console.error('Error loading user data:', error);
|
||||
// Set empty data to prevent infinite loops
|
||||
setUserData({});
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, []); // Empty dependency array - only run once
|
||||
|
||||
const loadUserData = async () => {
|
||||
try {
|
||||
// Load comprehensive user data for calendar generation
|
||||
const comprehensiveData = await contentPlanningApi.getComprehensiveUserData(1); // Pass user ID
|
||||
setUserData(comprehensiveData.data); // Extract the data from the response
|
||||
} catch (error) {
|
||||
console.error('Error loading user data:', error);
|
||||
// Auto-switch to Calendar Wizard tab when coming from strategy activation
|
||||
useEffect(() => {
|
||||
console.log('🔍 CreateTab: Checking strategy activation status:', { fromStrategyActivation });
|
||||
|
||||
// Check multiple sources for strategy activation status
|
||||
const isFromStrategy = fromStrategyActivation ||
|
||||
(strategyContext?.activationStatus === 'active') ||
|
||||
(location.state as any)?.fromStrategyActivation;
|
||||
|
||||
console.log('🔍 CreateTab: Strategy activation check:', {
|
||||
fromStrategyActivation,
|
||||
strategyContextActivationStatus: strategyContext?.activationStatus,
|
||||
windowLocationState: location.state || 'N/A',
|
||||
isFromStrategy
|
||||
});
|
||||
|
||||
if (isFromStrategy) {
|
||||
console.log('🎯 CreateTab: Switching to Calendar Wizard tab (index 1)');
|
||||
setTabValue(1); // Switch to Calendar Wizard tab
|
||||
}
|
||||
};
|
||||
}, [fromStrategyActivation, strategyContext?.activationStatus]);
|
||||
|
||||
// Also check on mount for immediate navigation state
|
||||
useEffect(() => {
|
||||
const checkNavigationState = () => {
|
||||
const locationState = location.state as any;
|
||||
console.log('🔍 CreateTab: Initial navigation state check:', locationState);
|
||||
|
||||
if (locationState?.fromStrategyActivation || locationState?.strategyContext) {
|
||||
console.log('🎯 CreateTab: Found navigation state, switching to Calendar Wizard tab (index 1)');
|
||||
setTabValue(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Check immediately
|
||||
checkNavigationState();
|
||||
|
||||
// Also check after a short delay to ensure context is loaded
|
||||
const timer = setTimeout(checkNavigationState, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}, [location.state]);
|
||||
|
||||
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
const handleGenerateCalendar = async (calendarConfig: any) => {
|
||||
const handleGenerateCalendar = useCallback(async (calendarConfig: any) => {
|
||||
try {
|
||||
await contentPlanningApi.generateComprehensiveCalendar({
|
||||
...calendarConfig,
|
||||
@@ -66,7 +127,7 @@ const CreateTab: React.FC = () => {
|
||||
} catch (error) {
|
||||
console.error('Error generating calendar:', error);
|
||||
}
|
||||
};
|
||||
}, [userData]);
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
@@ -104,6 +165,8 @@ const CreateTab: React.FC = () => {
|
||||
userData={userData}
|
||||
onGenerateCalendar={handleGenerateCalendar}
|
||||
loading={false}
|
||||
strategyContext={strategyContext}
|
||||
fromStrategyActivation={fromStrategyActivation}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
BarChart,
|
||||
Bar,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
RadarChart,
|
||||
PolarGrid,
|
||||
PolarAngleAxis,
|
||||
PolarRadiusAxis,
|
||||
Radar,
|
||||
ComposedChart
|
||||
} from 'recharts';
|
||||
import { Box, Typography, Paper, useTheme } from '@mui/material';
|
||||
|
||||
// Color palette for charts
|
||||
const CHART_COLORS = {
|
||||
primary: '#667eea',
|
||||
secondary: '#764ba2',
|
||||
success: '#4caf50',
|
||||
warning: '#ff9800',
|
||||
error: '#f44336',
|
||||
info: '#2196f3',
|
||||
light: '#f5f5f5',
|
||||
dark: '#333333'
|
||||
};
|
||||
|
||||
// Performance Trend Chart Component
|
||||
interface PerformanceTrendChartProps {
|
||||
data: Array<{
|
||||
date: string;
|
||||
traffic_growth: number;
|
||||
engagement_rate: number;
|
||||
conversion_rate: number;
|
||||
content_quality_score: number;
|
||||
}>;
|
||||
title?: string;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const PerformanceTrendChart: React.FC<PerformanceTrendChartProps> = ({
|
||||
data,
|
||||
title = 'Performance Trends',
|
||||
height = 400
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Paper elevation={2} sx={{ p: 3, height }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ mb: 3 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<ResponsiveContainer width="100%" height="85%">
|
||||
<ComposedChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={theme.palette.divider} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
stroke={theme.palette.text.secondary}
|
||||
fontSize={12}
|
||||
/>
|
||||
<YAxis
|
||||
stroke={theme.palette.text.secondary}
|
||||
fontSize={12}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 8
|
||||
}}
|
||||
/>
|
||||
<Legend />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="content_quality_score"
|
||||
stackId="1"
|
||||
stroke={CHART_COLORS.info}
|
||||
fill={CHART_COLORS.info}
|
||||
fillOpacity={0.3}
|
||||
name="Content Quality Score"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="traffic_growth"
|
||||
stroke={CHART_COLORS.primary}
|
||||
strokeWidth={3}
|
||||
dot={{ fill: CHART_COLORS.primary, strokeWidth: 2, r: 4 }}
|
||||
name="Traffic Growth (%)"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="engagement_rate"
|
||||
stroke={CHART_COLORS.success}
|
||||
strokeWidth={3}
|
||||
dot={{ fill: CHART_COLORS.success, strokeWidth: 2, r: 4 }}
|
||||
name="Engagement Rate (%)"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="conversion_rate"
|
||||
stroke={CHART_COLORS.warning}
|
||||
strokeWidth={3}
|
||||
dot={{ fill: CHART_COLORS.warning, strokeWidth: 2, r: 4 }}
|
||||
name="Conversion Rate (%)"
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
// Quality Metrics Radar Chart Component
|
||||
interface QualityMetricsRadarProps {
|
||||
data: Array<{
|
||||
metric: string;
|
||||
score: number;
|
||||
target: number;
|
||||
}>;
|
||||
title?: string;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const QualityMetricsRadar: React.FC<QualityMetricsRadarProps> = ({
|
||||
data,
|
||||
title = 'Quality Metrics Analysis',
|
||||
height = 400
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Paper elevation={2} sx={{ p: 3, height }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ mb: 3 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<ResponsiveContainer width="100%" height="85%">
|
||||
<RadarChart data={data}>
|
||||
<PolarGrid stroke={theme.palette.divider} />
|
||||
<PolarAngleAxis
|
||||
dataKey="metric"
|
||||
tick={{ fill: theme.palette.text.primary, fontSize: 12 }}
|
||||
/>
|
||||
<PolarRadiusAxis
|
||||
angle={90}
|
||||
domain={[0, 100]}
|
||||
tick={{ fill: theme.palette.text.secondary, fontSize: 10 }}
|
||||
/>
|
||||
<Radar
|
||||
name="Current Score"
|
||||
dataKey="score"
|
||||
stroke={CHART_COLORS.primary}
|
||||
fill={CHART_COLORS.primary}
|
||||
fillOpacity={0.3}
|
||||
/>
|
||||
<Radar
|
||||
name="Target Score"
|
||||
dataKey="target"
|
||||
stroke={CHART_COLORS.success}
|
||||
fill={CHART_COLORS.success}
|
||||
fillOpacity={0.1}
|
||||
strokeDasharray="5 5"
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 8
|
||||
}}
|
||||
/>
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
// Performance Metrics Bar Chart Component
|
||||
interface PerformanceMetricsBarProps {
|
||||
data: Array<{
|
||||
metric: string;
|
||||
value: number;
|
||||
target: number;
|
||||
status: 'excellent' | 'good' | 'needs_attention';
|
||||
}>;
|
||||
title?: string;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const PerformanceMetricsBar: React.FC<PerformanceMetricsBarProps> = ({
|
||||
data,
|
||||
title = 'Performance Metrics',
|
||||
height = 400
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'excellent': return CHART_COLORS.success;
|
||||
case 'good': return CHART_COLORS.warning;
|
||||
case 'needs_attention': return CHART_COLORS.error;
|
||||
default: return CHART_COLORS.primary;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper elevation={2} sx={{ p: 3, height }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ mb: 3 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<ResponsiveContainer width="100%" height="85%">
|
||||
<BarChart data={data} layout="horizontal">
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={theme.palette.divider} />
|
||||
<XAxis
|
||||
type="number"
|
||||
stroke={theme.palette.text.secondary}
|
||||
fontSize={12}
|
||||
/>
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="metric"
|
||||
stroke={theme.palette.text.secondary}
|
||||
fontSize={12}
|
||||
width={120}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 8
|
||||
}}
|
||||
/>
|
||||
<Legend />
|
||||
<Bar
|
||||
dataKey="value"
|
||||
fill={CHART_COLORS.primary}
|
||||
radius={[0, 4, 4, 0]}
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={getStatusColor(entry.status)}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
<Bar
|
||||
dataKey="target"
|
||||
fill={CHART_COLORS.info}
|
||||
fillOpacity={0.5}
|
||||
radius={[0, 4, 4, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
// Content Distribution Pie Chart Component
|
||||
interface ContentDistributionPieProps {
|
||||
data: Array<{
|
||||
name: string;
|
||||
value: number;
|
||||
color?: string;
|
||||
}>;
|
||||
title?: string;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const ContentDistributionPie: React.FC<ContentDistributionPieProps> = ({
|
||||
data,
|
||||
title = 'Content Distribution',
|
||||
height = 400
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const COLORS = [
|
||||
CHART_COLORS.primary,
|
||||
CHART_COLORS.secondary,
|
||||
CHART_COLORS.success,
|
||||
CHART_COLORS.warning,
|
||||
CHART_COLORS.error,
|
||||
CHART_COLORS.info
|
||||
];
|
||||
|
||||
return (
|
||||
<Paper elevation={2} sx={{ p: 3, height }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ mb: 3 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<ResponsiveContainer width="100%" height="85%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ name, percent }) => `${name} ${((percent || 0) * 100).toFixed(0)}%`}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={entry.color || COLORS[index % COLORS.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 8
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
// Real-time Performance Gauge Component
|
||||
interface PerformanceGaugeProps {
|
||||
value: number;
|
||||
maxValue: number;
|
||||
title: string;
|
||||
color?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const PerformanceGauge: React.FC<PerformanceGaugeProps> = ({
|
||||
value,
|
||||
maxValue,
|
||||
title,
|
||||
color = CHART_COLORS.primary,
|
||||
size = 200
|
||||
}) => {
|
||||
const percentage = (value / maxValue) * 100;
|
||||
const circumference = 2 * Math.PI * 80; // radius = 80
|
||||
const strokeDasharray = circumference;
|
||||
const strokeDashoffset = circumference - (percentage / 100) * circumference;
|
||||
|
||||
return (
|
||||
<Box sx={{ textAlign: 'center', p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{title}
|
||||
</Typography>
|
||||
<Box sx={{ position: 'relative', display: 'inline-block' }}>
|
||||
<svg width={size} height={size} viewBox="0 0 200 200">
|
||||
{/* Background circle */}
|
||||
<circle
|
||||
cx="100"
|
||||
cy="100"
|
||||
r="80"
|
||||
fill="none"
|
||||
stroke="#e0e0e0"
|
||||
strokeWidth="12"
|
||||
/>
|
||||
{/* Progress circle */}
|
||||
<circle
|
||||
cx="100"
|
||||
cy="100"
|
||||
r="80"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="12"
|
||||
strokeDasharray={strokeDasharray}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
strokeLinecap="round"
|
||||
transform="rotate(-90 100 100)"
|
||||
/>
|
||||
</svg>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4" sx={{ fontWeight: 'bold', color }}>
|
||||
{value}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
of {maxValue}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// Export all components
|
||||
export {
|
||||
PerformanceTrendChart,
|
||||
QualityMetricsRadar,
|
||||
PerformanceMetricsBar,
|
||||
ContentDistributionPie,
|
||||
PerformanceGauge
|
||||
};
|
||||
358
frontend/src/contexts/StrategyCalendarContext.tsx
Normal file
358
frontend/src/contexts/StrategyCalendarContext.tsx
Normal file
@@ -0,0 +1,358 @@
|
||||
import React, { createContext, useContext, useReducer, useEffect, useCallback, ReactNode } from 'react';
|
||||
import { StrategyContext, CalendarContext, WorkflowProgress } from '../services/navigationOrchestrator';
|
||||
|
||||
// Context state interface
|
||||
interface StrategyCalendarState {
|
||||
activeStrategy: any | null;
|
||||
strategyContext: StrategyContext | null;
|
||||
calendarContext: CalendarContext | null;
|
||||
workflowProgress: WorkflowProgress;
|
||||
isContextValid: boolean;
|
||||
lastUpdated: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
// Action types
|
||||
type StrategyCalendarAction =
|
||||
| { type: 'SET_ACTIVE_STRATEGY'; payload: any }
|
||||
| { type: 'SET_STRATEGY_CONTEXT'; payload: StrategyContext }
|
||||
| { type: 'SET_CALENDAR_CONTEXT'; payload: CalendarContext }
|
||||
| { type: 'UPDATE_WORKFLOW_PROGRESS'; payload: Partial<WorkflowProgress> }
|
||||
| { type: 'CLEAR_CONTEXT' }
|
||||
| { type: 'SET_ERROR'; payload: string }
|
||||
| { type: 'CLEAR_ERROR' }
|
||||
| { type: 'VALIDATE_CONTEXT' };
|
||||
|
||||
// Initial state
|
||||
const initialState: StrategyCalendarState = {
|
||||
activeStrategy: null,
|
||||
strategyContext: null,
|
||||
calendarContext: null,
|
||||
workflowProgress: {
|
||||
currentStep: 'strategy',
|
||||
completedSteps: [],
|
||||
totalSteps: 4,
|
||||
progressPercentage: 0
|
||||
},
|
||||
isContextValid: false,
|
||||
lastUpdated: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
// Reducer function
|
||||
function strategyCalendarReducer(state: StrategyCalendarState, action: StrategyCalendarAction): StrategyCalendarState {
|
||||
switch (action.type) {
|
||||
case 'SET_ACTIVE_STRATEGY':
|
||||
return {
|
||||
...state,
|
||||
activeStrategy: action.payload,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
error: null
|
||||
};
|
||||
|
||||
case 'SET_STRATEGY_CONTEXT':
|
||||
return {
|
||||
...state,
|
||||
strategyContext: action.payload,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
error: null
|
||||
};
|
||||
|
||||
case 'SET_CALENDAR_CONTEXT':
|
||||
return {
|
||||
...state,
|
||||
calendarContext: action.payload,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
error: null
|
||||
};
|
||||
|
||||
case 'UPDATE_WORKFLOW_PROGRESS':
|
||||
return {
|
||||
...state,
|
||||
workflowProgress: {
|
||||
...state.workflowProgress,
|
||||
...action.payload
|
||||
},
|
||||
lastUpdated: new Date().toISOString()
|
||||
};
|
||||
|
||||
case 'CLEAR_CONTEXT':
|
||||
return {
|
||||
...initialState,
|
||||
lastUpdated: new Date().toISOString()
|
||||
};
|
||||
|
||||
case 'SET_ERROR':
|
||||
return {
|
||||
...state,
|
||||
error: action.payload,
|
||||
lastUpdated: new Date().toISOString()
|
||||
};
|
||||
|
||||
case 'CLEAR_ERROR':
|
||||
return {
|
||||
...state,
|
||||
error: null
|
||||
};
|
||||
|
||||
case 'VALIDATE_CONTEXT':
|
||||
const isValid = validateContextIntegrity(state);
|
||||
return {
|
||||
...state,
|
||||
isContextValid: isValid,
|
||||
lastUpdated: new Date().toISOString()
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// Context validation function
|
||||
function validateContextIntegrity(state: StrategyCalendarState): boolean {
|
||||
try {
|
||||
// Check if strategy context exists and has required fields
|
||||
if (state.strategyContext) {
|
||||
const { strategyId, strategyData, activationStatus } = state.strategyContext;
|
||||
if (!strategyId || !strategyData || !activationStatus) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if calendar context is valid when it exists
|
||||
if (state.calendarContext) {
|
||||
const { strategyContext, autoPopulatedData } = state.calendarContext;
|
||||
if (strategyContext && !validateContextIntegrity({ ...state, strategyContext })) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check workflow progress validity
|
||||
const { currentStep, totalSteps, progressPercentage } = state.workflowProgress;
|
||||
if (!currentStep || totalSteps <= 0 || progressPercentage < 0 || progressPercentage > 100) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Context validation error:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Context interface
|
||||
interface StrategyCalendarContextType {
|
||||
state: StrategyCalendarState;
|
||||
dispatch: React.Dispatch<StrategyCalendarAction>;
|
||||
|
||||
// Convenience methods
|
||||
setActiveStrategy: (strategy: any) => void;
|
||||
setStrategyContext: (context: StrategyContext) => void;
|
||||
setCalendarContext: (context: CalendarContext) => void;
|
||||
updateWorkflowProgress: (progress: Partial<WorkflowProgress>) => void;
|
||||
clearContext: () => void;
|
||||
setError: (error: string) => void;
|
||||
clearError: () => void;
|
||||
validateContext: () => void;
|
||||
|
||||
// Utility methods
|
||||
getStrategyData: () => any | null;
|
||||
getCalendarData: () => any | null;
|
||||
isStrategyActive: () => boolean;
|
||||
isCalendarReady: () => boolean;
|
||||
getWorkflowStep: () => string;
|
||||
getProgressPercentage: () => number;
|
||||
isFromStrategyActivation: () => boolean;
|
||||
}
|
||||
|
||||
// Create context
|
||||
const StrategyCalendarContext = createContext<StrategyCalendarContextType | undefined>(undefined);
|
||||
|
||||
// Provider component
|
||||
interface StrategyCalendarProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const StrategyCalendarProvider: React.FC<StrategyCalendarProviderProps> = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(strategyCalendarReducer, initialState);
|
||||
|
||||
// Validate context on state changes
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'VALIDATE_CONTEXT' });
|
||||
}, [state.strategyContext, state.calendarContext, state.workflowProgress]);
|
||||
|
||||
// Persist context to session storage
|
||||
useEffect(() => {
|
||||
if (state.lastUpdated) {
|
||||
try {
|
||||
sessionStorage.setItem('strategyCalendarContext', JSON.stringify({
|
||||
strategyContext: state.strategyContext,
|
||||
calendarContext: state.calendarContext,
|
||||
workflowProgress: state.workflowProgress,
|
||||
lastUpdated: state.lastUpdated
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to persist context to session storage:', error);
|
||||
}
|
||||
}
|
||||
}, [state.strategyContext, state.calendarContext, state.workflowProgress, state.lastUpdated]);
|
||||
|
||||
// Restore context from session storage on mount
|
||||
useEffect(() => {
|
||||
try {
|
||||
const persisted = sessionStorage.getItem('strategyCalendarContext');
|
||||
if (persisted) {
|
||||
const data = JSON.parse(persisted);
|
||||
const lastUpdated = new Date(data.lastUpdated);
|
||||
const now = new Date();
|
||||
|
||||
// Check if context is still valid (not older than 30 minutes)
|
||||
if (now.getTime() - lastUpdated.getTime() < 30 * 60 * 1000) {
|
||||
if (data.strategyContext) {
|
||||
dispatch({ type: 'SET_STRATEGY_CONTEXT', payload: data.strategyContext });
|
||||
}
|
||||
if (data.calendarContext) {
|
||||
dispatch({ type: 'SET_CALENDAR_CONTEXT', payload: data.calendarContext });
|
||||
}
|
||||
if (data.workflowProgress) {
|
||||
dispatch({ type: 'UPDATE_WORKFLOW_PROGRESS', payload: data.workflowProgress });
|
||||
}
|
||||
} else {
|
||||
// Clear expired context
|
||||
sessionStorage.removeItem('strategyCalendarContext');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to restore context from session storage:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Convenience methods
|
||||
const setActiveStrategy = (strategy: any) => {
|
||||
dispatch({ type: 'SET_ACTIVE_STRATEGY', payload: strategy });
|
||||
};
|
||||
|
||||
const setStrategyContext = (context: StrategyContext) => {
|
||||
dispatch({ type: 'SET_STRATEGY_CONTEXT', payload: context });
|
||||
};
|
||||
|
||||
const setCalendarContext = (context: CalendarContext) => {
|
||||
dispatch({ type: 'SET_CALENDAR_CONTEXT', payload: context });
|
||||
};
|
||||
|
||||
const updateWorkflowProgress = (progress: Partial<WorkflowProgress>) => {
|
||||
dispatch({ type: 'UPDATE_WORKFLOW_PROGRESS', payload: progress });
|
||||
};
|
||||
|
||||
const clearContext = () => {
|
||||
dispatch({ type: 'CLEAR_CONTEXT' });
|
||||
sessionStorage.removeItem('strategyCalendarContext');
|
||||
};
|
||||
|
||||
const setError = (error: string) => {
|
||||
dispatch({ type: 'SET_ERROR', payload: error });
|
||||
};
|
||||
|
||||
const clearError = () => {
|
||||
dispatch({ type: 'CLEAR_ERROR' });
|
||||
};
|
||||
|
||||
const validateContext = () => {
|
||||
dispatch({ type: 'VALIDATE_CONTEXT' });
|
||||
};
|
||||
|
||||
// Utility methods
|
||||
const getStrategyData = () => state.strategyContext?.strategyData || null;
|
||||
|
||||
const getCalendarData = () => state.calendarContext?.autoPopulatedData || null;
|
||||
|
||||
const isStrategyActive = () => state.strategyContext?.activationStatus === 'active';
|
||||
|
||||
const isCalendarReady = () => {
|
||||
return state.calendarContext !== null &&
|
||||
state.calendarContext.autoPopulatedData !== null &&
|
||||
state.isContextValid;
|
||||
};
|
||||
|
||||
const getWorkflowStep = () => state.workflowProgress.currentStep;
|
||||
|
||||
const getProgressPercentage = () => state.workflowProgress.progressPercentage;
|
||||
|
||||
const isFromStrategyActivation = useCallback(() => {
|
||||
// Check if we have a preserved strategy context from navigation
|
||||
const result = state.strategyContext?.activationStatus === 'active' &&
|
||||
state.strategyContext?.activationTimestamp !== null;
|
||||
console.log('🔍 StrategyCalendarContext: isFromStrategyActivation check:', {
|
||||
activationStatus: state.strategyContext?.activationStatus,
|
||||
activationTimestamp: state.strategyContext?.activationTimestamp,
|
||||
result
|
||||
});
|
||||
return result;
|
||||
}, [state.strategyContext?.activationStatus, state.strategyContext?.activationTimestamp]);
|
||||
|
||||
const contextValue: StrategyCalendarContextType = {
|
||||
state,
|
||||
dispatch,
|
||||
setActiveStrategy,
|
||||
setStrategyContext,
|
||||
setCalendarContext,
|
||||
updateWorkflowProgress,
|
||||
clearContext,
|
||||
setError,
|
||||
clearError,
|
||||
validateContext,
|
||||
getStrategyData,
|
||||
getCalendarData,
|
||||
isStrategyActive,
|
||||
isCalendarReady,
|
||||
getWorkflowStep,
|
||||
getProgressPercentage,
|
||||
isFromStrategyActivation
|
||||
};
|
||||
|
||||
return (
|
||||
<StrategyCalendarContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</StrategyCalendarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Custom hook to use the context
|
||||
export const useStrategyCalendarContext = (): StrategyCalendarContextType => {
|
||||
const context = useContext(StrategyCalendarContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useStrategyCalendarContext must be used within a StrategyCalendarProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// Hook for components that only need read access
|
||||
export const useStrategyCalendarState = () => {
|
||||
const { state } = useStrategyCalendarContext();
|
||||
return state;
|
||||
};
|
||||
|
||||
// Hook for components that need to dispatch actions
|
||||
export const useStrategyCalendarActions = () => {
|
||||
const {
|
||||
setActiveStrategy,
|
||||
setStrategyContext,
|
||||
setCalendarContext,
|
||||
updateWorkflowProgress,
|
||||
clearContext,
|
||||
setError,
|
||||
clearError,
|
||||
validateContext
|
||||
} = useStrategyCalendarContext();
|
||||
|
||||
return {
|
||||
setActiveStrategy,
|
||||
setStrategyContext,
|
||||
setCalendarContext,
|
||||
updateWorkflowProgress,
|
||||
clearContext,
|
||||
setError,
|
||||
clearError,
|
||||
validateContext
|
||||
};
|
||||
};
|
||||
232
frontend/src/hooks/useRealTimeData.ts
Normal file
232
frontend/src/hooks/useRealTimeData.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
|
||||
interface RealTimeDataOptions {
|
||||
strategyId: number;
|
||||
reconnectInterval?: number;
|
||||
maxReconnectAttempts?: number;
|
||||
onConnect?: () => void;
|
||||
onDisconnect?: () => void;
|
||||
onError?: (error: Event) => void;
|
||||
onMessage?: (data: any) => void;
|
||||
}
|
||||
|
||||
interface RealTimeDataState {
|
||||
data: any;
|
||||
isConnected: boolean;
|
||||
isConnecting: boolean;
|
||||
error: string | null;
|
||||
reconnectAttempts: number;
|
||||
}
|
||||
|
||||
export const useRealTimeData = (options: RealTimeDataOptions) => {
|
||||
const {
|
||||
strategyId,
|
||||
reconnectInterval = 5000,
|
||||
maxReconnectAttempts = 5,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
onError,
|
||||
onMessage
|
||||
} = options;
|
||||
|
||||
const [state, setState] = useState<RealTimeDataState>({
|
||||
data: null,
|
||||
isConnected: false,
|
||||
isConnecting: false,
|
||||
error: null,
|
||||
reconnectAttempts: 0
|
||||
});
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const reconnectAttemptsRef = useRef(0);
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(prev => ({ ...prev, isConnecting: true, error: null }));
|
||||
|
||||
try {
|
||||
// For development, use a mock WebSocket connection
|
||||
// In production, this would be the actual WebSocket URL
|
||||
const wsUrl = process.env.NODE_ENV === 'development'
|
||||
? `ws://localhost:8000/ws/strategy/${strategyId}/live`
|
||||
: `wss://api.alwrity.com/ws/strategy/${strategyId}/live`;
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isConnected: true,
|
||||
isConnecting: false,
|
||||
error: null,
|
||||
reconnectAttempts: 0
|
||||
}));
|
||||
reconnectAttemptsRef.current = 0;
|
||||
onConnect?.();
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
setState(prev => ({ ...prev, data }));
|
||||
onMessage?.(data);
|
||||
} catch (error) {
|
||||
console.error('Error parsing WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isConnected: false,
|
||||
isConnecting: false
|
||||
}));
|
||||
onDisconnect?.();
|
||||
|
||||
// Attempt to reconnect if not a clean close
|
||||
if (event.code !== 1000 && reconnectAttemptsRef.current < maxReconnectAttempts) {
|
||||
reconnectAttemptsRef.current += 1;
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
reconnectAttempts: reconnectAttemptsRef.current
|
||||
}));
|
||||
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
connect();
|
||||
}, reconnectInterval);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
error: 'WebSocket connection error',
|
||||
isConnecting: false
|
||||
}));
|
||||
onError?.(error);
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
error: 'Failed to create WebSocket connection',
|
||||
isConnecting: false
|
||||
}));
|
||||
}
|
||||
}, [strategyId, reconnectInterval, maxReconnectAttempts, onConnect, onDisconnect, onError, onMessage]);
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
reconnectTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close(1000); // Clean close
|
||||
wsRef.current = null;
|
||||
}
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isConnected: false,
|
||||
isConnecting: false,
|
||||
reconnectAttempts: 0
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const sendMessage = useCallback((message: any) => {
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.send(JSON.stringify(message));
|
||||
} else {
|
||||
console.warn('WebSocket is not connected');
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Connect on mount and when strategyId changes
|
||||
useEffect(() => {
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
}, [connect, disconnect]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
}, [disconnect]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
connect,
|
||||
disconnect,
|
||||
sendMessage
|
||||
};
|
||||
};
|
||||
|
||||
// Mock real-time data for development
|
||||
export const useMockRealTimeData = (strategyId: number) => {
|
||||
const [data, setData] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate real-time data updates
|
||||
const interval = setInterval(() => {
|
||||
const mockData = {
|
||||
timestamp: new Date().toISOString(),
|
||||
strategyId,
|
||||
metrics: {
|
||||
traffic_growth: Math.random() * 20 + 5, // 5-25%
|
||||
engagement_rate: Math.random() * 10 + 5, // 5-15%
|
||||
conversion_rate: Math.random() * 3 + 1, // 1-4%
|
||||
content_quality_score: Math.random() * 20 + 80, // 80-100%
|
||||
strategy_adoption_rate: Math.random() * 20 + 80, // 80-100%
|
||||
roi_ratio: Math.random() * 2 + 2, // 2-4x
|
||||
competitive_position_rank: Math.floor(Math.random() * 5) + 1, // 1-5
|
||||
audience_growth_percentage: Math.random() * 15 + 8, // 8-23%
|
||||
confidence_score: Math.random() * 20 + 80 // 80-100%
|
||||
},
|
||||
alerts: Math.random() > 0.8 ? [
|
||||
{
|
||||
type: 'warning',
|
||||
message: 'Engagement rate dropped below target',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
] : [],
|
||||
trends: {
|
||||
daily: Array.from({ length: 7 }, (_, i) => ({
|
||||
date: new Date(Date.now() - (6 - i) * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
||||
traffic_growth: Math.random() * 20 + 5,
|
||||
engagement_rate: Math.random() * 10 + 5,
|
||||
conversion_rate: Math.random() * 3 + 1,
|
||||
content_quality_score: Math.random() * 20 + 80
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
setData(mockData);
|
||||
}, 5000); // Update every 5 seconds
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [strategyId]);
|
||||
|
||||
return {
|
||||
data,
|
||||
isConnected: true,
|
||||
isConnecting: false,
|
||||
error: null,
|
||||
reconnectAttempts: 0,
|
||||
connect: () => {},
|
||||
disconnect: () => {},
|
||||
sendMessage: () => {}
|
||||
};
|
||||
};
|
||||
|
||||
// Export both hooks
|
||||
export default useRealTimeData;
|
||||
282
frontend/src/services/navigationOrchestrator.ts
Normal file
282
frontend/src/services/navigationOrchestrator.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export interface StrategyContext {
|
||||
strategyId: string;
|
||||
strategyData: any;
|
||||
activationStatus: 'pending' | 'confirmed' | 'active';
|
||||
activationTimestamp: string;
|
||||
userPreferences: any;
|
||||
strategicIntelligence: any;
|
||||
}
|
||||
|
||||
export interface CalendarContext {
|
||||
strategyContext: StrategyContext | null;
|
||||
autoPopulatedData: any;
|
||||
userModifications: any;
|
||||
generationProgress: number;
|
||||
}
|
||||
|
||||
export interface WorkflowProgress {
|
||||
currentStep: 'strategy' | 'activation' | 'calendar' | 'generation';
|
||||
completedSteps: string[];
|
||||
totalSteps: number;
|
||||
progressPercentage: number;
|
||||
}
|
||||
|
||||
export interface NavigationState {
|
||||
fromStrategy: boolean;
|
||||
preservedContext: StrategyContext | null;
|
||||
returnPath: string | null;
|
||||
}
|
||||
|
||||
class NavigationOrchestrator {
|
||||
private static instance: NavigationOrchestrator;
|
||||
private contextStorage: Map<string, any> = new Map();
|
||||
private progressTracking: WorkflowProgress = {
|
||||
currentStep: 'strategy',
|
||||
completedSteps: [],
|
||||
totalSteps: 4,
|
||||
progressPercentage: 0
|
||||
};
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): NavigationOrchestrator {
|
||||
if (!NavigationOrchestrator.instance) {
|
||||
NavigationOrchestrator.instance = new NavigationOrchestrator();
|
||||
}
|
||||
return NavigationOrchestrator.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate from strategy activation to calendar wizard
|
||||
*/
|
||||
navigateToCalendarWizard(strategyId: string, strategyContext: StrategyContext): void {
|
||||
console.log('🎯 Navigation Orchestrator: Navigating to calendar wizard', { strategyId });
|
||||
|
||||
// Preserve strategy context
|
||||
this.preserveContext('strategy', strategyContext);
|
||||
|
||||
// Update progress
|
||||
this.updateProgress('activation');
|
||||
|
||||
// Store navigation state
|
||||
this.contextStorage.set('navigationState', {
|
||||
fromStrategy: true,
|
||||
preservedContext: strategyContext,
|
||||
returnPath: '/content-planning'
|
||||
});
|
||||
|
||||
// Also store in session storage for context restoration
|
||||
sessionStorage.setItem('strategyCalendarContext', JSON.stringify({
|
||||
strategyContext,
|
||||
lastUpdated: new Date().toISOString()
|
||||
}));
|
||||
|
||||
// Navigate to calendar wizard with context
|
||||
const navigate = this.getNavigateFunction();
|
||||
if (navigate) {
|
||||
navigate('/content-planning', {
|
||||
state: {
|
||||
activeTab: 4, // Create tab (where Calendar Wizard is located)
|
||||
strategyContext,
|
||||
fromStrategyActivation: true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preserve context for later restoration
|
||||
*/
|
||||
preserveContext(key: string, context: any): void {
|
||||
console.log('💾 Navigation Orchestrator: Preserving context', { key });
|
||||
this.contextStorage.set(key, {
|
||||
data: context,
|
||||
timestamp: new Date().toISOString(),
|
||||
expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString() // 30 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore context by key
|
||||
*/
|
||||
restoreContext(key: string): any | null {
|
||||
const stored = this.contextStorage.get(key);
|
||||
if (!stored) {
|
||||
console.log('⚠️ Navigation Orchestrator: No context found for key', { key });
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if context has expired
|
||||
if (new Date() > new Date(stored.expiresAt)) {
|
||||
console.log('⏰ Navigation Orchestrator: Context expired for key', { key });
|
||||
this.contextStorage.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('🔄 Navigation Orchestrator: Restoring context', { key });
|
||||
return stored.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear context by key
|
||||
*/
|
||||
clearContext(key: string): void {
|
||||
console.log('🗑️ Navigation Orchestrator: Clearing context', { key });
|
||||
this.contextStorage.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all contexts
|
||||
*/
|
||||
clearAllContexts(): void {
|
||||
console.log('🗑️ Navigation Orchestrator: Clearing all contexts');
|
||||
this.contextStorage.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update workflow progress
|
||||
*/
|
||||
updateProgress(step: WorkflowProgress['currentStep']): void {
|
||||
const steps = ['strategy', 'activation', 'calendar', 'generation'];
|
||||
const currentIndex = steps.indexOf(step);
|
||||
|
||||
this.progressTracking = {
|
||||
currentStep: step,
|
||||
completedSteps: steps.slice(0, currentIndex),
|
||||
totalSteps: steps.length,
|
||||
progressPercentage: ((currentIndex + 1) / steps.length) * 100
|
||||
};
|
||||
|
||||
console.log('📊 Navigation Orchestrator: Progress updated', this.progressTracking);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current progress
|
||||
*/
|
||||
getProgress(): WorkflowProgress {
|
||||
return { ...this.progressTracking };
|
||||
}
|
||||
|
||||
/**
|
||||
* Track navigation event
|
||||
*/
|
||||
trackNavigation(from: string, to: string, context?: any): void {
|
||||
console.log('🧭 Navigation Orchestrator: Navigation tracked', { from, to, hasContext: !!context });
|
||||
|
||||
// Store navigation history
|
||||
const history = this.contextStorage.get('navigationHistory') || [];
|
||||
history.push({
|
||||
from,
|
||||
to,
|
||||
timestamp: new Date().toISOString(),
|
||||
context: context ? Object.keys(context) : []
|
||||
});
|
||||
|
||||
// Keep only last 10 navigation events
|
||||
if (history.length > 10) {
|
||||
history.splice(0, history.length - 10);
|
||||
}
|
||||
|
||||
this.contextStorage.set('navigationHistory', history);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get navigation state
|
||||
*/
|
||||
getNavigationState(): NavigationState | null {
|
||||
return this.contextStorage.get('navigationState') || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if navigation is from strategy activation
|
||||
*/
|
||||
isFromStrategyActivation(): boolean {
|
||||
const state = this.getNavigationState();
|
||||
return state?.fromStrategy || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preserved strategy context
|
||||
*/
|
||||
getPreservedStrategyContext(): StrategyContext | null {
|
||||
return this.restoreContext('strategy');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle strategy activation success
|
||||
*/
|
||||
handleStrategyActivationSuccess(strategyId: string, strategyData: any): void {
|
||||
console.log('✅ Navigation Orchestrator: Strategy activation successful', { strategyId });
|
||||
|
||||
const strategyContext: StrategyContext = {
|
||||
strategyId,
|
||||
strategyData,
|
||||
activationStatus: 'active',
|
||||
activationTimestamp: new Date().toISOString(),
|
||||
userPreferences: strategyData.userPreferences || {},
|
||||
strategicIntelligence: strategyData.strategicIntelligence || {}
|
||||
};
|
||||
|
||||
// Navigate to calendar wizard
|
||||
this.navigateToCalendarWizard(strategyId, strategyContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle calendar generation completion
|
||||
*/
|
||||
handleCalendarGenerationComplete(calendarData: any): void {
|
||||
console.log('✅ Navigation Orchestrator: Calendar generation complete');
|
||||
|
||||
// Update progress
|
||||
this.updateProgress('generation');
|
||||
|
||||
// Clear strategy context as workflow is complete
|
||||
this.clearContext('strategy');
|
||||
|
||||
// Navigate to calendar view
|
||||
const navigate = this.getNavigateFunction();
|
||||
if (navigate) {
|
||||
navigate('/content-planning', {
|
||||
state: {
|
||||
activeTab: 1, // Calendar tab
|
||||
showGeneratedCalendar: true,
|
||||
calendarData
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get navigate function (to be set by components)
|
||||
*/
|
||||
private getNavigateFunction(): any {
|
||||
// Return the stored navigate function
|
||||
return (this as any).navigate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set navigate function (called by components)
|
||||
*/
|
||||
setNavigateFunction(navigate: any): void {
|
||||
// Store navigate function for internal use
|
||||
(this as any).navigate = navigate;
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const navigationOrchestrator = NavigationOrchestrator.getInstance();
|
||||
|
||||
// Hook for components to use the orchestrator
|
||||
export const useNavigationOrchestrator = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Set navigate function in orchestrator only once
|
||||
React.useEffect(() => {
|
||||
navigationOrchestrator.setNavigateFunction(navigate);
|
||||
}, [navigate]);
|
||||
|
||||
return navigationOrchestrator;
|
||||
};
|
||||
259
frontend/src/services/strategyMonitoringApi.ts
Normal file
259
frontend/src/services/strategyMonitoringApi.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import { apiClient } from '../api/client';
|
||||
import { useState } from 'react';
|
||||
|
||||
export interface MonitoringTask {
|
||||
component: string;
|
||||
title: string;
|
||||
description: string;
|
||||
assignee: 'ALwrity' | 'Human';
|
||||
frequency: string;
|
||||
metric: string;
|
||||
measurementMethod: string;
|
||||
successCriteria: string;
|
||||
alertThreshold: string;
|
||||
actionableInsights: string;
|
||||
}
|
||||
|
||||
export interface MonitoringComponent {
|
||||
name: string;
|
||||
icon: string;
|
||||
tasks: MonitoringTask[];
|
||||
}
|
||||
|
||||
export interface MonitoringPlan {
|
||||
totalTasks: number;
|
||||
alwrityTasks: number;
|
||||
humanTasks: number;
|
||||
metricsCount: number;
|
||||
monitoringTasks: MonitoringTask[];
|
||||
monitoringSchedule?: any;
|
||||
successMetrics?: any;
|
||||
metadata?: any;
|
||||
}
|
||||
|
||||
export const strategyMonitoringApi = {
|
||||
/**
|
||||
* Generate monitoring plan for a strategy
|
||||
*/
|
||||
async generateMonitoringPlan(strategyId: number): Promise<{ success: boolean; data: MonitoringPlan; message: string }> {
|
||||
try {
|
||||
const response = await apiClient.post(`/api/content-planning/strategy/${strategyId}/generate-monitoring-plan`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error generating monitoring plan:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to generate monitoring plan');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Activate strategy with monitoring plan
|
||||
*/
|
||||
async activateStrategyWithMonitoring(strategyId: number, monitoringPlan: MonitoringPlan): Promise<{ success: boolean; message: string; strategy_id: number }> {
|
||||
try {
|
||||
const response = await apiClient.post(`/api/content-planning/strategy/${strategyId}/activate-with-monitoring`, monitoringPlan);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error activating strategy with monitoring:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to activate strategy with monitoring');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get monitoring plan for a strategy
|
||||
*/
|
||||
async getMonitoringPlan(strategyId: number): Promise<{ success: boolean; data: any }> {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/content-planning/strategy/${strategyId}/monitoring-plan`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error getting monitoring plan:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to get monitoring plan');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update monitoring plan
|
||||
*/
|
||||
async updateMonitoringPlan(strategyId: number, monitoringPlan: Partial<MonitoringPlan>): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await apiClient.put(`/api/content-planning/strategy/${strategyId}/monitoring-plan`, monitoringPlan);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error updating monitoring plan:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to update monitoring plan');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get performance history for a strategy
|
||||
*/
|
||||
async getPerformanceHistory(strategyId: number, days: number = 30): Promise<{ success: boolean; data: any }> {
|
||||
try {
|
||||
const response = await apiClient.get(`/content-planning/strategy/${strategyId}/performance-history?days=${days}`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error getting performance history:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to get performance history');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Deactivate a strategy
|
||||
*/
|
||||
async deactivateStrategy(strategyId: number, userId: number = 1): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await apiClient.post(`/api/content-planning/strategy/${strategyId}/deactivate`, { user_id: userId });
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error deactivating strategy:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to deactivate strategy');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Pause a strategy
|
||||
*/
|
||||
async pauseStrategy(strategyId: number, userId: number = 1): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await apiClient.post(`/api/content-planning/strategy/${strategyId}/pause`, { user_id: userId });
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error pausing strategy:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to pause strategy');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resume a strategy
|
||||
*/
|
||||
async resumeStrategy(strategyId: number, userId: number = 1): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await apiClient.post(`/api/content-planning/strategy/${strategyId}/resume`, { user_id: userId });
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error resuming strategy:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to resume strategy');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get performance metrics for a strategy
|
||||
*/
|
||||
async getPerformanceMetrics(strategyId: number): Promise<{ success: boolean; data: any; message: string }> {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/content-planning/strategy/${strategyId}/performance-metrics`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error getting performance metrics:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to get performance metrics');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get trend data for a strategy
|
||||
*/
|
||||
async getTrendData(strategyId: number, timeRange: string = '30d'): Promise<{ success: boolean; data: any; message: string }> {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/content-planning/strategy/${strategyId}/trend-data?time_range=${timeRange}`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error getting trend data:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to get trend data');
|
||||
}
|
||||
},
|
||||
|
||||
// New API calls for transparency data
|
||||
async getTransparencyData(strategyId: number): Promise<{ success: boolean; data: any; message: string }> {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/content-planning/strategy/${strategyId}/transparency-data`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching transparency data:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to fetch transparency data');
|
||||
}
|
||||
},
|
||||
|
||||
async getMonitoringTasks(strategyId: number): Promise<{ success: boolean; data: any; message: string }> {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/content-planning/strategy/${strategyId}/monitoring-tasks`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching monitoring tasks:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to fetch monitoring tasks');
|
||||
}
|
||||
},
|
||||
|
||||
async getDataFreshness(strategyId: number): Promise<{ success: boolean; data: any; message: string }> {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/content-planning/strategy/${strategyId}/data-freshness`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching data freshness:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to fetch data freshness');
|
||||
}
|
||||
},
|
||||
|
||||
// Quality Analysis API methods
|
||||
async getQualityAnalysis(strategyId: number): Promise<{ success: boolean; data: any; message: string }> {
|
||||
try {
|
||||
const response = await apiClient.post(`/api/content-planning/quality-analysis/${strategyId}/analyze`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching quality analysis:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to fetch quality analysis');
|
||||
}
|
||||
},
|
||||
|
||||
async getQualityMetrics(strategyId: number): Promise<{ success: boolean; data: any; message: string }> {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/content-planning/quality-analysis/${strategyId}/metrics`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching quality metrics:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to fetch quality metrics');
|
||||
}
|
||||
},
|
||||
|
||||
async getQualityRecommendations(strategyId: number): Promise<{ success: boolean; data: any; message: string }> {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/content-planning/quality-analysis/${strategyId}/recommendations`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching quality recommendations:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to fetch quality recommendations');
|
||||
}
|
||||
},
|
||||
|
||||
async getQualityDashboard(strategyId: number): Promise<{ success: boolean; data: any; message: string }> {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/content-planning/quality-analysis/${strategyId}/dashboard`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching quality dashboard:', error);
|
||||
throw new Error(error.response?.data?.detail || 'Failed to fetch quality dashboard');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Hook for monitoring plan generation
|
||||
export const useMonitoringPlanGeneration = () => {
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const generatePlan = async (strategyId: number): Promise<MonitoringPlan> => {
|
||||
setIsGenerating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await strategyMonitoringApi.generateMonitoringPlan(strategyId);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { generatePlan, isGenerating, error };
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
812
frontend/src/stores/strategyBuilderStore.ts
Normal file
812
frontend/src/stores/strategyBuilderStore.ts
Normal file
@@ -0,0 +1,812 @@
|
||||
import { create } from 'zustand';
|
||||
import { contentPlanningApi } from '../services/contentPlanningApi';
|
||||
|
||||
// Global flag to prevent multiple simultaneous auto-population calls
|
||||
let isAutoPopulating = false;
|
||||
|
||||
// Enhanced Strategy Types
|
||||
export interface EnhancedStrategy {
|
||||
id: string;
|
||||
user_id: number;
|
||||
name: string;
|
||||
industry: string;
|
||||
|
||||
// Business Context (8 inputs)
|
||||
business_objectives?: any;
|
||||
target_metrics?: any;
|
||||
content_budget?: number;
|
||||
team_size?: number;
|
||||
implementation_timeline?: string;
|
||||
market_share?: string;
|
||||
competitive_position?: string;
|
||||
performance_metrics?: any;
|
||||
|
||||
// Audience Intelligence (6 inputs)
|
||||
content_preferences?: any;
|
||||
consumption_patterns?: any;
|
||||
audience_pain_points?: any;
|
||||
buying_journey?: any;
|
||||
seasonal_trends?: any;
|
||||
engagement_metrics?: any;
|
||||
|
||||
// Competitive Intelligence (5 inputs)
|
||||
top_competitors?: any;
|
||||
competitor_content_strategies?: any;
|
||||
market_gaps?: any;
|
||||
industry_trends?: any;
|
||||
emerging_trends?: any;
|
||||
|
||||
// Content Strategy (7 inputs)
|
||||
preferred_formats?: any;
|
||||
content_mix?: any;
|
||||
content_frequency?: string;
|
||||
optimal_timing?: any;
|
||||
quality_metrics?: any;
|
||||
editorial_guidelines?: any;
|
||||
brand_voice?: any;
|
||||
|
||||
// Performance & Analytics (4 inputs)
|
||||
traffic_sources?: any;
|
||||
conversion_rates?: any;
|
||||
content_roi_targets?: any;
|
||||
ab_testing_capabilities?: boolean;
|
||||
|
||||
// Enhanced AI Analysis
|
||||
comprehensive_ai_analysis?: any;
|
||||
onboarding_data_used?: any;
|
||||
strategic_scores?: any;
|
||||
market_positioning?: any;
|
||||
competitive_advantages?: any;
|
||||
strategic_risks?: any;
|
||||
opportunity_analysis?: any;
|
||||
|
||||
// Metadata
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
completion_percentage: number;
|
||||
data_source_transparency?: any;
|
||||
}
|
||||
|
||||
export interface StrategicInputField {
|
||||
id: string;
|
||||
category: string;
|
||||
label: string;
|
||||
description: string;
|
||||
tooltip: string;
|
||||
type: 'text' | 'number' | 'select' | 'multiselect' | 'json' | 'boolean';
|
||||
required: boolean;
|
||||
options?: string[];
|
||||
placeholder?: string;
|
||||
validation?: any;
|
||||
auto_populated?: boolean;
|
||||
data_source?: string;
|
||||
confidence_level?: number;
|
||||
}
|
||||
|
||||
// Strategy Builder Store Interface
|
||||
interface StrategyBuilderStore {
|
||||
// Strategy State
|
||||
strategies: EnhancedStrategy[];
|
||||
currentStrategy: EnhancedStrategy | null;
|
||||
|
||||
// Form State
|
||||
formData: Record<string, any>;
|
||||
formErrors: Record<string, string>;
|
||||
|
||||
// Auto-Population State
|
||||
autoPopulatedFields: Record<string, any>;
|
||||
dataSources: Record<string, string>;
|
||||
inputDataPoints: Record<string, any>; // Detailed input data points from backend
|
||||
personalizationData: Record<string, any>; // Personalization data for each field
|
||||
confidenceScores: Record<string, number>; // Confidence scores for each field
|
||||
autoPopulationBlocked: boolean;
|
||||
|
||||
// UI State
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
saving: boolean;
|
||||
|
||||
// Strategy Actions
|
||||
createStrategy: (strategy: Partial<EnhancedStrategy>) => Promise<EnhancedStrategy>;
|
||||
updateStrategy: (id: string, updates: Partial<EnhancedStrategy>) => Promise<void>;
|
||||
deleteStrategy: (id: string) => Promise<void>;
|
||||
setCurrentStrategy: (strategy: EnhancedStrategy | null) => void;
|
||||
loadStrategies: () => Promise<void>;
|
||||
|
||||
// Form Actions
|
||||
updateFormField: (fieldId: string, value: any) => void;
|
||||
validateFormField: (fieldId: string) => boolean;
|
||||
validateAllFields: () => boolean;
|
||||
resetForm: () => void;
|
||||
setFormData: (data: Record<string, any>) => void;
|
||||
setFormErrors: (errors: Record<string, string>) => void;
|
||||
|
||||
// Auto-Population Actions
|
||||
autoPopulateFromOnboarding: (forceRefresh?: boolean) => Promise<void>;
|
||||
updateAutoPopulatedField: (fieldId: string, value: any, source: string) => void;
|
||||
overrideAutoPopulatedField: (fieldId: string, value: any) => void;
|
||||
|
||||
// UI Actions
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
setSaving: (saving: boolean) => void;
|
||||
|
||||
// Completion Tracking
|
||||
calculateCompletionPercentage: () => number;
|
||||
getCompletionStats: () => {
|
||||
total_fields: number;
|
||||
filled_fields: number;
|
||||
completion_percentage: number;
|
||||
category_completion: Record<string, number>;
|
||||
};
|
||||
}
|
||||
|
||||
// Strategic input fields configuration
|
||||
export const STRATEGIC_INPUT_FIELDS: StrategicInputField[] = [
|
||||
// Business Context
|
||||
{
|
||||
id: 'business_objectives',
|
||||
category: 'business_context',
|
||||
label: 'Business Objectives',
|
||||
description: 'Primary and secondary business goals for content strategy',
|
||||
tooltip: 'Define your main business goals that content will support. Include both primary objectives (e.g., brand awareness) and secondary objectives (e.g., lead generation).',
|
||||
type: 'json',
|
||||
required: true,
|
||||
placeholder: 'Enter your business objectives'
|
||||
},
|
||||
{
|
||||
id: 'target_metrics',
|
||||
category: 'business_context',
|
||||
label: 'Target Metrics',
|
||||
description: 'KPIs and success metrics for content performance',
|
||||
tooltip: 'Specify the key performance indicators (KPIs) that will measure the success of your content strategy. Include metrics like traffic growth, engagement rates, and conversion rates.',
|
||||
type: 'json',
|
||||
required: true,
|
||||
placeholder: 'Define your target metrics'
|
||||
},
|
||||
{
|
||||
id: 'content_budget',
|
||||
category: 'business_context',
|
||||
label: 'Content Budget',
|
||||
description: 'Monthly or annual budget for content creation',
|
||||
tooltip: 'Set your content marketing budget. This helps determine the scope and scale of your content strategy, including team size, tools, and content production capabilities.',
|
||||
type: 'number',
|
||||
required: false,
|
||||
placeholder: 'Enter your content budget'
|
||||
},
|
||||
{
|
||||
id: 'team_size',
|
||||
category: 'business_context',
|
||||
label: 'Team Size',
|
||||
description: 'Number of people working on content',
|
||||
tooltip: 'Specify the size of your content team. This affects content production capacity and helps determine realistic content frequency and volume.',
|
||||
type: 'number',
|
||||
required: false,
|
||||
placeholder: 'Enter team size'
|
||||
},
|
||||
{
|
||||
id: 'implementation_timeline',
|
||||
category: 'business_context',
|
||||
label: 'Implementation Timeline',
|
||||
description: 'Timeline for strategy implementation',
|
||||
tooltip: 'Define how long you plan to implement this content strategy. Common timelines include 3 months, 6 months, or 1 year.',
|
||||
type: 'select',
|
||||
required: false,
|
||||
options: ['3 months', '6 months', '1 year', '2 years', 'Ongoing']
|
||||
},
|
||||
{
|
||||
id: 'market_share',
|
||||
category: 'business_context',
|
||||
label: 'Market Share',
|
||||
description: 'Current market position and share',
|
||||
tooltip: 'Indicate your current market share and position. This helps tailor content strategy to either defend your position or gain market share.',
|
||||
type: 'text',
|
||||
required: false,
|
||||
placeholder: 'Enter your market share'
|
||||
},
|
||||
{
|
||||
id: 'competitive_position',
|
||||
category: 'business_context',
|
||||
label: 'Competitive Position',
|
||||
description: 'Position relative to competitors',
|
||||
tooltip: 'Describe your competitive position in the market. Are you a leader, challenger, follower, or niche player?',
|
||||
type: 'select',
|
||||
required: false,
|
||||
options: ['Market Leader', 'Challenger', 'Follower', 'Niche Player']
|
||||
},
|
||||
{
|
||||
id: 'performance_metrics',
|
||||
category: 'business_context',
|
||||
label: 'Performance Metrics',
|
||||
description: 'Current performance indicators',
|
||||
tooltip: 'Document your current performance metrics to establish a baseline for measuring strategy success.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Enter current performance metrics'
|
||||
},
|
||||
|
||||
// Audience Intelligence
|
||||
{
|
||||
id: 'content_preferences',
|
||||
category: 'audience_intelligence',
|
||||
label: 'Content Preferences',
|
||||
description: 'Preferred content types and formats',
|
||||
tooltip: 'Identify what types of content your audience prefers. Consider formats like blog posts, videos, infographics, podcasts, etc.',
|
||||
type: 'multiselect',
|
||||
required: true,
|
||||
options: ['Blog Posts', 'Videos', 'Infographics', 'Podcasts', 'Webinars', 'Case Studies', 'Whitepapers', 'Social Media Posts']
|
||||
},
|
||||
{
|
||||
id: 'consumption_patterns',
|
||||
category: 'audience_intelligence',
|
||||
label: 'Consumption Patterns',
|
||||
description: 'How and when audience consumes content',
|
||||
tooltip: 'Understand when and how your audience consumes content. This helps optimize publishing schedules and content formats.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Describe consumption patterns'
|
||||
},
|
||||
{
|
||||
id: 'audience_pain_points',
|
||||
category: 'audience_intelligence',
|
||||
label: 'Audience Pain Points',
|
||||
description: 'Key challenges and problems',
|
||||
tooltip: 'Identify the main challenges and pain points your audience faces. This helps create content that addresses real needs.',
|
||||
type: 'multiselect',
|
||||
required: true,
|
||||
options: ['Lack of Time', 'Information Overload', 'Budget Constraints', 'Technical Complexity', 'Decision Paralysis', 'Quality Concerns']
|
||||
},
|
||||
{
|
||||
id: 'buying_journey',
|
||||
category: 'audience_intelligence',
|
||||
label: 'Buying Journey',
|
||||
description: 'Customer journey stages and touchpoints',
|
||||
tooltip: 'Map out your customer journey stages and identify key touchpoints where content can influence decisions.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Describe buying journey'
|
||||
},
|
||||
{
|
||||
id: 'seasonal_trends',
|
||||
category: 'audience_intelligence',
|
||||
label: 'Seasonal Trends',
|
||||
description: 'Seasonal patterns and trends',
|
||||
tooltip: 'Identify seasonal patterns in your industry or audience behavior that should influence content planning.',
|
||||
type: 'multiselect',
|
||||
required: false,
|
||||
options: ['Q1 Planning', 'Q2 Execution', 'Q3 Optimization', 'Q4 Review', 'Holiday Season', 'Back to School', 'Summer Slowdown']
|
||||
},
|
||||
{
|
||||
id: 'engagement_metrics',
|
||||
category: 'audience_intelligence',
|
||||
label: 'Engagement Metrics',
|
||||
description: 'Current engagement performance',
|
||||
tooltip: 'Document current engagement metrics to understand what content resonates with your audience.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Enter engagement metrics'
|
||||
},
|
||||
|
||||
// Competitive Intelligence
|
||||
{
|
||||
id: 'top_competitors',
|
||||
category: 'competitive_intelligence',
|
||||
label: 'Top Competitors',
|
||||
description: 'Main competitors in your market',
|
||||
tooltip: 'Identify your main competitors and analyze their strengths and weaknesses.',
|
||||
type: 'multiselect',
|
||||
required: true,
|
||||
placeholder: 'Enter competitor names'
|
||||
},
|
||||
{
|
||||
id: 'competitor_content_strategies',
|
||||
category: 'competitive_intelligence',
|
||||
label: 'Competitor Content Strategies',
|
||||
description: 'Content strategies of competitors',
|
||||
tooltip: 'Analyze what content strategies your competitors are using and their effectiveness.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Describe competitor content strategies'
|
||||
},
|
||||
{
|
||||
id: 'market_gaps',
|
||||
category: 'competitive_intelligence',
|
||||
label: 'Market Gaps',
|
||||
description: 'Unfilled market opportunities',
|
||||
tooltip: 'Identify gaps in the market that your content can address.',
|
||||
type: 'multiselect',
|
||||
required: false,
|
||||
options: ['Underserved Audience', 'Content Format Gap', 'Topic Gap', 'Channel Gap', 'Timing Gap']
|
||||
},
|
||||
{
|
||||
id: 'industry_trends',
|
||||
category: 'competitive_intelligence',
|
||||
label: 'Industry Trends',
|
||||
description: 'Current industry trends and patterns',
|
||||
tooltip: 'Stay updated on current trends in your industry that should influence content strategy.',
|
||||
type: 'multiselect',
|
||||
required: false,
|
||||
options: ['Digital Transformation', 'AI Integration', 'Sustainability', 'Remote Work', 'E-commerce Growth', 'Video Content', 'Personalization']
|
||||
},
|
||||
{
|
||||
id: 'emerging_trends',
|
||||
category: 'competitive_intelligence',
|
||||
label: 'Emerging Trends',
|
||||
description: 'New and emerging market trends',
|
||||
tooltip: 'Identify emerging trends that could impact your content strategy in the future.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Describe emerging trends'
|
||||
},
|
||||
|
||||
// Content Strategy
|
||||
{
|
||||
id: 'preferred_formats',
|
||||
category: 'content_strategy',
|
||||
label: 'Preferred Formats',
|
||||
description: 'Content formats to focus on',
|
||||
tooltip: 'Choose the content formats that align with your audience preferences and business goals.',
|
||||
type: 'multiselect',
|
||||
required: true,
|
||||
options: ['Blog Posts', 'Videos', 'Infographics', 'Podcasts', 'Webinars', 'Case Studies', 'Whitepapers', 'Social Media Posts', 'Email Newsletters', 'Interactive Content']
|
||||
},
|
||||
{
|
||||
id: 'content_mix',
|
||||
category: 'content_strategy',
|
||||
label: 'Content Mix',
|
||||
description: 'Distribution of content types',
|
||||
tooltip: 'Define the ideal mix of content types for your strategy (e.g., 40% educational, 30% promotional, 30% entertaining).',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Define content mix percentages'
|
||||
},
|
||||
{
|
||||
id: 'content_frequency',
|
||||
category: 'content_strategy',
|
||||
label: 'Content Frequency',
|
||||
description: 'How often to publish content',
|
||||
tooltip: 'Determine how frequently you will publish content across different channels.',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: ['Daily', '2-3 times per week', 'Weekly', 'Bi-weekly', 'Monthly', 'Quarterly']
|
||||
},
|
||||
{
|
||||
id: 'optimal_timing',
|
||||
category: 'content_strategy',
|
||||
label: 'Optimal Timing',
|
||||
description: 'Best times to publish content',
|
||||
tooltip: 'Identify the optimal times to publish content for maximum engagement.',
|
||||
type: 'multiselect',
|
||||
required: false,
|
||||
options: ['Monday Morning', 'Tuesday Midday', 'Wednesday Afternoon', 'Thursday Evening', 'Friday Morning', 'Weekend']
|
||||
},
|
||||
{
|
||||
id: 'quality_metrics',
|
||||
category: 'content_strategy',
|
||||
label: 'Quality Metrics',
|
||||
description: 'Standards for content quality',
|
||||
tooltip: 'Define the quality standards and metrics for your content.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Define quality metrics'
|
||||
},
|
||||
{
|
||||
id: 'editorial_guidelines',
|
||||
category: 'content_strategy',
|
||||
label: 'Editorial Guidelines',
|
||||
description: 'Content creation guidelines',
|
||||
tooltip: 'Establish editorial guidelines to maintain consistency across all content.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Define editorial guidelines'
|
||||
},
|
||||
{
|
||||
id: 'brand_voice',
|
||||
category: 'content_strategy',
|
||||
label: 'Brand Voice',
|
||||
description: 'Tone and style for content',
|
||||
tooltip: 'Define your brand voice and tone to ensure consistent messaging.',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: ['Professional', 'Casual', 'Friendly', 'Authoritative', 'Humorous', 'Inspirational', 'Educational']
|
||||
},
|
||||
|
||||
// Performance & Analytics
|
||||
{
|
||||
id: 'traffic_sources',
|
||||
category: 'performance_analytics',
|
||||
label: 'Traffic Sources',
|
||||
description: 'Primary sources of website traffic',
|
||||
tooltip: 'Identify your main traffic sources to optimize content distribution.',
|
||||
type: 'multiselect',
|
||||
required: false,
|
||||
options: ['Organic Search', 'Social Media', 'Email Marketing', 'Direct Traffic', 'Referral Traffic', 'Paid Advertising']
|
||||
},
|
||||
{
|
||||
id: 'conversion_rates',
|
||||
category: 'performance_analytics',
|
||||
label: 'Conversion Rates',
|
||||
description: 'Current conversion performance',
|
||||
tooltip: 'Track your current conversion rates to set realistic improvement targets.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Enter conversion rates'
|
||||
},
|
||||
{
|
||||
id: 'content_roi_targets',
|
||||
category: 'performance_analytics',
|
||||
label: 'Content ROI Targets',
|
||||
description: 'Target return on investment for content',
|
||||
tooltip: 'Set realistic ROI targets for your content marketing efforts.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Define ROI targets'
|
||||
},
|
||||
{
|
||||
id: 'ab_testing_capabilities',
|
||||
category: 'performance_analytics',
|
||||
label: 'A/B Testing Capabilities',
|
||||
description: 'Ability to test content variations',
|
||||
tooltip: 'Indicate whether you have the capability to conduct A/B testing on your content.',
|
||||
type: 'boolean',
|
||||
required: false
|
||||
}
|
||||
];
|
||||
|
||||
// Strategy Builder Store Implementation
|
||||
export const useStrategyBuilderStore = create<StrategyBuilderStore>((set, get) => ({
|
||||
// Initial State
|
||||
strategies: [],
|
||||
currentStrategy: null,
|
||||
|
||||
// Form State
|
||||
formData: {},
|
||||
formErrors: {},
|
||||
|
||||
// Auto-Population State
|
||||
autoPopulatedFields: {},
|
||||
dataSources: {},
|
||||
inputDataPoints: {},
|
||||
personalizationData: {},
|
||||
confidenceScores: {},
|
||||
autoPopulationBlocked: false,
|
||||
|
||||
// UI State
|
||||
loading: false,
|
||||
error: null,
|
||||
saving: false,
|
||||
|
||||
// Strategy Actions
|
||||
createStrategy: async (strategy) => {
|
||||
set({ saving: true, error: null });
|
||||
try {
|
||||
const newStrategy = await contentPlanningApi.createEnhancedStrategy(strategy);
|
||||
set((state) => ({
|
||||
strategies: [...state.strategies, newStrategy],
|
||||
saving: false,
|
||||
}));
|
||||
return newStrategy;
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to create strategy', saving: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateStrategy: async (id, updates) => {
|
||||
set({ saving: true, error: null });
|
||||
try {
|
||||
const updatedStrategy = await contentPlanningApi.updateEnhancedStrategy(id, updates);
|
||||
set((state) => ({
|
||||
strategies: state.strategies.map((strategy) =>
|
||||
strategy.id === id ? updatedStrategy : strategy
|
||||
),
|
||||
saving: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to update strategy', saving: false });
|
||||
}
|
||||
},
|
||||
|
||||
deleteStrategy: async (id) => {
|
||||
set({ saving: true, error: null });
|
||||
try {
|
||||
await contentPlanningApi.deleteEnhancedStrategy(id);
|
||||
set((state) => ({
|
||||
strategies: state.strategies.filter((strategy) => strategy.id !== id),
|
||||
saving: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to delete strategy', saving: false });
|
||||
}
|
||||
},
|
||||
|
||||
setCurrentStrategy: (strategy) => {
|
||||
set({ currentStrategy: strategy });
|
||||
},
|
||||
|
||||
loadStrategies: async () => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await contentPlanningApi.getEnhancedStrategies();
|
||||
set({ strategies: response.strategies || [], loading: false });
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to load strategies', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Form Actions
|
||||
updateFormField: (fieldId, value) => {
|
||||
set((state) => ({
|
||||
formData: { ...state.formData, [fieldId]: value },
|
||||
formErrors: { ...state.formErrors, [fieldId]: '' } // Clear error when field is updated
|
||||
}));
|
||||
},
|
||||
|
||||
validateFormField: (fieldId) => {
|
||||
const field = STRATEGIC_INPUT_FIELDS.find(f => f.id === fieldId);
|
||||
if (!field) return true;
|
||||
|
||||
const value = get().formData[fieldId];
|
||||
let isValid = true;
|
||||
let errorMessage = '';
|
||||
|
||||
if (field.required && (!value || (Array.isArray(value) && value.length === 0))) {
|
||||
isValid = false;
|
||||
errorMessage = `${field.label} is required`;
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
formErrors: { ...state.formErrors, [fieldId]: errorMessage }
|
||||
}));
|
||||
|
||||
return isValid;
|
||||
},
|
||||
|
||||
validateAllFields: () => {
|
||||
const formData = get().formData;
|
||||
const errors: Record<string, string> = {};
|
||||
let allValid = true;
|
||||
|
||||
STRATEGIC_INPUT_FIELDS.forEach(field => {
|
||||
const value = formData[field.id];
|
||||
if (field.required && (!value || (Array.isArray(value) && value.length === 0))) {
|
||||
errors[field.id] = `${field.label} is required`;
|
||||
allValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
set({ formErrors: errors });
|
||||
return allValid;
|
||||
},
|
||||
|
||||
resetForm: () => {
|
||||
set({ formData: {}, formErrors: {} });
|
||||
},
|
||||
|
||||
setFormData: (data) => {
|
||||
set({ formData: data });
|
||||
},
|
||||
|
||||
setFormErrors: (errors) => {
|
||||
set({ formErrors: errors });
|
||||
},
|
||||
|
||||
// Auto-Population Actions
|
||||
autoPopulateFromOnboarding: async (forceRefresh: boolean = false) => {
|
||||
// Global protection against multiple simultaneous calls
|
||||
if (isAutoPopulating) {
|
||||
console.log('⏸️ Auto-population skipped - already running globally');
|
||||
return;
|
||||
}
|
||||
|
||||
isAutoPopulating = true;
|
||||
|
||||
try {
|
||||
// Skip if already loading
|
||||
if (get().loading) {
|
||||
console.log('⏸️ Auto-population skipped - already loading');
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if already populated and not forcing refresh
|
||||
if (!forceRefresh && Object.keys(get().autoPopulatedFields).length > 0) {
|
||||
console.log('⏸️ Auto-population skipped - already populated');
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if there was a recent error
|
||||
const lastError = get().error;
|
||||
if (lastError && (lastError.includes('No response from server') || lastError.includes('Too many requests'))) {
|
||||
console.log('⏸️ Auto-population skipped - recent server error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if auto-population is blocked
|
||||
if (get().autoPopulationBlocked) {
|
||||
console.log('⏸️ Auto-population skipped - blocked due to previous errors');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a longer delay to prevent rate limiting
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
set({ loading: true });
|
||||
|
||||
console.log('🔄 Starting auto-population from onboarding data...');
|
||||
|
||||
// Optionally clear backend caches to force fresh values
|
||||
if (forceRefresh) {
|
||||
try {
|
||||
await contentPlanningApi.clearEnhancedCache(1);
|
||||
console.log('♻️ Cleared enhanced strategy cache for fresh onboarding data');
|
||||
} catch (e) {
|
||||
console.warn('Cache clear failed (non-blocking):', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch onboarding data to auto-populate fields
|
||||
const response = await contentPlanningApi.getOnboardingData();
|
||||
console.log('📡 Backend response:', response);
|
||||
|
||||
// Extract field values and sources from the new backend format
|
||||
const fields = response.data?.fields || {};
|
||||
const sources = response.data?.sources || {};
|
||||
const inputDataPoints = response.data?.input_data_points || {};
|
||||
|
||||
console.log('📋 Extracted fields:', fields);
|
||||
console.log('🔗 Data sources:', sources);
|
||||
console.log('📝 Input data points:', inputDataPoints);
|
||||
|
||||
// Transform the fields object to extract values for formData
|
||||
const fieldValues: Record<string, any> = {};
|
||||
const autoPopulatedFields: Record<string, any> = {};
|
||||
const personalizationData: Record<string, any> = {};
|
||||
const confidenceScores: Record<string, number> = {};
|
||||
|
||||
// Check if fields is empty and provide fallback
|
||||
if (Object.keys(fields).length === 0) {
|
||||
console.log('⚠️ No fields found in onboarding data, using default values');
|
||||
|
||||
// Set default values for strategy builder
|
||||
const defaultFields: Record<string, any> = {
|
||||
industry: 'Technology',
|
||||
business_objectives: 'Increase brand awareness and drive sales',
|
||||
target_metrics: { traffic: 10000, conversion_rate: 2.5 },
|
||||
content_budget: 5000,
|
||||
team_size: 3,
|
||||
content_preferences: ['Blog posts', 'Social media', 'Email marketing'],
|
||||
preferred_formats: ['Blog posts', 'Whitepapers', 'Videos'],
|
||||
content_mix: { blog_posts: 40, whitepapers: 20, videos: 15, social_media: 25 }
|
||||
};
|
||||
|
||||
Object.keys(defaultFields).forEach(fieldId => {
|
||||
fieldValues[fieldId] = defaultFields[fieldId];
|
||||
autoPopulatedFields[fieldId] = defaultFields[fieldId];
|
||||
confidenceScores[fieldId] = 0.7; // Medium confidence for defaults
|
||||
console.log(`✅ Set default value for ${fieldId}:`, defaultFields[fieldId]);
|
||||
});
|
||||
} else {
|
||||
// Process actual fields from backend
|
||||
Object.keys(fields).forEach(fieldId => {
|
||||
const fieldData = fields[fieldId];
|
||||
console.log(`🔍 Processing field ${fieldId}:`, fieldData);
|
||||
|
||||
if (fieldData && typeof fieldData === 'object' && 'value' in fieldData) {
|
||||
fieldValues[fieldId] = fieldData.value;
|
||||
autoPopulatedFields[fieldId] = fieldData.value;
|
||||
|
||||
// Extract personalization data if available
|
||||
if (fieldData.personalization_data) {
|
||||
personalizationData[fieldId] = fieldData.personalization_data;
|
||||
console.log(`🎯 Personalization data for ${fieldId}:`, fieldData.personalization_data);
|
||||
}
|
||||
|
||||
// Extract confidence score if available
|
||||
if (fieldData.confidence_score) {
|
||||
confidenceScores[fieldId] = fieldData.confidence_score;
|
||||
console.log(`💯 Confidence score for ${fieldId}:`, fieldData.confidence_score);
|
||||
}
|
||||
|
||||
console.log(`✅ Auto-populated ${fieldId}:`, fieldData.value);
|
||||
} else {
|
||||
console.log(`❌ Skipping ${fieldId} - invalid data structure`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('📝 Final field values:', fieldValues);
|
||||
console.log('🔄 Final auto-populated fields:', autoPopulatedFields);
|
||||
console.log('🎯 Personalization data:', personalizationData);
|
||||
console.log('💯 Confidence scores:', confidenceScores);
|
||||
|
||||
set((state) => ({
|
||||
autoPopulatedFields,
|
||||
dataSources: sources,
|
||||
inputDataPoints,
|
||||
personalizationData,
|
||||
confidenceScores,
|
||||
formData: { ...state.formData, ...fieldValues }
|
||||
}));
|
||||
|
||||
console.log('✅ Auto-population completed successfully');
|
||||
} catch (error: any) {
|
||||
console.error('❌ Auto-population error:', error);
|
||||
const errorMessage = error.message || 'Failed to auto-populate from onboarding';
|
||||
set({
|
||||
error: errorMessage,
|
||||
loading: false
|
||||
});
|
||||
|
||||
// If it's a rate limit error, set a flag to prevent further attempts
|
||||
if (errorMessage.includes('Too many requests') || errorMessage.includes('No response from server')) {
|
||||
set({ autoPopulationBlocked: true });
|
||||
}
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
isAutoPopulating = false; // Reset global flag
|
||||
}
|
||||
},
|
||||
|
||||
updateAutoPopulatedField: (fieldId, value, source) => {
|
||||
set((state) => ({
|
||||
autoPopulatedFields: { ...state.autoPopulatedFields, [fieldId]: value },
|
||||
dataSources: { ...state.dataSources, [fieldId]: source }
|
||||
}));
|
||||
},
|
||||
|
||||
overrideAutoPopulatedField: (fieldId, value) => {
|
||||
set((state) => ({
|
||||
formData: { ...state.formData, [fieldId]: value },
|
||||
autoPopulatedFields: { ...state.autoPopulatedFields, [fieldId]: value }
|
||||
}));
|
||||
},
|
||||
|
||||
// UI Actions
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setError: (error) => set({ error }),
|
||||
setSaving: (saving) => set({ saving }),
|
||||
|
||||
// Completion Tracking
|
||||
calculateCompletionPercentage: () => {
|
||||
const formData = get().formData;
|
||||
const requiredFields = STRATEGIC_INPUT_FIELDS.filter(field => field.required);
|
||||
const filledRequiredFields = requiredFields.filter(field => {
|
||||
const value = formData[field.id];
|
||||
return value && (Array.isArray(value) ? value.length > 0 : true);
|
||||
});
|
||||
|
||||
return requiredFields.length > 0 ? (filledRequiredFields.length / requiredFields.length) * 100 : 0;
|
||||
},
|
||||
|
||||
getCompletionStats: () => {
|
||||
const formData = get().formData;
|
||||
const totalFields = STRATEGIC_INPUT_FIELDS.length;
|
||||
const filledFields = STRATEGIC_INPUT_FIELDS.filter(field => {
|
||||
const value = formData[field.id];
|
||||
return value && (Array.isArray(value) ? value.length > 0 : true);
|
||||
}).length;
|
||||
|
||||
const completionPercentage = totalFields > 0 ? (filledFields / totalFields) * 100 : 0;
|
||||
|
||||
// Calculate completion by category
|
||||
const categoryCompletion: Record<string, number> = {};
|
||||
const categories = Array.from(new Set(STRATEGIC_INPUT_FIELDS.map(field => field.category)));
|
||||
|
||||
categories.forEach(category => {
|
||||
const categoryFields = STRATEGIC_INPUT_FIELDS.filter(field => field.category === category);
|
||||
const filledCategoryFields = categoryFields.filter(field => {
|
||||
const value = formData[field.id];
|
||||
return value && (Array.isArray(value) ? value.length > 0 : true);
|
||||
}).length;
|
||||
|
||||
categoryCompletion[category] = categoryFields.length > 0 ? (filledCategoryFields / categoryFields.length) * 100 : 0;
|
||||
});
|
||||
|
||||
return {
|
||||
total_fields: totalFields,
|
||||
filled_fields: filledFields,
|
||||
completion_percentage: completionPercentage,
|
||||
category_completion: categoryCompletion
|
||||
};
|
||||
}
|
||||
}));
|
||||
@@ -1,7 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
|
||||
export type ReviewStatus = 'not_reviewed' | 'in_review' | 'reviewed';
|
||||
export type ReviewStatus = 'not_reviewed' | 'in_review' | 'reviewed' | 'activated';
|
||||
|
||||
export interface StrategyComponent {
|
||||
id: string;
|
||||
@@ -18,16 +18,20 @@ export interface ReviewState {
|
||||
components: StrategyComponent[];
|
||||
isReviewing: boolean;
|
||||
reviewProgress: number;
|
||||
reviewProcessStarted: boolean;
|
||||
|
||||
// Actions
|
||||
initializeComponents: (components: Omit<StrategyComponent, 'status' | 'reviewedAt' | 'reviewedBy' | 'notes'>[]) => void;
|
||||
startReview: (componentId: string) => void;
|
||||
completeReview: (componentId: string, notes?: string) => void;
|
||||
activateStrategy: () => void;
|
||||
resetReview: (componentId: string) => void;
|
||||
resetAllReviews: () => void;
|
||||
startReviewProcess: () => void;
|
||||
updateReviewProgress: () => void;
|
||||
getReviewProgress: () => number;
|
||||
isAllReviewed: () => boolean;
|
||||
isActivated: () => boolean;
|
||||
getUnreviewedComponents: () => StrategyComponent[];
|
||||
getReviewedComponents: () => StrategyComponent[];
|
||||
}
|
||||
@@ -62,122 +66,192 @@ const STRATEGY_COMPONENTS = [
|
||||
|
||||
export const useStrategyReviewStore = create<ReviewState>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
components: [],
|
||||
isReviewing: false,
|
||||
reviewProgress: 0,
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
components: [],
|
||||
isReviewing: false,
|
||||
reviewProgress: 0,
|
||||
reviewProcessStarted: false,
|
||||
|
||||
// Initialize components with default review status
|
||||
initializeComponents: (components) => {
|
||||
const initializedComponents = components.map(component => ({
|
||||
...component,
|
||||
status: 'not_reviewed' as ReviewStatus
|
||||
}));
|
||||
|
||||
set({ components: initializedComponents });
|
||||
get().updateReviewProgress();
|
||||
},
|
||||
// Initialize components with default review status
|
||||
initializeComponents: (components) => {
|
||||
console.log('🔧 Initializing strategy components:', components.length);
|
||||
const initializedComponents = components.map(component => ({
|
||||
...component,
|
||||
status: 'not_reviewed' as ReviewStatus
|
||||
}));
|
||||
|
||||
set({ components: initializedComponents });
|
||||
get().updateReviewProgress();
|
||||
console.log('🔧 Components initialized, progress:', get().reviewProgress);
|
||||
},
|
||||
|
||||
// Start reviewing a component
|
||||
startReview: (componentId: string) => {
|
||||
set(state => ({
|
||||
isReviewing: true,
|
||||
components: state.components.map(comp =>
|
||||
comp.id === componentId
|
||||
? { ...comp, status: 'in_review' as ReviewStatus }
|
||||
: comp
|
||||
)
|
||||
}));
|
||||
},
|
||||
// Start reviewing a component
|
||||
startReview: (componentId: string) => {
|
||||
set(state => ({
|
||||
isReviewing: true,
|
||||
components: state.components.map(comp =>
|
||||
comp.id === componentId
|
||||
? { ...comp, status: 'in_review' as ReviewStatus }
|
||||
: comp
|
||||
)
|
||||
}));
|
||||
},
|
||||
|
||||
// Complete review for a component
|
||||
completeReview: (componentId: string, notes?: string) => {
|
||||
set(state => ({
|
||||
isReviewing: false,
|
||||
components: state.components.map(comp =>
|
||||
comp.id === componentId
|
||||
? {
|
||||
...comp,
|
||||
status: 'reviewed' as ReviewStatus,
|
||||
reviewedAt: new Date(),
|
||||
reviewedBy: 'current_user', // In real app, get from auth
|
||||
notes
|
||||
}
|
||||
: comp
|
||||
)
|
||||
}));
|
||||
|
||||
get().updateReviewProgress();
|
||||
},
|
||||
// Complete review for a component
|
||||
completeReview: (componentId: string, notes?: string) => {
|
||||
console.log('🔧 Completing review for component:', componentId);
|
||||
set(state => ({
|
||||
isReviewing: false,
|
||||
components: state.components.map(comp =>
|
||||
comp.id === componentId
|
||||
? {
|
||||
...comp,
|
||||
status: 'reviewed' as ReviewStatus,
|
||||
reviewedAt: new Date(),
|
||||
reviewedBy: 'current_user', // In real app, get from auth
|
||||
notes
|
||||
}
|
||||
: comp
|
||||
)
|
||||
}));
|
||||
|
||||
get().updateReviewProgress();
|
||||
console.log('🔧 Review completed, progress:', get().reviewProgress, 'all reviewed:', get().isAllReviewed());
|
||||
},
|
||||
|
||||
// Reset review for a component
|
||||
resetReview: (componentId: string) => {
|
||||
set(state => ({
|
||||
components: state.components.map(comp =>
|
||||
comp.id === componentId
|
||||
? {
|
||||
...comp,
|
||||
status: 'not_reviewed' as ReviewStatus,
|
||||
reviewedAt: undefined,
|
||||
reviewedBy: undefined,
|
||||
notes: undefined
|
||||
}
|
||||
: comp
|
||||
)
|
||||
}));
|
||||
|
||||
get().updateReviewProgress();
|
||||
},
|
||||
// Activate strategy - mark all components as activated
|
||||
activateStrategy: () => {
|
||||
console.log('🔧 Activating strategy - marking all components as activated');
|
||||
set(state => ({
|
||||
components: state.components.map(comp => ({
|
||||
...comp,
|
||||
status: 'activated' as ReviewStatus
|
||||
}))
|
||||
}));
|
||||
|
||||
get().updateReviewProgress();
|
||||
console.log('🔧 Strategy activated, all components now have activated status');
|
||||
},
|
||||
|
||||
// Reset all reviews
|
||||
resetAllReviews: () => {
|
||||
set(state => ({
|
||||
components: state.components.map(comp => ({
|
||||
...comp,
|
||||
status: 'not_reviewed' as ReviewStatus,
|
||||
reviewedAt: undefined,
|
||||
reviewedBy: undefined,
|
||||
notes: undefined
|
||||
}))
|
||||
}));
|
||||
|
||||
get().updateReviewProgress();
|
||||
},
|
||||
// Reset review for a component
|
||||
resetReview: (componentId: string) => {
|
||||
set(state => ({
|
||||
components: state.components.map(comp =>
|
||||
comp.id === componentId
|
||||
? {
|
||||
...comp,
|
||||
status: 'not_reviewed' as ReviewStatus,
|
||||
reviewedAt: undefined,
|
||||
reviewedBy: undefined,
|
||||
notes: undefined
|
||||
}
|
||||
: comp
|
||||
)
|
||||
}));
|
||||
|
||||
get().updateReviewProgress();
|
||||
},
|
||||
|
||||
// Update review progress
|
||||
updateReviewProgress: () => {
|
||||
const { components } = get();
|
||||
const reviewedCount = components.filter(comp => comp.status === 'reviewed').length;
|
||||
const totalCount = components.length;
|
||||
const progress = totalCount > 0 ? (reviewedCount / totalCount) * 100 : 0;
|
||||
|
||||
set({ reviewProgress: progress });
|
||||
},
|
||||
// Reset all reviews
|
||||
resetAllReviews: () => {
|
||||
set(state => ({
|
||||
components: state.components.map(comp => ({
|
||||
...comp,
|
||||
status: 'not_reviewed' as ReviewStatus,
|
||||
reviewedAt: undefined,
|
||||
reviewedBy: undefined,
|
||||
notes: undefined
|
||||
}))
|
||||
}));
|
||||
|
||||
get().updateReviewProgress();
|
||||
},
|
||||
|
||||
// Get review progress percentage
|
||||
getReviewProgress: () => {
|
||||
return get().reviewProgress;
|
||||
},
|
||||
// Start review process
|
||||
startReviewProcess: () => {
|
||||
set({ reviewProcessStarted: true });
|
||||
},
|
||||
|
||||
// Check if all components are reviewed
|
||||
isAllReviewed: () => {
|
||||
const { components } = get();
|
||||
return components.every(comp => comp.status === 'reviewed');
|
||||
},
|
||||
// Update review progress
|
||||
updateReviewProgress: () => {
|
||||
const { components } = get();
|
||||
const reviewedCount = components.filter(comp => comp.status === 'reviewed' || comp.status === 'activated').length;
|
||||
const totalCount = components.length;
|
||||
const progress = totalCount > 0 ? (reviewedCount / totalCount) * 100 : 0;
|
||||
|
||||
console.log('🔧 Updating progress:', { reviewedCount, totalCount, progress, components: components.map(c => ({ id: c.id, status: c.status })) });
|
||||
set({ reviewProgress: progress });
|
||||
},
|
||||
|
||||
// Get unreviewed components
|
||||
getUnreviewedComponents: () => {
|
||||
const { components } = get();
|
||||
return components.filter(comp => comp.status !== 'reviewed');
|
||||
},
|
||||
// Get review progress percentage
|
||||
getReviewProgress: () => {
|
||||
return get().reviewProgress;
|
||||
},
|
||||
|
||||
// Get reviewed components
|
||||
getReviewedComponents: () => {
|
||||
const { components } = get();
|
||||
return components.filter(comp => comp.status === 'reviewed');
|
||||
// Check if all components are reviewed
|
||||
isAllReviewed: () => {
|
||||
const { components } = get();
|
||||
return components.every(comp => comp.status === 'reviewed' || comp.status === 'activated');
|
||||
},
|
||||
|
||||
// Check if strategy is activated
|
||||
isActivated: () => {
|
||||
const { components } = get();
|
||||
return components.every(comp => comp.status === 'activated');
|
||||
},
|
||||
|
||||
// Get unreviewed components
|
||||
getUnreviewedComponents: () => {
|
||||
const { components } = get();
|
||||
return components.filter(comp => comp.status !== 'reviewed' && comp.status !== 'activated');
|
||||
},
|
||||
|
||||
// Get reviewed components
|
||||
getReviewedComponents: () => {
|
||||
const { components } = get();
|
||||
return components.filter(comp => comp.status === 'reviewed' || comp.status === 'activated');
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'strategy-review-persist',
|
||||
partialize: (state: ReviewState) => ({
|
||||
components: state.components,
|
||||
reviewProgress: state.reviewProgress,
|
||||
reviewProcessStarted: state.reviewProcessStarted
|
||||
}),
|
||||
onRehydrateStorage: () => (state: ReviewState | undefined) => {
|
||||
if (state) {
|
||||
console.log('🔧 Rehydrating store state:', {
|
||||
componentsCount: state.components.length,
|
||||
reviewProcessStarted: state.reviewProcessStarted,
|
||||
reviewProgress: state.reviewProgress
|
||||
});
|
||||
|
||||
// Initialize components if they don't exist
|
||||
if (state.components.length === 0) {
|
||||
console.log('🔧 No components found during rehydration, initializing...');
|
||||
const initializedComponents = STRATEGY_COMPONENTS.map(component => ({
|
||||
...component,
|
||||
status: 'not_reviewed' as ReviewStatus
|
||||
}));
|
||||
state.components = initializedComponents;
|
||||
} else {
|
||||
// Convert string dates back to Date objects after rehydration
|
||||
state.components = state.components.map(comp => ({
|
||||
...comp,
|
||||
reviewedAt: comp.reviewedAt ? new Date(comp.reviewedAt) : undefined
|
||||
}));
|
||||
}
|
||||
|
||||
// Recalculate progress when rehydrating from storage
|
||||
state.updateReviewProgress();
|
||||
console.log('🔧 Store rehydrated successfully');
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
{
|
||||
name: 'strategy-review-store',
|
||||
enabled: process.env.NODE_ENV === 'development'
|
||||
@@ -185,5 +259,4 @@ export const useStrategyReviewStore = create<ReviewState>()(
|
||||
)
|
||||
);
|
||||
|
||||
// Initialize components when store is created
|
||||
useStrategyReviewStore.getState().initializeComponents(STRATEGY_COMPONENTS);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user