Alwrity calendar generation framework - step 1-3 completed with real database integration

This commit is contained in:
ajaysi
2025-08-24 19:50:37 +05:30
parent 5d8d1cfb73
commit 6c72ef1a68
124 changed files with 30532 additions and 7066 deletions

View File

@@ -9,7 +9,8 @@ import { SxProps, Theme } from '@mui/material/styles';
export const dialogStyles = {
paper: {
height: '90vh',
maxHeight: '90vh'
maxHeight: '90vh',
zIndex: 9999 // Ensure modal appears above everything
}
};
@@ -325,8 +326,8 @@ export const smallPulseAnimation = {
// Color Animation Config
export const colorPulseAnimation = {
scale: [1, 1.2, 1],
backgroundColor: ['#1976d2', '#42a5f5', '#1976d2']
scale: [1, 1.2, 1]
// Removed backgroundColor animation to prevent "inherit" animation warning
};
// Progress Animation Config

View File

@@ -46,6 +46,9 @@ import {
type QualityScores
} from './calendarGenerationModalPanels';
// Import new StepProgressTracker component
import StepProgressTracker from './calendarGenerationModalPanels/StepProgressTracker';
// Import styles
import {
dialogStyles,
@@ -268,25 +271,42 @@ const CalendarGenerationModal: React.FC<CalendarGenerationModalProps> = ({
});
// Use polling hook for real backend data only
const { progress, isPolling, error, startPolling, stopPolling } = useCalendarGenerationPolling(sessionId);
const {
progress,
isPolling,
error,
startPolling,
stopPolling,
getStepStatus,
getStepQualityScore,
getStepErrors,
getStepWarnings
} = 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
// Start real polling when modal opens with session ID
console.log('🎯 Modal opened, starting polling for session:', sessionId);
startPolling();
} else if (open && !sessionId) {
// Modal opened but no session ID yet - show loading state
console.log('🎯 Modal opened, waiting for session ID...');
} else if (!open) {
console.log('🔒 Modal closed, stopping polling');
stopPolling();
}
}, [open, sessionId, startPolling, stopPolling]);
useEffect(() => {
console.log('📊 Progress updated:', currentProgress);
if (currentProgress?.status === 'completed') {
// Handle completion
console.log('Calendar generation completed');
console.log('🎉 Calendar generation completed');
} else if (currentProgress?.status === 'error') {
console.log('❌ Calendar generation error:', currentProgress.errors);
onError(currentProgress.errors[0]?.message || 'Unknown error');
}
}, [currentProgress?.status, currentProgress?.errors, onError]);
@@ -376,16 +396,10 @@ const CalendarGenerationModal: React.FC<CalendarGenerationModalProps> = ({
transition={{ delay: 0.3, duration: 0.5 }}
>
<Typography variant="h6" sx={{ mt: 2 }}>
Initializing Calendar Generation...
{!sessionId ? 'Starting Calendar Generation...' : '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 variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{!sessionId ? 'Please wait while we prepare your session...' : 'Please wait while we initialize the process...'}
</Typography>
</motion.div>
</Box>
@@ -428,8 +442,8 @@ const CalendarGenerationModal: React.FC<CalendarGenerationModalProps> = ({
</Box>
<motion.div
key={currentProgress.overallProgress}
initial={{ scale: 1.2, color: '#1976d2' }}
animate={{ scale: 1, color: 'inherit' }}
initial={{ scale: 1.2 }}
animate={{ scale: 1 }}
transition={{ duration: 0.3 }}
>
<Typography variant="body2" color="text.secondary">
@@ -545,12 +559,13 @@ const CalendarGenerationModal: React.FC<CalendarGenerationModalProps> = ({
{/* 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) => (
{[
{ id: 0, label: 'Live Progress' },
{ id: 1, label: 'Step Results' },
{ id: 2, label: 'Step Tracker' },
{ id: 3, label: 'Data Sources' },
{ id: 4, label: 'Quality Gates' }
].map((tab, index) => (
<Grid item key={tab.id}>
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -595,26 +610,46 @@ const CalendarGenerationModal: React.FC<CalendarGenerationModalProps> = ({
transition={{ duration: 0.4, ease: animationEasing.easeInOut }}
>
<StepResultsPanel
progress={currentProgress}
getStepStatus={getStepStatus}
getStepQualityScore={getStepQualityScore}
getStepErrors={getStepErrors}
getStepWarnings={getStepWarnings}
/>
</motion.div>
)}
{activeTab === 2 && (
<motion.div
key="step-tracker"
{...fadeInLeft}
transition={{ duration: 0.4, ease: animationEasing.easeInOut }}
>
<StepProgressTracker
progress={currentProgress}
isPolling={isPolling}
getStepStatus={getStepStatus}
getStepQualityScore={getStepQualityScore}
getStepErrors={getStepErrors}
getStepWarnings={getStepWarnings}
/>
</motion.div>
)}
{activeTab === 3 && (
<motion.div
key="data-sources"
{...fadeInLeft}
transition={{ duration: 0.4, ease: animationEasing.easeInOut }}
>
<DataSourcePanel
currentStep={currentProgress.currentStep}
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 && (
{activeTab === 4 && (
<motion.div
key="quality-gates"
{...fadeInLeft}

View File

@@ -6,10 +6,12 @@ const TestCalendarGenerationModal: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const handleOpenModal = () => {
console.log('🎯 TestModal: Opening modal');
setIsModalOpen(true);
};
const handleCloseModal = () => {
console.log('🎯 TestModal: Closing modal');
setIsModalOpen(false);
};
@@ -32,6 +34,8 @@ const TestCalendarGenerationModal: React.FC = () => {
postingFrequency: 'daily' as const
};
console.log('🎯 TestModal render:', { isModalOpen });
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
@@ -46,6 +50,10 @@ const TestCalendarGenerationModal: React.FC = () => {
Open Calendar Generation Modal
</Button>
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}>
Modal state: {isModalOpen ? 'OPEN' : 'CLOSED'}
</Typography>
<CalendarGenerationModal
open={isModalOpen}
onClose={handleCloseModal}

View File

@@ -7,9 +7,31 @@ import {
LinearProgress,
Chip,
CircularProgress,
Card
Card,
Tooltip,
IconButton,
Collapse,
Alert
} from '@mui/material';
import { motion } from 'framer-motion';
import {
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
Warning as WarningIcon,
Info as InfoIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
Schedule as ScheduleIcon,
TrendingUp as TrendingUpIcon
} from '@mui/icons-material';
import { motion, AnimatePresence } from 'framer-motion';
// Import enhanced types and STEP_INFO from the updated polling hook
import {
type CalendarGenerationProgress,
type QualityScores,
type StepResult,
STEP_INFO
} from './useCalendarGenerationPolling';
// Import styles
import {
@@ -28,343 +50,450 @@ import {
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;
getStepStatus?: (stepNumber: number) => string;
getStepQualityScore?: (stepNumber: number) => number;
getStepErrors?: (stepNumber: number) => any[];
getStepWarnings?: (stepNumber: number) => any[];
}
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}>
const LiveProgressPanel: React.FC<LiveProgressPanelProps> = ({
progress,
isPolling,
getStepStatus,
getStepQualityScore,
getStepErrors,
getStepWarnings
}) => {
const [expandedSteps, setExpandedSteps] = React.useState<Set<number>>(new Set());
const toggleStepExpansion = (stepNumber: number) => {
const newExpanded = new Set(expandedSteps);
if (newExpanded.has(stepNumber)) {
newExpanded.delete(stepNumber);
} else {
newExpanded.add(stepNumber);
}
setExpandedSteps(newExpanded);
};
const getStepStatusColor = (stepNumber: number) => {
const status = getStepStatus ? getStepStatus(stepNumber) : progress.stepResults[stepNumber]?.status || 'pending';
switch (status) {
case 'completed': return '#4caf50';
case 'running': return '#1976d2';
case 'failed': return '#f44336';
case 'skipped': return '#ff9800';
default: return '#9e9e9e';
}
};
const getStepStatusIcon = (stepNumber: number) => {
const status = getStepStatus ? getStepStatus(stepNumber) : progress.stepResults[stepNumber]?.status || 'pending';
switch (status) {
case 'completed': return <CheckCircleIcon fontSize="small" />;
case 'running': return <CircularProgress size={16} />;
case 'failed': return <ErrorIcon fontSize="small" />;
case 'skipped': return <WarningIcon fontSize="small" />;
default: return <InfoIcon fontSize="small" />;
}
};
const getQualityScoreColor = (stepNumber: number) => {
const score = getStepQualityScore ? getStepQualityScore(stepNumber) : progress.qualityScores[`step${stepNumber}` as keyof QualityScores] || 0;
if (score >= 0.9) return 'success';
if (score >= 0.8) return 'warning';
return 'error';
};
return (
<Paper elevation={1} sx={{ p: 2 }}>
<Typography variant="h6" gutterBottom>
Live Progress - 12-Step Calendar Generation
</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} - {STEP_INFO[progress.currentStep as keyof typeof STEP_INFO]?.name || 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
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 }}
key={progress.stepProgress}
initial={{ scale: 1.2 }}
animate={{ scale: 1 }}
transition={{ duration: 0.3 }}
>
<Typography variant="subtitle1">
Current Step: {progress.currentStep} - {progress.status}
<Typography variant="body2" color="text.secondary">
{Math.round(progress.stepProgress || 0)}%
</Typography>
</motion.div>
</Box>
</motion.div>
<Box display="flex" alignItems="center" gap={2}>
<Typography variant="body2" color="text.secondary">
Step Progress:
</Box>
{/* 12-Step Progress Grid with Enhanced Visualization */}
<Box mb={3}>
<Typography variant="subtitle1" gutterBottom>
Step-by-Step Progress ({progress.metadata?.completedSteps || 0}/12 Completed)
</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}>
<Grid container spacing={2}>
{Array.from({ length: 12 }, (_, i) => i + 1).map((step, index) => {
const stepInfo = STEP_INFO[step as keyof typeof STEP_INFO];
const stepStatus = getStepStatus ? getStepStatus(step) : progress.stepResults[step]?.status || 'pending';
const stepErrors = getStepErrors ? getStepErrors(step) : progress.errors.filter(error => error.step === step);
const stepWarnings = getStepWarnings ? getStepWarnings(step) : progress.warnings.filter(warning => warning.step === step);
const isExpanded = expandedSteps.has(step);
return (
<Grid item xs={12} sm={6} md={4} lg={3} key={step}>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
initial={{ opacity: 0, y: 50, scale: 0.8 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{
delay: index * 0.1 + 0.3,
delay: index * 0.1,
duration: 0.6,
ease: "easeOut",
type: "spring",
stiffness: 200,
damping: 10
stiffness: 100
}}
whileHover={{
scale: 1.02,
y: -5,
transition: { duration: 0.2 }
}}
>
<Typography
variant="h5"
sx={{ color: metric.color }}
gutterBottom
<Card
variant="outlined"
sx={{
...getStepCardStyles(progress.currentStep, step),
borderColor: getStepStatusColor(step),
borderWidth: stepStatus === 'running' ? 2 : 1
}}
>
<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>
<Box display="flex" alignItems="center" gap={2} mb={1}>
<motion.div
animate={{
rotate: stepStatus === 'running' ? [0, 10, -10, 0] : 0,
scale: stepStatus === 'running' ? 1.1 : 1,
backgroundColor: getStepStatusColor(step)
}}
transition={{
rotate: { duration: 0.5 },
scale: { duration: 0.3 },
backgroundColor: { duration: 0.3 }
}}
style={{
...stepCircleBaseStyles,
backgroundColor: getStepStatusColor(step)
}}
>
{stepStatus === 'completed' ? (
<motion.div
initial={{ scale: 0, rotate: -180 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ type: "spring", stiffness: 200, damping: 10 }}
>
<CheckCircleIcon fontSize="small" />
</motion.div>
) : (
<Box display="flex" alignItems="center" justifyContent="center">
{getStepStatusIcon(step)}
</Box>
)}
</motion.div>
<Box flex={1}>
<Typography variant="subtitle2">
Step {step}
</Typography>
<Typography variant="caption" color="text.secondary">
{stepInfo?.name || `Step ${step}`}
</Typography>
</Box>
<IconButton
size="small"
onClick={() => toggleStepExpansion(step)}
sx={{ p: 0.5 }}
>
{isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
</Box>
<Typography variant="body2" color="text.secondary" gutterBottom>
{stepInfo?.description || 'Processing...'}
</Typography>
{/* Quality Score */}
{stepStatus !== 'pending' && (
<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} mb={1}>
<motion.div
animate={smallPulseAnimation}
transition={{
duration: 2,
repeat: Infinity,
ease: animationEasing.easeInOut
}}
>
<Chip
label={`${Math.round(((getStepQualityScore ? getStepQualityScore(step) : progress.qualityScores[`step${step}` as keyof QualityScores] || 0) * 100) || 0)}%`}
size="small"
color={getQualityScoreColor(step)}
icon={<TrendingUpIcon />}
/>
</motion.div>
<Typography variant="caption" color="text.secondary">
Quality
</Typography>
</Box>
</motion.div>
)}
{/* Step Details Collapse */}
<Collapse in={isExpanded}>
<Box mt={1} p={1} bgcolor="grey.50" borderRadius={1}>
{/* Step Duration */}
{progress.stepResults[step]?.duration && (
<Box display="flex" alignItems="center" gap={1} mb={1}>
<ScheduleIcon fontSize="small" color="action" />
<Typography variant="caption">
Duration: {Math.round(progress.stepResults[step]?.duration || 0)}s
</Typography>
</Box>
)}
{/* Errors */}
{stepErrors.length > 0 && (
<Box mb={1}>
<Alert severity="error">
{stepErrors.length} error(s) found
</Alert>
</Box>
)}
{/* Warnings */}
{stepWarnings.length > 0 && (
<Box mb={1}>
<Alert severity="warning">
{stepWarnings.length} warning(s) found
</Alert>
</Box>
)}
{/* Step Metadata */}
{progress.stepResults[step]?.metadata && (
<Box>
<Typography variant="caption" color="text.secondary">
Data Sources: {progress.stepResults[step]?.metadata?.dataSources?.join(', ') || 'None'}
</Typography>
</Box>
)}
</Box>
</Collapse>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: index * 0.1 + 0.8, duration: 0.5 }}
>
</Grid>
);
})}
</Grid>
</Box>
{/* Recent Activity with Staggered Animation */}
<Box mb={3}>
<Typography variant="subtitle1" gutterBottom>
Recent Activity
</Typography>
<Box sx={{ maxHeight: 200, overflowY: 'auto' }}>
<AnimatePresence>
{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 }}
exit={{ opacity: 0, x: 20, scale: 0.95 }}
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">
{metric.label}
{message}
</Typography>
</motion.div>
</Box>
</motion.div>
</Grid>
))}
</Grid>
</Box>
</Paper>
);
</Box>
</motion.div>
))}
</AnimatePresence>
</Box>
</Box>
{/* Enhanced 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.metadata?.completedSteps || 0,
label: 'Steps Completed',
color: 'info.main',
suffix: '/12'
},
{
value: progress.errors.length,
label: 'Issues Found',
color: 'error.main',
suffix: ''
},
{
value: progress.metadata?.failedSteps || 0,
label: 'Failed Steps',
color: 'warning.main',
suffix: ''
},
{
value: progress.metadata?.averageStepDuration || 0,
label: 'Avg Step Duration',
color: 'secondary.main',
suffix: 's'
}
].map((metric, index) => (
<Grid item xs={12} sm={6} md={4} 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;

View File

@@ -0,0 +1,570 @@
import React, { useState, useMemo } from 'react';
import {
Paper,
Typography,
Box,
Grid,
Card,
CardContent,
LinearProgress,
Chip,
IconButton,
Tooltip,
Collapse,
Alert,
List,
ListItem,
ListItemIcon,
ListItemText,
Divider,
Badge,
CircularProgress
} from '@mui/material';
import {
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
Warning as WarningIcon,
Info as InfoIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
TrendingUp as TrendingUpIcon,
Schedule as ScheduleIcon,
Speed as SpeedIcon,
DataUsage as DataUsageIcon,
Lightbulb as LightbulbIcon,
Recommend as RecommendIcon,
Timeline as TimelineIcon,
Assessment as AssessmentIcon,
Security as SecurityIcon,
Build as BuildIcon,
AutoAwesome as AutoAwesomeIcon,
PlayArrow as PlayArrowIcon,
Pause as PauseIcon,
Stop as StopIcon,
Refresh as RefreshIcon,
SkipNext as SkipNextIcon
} from '@mui/icons-material';
import { motion, AnimatePresence } from 'framer-motion';
// Import enhanced types and STEP_INFO from the updated polling hook
import {
type CalendarGenerationProgress,
type QualityScores,
type StepResult,
STEP_INFO
} from './useCalendarGenerationPolling';
// Import styles
import {
stepResultsCardStyles,
stepResultsHeaderStyles,
stepResultsContentStyles,
animationDurations,
animationEasing,
springConfig,
staggerDelay
} from '../CalendarGenerationModal.styles';
interface StepProgressTrackerProps {
progress: CalendarGenerationProgress;
isPolling?: boolean;
getStepStatus?: (stepNumber: number) => string;
getStepQualityScore?: (stepNumber: number) => number;
getStepErrors?: (stepNumber: number) => any[];
getStepWarnings?: (stepNumber: number) => any[];
onStepClick?: (stepNumber: number) => void;
onRetryStep?: (stepNumber: number) => void;
onSkipStep?: (stepNumber: number) => void;
onPauseGeneration?: () => void;
onResumeGeneration?: () => void;
onCancelGeneration?: () => void;
}
// Step-specific icons for visual identification
const STEP_ICONS = {
1: <AssessmentIcon />,
2: <DataUsageIcon />,
3: <TimelineIcon />,
4: <ScheduleIcon />,
5: <BuildIcon />,
6: <SpeedIcon />,
7: <LightbulbIcon />,
8: <TimelineIcon />,
9: <RecommendIcon />,
10: <TrendingUpIcon />,
11: <SecurityIcon />,
12: <AutoAwesomeIcon />
};
// Step status colors
const STEP_STATUS_COLORS = {
pending: '#9e9e9e',
running: '#2196f3',
completed: '#4caf50',
failed: '#f44336',
skipped: '#ff9800'
};
const StepProgressTracker: React.FC<StepProgressTrackerProps> = ({
progress,
isPolling,
getStepStatus,
getStepQualityScore,
getStepErrors,
getStepWarnings,
onStepClick,
onRetryStep,
onSkipStep,
onPauseGeneration,
onResumeGeneration,
onCancelGeneration
}) => {
const [expandedStep, setExpandedStep] = useState<number | null>(null);
const [showDetails, setShowDetails] = useState(false);
// Helper functions
const getStepStatusColor = (stepNumber: number) => {
const status = getStepStatus ? getStepStatus(stepNumber) : progress.stepResults[stepNumber]?.status || 'pending';
return STEP_STATUS_COLORS[status as keyof typeof STEP_STATUS_COLORS] || STEP_STATUS_COLORS.pending;
};
const getStepStatusIcon = (stepNumber: number) => {
const status = getStepStatus ? getStepStatus(stepNumber) : progress.stepResults[stepNumber]?.status || 'pending';
switch (status) {
case 'completed': return <CheckCircleIcon />;
case 'running': return <CircularProgress size={20} />;
case 'failed': return <ErrorIcon />;
case 'skipped': return <WarningIcon />;
default: return <InfoIcon />;
}
};
const getQualityScoreColor = (stepNumber: number) => {
const score = getStepQualityScore ? getStepQualityScore(stepNumber) : progress.qualityScores[`step${stepNumber}` as keyof QualityScores] || 0;
if (score >= 0.9) return 'success';
if (score >= 0.8) return 'warning';
return 'error';
};
// Computed values
const completedSteps = useMemo(() => {
return Object.values(progress.stepResults).filter(result => result.status === 'completed').length;
}, [progress.stepResults]);
const failedSteps = useMemo(() => {
return Object.values(progress.stepResults).filter(result => result.status === 'failed').length;
}, [progress.stepResults]);
const runningSteps = useMemo(() => {
return Object.values(progress.stepResults).filter(result => result.status === 'running').length;
}, [progress.stepResults]);
const isGenerationActive = progress.status !== 'completed' && progress.status !== 'error';
const renderStepCard = (stepNumber: number) => {
const stepResult = progress.stepResults[stepNumber];
const stepInfo = STEP_INFO[stepNumber as keyof typeof STEP_INFO];
const stepStatus = getStepStatus ? getStepStatus(stepNumber) : stepResult?.status || 'pending';
const stepErrors = getStepErrors ? getStepErrors(stepNumber) : progress.errors.filter(error => error.step === stepNumber);
const stepWarnings = getStepWarnings ? getStepWarnings(stepNumber) : progress.warnings.filter(warning => warning.step === stepNumber);
const isExpanded = expandedStep === stepNumber;
const hasIssues = stepErrors.length > 0 || stepWarnings.length > 0;
return (
<motion.div
key={stepNumber}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
delay: (stepNumber - 1) * 0.05,
duration: 0.3,
ease: "easeOut"
}}
>
<Card
sx={{
mb: 2,
border: `2px solid ${getStepStatusColor(stepNumber)}`,
backgroundColor: stepStatus === 'running' ? 'action.hover' : 'background.paper',
cursor: 'pointer',
'&:hover': {
boxShadow: 4,
transform: 'translateY(-2px)',
transition: 'all 0.2s ease-in-out'
}
}}
onClick={() => {
setExpandedStep(isExpanded ? null : stepNumber);
onStepClick?.(stepNumber);
}}
>
<CardContent sx={{ p: 2 }}>
<Grid container alignItems="center" spacing={2}>
{/* Step Icon and Number */}
<Grid item>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 48,
height: 48,
borderRadius: '50%',
backgroundColor: `${getStepStatusColor(stepNumber)}20`,
color: getStepStatusColor(stepNumber)
}}
>
{getStepStatusIcon(stepNumber)}
</Box>
</Grid>
{/* Step Info */}
<Grid item xs>
<Box>
<Typography variant="h6" fontWeight="bold" gutterBottom>
Step {stepNumber}: {stepInfo?.name || `Step ${stepNumber}`}
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
{stepInfo?.description || 'Processing...'}
</Typography>
{/* Progress Bar for Running Steps */}
{stepStatus === 'running' && (
<Box mt={1}>
<LinearProgress
variant="indeterminate"
sx={{ height: 4, borderRadius: 2 }}
/>
</Box>
)}
</Box>
</Grid>
{/* Status and Quality */}
<Grid item>
<Box display="flex" alignItems="center" gap={1}>
<Chip
label={stepStatus}
size="small"
sx={{
backgroundColor: getStepStatusColor(stepNumber),
color: 'white',
fontWeight: 'bold'
}}
/>
{stepResult?.qualityScore && (
<Chip
icon={<TrendingUpIcon />}
label={`${Math.round(stepResult.qualityScore * 100)}%`}
color={getQualityScoreColor(stepNumber)}
size="small"
/>
)}
{hasIssues && (
<Badge badgeContent={stepErrors.length + stepWarnings.length} color="error">
<WarningIcon color="warning" />
</Badge>
)}
</Box>
</Grid>
{/* Expand/Collapse Icon */}
<Grid item>
<IconButton size="small">
{isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
</Grid>
</Grid>
{/* Expanded Details */}
<Collapse in={isExpanded}>
<Box mt={2}>
<Divider sx={{ mb: 2 }} />
{/* Execution Details */}
{stepResult && (
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Typography variant="subtitle2" gutterBottom>
Execution Details
</Typography>
<List dense>
{stepResult.startTime && (
<ListItem>
<ListItemIcon>
<ScheduleIcon color="action" />
</ListItemIcon>
<ListItemText
primary="Start Time"
secondary={new Date(stepResult.startTime).toLocaleString()}
/>
</ListItem>
)}
{stepResult.endTime && (
<ListItem>
<ListItemIcon>
<ScheduleIcon color="action" />
</ListItemIcon>
<ListItemText
primary="End Time"
secondary={new Date(stepResult.endTime).toLocaleString()}
/>
</ListItem>
)}
{stepResult.duration && (
<ListItem>
<ListItemIcon>
<SpeedIcon color="action" />
</ListItemIcon>
<ListItemText
primary="Duration"
secondary={`${Math.round(stepResult.duration)} seconds`}
/>
</ListItem>
)}
</List>
</Grid>
{/* Data Sources */}
<Grid item xs={12} md={6}>
<Typography variant="subtitle2" gutterBottom>
Data Sources Used
</Typography>
<Box display="flex" flexWrap="wrap" gap={1}>
{stepResult.metadata?.dataSources?.map((source: string, index: number) => (
<Chip
key={index}
label={source}
size="small"
variant="outlined"
icon={<DataUsageIcon />}
/>
)) || (
<Typography variant="body2" color="text.secondary">
No data sources recorded
</Typography>
)}
</Box>
</Grid>
</Grid>
)}
{/* Errors and Warnings */}
{hasIssues && (
<Box mt={2}>
<Divider sx={{ mb: 2 }} />
{stepErrors.length > 0 && (
<Alert severity="error" sx={{ mb: 1 }}>
<Typography variant="subtitle2" gutterBottom>
Errors ({stepErrors.length})
</Typography>
{stepErrors.map((error, index) => (
<Typography key={index} variant="body2">
{error.message}
</Typography>
))}
</Alert>
)}
{stepWarnings.length > 0 && (
<Alert severity="warning" sx={{ mb: 1 }}>
<Typography variant="subtitle2" gutterBottom>
Warnings ({stepWarnings.length})
</Typography>
{stepWarnings.map((warning, index) => (
<Typography key={index} variant="body2">
{warning.message}
</Typography>
))}
</Alert>
)}
</Box>
)}
{/* Action Buttons */}
<Box mt={2} display="flex" gap={1}>
{stepStatus === 'failed' && onRetryStep && (
<Tooltip title="Retry this step">
<IconButton
size="small"
color="primary"
onClick={(e) => {
e.stopPropagation();
onRetryStep(stepNumber);
}}
>
<RefreshIcon />
</IconButton>
</Tooltip>
)}
{stepStatus === 'pending' && onSkipStep && (
<Tooltip title="Skip this step">
<IconButton
size="small"
color="warning"
onClick={(e) => {
e.stopPropagation();
onSkipStep(stepNumber);
}}
>
<SkipNextIcon />
</IconButton>
</Tooltip>
)}
</Box>
</Box>
</Collapse>
</CardContent>
</Card>
</motion.div>
);
};
return (
<Paper elevation={1} sx={{ p: 3 }}>
{/* Header */}
<Box display="flex" alignItems="center" justifyContent="space-between" mb={3}>
<Box>
<Typography variant="h5" fontWeight="bold" gutterBottom>
Step Progress Tracker
</Typography>
<Typography variant="body2" color="text.secondary">
Real-time tracking of the 12-step calendar generation process
</Typography>
</Box>
<Box display="flex" alignItems="center" gap={2}>
{/* Generation Controls */}
{isGenerationActive && (
<Box display="flex" gap={1}>
{onPauseGeneration && (
<Tooltip title="Pause Generation">
<IconButton color="warning" onClick={onPauseGeneration}>
<PauseIcon />
</IconButton>
</Tooltip>
)}
{onResumeGeneration && (
<Tooltip title="Resume Generation">
<IconButton color="primary" onClick={onResumeGeneration}>
<PlayArrowIcon />
</IconButton>
</Tooltip>
)}
{onCancelGeneration && (
<Tooltip title="Cancel Generation">
<IconButton color="error" onClick={onCancelGeneration}>
<StopIcon />
</IconButton>
</Tooltip>
)}
</Box>
)}
{/* Toggle Details */}
<Tooltip title={showDetails ? "Hide Details" : "Show Details"}>
<IconButton onClick={() => setShowDetails(!showDetails)}>
{showDetails ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
</Tooltip>
</Box>
</Box>
{/* Overall Progress Summary */}
<Box mb={3}>
<Grid container spacing={2}>
<Grid item xs={12} md={3}>
<Card variant="outlined" sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" color="primary" fontWeight="bold">
{completedSteps}/12
</Typography>
<Typography variant="body2" color="text.secondary">
Steps Completed
</Typography>
</Card>
</Grid>
<Grid item xs={12} md={3}>
<Card variant="outlined" sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" color="error" fontWeight="bold">
{failedSteps}
</Typography>
<Typography variant="body2" color="text.secondary">
Failed Steps
</Typography>
</Card>
</Grid>
<Grid item xs={12} md={3}>
<Card variant="outlined" sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" color="primary" fontWeight="bold">
{runningSteps}
</Typography>
<Typography variant="body2" color="text.secondary">
Currently Running
</Typography>
</Card>
</Grid>
<Grid item xs={12} md={3}>
<Card variant="outlined" sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" color="success.main" fontWeight="bold">
{Math.round(progress.qualityScores.overall * 100)}%
</Typography>
<Typography variant="body2" color="text.secondary">
Overall Quality
</Typography>
</Card>
</Grid>
</Grid>
</Box>
{/* Overall Progress Bar */}
<Box mb={3}>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
<Typography variant="subtitle2">
Overall Progress
</Typography>
<Typography variant="body2" color="text.secondary">
{Math.round(progress.overallProgress)}%
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={progress.overallProgress}
sx={{ height: 8, borderRadius: 4 }}
/>
</Box>
{/* Step Cards */}
<AnimatePresence>
{Array.from({ length: 12 }, (_, i) => i + 1).map(stepNumber =>
renderStepCard(stepNumber)
)}
</AnimatePresence>
{/* No Progress Message */}
{Object.keys(progress.stepResults).length === 0 && (
<Box textAlign="center" py={4}>
<InfoIcon sx={{ fontSize: 48, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom>
No Steps Started
</Typography>
<Typography variant="body2" color="text.secondary">
Step progress will appear here once the calendar generation begins.
</Typography>
</Box>
)}
</Paper>
);
};
export default StepProgressTracker;

View File

@@ -1,151 +1,483 @@
import React from 'react';
import React, { useState } from 'react';
import {
Paper,
Typography,
Box,
Chip,
Card
Card,
Accordion,
AccordionSummary,
AccordionDetails,
Grid,
LinearProgress,
Alert,
IconButton,
Tooltip,
Divider,
List,
ListItem,
ListItemIcon,
ListItemText,
Badge
} from '@mui/material';
import {
CheckCircle as CheckCircleIcon
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
Warning as WarningIcon,
Info as InfoIcon,
ExpandMore as ExpandMoreIcon,
TrendingUp as TrendingUpIcon,
Schedule as ScheduleIcon,
DataUsage as DataUsageIcon,
Lightbulb as LightbulbIcon,
Recommend as RecommendIcon,
Timeline as TimelineIcon,
Assessment as AssessmentIcon,
Speed as SpeedIcon,
Security as SecurityIcon,
Build as BuildIcon,
AutoAwesome as AutoAwesomeIcon
} from '@mui/icons-material';
import { motion, AnimatePresence } from 'framer-motion';
// Import enhanced types and STEP_INFO from the updated polling hook
import {
type CalendarGenerationProgress,
type QualityScores,
type StepResult,
STEP_INFO
} from './useCalendarGenerationPolling';
// Import styles
import {
stepResultsCardStyles,
stepResultsHeaderStyles,
stepResultsContentStyles
stepResultsContentStyles,
animationDurations,
animationEasing,
springConfig,
staggerDelay
} 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;
progress: CalendarGenerationProgress;
getStepStatus?: (stepNumber: number) => string;
getStepQualityScore?: (stepNumber: number) => number;
getStepErrors?: (stepNumber: number) => any[];
getStepWarnings?: (stepNumber: number) => any[];
}
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}
// Step-specific icons for better visual identification
const STEP_ICONS = {
1: <AssessmentIcon />,
2: <DataUsageIcon />,
3: <TimelineIcon />,
4: <ScheduleIcon />,
5: <BuildIcon />,
6: <SpeedIcon />,
7: <LightbulbIcon />,
8: <TimelineIcon />,
9: <RecommendIcon />,
10: <TrendingUpIcon />,
11: <SecurityIcon />,
12: <AutoAwesomeIcon />
};
const StepResultsPanel: React.FC<StepResultsPanelProps> = ({
progress,
getStepStatus,
getStepQualityScore,
getStepErrors,
getStepWarnings
}) => {
const [expandedSteps, setExpandedSteps] = useState<Set<number>>(new Set());
const toggleStepExpansion = (stepNumber: number) => {
const newExpanded = new Set(expandedSteps);
if (newExpanded.has(stepNumber)) {
newExpanded.delete(stepNumber);
} else {
newExpanded.add(stepNumber);
}
setExpandedSteps(newExpanded);
};
const getStepStatusColor = (stepNumber: number) => {
const status = getStepStatus ? getStepStatus(stepNumber) : progress.stepResults[stepNumber]?.status || 'pending';
switch (status) {
case 'completed': return 'success';
case 'running': return 'primary';
case 'failed': return 'error';
case 'skipped': return 'warning';
default: return 'default';
}
};
const getStepStatusIcon = (stepNumber: number) => {
const status = getStepStatus ? getStepStatus(stepNumber) : progress.stepResults[stepNumber]?.status || 'pending';
switch (status) {
case 'completed': return <CheckCircleIcon />;
case 'running': return <InfoIcon />;
case 'failed': return <ErrorIcon />;
case 'skipped': return <WarningIcon />;
default: return <InfoIcon />;
}
};
const getQualityScoreColor = (stepNumber: number) => {
const score = getStepQualityScore ? getStepQualityScore(stepNumber) : progress.qualityScores[`step${stepNumber}` as keyof QualityScores] || 0;
if (score >= 0.9) return 'success';
if (score >= 0.8) return 'warning';
return 'error';
};
const formatStepData = (data: any): string => {
if (typeof data === 'string') return data;
if (typeof data === 'number') return data.toString();
if (Array.isArray(data)) return data.join(', ');
if (typeof data === 'object' && data !== null) {
return Object.entries(data)
.map(([key, value]) => `${key}: ${formatStepData(value)}`)
.join('; ');
}
return String(data);
};
const renderStepResults = (stepNumber: number) => {
const stepResult = progress.stepResults[stepNumber];
if (!stepResult) return null;
const stepInfo = STEP_INFO[stepNumber as keyof typeof STEP_INFO];
const stepStatus = getStepStatus ? getStepStatus(stepNumber) : stepResult.status || 'pending';
const stepErrors = getStepErrors ? getStepErrors(stepNumber) : progress.errors.filter(error => error.step === stepNumber);
const stepWarnings = getStepWarnings ? getStepWarnings(stepNumber) : progress.warnings.filter(warning => warning.step === stepNumber);
const isExpanded = expandedSteps.has(stepNumber);
return (
<motion.div
key={stepNumber}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
delay: (stepNumber - 1) * 0.1,
duration: 0.5,
ease: "easeOut"
}}
>
<Accordion
expanded={isExpanded}
onChange={() => toggleStepExpansion(stepNumber)}
sx={{
mb: 2,
border: `2px solid`,
borderColor: getStepStatusColor(stepNumber) === 'success' ? 'success.main' :
getStepStatusColor(stepNumber) === 'error' ? 'error.main' :
getStepStatusColor(stepNumber) === 'warning' ? 'warning.main' :
'grey.300'
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
sx={{
'&:hover': {
backgroundColor: 'action.hover'
}
}}
>
<Grid container alignItems="center" spacing={2}>
<Grid item>
<Box display="flex" alignItems="center" gap={1}>
<Box sx={{ color: `${getStepStatusColor(stepNumber)}.main` }}>
{STEP_ICONS[stepNumber as keyof typeof STEP_ICONS]}
</Box>
<Typography variant="h6" component="span">
Step {stepNumber}
</Typography>
</Box>
</Grid>
<Grid item xs>
<Box>
<Typography variant="subtitle1" fontWeight="bold">
{stepInfo?.name || `Step ${stepNumber}`}
</Typography>
<Typography variant="body2" color="text.secondary">
{stepInfo?.description || 'Processing...'}
</Typography>
</Box>
</Grid>
<Grid item>
<Box display="flex" alignItems="center" gap={1}>
<Chip
icon={getStepStatusIcon(stepNumber)}
label={stepStatus}
color={getStepStatusColor(stepNumber)}
size="small"
/>
<Chip
icon={<TrendingUpIcon />}
label={`${Math.round((getStepQualityScore ? getStepQualityScore(stepNumber) : progress.qualityScores[`step${stepNumber}` as keyof QualityScores] || 0) * 100)}%`}
color={getQualityScoreColor(stepNumber)}
size="small"
/>
{stepErrors.length > 0 && (
<Badge badgeContent={stepErrors.length} color="error">
<ErrorIcon color="error" />
</Badge>
)}
{stepWarnings.length > 0 && (
<Badge badgeContent={stepWarnings.length} color="warning">
<WarningIcon color="warning" />
</Badge>
)}
</Box>
</Grid>
</Grid>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={3}>
{/* Step Execution Details */}
<Grid item xs={12} md={6}>
<Typography variant="h6" gutterBottom>
Execution Details
</Typography>
<Typography variant="body2" color="text.secondary">
Execution Time: {results.executionTime}
<List dense>
{stepResult.startTime && (
<ListItem>
<ListItemIcon>
<ScheduleIcon color="action" />
</ListItemIcon>
<ListItemText
primary="Start Time"
secondary={new Date(stepResult.startTime).toLocaleString()}
/>
</ListItem>
)}
{stepResult.endTime && (
<ListItem>
<ListItemIcon>
<ScheduleIcon color="action" />
</ListItemIcon>
<ListItemText
primary="End Time"
secondary={new Date(stepResult.endTime).toLocaleString()}
/>
</ListItem>
)}
{stepResult.duration && (
<ListItem>
<ListItemIcon>
<SpeedIcon color="action" />
</ListItemIcon>
<ListItemText
primary="Duration"
secondary={`${Math.round(stepResult.duration)} seconds`}
/>
</ListItem>
)}
{stepResult.qualityScore && (
<ListItem>
<ListItemIcon>
<TrendingUpIcon color="action" />
</ListItemIcon>
<ListItemText
primary="Quality Score"
secondary={`${Math.round(stepResult.qualityScore * 100)}%`}
/>
</ListItem>
)}
</List>
</Grid>
{/* Step Data and Results */}
<Grid item xs={12} md={6}>
<Typography variant="h6" gutterBottom>
Results & Data
</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>
{stepResult.data && (
<Box mb={2}>
<Typography variant="subtitle2" gutterBottom>
Generated Data:
</Typography>
<Box sx={stepResultsContentStyles}>
{Object.entries(stepResult.data).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">
{formatStepData(value)}
</Typography>
</Box>
))}
</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>
{stepResult.metadata?.dataSources && (
<Box mb={2}>
<Typography variant="subtitle2" gutterBottom>
Data Sources Used:
</Typography>
<Box display="flex" flexWrap="wrap" gap={1}>
{stepResult.metadata.dataSources.map((source: string, index: number) => (
<Chip
key={index}
label={source}
size="small"
variant="outlined"
color="primary"
icon={<DataUsageIcon />}
/>
))}
</Box>
</Box>
)}
</Grid>
{/* 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>
{/* Errors and Warnings */}
{(stepErrors.length > 0 || stepWarnings.length > 0) && (
<Grid item xs={12}>
<Divider sx={{ my: 2 }} />
{stepErrors.length > 0 && (
<Box mb={2}>
<Typography variant="h6" color="error" gutterBottom>
Errors ({stepErrors.length})
</Typography>
{stepErrors.map((error, index) => (
<Alert key={index} severity="error" sx={{ mb: 1 }}>
<Typography variant="body2">
{error.message}
</Typography>
{error.timestamp && (
<Typography variant="caption" color="text.secondary">
{new Date(error.timestamp).toLocaleString()}
</Typography>
)}
</Alert>
))}
</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>
{stepWarnings.length > 0 && (
<Box mb={2}>
<Typography variant="h6" color="warning.main" gutterBottom>
Warnings ({stepWarnings.length})
</Typography>
{stepWarnings.map((warning, index) => (
<Alert key={index} severity="warning" sx={{ mb: 1 }}>
<Typography variant="body2">
{warning.message}
</Typography>
{warning.timestamp && (
<Typography variant="caption" color="text.secondary">
{new Date(warning.timestamp).toLocaleString()}
</Typography>
)}
</Alert>
))}
</Box>
)}
</Grid>
)}
{/* 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}
{/* Performance Metrics */}
{stepResult.metadata?.performanceMetrics && (
<Grid item xs={12}>
<Divider sx={{ my: 2 }} />
<Typography variant="h6" gutterBottom>
Performance Metrics
</Typography>
</Box>
))}
</Box>
</Box>
</Card>
<Grid container spacing={2}>
{Object.entries(stepResult.metadata.performanceMetrics).map(([key, value]) => (
<Grid item xs={12} sm={6} md={4} key={key}>
<Card variant="outlined" sx={{ p: 2 }}>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
</Typography>
<Typography variant="h6">
{typeof value === 'number' ? value.toFixed(2) : String(value)}
</Typography>
</Card>
</Grid>
))}
</Grid>
</Grid>
)}
</Grid>
</AccordionDetails>
</Accordion>
</motion.div>
);
};
return (
<Paper elevation={1} sx={{ p: 2 }}>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={3}>
<Typography variant="h6">
Step Results - 12-Step Calendar Generation
</Typography>
<Box display="flex" alignItems="center" gap={1}>
<Chip
label={`${progress.metadata?.completedSteps || 0}/12 Completed`}
color="primary"
variant="outlined"
/>
<Chip
icon={<TrendingUpIcon />}
label={`${Math.round(progress.qualityScores.overall * 100)}% Quality`}
color={progress.qualityScores.overall >= 0.9 ? 'success' :
progress.qualityScores.overall >= 0.8 ? 'warning' : 'error'}
/>
</Box>
</Box>
))}
</Paper>
);
{/* Overall Progress */}
<Box mb={3}>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
<Typography variant="subtitle2">
Overall Progress
</Typography>
<Typography variant="body2" color="text.secondary">
{Math.round(progress.overallProgress)}%
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={progress.overallProgress}
sx={{ height: 8, borderRadius: 4 }}
/>
</Box>
{/* Step Results */}
<AnimatePresence>
{Array.from({ length: 12 }, (_, i) => i + 1).map(stepNumber =>
renderStepResults(stepNumber)
)}
</AnimatePresence>
{/* No Results Message */}
{Object.keys(progress.stepResults).length === 0 && (
<Box textAlign="center" py={4}>
<InfoIcon sx={{ fontSize: 48, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom>
No Step Results Available
</Typography>
<Typography variant="body2" color="text.secondary">
Step results will appear here as the calendar generation progresses.
</Typography>
</Box>
)}
</Paper>
);
};
export default StepResultsPanel;

