ALwrity version 0.5.6
This commit is contained in:
@@ -30,12 +30,11 @@ import AnalyticsTab from './tabs/AnalyticsTab';
|
||||
import GapAnalysisTab from './tabs/GapAnalysisTab';
|
||||
import CreateTab from './tabs/CreateTab';
|
||||
import AIInsightsPanel from './components/AIInsightsPanel';
|
||||
import ServiceStatusPanel from './components/ServiceStatusPanel';
|
||||
import SystemStatusIndicator from './components/SystemStatusIndicator';
|
||||
import ProgressIndicator from './components/ProgressIndicator';
|
||||
import { useContentPlanningStore } from '../../stores/contentPlanningStore';
|
||||
import {
|
||||
contentPlanningOrchestrator,
|
||||
ServiceStatus,
|
||||
DashboardData
|
||||
} from '../../services/contentPlanningOrchestrator';
|
||||
import { StrategyCalendarProvider } from '../../contexts/StrategyCalendarContext';
|
||||
@@ -76,7 +75,6 @@ function a11yProps(index: number) {
|
||||
const ContentPlanningDashboard: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [serviceStatuses, setServiceStatuses] = useState<ServiceStatus[]>([]);
|
||||
const [dashboardData, setDashboardData] = useState<DashboardData>({
|
||||
strategies: [],
|
||||
gapAnalyses: [],
|
||||
@@ -89,7 +87,6 @@ const ContentPlanningDashboard: React.FC = () => {
|
||||
aiServices: false
|
||||
}
|
||||
});
|
||||
const [statusPanelExpanded, setStatusPanelExpanded] = useState(false);
|
||||
const [progressExpanded, setProgressExpanded] = useState(true);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -104,10 +101,6 @@ const ContentPlanningDashboard: React.FC = () => {
|
||||
|
||||
// Initialize orchestrator callbacks
|
||||
useEffect(() => {
|
||||
contentPlanningOrchestrator.setProgressCallback((statuses) => {
|
||||
setServiceStatuses(statuses);
|
||||
});
|
||||
|
||||
contentPlanningOrchestrator.setDataUpdateCallback((data) => {
|
||||
setDashboardData(prev => ({ ...prev, ...data }));
|
||||
|
||||
@@ -134,15 +127,15 @@ const ContentPlanningDashboard: React.FC = () => {
|
||||
// Load dashboard data using orchestrator
|
||||
useEffect(() => {
|
||||
const loadDashboardData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
await contentPlanningOrchestrator.loadDashboardData();
|
||||
setLoading(false);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load dashboard data:', error);
|
||||
console.error('Error loading dashboard data:', error);
|
||||
setError(error.message || 'Failed to load dashboard data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
@@ -157,27 +150,10 @@ const ContentPlanningDashboard: React.FC = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleRefreshService = (serviceName: string) => {
|
||||
contentPlanningOrchestrator.refreshService(serviceName);
|
||||
};
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setActiveTab(newValue);
|
||||
};
|
||||
|
||||
const getOverallHealthStatus = () => {
|
||||
const { healthStatus } = dashboardData;
|
||||
if (healthStatus.backend && healthStatus.database && healthStatus.aiServices) {
|
||||
return { status: 'success', text: 'Connected' };
|
||||
} else if (healthStatus.backend && healthStatus.database) {
|
||||
return { status: 'warning', text: 'Connected API & DB' };
|
||||
} else {
|
||||
return { status: 'error', text: 'Disconnected' };
|
||||
}
|
||||
};
|
||||
|
||||
const overallHealth = getOverallHealthStatus();
|
||||
|
||||
const tabs = [
|
||||
{ label: 'CONTENT STRATEGY', icon: <StrategyIcon />, component: <ContentStrategyTab /> },
|
||||
{ label: 'CALENDAR', icon: <CalendarIcon />, component: <CalendarTab /> },
|
||||
@@ -197,12 +173,7 @@ const ContentPlanningDashboard: React.FC = () => {
|
||||
Content Planning Dashboard
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<ServiceStatusPanel
|
||||
serviceStatuses={serviceStatuses}
|
||||
onRefreshService={handleRefreshService}
|
||||
expanded={statusPanelExpanded}
|
||||
onToggleExpanded={() => setStatusPanelExpanded(!statusPanelExpanded)}
|
||||
/>
|
||||
<SystemStatusIndicator />
|
||||
|
||||
{/* AI Insights Button with Badge */}
|
||||
<motion.div
|
||||
@@ -244,43 +215,59 @@ const ContentPlanningDashboard: React.FC = () => {
|
||||
{loading && (
|
||||
<Box sx={{ m: 2 }}>
|
||||
<ProgressIndicator
|
||||
serviceStatuses={serviceStatuses}
|
||||
onRefreshService={handleRefreshService}
|
||||
expanded={progressExpanded}
|
||||
onToggleExpanded={() => setProgressExpanded(!progressExpanded)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', height: 'calc(100vh - 64px)' }}>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
aria-label="content planning tabs"
|
||||
sx={{ px: 2 }}
|
||||
{/* Main Content */}
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
aria-label="content planning tabs"
|
||||
sx={{
|
||||
'& .MuiTab-root': {
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
minHeight: 64,
|
||||
fontSize: '0.875rem'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</Box>
|
||||
}
|
||||
{...a11yProps(index)}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flexGrow: 1, overflow: 'auto' }}>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeTab}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{tabs.map((tab, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</Box>
|
||||
}
|
||||
{...a11yProps(index)}
|
||||
/>
|
||||
<TabPanel key={index} value={activeTab} index={index}>
|
||||
{tab.component}
|
||||
</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{tabs.map((tab, index) => (
|
||||
<TabPanel key={index} value={activeTab} index={index}>
|
||||
{tab.component}
|
||||
</TabPanel>
|
||||
))}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -291,43 +278,22 @@ const ContentPlanningDashboard: React.FC = () => {
|
||||
onClose={() => setAiInsightsDrawerOpen(false)}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: 400,
|
||||
height: '100%',
|
||||
backgroundColor: 'background.paper',
|
||||
borderLeft: '1px solid',
|
||||
borderColor: 'divider'
|
||||
width: { xs: '100%', sm: 400 },
|
||||
maxWidth: '100vw'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<AIInsightsIcon sx={{ mr: 1 }} />
|
||||
AI Insights
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={() => setAiInsightsDrawerOpen(false)}
|
||||
size="small"
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6">AI Insights & Recommendations</Typography>
|
||||
<IconButton onClick={() => setAiInsightsDrawerOpen(false)}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: 1, overflow: 'auto' }}>
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<AIInsightsPanel />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
<AIInsightsPanel />
|
||||
</Drawer>
|
||||
</Container>
|
||||
</Container>
|
||||
</StrategyCalendarProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
import { SxProps, Theme } from '@mui/material/styles';
|
||||
|
||||
/**
|
||||
* Styles for CalendarGenerationModal Component
|
||||
* All styling logic extracted for maintainability and reusability
|
||||
*/
|
||||
|
||||
// Dialog and Layout Styles
|
||||
export const dialogStyles = {
|
||||
paper: {
|
||||
height: '90vh',
|
||||
maxHeight: '90vh'
|
||||
}
|
||||
};
|
||||
|
||||
export const contentContainerStyles: SxProps<Theme> = {
|
||||
p: 2
|
||||
};
|
||||
|
||||
// Progress Bar Styles
|
||||
export const progressBarContainerStyles: SxProps<Theme> = {
|
||||
flexGrow: 1,
|
||||
position: 'relative'
|
||||
};
|
||||
|
||||
export const progressBarStyles: SxProps<Theme> = {
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'grey.200',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
borderRadius: 4,
|
||||
background: 'linear-gradient(90deg, #1976d2 0%, #42a5f5 100%)',
|
||||
transition: 'transform 0.8s ease-in-out'
|
||||
}
|
||||
};
|
||||
|
||||
export const stepProgressBarStyles: SxProps<Theme> = {
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: 'grey.200',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
borderRadius: 5,
|
||||
background: 'linear-gradient(90deg, #1976d2 0%, #42a5f5 100%)',
|
||||
transition: 'transform 0.6s ease-in-out'
|
||||
}
|
||||
};
|
||||
|
||||
// Step Indicator Styles
|
||||
export const getStepIndicatorStyles = (currentStep: number, step: number): SxProps<Theme> => ({
|
||||
p: 1,
|
||||
borderRadius: 1,
|
||||
backgroundColor: currentStep === step ? 'primary.light' : 'grey.100',
|
||||
color: currentStep === step ? 'primary.contrastText' : 'text.secondary',
|
||||
transition: 'all 0.3s ease',
|
||||
cursor: 'pointer'
|
||||
});
|
||||
|
||||
// Step Card Styles
|
||||
export const getStepCardStyles = (currentStep: number, step: number): SxProps<Theme> => ({
|
||||
p: 2,
|
||||
backgroundColor: currentStep === step ? 'primary.light' : 'grey.50',
|
||||
borderColor: currentStep === step ? 'primary.main' : 'grey.300',
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: '-100%',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: currentStep === step
|
||||
? 'linear-gradient(90deg, transparent, rgba(25, 118, 210, 0.1), transparent)'
|
||||
: 'none',
|
||||
transition: 'left 0.6s ease-in-out'
|
||||
},
|
||||
'&:hover::before': {
|
||||
left: '100%'
|
||||
}
|
||||
});
|
||||
|
||||
// Step Circle Styles
|
||||
export const stepCircleBaseStyles = {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: '50%',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 'bold'
|
||||
};
|
||||
|
||||
export const getStepCircleColor = (currentStep: number, step: number): string => {
|
||||
if (currentStep > step) return '#4caf50';
|
||||
if (currentStep === step) return '#1976d2';
|
||||
return '#9e9e9e';
|
||||
};
|
||||
|
||||
// Tab Button Styles
|
||||
export const tabButtonStyles: SxProps<Theme> = {
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: '-100%',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent)',
|
||||
transition: 'left 0.5s'
|
||||
},
|
||||
'&:hover::before': {
|
||||
left: '100%'
|
||||
}
|
||||
};
|
||||
|
||||
// Activity Indicator Styles
|
||||
export const activityIndicatorStyles = {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#1976d2',
|
||||
marginTop: 8,
|
||||
flexShrink: 0
|
||||
};
|
||||
|
||||
// Quality Score Styles
|
||||
export const qualityScoreContainerStyles: SxProps<Theme> = {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative'
|
||||
};
|
||||
|
||||
export const getQualityScoreBackground = (score: number): string => {
|
||||
const color = score >= 0.9 ? '#4caf50' : score >= 0.8 ? '#ff9800' : '#f44336';
|
||||
return `conic-gradient(${color} 0deg, ${score * 360}deg, #e0e0e0 ${score * 360}deg, 360deg)`;
|
||||
};
|
||||
|
||||
export const qualityScoreInnerStyles: SxProps<Theme> = {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '1.2rem'
|
||||
};
|
||||
|
||||
// Data Source Card Styles
|
||||
export const dataSourceCardStyles: SxProps<Theme> = {
|
||||
p: 2
|
||||
};
|
||||
|
||||
export const dataSourceIconStyles = {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: '50%',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '0.875rem'
|
||||
};
|
||||
|
||||
export const getDataSourceIconColor = (type: string): string => {
|
||||
switch (type) {
|
||||
case 'strategy':
|
||||
return '#4caf50'; // success.main
|
||||
case 'onboarding':
|
||||
return '#2196f3'; // info.main
|
||||
case 'ai':
|
||||
return '#1976d2'; // primary.main
|
||||
case 'performance':
|
||||
return '#9c27b0'; // secondary.main
|
||||
default:
|
||||
return '#757575'; // grey
|
||||
}
|
||||
};
|
||||
|
||||
// Quality Metrics Styles
|
||||
export const qualityMetricsContainerStyles: SxProps<Theme> = {
|
||||
textAlign: 'center',
|
||||
p: 2
|
||||
};
|
||||
|
||||
export const getMetricColor = (label: string): string => {
|
||||
switch (label) {
|
||||
case 'Overall Data Quality':
|
||||
return '#4caf50'; // success.main
|
||||
case 'Data Completeness':
|
||||
return '#2196f3'; // info.main
|
||||
case 'Data Freshness':
|
||||
return '#1976d2'; // primary.main
|
||||
default:
|
||||
return '#757575';
|
||||
}
|
||||
};
|
||||
|
||||
// Step Results Styles
|
||||
export const stepResultsCardStyles: SxProps<Theme> = {
|
||||
p: 2
|
||||
};
|
||||
|
||||
export const stepResultsHeaderStyles: SxProps<Theme> = {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: 'bold'
|
||||
};
|
||||
|
||||
export const stepResultsContentStyles: SxProps<Theme> = {
|
||||
backgroundColor: 'grey.50',
|
||||
p: 2,
|
||||
borderRadius: 1
|
||||
};
|
||||
|
||||
// Loading State Styles
|
||||
export const loadingContainerStyles: SxProps<Theme> = {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '400px'
|
||||
};
|
||||
|
||||
export const loadingContentStyles: SxProps<Theme> = {
|
||||
textAlign: 'center'
|
||||
};
|
||||
|
||||
// Animation Constants
|
||||
export const animationDurations = {
|
||||
fast: 0.3,
|
||||
medium: 0.5,
|
||||
slow: 0.8,
|
||||
extraSlow: 1.0
|
||||
};
|
||||
|
||||
export const animationEasing = {
|
||||
easeOut: "easeOut" as const,
|
||||
easeInOut: "easeInOut" as const,
|
||||
linear: "linear" as const
|
||||
};
|
||||
|
||||
export const springConfig = {
|
||||
type: "spring" as const,
|
||||
stiffness: 200,
|
||||
damping: 10
|
||||
};
|
||||
|
||||
export const staggerDelay = 0.1;
|
||||
export const cardStaggerDelay = 0.2;
|
||||
|
||||
// Motion Variants
|
||||
export const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
animate: { opacity: 1, y: 0 },
|
||||
exit: { opacity: 0, y: -20 }
|
||||
};
|
||||
|
||||
export const fadeInLeft = {
|
||||
initial: { opacity: 0, x: -20, scale: 0.95 },
|
||||
animate: { opacity: 1, x: 0, scale: 1 },
|
||||
exit: { opacity: 0, x: 50, scale: 0.95 }
|
||||
};
|
||||
|
||||
export const scaleIn = {
|
||||
initial: { opacity: 0, scale: 0.8 },
|
||||
animate: { opacity: 1, scale: 1 },
|
||||
exit: { opacity: 0, scale: 0.8 }
|
||||
};
|
||||
|
||||
export const slideInStaggered = {
|
||||
initial: { opacity: 0, y: 50, scale: 0.8 },
|
||||
animate: { opacity: 1, y: 0, scale: 1 },
|
||||
exit: { opacity: 0, y: -50, scale: 0.8 }
|
||||
};
|
||||
|
||||
// Hover and Interaction Styles
|
||||
export const hoverLift = {
|
||||
scale: 1.02,
|
||||
y: -5
|
||||
};
|
||||
|
||||
export const hoverScale = {
|
||||
scale: 1.05
|
||||
};
|
||||
|
||||
export const tapScale = {
|
||||
scale: 0.95
|
||||
};
|
||||
|
||||
// Pulse Animation Config
|
||||
export const pulseAnimation = {
|
||||
scale: [1, 1.1, 1],
|
||||
boxShadow: [
|
||||
"0 0 0 0 rgba(76, 175, 80, 0.4)",
|
||||
"0 0 0 10px rgba(76, 175, 80, 0)",
|
||||
"0 0 0 0 rgba(76, 175, 80, 0)"
|
||||
]
|
||||
};
|
||||
|
||||
export const smallPulseAnimation = {
|
||||
scale: [1, 1.1, 1],
|
||||
boxShadow: [
|
||||
"0 0 0 0 rgba(76, 175, 80, 0.4)",
|
||||
"0 0 0 6px rgba(76, 175, 80, 0)",
|
||||
"0 0 0 0 rgba(76, 175, 80, 0)"
|
||||
]
|
||||
};
|
||||
|
||||
// Color Animation Config
|
||||
export const colorPulseAnimation = {
|
||||
scale: [1, 1.2, 1],
|
||||
backgroundColor: ['#1976d2', '#42a5f5', '#1976d2']
|
||||
};
|
||||
|
||||
// Progress Animation Config
|
||||
export const progressFillAnimation = {
|
||||
initial: { scaleX: 0 },
|
||||
animate: (progress: number) => ({ scaleX: progress / 100 }),
|
||||
transition: { duration: animationDurations.slow, ease: animationEasing.easeOut }
|
||||
};
|
||||
|
||||
export const progressOverlayStyles = {
|
||||
position: 'absolute' as const,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'linear-gradient(90deg, #4caf50 0%, #8bc34a 100%)',
|
||||
borderRadius: 4,
|
||||
transformOrigin: 'left' as const
|
||||
};
|
||||
|
||||
export const stepProgressOverlayStyles = {
|
||||
position: 'absolute' as const,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'linear-gradient(90deg, #4caf50 0%, #8bc34a 100%)',
|
||||
borderRadius: 5,
|
||||
transformOrigin: 'left' as const
|
||||
};
|
||||
@@ -0,0 +1,724 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Grid,
|
||||
Paper,
|
||||
LinearProgress,
|
||||
Chip,
|
||||
IconButton,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
Card
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Close as CloseIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Error as ErrorIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
School as SchoolIcon,
|
||||
DataUsage as DataUsageIcon,
|
||||
ViewModule as ViewModuleIcon,
|
||||
Devices as DevicesIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
// Import existing components for reuse
|
||||
import DataSourceTransparency from '../DataSourceTransparency';
|
||||
import ProgressIndicator from '../ProgressIndicator';
|
||||
|
||||
// Import panel components
|
||||
import {
|
||||
LiveProgressPanel,
|
||||
QualityGatesPanel,
|
||||
DataSourcePanel,
|
||||
StepResultsPanel,
|
||||
EducationalPanel,
|
||||
useCalendarGenerationPolling,
|
||||
type CalendarGenerationProgress,
|
||||
type QualityScores
|
||||
} from './calendarGenerationModalPanels';
|
||||
|
||||
// Import styles
|
||||
import {
|
||||
dialogStyles,
|
||||
contentContainerStyles,
|
||||
progressBarContainerStyles,
|
||||
progressBarStyles,
|
||||
stepProgressBarStyles,
|
||||
getStepIndicatorStyles,
|
||||
getStepCardStyles,
|
||||
stepCircleBaseStyles,
|
||||
getStepCircleColor,
|
||||
tabButtonStyles,
|
||||
activityIndicatorStyles,
|
||||
qualityScoreContainerStyles,
|
||||
getQualityScoreBackground,
|
||||
qualityScoreInnerStyles,
|
||||
dataSourceCardStyles,
|
||||
dataSourceIconStyles,
|
||||
getDataSourceIconColor,
|
||||
qualityMetricsContainerStyles,
|
||||
getMetricColor,
|
||||
stepResultsCardStyles,
|
||||
stepResultsHeaderStyles,
|
||||
stepResultsContentStyles,
|
||||
loadingContainerStyles,
|
||||
loadingContentStyles,
|
||||
animationDurations,
|
||||
animationEasing,
|
||||
springConfig,
|
||||
staggerDelay,
|
||||
cardStaggerDelay,
|
||||
fadeInUp,
|
||||
fadeInLeft,
|
||||
scaleIn,
|
||||
slideInStaggered,
|
||||
hoverLift,
|
||||
hoverScale,
|
||||
tapScale,
|
||||
pulseAnimation,
|
||||
smallPulseAnimation,
|
||||
colorPulseAnimation,
|
||||
progressFillAnimation,
|
||||
progressOverlayStyles,
|
||||
stepProgressOverlayStyles
|
||||
} from './CalendarGenerationModal.styles';
|
||||
|
||||
// Types
|
||||
interface CalendarGenerationModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
sessionId: string;
|
||||
initialConfig: CalendarConfig;
|
||||
onComplete: (results: CalendarGenerationResults) => void;
|
||||
onError: (error: string) => void;
|
||||
}
|
||||
|
||||
interface CalendarConfig {
|
||||
userId: string;
|
||||
strategyId: string;
|
||||
calendarType: 'monthly' | 'quarterly' | 'yearly';
|
||||
platforms: string[];
|
||||
duration: number;
|
||||
postingFrequency: 'daily' | 'weekly' | 'biweekly';
|
||||
}
|
||||
|
||||
interface CalendarGenerationResults {
|
||||
calendar: CalendarData;
|
||||
qualityScores: QualityScores;
|
||||
insights: GenerationInsights;
|
||||
recommendations: Recommendations;
|
||||
exportData: ExportData;
|
||||
}
|
||||
|
||||
interface CalendarData {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
content: CalendarContent[];
|
||||
themes: Theme[];
|
||||
platforms: Platform[];
|
||||
}
|
||||
|
||||
interface CalendarContent {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
contentType: string;
|
||||
platform: string;
|
||||
scheduledDate: string;
|
||||
theme: string;
|
||||
keywords: string[];
|
||||
}
|
||||
|
||||
interface Theme {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
weekNumber: number;
|
||||
contentTypes: string[];
|
||||
}
|
||||
|
||||
interface Platform {
|
||||
id: string;
|
||||
name: string;
|
||||
contentCount: number;
|
||||
postingSchedule: PostingSchedule[];
|
||||
}
|
||||
|
||||
interface PostingSchedule {
|
||||
day: string;
|
||||
time: string;
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
// QualityScores type imported from panels
|
||||
|
||||
interface GenerationInsights {
|
||||
contentGaps: ContentGap[];
|
||||
keywordOpportunities: KeywordOpportunity[];
|
||||
audienceInsights: AudienceInsight[];
|
||||
platformPerformance: PlatformPerformance[];
|
||||
}
|
||||
|
||||
interface ContentGap {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
impact: number;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
estimatedTraffic: number;
|
||||
}
|
||||
|
||||
interface KeywordOpportunity {
|
||||
id: string;
|
||||
keyword: string;
|
||||
searchVolume: number;
|
||||
competition: number;
|
||||
relevance: number;
|
||||
estimatedTraffic: number;
|
||||
}
|
||||
|
||||
interface AudienceInsight {
|
||||
id: string;
|
||||
segment: string;
|
||||
demographics: string[];
|
||||
preferences: string[];
|
||||
engagementRate: number;
|
||||
bestTimes: string[];
|
||||
}
|
||||
|
||||
interface PlatformPerformance {
|
||||
id: string;
|
||||
platform: string;
|
||||
engagementRate: number;
|
||||
reach: number;
|
||||
conversionRate: number;
|
||||
bestContentTypes: string[];
|
||||
}
|
||||
|
||||
interface Recommendations {
|
||||
contentMix: ContentMixRecommendation;
|
||||
postingSchedule: PostingScheduleRecommendation;
|
||||
platformStrategy: PlatformStrategyRecommendation;
|
||||
optimizationTips: string[];
|
||||
}
|
||||
|
||||
interface ContentMixRecommendation {
|
||||
educational: number;
|
||||
thoughtLeadership: number;
|
||||
engagement: number;
|
||||
promotional: number;
|
||||
reasoning: string;
|
||||
}
|
||||
|
||||
interface PostingScheduleRecommendation {
|
||||
bestDays: string[];
|
||||
bestTimes: string[];
|
||||
frequency: string;
|
||||
reasoning: string;
|
||||
}
|
||||
|
||||
interface PlatformStrategyRecommendation {
|
||||
primaryPlatforms: string[];
|
||||
contentDistribution: Record<string, number>;
|
||||
crossPlatformStrategy: string;
|
||||
}
|
||||
|
||||
interface ExportData {
|
||||
calendarJson: string;
|
||||
insightsCsv: string;
|
||||
recommendationsPdf: string;
|
||||
qualityReport: string;
|
||||
}
|
||||
|
||||
// Polling hook imported from panels
|
||||
|
||||
// Types imported from panels
|
||||
|
||||
// Remove mock data completely - no fallback
|
||||
// const mockProgressData: CalendarGenerationProgress = { ... };
|
||||
|
||||
const CalendarGenerationModal: React.FC<CalendarGenerationModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
sessionId,
|
||||
initialConfig,
|
||||
onComplete,
|
||||
onError
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [educationalPanelExpanded, setEducationalPanelExpanded] = useState(false);
|
||||
const [expandedSections, setExpandedSections] = useState({
|
||||
dataSources: true,
|
||||
progress: true,
|
||||
educational: false,
|
||||
messages: true,
|
||||
stepResults: true
|
||||
});
|
||||
|
||||
// Use polling hook for real backend data only
|
||||
const { progress, isPolling, error, startPolling, stopPolling } = useCalendarGenerationPolling(sessionId);
|
||||
|
||||
// Use only real progress data - no fallback to mock data
|
||||
const currentProgress = progress;
|
||||
|
||||
useEffect(() => {
|
||||
if (open && sessionId) {
|
||||
// Start real polling when modal opens
|
||||
startPolling();
|
||||
} else if (!open) {
|
||||
stopPolling();
|
||||
}
|
||||
}, [open, sessionId, startPolling, stopPolling]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentProgress?.status === 'completed') {
|
||||
// Handle completion
|
||||
console.log('Calendar generation completed');
|
||||
} else if (currentProgress?.status === 'error') {
|
||||
onError(currentProgress.errors[0]?.message || 'Unknown error');
|
||||
}
|
||||
}, [currentProgress?.status, currentProgress?.errors, onError]);
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setActiveTab(newValue);
|
||||
};
|
||||
|
||||
const getQualityColor = (score: number) => {
|
||||
if (score >= 0.9) return 'success';
|
||||
if (score >= 0.8) return 'warning';
|
||||
return 'error';
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 'success';
|
||||
case 'error':
|
||||
return 'error';
|
||||
case 'initializing':
|
||||
return 'info';
|
||||
default:
|
||||
return 'primary';
|
||||
}
|
||||
};
|
||||
|
||||
const getStepIcon = (stepNumber: number) => {
|
||||
switch (stepNumber) {
|
||||
case 1:
|
||||
return <SchoolIcon />;
|
||||
case 2:
|
||||
return <DataUsageIcon />;
|
||||
case 3:
|
||||
return <TrendingUpIcon />;
|
||||
case 4:
|
||||
return <ScheduleIcon />;
|
||||
case 5:
|
||||
return <ViewModuleIcon />;
|
||||
case 6:
|
||||
return <DevicesIcon />;
|
||||
default:
|
||||
return <ScheduleIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="xl"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: dialogStyles.paper
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Typography variant="h5">
|
||||
Calendar Generation Progress
|
||||
</Typography>
|
||||
<IconButton onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
{!currentProgress ? (
|
||||
// Loading state when no progress data is available
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
>
|
||||
<Box sx={loadingContainerStyles}>
|
||||
<Box sx={loadingContentStyles}>
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
|
||||
>
|
||||
<CircularProgress size={60} />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3, duration: 0.5 }}
|
||||
>
|
||||
<Typography variant="h6" sx={{ mt: 2 }}>
|
||||
Initializing Calendar Generation...
|
||||
</Typography>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5, duration: 0.5 }}
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Please wait while we set up your generation session
|
||||
</Typography>
|
||||
</motion.div>
|
||||
</Box>
|
||||
</Box>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
>
|
||||
<Grid container spacing={2}>
|
||||
{/* Header Section with Enhanced Animations */}
|
||||
<Grid item xs={12}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
>
|
||||
<Paper elevation={1} sx={{ p: 2, mb: 2 }}>
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
{/* Progress Bar with Animation */}
|
||||
<Grid item xs={12}>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Overall Progress
|
||||
</Typography>
|
||||
<Box sx={progressBarContainerStyles}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={currentProgress.overallProgress}
|
||||
sx={progressBarStyles}
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ scaleX: 0 }}
|
||||
animate={{ scaleX: currentProgress.overallProgress / 100 }}
|
||||
transition={{ duration: animationDurations.slow, ease: animationEasing.easeOut }}
|
||||
style={progressOverlayStyles}
|
||||
/>
|
||||
</Box>
|
||||
<motion.div
|
||||
key={currentProgress.overallProgress}
|
||||
initial={{ scale: 1.2, color: '#1976d2' }}
|
||||
animate={{ scale: 1, color: 'inherit' }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{Math.round(currentProgress.overallProgress)}%
|
||||
</Typography>
|
||||
</motion.div>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Step Indicators with Staggered Animation */}
|
||||
<Grid item xs={12}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
{[1, 2, 3, 4, 5, 6].map((step, index) => (
|
||||
<motion.div
|
||||
key={step}
|
||||
initial={{ opacity: 0, x: -20, scale: 0.8 }}
|
||||
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||
transition={{
|
||||
delay: index * 0.2,
|
||||
duration: 0.5,
|
||||
ease: "easeOut"
|
||||
}}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
sx={getStepIndicatorStyles(currentProgress.currentStep, step)}
|
||||
>
|
||||
<motion.div
|
||||
animate={{
|
||||
rotate: currentProgress.currentStep === step ? [0, 10, -10, 0] : 0,
|
||||
scale: currentProgress.currentStep === step ? 1.1 : 1
|
||||
}}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{getStepIcon(step)}
|
||||
</motion.div>
|
||||
<Typography variant="body2">
|
||||
Step {step}
|
||||
</Typography>
|
||||
{currentProgress.qualityScores[`step${step}` as keyof QualityScores] > 0 && (
|
||||
<motion.div
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ delay: 0.5, type: "spring", stiffness: 200 }}
|
||||
>
|
||||
<Chip
|
||||
label={`${Math.round(currentProgress.qualityScores[`step${step}` as keyof QualityScores] * 100)}%`}
|
||||
size="small"
|
||||
color={getQualityColor(currentProgress.qualityScores[`step${step}` as keyof QualityScores])}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</Box>
|
||||
</motion.div>
|
||||
))}
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Quality Score and Status with Pulse Animation */}
|
||||
<Grid item xs={6}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Overall Quality:
|
||||
</Typography>
|
||||
<motion.div
|
||||
animate={pulseAnimation}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: animationEasing.easeInOut
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
label={`${Math.round(currentProgress.qualityScores.overall * 100)}%`}
|
||||
color={getQualityColor(currentProgress.qualityScores.overall)}
|
||||
size="small"
|
||||
/>
|
||||
</motion.div>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Status:
|
||||
</Typography>
|
||||
<motion.div
|
||||
key={currentProgress.status}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Chip
|
||||
label={currentProgress.status}
|
||||
color={getStatusColor(currentProgress.status)}
|
||||
size="small"
|
||||
/>
|
||||
</motion.div>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<Grid item xs={12}>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
{/* Tabs with Enhanced Animations */}
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
||||
<Grid container spacing={1}>
|
||||
{[
|
||||
{ id: 0, label: 'Live Progress' },
|
||||
{ id: 1, label: 'Step Results' },
|
||||
{ id: 2, label: 'Data Sources' },
|
||||
{ id: 3, label: 'Quality Gates' }
|
||||
].map((tab, index) => (
|
||||
<Grid item key={tab.id}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1, duration: 0.3 }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Button
|
||||
variant={activeTab === tab.id ? 'contained' : 'outlined'}
|
||||
size="small"
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
sx={tabButtonStyles}
|
||||
>
|
||||
{tab.label}
|
||||
</Button>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* Tab Content with Enhanced Transitions */}
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab === 0 && (
|
||||
<motion.div
|
||||
key="live-progress"
|
||||
{...fadeInLeft}
|
||||
transition={{ duration: 0.4, ease: animationEasing.easeInOut }}
|
||||
>
|
||||
<LiveProgressPanel
|
||||
progress={currentProgress}
|
||||
isPolling={isPolling}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{activeTab === 1 && (
|
||||
<motion.div
|
||||
key="step-results"
|
||||
{...fadeInLeft}
|
||||
transition={{ duration: 0.4, ease: animationEasing.easeInOut }}
|
||||
>
|
||||
<StepResultsPanel
|
||||
stepResults={currentProgress.stepResults}
|
||||
qualityScores={currentProgress.qualityScores}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{activeTab === 2 && (
|
||||
<motion.div
|
||||
key="data-sources"
|
||||
{...fadeInLeft}
|
||||
transition={{ duration: 0.4, ease: animationEasing.easeInOut }}
|
||||
>
|
||||
<DataSourcePanel
|
||||
currentStep={currentProgress.currentStep}
|
||||
stepResults={currentProgress.stepResults}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{activeTab === 3 && (
|
||||
<motion.div
|
||||
key="quality-gates"
|
||||
{...fadeInLeft}
|
||||
transition={{ duration: 0.4, ease: animationEasing.easeInOut }}
|
||||
>
|
||||
<QualityGatesPanel
|
||||
qualityScores={currentProgress.qualityScores}
|
||||
stepResults={currentProgress.stepResults}
|
||||
currentStep={currentProgress.currentStep}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Educational Panel with Animation */}
|
||||
<Grid item xs={12}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.8, duration: 0.5 }}
|
||||
>
|
||||
<EducationalPanel
|
||||
content={currentProgress.educationalContent}
|
||||
currentStep={currentProgress.currentStep}
|
||||
isExpanded={educationalPanelExpanded}
|
||||
onToggleExpanded={() => setEducationalPanelExpanded(!educationalPanelExpanded)}
|
||||
/>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</motion.div>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Box display="flex" gap={1}>
|
||||
{currentProgress && currentProgress.status !== 'completed' && currentProgress.status !== 'error' && (
|
||||
<motion.div
|
||||
whileHover={hoverScale}
|
||||
whileTap={tapScale}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await fetch(`/api/content-planning/calendar-generation/cancel/${sessionId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error cancelling generation:', error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cancel Generation
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</motion.div>
|
||||
{currentProgress && currentProgress.status === 'completed' && (
|
||||
<motion.div
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
whileHover={hoverScale}
|
||||
whileTap={tapScale}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
// Handle completion
|
||||
console.log('Calendar generation completed');
|
||||
onComplete({
|
||||
calendar: {} as any,
|
||||
qualityScores: currentProgress.qualityScores,
|
||||
insights: {} as any,
|
||||
recommendations: {} as any,
|
||||
exportData: {} as any
|
||||
});
|
||||
}}
|
||||
>
|
||||
View Calendar
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</Box>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
// Components imported from panels
|
||||
|
||||
export default CalendarGenerationModal;
|
||||
@@ -0,0 +1,257 @@
|
||||
# Calendar Generation Modal
|
||||
|
||||
## Overview
|
||||
|
||||
The CalendarGenerationModal is a specialized transparency modal for the 12-step calendar generation process. It provides real-time progress tracking, educational content, and detailed insights for each step of the calendar generation workflow.
|
||||
|
||||
## Features
|
||||
|
||||
### ✅ Implemented (Phase 1A - Modal Foundation)
|
||||
|
||||
1. **Core Modal Structure**
|
||||
- Full-screen dialog with proper Material-UI integration
|
||||
- Responsive design with proper sizing and layout
|
||||
- Close functionality and action buttons
|
||||
|
||||
2. **Progress Tracking**
|
||||
- Overall progress bar with percentage display
|
||||
- Step indicators for Phase 1 (Steps 1-3)
|
||||
- Quality score chips with color coding
|
||||
- Status indicators (initializing, step1, step2, step3, completed, error)
|
||||
|
||||
3. **Tabbed Interface**
|
||||
- Live Progress tab
|
||||
- Step Results tab
|
||||
- Data Sources tab
|
||||
- Quality Gates tab
|
||||
- Smooth animations with Framer Motion
|
||||
|
||||
4. **Educational Panel**
|
||||
- Expandable educational content section
|
||||
- Step-specific tips and examples
|
||||
- User-friendly toggle interface
|
||||
|
||||
5. **Polling Integration**
|
||||
- Custom hook for real-time progress updates
|
||||
- Error handling and retry logic
|
||||
- Configurable polling intervals
|
||||
|
||||
6. **Mock Data Support**
|
||||
- Development-ready mock data for Phase 1
|
||||
- Realistic progress simulation
|
||||
- Quality scores and educational content
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
CalendarGenerationModal/
|
||||
├── index.ts # Main exports
|
||||
├── types.ts # TypeScript interfaces
|
||||
├── CalendarGenerationModal.tsx # Main modal component
|
||||
├── TestModal.tsx # Test component
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```tsx
|
||||
import { CalendarGenerationModal } from './CalendarGenerationModal';
|
||||
|
||||
const MyComponent = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const handleComplete = (results) => {
|
||||
console.log('Calendar generation completed:', results);
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleError = (error) => {
|
||||
console.error('Calendar generation error:', error);
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<CalendarGenerationModal
|
||||
open={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
sessionId="session-123"
|
||||
initialConfig={{
|
||||
userId: 'user123',
|
||||
strategyId: 'strategy456',
|
||||
calendarType: 'monthly',
|
||||
platforms: ['LinkedIn', 'Twitter', 'Website'],
|
||||
duration: 30,
|
||||
postingFrequency: 'daily'
|
||||
}}
|
||||
onComplete={handleComplete}
|
||||
onError={handleError}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
Use the TestModal component to verify functionality:
|
||||
|
||||
```tsx
|
||||
import { TestCalendarGenerationModal } from './CalendarGenerationModal';
|
||||
|
||||
// Add to your app for testing
|
||||
<TestCalendarGenerationModal />
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### CalendarConfig Interface
|
||||
|
||||
```typescript
|
||||
interface CalendarConfig {
|
||||
userId: string;
|
||||
strategyId: string;
|
||||
calendarType: 'monthly' | 'quarterly' | 'yearly';
|
||||
platforms: string[];
|
||||
duration: number;
|
||||
postingFrequency: 'daily' | 'weekly' | 'biweekly';
|
||||
}
|
||||
```
|
||||
|
||||
### Polling Configuration
|
||||
|
||||
The modal uses a polling mechanism with the following defaults:
|
||||
- Polling interval: 2 seconds
|
||||
- Retry interval: 5 seconds (on error)
|
||||
- Endpoint: `/api/calendar-generation/progress/${sessionId}`
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Backend Integration
|
||||
|
||||
The modal expects the following API endpoints:
|
||||
|
||||
1. **Progress Endpoint**: `GET /api/calendar-generation/progress/{sessionId}`
|
||||
- Returns real-time progress data
|
||||
- Includes step results, quality scores, and educational content
|
||||
|
||||
2. **Completion Handling**:
|
||||
- Modal automatically handles completion state
|
||||
- Calls `onComplete` callback with results
|
||||
- Calls `onError` callback on failures
|
||||
|
||||
### Frontend Integration
|
||||
|
||||
1. **State Management**: Ready for Zustand store integration
|
||||
2. **Routing**: Can be integrated with React Router
|
||||
3. **Theming**: Uses Material-UI theme system
|
||||
4. **Accessibility**: Built with accessibility best practices
|
||||
|
||||
## Development Status
|
||||
|
||||
### ✅ Completed
|
||||
- Modal foundation and structure
|
||||
- Progress tracking UI
|
||||
- Tabbed interface
|
||||
- Educational panel
|
||||
- Polling mechanism
|
||||
- Mock data for Phase 1
|
||||
- TypeScript types
|
||||
- Test component
|
||||
|
||||
### 🔄 Next Steps (Phase 1B)
|
||||
1. **Enhanced Step Results Panel**
|
||||
- Detailed step result display
|
||||
- Data source attribution
|
||||
- Quality gate validation results
|
||||
|
||||
2. **Data Source Transparency**
|
||||
- Integration with existing DataSourceTransparency component
|
||||
- Real data source attribution
|
||||
- Confidence scores and timestamps
|
||||
|
||||
3. **Quality Gates Panel**
|
||||
- Real-time quality gate validation
|
||||
- Pass/fail status indicators
|
||||
- Recommendations and improvements
|
||||
|
||||
4. **Backend Integration**
|
||||
- Connect to real Phase 1 backend endpoints
|
||||
- Replace mock data with live data
|
||||
- Error handling for real API calls
|
||||
|
||||
### 📋 Future Enhancements (Phase 2+)
|
||||
1. **Advanced Animations**
|
||||
- Step transition animations
|
||||
- Progress bar animations
|
||||
- Loading states and spinners
|
||||
|
||||
2. **User Preferences**
|
||||
- Transparency level settings
|
||||
- Educational content preferences
|
||||
- Auto-expand options
|
||||
|
||||
3. **Export Functionality**
|
||||
- Progress reports
|
||||
- Quality analysis exports
|
||||
- Educational content downloads
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Dependencies
|
||||
- React 18+
|
||||
- Material-UI (MUI) v5
|
||||
- Framer Motion
|
||||
- TypeScript
|
||||
|
||||
### Performance Considerations
|
||||
- Lazy loading of tab content
|
||||
- Optimized re-renders with React.memo
|
||||
- Efficient polling with useCallback
|
||||
- Minimal bundle size impact
|
||||
|
||||
### Accessibility Features
|
||||
- ARIA labels and descriptions
|
||||
- Keyboard navigation support
|
||||
- Screen reader compatibility
|
||||
- High contrast mode support
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Import Errors**
|
||||
- Ensure all dependencies are installed
|
||||
- Check TypeScript configuration
|
||||
- Verify file paths and exports
|
||||
|
||||
2. **Polling Issues**
|
||||
- Check network connectivity
|
||||
- Verify API endpoint availability
|
||||
- Review browser console for errors
|
||||
|
||||
3. **Styling Issues**
|
||||
- Ensure Material-UI theme is properly configured
|
||||
- Check for CSS conflicts
|
||||
- Verify responsive breakpoints
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging by setting the environment variable:
|
||||
```bash
|
||||
REACT_APP_DEBUG_CALENDAR_MODAL=true
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
When contributing to this component:
|
||||
|
||||
1. Follow the existing code structure
|
||||
2. Add TypeScript types for new features
|
||||
3. Include test cases for new functionality
|
||||
4. Update this README for new features
|
||||
5. Ensure accessibility compliance
|
||||
|
||||
## License
|
||||
|
||||
This component is part of the ALwrity project and follows the same licensing terms.
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Box, Typography } from '@mui/material';
|
||||
import CalendarGenerationModal from './CalendarGenerationModal';
|
||||
|
||||
const TestCalendarGenerationModal: React.FC = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const handleOpenModal = () => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleComplete = (results: any) => {
|
||||
console.log('Calendar generation completed:', results);
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleError = (error: string) => {
|
||||
console.error('Calendar generation error:', error);
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const mockConfig = {
|
||||
userId: 'user123',
|
||||
strategyId: 'strategy456',
|
||||
calendarType: 'monthly' as const,
|
||||
platforms: ['LinkedIn', 'Twitter', 'Website'],
|
||||
duration: 30,
|
||||
postingFrequency: 'daily' as const
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Calendar Generation Modal Test
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleOpenModal}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
Open Calendar Generation Modal
|
||||
</Button>
|
||||
|
||||
<CalendarGenerationModal
|
||||
open={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
sessionId="test-session-123"
|
||||
initialConfig={mockConfig}
|
||||
onComplete={handleComplete}
|
||||
onError={handleError}
|
||||
/>
|
||||
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
This test component allows you to verify the CalendarGenerationModal functionality.
|
||||
The modal will display mock data for Phase 1 (Steps 1-3) with a 94% quality score.
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestCalendarGenerationModal;
|
||||
@@ -0,0 +1,174 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Grid
|
||||
} from '@mui/material';
|
||||
import CalendarGenerationModal from './CalendarGenerationModal';
|
||||
|
||||
// Mock data for testing Phase 2 integration
|
||||
const mockPhase2Progress = {
|
||||
status: 'in_progress',
|
||||
currentStep: 4,
|
||||
stepProgress: 75,
|
||||
overallProgress: 45,
|
||||
stepResults: {
|
||||
1: {
|
||||
stepName: 'Content Strategy Analysis',
|
||||
executionTime: '2.3s',
|
||||
qualityScore: 0.94,
|
||||
dataSourcesUsed: ['Content Strategy', 'Business Goals', 'Target Audience']
|
||||
},
|
||||
2: {
|
||||
stepName: 'Gap Analysis and Opportunity Identification',
|
||||
executionTime: '3.1s',
|
||||
qualityScore: 0.89,
|
||||
dataSourcesUsed: ['Gap Analysis', 'Keyword Research', 'Competitor Analysis']
|
||||
},
|
||||
3: {
|
||||
stepName: 'Audience and Platform Strategy',
|
||||
executionTime: '2.8s',
|
||||
qualityScore: 0.92,
|
||||
dataSourcesUsed: ['Audience Data', 'Platform Performance', 'Content Mix Analysis']
|
||||
},
|
||||
4: {
|
||||
stepName: 'Calendar Framework and Timeline',
|
||||
executionTime: '1.9s',
|
||||
qualityScore: 0.91,
|
||||
dataSourcesUsed: ['Calendar Configuration', 'Timeline Optimization', 'Duration Control']
|
||||
}
|
||||
},
|
||||
qualityScores: {
|
||||
overall: 0.91,
|
||||
step1: 0.94,
|
||||
step2: 0.89,
|
||||
step3: 0.92,
|
||||
step4: 0.91,
|
||||
step5: 0.0,
|
||||
step6: 0.0,
|
||||
step7: 0.0,
|
||||
step8: 0.0,
|
||||
step9: 0.0,
|
||||
step10: 0.0,
|
||||
step11: 0.0,
|
||||
step12: 0.0
|
||||
},
|
||||
transparencyMessages: [
|
||||
'Step 4: Calendar Framework and Timeline completed with 91% quality score',
|
||||
'Calendar structure optimized for your posting preferences',
|
||||
'Timeline duration validated against business goals'
|
||||
],
|
||||
educationalContent: [
|
||||
{
|
||||
title: 'Calendar Framework & Timeline',
|
||||
description: 'Building the structural foundation of your content calendar with optimal timing and duration control.',
|
||||
tips: [
|
||||
'Optimize posting frequency for your audience',
|
||||
'Consider timezone and peak engagement hours',
|
||||
'Balance content types across the timeline',
|
||||
'Ensure strategic alignment with business goals'
|
||||
]
|
||||
}
|
||||
],
|
||||
errors: [],
|
||||
warnings: []
|
||||
};
|
||||
|
||||
const TestPhase2Integration: React.FC = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [sessionId] = useState('test-phase2-session-123');
|
||||
const [calendarConfig] = useState({
|
||||
userId: '1',
|
||||
strategyId: '1',
|
||||
calendarType: 'monthly' as const,
|
||||
platforms: ['LinkedIn', 'Twitter', 'Blog'],
|
||||
duration: 30,
|
||||
postingFrequency: 'weekly' as const
|
||||
});
|
||||
|
||||
const handleComplete = (results: any) => {
|
||||
console.log('Calendar generation completed:', results);
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleError = (error: string) => {
|
||||
console.error('Calendar generation error:', error);
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Paper elevation={2} sx={{ p: 3 }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Phase 2 Frontend Integration Test
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" color="text.secondary" paragraph>
|
||||
This test verifies that the frontend properly displays Phase 2 steps (4-6) with:
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2} sx={{ mb: 3 }}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
✅ What's Implemented:
|
||||
</Typography>
|
||||
<ul>
|
||||
<li>Step indicators for Steps 1-6</li>
|
||||
<li>Step-specific icons for Phase 2</li>
|
||||
<li>Educational content for Steps 4-6</li>
|
||||
<li>Data source panel updates for Phase 2</li>
|
||||
<li>Quality score display for all steps</li>
|
||||
</ul>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
🧪 Test Features:
|
||||
</Typography>
|
||||
<ul>
|
||||
<li>Mock Phase 2 progress data</li>
|
||||
<li>Step 4 completion simulation</li>
|
||||
<li>Quality scores for Steps 1-4</li>
|
||||
<li>Educational content for Step 4</li>
|
||||
<li>Data sources for Step 4</li>
|
||||
</ul>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
Test Phase 2 Modal
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
// Simulate backend call
|
||||
console.log('Simulating backend Phase 2 completion...');
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Simulate Backend Integration
|
||||
</Button>
|
||||
</Paper>
|
||||
|
||||
<CalendarGenerationModal
|
||||
open={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
sessionId={sessionId}
|
||||
initialConfig={calendarConfig}
|
||||
onComplete={handleComplete}
|
||||
onError={handleError}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestPhase2Integration;
|
||||
@@ -0,0 +1,352 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Paper,
|
||||
Typography,
|
||||
Box,
|
||||
Grid,
|
||||
Chip,
|
||||
Card,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
|
||||
// Import styles
|
||||
import {
|
||||
dataSourceCardStyles,
|
||||
dataSourceIconStyles,
|
||||
getDataSourceIconColor,
|
||||
qualityMetricsContainerStyles,
|
||||
getMetricColor
|
||||
} from '../CalendarGenerationModal.styles';
|
||||
|
||||
interface DataSourcePanelProps {
|
||||
currentStep?: number;
|
||||
stepResults?: Record<number, any>;
|
||||
}
|
||||
|
||||
const DataSourcePanel: React.FC<DataSourcePanelProps> = ({
|
||||
currentStep = 1,
|
||||
stepResults = {}
|
||||
}) => {
|
||||
// Get data sources for current step
|
||||
const getStepDataSources = (step: number) => {
|
||||
switch (step) {
|
||||
case 1:
|
||||
return [
|
||||
{
|
||||
name: "Content Strategy",
|
||||
description: "Your existing content strategy and business goals",
|
||||
confidence: "High Confidence",
|
||||
confidenceColor: "success" as const,
|
||||
lastUpdated: "Updated 2 hours ago",
|
||||
icon: "✓",
|
||||
iconColor: "strategy"
|
||||
},
|
||||
{
|
||||
name: "Business Goals",
|
||||
description: "KPI mapping and strategic objectives",
|
||||
confidence: "High Confidence",
|
||||
confidenceColor: "success" as const,
|
||||
lastUpdated: "Updated 2 hours ago",
|
||||
icon: "🎯",
|
||||
iconColor: "primary"
|
||||
},
|
||||
{
|
||||
name: "Target Audience",
|
||||
description: "Audience personas and demographics",
|
||||
confidence: "Medium Confidence",
|
||||
confidenceColor: "warning" as const,
|
||||
lastUpdated: "Updated 1 day ago",
|
||||
icon: "👥",
|
||||
iconColor: "info"
|
||||
}
|
||||
];
|
||||
case 2:
|
||||
return [
|
||||
{
|
||||
name: "Gap Analysis",
|
||||
description: "Content gaps and opportunity identification",
|
||||
confidence: "High Confidence",
|
||||
confidenceColor: "success" as const,
|
||||
lastUpdated: "Real-time analysis",
|
||||
icon: "📊",
|
||||
iconColor: "info"
|
||||
},
|
||||
{
|
||||
name: "Keyword Research",
|
||||
description: "High-value keywords and search volume data",
|
||||
confidence: "High Confidence",
|
||||
confidenceColor: "success" as const,
|
||||
lastUpdated: "Real-time analysis",
|
||||
icon: "🔍",
|
||||
iconColor: "primary"
|
||||
},
|
||||
{
|
||||
name: "Competitor Analysis",
|
||||
description: "Competitive insights and differentiation strategies",
|
||||
confidence: "Medium Confidence",
|
||||
confidenceColor: "warning" as const,
|
||||
lastUpdated: "Updated 3 hours ago",
|
||||
icon: "🏆",
|
||||
iconColor: "secondary"
|
||||
}
|
||||
];
|
||||
case 3:
|
||||
return [
|
||||
{
|
||||
name: "Audience Data",
|
||||
description: "Detailed audience personas and preferences",
|
||||
confidence: "High Confidence",
|
||||
confidenceColor: "success" as const,
|
||||
lastUpdated: "Updated 1 day ago",
|
||||
icon: "👥",
|
||||
iconColor: "info"
|
||||
},
|
||||
{
|
||||
name: "Platform Performance",
|
||||
description: "Historical platform engagement metrics",
|
||||
confidence: "Medium Confidence",
|
||||
confidenceColor: "warning" as const,
|
||||
lastUpdated: "Updated 3 days ago",
|
||||
icon: "📈",
|
||||
iconColor: "secondary"
|
||||
},
|
||||
{
|
||||
name: "Content Mix Analysis",
|
||||
description: "Optimal content type distribution",
|
||||
confidence: "High Confidence",
|
||||
confidenceColor: "success" as const,
|
||||
lastUpdated: "Real-time analysis",
|
||||
icon: "🎨",
|
||||
iconColor: "primary"
|
||||
}
|
||||
];
|
||||
case 4:
|
||||
return [
|
||||
{
|
||||
name: "Calendar Configuration",
|
||||
description: "User posting preferences and calendar settings",
|
||||
confidence: "High Confidence",
|
||||
confidenceColor: "success" as const,
|
||||
lastUpdated: "Just configured",
|
||||
icon: "📅",
|
||||
iconColor: "primary"
|
||||
},
|
||||
{
|
||||
name: "Timeline Optimization",
|
||||
description: "Optimal posting times and frequency analysis",
|
||||
confidence: "High Confidence",
|
||||
confidenceColor: "success" as const,
|
||||
lastUpdated: "Real-time analysis",
|
||||
icon: "⏰",
|
||||
iconColor: "info"
|
||||
},
|
||||
{
|
||||
name: "Duration Control",
|
||||
description: "Calendar duration and structure validation",
|
||||
confidence: "High Confidence",
|
||||
confidenceColor: "success" as const,
|
||||
lastUpdated: "Real-time analysis",
|
||||
icon: "📏",
|
||||
iconColor: "secondary"
|
||||
}
|
||||
];
|
||||
case 5:
|
||||
return [
|
||||
{
|
||||
name: "Content Pillars",
|
||||
description: "Strategic content pillar definitions from Step 1",
|
||||
confidence: "High Confidence",
|
||||
confidenceColor: "success" as const,
|
||||
lastUpdated: "From Step 1",
|
||||
icon: "🏗️",
|
||||
iconColor: "primary"
|
||||
},
|
||||
{
|
||||
name: "Timeline Structure",
|
||||
description: "Calendar framework from Step 4",
|
||||
confidence: "High Confidence",
|
||||
confidenceColor: "success" as const,
|
||||
lastUpdated: "From Step 4",
|
||||
icon: "📅",
|
||||
iconColor: "info"
|
||||
},
|
||||
{
|
||||
name: "Theme Development",
|
||||
description: "Industry-specific theme generation",
|
||||
confidence: "High Confidence",
|
||||
confidenceColor: "success" as const,
|
||||
lastUpdated: "Real-time analysis",
|
||||
icon: "🎨",
|
||||
iconColor: "secondary"
|
||||
}
|
||||
];
|
||||
case 6:
|
||||
return [
|
||||
{
|
||||
name: "Platform Performance",
|
||||
description: "Platform-specific engagement data from Step 3",
|
||||
confidence: "Medium Confidence",
|
||||
confidenceColor: "warning" as const,
|
||||
lastUpdated: "From Step 3",
|
||||
icon: "📈",
|
||||
iconColor: "secondary"
|
||||
},
|
||||
{
|
||||
name: "Content Adaptation",
|
||||
description: "Platform-specific content optimization",
|
||||
confidence: "High Confidence",
|
||||
confidenceColor: "success" as const,
|
||||
lastUpdated: "Real-time analysis",
|
||||
icon: "🔄",
|
||||
iconColor: "primary"
|
||||
},
|
||||
{
|
||||
name: "Cross-Platform Coordination",
|
||||
description: "Multi-platform strategy alignment",
|
||||
confidence: "High Confidence",
|
||||
confidenceColor: "success" as const,
|
||||
lastUpdated: "Real-time analysis",
|
||||
icon: "🔗",
|
||||
iconColor: "info"
|
||||
}
|
||||
];
|
||||
default:
|
||||
return [
|
||||
{
|
||||
name: "Content Strategy",
|
||||
description: "Your existing content strategy and business goals",
|
||||
confidence: "High Confidence",
|
||||
confidenceColor: "success" as const,
|
||||
lastUpdated: "Updated 2 hours ago",
|
||||
icon: "✓",
|
||||
iconColor: "strategy"
|
||||
},
|
||||
{
|
||||
name: "Onboarding Data",
|
||||
description: "Industry, audience, and platform preferences",
|
||||
confidence: "Medium Confidence",
|
||||
confidenceColor: "warning" as const,
|
||||
lastUpdated: "Updated 1 day ago",
|
||||
icon: "📊",
|
||||
iconColor: "info"
|
||||
},
|
||||
{
|
||||
name: "AI Analysis",
|
||||
description: "AI-powered content and performance insights",
|
||||
confidence: "High Confidence",
|
||||
confidenceColor: "success" as const,
|
||||
lastUpdated: "Real-time analysis",
|
||||
icon: "🤖",
|
||||
iconColor: "primary"
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
const currentDataSources = getStepDataSources(currentStep);
|
||||
|
||||
return (
|
||||
<Paper elevation={1} sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Data Sources & Transparency
|
||||
</Typography>
|
||||
|
||||
<Box mb={3}>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
This calendar generation uses multiple data sources to ensure high-quality, personalized results.
|
||||
{currentStep <= 6 && (
|
||||
<span> Currently showing data sources for <strong>Step {currentStep}</strong>.</span>
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Data Source Attribution */}
|
||||
<Box mb={3}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Data Sources Used {currentStep <= 6 && `(Step ${currentStep})`}
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{currentDataSources.map((source, index) => (
|
||||
<Grid item xs={12} md={6} key={index}>
|
||||
<Card variant="outlined" sx={dataSourceCardStyles}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={1}>
|
||||
<Box
|
||||
sx={{
|
||||
...dataSourceIconStyles,
|
||||
backgroundColor: getDataSourceIconColor(source.iconColor)
|
||||
}}
|
||||
>
|
||||
{source.icon}
|
||||
</Box>
|
||||
<Typography variant="subtitle2">{source.name}</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{source.description}
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center" gap={1} mt={1}>
|
||||
<Chip label={source.confidence} size="small" color={source.confidenceColor} />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{source.lastUpdated}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* Data Quality Metrics */}
|
||||
<Box mb={3}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Data Quality Metrics
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Box sx={qualityMetricsContainerStyles}>
|
||||
<Typography variant="h4" sx={{ color: getMetricColor('Overall Data Quality') }} gutterBottom>
|
||||
94%
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Overall Data Quality
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<Box sx={qualityMetricsContainerStyles}>
|
||||
<Typography variant="h4" sx={{ color: getMetricColor('Data Completeness') }} gutterBottom>
|
||||
87%
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Data Completeness
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<Box sx={qualityMetricsContainerStyles}>
|
||||
<Typography variant="h4" sx={{ color: getMetricColor('Data Freshness') }} gutterBottom>
|
||||
91%
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Data Freshness
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* Transparency Note */}
|
||||
<Alert severity="info">
|
||||
<Typography variant="body2">
|
||||
<strong>Transparency Note:</strong> All data sources are processed securely and used only for calendar generation.
|
||||
No personal data is shared with third parties. You can review and update your data sources in the settings.
|
||||
</Typography>
|
||||
</Alert>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataSourcePanel;
|
||||
@@ -0,0 +1,202 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Paper,
|
||||
Typography,
|
||||
Box,
|
||||
IconButton,
|
||||
Chip,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails
|
||||
} from '@mui/material';
|
||||
import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
School as SchoolIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
ViewModule as ViewModuleIcon,
|
||||
Devices as DevicesIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
DataUsage as DataUsageIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface EducationalPanelProps {
|
||||
content: any[];
|
||||
currentStep: number;
|
||||
isExpanded: boolean;
|
||||
onToggleExpanded: () => void;
|
||||
}
|
||||
|
||||
const EducationalPanel: React.FC<EducationalPanelProps> = ({
|
||||
content,
|
||||
currentStep,
|
||||
isExpanded,
|
||||
onToggleExpanded
|
||||
}) => {
|
||||
// Step-specific educational content
|
||||
const getStepEducationalContent = (step: number) => {
|
||||
switch (step) {
|
||||
case 1:
|
||||
return {
|
||||
title: "Content Strategy Analysis",
|
||||
description: "Analyzing your business goals, target audience, and content pillars to establish a strategic foundation.",
|
||||
tips: [
|
||||
"Review your business objectives and KPIs",
|
||||
"Identify your target audience personas",
|
||||
"Define your core content pillars",
|
||||
"Align content with business goals"
|
||||
],
|
||||
icon: <SchoolIcon />
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
title: "Gap Analysis & Opportunities",
|
||||
description: "Identifying content gaps, keyword opportunities, and competitive insights to optimize your strategy.",
|
||||
tips: [
|
||||
"Analyze competitor content strategies",
|
||||
"Identify high-value keyword opportunities",
|
||||
"Find content gaps in your niche",
|
||||
"Prioritize opportunities by impact"
|
||||
],
|
||||
icon: <DataUsageIcon />
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
title: "Audience & Platform Strategy",
|
||||
description: "Developing audience personas and platform-specific strategies for maximum engagement.",
|
||||
tips: [
|
||||
"Create detailed audience personas",
|
||||
"Analyze platform performance metrics",
|
||||
"Develop platform-specific content strategies",
|
||||
"Optimize for each platform's unique features"
|
||||
],
|
||||
icon: <TrendingUpIcon />
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
title: "Calendar Framework & Timeline",
|
||||
description: "Building the structural foundation of your content calendar with optimal timing and duration control.",
|
||||
tips: [
|
||||
"Optimize posting frequency for your audience",
|
||||
"Consider timezone and peak engagement hours",
|
||||
"Balance content types across the timeline",
|
||||
"Ensure strategic alignment with business goals"
|
||||
],
|
||||
icon: <ScheduleIcon />
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
title: "Content Pillar Distribution",
|
||||
description: "Mapping content pillars across your timeline to ensure balanced and strategic content distribution.",
|
||||
tips: [
|
||||
"Distribute pillars based on strategic importance",
|
||||
"Maintain content variety and freshness",
|
||||
"Ensure each pillar gets adequate coverage",
|
||||
"Create thematic content clusters"
|
||||
],
|
||||
icon: <ViewModuleIcon />
|
||||
};
|
||||
case 6:
|
||||
return {
|
||||
title: "Platform-Specific Strategy",
|
||||
description: "Optimizing content for each platform's unique characteristics and audience preferences.",
|
||||
tips: [
|
||||
"Adapt content format for each platform",
|
||||
"Optimize posting times per platform",
|
||||
"Maintain brand consistency across platforms",
|
||||
"Leverage platform-specific features"
|
||||
],
|
||||
icon: <DevicesIcon />
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: "Calendar Generation",
|
||||
description: "Creating your comprehensive content calendar with strategic alignment and optimization.",
|
||||
tips: [
|
||||
"Review all generated content",
|
||||
"Validate strategic alignment",
|
||||
"Check quality scores and recommendations",
|
||||
"Customize based on your preferences"
|
||||
],
|
||||
icon: <ScheduleIcon />
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const stepContent = getStepEducationalContent(currentStep);
|
||||
|
||||
return (
|
||||
<Paper elevation={1} sx={{ p: 2 }}>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Typography variant="h6">
|
||||
Educational Content
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`Step ${currentStep}`}
|
||||
size="small"
|
||||
color="primary"
|
||||
icon={stepContent.icon}
|
||||
/>
|
||||
</Box>
|
||||
<IconButton onClick={onToggleExpanded} size="small">
|
||||
{isExpanded ? <CheckCircleIcon /> : <SchoolIcon />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{isExpanded && (
|
||||
<Box>
|
||||
<Accordion defaultExpanded>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="subtitle1" fontWeight="bold">
|
||||
{stepContent.title}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{stepContent.description}
|
||||
</Typography>
|
||||
|
||||
<Box mt={2}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Key Tips:
|
||||
</Typography>
|
||||
<Box display="flex" flexDirection="column" gap={1}>
|
||||
{stepContent.tips.map((tip: string, index: number) => (
|
||||
<Box key={index} display="flex" alignItems="center" gap={1}>
|
||||
<CheckCircleIcon fontSize="small" color="primary" />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{tip}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
{/* Additional educational content from backend */}
|
||||
{content.length > 0 && content[0].title !== stepContent.title && (
|
||||
<Box mt={2}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Additional Insights:
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{content[0].description}
|
||||
</Typography>
|
||||
{content[0].tips && content[0].tips.length > 0 && (
|
||||
<Box display="flex" flexWrap="wrap" gap={1} mt={1}>
|
||||
{content[0].tips.map((tip: string, index: number) => (
|
||||
<Chip key={index} label={tip} size="small" variant="outlined" />
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default EducationalPanel;
|
||||
@@ -0,0 +1,370 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Paper,
|
||||
Typography,
|
||||
Box,
|
||||
Grid,
|
||||
LinearProgress,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Card
|
||||
} from '@mui/material';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
// Import styles
|
||||
import {
|
||||
progressBarContainerStyles,
|
||||
stepProgressBarStyles,
|
||||
getStepCardStyles,
|
||||
stepCircleBaseStyles,
|
||||
getStepCircleColor,
|
||||
activityIndicatorStyles,
|
||||
animationDurations,
|
||||
animationEasing,
|
||||
springConfig,
|
||||
staggerDelay,
|
||||
smallPulseAnimation,
|
||||
colorPulseAnimation,
|
||||
stepProgressOverlayStyles
|
||||
} from '../CalendarGenerationModal.styles';
|
||||
|
||||
// Types
|
||||
interface QualityScores {
|
||||
overall: number;
|
||||
step1: number;
|
||||
step2: number;
|
||||
step3: number;
|
||||
step4: number;
|
||||
step5: number;
|
||||
step6: number;
|
||||
step7: number;
|
||||
step8: number;
|
||||
step9: number;
|
||||
step10: number;
|
||||
step11: number;
|
||||
step12: number;
|
||||
}
|
||||
|
||||
interface CalendarGenerationProgress {
|
||||
status: 'initializing' | 'step1' | 'step2' | 'step3' | 'completed' | 'error';
|
||||
currentStep: number;
|
||||
stepProgress: number;
|
||||
overallProgress: number;
|
||||
stepResults: Record<number, any>;
|
||||
qualityScores: QualityScores;
|
||||
transparencyMessages: string[];
|
||||
educationalContent: any[];
|
||||
errors: any[];
|
||||
warnings: any[];
|
||||
}
|
||||
|
||||
interface LiveProgressPanelProps {
|
||||
progress: CalendarGenerationProgress;
|
||||
isPolling: boolean;
|
||||
}
|
||||
|
||||
const LiveProgressPanel: React.FC<LiveProgressPanelProps> = ({ progress, isPolling }) => (
|
||||
<Paper elevation={1} sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Live Progress
|
||||
</Typography>
|
||||
|
||||
{/* Current Status with Enhanced Animation */}
|
||||
<Box mb={3}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<motion.div
|
||||
animate={{
|
||||
rotate: isPolling ? 360 : 0,
|
||||
scale: isPolling ? [1, 1.2, 1] : 1
|
||||
}}
|
||||
transition={{
|
||||
rotate: { duration: 2, repeat: Infinity, ease: "linear" },
|
||||
scale: { duration: 1, repeat: Infinity, ease: "easeInOut" }
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={20} />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
key={progress.status}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Typography variant="subtitle1">
|
||||
Current Step: {progress.currentStep} - {progress.status}
|
||||
</Typography>
|
||||
</motion.div>
|
||||
</Box>
|
||||
</motion.div>
|
||||
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Step Progress:
|
||||
</Typography>
|
||||
<Box sx={progressBarContainerStyles}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress.stepProgress}
|
||||
sx={stepProgressBarStyles}
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ scaleX: 0 }}
|
||||
animate={{ scaleX: progress.stepProgress / 100 }}
|
||||
transition={{ duration: 0.6, ease: animationEasing.easeOut }}
|
||||
style={stepProgressOverlayStyles}
|
||||
/>
|
||||
</Box>
|
||||
<motion.div
|
||||
key={progress.stepProgress}
|
||||
initial={{ scale: 1.2, color: '#1976d2' }}
|
||||
animate={{ scale: 1, color: 'inherit' }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{Math.round(progress.stepProgress)}%
|
||||
</Typography>
|
||||
</motion.div>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Step-by-Step Progress with Staggered Animation */}
|
||||
<Box mb={3}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Step-by-Step Progress
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{[1, 2, 3].map((step, index) => (
|
||||
<Grid item xs={12} md={4} key={step}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50, scale: 0.8 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{
|
||||
delay: index * 0.2,
|
||||
duration: 0.6,
|
||||
ease: "easeOut",
|
||||
type: "spring",
|
||||
stiffness: 100
|
||||
}}
|
||||
whileHover={{
|
||||
scale: 1.02,
|
||||
y: -5,
|
||||
transition: { duration: 0.2 }
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
variant="outlined"
|
||||
sx={getStepCardStyles(progress.currentStep, step)}
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={1}>
|
||||
<motion.div
|
||||
animate={{
|
||||
rotate: progress.currentStep === step ? [0, 10, -10, 0] : 0,
|
||||
scale: progress.currentStep === step ? 1.1 : 1,
|
||||
backgroundColor: progress.currentStep > step ? '#4caf50' :
|
||||
progress.currentStep === step ? '#1976d2' : '#9e9e9e'
|
||||
}}
|
||||
transition={{
|
||||
rotate: { duration: 0.5 },
|
||||
scale: { duration: 0.3 },
|
||||
backgroundColor: { duration: 0.3 }
|
||||
}}
|
||||
style={{
|
||||
...stepCircleBaseStyles,
|
||||
backgroundColor: getStepCircleColor(progress.currentStep, step)
|
||||
}}
|
||||
>
|
||||
{progress.currentStep > step ? (
|
||||
<motion.div
|
||||
initial={{ scale: 0, rotate: -180 }}
|
||||
animate={{ scale: 1, rotate: 0 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 10 }}
|
||||
>
|
||||
✓
|
||||
</motion.div>
|
||||
) : (
|
||||
step
|
||||
)}
|
||||
</motion.div>
|
||||
<Typography variant="subtitle2">
|
||||
Step {step}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{step === 1 ? 'Content Strategy Analysis' :
|
||||
step === 2 ? 'Gap Analysis & Opportunities' :
|
||||
'Audience & Platform Strategy'}
|
||||
</Typography>
|
||||
|
||||
{progress.currentStep >= step && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.3, type: "spring", stiffness: 200 }}
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<motion.div
|
||||
animate={smallPulseAnimation}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: animationEasing.easeInOut
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
label={`${Math.round(progress.qualityScores[`step${step}` as keyof QualityScores] * 100)}%`}
|
||||
size="small"
|
||||
color={progress.qualityScores[`step${step}` as keyof QualityScores] >= 0.9 ? 'success' :
|
||||
progress.qualityScores[`step${step}` as keyof QualityScores] >= 0.8 ? 'warning' : 'error'}
|
||||
/>
|
||||
</motion.div>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Quality Score
|
||||
</Typography>
|
||||
</Box>
|
||||
</motion.div>
|
||||
)}
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* Recent Activity with Staggered Animation */}
|
||||
<Box mb={3}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Recent Activity
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ maxHeight: 200, overflowY: 'auto' }}>
|
||||
{progress.transparencyMessages.map((message, index) => (
|
||||
<motion.div
|
||||
key={`${message}-${index}`}
|
||||
initial={{ opacity: 0, x: -20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||
transition={{
|
||||
delay: index * 0.1,
|
||||
duration: 0.4,
|
||||
ease: "easeOut"
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="flex-start" gap={2} mb={1}>
|
||||
<motion.div
|
||||
animate={colorPulseAnimation}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
style={activityIndicatorStyles}
|
||||
/>
|
||||
<Typography variant="body2">
|
||||
{message}
|
||||
</Typography>
|
||||
</Box>
|
||||
</motion.div>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Performance Metrics with Counter Animation */}
|
||||
<Box>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Performance Metrics
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{[
|
||||
{
|
||||
value: progress.overallProgress,
|
||||
label: 'Overall Progress',
|
||||
color: 'primary.main',
|
||||
suffix: '%'
|
||||
},
|
||||
{
|
||||
value: progress.qualityScores.overall * 100,
|
||||
label: 'Quality Score',
|
||||
color: 'success.main',
|
||||
suffix: '%'
|
||||
},
|
||||
{
|
||||
value: progress.currentStep,
|
||||
label: 'Steps Completed',
|
||||
color: 'info.main',
|
||||
suffix: '/3'
|
||||
},
|
||||
{
|
||||
value: progress.errors.length,
|
||||
label: 'Issues Found',
|
||||
color: 'secondary.main',
|
||||
suffix: ''
|
||||
}
|
||||
].map((metric, index) => (
|
||||
<Grid item xs={12} md={3} key={index}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30, scale: 0.8 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{
|
||||
delay: index * staggerDelay,
|
||||
duration: animationDurations.medium,
|
||||
type: springConfig.type,
|
||||
stiffness: 100
|
||||
}}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
>
|
||||
<Box textAlign="center" p={2}>
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{
|
||||
delay: index * 0.1 + 0.3,
|
||||
type: "spring",
|
||||
stiffness: 200,
|
||||
damping: 10
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: metric.color }}
|
||||
gutterBottom
|
||||
>
|
||||
<motion.span
|
||||
initial={{ opacity: 0, scale: 0.5 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
delay: index * 0.1 + 0.5,
|
||||
duration: 0.5,
|
||||
ease: "easeOut"
|
||||
}}
|
||||
>
|
||||
{Math.round(metric.value)}
|
||||
</motion.span>
|
||||
{metric.suffix}
|
||||
</Typography>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: index * 0.1 + 0.8, duration: 0.5 }}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
{metric.label}
|
||||
</Typography>
|
||||
</motion.div>
|
||||
</Box>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
export default LiveProgressPanel;
|
||||
@@ -0,0 +1,307 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Paper,
|
||||
Typography,
|
||||
Box,
|
||||
Grid,
|
||||
Chip,
|
||||
Card,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails
|
||||
} from '@mui/material';
|
||||
import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Warning as WarningIcon,
|
||||
Error as ErrorIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
ViewModule as ViewModuleIcon,
|
||||
Devices as DevicesIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
DataUsage as DataUsageIcon,
|
||||
School as SchoolIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
// Import styles
|
||||
import {
|
||||
qualityScoreContainerStyles,
|
||||
getQualityScoreBackground,
|
||||
qualityScoreInnerStyles
|
||||
} from '../CalendarGenerationModal.styles';
|
||||
|
||||
// Types
|
||||
interface QualityScores {
|
||||
overall: number;
|
||||
step1: number;
|
||||
step2: number;
|
||||
step3: number;
|
||||
step4: number;
|
||||
step5: number;
|
||||
step6: number;
|
||||
step7: number;
|
||||
step8: number;
|
||||
step9: number;
|
||||
step10: number;
|
||||
step11: number;
|
||||
step12: number;
|
||||
}
|
||||
|
||||
interface QualityGatesPanelProps {
|
||||
qualityScores: QualityScores;
|
||||
stepResults: Record<number, any>;
|
||||
currentStep?: number;
|
||||
}
|
||||
|
||||
const QualityGatesPanel: React.FC<QualityGatesPanelProps> = ({
|
||||
qualityScores,
|
||||
stepResults,
|
||||
currentStep = 1
|
||||
}) => {
|
||||
|
||||
// Get step-specific quality gates
|
||||
const getQualityGatesForStep = (step: number) => {
|
||||
const gates = [];
|
||||
|
||||
// Phase 1 Quality Gates (Steps 1-3)
|
||||
if (step >= 1) {
|
||||
gates.push({
|
||||
id: 'strategy_alignment',
|
||||
title: 'Strategy Alignment',
|
||||
description: 'Content strategy alignment with business goals',
|
||||
score: qualityScores.step1,
|
||||
icon: <SchoolIcon />,
|
||||
category: 'Phase 1: Foundation'
|
||||
});
|
||||
}
|
||||
|
||||
if (step >= 2) {
|
||||
gates.push({
|
||||
id: 'content_quality',
|
||||
title: 'Content Gap Analysis',
|
||||
description: 'Content gaps and opportunity identification quality',
|
||||
score: qualityScores.step2,
|
||||
icon: <DataUsageIcon />,
|
||||
category: 'Phase 1: Foundation'
|
||||
});
|
||||
}
|
||||
|
||||
if (step >= 3) {
|
||||
gates.push({
|
||||
id: 'platform_optimization',
|
||||
title: 'Audience & Platform Strategy',
|
||||
description: 'Platform-specific content optimization and audience alignment',
|
||||
score: qualityScores.step3,
|
||||
icon: <TrendingUpIcon />,
|
||||
category: 'Phase 1: Foundation'
|
||||
});
|
||||
}
|
||||
|
||||
// Phase 2 Quality Gates (Steps 4-6)
|
||||
if (step >= 4) {
|
||||
gates.push({
|
||||
id: 'calendar_framework',
|
||||
title: 'Calendar Framework Quality',
|
||||
description: 'Calendar structure, timeline optimization, and duration control',
|
||||
score: qualityScores.step4,
|
||||
icon: <ScheduleIcon />,
|
||||
category: 'Phase 2: Structure'
|
||||
});
|
||||
}
|
||||
|
||||
if (step >= 5) {
|
||||
gates.push({
|
||||
id: 'pillar_distribution',
|
||||
title: 'Content Pillar Distribution',
|
||||
description: 'Balanced content pillar mapping and theme variety',
|
||||
score: qualityScores.step5,
|
||||
icon: <ViewModuleIcon />,
|
||||
category: 'Phase 2: Structure'
|
||||
});
|
||||
}
|
||||
|
||||
if (step >= 6) {
|
||||
gates.push({
|
||||
id: 'platform_strategy',
|
||||
title: 'Platform-Specific Strategy',
|
||||
description: 'Cross-platform coordination and content adaptation quality',
|
||||
score: qualityScores.step6,
|
||||
icon: <DevicesIcon />,
|
||||
category: 'Phase 2: Structure'
|
||||
});
|
||||
}
|
||||
|
||||
return gates;
|
||||
};
|
||||
|
||||
const getQualityStatus = (score: number) => {
|
||||
if (score >= 0.9) return { label: 'EXCELLENT', color: 'success' as const, icon: <CheckCircleIcon /> };
|
||||
if (score >= 0.8) return { label: 'GOOD', color: 'warning' as const, icon: <CheckCircleIcon /> };
|
||||
if (score >= 0.7) return { label: 'ACCEPTABLE', color: 'warning' as const, icon: <WarningIcon /> };
|
||||
if (score > 0) return { label: 'NEEDS IMPROVEMENT', color: 'error' as const, icon: <ErrorIcon /> };
|
||||
return { label: 'PENDING', color: 'default' as const, icon: <ScheduleIcon /> };
|
||||
};
|
||||
|
||||
const currentQualityGates = getQualityGatesForStep(currentStep);
|
||||
const gatesByCategory = currentQualityGates.reduce((acc, gate) => {
|
||||
if (!acc[gate.category]) acc[gate.category] = [];
|
||||
acc[gate.category].push(gate);
|
||||
return acc;
|
||||
}, {} as Record<string, typeof currentQualityGates>);
|
||||
|
||||
return (
|
||||
<Paper elevation={1} sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Quality Gates & Validation
|
||||
</Typography>
|
||||
|
||||
<Box mb={3}>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Quality gates ensure your calendar meets high standards for strategy alignment, content quality, and performance optimization.
|
||||
{currentStep <= 6 && (
|
||||
<span> Currently showing quality gates through <strong>Step {currentStep}</strong>.</span>
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Overall Quality Score */}
|
||||
<Box mb={3}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Overall Quality Score
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Box
|
||||
sx={{
|
||||
...qualityScoreContainerStyles,
|
||||
background: getQualityScoreBackground(qualityScores.overall)
|
||||
}}
|
||||
>
|
||||
<Box sx={qualityScoreInnerStyles}>
|
||||
{Math.round(qualityScores.overall * 100)}%
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h6">
|
||||
{qualityScores.overall >= 0.9 ? 'Excellent' : qualityScores.overall >= 0.8 ? 'Good' : 'Needs Improvement'}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Calendar quality meets {qualityScores.overall >= 0.9 ? 'excellent' : qualityScores.overall >= 0.8 ? 'good' : 'minimum'} standards
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Phase-Based Quality Gates */}
|
||||
<Box mb={3}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Quality Gate Validation by Phase
|
||||
</Typography>
|
||||
|
||||
{Object.entries(gatesByCategory).map(([category, gates]) => (
|
||||
<Accordion key={category} defaultExpanded>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="subtitle2" fontWeight="bold">
|
||||
{category} ({gates.length} gates)
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={2}>
|
||||
{gates.map((gate) => {
|
||||
const status = getQualityStatus(gate.score);
|
||||
return (
|
||||
<Grid item xs={12} md={6} key={gate.id}>
|
||||
<Card variant="outlined" sx={{ p: 2 }}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={1}>
|
||||
<Box sx={{ color: status.color === 'success' ? 'success.main' : status.color === 'error' ? 'error.main' : 'warning.main' }}>
|
||||
{gate.icon}
|
||||
</Box>
|
||||
<Typography variant="subtitle2">{gate.title}</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{gate.description}
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Chip
|
||||
label={status.label}
|
||||
size="small"
|
||||
color={status.color}
|
||||
icon={status.icon}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Score: {gate.score > 0 ? `${Math.round(gate.score * 100)}%` : 'Pending'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Phase 2 Specific Quality Recommendations */}
|
||||
{currentStep >= 4 && (
|
||||
<Box mb={3}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Phase 2 Quality Recommendations
|
||||
</Typography>
|
||||
|
||||
<Card variant="outlined" sx={{ p: 2 }}>
|
||||
<Box component="ul" sx={{ pl: 2, m: 0 }}>
|
||||
<Box component="li" mb={1}>
|
||||
<Typography variant="body2">
|
||||
<strong>Calendar Framework:</strong> Ensure posting frequency aligns with audience engagement patterns
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box component="li" mb={1}>
|
||||
<Typography variant="body2">
|
||||
<strong>Content Pillar Balance:</strong> Maintain 30-40% educational, 25-35% thought leadership content
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box component="li" mb={1}>
|
||||
<Typography variant="body2">
|
||||
<strong>Platform Strategy:</strong> Customize content format and timing for each platform's best practices
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box component="li">
|
||||
<Typography variant="body2">
|
||||
<strong>Timeline Optimization:</strong> Consider timezone differences for global audiences
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Quality Metrics Summary */}
|
||||
<Box>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Quality Metrics Summary
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{currentQualityGates.slice(0, 6).map((gate, index) => (
|
||||
<Grid item xs={12} md={2} key={gate.id}>
|
||||
<Box textAlign="center" p={2}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
color={gate.score >= 0.9 ? 'success.main' : gate.score >= 0.8 ? 'warning.main' : gate.score > 0 ? 'error.main' : 'text.secondary'}
|
||||
gutterBottom
|
||||
>
|
||||
{gate.score > 0 ? `${Math.round(gate.score * 100)}%` : '--'}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontSize: '0.75rem' }}>
|
||||
{gate.title}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default QualityGatesPanel;
|
||||
@@ -0,0 +1,151 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Paper,
|
||||
Typography,
|
||||
Box,
|
||||
Chip,
|
||||
Card
|
||||
} from '@mui/material';
|
||||
import {
|
||||
CheckCircle as CheckCircleIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
// Import styles
|
||||
import {
|
||||
stepResultsCardStyles,
|
||||
stepResultsHeaderStyles,
|
||||
stepResultsContentStyles
|
||||
} from '../CalendarGenerationModal.styles';
|
||||
|
||||
// Types
|
||||
interface QualityScores {
|
||||
overall: number;
|
||||
step1: number;
|
||||
step2: number;
|
||||
step3: number;
|
||||
step4: number;
|
||||
step5: number;
|
||||
step6: number;
|
||||
step7: number;
|
||||
step8: number;
|
||||
step9: number;
|
||||
step10: number;
|
||||
step11: number;
|
||||
step12: number;
|
||||
}
|
||||
|
||||
interface StepResultsPanelProps {
|
||||
stepResults: Record<number, any>;
|
||||
qualityScores: QualityScores;
|
||||
}
|
||||
|
||||
const StepResultsPanel: React.FC<StepResultsPanelProps> = ({ stepResults, qualityScores }) => (
|
||||
<Paper elevation={1} sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Step Results
|
||||
</Typography>
|
||||
|
||||
{Object.entries(stepResults).map(([stepNumber, results]) => (
|
||||
<Box key={stepNumber} mb={3}>
|
||||
<Card variant="outlined" sx={stepResultsCardStyles}>
|
||||
{/* Step Header */}
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Box sx={stepResultsHeaderStyles}>
|
||||
{stepNumber}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h6">
|
||||
{results.stepName}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Execution Time: {results.executionTime}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Chip
|
||||
label={`${Math.round(results.qualityScore * 100)}%`}
|
||||
color={results.qualityScore >= 0.9 ? 'success' : results.qualityScore >= 0.8 ? 'warning' : 'error'}
|
||||
size="small"
|
||||
/>
|
||||
<CheckCircleIcon color="success" />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Data Sources Used */}
|
||||
<Box mb={2}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Data Sources Used:
|
||||
</Typography>
|
||||
<Box display="flex" flexWrap="wrap" gap={1}>
|
||||
{results.dataSourcesUsed.map((source: string, index: number) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={source}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Step Results */}
|
||||
<Box mb={2}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Results:
|
||||
</Typography>
|
||||
<Box sx={stepResultsContentStyles}>
|
||||
{Object.entries(results.results).map(([key, value]) => (
|
||||
<Box key={key} mb={1}>
|
||||
<Typography variant="body2" fontWeight="bold" color="text.secondary">
|
||||
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}:
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{Array.isArray(value) ? value.join(', ') : String(value)}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Insights */}
|
||||
<Box mb={2}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Key Insights:
|
||||
</Typography>
|
||||
<Box component="ul" sx={{ pl: 2, m: 0 }}>
|
||||
{results.insights.map((insight: string, index: number) => (
|
||||
<Box component="li" key={index} mb={0.5}>
|
||||
<Typography variant="body2">
|
||||
{insight}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Recommendations */}
|
||||
<Box>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Recommendations:
|
||||
</Typography>
|
||||
<Box component="ul" sx={{ pl: 2, m: 0 }}>
|
||||
{results.recommendations.map((rec: string, index: number) => (
|
||||
<Box component="li" key={index} mb={0.5}>
|
||||
<Typography variant="body2" color="primary">
|
||||
{rec}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
))}
|
||||
</Paper>
|
||||
);
|
||||
|
||||
export default StepResultsPanel;
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default as LiveProgressPanel } from './LiveProgressPanel';
|
||||
export { default as QualityGatesPanel } from './QualityGatesPanel';
|
||||
export { default as DataSourcePanel } from './DataSourcePanel';
|
||||
export { default as StepResultsPanel } from './StepResultsPanel';
|
||||
export { default as EducationalPanel } from './EducationalPanel';
|
||||
export { default as useCalendarGenerationPolling } from './useCalendarGenerationPolling';
|
||||
export type { CalendarGenerationProgress, QualityScores } from './useCalendarGenerationPolling';
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
// Types
|
||||
interface QualityScores {
|
||||
overall: number;
|
||||
step1: number;
|
||||
step2: number;
|
||||
step3: number;
|
||||
step4: number;
|
||||
step5: number;
|
||||
step6: number;
|
||||
step7: number;
|
||||
step8: number;
|
||||
step9: number;
|
||||
step10: number;
|
||||
step11: number;
|
||||
step12: number;
|
||||
}
|
||||
|
||||
interface CalendarGenerationProgress {
|
||||
status: 'initializing' | 'step1' | 'step2' | 'step3' | 'completed' | 'error';
|
||||
currentStep: number;
|
||||
stepProgress: number;
|
||||
overallProgress: number;
|
||||
stepResults: Record<number, any>;
|
||||
qualityScores: QualityScores;
|
||||
transparencyMessages: string[];
|
||||
educationalContent: any[];
|
||||
errors: any[];
|
||||
warnings: any[];
|
||||
}
|
||||
|
||||
// Polling hook for calendar generation progress
|
||||
const useCalendarGenerationPolling = (sessionId: string) => {
|
||||
const [progress, setProgress] = useState<CalendarGenerationProgress | null>(null);
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const startPolling = useCallback(async () => {
|
||||
setIsPolling(true);
|
||||
setError(null);
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/content-planning/calendar-generation/progress/${sessionId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Transform backend data to frontend format
|
||||
const transformedProgress: CalendarGenerationProgress = {
|
||||
status: data.status,
|
||||
currentStep: data.current_step,
|
||||
stepProgress: data.step_progress,
|
||||
overallProgress: data.overall_progress,
|
||||
stepResults: data.step_results,
|
||||
qualityScores: data.quality_scores,
|
||||
transparencyMessages: data.transparency_messages,
|
||||
educationalContent: data.educational_content,
|
||||
errors: data.errors,
|
||||
warnings: data.warnings
|
||||
};
|
||||
|
||||
setProgress(transformedProgress);
|
||||
|
||||
if (data.status === 'completed' || data.status === 'error') {
|
||||
setIsPolling(false);
|
||||
if (data.status === 'error') {
|
||||
setError(data.errors?.[0]?.message || 'Unknown error occurred');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Continue polling every 2 seconds
|
||||
setTimeout(poll, 2000);
|
||||
} catch (error) {
|
||||
console.error('Calendar generation polling error:', error);
|
||||
setError(error instanceof Error ? error.message : 'Polling failed');
|
||||
// Retry after 5 seconds
|
||||
setTimeout(poll, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
poll();
|
||||
}, [sessionId]);
|
||||
|
||||
const stopPolling = useCallback(() => {
|
||||
setIsPolling(false);
|
||||
}, []);
|
||||
|
||||
return { progress, isPolling, error, startPolling, stopPolling };
|
||||
};
|
||||
|
||||
export default useCalendarGenerationPolling;
|
||||
export type { CalendarGenerationProgress, QualityScores };
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as CalendarGenerationModal } from './CalendarGenerationModal';
|
||||
export { default as TestCalendarGenerationModal } from './TestModal';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,755 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Grid,
|
||||
Paper,
|
||||
LinearProgress,
|
||||
Chip,
|
||||
IconButton,
|
||||
Alert,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Close as CloseIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Error as ErrorIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
School as SchoolIcon,
|
||||
DataUsage as DataUsageIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
// Import existing components for reuse
|
||||
import DataSourceTransparency from '../DataSourceTransparency';
|
||||
import ProgressIndicator from '../ProgressIndicator';
|
||||
|
||||
// Types
|
||||
interface CalendarGenerationModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
sessionId: string;
|
||||
initialConfig: CalendarConfig;
|
||||
onComplete: (results: CalendarGenerationResults) => void;
|
||||
onError: (error: string) => void;
|
||||
}
|
||||
|
||||
interface CalendarConfig {
|
||||
userId: string;
|
||||
strategyId: string;
|
||||
calendarType: 'monthly' | 'quarterly' | 'yearly';
|
||||
platforms: string[];
|
||||
duration: number;
|
||||
postingFrequency: 'daily' | 'weekly' | 'biweekly';
|
||||
}
|
||||
|
||||
interface CalendarGenerationResults {
|
||||
calendar: CalendarData;
|
||||
qualityScores: QualityScores;
|
||||
insights: GenerationInsights;
|
||||
recommendations: Recommendations;
|
||||
exportData: ExportData;
|
||||
}
|
||||
|
||||
interface CalendarData {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
content: CalendarContent[];
|
||||
themes: Theme[];
|
||||
platforms: Platform[];
|
||||
}
|
||||
|
||||
interface CalendarContent {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
contentType: string;
|
||||
platform: string;
|
||||
scheduledDate: string;
|
||||
theme: string;
|
||||
keywords: string[];
|
||||
}
|
||||
|
||||
interface Theme {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
weekNumber: number;
|
||||
contentTypes: string[];
|
||||
}
|
||||
|
||||
interface Platform {
|
||||
id: string;
|
||||
name: string;
|
||||
contentCount: number;
|
||||
postingSchedule: PostingSchedule[];
|
||||
}
|
||||
|
||||
interface PostingSchedule {
|
||||
day: string;
|
||||
time: string;
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
interface QualityScores {
|
||||
overall: number;
|
||||
step1: number;
|
||||
step2: number;
|
||||
step3: number;
|
||||
step4: number;
|
||||
step5: number;
|
||||
step6: number;
|
||||
step7: number;
|
||||
step8: number;
|
||||
step9: number;
|
||||
step10: number;
|
||||
step11: number;
|
||||
step12: number;
|
||||
}
|
||||
|
||||
interface GenerationInsights {
|
||||
contentGaps: ContentGap[];
|
||||
keywordOpportunities: KeywordOpportunity[];
|
||||
audienceInsights: AudienceInsight[];
|
||||
platformPerformance: PlatformPerformance[];
|
||||
}
|
||||
|
||||
interface ContentGap {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
impact: number;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
estimatedTraffic: number;
|
||||
}
|
||||
|
||||
interface KeywordOpportunity {
|
||||
id: string;
|
||||
keyword: string;
|
||||
searchVolume: number;
|
||||
competition: number;
|
||||
relevance: number;
|
||||
estimatedTraffic: number;
|
||||
}
|
||||
|
||||
interface AudienceInsight {
|
||||
id: string;
|
||||
segment: string;
|
||||
demographics: string[];
|
||||
preferences: string[];
|
||||
engagementRate: number;
|
||||
bestTimes: string[];
|
||||
}
|
||||
|
||||
interface PlatformPerformance {
|
||||
id: string;
|
||||
platform: string;
|
||||
engagementRate: number;
|
||||
reach: number;
|
||||
conversionRate: number;
|
||||
bestContentTypes: string[];
|
||||
}
|
||||
|
||||
interface Recommendations {
|
||||
contentMix: ContentMixRecommendation;
|
||||
postingSchedule: PostingScheduleRecommendation;
|
||||
platformStrategy: PlatformStrategyRecommendation;
|
||||
optimizationTips: string[];
|
||||
}
|
||||
|
||||
interface ContentMixRecommendation {
|
||||
educational: number;
|
||||
thoughtLeadership: number;
|
||||
engagement: number;
|
||||
promotional: number;
|
||||
reasoning: string;
|
||||
}
|
||||
|
||||
interface PostingScheduleRecommendation {
|
||||
bestDays: string[];
|
||||
bestTimes: string[];
|
||||
frequency: string;
|
||||
reasoning: string;
|
||||
}
|
||||
|
||||
interface PlatformStrategyRecommendation {
|
||||
primaryPlatforms: string[];
|
||||
contentDistribution: Record<string, number>;
|
||||
crossPlatformStrategy: string;
|
||||
}
|
||||
|
||||
interface ExportData {
|
||||
calendarJson: string;
|
||||
insightsCsv: string;
|
||||
recommendationsPdf: string;
|
||||
qualityReport: string;
|
||||
}
|
||||
|
||||
// Polling hook for calendar generation progress
|
||||
const useCalendarGenerationPolling = (sessionId: string) => {
|
||||
const [progress, setProgress] = useState<CalendarGenerationProgress | null>(null);
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const startPolling = useCallback(async () => {
|
||||
setIsPolling(true);
|
||||
setError(null);
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/content-planning/calendar-generation/progress/${sessionId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setProgress(data);
|
||||
|
||||
if (data.status === 'completed' || data.status === 'error') {
|
||||
setIsPolling(false);
|
||||
if (data.status === 'error') {
|
||||
setError(data.error || 'Unknown error occurred');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Continue polling every 2 seconds
|
||||
setTimeout(poll, 2000);
|
||||
} catch (error) {
|
||||
console.error('Calendar generation polling error:', error);
|
||||
setError(error instanceof Error ? error.message : 'Polling failed');
|
||||
// Retry after 5 seconds
|
||||
setTimeout(poll, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
poll();
|
||||
}, [sessionId]);
|
||||
|
||||
const stopPolling = useCallback(() => {
|
||||
setIsPolling(false);
|
||||
}, []);
|
||||
|
||||
return { progress, isPolling, error, startPolling, stopPolling };
|
||||
};
|
||||
|
||||
// Mock progress data for development (remove when backend is ready)
|
||||
interface CalendarGenerationProgress {
|
||||
status: 'initializing' | 'step1' | 'step2' | 'step3' | 'completed' | 'error';
|
||||
currentStep: number;
|
||||
stepProgress: number;
|
||||
overallProgress: number;
|
||||
stepResults: Record<number, any>;
|
||||
qualityScores: QualityScores;
|
||||
transparencyMessages: string[];
|
||||
educationalContent: any[];
|
||||
errors: any[];
|
||||
warnings: any[];
|
||||
}
|
||||
|
||||
// Mock progress data for Phase 1 testing
|
||||
const mockProgressData: CalendarGenerationProgress = {
|
||||
status: 'step1',
|
||||
currentStep: 1,
|
||||
stepProgress: 75,
|
||||
overallProgress: 25,
|
||||
stepResults: {
|
||||
1: {
|
||||
stepNumber: 1,
|
||||
stepName: 'Content Strategy Analysis',
|
||||
results: {
|
||||
contentPillars: ['Educational', 'Thought Leadership', 'Product Updates', 'Industry Insights'],
|
||||
targetAudience: ['Marketing Professionals', 'Business Owners', 'Content Creators'],
|
||||
businessGoals: ['Increase Brand Awareness', 'Generate Leads', 'Establish Thought Leadership'],
|
||||
strategyAlignment: 0.94
|
||||
},
|
||||
qualityScore: 0.94,
|
||||
executionTime: '2.3s',
|
||||
dataSourcesUsed: ['Content Strategy', 'Onboarding Data', 'AI Analysis'],
|
||||
insights: [
|
||||
'Content strategy shows strong alignment with business goals',
|
||||
'4 content pillars identified with clear focus areas',
|
||||
'3 distinct audience segments with specific preferences'
|
||||
],
|
||||
recommendations: [
|
||||
'Focus on educational content (40%) for lead generation',
|
||||
'Increase thought leadership content (30%) for brand awareness',
|
||||
'Optimize content mix for platform-specific performance'
|
||||
]
|
||||
}
|
||||
},
|
||||
qualityScores: {
|
||||
overall: 0.94,
|
||||
step1: 0.94,
|
||||
step2: 0.0,
|
||||
step3: 0.0,
|
||||
step4: 0.0,
|
||||
step5: 0.0,
|
||||
step6: 0.0,
|
||||
step7: 0.0,
|
||||
step8: 0.0,
|
||||
step9: 0.0,
|
||||
step10: 0.0,
|
||||
step11: 0.0,
|
||||
step12: 0.0
|
||||
},
|
||||
transparencyMessages: [
|
||||
'Starting content strategy analysis...',
|
||||
'Analyzing your content pillars and target audience...',
|
||||
'Generating strategic insights with AI analysis...',
|
||||
'Content strategy analysis completed with 94% quality score'
|
||||
],
|
||||
educationalContent: [
|
||||
{
|
||||
title: 'Content Strategy Analysis',
|
||||
description: 'Understanding how your content strategy influences calendar generation',
|
||||
level: 'intermediate',
|
||||
category: 'strategy',
|
||||
tips: [
|
||||
'Your content pillars define the main themes for your calendar',
|
||||
'Target audience data helps determine content timing and platforms',
|
||||
'Business goals influence content mix and promotional frequency'
|
||||
],
|
||||
examples: [
|
||||
'Educational content: 40% of calendar based on your strategy',
|
||||
'Thought leadership: 30% aligned with your expertise areas',
|
||||
'Engagement content: 20% to build audience relationships'
|
||||
]
|
||||
}
|
||||
],
|
||||
errors: [],
|
||||
warnings: []
|
||||
};
|
||||
|
||||
const CalendarGenerationModal: React.FC<CalendarGenerationModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
sessionId,
|
||||
initialConfig,
|
||||
onComplete,
|
||||
onError
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [educationalPanelExpanded, setEducationalPanelExpanded] = useState(false);
|
||||
const [expandedSections, setExpandedSections] = useState({
|
||||
dataSources: true,
|
||||
progress: true,
|
||||
educational: false,
|
||||
messages: true,
|
||||
stepResults: true
|
||||
});
|
||||
|
||||
// Use polling hook (replace with real implementation when backend is ready)
|
||||
const { progress, isPolling, error, startPolling, stopPolling } = useCalendarGenerationPolling(sessionId);
|
||||
|
||||
// For development, use mock data
|
||||
const currentProgress = progress || mockProgressData;
|
||||
|
||||
useEffect(() => {
|
||||
if (open && sessionId) {
|
||||
// For development, simulate polling
|
||||
// startPolling();
|
||||
} else if (!open) {
|
||||
stopPolling();
|
||||
}
|
||||
}, [open, sessionId, startPolling, stopPolling]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentProgress.status === 'completed') {
|
||||
// Handle completion
|
||||
console.log('Calendar generation completed');
|
||||
} else if (currentProgress.status === 'error') {
|
||||
onError(currentProgress.errors[0]?.message || 'Unknown error');
|
||||
}
|
||||
}, [currentProgress.status, currentProgress.errors, onError]);
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setActiveTab(newValue);
|
||||
};
|
||||
|
||||
const getQualityColor = (score: number) => {
|
||||
if (score >= 0.9) return 'success';
|
||||
if (score >= 0.8) return 'warning';
|
||||
return 'error';
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 'success';
|
||||
case 'error':
|
||||
return 'error';
|
||||
case 'initializing':
|
||||
return 'info';
|
||||
default:
|
||||
return 'primary';
|
||||
}
|
||||
};
|
||||
|
||||
const getStepIcon = (stepNumber: number) => {
|
||||
switch (stepNumber) {
|
||||
case 1:
|
||||
return <SchoolIcon />;
|
||||
case 2:
|
||||
return <DataUsageIcon />;
|
||||
case 3:
|
||||
return <TrendingUpIcon />;
|
||||
default:
|
||||
return <ScheduleIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="xl"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
height: '90vh',
|
||||
maxHeight: '90vh'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Typography variant="h5">
|
||||
Calendar Generation Progress
|
||||
</Typography>
|
||||
<IconButton onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Grid container spacing={2}>
|
||||
{/* Header Section */}
|
||||
<Grid item xs={12}>
|
||||
<Paper elevation={1} sx={{ p: 2, mb: 2 }}>
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
{/* Progress Bar */}
|
||||
<Grid item xs={12}>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Overall Progress
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={currentProgress.overallProgress}
|
||||
sx={{ flexGrow: 1 }}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{Math.round(currentProgress.overallProgress)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Step Indicators */}
|
||||
<Grid item xs={12}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
{[1, 2, 3].map((step) => (
|
||||
<Box
|
||||
key={step}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
sx={{
|
||||
p: 1,
|
||||
borderRadius: 1,
|
||||
backgroundColor: currentProgress.currentStep === step ? 'primary.light' : 'grey.100',
|
||||
color: currentProgress.currentStep === step ? 'primary.contrastText' : 'text.secondary'
|
||||
}}
|
||||
>
|
||||
{getStepIcon(step)}
|
||||
<Typography variant="body2">
|
||||
Step {step}
|
||||
</Typography>
|
||||
{currentProgress.qualityScores[`step${step}` as keyof QualityScores] > 0 && (
|
||||
<Chip
|
||||
label={`${Math.round(currentProgress.qualityScores[`step${step}` as keyof QualityScores] * 100)}%`}
|
||||
size="small"
|
||||
color={getQualityColor(currentProgress.qualityScores[`step${step}` as keyof QualityScores])}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Quality Score and Status */}
|
||||
<Grid item xs={6}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Overall Quality:
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`${Math.round(currentProgress.qualityScores.overall * 100)}%`}
|
||||
color={getQualityColor(currentProgress.qualityScores.overall)}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Status:
|
||||
</Typography>
|
||||
<Chip
|
||||
label={currentProgress.status}
|
||||
color={getStatusColor(currentProgress.status)}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<Grid item xs={12}>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
{/* Tabs */}
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<Button
|
||||
variant={activeTab === 0 ? 'contained' : 'outlined'}
|
||||
size="small"
|
||||
onClick={() => setActiveTab(0)}
|
||||
>
|
||||
Live Progress
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
variant={activeTab === 1 ? 'contained' : 'outlined'}
|
||||
size="small"
|
||||
onClick={() => setActiveTab(1)}
|
||||
>
|
||||
Step Results
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
variant={activeTab === 2 ? 'contained' : 'outlined'}
|
||||
size="small"
|
||||
onClick={() => setActiveTab(2)}
|
||||
>
|
||||
Data Sources
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
variant={activeTab === 3 ? 'contained' : 'outlined'}
|
||||
size="small"
|
||||
onClick={() => setActiveTab(3)}
|
||||
>
|
||||
Quality Gates
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* Tab Content */}
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab === 0 && (
|
||||
<motion.div
|
||||
key="live-progress"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<LiveProgressPanel
|
||||
progress={currentProgress}
|
||||
isPolling={isPolling}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{activeTab === 1 && (
|
||||
<motion.div
|
||||
key="step-results"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<StepResultsPanel
|
||||
stepResults={currentProgress.stepResults}
|
||||
qualityScores={currentProgress.qualityScores}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{activeTab === 2 && (
|
||||
<motion.div
|
||||
key="data-sources"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<DataSourcePanel />
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{activeTab === 3 && (
|
||||
<motion.div
|
||||
key="quality-gates"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<QualityGatesPanel
|
||||
qualityScores={currentProgress.qualityScores}
|
||||
stepResults={currentProgress.stepResults}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Educational Panel */}
|
||||
<Grid item xs={12}>
|
||||
<EducationalPanel
|
||||
content={currentProgress.educationalContent}
|
||||
currentStep={currentProgress.currentStep}
|
||||
isExpanded={educationalPanelExpanded}
|
||||
onToggleExpanded={() => setEducationalPanelExpanded(!educationalPanelExpanded)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Box display="flex" gap={1}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
{currentProgress.status === 'completed' && (
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
// Handle completion
|
||||
console.log('Calendar generation completed');
|
||||
}}
|
||||
>
|
||||
View Calendar
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
// Placeholder components (to be implemented)
|
||||
const LiveProgressPanel: React.FC<{ progress: CalendarGenerationProgress; isPolling: boolean }> = ({ progress, isPolling }) => (
|
||||
<Paper elevation={1} sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Live Progress
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
{isPolling && <CircularProgress size={20} />}
|
||||
<Typography variant="body2">
|
||||
Current Step: {progress.currentStep} - {progress.status}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Step Progress: {progress.stepProgress}%
|
||||
</Typography>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
const StepResultsPanel: React.FC<{ stepResults: Record<number, any>; qualityScores: QualityScores }> = ({ stepResults, qualityScores }) => (
|
||||
<Paper elevation={1} sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Step Results
|
||||
</Typography>
|
||||
{Object.entries(stepResults).map(([stepNumber, results]) => (
|
||||
<Box key={stepNumber} mb={2}>
|
||||
<Typography variant="subtitle1">
|
||||
Step {stepNumber}: {results.stepName}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Quality Score: {Math.round(results.qualityScore * 100)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Paper>
|
||||
);
|
||||
|
||||
const DataSourcePanel: React.FC = () => (
|
||||
<Paper elevation={1} sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Data Sources
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Data source transparency information will be displayed here.
|
||||
</Typography>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
const QualityGatesPanel: React.FC<{ qualityScores: QualityScores; stepResults: Record<number, any> }> = ({ qualityScores, stepResults }) => (
|
||||
<Paper elevation={1} sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Quality Gates
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Quality gate validation results will be displayed here.
|
||||
</Typography>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
const EducationalPanel: React.FC<{
|
||||
content: any[];
|
||||
currentStep: number;
|
||||
isExpanded: boolean;
|
||||
onToggleExpanded: () => void;
|
||||
}> = ({ content, currentStep, isExpanded, onToggleExpanded }) => (
|
||||
<Paper elevation={1} sx={{ p: 2 }}>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
|
||||
<Typography variant="h6">
|
||||
Educational Content
|
||||
</Typography>
|
||||
<IconButton onClick={onToggleExpanded} size="small">
|
||||
{isExpanded ? <CheckCircleIcon /> : <SchoolIcon />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
{isExpanded && content.length > 0 && (
|
||||
<Box>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{content[0].title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{content[0].description}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Tips: {content[0].tips.join(', ')}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
|
||||
export default CalendarGenerationModal;
|
||||
@@ -0,0 +1,251 @@
|
||||
// Calendar Generation Modal Types
|
||||
|
||||
export interface CalendarGenerationModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
sessionId: string;
|
||||
initialConfig: CalendarConfig;
|
||||
onComplete: (results: CalendarGenerationResults) => void;
|
||||
onError: (error: string) => void;
|
||||
}
|
||||
|
||||
export interface CalendarConfig {
|
||||
userId: string;
|
||||
strategyId: string;
|
||||
calendarType: 'monthly' | 'quarterly' | 'yearly';
|
||||
platforms: string[];
|
||||
duration: number;
|
||||
postingFrequency: 'daily' | 'weekly' | 'biweekly';
|
||||
}
|
||||
|
||||
export interface CalendarGenerationResults {
|
||||
calendar: CalendarData;
|
||||
qualityScores: QualityScores;
|
||||
insights: GenerationInsights;
|
||||
recommendations: Recommendations;
|
||||
exportData: ExportData;
|
||||
}
|
||||
|
||||
export interface CalendarData {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
content: CalendarContent[];
|
||||
themes: Theme[];
|
||||
platforms: Platform[];
|
||||
}
|
||||
|
||||
export interface CalendarContent {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
contentType: string;
|
||||
platform: string;
|
||||
scheduledDate: string;
|
||||
theme: string;
|
||||
keywords: string[];
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
weekNumber: number;
|
||||
contentTypes: string[];
|
||||
}
|
||||
|
||||
export interface Platform {
|
||||
id: string;
|
||||
name: string;
|
||||
contentCount: number;
|
||||
postingSchedule: PostingSchedule[];
|
||||
}
|
||||
|
||||
export interface PostingSchedule {
|
||||
day: string;
|
||||
time: string;
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
export interface QualityScores {
|
||||
overall: number;
|
||||
step1: number;
|
||||
step2: number;
|
||||
step3: number;
|
||||
step4: number;
|
||||
step5: number;
|
||||
step6: number;
|
||||
step7: number;
|
||||
step8: number;
|
||||
step9: number;
|
||||
step10: number;
|
||||
step11: number;
|
||||
step12: number;
|
||||
}
|
||||
|
||||
export interface GenerationInsights {
|
||||
contentGaps: ContentGap[];
|
||||
keywordOpportunities: KeywordOpportunity[];
|
||||
audienceInsights: AudienceInsight[];
|
||||
platformPerformance: PlatformPerformance[];
|
||||
}
|
||||
|
||||
export interface ContentGap {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
impact: number;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
estimatedTraffic: number;
|
||||
}
|
||||
|
||||
export interface KeywordOpportunity {
|
||||
id: string;
|
||||
keyword: string;
|
||||
searchVolume: number;
|
||||
competition: number;
|
||||
relevance: number;
|
||||
estimatedTraffic: number;
|
||||
}
|
||||
|
||||
export interface AudienceInsight {
|
||||
id: string;
|
||||
segment: string;
|
||||
demographics: string[];
|
||||
preferences: string[];
|
||||
engagementRate: number;
|
||||
bestTimes: string[];
|
||||
}
|
||||
|
||||
export interface PlatformPerformance {
|
||||
id: string;
|
||||
platform: string;
|
||||
engagementRate: number;
|
||||
reach: number;
|
||||
conversionRate: number;
|
||||
bestContentTypes: string[];
|
||||
}
|
||||
|
||||
export interface Recommendations {
|
||||
contentMix: ContentMixRecommendation;
|
||||
postingSchedule: PostingScheduleRecommendation;
|
||||
platformStrategy: PlatformStrategyRecommendation;
|
||||
optimizationTips: string[];
|
||||
}
|
||||
|
||||
export interface ContentMixRecommendation {
|
||||
educational: number;
|
||||
thoughtLeadership: number;
|
||||
engagement: number;
|
||||
promotional: number;
|
||||
reasoning: string;
|
||||
}
|
||||
|
||||
export interface PostingScheduleRecommendation {
|
||||
bestDays: string[];
|
||||
bestTimes: string[];
|
||||
frequency: string;
|
||||
reasoning: string;
|
||||
}
|
||||
|
||||
export interface PlatformStrategyRecommendation {
|
||||
primaryPlatforms: string[];
|
||||
contentDistribution: Record<string, number>;
|
||||
crossPlatformStrategy: string;
|
||||
}
|
||||
|
||||
export interface ExportData {
|
||||
calendarJson: string;
|
||||
insightsCsv: string;
|
||||
recommendationsPdf: string;
|
||||
qualityReport: string;
|
||||
}
|
||||
|
||||
export interface CalendarGenerationProgress {
|
||||
status: 'initializing' | 'step1' | 'step2' | 'step3' | 'completed' | 'error';
|
||||
currentStep: number;
|
||||
stepProgress: number;
|
||||
overallProgress: number;
|
||||
stepResults: Record<number, StepResult>;
|
||||
qualityScores: QualityScores;
|
||||
transparencyMessages: string[];
|
||||
educationalContent: EducationalContent[];
|
||||
errors: ErrorInfo[];
|
||||
warnings: WarningInfo[];
|
||||
}
|
||||
|
||||
export interface StepResult {
|
||||
stepNumber: number;
|
||||
stepName: string;
|
||||
results: any;
|
||||
qualityScore: number;
|
||||
executionTime: string;
|
||||
dataSourcesUsed: string[];
|
||||
insights: string[];
|
||||
recommendations: string[];
|
||||
}
|
||||
|
||||
export interface EducationalContent {
|
||||
title: string;
|
||||
description: string;
|
||||
level: 'beginner' | 'intermediate' | 'advanced';
|
||||
category: 'strategy' | 'analysis' | 'optimization' | 'quality';
|
||||
tips: string[];
|
||||
examples: string[];
|
||||
relatedConcepts: string[];
|
||||
nextSteps: string[];
|
||||
}
|
||||
|
||||
export interface ErrorInfo {
|
||||
id: string;
|
||||
message: string;
|
||||
step: number;
|
||||
timestamp: string;
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
}
|
||||
|
||||
export interface WarningInfo {
|
||||
id: string;
|
||||
message: string;
|
||||
step: number;
|
||||
timestamp: string;
|
||||
severity: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
export interface DataSourceAttribution {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'strategy' | 'onboarding' | 'gap_analysis' | 'ai_analysis' | 'performance' | 'competitor';
|
||||
confidence: number;
|
||||
lastUpdated: string;
|
||||
dataPoints: DataPoint[];
|
||||
}
|
||||
|
||||
export interface DataPoint {
|
||||
id: string;
|
||||
name: string;
|
||||
value: any;
|
||||
confidence: number;
|
||||
source: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface QualityGateResult {
|
||||
id: string;
|
||||
name: string;
|
||||
status: 'passed' | 'failed' | 'warning';
|
||||
score: number;
|
||||
threshold: number;
|
||||
details: string;
|
||||
recommendations: string[];
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
transparencyLevel: 'basic' | 'detailed' | 'expert';
|
||||
educationalLevel: 'beginner' | 'intermediate' | 'advanced';
|
||||
autoExpandResults: boolean;
|
||||
showQualityDetails: boolean;
|
||||
enableNotifications: boolean;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@@ -15,7 +15,10 @@ import {
|
||||
Tooltip,
|
||||
IconButton,
|
||||
Alert,
|
||||
FormHelperText
|
||||
FormHelperText,
|
||||
Button,
|
||||
Chip,
|
||||
LinearProgress
|
||||
} from '@mui/material';
|
||||
import {
|
||||
CalendarToday as CalendarIcon,
|
||||
@@ -24,12 +27,27 @@ import {
|
||||
TrendingUp as TrendingUpIcon,
|
||||
Public as PublicIcon,
|
||||
AccessTime as AccessTimeIcon,
|
||||
ContentPaste as ContentPasteIcon
|
||||
ContentPaste as ContentPasteIcon,
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
Lightbulb as LightbulbIcon,
|
||||
Warning as WarningIcon,
|
||||
CheckCircle as CheckCircleIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
// Import calendar-specific types
|
||||
import { type CalendarConfig } from './types';
|
||||
|
||||
// Import simplified mapper
|
||||
import {
|
||||
generateSmartDefaults,
|
||||
generateUserGuidance,
|
||||
generateTransparencyIndicators,
|
||||
applySmartDefaultsToConfig,
|
||||
type SmartDefaults,
|
||||
type UserGuidance,
|
||||
type TransparencyIndicators
|
||||
} from '../../../../services/strategyCalendarMapper';
|
||||
|
||||
interface CalendarConfigurationStepProps {
|
||||
calendarConfig: CalendarConfig;
|
||||
onConfigUpdate: (updates: Partial<CalendarConfig>) => void;
|
||||
@@ -158,6 +176,87 @@ const CalendarConfigurationStep: React.FC<CalendarConfigurationStepProps> = ({
|
||||
strategyContext,
|
||||
isFromStrategyActivation = false
|
||||
}) => {
|
||||
// Smart defaults and guidance state
|
||||
const [smartDefaults, setSmartDefaults] = useState<SmartDefaults | null>(null);
|
||||
const [userGuidance, setUserGuidance] = useState<UserGuidance | null>(null);
|
||||
const [transparencyIndicators, setTransparencyIndicators] = useState<TransparencyIndicators | null>(null);
|
||||
const [showSmartDefaults, setShowSmartDefaults] = useState(true);
|
||||
const [showUserGuidance, setShowUserGuidance] = useState(true);
|
||||
const [showTransparency, setShowTransparency] = useState(true);
|
||||
|
||||
// Generate smart defaults and guidance when strategy context changes
|
||||
useEffect(() => {
|
||||
console.log('🎯 CalendarConfigurationStep: Strategy context changed:', {
|
||||
hasStrategyContext: !!strategyContext,
|
||||
hasStrategyData: !!strategyContext?.strategyData,
|
||||
strategyDataType: strategyContext?.strategyData ? typeof strategyContext.strategyData : 'none'
|
||||
});
|
||||
|
||||
if (strategyContext?.strategyData) {
|
||||
console.log('🎯 CalendarConfigurationStep: Generating smart defaults from strategy data');
|
||||
console.log('🎯 CalendarConfigurationStep: About to call generateSmartDefaults with:', strategyContext.strategyData);
|
||||
|
||||
const defaults = generateSmartDefaults(strategyContext.strategyData);
|
||||
console.log('🎯 CalendarConfigurationStep: generateSmartDefaults returned:', defaults);
|
||||
|
||||
const guidance = generateUserGuidance(strategyContext.strategyData);
|
||||
const transparency = generateTransparencyIndicators(strategyContext.strategyData);
|
||||
|
||||
console.log('🎯 CalendarConfigurationStep: Generated data:', {
|
||||
defaults: defaults,
|
||||
guidance: {
|
||||
warnings: guidance.warnings.length,
|
||||
recommendations: guidance.recommendations.length,
|
||||
missingData: guidance.missingData.length
|
||||
},
|
||||
transparency: {
|
||||
integrationLevel: transparency.integrationStatus.integrationLevel,
|
||||
alignmentScore: transparency.strategyAlignment.alignmentScore
|
||||
}
|
||||
});
|
||||
|
||||
setSmartDefaults(defaults);
|
||||
setUserGuidance(guidance);
|
||||
setTransparencyIndicators(transparency);
|
||||
|
||||
console.log('🎯 CalendarConfigurationStep: Smart defaults generated:', {
|
||||
calendarType: defaults.suggestedCalendarType,
|
||||
postingFrequency: defaults.suggestedPostingFrequency,
|
||||
platforms: defaults.suggestedPlatforms,
|
||||
guidanceCount: guidance.warnings.length + guidance.recommendations.length + guidance.missingData.length
|
||||
});
|
||||
} else {
|
||||
console.log('🎯 CalendarConfigurationStep: No strategy context available, using default defaults');
|
||||
const defaults = generateSmartDefaults(null);
|
||||
const guidance = generateUserGuidance(null);
|
||||
const transparency = generateTransparencyIndicators(null);
|
||||
|
||||
setSmartDefaults(defaults);
|
||||
setUserGuidance(guidance);
|
||||
setTransparencyIndicators(transparency);
|
||||
}
|
||||
}, [strategyContext]);
|
||||
|
||||
// Validate timezone on component mount and when timezone changes
|
||||
useEffect(() => {
|
||||
const validTimezones = timeZones.map(tz => tz.value);
|
||||
if (!validTimezones.includes(calendarConfig.timeZone)) {
|
||||
console.log('🎯 CalendarConfigurationStep: Invalid timezone detected, fixing:', calendarConfig.timeZone);
|
||||
// Fix invalid timezone
|
||||
onConfigUpdate({ timeZone: 'America/New_York' });
|
||||
}
|
||||
}, [calendarConfig.timeZone, onConfigUpdate]);
|
||||
|
||||
// Apply smart defaults to configuration
|
||||
const handleApplySmartDefaults = (applyAll: boolean = false) => {
|
||||
if (smartDefaults) {
|
||||
const updates = applySmartDefaultsToConfig(calendarConfig, smartDefaults, applyAll);
|
||||
onConfigUpdate(updates);
|
||||
|
||||
console.log('🎯 CalendarConfigurationStep: Applied smart defaults:', updates);
|
||||
}
|
||||
};
|
||||
|
||||
// Enhanced calendar-specific handlers
|
||||
const handleCalendarTypeChange = (type: 'weekly' | 'monthly' | 'quarterly') => {
|
||||
onConfigUpdate({ calendarType: type });
|
||||
@@ -190,7 +289,14 @@ const CalendarConfigurationStep: React.FC<CalendarConfigurationStepProps> = ({
|
||||
};
|
||||
|
||||
const handleTimeZoneChange = (timezone: string) => {
|
||||
onConfigUpdate({ timeZone: timezone });
|
||||
// Ensure the timezone is valid
|
||||
const validTimezones = timeZones.map(tz => tz.value);
|
||||
if (validTimezones.includes(timezone)) {
|
||||
onConfigUpdate({ timeZone: timezone });
|
||||
} else {
|
||||
// Fallback to a valid timezone
|
||||
onConfigUpdate({ timeZone: 'America/New_York' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleContentDistributionChange = (distribution: 'even' | 'frontloaded' | 'backloaded') => {
|
||||
@@ -255,6 +361,308 @@ const CalendarConfigurationStep: React.FC<CalendarConfigurationStepProps> = ({
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Smart Defaults Section */}
|
||||
{smartDefaults && (
|
||||
<Card sx={{ ...ENHANCED_STYLES.card, mb: 3, background: 'linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%)' }}>
|
||||
<CardContent sx={ENHANCED_STYLES.cardContent}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Box sx={{
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<AutoAwesomeIcon sx={{ color: 'white', fontSize: 24 }} />
|
||||
</Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, color: '#2c3e50' }}>
|
||||
Smart Defaults
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => setShowSmartDefaults(!showSmartDefaults)}
|
||||
startIcon={<LightbulbIcon />}
|
||||
sx={{
|
||||
borderColor: '#667eea',
|
||||
color: '#667eea',
|
||||
'&:hover': { borderColor: '#764ba2', color: '#764ba2' }
|
||||
}}
|
||||
>
|
||||
{showSmartDefaults ? 'Hide' : 'Show'} Suggestions
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{showSmartDefaults && (
|
||||
<Box>
|
||||
<Typography variant="body2" color="#666" sx={{ mb: 3 }}>
|
||||
Based on your strategy data, here are our smart suggestions for optimal calendar configuration:
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2} sx={{ mb: 3 }}>
|
||||
<Grid item xs={12} md={3}>
|
||||
<Box sx={{ p: 2, bgcolor: 'rgba(255, 255, 255, 0.7)', borderRadius: 2, border: '1px solid rgba(102, 126, 234, 0.2)' }}>
|
||||
<Typography variant="caption" color="#666" display="block">Suggested Calendar Type</Typography>
|
||||
<Typography variant="body1" fontWeight={600} color="#2c3e50">
|
||||
{smartDefaults.suggestedCalendarType.charAt(0).toUpperCase() + smartDefaults.suggestedCalendarType.slice(1)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={3}>
|
||||
<Box sx={{ p: 2, bgcolor: 'rgba(255, 255, 255, 0.7)', borderRadius: 2, border: '1px solid rgba(102, 126, 234, 0.2)' }}>
|
||||
<Typography variant="caption" color="#666" display="block">Suggested Posts per Week</Typography>
|
||||
<Typography variant="body1" fontWeight={600} color="#2c3e50">
|
||||
{smartDefaults.suggestedPostingFrequency}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={3}>
|
||||
<Box sx={{ p: 2, bgcolor: 'rgba(255, 255, 255, 0.7)', borderRadius: 2, border: '1px solid rgba(102, 126, 234, 0.2)' }}>
|
||||
<Typography variant="caption" color="#666" display="block">Suggested Duration</Typography>
|
||||
<Typography variant="body1" fontWeight={600} color="#2c3e50">
|
||||
{smartDefaults.suggestedDuration} {smartDefaults.suggestedCalendarType === 'weekly' ? 'weeks' :
|
||||
smartDefaults.suggestedCalendarType === 'monthly' ? 'months' : 'quarters'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={3}>
|
||||
<Box sx={{ p: 2, bgcolor: 'rgba(255, 255, 255, 0.7)', borderRadius: 2, border: '1px solid rgba(102, 126, 234, 0.2)' }}>
|
||||
<Typography variant="caption" color="#666" display="block">Suggested Platforms</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mt: 0.5 }}>
|
||||
{smartDefaults.suggestedPlatforms.slice(0, 2).map((platform: string, index: number) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={platform}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: 'rgba(102, 126, 234, 0.1)',
|
||||
color: '#667eea',
|
||||
fontSize: '0.7rem'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{smartDefaults.suggestedPlatforms.length > 2 && (
|
||||
<Chip
|
||||
label={`+${smartDefaults.suggestedPlatforms.length - 2}`}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: 'rgba(102, 126, 234, 0.1)',
|
||||
color: '#667eea',
|
||||
fontSize: '0.7rem'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => handleApplySmartDefaults(false)}
|
||||
startIcon={<AutoAwesomeIcon />}
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
'&:hover': { background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)' }
|
||||
}}
|
||||
>
|
||||
Apply Smart Defaults
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => handleApplySmartDefaults(true)}
|
||||
sx={{
|
||||
borderColor: '#667eea',
|
||||
color: '#667eea',
|
||||
'&:hover': { borderColor: '#764ba2', color: '#764ba2' }
|
||||
}}
|
||||
>
|
||||
Apply All Suggestions
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* User Guidance Section */}
|
||||
{userGuidance && (
|
||||
<Card sx={{ ...ENHANCED_STYLES.card, mb: 3, background: 'rgba(255, 248, 220, 0.3)' }}>
|
||||
<CardContent sx={ENHANCED_STYLES.cardContent}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<Box sx={{
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
background: 'linear-gradient(135deg, #ff9800 0%, #f57c00 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<LightbulbIcon sx={{ color: 'white', fontSize: 24 }} />
|
||||
</Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, color: '#2c3e50' }}>
|
||||
Strategy Guidance
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{userGuidance.warnings.length > 0 && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<Box sx={{ p: 2, bgcolor: 'rgba(255, 193, 7, 0.1)', borderRadius: 2, border: '1px solid rgba(255, 193, 7, 0.3)' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||
<WarningIcon sx={{ color: '#f57c00', fontSize: 20 }} />
|
||||
<Typography variant="subtitle2" fontWeight={600} color="#f57c00">
|
||||
Warnings ({userGuidance.warnings.length})
|
||||
</Typography>
|
||||
</Box>
|
||||
{userGuidance.warnings.slice(0, 2).map((warning: any, index: number) => (
|
||||
<Typography key={index} variant="body2" color="#666" sx={{ mb: 0.5 }}>
|
||||
• {warning.message}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{userGuidance.recommendations.length > 0 && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<Box sx={{ p: 2, bgcolor: 'rgba(76, 175, 80, 0.1)', borderRadius: 2, border: '1px solid rgba(76, 175, 80, 0.3)' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||
<LightbulbIcon sx={{ color: '#2e7d32', fontSize: 20 }} />
|
||||
<Typography variant="subtitle2" fontWeight={600} color="#2e7d32">
|
||||
Recommendations ({userGuidance.recommendations.length})
|
||||
</Typography>
|
||||
</Box>
|
||||
{userGuidance.recommendations.slice(0, 2).map((rec: any, index: number) => (
|
||||
<Typography key={index} variant="body2" color="#666" sx={{ mb: 0.5 }}>
|
||||
• {rec.message}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{userGuidance.missingData.length > 0 && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<Box sx={{ p: 2, bgcolor: 'rgba(33, 150, 243, 0.1)', borderRadius: 2, border: '1px solid rgba(33, 150, 243, 0.3)' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||
<HelpIcon sx={{ color: '#1976d2', fontSize: 20 }} />
|
||||
<Typography variant="subtitle2" fontWeight={600} color="#1976d2">
|
||||
Missing Data ({userGuidance.missingData.length})
|
||||
</Typography>
|
||||
</Box>
|
||||
{userGuidance.missingData.slice(0, 2).map((missing: any, index: number) => (
|
||||
<Typography key={index} variant="body2" color="#666" sx={{ mb: 0.5 }}>
|
||||
• {missing.message}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Show success state when no issues */}
|
||||
{userGuidance.warnings.length === 0 && userGuidance.recommendations.length === 0 && userGuidance.missingData.length === 0 && (
|
||||
<Grid item xs={12}>
|
||||
<Box sx={{ p: 2, bgcolor: 'rgba(76, 175, 80, 0.1)', borderRadius: 2, border: '1px solid rgba(76, 175, 80, 0.3)', textAlign: 'center' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, mb: 1 }}>
|
||||
<CheckCircleIcon sx={{ color: '#2e7d32', fontSize: 20 }} />
|
||||
<Typography variant="subtitle2" fontWeight={600} color="#2e7d32">
|
||||
Strategy Analysis Complete
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="#666">
|
||||
Your strategy data is comprehensive and ready for calendar generation. No issues or missing data detected.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Transparency Indicators */}
|
||||
{transparencyIndicators && (
|
||||
<Card sx={{ ...ENHANCED_STYLES.card, mb: 3, background: 'rgba(240, 248, 255, 0.3)' }}>
|
||||
<CardContent sx={ENHANCED_STYLES.cardContent}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<Box sx={{
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
background: 'linear-gradient(135deg, #2196f3 0%, #1976d2 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<TrendingUpIcon sx={{ color: 'white', fontSize: 24 }} />
|
||||
</Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, color: '#2c3e50' }}>
|
||||
Strategy Integration Status
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ p: 2, bgcolor: 'rgba(255, 255, 255, 0.7)', borderRadius: 2 }}>
|
||||
<Typography variant="subtitle2" fontWeight={600} color="#2c3e50" gutterBottom>
|
||||
Integration Level
|
||||
</Typography>
|
||||
<Chip
|
||||
label={transparencyIndicators.integrationStatus.integrationLevel.toUpperCase()}
|
||||
color={
|
||||
transparencyIndicators.integrationStatus.integrationLevel === 'full' ? 'success' :
|
||||
transparencyIndicators.integrationStatus.integrationLevel === 'enhanced' ? 'primary' :
|
||||
transparencyIndicators.integrationStatus.integrationLevel === 'basic' ? 'warning' : 'default'
|
||||
}
|
||||
size="small"
|
||||
/>
|
||||
{transparencyIndicators.integrationStatus.integrationBenefits.length > 0 && (
|
||||
<Typography variant="body2" color="#666" sx={{ mt: 1 }}>
|
||||
{transparencyIndicators.integrationStatus.integrationBenefits[0]}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ p: 2, bgcolor: 'rgba(255, 255, 255, 0.7)', borderRadius: 2 }}>
|
||||
<Typography variant="subtitle2" fontWeight={600} color="#2c3e50" gutterBottom>
|
||||
Strategy Alignment
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={transparencyIndicators.strategyAlignment.alignmentScore}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
bgcolor: 'rgba(0, 0, 0, 0.1)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Typography variant="body2" color="#666">
|
||||
{transparencyIndicators.strategyAlignment.alignmentScore}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="#666">
|
||||
{transparencyIndicators.strategyAlignment.isAligned ? 'Strategy aligned' : 'Strategy not aligned'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Basic Calendar Setup */}
|
||||
<Card sx={{ ...ENHANCED_STYLES.card, mb: 3 }}>
|
||||
<CardContent sx={ENHANCED_STYLES.cardContent}>
|
||||
|
||||
@@ -441,21 +441,7 @@ const GenerateCalendarStep: React.FC<GenerateCalendarStepProps> = ({
|
||||
</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 */}
|
||||
{/* Note: Generate button is handled by the stepper navigation above */}
|
||||
{loading && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<LinearProgress />
|
||||
|
||||
@@ -260,6 +260,49 @@ export const useCalendarWizardState = (
|
||||
return [state, actions];
|
||||
};
|
||||
|
||||
// Helper function to get a valid timezone from user's timezone or fallback to default
|
||||
const getValidTimezone = (userTimezone: string): string => {
|
||||
const validTimezones = [
|
||||
'America/New_York',
|
||||
'America/Chicago',
|
||||
'America/Denver',
|
||||
'America/Los_Angeles',
|
||||
'Europe/London',
|
||||
'Europe/Paris',
|
||||
'Asia/Tokyo',
|
||||
'Asia/Shanghai',
|
||||
'Australia/Sydney'
|
||||
];
|
||||
|
||||
// If user's timezone is in our valid list, use it
|
||||
if (validTimezones.includes(userTimezone)) {
|
||||
return userTimezone;
|
||||
}
|
||||
|
||||
// Otherwise, try to map common timezones to our valid options
|
||||
const timezoneMap: { [key: string]: string } = {
|
||||
'Asia/Calcutta': 'Asia/Tokyo', // Map IST to JST as closest option
|
||||
'Asia/Kolkata': 'Asia/Tokyo', // Alternative IST name
|
||||
'Asia/Colombo': 'Asia/Tokyo', // Sri Lanka time
|
||||
'Asia/Dhaka': 'Asia/Tokyo', // Bangladesh time
|
||||
'Asia/Karachi': 'Asia/Tokyo', // Pakistan time
|
||||
'UTC': 'Europe/London', // UTC to GMT
|
||||
'GMT': 'Europe/London', // GMT to London
|
||||
'EST': 'America/New_York', // EST to Eastern Time
|
||||
'PST': 'America/Los_Angeles', // PST to Pacific Time
|
||||
'CST': 'America/Chicago', // CST to Central Time
|
||||
'MST': 'America/Denver', // MST to Mountain Time
|
||||
};
|
||||
|
||||
// Check if we have a mapping for the user's timezone
|
||||
if (timezoneMap[userTimezone]) {
|
||||
return timezoneMap[userTimezone];
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return 'America/New_York';
|
||||
};
|
||||
|
||||
// Helper function to create default calendar config
|
||||
const createDefaultConfig = (): CalendarConfig => {
|
||||
return {
|
||||
@@ -274,7 +317,7 @@ const createDefaultConfig = (): CalendarConfig => {
|
||||
|
||||
// Platform Scheduling
|
||||
priorityPlatforms: ['LinkedIn', 'Twitter'],
|
||||
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, // User's timezone
|
||||
timeZone: getValidTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone), // User's timezone or fallback
|
||||
|
||||
// Calendar Preferences
|
||||
excludeDates: [],
|
||||
|
||||
@@ -146,25 +146,25 @@ const HeaderSection: React.FC<HeaderSectionProps> = ({
|
||||
<Box sx={{ position: 'relative', zIndex: 1 }}>
|
||||
{/* Main Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
gutterBottom
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(45deg, #fff, #f0f0f0)',
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
textShadow: '0 0 20px rgba(255,255,255,0.5)',
|
||||
mb: 1
|
||||
}}
|
||||
>
|
||||
AI Content Strategy Co-pilot
|
||||
</Typography>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
gutterBottom
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(45deg, #fff, #f0f0f0)',
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
textShadow: '0 0 20px rgba(255,255,255,0.5)',
|
||||
mb: 1
|
||||
}}
|
||||
>
|
||||
AI Content Strategy Co-pilot
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ opacity: 0.9, fontSize: '0.9rem' }}>
|
||||
Build a comprehensive content strategy with 30+ strategic inputs
|
||||
</Typography>
|
||||
Build a comprehensive content strategy with 30+ strategic inputs
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -300,41 +300,41 @@ const HeaderSection: React.FC<HeaderSectionProps> = ({
|
||||
|
||||
{/* Enhanced Status Chips */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 2, flexWrap: 'wrap' }}>
|
||||
{cacheStatus === 'cached' && (
|
||||
<Chip
|
||||
icon={<CheckCircleIcon />}
|
||||
label={`${Object.keys(autoPopulatedFields).length} fields auto-populated`}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(76, 175, 80, 0.2)',
|
||||
color: 'white',
|
||||
border: '1px solid rgba(76, 175, 80, 0.3)',
|
||||
{cacheStatus === 'cached' && (
|
||||
<Chip
|
||||
icon={<CheckCircleIcon />}
|
||||
label={`${Object.keys(autoPopulatedFields).length} fields auto-populated`}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(76, 175, 80, 0.2)',
|
||||
color: 'white',
|
||||
border: '1px solid rgba(76, 175, 80, 0.3)',
|
||||
'& .MuiChip-icon': { color: 'rgba(76, 175, 80, 0.8)' },
|
||||
fontWeight: 500,
|
||||
fontSize: '0.8rem'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{dataSource && (
|
||||
<Tooltip title="Click to view data source information">
|
||||
<Chip
|
||||
icon={<InfoIcon />}
|
||||
label={`Source: ${dataSource}`}
|
||||
onClick={() => setShowDataInfo(!showDataInfo)}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
color: 'white',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 500,
|
||||
fontSize: '0.8rem',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
)}
|
||||
|
||||
{dataSource && (
|
||||
<Tooltip title="Click to view data source information">
|
||||
<Chip
|
||||
icon={<InfoIcon />}
|
||||
label={`Source: ${dataSource}`}
|
||||
onClick={() => setShowDataInfo(!showDataInfo)}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
color: 'white',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 500,
|
||||
fontSize: '0.8rem',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Category Distribution Chips */}
|
||||
{Object.keys(fieldCountByCategory).length > 0 && (
|
||||
@@ -351,155 +351,155 @@ const HeaderSection: React.FC<HeaderSectionProps> = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Data Source Information */}
|
||||
<Collapse in={showDataInfo}>
|
||||
<Alert
|
||||
severity="info"
|
||||
sx={{
|
||||
mb: 2,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
color: 'white',
|
||||
'& .MuiAlert-icon': { color: 'rgba(255, 255, 255, 0.8)' }
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Data Source:</strong> {dataSource || 'Onboarding Database'}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Input Data Points:</strong> {Object.keys(inputDataPoints).length} available
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<strong>Adaptive Monitoring:</strong> ALwrity continuously monitors databases for new data points to ensure you have the latest information.
|
||||
</Typography>
|
||||
</Alert>
|
||||
</Collapse>
|
||||
{/* Data Source Information */}
|
||||
<Collapse in={showDataInfo}>
|
||||
<Alert
|
||||
severity="info"
|
||||
sx={{
|
||||
mb: 2,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
color: 'white',
|
||||
'& .MuiAlert-icon': { color: 'rgba(255, 255, 255, 0.8)' }
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Data Source:</strong> {dataSource || 'Onboarding Database'}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Input Data Points:</strong> {Object.keys(inputDataPoints).length} available
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<strong>Adaptive Monitoring:</strong> ALwrity continuously monitors databases for new data points to ensure you have the latest information.
|
||||
</Typography>
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
{/* Conditional Action Buttons */}
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
{cacheStatus === 'cached' ? (
|
||||
// Case 1: Data exists in cache - show refresh vs continue options
|
||||
<>
|
||||
{/* Conditional Action Buttons */}
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
{cacheStatus === 'cached' ? (
|
||||
// Case 1: Data exists in cache - show refresh vs continue options
|
||||
<>
|
||||
<Tooltip title="Refresh with latest database data and AI analysis">
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={onRefreshAutofill}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
color: 'white',
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
'&:hover': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? 'Refreshing...' : 'Refresh & Autofill Inputs'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Continue with current autofilled values">
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<PlayArrowIcon />}
|
||||
onClick={onContinueWithPresent}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.3)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Continue with Present Values
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : cacheStatus === 'partial' ? (
|
||||
// Case 2: Partial data - show refresh option
|
||||
<Tooltip title="Refresh with latest database data and AI analysis">
|
||||
<Button
|
||||
variant="outlined"
|
||||
variant="contained"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={onRefreshAutofill}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 193, 7, 0.8)',
|
||||
color: 'white',
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
'&:hover': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
||||
backgroundColor: 'rgba(255, 193, 7, 0.9)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? 'Refreshing...' : 'Refresh & Autofill Strategy Inputs'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
// Case 3: No data - show initial autofill
|
||||
<Tooltip title="Fetch latest data from database and autofill strategy inputs">
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={onRefreshAutofill}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(76, 175, 80, 0.8)',
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(76, 175, 80, 0.9)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? 'Autofilling...' : 'Refresh & Autofill Strategy Inputs'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Next Step Button - shown after autofill completion */}
|
||||
{showNextButton && (
|
||||
<Tooltip title="Scroll to review section and mark inputs as reviewed">
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<ArrowDownwardIcon />}
|
||||
onClick={onScrollToReview}
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #4caf50 0%, #66bb6a 50%, #81c784 100%)',
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(135deg, #66bb6a 0%, #81c784 50%, #a5d6a7 100%)',
|
||||
transform: 'translateY(-1px)'
|
||||
},
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
Next: Review Strategy Inputs & Create Strategy
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Know More Details Button - shown when autofill data exists */}
|
||||
{hasAutofillData && Object.keys(autoPopulatedFields).length > 0 && (
|
||||
<Tooltip title="View detailed information about autofill data sources and AI analysis">
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={<VisibilityIcon />}
|
||||
onClick={() => setShowTransparencyModal(true)}
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
'&:hover': {
|
||||
color: 'white',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? 'Refreshing...' : 'Refresh & Autofill Inputs'}
|
||||
Know More Details
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Continue with current autofilled values">
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<PlayArrowIcon />}
|
||||
onClick={onContinueWithPresent}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.3)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Continue with Present Values
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : cacheStatus === 'partial' ? (
|
||||
// Case 2: Partial data - show refresh option
|
||||
<Tooltip title="Refresh with latest database data and AI analysis">
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={onRefreshAutofill}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 193, 7, 0.8)',
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 193, 7, 0.9)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? 'Refreshing...' : 'Refresh & Autofill Strategy Inputs'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
// Case 3: No data - show initial autofill
|
||||
<Tooltip title="Fetch latest data from database and autofill strategy inputs">
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={onRefreshAutofill}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(76, 175, 80, 0.8)',
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(76, 175, 80, 0.9)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? 'Autofilling...' : 'Refresh & Autofill Strategy Inputs'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Next Step Button - shown after autofill completion */}
|
||||
{showNextButton && (
|
||||
<Tooltip title="Scroll to review section and mark inputs as reviewed">
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<ArrowDownwardIcon />}
|
||||
onClick={onScrollToReview}
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #4caf50 0%, #66bb6a 50%, #81c784 100%)',
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(135deg, #66bb6a 0%, #81c784 50%, #a5d6a7 100%)',
|
||||
transform: 'translateY(-1px)'
|
||||
},
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
Next: Review Strategy Inputs & Create Strategy
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Know More Details Button - shown when autofill data exists */}
|
||||
{hasAutofillData && Object.keys(autoPopulatedFields).length > 0 && (
|
||||
<Tooltip title="View detailed information about autofill data sources and AI analysis">
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={<VisibilityIcon />}
|
||||
onClick={() => setShowTransparencyModal(true)}
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
'&:hover': {
|
||||
color: 'white',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Know More Details
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Typography,
|
||||
Grid,
|
||||
Avatar,
|
||||
Chip
|
||||
} from '@mui/material';
|
||||
import {
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
Speed as SpeedIcon,
|
||||
BugReport as BugReportIcon,
|
||||
Storage as StorageIcon,
|
||||
Timeline as TimelineIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||
BarChart, Bar, PieChart, Pie, Cell, AreaChart, Area, RadarChart, PolarGrid,
|
||||
PolarAngleAxis, PolarRadiusAxis, Radar
|
||||
} from 'recharts';
|
||||
|
||||
interface ChartData {
|
||||
name: string;
|
||||
requests: number;
|
||||
avgTime: number;
|
||||
errors: number;
|
||||
hitRate: number;
|
||||
}
|
||||
|
||||
interface MonitoringChartsProps {
|
||||
chartData: ChartData[];
|
||||
cachePerformance: {
|
||||
hits: number;
|
||||
misses: number;
|
||||
hit_rate: number;
|
||||
};
|
||||
apiPerformance: {
|
||||
recent_requests: number;
|
||||
recent_errors: number;
|
||||
error_rate: number;
|
||||
};
|
||||
}
|
||||
|
||||
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8'];
|
||||
|
||||
const MonitoringCharts: React.FC<MonitoringChartsProps> = ({
|
||||
chartData,
|
||||
cachePerformance,
|
||||
apiPerformance
|
||||
}) => {
|
||||
// Generate time series data for line chart
|
||||
const timeSeriesData = chartData.map((item, index) => ({
|
||||
time: `${index + 1}`,
|
||||
requests: item.requests,
|
||||
errors: item.errors,
|
||||
avgTime: item.avgTime * 1000, // Convert to milliseconds for better visualization
|
||||
}));
|
||||
|
||||
// Generate radar chart data
|
||||
const radarData = [
|
||||
{ metric: 'Performance', value: 100 - apiPerformance.error_rate },
|
||||
{ metric: 'Reliability', value: 100 - (apiPerformance.recent_errors / Math.max(apiPerformance.recent_requests, 1)) * 100 },
|
||||
{ metric: 'Cache Hit Rate', value: cachePerformance.hit_rate },
|
||||
{ metric: 'Response Time', value: Math.max(0, 100 - (chartData.reduce((acc, item) => acc + item.avgTime, 0) / Math.max(chartData.length, 1)) * 1000) },
|
||||
{ metric: 'Error Rate', value: Math.max(0, 100 - apiPerformance.error_rate) },
|
||||
];
|
||||
|
||||
// Generate pie chart data for cache performance
|
||||
const cacheData = [
|
||||
{ name: 'Cache Hits', value: cachePerformance.hits, color: '#00C49F' },
|
||||
{ name: 'Cache Misses', value: cachePerformance.misses, color: '#FF8042' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
{/* Line Chart - Request Trends */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<Card sx={{ height: '100%', background: 'rgba(255,255,255,0.95)' }}>
|
||||
<CardHeader
|
||||
avatar={<Avatar sx={{ bgcolor: 'primary.main' }}><TimelineIcon /></Avatar>}
|
||||
title="Request Trends"
|
||||
subheader="Real-time request and error patterns"
|
||||
/>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<LineChart data={timeSeriesData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="time" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="requests"
|
||||
stroke="#8884d8"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: '#8884d8', strokeWidth: 2, r: 4 }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="errors"
|
||||
stroke="#ff0000"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: '#ff0000', strokeWidth: 2, r: 4 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
|
||||
{/* Area Chart - Response Times */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<Card sx={{ height: '100%', background: 'rgba(255,255,255,0.95)' }}>
|
||||
<CardHeader
|
||||
avatar={<Avatar sx={{ bgcolor: 'info.main' }}><SpeedIcon /></Avatar>}
|
||||
title="Response Times"
|
||||
subheader="Average response time trends"
|
||||
/>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<AreaChart data={timeSeriesData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="time" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="avgTime"
|
||||
stroke="#82ca9d"
|
||||
fill="#82ca9d"
|
||||
fillOpacity={0.6}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
|
||||
{/* Bar Chart - Endpoint Performance */}
|
||||
<Grid item xs={12} md={8}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
<Card sx={{ background: 'rgba(255,255,255,0.95)' }}>
|
||||
<CardHeader
|
||||
avatar={<Avatar sx={{ bgcolor: 'success.main' }}><TrendingUpIcon /></Avatar>}
|
||||
title="Endpoint Performance"
|
||||
subheader="Request volume and error distribution"
|
||||
/>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Bar dataKey="requests" fill="#8884d8" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="errors" fill="#ff0000" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
|
||||
{/* Pie Chart - Cache Performance */}
|
||||
<Grid item xs={12} md={4}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
>
|
||||
<Card sx={{ height: '100%', background: 'rgba(255,255,255,0.95)' }}>
|
||||
<CardHeader
|
||||
avatar={<Avatar sx={{ bgcolor: 'warning.main' }}><StorageIcon /></Avatar>}
|
||||
title="Cache Performance"
|
||||
subheader="Hit vs Miss distribution"
|
||||
/>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={cacheData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ name, percent }) => `${name} ${((percent || 0) * 100).toFixed(0)}%`}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
>
|
||||
{cacheData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<Box sx={{ textAlign: 'center', mt: 2 }}>
|
||||
<Typography variant="h6" color="primary">
|
||||
{cachePerformance.hit_rate.toFixed(1)}%
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Overall Hit Rate
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
|
||||
{/* Radar Chart - System Health */}
|
||||
<Grid item xs={12}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
>
|
||||
<Card sx={{ background: 'rgba(255,255,255,0.95)' }}>
|
||||
<CardHeader
|
||||
avatar={<Avatar sx={{ bgcolor: 'error.main' }}><BugReportIcon /></Avatar>}
|
||||
title="System Health Overview"
|
||||
subheader="Multi-dimensional performance metrics"
|
||||
action={
|
||||
<Chip
|
||||
label={`${100 - apiPerformance.error_rate}% Healthy`}
|
||||
color={apiPerformance.error_rate > 5 ? 'error' : 'success'}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<RadarChart data={radarData}>
|
||||
<PolarGrid />
|
||||
<PolarAngleAxis dataKey="metric" />
|
||||
<PolarRadiusAxis angle={90} domain={[0, 100]} />
|
||||
<Radar
|
||||
name="Performance"
|
||||
dataKey="value"
|
||||
stroke="#8884d8"
|
||||
fill="#8884d8"
|
||||
fillOpacity={0.6}
|
||||
/>
|
||||
<Tooltip />
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
|
||||
{/* Performance Metrics Cards */}
|
||||
<Grid item xs={12}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Card sx={{ background: 'rgba(255,255,255,0.95)' }}>
|
||||
<CardContent sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="h4" color="primary" gutterBottom>
|
||||
{apiPerformance.recent_requests}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Recent Requests
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Card sx={{ background: 'rgba(255,255,255,0.95)' }}>
|
||||
<CardContent sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="h4" color="error" gutterBottom>
|
||||
{apiPerformance.recent_errors}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Recent Errors
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Card sx={{ background: 'rgba(255,255,255,0.95)' }}>
|
||||
<CardContent sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="h4" color="success.main" gutterBottom>
|
||||
{cachePerformance.hit_rate.toFixed(1)}%
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Cache Hit Rate
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Card sx={{ background: 'rgba(255,255,255,0.95)' }}>
|
||||
<CardContent sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="h4" color="info.main" gutterBottom>
|
||||
{chartData.length}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Active Endpoints
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonitoringCharts;
|
||||
@@ -7,12 +7,7 @@ import {
|
||||
Chip,
|
||||
IconButton,
|
||||
Collapse,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Alert,
|
||||
Button
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
@@ -26,223 +21,171 @@ import {
|
||||
CalendarToday as CalendarIcon,
|
||||
HealthAndSafety as HealthIcon
|
||||
} from '@mui/icons-material';
|
||||
import { ServiceStatus } from '../../../services/contentPlanningOrchestrator';
|
||||
|
||||
interface ProgressIndicatorProps {
|
||||
serviceStatuses: ServiceStatus[];
|
||||
onRefreshService: (serviceName: string) => void;
|
||||
expanded?: boolean;
|
||||
onToggleExpanded?: () => void;
|
||||
}
|
||||
|
||||
const ProgressIndicator: React.FC<ProgressIndicatorProps> = ({
|
||||
serviceStatuses,
|
||||
onRefreshService,
|
||||
expanded = false,
|
||||
onToggleExpanded
|
||||
}) => {
|
||||
const getServiceIcon = (serviceName: string) => {
|
||||
switch (serviceName) {
|
||||
case 'Content Strategies':
|
||||
return <StrategyIcon />;
|
||||
case 'Gap Analysis':
|
||||
return <SearchIcon />;
|
||||
case 'AI Analytics':
|
||||
return <AnalyticsIcon />;
|
||||
case 'Calendar Events':
|
||||
return <CalendarIcon />;
|
||||
case 'System Health':
|
||||
return <HealthIcon />;
|
||||
default:
|
||||
return <AnalyticsIcon />;
|
||||
}
|
||||
// Simple loading state for dashboard initialization
|
||||
const [loadingProgress, setLoadingProgress] = React.useState(0);
|
||||
const [loadingMessage, setLoadingMessage] = React.useState('Initializing dashboard...');
|
||||
|
||||
React.useEffect(() => {
|
||||
// Simulate loading progress
|
||||
const interval = setInterval(() => {
|
||||
setLoadingProgress(prev => {
|
||||
if (prev >= 100) {
|
||||
clearInterval(interval);
|
||||
return 100;
|
||||
}
|
||||
return prev + 10;
|
||||
});
|
||||
}, 200);
|
||||
|
||||
// Update loading messages
|
||||
const messageInterval = setInterval(() => {
|
||||
setLoadingMessage(prev => {
|
||||
if (prev.includes('Loading content strategies...')) {
|
||||
return 'Loading calendar data...';
|
||||
} else if (prev.includes('Loading calendar data...')) {
|
||||
return 'Loading analytics...';
|
||||
} else if (prev.includes('Loading analytics...')) {
|
||||
return 'Loading gap analysis...';
|
||||
} else if (prev.includes('Loading gap analysis...')) {
|
||||
return 'Dashboard ready!';
|
||||
}
|
||||
return 'Loading content strategies...';
|
||||
});
|
||||
}, 800);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
clearInterval(messageInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getStatusColor = () => {
|
||||
if (loadingProgress >= 100) return 'success';
|
||||
if (loadingProgress >= 75) return 'primary';
|
||||
if (loadingProgress >= 50) return 'warning';
|
||||
return 'info';
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return 'success';
|
||||
case 'error':
|
||||
return 'error';
|
||||
case 'loading':
|
||||
return 'primary';
|
||||
default:
|
||||
return 'primary';
|
||||
}
|
||||
const getStatusIcon = () => {
|
||||
if (loadingProgress >= 100) return <CheckCircleIcon color="success" />;
|
||||
return <RefreshIcon sx={{ animation: 'spin 1s linear infinite' }} />;
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return <CheckCircleIcon color="success" />;
|
||||
case 'error':
|
||||
return <ErrorIcon color="error" />;
|
||||
case 'loading':
|
||||
return <RefreshIcon sx={{ animation: 'spin 1s linear infinite' }} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = serviceStatuses.some(status => status.status === 'loading');
|
||||
const hasErrors = serviceStatuses.some(status => status.status === 'error');
|
||||
const allComplete = serviceStatuses.every(status => status.status === 'success');
|
||||
|
||||
const overallProgress = serviceStatuses.reduce((acc, status) => acc + status.progress, 0) / serviceStatuses.length;
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
p: 2,
|
||||
mb: 2,
|
||||
border: hasErrors ? '1px solid #f44336' : '1px solid transparent',
|
||||
backgroundColor: hasErrors ? 'rgba(244, 67, 54, 0.05)' : 'background.paper',
|
||||
'@keyframes spin': {
|
||||
from: { transform: 'rotate(0deg)' },
|
||||
to: { transform: 'rotate(360deg)' }
|
||||
}
|
||||
border: '1px solid transparent',
|
||||
backgroundColor: 'background.paper'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{isLoading && <RefreshIcon sx={{ animation: 'spin 1s linear infinite' }} />}
|
||||
Content Planning Progress
|
||||
{allComplete && <CheckCircleIcon color="success" />}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Chip
|
||||
label={`${Math.round(overallProgress)}%`}
|
||||
color={allComplete ? 'success' : isLoading ? 'primary' : 'default'}
|
||||
{getStatusIcon()}
|
||||
<Typography variant="subtitle2">
|
||||
Dashboard Loading
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`${loadingProgress}%`}
|
||||
size="small"
|
||||
color={getStatusColor()}
|
||||
variant="outlined"
|
||||
/>
|
||||
{onToggleExpanded && (
|
||||
<IconButton size="small" onClick={onToggleExpanded}>
|
||||
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
{onToggleExpanded && (
|
||||
<IconButton size="small" onClick={onToggleExpanded}>
|
||||
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Overall Progress Bar */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={overallProgress}
|
||||
color={allComplete ? 'success' : isLoading ? 'primary' : 'inherit'}
|
||||
sx={{ height: 8, borderRadius: 4 }}
|
||||
/>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={loadingProgress}
|
||||
color={getStatusColor()}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
{/* Status Messages */}
|
||||
{isLoading && (
|
||||
<Alert severity="info" sx={{ mb: 2 }}>
|
||||
<Typography variant="body2">
|
||||
Loading content planning data... This may take a few moments as we analyze your content strategy.
|
||||
</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{loadingMessage}
|
||||
</Typography>
|
||||
|
||||
{hasErrors && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
<Typography variant="body2">
|
||||
Some services encountered errors. You can refresh individual services below.
|
||||
</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{allComplete && (
|
||||
<Alert severity="success" sx={{ mb: 2 }}>
|
||||
<Typography variant="body2">
|
||||
All content planning services are ready! Your dashboard is fully loaded.
|
||||
</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Detailed Service Status */}
|
||||
<Collapse in={expanded}>
|
||||
<List dense>
|
||||
{serviceStatuses.map((status, index) => (
|
||||
<ListItem
|
||||
key={index}
|
||||
sx={{
|
||||
border: '1px solid',
|
||||
borderColor: getStatusColor(status.status) === 'error' ? 'error.main' : 'divider',
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
backgroundColor: getStatusColor(status.status) === 'error' ? 'rgba(244, 67, 54, 0.05)' : 'transparent'
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
{getServiceIcon(status.name)}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{status.name}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{getStatusIcon(status.status)}
|
||||
<Chip
|
||||
label={`${status.progress}%`}
|
||||
size="small"
|
||||
color={getStatusColor(status.status)}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{status.message}
|
||||
</Typography>
|
||||
{status.error && (
|
||||
<Typography variant="caption" color="error" sx={{ display: 'block', mt: 0.5 }}>
|
||||
Error: {status.error}
|
||||
</Typography>
|
||||
)}
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={status.progress}
|
||||
color={getStatusColor(status.status)}
|
||||
sx={{ height: 4, borderRadius: 2 }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Loading Components:
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<StrategyIcon fontSize="small" />
|
||||
<Typography variant="body2">Content Strategy</Typography>
|
||||
<Chip
|
||||
label={loadingProgress >= 20 ? "✓" : "..."}
|
||||
size="small"
|
||||
color={loadingProgress >= 20 ? "success" : "default"}
|
||||
/>
|
||||
{status.status === 'error' && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onRefreshService(status.name.toLowerCase().replace(' ', ''))}
|
||||
color="primary"
|
||||
>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<CalendarIcon fontSize="small" />
|
||||
<Typography variant="body2">Calendar</Typography>
|
||||
<Chip
|
||||
label={loadingProgress >= 40 ? "✓" : "..."}
|
||||
size="small"
|
||||
color={loadingProgress >= 40 ? "success" : "default"}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<AnalyticsIcon fontSize="small" />
|
||||
<Typography variant="body2">Analytics</Typography>
|
||||
<Chip
|
||||
label={loadingProgress >= 60 ? "✓" : "..."}
|
||||
size="small"
|
||||
color={loadingProgress >= 60 ? "success" : "default"}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<SearchIcon fontSize="small" />
|
||||
<Typography variant="body2">Gap Analysis</Typography>
|
||||
<Chip
|
||||
label={loadingProgress >= 80 ? "✓" : "..."}
|
||||
size="small"
|
||||
color={loadingProgress >= 80 ? "success" : "default"}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<HealthIcon fontSize="small" />
|
||||
<Typography variant="body2">System Health</Typography>
|
||||
<Chip
|
||||
label={loadingProgress >= 100 ? "✓" : "..."}
|
||||
size="small"
|
||||
color={loadingProgress >= 100 ? "success" : "default"}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Collapse>
|
||||
|
||||
{/* Quick Actions */}
|
||||
{hasErrors && (
|
||||
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => serviceStatuses.forEach(status => {
|
||||
if (status.status === 'error') {
|
||||
onRefreshService(status.name.toLowerCase().replace(' ', ''));
|
||||
}
|
||||
})}
|
||||
>
|
||||
Refresh All Failed Services
|
||||
</Button>
|
||||
</Box>
|
||||
{loadingProgress >= 100 && (
|
||||
<Alert severity="success" sx={{ mt: 2 }}>
|
||||
Dashboard loaded successfully! You can now start using all features.
|
||||
</Alert>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
IconButton,
|
||||
Chip,
|
||||
Collapse,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Error as ErrorIcon,
|
||||
Refresh as RefreshIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
ExpandLess as ExpandLessIcon,
|
||||
Warning as WarningIcon
|
||||
} from '@mui/icons-material';
|
||||
import { ServiceStatus } from '../../../services/contentPlanningOrchestrator';
|
||||
|
||||
interface ServiceStatusPanelProps {
|
||||
serviceStatuses: ServiceStatus[];
|
||||
onRefreshService: (serviceName: string) => void;
|
||||
expanded: boolean;
|
||||
onToggleExpanded: () => void;
|
||||
}
|
||||
|
||||
const ServiceStatusPanel: React.FC<ServiceStatusPanelProps> = ({
|
||||
serviceStatuses,
|
||||
onRefreshService,
|
||||
expanded,
|
||||
onToggleExpanded
|
||||
}) => {
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success': return 'success';
|
||||
case 'error': return 'error';
|
||||
case 'loading': return 'primary';
|
||||
default: return 'primary';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success': return <CheckCircleIcon fontSize="small" />;
|
||||
case 'error': return <ErrorIcon fontSize="small" />;
|
||||
case 'loading': return <WarningIcon fontSize="small" />;
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getOverallStatus = () => {
|
||||
const hasErrors = serviceStatuses.some(s => s.status === 'error');
|
||||
const hasLoading = serviceStatuses.some(s => s.status === 'loading');
|
||||
const allSuccess = serviceStatuses.every(s => s.status === 'success');
|
||||
|
||||
if (hasErrors) return { status: 'error', text: 'Some services failed' };
|
||||
if (hasLoading) return { status: 'loading', text: 'Services loading' };
|
||||
if (allSuccess) return { status: 'success', text: 'All services operational' };
|
||||
return { status: 'idle', text: 'Services idle' };
|
||||
};
|
||||
|
||||
const overallStatus = getOverallStatus();
|
||||
|
||||
return (
|
||||
<Paper sx={{ mb: 2 }}>
|
||||
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{getStatusIcon(overallStatus.status)}
|
||||
<Typography variant="subtitle2">
|
||||
System Status: {overallStatus.text}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`${serviceStatuses.filter(s => s.status === 'success').length}/${serviceStatuses.length}`}
|
||||
size="small"
|
||||
color={getStatusColor(overallStatus.status)}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
<IconButton size="small" onClick={onToggleExpanded}>
|
||||
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Collapse in={expanded}>
|
||||
<Box sx={{ p: 2 }}>
|
||||
{serviceStatuses.map((service) => (
|
||||
<Box key={service.name} sx={{ mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{getStatusIcon(service.status)}
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{service.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{service.progress}%
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onRefreshService(service.name.toLowerCase().replace(/\s+/g, ''))}
|
||||
disabled={service.status === 'loading'}
|
||||
>
|
||||
<RefreshIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={service.progress}
|
||||
color={getStatusColor(service.status)}
|
||||
sx={{ mb: 1 }}
|
||||
/>
|
||||
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{service.message}
|
||||
</Typography>
|
||||
|
||||
{service.error && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{service.error}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceStatusPanel;
|
||||
@@ -26,6 +26,8 @@ import { contentPlanningApi } from '../../../../../services/contentPlanningApi';
|
||||
import EnhancedStrategyActivationButton from './EnhancedStrategyActivationButton';
|
||||
import { StrategyData } from '../types/strategy.types';
|
||||
import { useNavigationOrchestrator } from '../../../../../services/navigationOrchestrator';
|
||||
import { useStrategyCalendarContext } from '../../../../../contexts/StrategyCalendarContext';
|
||||
import { useStrategyBuilderStore } from '../../../../../stores/strategyBuilderStore';
|
||||
|
||||
interface ReviewProgressHeaderProps {
|
||||
strategyData?: StrategyData | null;
|
||||
@@ -44,6 +46,12 @@ const ReviewProgressHeader: React.FC<ReviewProgressHeaderProps> = ({ strategyDat
|
||||
|
||||
// Initialize navigation orchestrator
|
||||
const navigationOrchestrator = useNavigationOrchestrator();
|
||||
|
||||
// Get strategy calendar context
|
||||
const { setStrategyContext } = useStrategyCalendarContext();
|
||||
|
||||
// Get actual strategy data from strategy builder store
|
||||
const strategyBuilderData = useStrategyBuilderStore(state => state.currentStrategy);
|
||||
|
||||
// Extract domain name from strategy data
|
||||
const getDomainName = () => {
|
||||
@@ -96,16 +104,37 @@ const ReviewProgressHeader: React.FC<ReviewProgressHeaderProps> = ({ strategyDat
|
||||
|
||||
const handleGenerateContentCalendar = () => {
|
||||
|
||||
// Use actual strategy data from strategy builder store
|
||||
const actualStrategyData = strategyBuilderData || strategyData;
|
||||
|
||||
console.log('🎯 ReviewProgressHeader: Strategy data for calendar generation:', {
|
||||
strategyBuilderData: !!strategyBuilderData,
|
||||
strategyData: !!strategyData,
|
||||
actualStrategyData: !!actualStrategyData
|
||||
});
|
||||
|
||||
// Prepare strategy context for navigation
|
||||
const strategyContext = {
|
||||
strategyId: (strategyData?.strategy_metadata?.user_id || strategyData?.metadata?.user_id || '1').toString(),
|
||||
strategyData: strategyData,
|
||||
strategyId: (() => {
|
||||
if (actualStrategyData && 'id' in actualStrategyData) {
|
||||
return actualStrategyData.id.toString();
|
||||
} else if (actualStrategyData && 'strategy_metadata' in actualStrategyData) {
|
||||
return actualStrategyData.strategy_metadata?.user_id?.toString() || '1';
|
||||
} else if (actualStrategyData && 'metadata' in actualStrategyData) {
|
||||
return actualStrategyData.metadata?.user_id?.toString() || '1';
|
||||
}
|
||||
return '1';
|
||||
})(),
|
||||
strategyData: actualStrategyData,
|
||||
activationStatus: 'active' as const,
|
||||
activationTimestamp: new Date().toISOString(),
|
||||
userPreferences: {},
|
||||
strategicIntelligence: {}
|
||||
};
|
||||
|
||||
// Set strategy context in the StrategyCalendarContext
|
||||
setStrategyContext(strategyContext);
|
||||
|
||||
// Navigate to calendar wizard using navigation orchestrator
|
||||
navigationOrchestrator.navigateToCalendarWizard(
|
||||
strategyContext.strategyId,
|
||||
|
||||
@@ -19,6 +19,8 @@ import { useStrategyReviewStore, StrategyComponent } from '../../../../../stores
|
||||
import { ANALYSIS_CARD_STYLES } from '../styles';
|
||||
import EnhancedStrategyActivationButton from './EnhancedStrategyActivationButton';
|
||||
import { useNavigationOrchestrator } from '../../../../../services/navigationOrchestrator';
|
||||
import { useStrategyCalendarContext } from '../../../../../contexts/StrategyCalendarContext';
|
||||
import { useStrategyBuilderStore } from '../../../../../stores/strategyBuilderStore';
|
||||
|
||||
interface ReviewProgressSectionProps {
|
||||
strategyData: StrategyData;
|
||||
@@ -38,6 +40,12 @@ const ReviewProgressSection: React.FC<ReviewProgressSectionProps> = ({ strategyD
|
||||
|
||||
// Initialize navigation orchestrator
|
||||
const navigationOrchestrator = useNavigationOrchestrator();
|
||||
|
||||
// Get strategy calendar context
|
||||
const { setStrategyContext } = useStrategyCalendarContext();
|
||||
|
||||
// Get actual strategy data from strategy builder store
|
||||
const strategyBuilderData = useStrategyBuilderStore(state => state.currentStrategy);
|
||||
|
||||
// Extract domain name from strategy data
|
||||
const getDomainName = () => {
|
||||
@@ -102,16 +110,37 @@ const ReviewProgressSection: React.FC<ReviewProgressSectionProps> = ({ strategyD
|
||||
const handleGenerateContentCalendar = () => {
|
||||
console.log('🎯 Generate content calendar clicked');
|
||||
|
||||
// Use actual strategy data from strategy builder store
|
||||
const actualStrategyData = strategyBuilderData || strategyData;
|
||||
|
||||
console.log('🎯 ReviewProgressSection: Strategy data for calendar generation:', {
|
||||
strategyBuilderData: !!strategyBuilderData,
|
||||
strategyData: !!strategyData,
|
||||
actualStrategyData: !!actualStrategyData
|
||||
});
|
||||
|
||||
// Prepare strategy context for navigation
|
||||
const strategyContext = {
|
||||
strategyId: (strategyData?.strategy_metadata?.user_id || strategyData?.metadata?.user_id || '1').toString(),
|
||||
strategyData: strategyData,
|
||||
strategyId: (() => {
|
||||
if (actualStrategyData && 'id' in actualStrategyData) {
|
||||
return actualStrategyData.id.toString();
|
||||
} else if (actualStrategyData && 'strategy_metadata' in actualStrategyData) {
|
||||
return actualStrategyData.strategy_metadata?.user_id?.toString() || '1';
|
||||
} else if (actualStrategyData && 'metadata' in actualStrategyData) {
|
||||
return actualStrategyData.metadata?.user_id?.toString() || '1';
|
||||
}
|
||||
return '1';
|
||||
})(),
|
||||
strategyData: actualStrategyData,
|
||||
activationStatus: 'active' as const,
|
||||
activationTimestamp: new Date().toISOString(),
|
||||
userPreferences: {},
|
||||
strategicIntelligence: {}
|
||||
};
|
||||
|
||||
// Set strategy context in the StrategyCalendarContext
|
||||
setStrategyContext(strategyContext);
|
||||
|
||||
// Navigate to calendar wizard using navigation orchestrator
|
||||
navigationOrchestrator.navigateToCalendarWizard(
|
||||
strategyContext.strategyId,
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Grid,
|
||||
Paper,
|
||||
LinearProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
Divider,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Avatar,
|
||||
Badge
|
||||
} from '@mui/material';
|
||||
import {
|
||||
CheckCircle as HealthyIcon,
|
||||
Warning as WarningIcon,
|
||||
Error as CriticalIcon,
|
||||
Help as UnknownIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Close as CloseIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
Speed as SpeedIcon,
|
||||
BugReport as BugReportIcon,
|
||||
Storage as StorageIcon,
|
||||
Timeline as TimelineIcon,
|
||||
Analytics as AnalyticsIcon,
|
||||
NetworkCheck as NetworkCheckIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import MonitoringCharts from './MonitoringCharts';
|
||||
|
||||
interface SystemStatusData {
|
||||
status: 'healthy' | 'warning' | 'critical' | 'unknown';
|
||||
icon: string;
|
||||
recent_requests: number;
|
||||
recent_errors: number;
|
||||
error_rate: number;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface DetailedStatsData {
|
||||
overview: {
|
||||
total_requests: number;
|
||||
total_errors: number;
|
||||
recent_requests: number;
|
||||
recent_errors: number;
|
||||
};
|
||||
cache_performance: {
|
||||
hits: number;
|
||||
misses: number;
|
||||
hit_rate: number;
|
||||
};
|
||||
top_endpoints: Array<{
|
||||
endpoint: string;
|
||||
count: number;
|
||||
avg_time: number;
|
||||
errors: number;
|
||||
last_called: string;
|
||||
cache_hit_rate: number;
|
||||
}>;
|
||||
recent_errors: Array<{
|
||||
timestamp: string;
|
||||
path: string;
|
||||
method: string;
|
||||
status_code: number;
|
||||
duration: number;
|
||||
}>;
|
||||
system_health: {
|
||||
status: string;
|
||||
error_rate: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface SystemStatusIndicatorProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SystemStatusIndicator: React.FC<SystemStatusIndicatorProps> = ({ className }) => {
|
||||
const [statusData, setStatusData] = useState<SystemStatusData | null>(null);
|
||||
const [detailedStats, setDetailedStats] = useState<DetailedStatsData | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [dashboardOpen, setDashboardOpen] = useState(false);
|
||||
const [chartData, setChartData] = useState<any[]>([]);
|
||||
|
||||
const fetchStatus = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/content-planning/monitoring/lightweight-stats');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch system status');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (result.status === 'success') {
|
||||
setStatusData(result.data);
|
||||
} else {
|
||||
throw new Error(result.message || 'Failed to get system status');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
setStatusData({
|
||||
status: 'unknown',
|
||||
icon: '⚪',
|
||||
recent_requests: 0,
|
||||
recent_errors: 0,
|
||||
error_rate: 0,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDetailedStats = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/content-planning/monitoring/api-stats');
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.status === 'success') {
|
||||
setDetailedStats(result.data);
|
||||
|
||||
// Generate chart data
|
||||
const chartData = result.data.top_endpoints.slice(0, 5).map((endpoint: any, index: number) => ({
|
||||
name: endpoint.endpoint.split(' ')[1].split('/').pop() || 'API',
|
||||
requests: endpoint.count,
|
||||
avgTime: endpoint.avg_time,
|
||||
errors: endpoint.errors,
|
||||
hitRate: endpoint.cache_hit_rate
|
||||
}));
|
||||
setChartData(chartData);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching detailed stats:', err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatus();
|
||||
|
||||
// Refresh every 30 seconds
|
||||
const interval = setInterval(fetchStatus, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (dashboardOpen) {
|
||||
fetchDetailedStats();
|
||||
const interval = setInterval(fetchDetailedStats, 10000); // Refresh every 10 seconds when dashboard is open
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [dashboardOpen]);
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
return <HealthyIcon sx={{ color: 'success.main', fontSize: 20 }} />;
|
||||
case 'warning':
|
||||
return <WarningIcon sx={{ color: 'warning.main', fontSize: 20 }} />;
|
||||
case 'critical':
|
||||
return <CriticalIcon sx={{ color: 'error.main', fontSize: 20 }} />;
|
||||
default:
|
||||
return <UnknownIcon sx={{ color: 'grey.500', fontSize: 20 }} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
return 'success';
|
||||
case 'warning':
|
||||
return 'warning';
|
||||
case 'critical':
|
||||
return 'error';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const formatTimestamp = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleTimeString();
|
||||
};
|
||||
|
||||
const tooltipContent = statusData ? (
|
||||
<Box sx={{ p: 1, maxWidth: 300 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
System Status: {statusData.status.toUpperCase()}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
Recent Requests: {statusData.recent_requests}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
Recent Errors: {statusData.recent_errors}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
Error Rate: {statusData.error_rate}%
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Last Updated: {formatTimestamp(statusData.timestamp)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="primary" sx={{ display: 'block', mt: 1 }}>
|
||||
Click for detailed dashboard
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography>Loading system status...</Typography>
|
||||
);
|
||||
|
||||
const handleDashboardClick = () => {
|
||||
console.log('Dashboard clicked, setting dashboardOpen to true');
|
||||
setDashboardOpen(true);
|
||||
};
|
||||
|
||||
if (loading && !statusData) {
|
||||
return (
|
||||
<Box className={className} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<CircularProgress size={16} />
|
||||
<Typography variant="caption">System Status</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box className={className} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Tooltip title={tooltipContent} arrow placement="bottom">
|
||||
<Box
|
||||
sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={handleDashboardClick}
|
||||
>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
{statusData ? getStatusIcon(statusData.status) : <UnknownIcon />}
|
||||
</motion.div>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
|
||||
<Chip
|
||||
label={statusData?.status || 'Unknown'}
|
||||
size="small"
|
||||
color={getStatusColor(statusData?.status || 'unknown')}
|
||||
variant="outlined"
|
||||
sx={{ height: 24, fontSize: '0.75rem' }}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={fetchStatus}
|
||||
disabled={loading}
|
||||
sx={{ p: 0.5 }}
|
||||
>
|
||||
<RefreshIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
|
||||
{/* Debug button to test dashboard opening */}
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleDashboardClick}
|
||||
sx={{ p: 0.5, color: 'primary.main' }}
|
||||
title="Open Dashboard (Debug)"
|
||||
>
|
||||
<AnalyticsIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ fontSize: '0.75rem', py: 0 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Enhanced Monitoring Dashboard */}
|
||||
<Dialog
|
||||
open={dashboardOpen}
|
||||
onClose={() => {
|
||||
console.log('Dialog closing, setting dashboardOpen to false');
|
||||
setDashboardOpen(false);
|
||||
}}
|
||||
maxWidth="lg"
|
||||
fullWidth
|
||||
sx={{
|
||||
'& .MuiDialog-paper': {
|
||||
borderRadius: 3,
|
||||
background: 'linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%)',
|
||||
maxHeight: '90vh'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white',
|
||||
borderRadius: '12px 12px 0 0'
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<AnalyticsIcon />
|
||||
<Typography variant="h6">System Monitoring Dashboard</Typography>
|
||||
{statusData && (
|
||||
<Chip
|
||||
label={statusData.status.toUpperCase()}
|
||||
color={getStatusColor(statusData.status)}
|
||||
size="small"
|
||||
sx={{ color: 'white' }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<IconButton onClick={() => setDashboardOpen(false)} sx={{ color: 'white' }}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ p: 3, overflow: 'auto' }}>
|
||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||
Dashboard is open! Status: {dashboardOpen ? 'Open' : 'Closed'}
|
||||
</Typography>
|
||||
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{detailedStats ? (
|
||||
<MonitoringCharts
|
||||
chartData={chartData}
|
||||
cachePerformance={detailedStats.cache_performance}
|
||||
apiPerformance={{
|
||||
recent_requests: detailedStats.overview.recent_requests,
|
||||
recent_errors: detailedStats.overview.recent_errors,
|
||||
error_rate: detailedStats.system_health.error_rate
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: 400 }}>
|
||||
<CircularProgress size={60} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Recent Errors Section */}
|
||||
{detailedStats?.recent_errors && detailedStats.recent_errors.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.7 }}
|
||||
>
|
||||
<Card sx={{ mt: 3, background: 'rgba(255,255,255,0.95)' }}>
|
||||
<CardHeader
|
||||
avatar={<Avatar sx={{ bgcolor: 'error.main' }}><BugReportIcon /></Avatar>}
|
||||
title="Recent Errors"
|
||||
subheader="Latest API errors and issues"
|
||||
/>
|
||||
<CardContent>
|
||||
<List>
|
||||
{detailedStats.recent_errors.slice(0, 5).map((error, index) => (
|
||||
<ListItem key={index} sx={{ border: '1px solid #f0f0f0', borderRadius: 1, mb: 1 }}>
|
||||
<ListItemIcon>
|
||||
<CriticalIcon color="error" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={`${error.method} ${error.path}`}
|
||||
secondary={`Status: ${error.status_code} | Duration: ${error.duration.toFixed(3)}s | ${formatTimestamp(error.timestamp)}`}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ p: 3, background: 'rgba(255,255,255,0.9)' }}>
|
||||
<Button
|
||||
onClick={() => setDashboardOpen(false)}
|
||||
variant="outlined"
|
||||
startIcon={<CloseIcon />}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
onClick={fetchDetailedStats}
|
||||
variant="contained"
|
||||
startIcon={<RefreshIcon />}
|
||||
disabled={loading}
|
||||
>
|
||||
Refresh Data
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemStatusIndicator;
|
||||
@@ -58,7 +58,8 @@ import {
|
||||
Insights as InsightsIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
Campaign as CampaignIcon,
|
||||
Speed as SpeedIcon
|
||||
Speed as SpeedIcon,
|
||||
AutoAwesome as AutoAwesomeIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||
@@ -101,7 +102,8 @@ const CalendarTab: React.FC = () => {
|
||||
contentRepurposing,
|
||||
aiInsights,
|
||||
calendarGenerationError,
|
||||
dataLoading
|
||||
dataLoading,
|
||||
calendarGenerationLoading
|
||||
} = useContentPlanningStore();
|
||||
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
@@ -257,6 +259,226 @@ const CalendarTab: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const renderGeneratedCalendar = () => {
|
||||
if (!generatedCalendar) {
|
||||
return (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<AutoAwesomeIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
|
||||
<Typography variant="h6" color="text.secondary" gutterBottom>
|
||||
No AI-generated calendar available
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Generate a calendar using the Calendar Wizard to see AI-powered content recommendations
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
{/* Calendar Overview */}
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<AutoAwesomeIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
AI-Generated Content Calendar
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
|
||||
<Chip label={`Type: ${generatedCalendar.calendar_type || 'Monthly'}`} variant="outlined" />
|
||||
<Chip label={`Industry: ${generatedCalendar.industry || 'Technology'}`} variant="outlined" />
|
||||
<Chip label={`Business Size: ${generatedCalendar.business_size || 'SME'}`} variant="outlined" />
|
||||
</Box>
|
||||
{generatedCalendar.metadata && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Generated: {new Date(generatedCalendar.metadata.generated_at).toLocaleString()}
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{/* Daily Schedule */}
|
||||
{generatedCalendar.daily_schedule && generatedCalendar.daily_schedule.length > 0 && (
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<ScheduleIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Daily Schedule
|
||||
</Typography>
|
||||
<List>
|
||||
{generatedCalendar.daily_schedule.map((item: any, index: number) => (
|
||||
<ListItem key={index} divider>
|
||||
<ListItemIcon>
|
||||
<EventIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={item.topic || item.content_type}
|
||||
secondary={`${item.platform} • ${item.content_type} • ${item.estimated_engagement}% engagement`}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Weekly Themes */}
|
||||
{generatedCalendar.weekly_themes && generatedCalendar.weekly_themes.length > 0 && (
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<TimelineIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Weekly Themes
|
||||
</Typography>
|
||||
<List>
|
||||
{generatedCalendar.weekly_themes.map((theme: any, index: number) => (
|
||||
<ListItem key={index} divider>
|
||||
<ListItemIcon>
|
||||
<LightbulbIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={`Week ${theme.week}: ${theme.theme}`}
|
||||
secondary={`${theme.content_count} pieces • ${theme.platforms?.join(', ')}`}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Content Recommendations */}
|
||||
{generatedCalendar.content_recommendations && generatedCalendar.content_recommendations.length > 0 && (
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<TrendingIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Content Recommendations
|
||||
</Typography>
|
||||
<List>
|
||||
{generatedCalendar.content_recommendations.map((rec: any, index: number) => (
|
||||
<ListItem key={index} divider>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={`${rec.type}: ${rec.topic}`}
|
||||
secondary={`Priority: ${rec.priority} • ROI: ${(rec.estimated_roi * 100).toFixed(0)}%`}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Performance Predictions */}
|
||||
{generatedCalendar.performance_predictions && (
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Performance Predictions
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="body2" color="text.secondary">Estimated Engagement</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={generatedCalendar.performance_predictions.estimated_engagement || 0}
|
||||
sx={{ height: 8, borderRadius: 4 }}
|
||||
/>
|
||||
<Typography variant="body2">{generatedCalendar.performance_predictions.estimated_engagement || 0}%</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2" color="text.secondary">Estimated Reach</Typography>
|
||||
<Typography variant="h6">{generatedCalendar.performance_predictions.estimated_reach || 0}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2" color="text.secondary">Estimated Conversions</Typography>
|
||||
<Typography variant="h6">{generatedCalendar.performance_predictions.estimated_conversions || 0}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* AI Insights */}
|
||||
{generatedCalendar.ai_insights && generatedCalendar.ai_insights.length > 0 && (
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<PsychologyIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
AI Insights
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{generatedCalendar.ai_insights.map((insight: any, index: number) => (
|
||||
<Grid item xs={12} md={4} key={index}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Confidence: {(insight.confidence * 100).toFixed(0)}%
|
||||
</Typography>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
{insight.insight}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="primary">
|
||||
Action: {insight.action}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Strategy Data */}
|
||||
{generatedCalendar.strategy_data && Object.keys(generatedCalendar.strategy_data).length > 0 && (
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<BusinessIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Strategy Integration
|
||||
</Typography>
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>Strategy Analysis & Quality Indicators</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={2}>
|
||||
{generatedCalendar.strategy_analysis && (
|
||||
<Grid item xs={12} md={6}>
|
||||
<Typography variant="subtitle2" gutterBottom>Strategy Analysis</Typography>
|
||||
<Typography variant="body2">
|
||||
Completion: {generatedCalendar.strategy_analysis.completion_percentage || 0}%
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Filled Fields: {generatedCalendar.strategy_analysis.filled_fields || 0}/{generatedCalendar.strategy_analysis.total_fields || 30}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
{generatedCalendar.quality_indicators && (
|
||||
<Grid item xs={12} md={6}>
|
||||
<Typography variant="subtitle2" gutterBottom>Quality Indicators</Typography>
|
||||
<Typography variant="body2">
|
||||
Overall Quality: {generatedCalendar.quality_indicators.overall_quality_score || 0}%
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Strategic Alignment: {generatedCalendar.quality_indicators.strategic_alignment || 0}%
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
@@ -294,9 +516,19 @@ const CalendarTab: React.FC = () => {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{calendarGenerationLoading && (
|
||||
<Alert severity="info" sx={{ mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<CircularProgress size={20} />
|
||||
<Typography>Generating AI-powered content calendar...</Typography>
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||
<Tabs value={tabValue} onChange={(e, newValue) => setTabValue(newValue)}>
|
||||
<Tab label="Calendar Events" icon={<CalendarIcon />} iconPosition="start" />
|
||||
<Tab label="AI-Generated Calendar" icon={<AutoAwesomeIcon />} iconPosition="start" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
@@ -389,6 +621,17 @@ const CalendarTab: React.FC = () => {
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={1}>
|
||||
{/* AI-Generated Calendar Tab */}
|
||||
{dataLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : (
|
||||
renderGeneratedCalendar()
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
{/* Event Dialog */}
|
||||
<Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Tabs,
|
||||
Tab,
|
||||
Typography
|
||||
Tab
|
||||
} from '@mui/material';
|
||||
import {
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
CalendarToday as CalendarIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
// Import components
|
||||
import ContentStrategyBuilder from '../components/ContentStrategyBuilder';
|
||||
import CalendarGenerationWizard from '../components/CalendarGenerationWizard';
|
||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||
import { useStrategyCalendarContext } from '../../../contexts/StrategyCalendarContext';
|
||||
import { CalendarGenerationModal } from '../components/CalendarGenerationModal';
|
||||
|
||||
// Import hooks and services
|
||||
import { useStrategyCalendarContext } from '../../../contexts/StrategyCalendarContext';
|
||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||
|
||||
// Import types
|
||||
import { type CalendarConfig } from '../components/CalendarWizardSteps/types';
|
||||
|
||||
// TabPanel component
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
const TabPanel: React.FC<TabPanelProps> = ({ children, value, index, ...other }) => {
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
@@ -35,53 +42,27 @@ function TabPanel(props: TabPanelProps) {
|
||||
{value === index && <Box>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const CreateTab: React.FC = () => {
|
||||
const [userData, setUserData] = useState<any>({});
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [currentCalendarConfig, setCurrentCalendarConfig] = useState<CalendarConfig | null>(null);
|
||||
const [sessionId, setSessionId] = useState<string>('');
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
// Get strategy context from the provider
|
||||
const { state: { strategyContext }, isFromStrategyActivation } = useStrategyCalendarContext();
|
||||
|
||||
// Removed verbose logging for cleaner console
|
||||
|
||||
// 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
|
||||
const [userData, setUserData] = useState<any>({});
|
||||
|
||||
// Handle navigation from strategy activation
|
||||
useEffect(() => {
|
||||
// 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
|
||||
|
||||
// Auto-switch to Calendar Wizard tab when coming from strategy activation
|
||||
useEffect(() => {
|
||||
// Removed verbose logging for cleaner console
|
||||
|
||||
// Check multiple sources for strategy activation status
|
||||
const fromStrategyActivation = isFromStrategyActivation();
|
||||
const isFromStrategy = fromStrategyActivation ||
|
||||
(strategyContext?.activationStatus === 'active') ||
|
||||
(location.state as any)?.fromStrategyActivation;
|
||||
|
||||
console.log('🔍 CreateTab: Strategy activation check:', {
|
||||
(location.state as any)?.fromStrategyActivation ||
|
||||
(location.state as any)?.strategyContext;
|
||||
|
||||
console.log('🔍 CreateTab: Navigation state check:', {
|
||||
fromStrategyActivation,
|
||||
strategyContextActivationStatus: strategyContext?.activationStatus,
|
||||
windowLocationState: location.state || 'N/A',
|
||||
isFromStrategy
|
||||
});
|
||||
@@ -90,7 +71,7 @@ const CreateTab: React.FC = () => {
|
||||
console.log('🎯 CreateTab: Switching to Calendar Wizard tab (index 1)');
|
||||
setTabValue(1); // Switch to Calendar Wizard tab
|
||||
}
|
||||
}, [fromStrategyActivation, strategyContext?.activationStatus]);
|
||||
}, [isFromStrategyActivation, strategyContext?.activationStatus]);
|
||||
|
||||
// Also check on mount for immediate navigation state
|
||||
useEffect(() => {
|
||||
@@ -112,22 +93,77 @@ const CreateTab: React.FC = () => {
|
||||
return () => clearTimeout(timer);
|
||||
}, [location.state]);
|
||||
|
||||
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
const handleGenerateCalendar = useCallback(async (calendarConfig: any) => {
|
||||
const handleGenerateCalendar = useCallback(async (calendarConfig: CalendarConfig) => {
|
||||
try {
|
||||
await contentPlanningApi.generateComprehensiveCalendar({
|
||||
...calendarConfig,
|
||||
userData
|
||||
// Transform calendarConfig to match backend CalendarGenerationRequest format
|
||||
const requestData = {
|
||||
user_id: 1, // Default user ID
|
||||
strategy_id: strategyContext?.strategyId ? parseInt(strategyContext.strategyId) : undefined,
|
||||
calendar_type: calendarConfig.calendarType || 'monthly',
|
||||
industry: userData?.industry || 'technology',
|
||||
business_size: 'sme',
|
||||
force_refresh: false
|
||||
};
|
||||
|
||||
console.log('🎯 Starting calendar generation with modal:', requestData);
|
||||
|
||||
// Call the new start endpoint to get session ID
|
||||
const startResponse = await fetch('/api/content-planning/calendar-generation/start', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestData),
|
||||
});
|
||||
|
||||
if (!startResponse.ok) {
|
||||
throw new Error(`Failed to start calendar generation: ${startResponse.statusText}`);
|
||||
}
|
||||
|
||||
const startData = await startResponse.json();
|
||||
const sessionId = startData.session_id;
|
||||
|
||||
// Store the session ID and calendar config for the modal
|
||||
setSessionId(sessionId);
|
||||
setCurrentCalendarConfig(calendarConfig);
|
||||
|
||||
// Open the modal to show progress
|
||||
setIsModalOpen(true);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating calendar:', error);
|
||||
console.error('Error starting calendar generation:', error);
|
||||
// The modal will handle error display
|
||||
}
|
||||
}, [userData]);
|
||||
}, [userData, strategyContext]);
|
||||
|
||||
const handleModalComplete = useCallback((results: any) => {
|
||||
console.log('🎉 Calendar generation completed:', results);
|
||||
setIsModalOpen(false);
|
||||
setCurrentCalendarConfig(null);
|
||||
setSessionId('');
|
||||
|
||||
// TODO: Handle the completed calendar results
|
||||
// This could include navigating to a calendar view, showing success message, etc.
|
||||
}, []);
|
||||
|
||||
const handleModalError = useCallback((error: string) => {
|
||||
console.error('❌ Calendar generation error:', error);
|
||||
setIsModalOpen(false);
|
||||
setCurrentCalendarConfig(null);
|
||||
setSessionId('');
|
||||
|
||||
// TODO: Handle error display (could show a toast notification)
|
||||
}, []);
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
setIsModalOpen(false);
|
||||
setCurrentCalendarConfig(null);
|
||||
setSessionId('');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
@@ -166,9 +202,31 @@ const CreateTab: React.FC = () => {
|
||||
onGenerateCalendar={handleGenerateCalendar}
|
||||
loading={false}
|
||||
strategyContext={strategyContext}
|
||||
fromStrategyActivation={fromStrategyActivation}
|
||||
fromStrategyActivation={isFromStrategyActivation()}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
{/* Calendar Generation Modal */}
|
||||
{currentCalendarConfig && (
|
||||
<CalendarGenerationModal
|
||||
open={isModalOpen}
|
||||
onClose={handleModalClose}
|
||||
sessionId={sessionId}
|
||||
initialConfig={{
|
||||
userId: userData?.id?.toString() || '1',
|
||||
strategyId: strategyContext?.strategyId || '',
|
||||
calendarType: currentCalendarConfig.calendarType === 'weekly' ? 'monthly' :
|
||||
currentCalendarConfig.calendarType === 'quarterly' ? 'quarterly' : 'monthly',
|
||||
platforms: currentCalendarConfig.priorityPlatforms || [],
|
||||
duration: currentCalendarConfig.calendarDuration || 30,
|
||||
postingFrequency: currentCalendarConfig.postingFrequency ?
|
||||
(currentCalendarConfig.postingFrequency >= 7 ? 'daily' :
|
||||
currentCalendarConfig.postingFrequency >= 3 ? 'biweekly' : 'weekly') : 'weekly'
|
||||
}}
|
||||
onComplete={handleModalComplete}
|
||||
onError={handleModalError}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user