Alwrity calendar generation framework - step 1-3 completed with real database integration
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user