View File

@@ -1,6 +1,24 @@
import { useState, useCallback } from 'react';
// Types
// Enhanced types for 12-step support
interface StepResult {
stepNumber: number;
stepName: string;
status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
startTime?: string;
endTime?: string;
duration?: number;
qualityScore?: number;
data?: any;
errors?: string[];
warnings?: string[];
metadata?: {
dataSources?: string[];
qualityGates?: string[];
performanceMetrics?: Record<string, number>;
};
}
interface QualityScores {
overall: number;
step1: number;
@@ -18,30 +36,86 @@ interface QualityScores {
}
interface CalendarGenerationProgress {
status: 'initializing' | 'step1' | 'step2' | 'step3' | 'completed' | 'error';
// Enhanced status to support all 12 steps
status: 'initializing' | 'step1' | 'step2' | 'step3' | 'step4' | 'step5' | 'step6' | 'step7' | 'step8' | 'step9' | 'step10' | 'step11' | 'step12' | 'completed' | 'error';
currentStep: number;
stepProgress: number;
overallProgress: number;
stepResults: Record<number, any>;
// Enhanced step results with detailed typing
stepResults: Record<number, StepResult>;
// Quality and transparency data
qualityScores: QualityScores;
transparencyMessages: string[];
educationalContent: any[];
errors: any[];
warnings: any[];
// Error handling
errors: Array<{
step?: number;
message: string;
timestamp: string;
severity: 'error' | 'warning' | 'info';
recoverable: boolean;
}>;
warnings: Array<{
step?: number;
message: string;
timestamp: string;
severity: 'warning' | 'info';
}>;
// Enhanced metadata
metadata?: {
sessionId: string;
startTime: string;
estimatedCompletionTime?: string;
totalSteps: number;
completedSteps: number;
failedSteps: number;
skippedSteps: number;
averageStepDuration?: number;
performanceMetrics?: {
averageQualityScore: number;
totalErrors: number;
totalWarnings: number;
dataSourceUtilization: Record<string, number>;
};
};
}
// Polling hook for calendar generation progress
// Step information for UI display
export const STEP_INFO = {
1: { name: 'Content Strategy Analysis', description: 'Analyzing content strategy and business goals' },
2: { name: 'Gap Analysis', description: 'Identifying content gaps and opportunities' },
3: { name: 'Audience & Platform Strategy', description: 'Defining audience personas and platform strategies' },
4: { name: 'Calendar Framework', description: 'Creating calendar structure and timeline' },
5: { name: 'Content Pillar Distribution', description: 'Distributing content pillars across timeline' },
6: { name: 'Platform-Specific Strategy', description: 'Optimizing content for specific platforms' },
7: { name: 'Weekly Theme Development', description: 'Generating weekly content themes' },
8: { name: 'Daily Content Planning', description: 'Creating detailed daily content schedules' },
9: { name: 'Content Recommendations', description: 'Generating AI-powered content recommendations' },
10: { name: 'Performance Optimization', description: 'Optimizing content for maximum performance' },
11: { name: 'Strategy Alignment Validation', description: 'Validating alignment with original strategy' },
12: { name: 'Final Calendar Assembly', description: 'Assembling final calendar with all components' }
} as const;
// Polling hook for calendar generation progress with enhanced 12-step support
const useCalendarGenerationPolling = (sessionId: string) => {
const [progress, setProgress] = useState<CalendarGenerationProgress | null>(null);
const [isPolling, setIsPolling] = useState(false);
const [error, setError] = useState<string | null>(null);
const [retryCount, setRetryCount] = useState(0);
const startPolling = useCallback(async () => {
console.log('🎯 Starting polling for session:', sessionId);
setIsPolling(true);
setError(null);
setRetryCount(0);
const poll = async () => {
try {
console.log('🔄 Polling session:', sessionId);
const response = await fetch(`/api/content-planning/calendar-generation/progress/${sessionId}`);
if (!response.ok) {
@@ -49,27 +123,66 @@ const useCalendarGenerationPolling = (sessionId: string) => {
}
const data = await response.json();
console.log('📊 Received progress data:', data);
// 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
currentStep: data.current_step || 0,
stepProgress: data.step_progress || 0,
overallProgress: data.overall_progress || 0,
// Transform step results - handle both formats
stepResults: data.step_results || {},
// Transform quality scores - calculate overall from individual steps
qualityScores: {
overall: calculateOverallQualityScore(data.quality_scores || {}),
step1: Number(data.quality_scores?.step_01 || data.quality_scores?.step1 || 0),
step2: Number(data.quality_scores?.step_02 || data.quality_scores?.step2 || 0),
step3: Number(data.quality_scores?.step_03 || data.quality_scores?.step3 || 0),
step4: Number(data.quality_scores?.step_04 || data.quality_scores?.step4 || 0),
step5: Number(data.quality_scores?.step_05 || data.quality_scores?.step5 || 0),
step6: Number(data.quality_scores?.step_06 || data.quality_scores?.step6 || 0),
step7: Number(data.quality_scores?.step_07 || data.quality_scores?.step7 || 0),
step8: Number(data.quality_scores?.step_08 || data.quality_scores?.step8 || 0),
step9: Number(data.quality_scores?.step_09 || data.quality_scores?.step9 || 0),
step10: Number(data.quality_scores?.step_10 || data.quality_scores?.step10 || 0),
step11: Number(data.quality_scores?.step_11 || data.quality_scores?.step11 || 0),
step12: Number(data.quality_scores?.step_12 || data.quality_scores?.step12 || 0)
},
transparencyMessages: data.transparency_messages || [],
educationalContent: data.educational_content || [],
// Enhanced error handling
errors: data.errors || [],
warnings: data.warnings || [],
// Enhanced metadata
metadata: {
sessionId,
startTime: data.start_time || new Date().toISOString(),
estimatedCompletionTime: data.estimated_completion_time,
totalSteps: 12,
completedSteps: calculateCompletedSteps(data.step_results || {}),
failedSteps: calculateFailedSteps(data.step_results || {}),
skippedSteps: 0,
averageStepDuration: data.average_step_duration,
performanceMetrics: data.performance_metrics
}
};
console.log('✅ Transformed progress:', transformedProgress);
setProgress(transformedProgress);
setRetryCount(0); // Reset retry count on successful response
// Check for completion or error
if (data.status === 'completed' || data.status === 'error') {
console.log('🏁 Process completed with status:', data.status);
setIsPolling(false);
if (data.status === 'error') {
setError(data.errors?.[0]?.message || 'Unknown error occurred');
const errorMessage = data.errors?.[0]?.message || 'Unknown error occurred';
setError(errorMessage);
}
return;
}
@@ -77,22 +190,120 @@ const useCalendarGenerationPolling = (sessionId: string) => {
// 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);
console.error('Calendar generation polling error:', error);
const errorMessage = error instanceof Error ? error.message : 'Polling failed';
setError(errorMessage);
// Implement exponential backoff for retries
const newRetryCount = retryCount + 1;
setRetryCount(newRetryCount);
if (newRetryCount <= 5) {
// Retry with exponential backoff: 5s, 10s, 20s, 40s, 80s
const retryDelay = Math.min(5000 * Math.pow(2, newRetryCount - 1), 80000);
console.log(`🔄 Retrying in ${retryDelay}ms (attempt ${newRetryCount}/5)`);
setTimeout(poll, retryDelay);
} else {
setIsPolling(false);
setError('Maximum retry attempts reached. Please refresh the page.');
}
}
};
poll();
}, [sessionId]);
}, [sessionId, retryCount]);
const stopPolling = useCallback(() => {
setIsPolling(false);
}, []);
return { progress, isPolling, error, startPolling, stopPolling };
const resetPolling = useCallback(() => {
setIsPolling(false);
setError(null);
setRetryCount(0);
setProgress(null);
}, []);
// Helper functions for step analysis
const getStepStatus = useCallback((stepNumber: number) => {
if (!progress) return 'pending';
return progress.stepResults[stepNumber]?.status || 'pending';
}, [progress]);
const getStepQualityScore = useCallback((stepNumber: number) => {
if (!progress) return 0;
return progress.qualityScores[`step${stepNumber}` as keyof QualityScores] || 0;
}, [progress]);
const getCompletedSteps = useCallback(() => {
if (!progress) return 0;
return progress.metadata?.completedSteps || 0;
}, [progress]);
const getFailedSteps = useCallback(() => {
if (!progress) return 0;
return Object.values(progress.stepResults).filter(result => result.status === 'failed').length;
}, [progress]);
const getStepErrors = useCallback((stepNumber: number) => {
if (!progress) return [];
return progress.stepResults[stepNumber]?.errors || [];
}, [progress]);
const getStepWarnings = useCallback((stepNumber: number) => {
if (!progress) return [];
return progress.stepResults[stepNumber]?.warnings || [];
}, [progress]);
// Helper functions for data transformation
const calculateOverallQualityScore = (qualityScores: any): number => {
const stepScores = [];
for (let i = 1; i <= 12; i++) {
const stepKey = `step_${i.toString().padStart(2, '0')}`;
const score = Number(qualityScores[stepKey] || qualityScores[`step${i}`] || 0);
if (score > 0) {
stepScores.push(score);
}
}
return stepScores.length > 0 ? stepScores.reduce((a, b) => a + b, 0) / stepScores.length : 0;
};
const calculateCompletedSteps = (stepResults: any): number => {
let completed = 0;
for (const stepKey in stepResults) {
if (stepResults[stepKey]?.status === 'completed') {
completed++;
}
}
return completed;
};
const calculateFailedSteps = (stepResults: any): number => {
let failed = 0;
for (const stepKey in stepResults) {
if (stepResults[stepKey]?.status === 'error' || stepResults[stepKey]?.status === 'failed') {
failed++;
}
}
return failed;
};
return {
progress,
isPolling,
error,
retryCount,
startPolling,
stopPolling,
resetPolling,
getStepStatus,
getStepQualityScore,
getCompletedSteps,
getFailedSteps,
getStepErrors,
getStepWarnings
};
};
export default useCalendarGenerationPolling;
export type { CalendarGenerationProgress, QualityScores };
export type { CalendarGenerationProgress, QualityScores, StepResult };

