import React, { useEffect, useRef } from 'react'; import { Modal } from '../ui/modal'; import Button from '../ui/button/Button'; export interface ProgressModalProps { isOpen: boolean; title: string; percentage: number; // 0-100 status: 'pending' | 'processing' | 'completed' | 'error'; message: string; details?: { current: number; total: number; completed: number; currentItem?: string; phase?: string; }; onClose?: () => void; onCancel?: () => void; taskId?: string; functionId?: string; // AI function ID for tracking (e.g., "ai-cluster-01") stepLogs?: Array<{ stepNumber: number; stepName: string; status: string; message: string; timestamp?: number; }>; // Step logs for debugging } // Success messages per function with counts const getSuccessMessage = (functionId?: string, title?: string, stepLogs?: any[]): string => { const funcName = functionId?.toLowerCase() || title?.toLowerCase() || ''; // Extract counts from step logs const extractCount = (pattern: RegExp, logs: any[]): string => { for (const log of logs) { const match = log.message?.match(pattern); if (match && match[1]) return match[1]; } return ''; }; if (funcName.includes('cluster')) { const keywordCount = extractCount(/(\d+)\s+keyword/i, stepLogs || []); const clusterCount = extractCount(/(\d+)\s+cluster/i, stepLogs || []); if (keywordCount && clusterCount) { return `Clustering complete — ${clusterCount} cluster${clusterCount !== '1' ? 's' : ''} created from ${keywordCount} keyword${keywordCount !== '1' ? 's' : ''}.`; } else if (clusterCount) { return `Clustering complete — ${clusterCount} cluster${clusterCount !== '1' ? 's' : ''} created.`; } else if (keywordCount) { return `Clustering complete — processed ${keywordCount} keyword${keywordCount !== '1' ? 's' : ''}.`; } return 'Clustering complete — keywords grouped into meaningful clusters.'; } if (funcName.includes('idea')) { const clusterCount = extractCount(/(\d+)\s+cluster/i, stepLogs || []); const ideaCount = extractCount(/(\d+)\s+idea/i, stepLogs || []); if (ideaCount) { return `Content ideas created successfully — ${ideaCount} idea${ideaCount !== '1' ? 's' : ''} generated.`; } else if (clusterCount) { return `Content ideas created successfully — generated for ${clusterCount} cluster${clusterCount !== '1' ? 's' : ''}.`; } return 'Content ideas and outlines created successfully.'; } if (funcName.includes('content')) { const taskCount = extractCount(/(\d+)\s+task/i, stepLogs || []); const articleCount = extractCount(/(\d+)\s+article/i, stepLogs || []); if (articleCount) { return `Article${articleCount !== '1' ? 's' : ''} drafted successfully — ${articleCount} article${articleCount !== '1' ? 's' : ''} generated.`; } else if (taskCount) { return `Article${taskCount !== '1' ? 's' : ''} drafted successfully — ${taskCount} task${taskCount !== '1' ? 's' : ''} completed.`; } return 'Article drafted successfully.'; } if (funcName.includes('image')) { const imageCount = extractCount(/(\d+)\s+image/i, stepLogs || []); const taskCount = extractCount(/(\d+)\s+task/i, stepLogs || []); if (imageCount) { return `Images created successfully — ${imageCount} image${imageCount !== '1' ? 's' : ''} generated.`; } else if (taskCount) { return `Images created successfully — ${taskCount} task${taskCount !== '1' ? 's' : ''} completed.`; } return 'Images created and saved successfully.'; } return 'Task completed successfully.'; }; // Get step definitions per function const getStepsForFunction = (functionId?: string, title?: string): Array<{phase: string, label: string}> => { const funcName = functionId?.toLowerCase() || title?.toLowerCase() || ''; if (funcName.includes('cluster')) { return [ { phase: 'INIT', label: 'Validating keywords' }, { phase: 'PREP', label: 'Loading keyword data' }, { phase: 'AI_CALL', label: 'Generating clusters' }, { phase: 'PARSE', label: 'Organizing clusters' }, { phase: 'SAVE', label: 'Saving clusters' }, ]; } if (funcName.includes('idea')) { return [ { phase: 'INIT', label: 'Validating clusters' }, { phase: 'PREP', label: 'Loading cluster data' }, { phase: 'AI_CALL', label: 'Generating blog ideas' }, { phase: 'PARSE', label: 'Structuring outlines' }, { phase: 'SAVE', label: 'Saving ideas' }, ]; } if (funcName.includes('content')) { return [ { phase: 'INIT', label: 'Validating task' }, { phase: 'PREP', label: 'Preparing content idea' }, { phase: 'AI_CALL', label: 'Writing article with AI' }, { phase: 'PARSE', label: 'Formatting content' }, { phase: 'SAVE', label: 'Saving article' }, ]; } if (funcName.includes('image')) { return [ { phase: 'INIT', label: 'Validating task' }, { phase: 'PREP', label: 'Extracting image prompts' }, { phase: 'AI_CALL', label: 'Creating images with AI' }, { phase: 'PARSE', label: 'Processing images' }, { phase: 'SAVE', label: 'Saving images' }, ]; } // Default fallback return [ { phase: 'INIT', label: 'Initializing...' }, { phase: 'PREP', label: 'Preparing...' }, { phase: 'AI_CALL', label: 'Processing with AI...' }, { phase: 'PARSE', label: 'Processing results...' }, { phase: 'SAVE', label: 'Saving results...' }, ]; }; // Get current phase from step logs or percentage const getCurrentPhase = (stepLogs: any[], percentage: number): string => { if (stepLogs.length > 0) { const lastStep = stepLogs[stepLogs.length - 1]; return lastStep.stepName || ''; } // Fallback to percentage if (percentage < 10) return 'INIT'; if (percentage < 25) return 'PREP'; if (percentage < 70) return 'AI_CALL'; if (percentage < 85) return 'PARSE'; if (percentage < 100) return 'SAVE'; return 'DONE'; }; // Check if step is completed const isStepCompleted = (stepPhase: string, currentPhase: string, stepLogs: any[]): boolean => { const phaseOrder = ['INIT', 'PREP', 'AI_CALL', 'PARSE', 'SAVE', 'DONE']; const stepIndex = phaseOrder.indexOf(stepPhase); const currentIndex = phaseOrder.indexOf(currentPhase); // Step is completed if we've moved past it if (currentIndex > stepIndex) return true; // Or if we have a log entry for it with success status return stepLogs.some(log => log.stepName === stepPhase && log.status === 'success' ); }; // Check if step is in progress const isStepInProgress = (stepPhase: string, currentPhase: string): boolean => { return stepPhase === currentPhase; }; export default function ProgressModal({ isOpen, title, percentage, status, message, onClose, onCancel, taskId, functionId, stepLogs = [], }: ProgressModalProps) { const hasAutoClosedRef = useRef(false); // Track which steps are visually completed (with delay) const [visuallyCompletedSteps, setVisuallyCompletedSteps] = React.useState>(new Set()); const stepCompletionTimersRef = useRef>(new Map()); // Get steps for this function const steps = getStepsForFunction(functionId, title); const currentPhase = getCurrentPhase(stepLogs, percentage); // Format step message with counts and better formatting const formatStepMessage = (stepPhase: string, stepLog: any, defaultLabel: string, allStepLogs: any[], functionId?: string, title?: string): string => { const funcName = (functionId || title || '').toLowerCase(); const message = stepLog?.message || defaultLabel; // Extract counts from message const extractCount = (pattern: RegExp): string => { const match = message.match(pattern); return match && match[1] ? match[1] : ''; }; if (funcName.includes('cluster')) { if (stepPhase === 'INIT') { // For INIT: Message already includes keyword names from backend (e.g., "Validating keyword1, keyword2, keyword3 and 5 more keywords") // Just return the message as-is since backend formats it return message; } else if (stepPhase === 'PREP') { // For PREP: Show count of keywords being loaded const keywordCount = extractCount(/(\d+)\s+keyword/i); if (keywordCount) { return `Loading ${keywordCount} keyword${keywordCount !== '1' ? 's' : ''} for clustering`; } return message; } else if (stepPhase === 'AI_CALL') { // For AI_CALL: Show count of keywords being sent const keywordCount = extractCount(/(\d+)\s+keyword/i); if (keywordCount) { return `Sending ${keywordCount} keyword${keywordCount !== '1' ? 's' : ''} for clustering`; } return message; } else if (stepPhase === 'PARSE') { // For PARSE: Show count of clusters created const clusterCount = extractCount(/(\d+)\s+cluster/i); if (clusterCount) { return `${clusterCount} cluster${clusterCount !== '1' ? 's' : ''} created`; } // Try to find cluster count in any step log for (const log of allStepLogs) { const count = log.message?.match(/(\d+)\s+cluster/i); if (count && count[1]) { return `${count[1]} cluster${count[1] !== '1' ? 's' : ''} created`; } } return message; } else if (stepPhase === 'SAVE') { // For SAVE: Show count of clusters being saved const clusterCount = extractCount(/(\d+)\s+cluster/i); if (clusterCount) { return `Saving ${clusterCount} cluster${clusterCount !== '1' ? 's' : ''}`; } return message; } } else if (funcName.includes('idea')) { if (stepPhase === 'PARSE') { const ideaCount = extractCount(/(\d+)\s+idea/i); if (ideaCount) { return `${ideaCount} idea${ideaCount !== '1' ? 's' : ''} created`; } } } else if (funcName.includes('content')) { if (stepPhase === 'PARSE') { const articleCount = extractCount(/(\d+)\s+article/i); if (articleCount) { return `${articleCount} article${articleCount !== '1' ? 's' : ''} created`; } } } else if (funcName.includes('image')) { if (stepPhase === 'PARSE') { const imageCount = extractCount(/(\d+)\s+image/i); if (imageCount) { return `${imageCount} image${imageCount !== '1' ? 's' : ''} created`; } } } return message; }; // Build checklist items with visual completion state (needed for allStepsVisuallyCompleted) const checklistItems = steps.map((step) => { const actuallyCompleted = isStepCompleted(step.phase, currentPhase, stepLogs); const visuallyCompleted = visuallyCompletedSteps.has(step.phase); const inProgress = isStepInProgress(step.phase, currentPhase) && !visuallyCompleted; // Get step log and format message const stepLog = stepLogs.find(log => log.stepName === step.phase); const stepMessage = formatStepMessage(step.phase, stepLog, step.label, stepLogs, functionId, title); return { label: stepMessage, phase: step.phase, completed: visuallyCompleted, inProgress, }; }); // Check if all steps are visually completed const allStepsVisuallyCompleted = steps.length > 0 && steps.every(step => visuallyCompletedSteps.has(step.phase)); // Track step completions with 2-second delay between each step useEffect(() => { if (!isOpen) { // Reset when modal closes setVisuallyCompletedSteps(new Set()); stepCompletionTimersRef.current.forEach(timer => clearTimeout(timer)); stepCompletionTimersRef.current.clear(); return; } const phaseOrder = ['INIT', 'PREP', 'AI_CALL', 'PARSE', 'SAVE', 'DONE']; // Check each step in order steps.forEach((step, index) => { const stepPhase = step.phase; const stepIndex = phaseOrder.indexOf(stepPhase); const currentIndex = phaseOrder.indexOf(currentPhase); // Check if step should be completed (we've moved past it or have log entry) const shouldBeCompleted = currentIndex > stepIndex || stepLogs.some(log => log.stepName === stepPhase && log.status === 'success'); // If step should be completed but isn't visually completed yet if (shouldBeCompleted && !visuallyCompletedSteps.has(stepPhase)) { // Check if previous step is visually completed (or if this is the first step) const previousStep = index > 0 ? steps[index - 1] : null; const previousStepCompleted = !previousStep || visuallyCompletedSteps.has(previousStep.phase); // Only schedule if previous step is completed (or this is first step) if (previousStepCompleted) { // Clear any existing timer for this step const existingTimer = stepCompletionTimersRef.current.get(stepPhase); if (existingTimer) { clearTimeout(existingTimer); } // Calculate delay: 2 seconds after previous step visually completed (or 0 for first step) const delay = previousStep ? 2000 : 0; // Schedule completion const timer = setTimeout(() => { setVisuallyCompletedSteps(prev => new Set([...prev, stepPhase])); stepCompletionTimersRef.current.delete(stepPhase); }, delay); stepCompletionTimersRef.current.set(stepPhase, timer); } } }); // Cleanup on unmount return () => { stepCompletionTimersRef.current.forEach(timer => clearTimeout(timer)); stepCompletionTimersRef.current.clear(); }; }, [isOpen, currentPhase, stepLogs, steps, visuallyCompletedSteps]); // Auto-close on completion after all steps are visually completed + 3 seconds useEffect(() => { if (status === 'completed' && allStepsVisuallyCompleted && onClose && !hasAutoClosedRef.current) { hasAutoClosedRef.current = true; // Wait 3 seconds after success alert appears const timer = setTimeout(() => { onClose(); }, 3000); return () => clearTimeout(timer); } if (status !== 'completed' || !allStepsVisuallyCompleted) { hasAutoClosedRef.current = false; } }, [status, allStepsVisuallyCompleted, onClose]); // Show success alert only when all steps are visually completed AND status is completed const showSuccess = status === 'completed' && allStepsVisuallyCompleted; const successMessage = getSuccessMessage(functionId, title, stepLogs); return ( {})} className="max-w-lg" showCloseButton={status === 'completed' || status === 'error'} >
{/* Header */}
{status === 'error' ? ( ) : showSuccess ? ( ) : ( )}

{title}

{!showSuccess && status !== 'completed' && (

{message}

)} {status === 'completed' && !allStepsVisuallyCompleted && (

Processing...

)}
{/* Success Alert (shown when all steps are visually completed) */} {showSuccess && (
{/* Big centered check icon */}
{/* Dark success alert box with centered text */}

{successMessage}

)} {/* Checklist-style Progress Steps - Always visible */}
{checklistItems.map((item, index) => (
{/* Icon */}
{item.completed ? ( ) : item.inProgress ? ( ) : (
)}
{/* Step Text */} {item.label}
))}
{/* Footer */}
{onCancel && status !== 'completed' && status !== 'error' && ( )} {(status === 'completed' || status === 'error') && onClose && ( )}
); }