View File

@@ -31,7 +31,6 @@ import { type CalendarConfig } from './CalendarWizardSteps/types';
interface CalendarGenerationWizardProps {
userData: any;
onGenerateCalendar: (calendarConfig: any) => void;
loading?: boolean;
strategyContext?: any;
fromStrategyActivation?: boolean;
}
@@ -39,17 +38,19 @@ interface CalendarGenerationWizardProps {
const CalendarGenerationWizard: React.FC<CalendarGenerationWizardProps> = ({
userData,
onGenerateCalendar,
loading = false,
strategyContext,
fromStrategyActivation = false
}) => {
// SIMPLIFIED CALENDAR WIZARD - Focused on calendar-specific inputs only
// Strategy context is used internally during generation, not for mapping
console.log('🔍 CalendarGenerationWizard: Starting calendar wizard', {
fromStrategyActivation,
hasStrategyContext: !!strategyContext
});
// Only log when component mounts or key props change
useEffect(() => {
console.log('🔍 CalendarGenerationWizard: Starting calendar wizard', {
fromStrategyActivation,
hasStrategyContext: !!strategyContext
});
}, [fromStrategyActivation, strategyContext]);
// Use enhanced state management with calendar-specific config
const [state, actions] = useCalendarWizardState(onGenerateCalendar);
@@ -89,18 +90,13 @@ const CalendarGenerationWizard: React.FC<CalendarGenerationWizardProps> = ({
// Create stable callback for generate calendar
const handleGenerateCalendar = useCallback(() => {
console.log('🎯 CalendarGenerationWizard: handleGenerateCalendar called');
console.log('🎯 CalendarGenerationWizard: calendarConfig:', calendarConfig);
onGenerateCalendar(calendarConfig);
}, [onGenerateCalendar, calendarConfig]);
// Show loading state if generating
if (isGenerating) {
return (
<CalendarGenerationLoading
progress={generationProgress}
error={error || undefined}
/>
);
}
// REMOVED: Show loading state if generating - Let modal handle progress display
// The modal should open immediately and show progress inside
// Show error state if there's an error
if (error) {
@@ -136,7 +132,6 @@ const CalendarGenerationWizard: React.FC<CalendarGenerationWizardProps> = ({
<GenerateCalendarStep
calendarConfig={calendarConfig}
onGenerateCalendar={handleGenerateCalendar}
loading={loading}
strategyContext={strategyContext}
isFromStrategyActivation={fromStrategyActivation}
/>
@@ -182,9 +177,16 @@ const CalendarGenerationWizard: React.FC<CalendarGenerationWizardProps> = ({
<Box sx={{ mt: 2 }}>
<Button
variant="contained"
onClick={index === steps.length - 1 ? handleGenerateCalendar : handleNext}
onClick={(e) => {
console.log('🎯 Button clicked:', { index, isLastStep: index === steps.length - 1 });
if (index === steps.length - 1) {
handleGenerateCalendar();
} else {
handleNext();
}
}}
sx={{ mr: 1 }}
disabled={loading || !actions.canProceedToStep(index + 1)}
disabled={isLoading || !actions.canProceedToStep(index + 1)}
>
{index === steps.length - 1 ? 'Generate Calendar' : 'Continue'}
</Button>

View File

@@ -29,7 +29,6 @@ import { type CalendarConfig } from './types';
interface GenerateCalendarStepProps {
calendarConfig: CalendarConfig;
onGenerateCalendar: (config: CalendarConfig) => void;
loading?: boolean;
strategyContext?: any;
isFromStrategyActivation?: boolean; // Strategy context available for generation
}
@@ -37,7 +36,6 @@ interface GenerateCalendarStepProps {
const GenerateCalendarStep: React.FC<GenerateCalendarStepProps> = ({
calendarConfig,
onGenerateCalendar,
loading = false,
strategyContext,
isFromStrategyActivation = false
}) => {
@@ -115,7 +113,7 @@ const GenerateCalendarStep: React.FC<GenerateCalendarStepProps> = ({
onGenerateCalendar(enhancedConfig);
};
const canGenerate = validationErrors.length === 0 && !loading;
const canGenerate = validationErrors.length === 0;
return (
<Box sx={{ p: 2 }}>
@@ -442,14 +440,7 @@ const GenerateCalendarStep: React.FC<GenerateCalendarStepProps> = ({
</Card>
{/* Note: Generate button is handled by the stepper navigation above */}
{loading && (
<Box sx={{ mt: 3 }}>
<LinearProgress />
<Typography variant="body2" color="text.secondary" sx={{ mt: 1, textAlign: 'center' }}>
Generating your optimized content calendar...
</Typography>
</Box>
)}
{/* REMOVED: Loading state display - Let modal handle all progress */}
</Box>
);
};

View File

@@ -168,46 +168,23 @@ export const useCalendarWizardState = (
}
}, [canProceedToStep, clearErrors]);
// Generate calendar with progress tracking
// Generate calendar - simplified to just call the callback
// Let the modal handle all progress display
const generateCalendar = useCallback(async () => {
if (!validateAllSteps()) {
setError('Please fix validation errors before generating calendar');
return;
}
setIsGenerating(true);
setGenerationProgress(0);
setError(null);
try {
// Simulate progress updates
const progressInterval = setInterval(() => {
setGenerationProgress(prev => {
if (prev >= 90) {
clearInterval(progressInterval);
return 90;
}
return prev + 10;
});
}, 200);
// Simply call the callback - let the modal handle progress
await onGenerateCalendarRef.current(calendarConfig);
clearInterval(progressInterval);
setGenerationProgress(100);
// Reset after successful generation
setTimeout(() => {
setIsGenerating(false);
setGenerationProgress(0);
}, 1000);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to generate calendar');
setIsGenerating(false);
setGenerationProgress(0);
}
}, [calendarConfig, validateAllSteps]); // Remove onGenerateCalendar dependency
}, [calendarConfig, validateAllSteps]);
const state: WizardState = useMemo(() => ({
activeStep,

View File

@@ -1,9 +1,10 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
Box,
Typography,
Tabs,
Tab
Tab,
Button
} from '@mui/material';
import {
AutoAwesome as AutoAwesomeIcon,
@@ -49,6 +50,7 @@ const CreateTab: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [currentCalendarConfig, setCurrentCalendarConfig] = useState<CalendarConfig | null>(null);
const [sessionId, setSessionId] = useState<string>('');
const [isStartingGeneration, setIsStartingGeneration] = useState(false);
const location = useLocation();
const { state: { strategyContext }, isFromStrategyActivation } = useStrategyCalendarContext();
@@ -99,6 +101,16 @@ const CreateTab: React.FC = () => {
const handleGenerateCalendar = useCallback(async (calendarConfig: CalendarConfig) => {
try {
console.log('🎯 handleGenerateCalendar called with config:', calendarConfig);
// OPEN MODAL IMMEDIATELY - Don't wait for backend response
console.log('🎯 Opening modal immediately');
setCurrentCalendarConfig(calendarConfig);
setIsModalOpen(true);
// Set loading state to prevent multiple clicks
setIsStartingGeneration(true);
// Transform calendarConfig to match backend CalendarGenerationRequest format
const requestData = {
user_id: 1, // Default user ID
@@ -109,34 +121,74 @@ const CreateTab: React.FC = () => {
force_refresh: false
};
console.log('🎯 Starting calendar generation with modal:', requestData);
console.log('🎯 Starting calendar generation request:', 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),
});
// Call the new start endpoint to get session ID with retry logic
let startResponse;
let retryCount = 0;
const maxRetries = 3;
if (!startResponse.ok) {
throw new Error(`Failed to start calendar generation: ${startResponse.statusText}`);
while (retryCount < maxRetries) {
try {
startResponse = await fetch('/api/content-planning/calendar-generation/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData),
});
if (startResponse.ok) {
break; // Success, exit retry loop
} else {
console.warn(`⚠️ Attempt ${retryCount + 1} failed with status: ${startResponse.status}`);
retryCount++;
if (retryCount < maxRetries) {
// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
}
}
} catch (error) {
console.warn(`⚠️ Attempt ${retryCount + 1} failed with error:`, error);
retryCount++;
if (retryCount < maxRetries) {
// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
}
}
}
if (!startResponse || !startResponse.ok) {
throw new Error(`Failed to start calendar generation after ${maxRetries} attempts: ${startResponse?.statusText || 'Network error'}`);
}
const startData = await startResponse.json();
const sessionId = startData.session_id;
// Store the session ID and calendar config for the modal
setSessionId(sessionId);
setCurrentCalendarConfig(calendarConfig);
console.log('🎯 Backend response received, session ID:', sessionId);
console.log('🎯 Session status:', startData.status);
// Open the modal to show progress
setIsModalOpen(true);
// Update modal with the real session ID
console.log('🎯 Updating modal with real session ID');
setSessionId(sessionId);
console.log('🎯 Modal updated with session ID - polling should start immediately');
} catch (error) {
console.error('Error starting calendar generation:', error);
// The modal will handle error display
// Show user-friendly error message
const errorMessage = error instanceof Error ? error.message : 'Failed to start calendar generation';
console.error('❌ Calendar generation failed:', errorMessage);
// Show error to user and close modal
alert(`Failed to start calendar generation: ${errorMessage}`);
setIsModalOpen(false);
setCurrentCalendarConfig(null);
setSessionId('');
} finally {
// Clear loading state
setIsStartingGeneration(false);
}
}, [userData, strategyContext]);
@@ -165,11 +217,15 @@ const CreateTab: React.FC = () => {
setSessionId('');
}, []);
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
Create
</Typography>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs value={tabValue} onChange={handleTabChange} aria-label="create tabs">
@@ -200,33 +256,30 @@ const CreateTab: React.FC = () => {
<CalendarGenerationWizard
userData={userData}
onGenerateCalendar={handleGenerateCalendar}
loading={false}
strategyContext={strategyContext}
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}
/>
)}
<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>
);
};