import React, { useEffect, useRef, useMemo } 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 `✓ Created ${clusterCount} cluster${clusterCount !== '1' ? 's' : ''} from ${keywordCount} keyword${keywordCount !== '1' ? 's' : ''}`; } else if (clusterCount) { return `✓ Created ${clusterCount} cluster${clusterCount !== '1' ? 's' : ''}`; } else if (keywordCount) { return `✓ Created clusters from ${keywordCount} keyword${keywordCount !== '1' ? 's' : ''}`; } return '✓ Keywords clustered successfully'; } if (funcName.includes('idea')) { const ideaCount = extractCount(/(\d+)\s+idea/i, stepLogs || []); const clusterCount = extractCount(/(\d+)\s+cluster/i, stepLogs || []); if (ideaCount && clusterCount) { return `✓ Generated ${ideaCount} content idea${ideaCount !== '1' ? 's' : ''} from ${clusterCount} cluster${clusterCount !== '1' ? 's' : ''}`; } else if (ideaCount) { return `✓ Generated ${ideaCount} content idea${ideaCount !== '1' ? 's' : ''} with outlines`; } return '✓ Content ideas & outlines created successfully'; } if (funcName.includes('content')) { const taskCount = extractCount(/(\d+)\s+task/i, stepLogs || []); const articleCount = extractCount(/(\d+)\s+article/i, stepLogs || []); const wordCount = extractCount(/(\d+[,\d]*)\s+word/i, stepLogs || []); if (articleCount && wordCount) { return `✓ ${articleCount} article${articleCount !== '1' ? 's' : ''} generated (${wordCount} words total)`; } else if (articleCount) { return `✓ ${articleCount} article${articleCount !== '1' ? 's' : ''} generated`; } else if (taskCount) { return `✓ ${taskCount} article${taskCount !== '1' ? 's' : ''} generated`; } return '✓ Article generated successfully'; } // Check for image generation from prompts FIRST (more specific) if (funcName.includes('image') && funcName.includes('from')) { // Image generation from prompts const imageCount = extractCount(/(\d+)\s+image/i, stepLogs || []); if (imageCount) { return `✓ ${imageCount} image${imageCount !== '1' ? 's' : ''} generated and saved`; } return '✓ Images generated and saved'; } else if (funcName.includes('image') && (funcName.includes('prompt') || funcName.includes('extract'))) { // Image prompt generation // Try to extract from SAVE step message first (most reliable) const saveStepLog = stepLogs?.find(log => log.stepName === 'SAVE'); if (saveStepLog?.message) { // Look for "Assigning X Prompts to Dedicated Slots" const countMatch = saveStepLog.message.match(/Assigning (\d+)\s+Prompts/i); if (countMatch) { const totalPrompts = parseInt(countMatch[1], 10); const inArticleCount = totalPrompts > 1 ? totalPrompts - 1 : 0; if (inArticleCount > 0) { return `✓ ${totalPrompts} image prompts ready (1 featured + ${inArticleCount} in-article)`; } else { return `✓ 1 image prompt ready`; } } } // Try to extract from PREP step to get total count const prepStepLog = stepLogs?.find(log => log.stepName === 'PREP'); if (prepStepLog?.message) { const match = prepStepLog.message.match(/Mapping Content for (\d+)\s+Image Prompts/i); if (match && match[1]) { const totalPrompts = parseInt(match[1], 10); const inArticleCount = totalPrompts > 1 ? totalPrompts - 1 : 0; if (inArticleCount > 0) { return `✓ ${totalPrompts} image prompts ready (1 featured + ${inArticleCount} in-article)`; } else { return `✓ 1 image prompt ready`; } } } // Fallback: extract prompt count from any step log const promptCount = extractCount(/(\d+)\s+prompt/i, stepLogs || []); if (promptCount) { const totalPrompts = parseInt(promptCount, 10); const inArticleCount = totalPrompts > 1 ? totalPrompts - 1 : 0; if (inArticleCount > 0) { return `✓ ${totalPrompts} image prompts ready (1 featured + ${inArticleCount} in-article)`; } else { return `✓ 1 image prompt ready`; } } // Default message return '✓ Image prompts ready'; } return '✓ Task completed successfully'; }; // Get step definitions per function - these are default labels that get replaced with dynamic counts 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 for clustering' }, { phase: 'PREP', label: 'Analyzing keyword relationships' }, { phase: 'AI_CALL', label: 'Grouping keywords by search intent' }, { phase: 'PARSE', label: 'Organizing semantic clusters' }, { phase: 'SAVE', label: 'Saving clusters' }, ]; } if (funcName.includes('idea')) { return [ { phase: 'INIT', label: 'Analyzing clusters for content opportunities' }, { phase: 'PREP', label: 'Mapping keywords to topic briefs' }, { phase: 'AI_CALL', label: 'Generating content ideas' }, { phase: 'PARSE', label: 'Structuring article outlines' }, { phase: 'SAVE', label: 'Saving content ideas with outlines' }, ]; } if (funcName.includes('content')) { return [ { phase: 'INIT', label: 'Preparing articles for generation' }, { phase: 'PREP', label: 'Building content brief with target keywords' }, { phase: 'AI_CALL', label: 'Writing articles with Igny8 Semantic AI' }, { phase: 'PARSE', label: 'Formatting HTML content and metadata' }, { phase: 'SAVE', label: 'Saving articles' }, ]; } // Check for image generation from prompts FIRST (more specific) if (funcName.includes('image') && funcName.includes('from')) { // Image generation from prompts return [ { phase: 'INIT', label: 'Queuing images for generation' }, { phase: 'PREP', label: 'Preparing AI image generation' }, { phase: 'AI_CALL', label: 'Generating images with AI' }, { phase: 'PARSE', label: 'Processing generated images' }, { phase: 'SAVE', label: 'Uploading images to media library' }, ]; } else if (funcName.includes('image') && (funcName.includes('prompt') || funcName.includes('extract'))) { // Image prompt generation return [ { phase: 'INIT', label: 'Analyzing content for image opportunities' }, { phase: 'PREP', label: 'Identifying image slots' }, { phase: 'AI_CALL', label: 'Creating optimized prompts' }, { phase: 'PARSE', label: 'Refining contextual image descriptions' }, { phase: 'SAVE', label: 'Assigning prompts to image slots' }, ]; } // Default fallback return [ { phase: 'INIT', label: 'Initializing...' }, { phase: 'PREP', label: 'Preparing...' }, { phase: 'AI_CALL', label: 'Processing with Igny8 Semantic 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) { // Track which steps are visually completed (with delay) const [visuallyCompletedSteps, setVisuallyCompletedSteps] = React.useState>(new Set()); const stepCompletionTimersRef = useRef>(new Map()); const visuallyCompletedStepsRef = useRef>(new Set()); const lastProcessedStepLogsHashRef = useRef(''); const lastProcessedPhaseRef = useRef(''); const lastVisuallyCompletedCountRef = useRef(0); // Sync ref with state useEffect(() => { visuallyCompletedStepsRef.current = visuallyCompletedSteps; }, [visuallyCompletedSteps]); // Track count to detect when steps complete visually (without causing loops) const visuallyCompletedCount = visuallyCompletedSteps.size; // Memoize steps to prevent unnecessary re-renders const steps = useMemo(() => getStepsForFunction(functionId, title), [functionId, title]); // Memoize currentPhase to prevent unnecessary re-renders const currentPhase = useMemo(() => getCurrentPhase(stepLogs, percentage), [stepLogs, percentage]); // Create a stable hash of stepLogs to detect meaningful changes const stepLogsHash = useMemo(() => { return JSON.stringify(stepLogs.map(log => ({ stepName: log.stepName, status: log.status, }))); }, [stepLogs]); // 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] : ''; }; // Helper to extract count from all step logs const extractCountFromLogs = (pattern: RegExp): string => { for (const log of allStepLogs) { const match = log.message?.match(pattern); if (match && match[1]) return match[1]; } return ''; }; if (funcName.includes('cluster')) { if (stepPhase === 'INIT') { // For INIT: Try to extract keyword count const keywordCount = extractCount(/(\d+)\s+keyword/i) || extractCountFromLogs(/(\d+)\s+keyword/i); if (keywordCount) { return `Validating ${keywordCount} keyword${keywordCount !== '1' ? 's' : ''} for clustering`; } // Try to extract from "and X more keywords" format const moreMatch = message.match(/(\d+)\s+more keyword/i); if (moreMatch) { const totalCount = parseInt(moreMatch[1], 10) + 3; // 3 shown + more return `Validating ${totalCount} keywords for clustering`; } return 'Validating keywords for clustering'; } else if (stepPhase === 'PREP') { // For PREP: Show "Analyzing keyword relationships" return 'Analyzing keyword relationships'; } else if (stepPhase === 'AI_CALL') { // For AI_CALL: Try to get keyword count const keywordCount = extractCount(/(\d+)\s+keyword/i) || extractCountFromLogs(/(\d+)\s+keyword/i); if (keywordCount) { return `Grouping keywords by search intent (${keywordCount} keywords)`; } return 'Grouping keywords by search intent'; } else if (stepPhase === 'PARSE') { // For PARSE: Show "Organizing X semantic clusters" const clusterCount = extractCount(/(\d+)\s+cluster/i) || extractCountFromLogs(/(\d+)\s+cluster/i); if (clusterCount) { return `Organizing ${clusterCount} semantic cluster${clusterCount !== '1' ? 's' : ''}`; } return 'Organizing semantic clusters'; } else if (stepPhase === 'SAVE') { // For SAVE: Show "Saving X clusters with Y keywords" const clusterCount = extractCount(/(\d+)\s+cluster/i) || extractCountFromLogs(/(\d+)\s+cluster/i); const keywordCount = extractCountFromLogs(/(\d+)\s+keyword/i); if (clusterCount && keywordCount) { return `Saving ${clusterCount} cluster${clusterCount !== '1' ? 's' : ''} with ${keywordCount} keywords`; } else if (clusterCount) { return `Saving ${clusterCount} cluster${clusterCount !== '1' ? 's' : ''}`; } return 'Saving clusters'; } } else if (funcName.includes('idea')) { if (stepPhase === 'INIT') { // For INIT: Try to extract cluster count const clusterCount = extractCount(/(\d+)\s+cluster/i); if (clusterCount) { return `Analyzing ${clusterCount} cluster${clusterCount !== '1' ? 's' : ''} for content opportunities`; } // 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 `Analyzing ${count[1]} cluster${count[1] !== '1' ? 's' : ''} for content opportunities`; } } return 'Analyzing clusters for content opportunities'; } else if (stepPhase === 'PREP') { // For PREP: Try to extract keyword count const keywordCount = extractCount(/(\d+)\s+keyword/i); if (keywordCount) { return `Mapping ${keywordCount} keyword${keywordCount !== '1' ? 's' : ''} to topic briefs`; } return 'Mapping keywords to topic briefs'; } else if (stepPhase === 'AI_CALL') { // For AI_CALL: Try to extract cluster count const clusterCount = extractCount(/(\d+)\s+cluster/i); if (clusterCount) { return `Generating content ideas for ${clusterCount} cluster${clusterCount !== '1' ? 's' : ''}`; } // 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 `Generating content ideas for ${count[1]} cluster${count[1] !== '1' ? 's' : ''}`; } } return 'Generating content ideas'; } else if (stepPhase === 'PARSE') { // For PARSE: Show "Structuring X article outlines" const ideaCount = extractCount(/(\d+)\s+idea/i); if (ideaCount) { return `Structuring ${ideaCount} article outline${ideaCount !== '1' ? 's' : ''}`; } // Try to find idea count in any step log for (const log of allStepLogs) { const count = log.message?.match(/(\d+)\s+idea/i); if (count && count[1]) { return `Structuring ${count[1]} article outline${count[1] !== '1' ? 's' : ''}`; } } return 'Structuring article outlines'; } else if (stepPhase === 'SAVE') { // For SAVE: Show "Saving X content ideas with outlines" const ideaCount = extractCount(/(\d+)\s+idea/i); if (ideaCount) { return `Saving ${ideaCount} content idea${ideaCount !== '1' ? 's' : ''} with outlines`; } // Try to find idea count in any step log for (const log of allStepLogs) { const count = log.message?.match(/(\d+)\s+idea/i); if (count && count[1]) { return `Saving ${count[1]} content idea${count[1] !== '1' ? 's' : ''} with outlines`; } } return 'Saving content ideas with outlines'; } } else if (funcName.includes('content')) { if (stepPhase === 'INIT') { // Try to extract task/article count const taskCount = extractCount(/(\d+)\s+task/i) || extractCount(/(\d+)\s+article/i); if (taskCount) { return `Preparing ${taskCount} article${taskCount !== '1' ? 's' : ''} for generation`; } return 'Preparing articles for generation'; } else if (stepPhase === 'PREP') { // Try to extract keyword count const keywordCount = extractCount(/(\d+)\s+keyword/i); if (keywordCount) { return `Building content brief with ${keywordCount} target keyword${keywordCount !== '1' ? 's' : ''}`; } return 'Building content brief with target keywords'; } else if (stepPhase === 'AI_CALL') { // Try to extract count const taskCount = extractCount(/(\d+)\s+task/i) || extractCount(/(\d+)\s+article/i); if (taskCount) { return `Writing ${taskCount} article${taskCount !== '1' ? 's' : ''} with Igny8 Semantic AI`; } return 'Writing articles with Igny8 Semantic AI'; } else if (stepPhase === 'PARSE') { return 'Formatting HTML content and metadata'; } else if (stepPhase === 'SAVE') { const articleCount = extractCount(/(\d+)\s+article/i); const wordCount = extractCount(/(\d+[,\d]*)\s+word/i); if (articleCount && wordCount) { return `Saving ${articleCount} article${articleCount !== '1' ? 's' : ''} (${wordCount} words)`; } else if (articleCount) { return `Saving ${articleCount} article${articleCount !== '1' ? 's' : ''}`; } return 'Saving articles'; } } else if (funcName.includes('image') && funcName.includes('from')) { // Image generation from prompts if (stepPhase === 'INIT') { // Try to get image count const imageCount = extractCount(/(\d+)\s+image/i) || extractCountFromLogs(/(\d+)\s+image/i); if (imageCount) { return `Queuing ${imageCount} image${imageCount !== '1' ? 's' : ''} for generation`; } return 'Queuing images for generation'; } else if (stepPhase === 'PREP') { // Extract image count from PREP step message const imageCount = extractCount(/(\d+)\s+image/i) || extractCountFromLogs(/(\d+)\s+image/i); if (imageCount) { return `Preparing AI image generation (${imageCount} images)`; } return 'Preparing AI image generation'; } else if (stepPhase === 'AI_CALL') { // Extract current image number from message for "Generating image X/Y..." const currentMatch = stepLog?.message?.match(/image (\d+)/i); const totalCount = extractCountFromLogs(/(\d+)\s+image/i); if (currentMatch && totalCount) { return `Generating image ${currentMatch[1]}/${totalCount}...`; } else if (currentMatch) { return `Generating image ${currentMatch[1]}...`; } return 'Generating images with AI'; } else if (stepPhase === 'PARSE') { // Extract image count from PARSE step const imageCount = extractCount(/(\d+)\s+image/i) || extractCountFromLogs(/(\d+)\s+image/i); if (imageCount) { return `Processing ${imageCount} generated image${imageCount !== '1' ? 's' : ''}`; } return 'Processing generated images'; } else if (stepPhase === 'SAVE') { // Extract image count from SAVE step const imageCount = extractCount(/(\d+)\s+image/i) || extractCountFromLogs(/(\d+)\s+image/i); if (imageCount) { return `Uploading ${imageCount} image${imageCount !== '1' ? 's' : ''} to media library`; } return 'Uploading images to media library'; } } else if (funcName.includes('image') && (funcName.includes('prompt') || funcName.includes('extract'))) { // Image prompt generation if (stepPhase === 'INIT') { // Try to get image count const imageCount = extractCount(/(\d+)\s+image/i) || extractCountFromLogs(/(\d+)\s+image/i); if (imageCount) { return `Analyzing content for ${imageCount} image opportunit${imageCount !== '1' ? 'ies' : 'y'}`; } return 'Analyzing content for image opportunities'; } else if (stepPhase === 'PREP') { // Extract total image count and calculate in-article count const totalCount = extractCount(/(\d+)\s+Image Prompts/i) || extractCount(/(\d+)\s+image/i) || extractCountFromLogs(/(\d+)\s+image/i); if (totalCount) { const total = parseInt(totalCount, 10); const inArticleCount = total > 1 ? total - 1 : 0; if (inArticleCount > 0) { return `Identifying featured image and ${inArticleCount} in-article image slot${inArticleCount !== 1 ? 's' : ''}`; } return `Identifying featured image slot`; } return 'Identifying image slots'; } else if (stepPhase === 'AI_CALL') { // For AI_CALL: Try to get count const totalCount = extractCountFromLogs(/(\d+)\s+image/i) || extractCountFromLogs(/(\d+)\s+prompt/i); if (totalCount) { return `Creating optimized prompts for ${totalCount} image${totalCount !== '1' ? 's' : ''}`; } return 'Creating optimized prompts'; } else if (stepPhase === 'PARSE') { // Extract in-article image count from PARSE step const inArticleCount = extractCount(/(\d+)\s+In[‑-]article/i); if (inArticleCount) { return `Refining ${inArticleCount} contextual image description${inArticleCount !== '1' ? 's' : ''}`; } // Fallback: calculate from total const totalCount = extractCountFromLogs(/(\d+)\s+image/i); if (totalCount) { const total = parseInt(totalCount, 10); const inArticle = total > 1 ? total - 1 : 0; if (inArticle > 0) { return `Refining ${inArticle} contextual image description${inArticle !== 1 ? 's' : ''}`; } } return 'Refining contextual image descriptions'; } else if (stepPhase === 'SAVE') { // For SAVE: Extract prompt count from message const promptCount = extractCount(/(\d+)\s+Prompts/i) || extractCount(/(\d+)\s+prompt/i) || extractCountFromLogs(/(\d+)\s+prompt/i); if (promptCount) { return `Assigning ${promptCount} prompt${promptCount !== '1' ? 's' : ''} to image slots`; } return 'Assigning prompts to image slots'; } } return message; }; // Build checklist items with visual completion state (needed for allStepsVisuallyCompleted) const checklistItems = useMemo(() => { return steps.map((step) => { const actuallyCompleted = isStepCompleted(step.phase, currentPhase, stepLogs); const visuallyCompleted = visuallyCompletedSteps.has(step.phase); // Don't show any step as in-progress (no blue styling) // Steps are either completed (green) or pending (gray) const inProgress = false; // 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, }; }); }, [steps, currentPhase, stepLogs, visuallyCompletedSteps, functionId, title]); // 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()); visuallyCompletedStepsRef.current = new Set(); lastProcessedStepLogsHashRef.current = ''; lastProcessedPhaseRef.current = ''; lastVisuallyCompletedCountRef.current = 0; stepCompletionTimersRef.current.forEach(timer => clearTimeout(timer)); stepCompletionTimersRef.current.clear(); return; } // Check if we need to process: // 1. Backend progress changed (stepLogsHash or currentPhase) // 2. A step completed visually (count increased) const hashChanged = stepLogsHash !== lastProcessedStepLogsHashRef.current; const phaseChanged = currentPhase !== lastProcessedPhaseRef.current; const countChanged = visuallyCompletedCount > lastVisuallyCompletedCountRef.current; if (!hashChanged && !phaseChanged && !countChanged) { return; // Nothing changed, skip processing } // Update last processed values lastProcessedStepLogsHashRef.current = stepLogsHash; lastProcessedPhaseRef.current = currentPhase; lastVisuallyCompletedCountRef.current = visuallyCompletedCount; const phaseOrder = ['INIT', 'PREP', 'AI_CALL', 'PARSE', 'SAVE', 'DONE']; // If status is completed, mark all steps as shouldBeCompleted const allStepsShouldComplete = status === 'completed'; // Check each step in order for (let index = 0; index < steps.length; index++) { const step = steps[index]; const stepPhase = step.phase; const stepIndex = phaseOrder.indexOf(stepPhase); const currentIndex = phaseOrder.indexOf(currentPhase); // Check if step should be completed: // 1. Status is completed (all steps should complete) // 2. We've moved past it (currentIndex > stepIndex) // 3. We have a log entry for it with success status const shouldBeCompleted = allStepsShouldComplete || currentIndex > stepIndex || stepLogs.some(log => log.stepName === stepPhase && log.status === 'success'); // If step should be completed but isn't visually completed yet and not already scheduled if (shouldBeCompleted && !visuallyCompletedStepsRef.current.has(stepPhase) && !stepCompletionTimersRef.current.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 || visuallyCompletedStepsRef.current.has(previousStep.phase); // Only schedule if previous step is completed (or this is first step) if (previousStepCompleted) { // 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 => { const newSet = new Set([...prev, stepPhase]); stepCompletionTimersRef.current.delete(stepPhase); return newSet; }); }, delay); stepCompletionTimersRef.current.set(stepPhase, timer); // Only process one step at a time - break after scheduling the first eligible step break; } else { // Previous step is not completed yet, stop processing break; } } } // Cleanup on unmount return () => { stepCompletionTimersRef.current.forEach(timer => clearTimeout(timer)); stepCompletionTimersRef.current.clear(); }; }, [isOpen, currentPhase, stepLogsHash, steps, status, visuallyCompletedCount]); // Added status and count to detect completion // Don't auto-close - user must click close button // 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={false} >
{/* Header */}

{(() => { const funcName = (functionId || title || '').toLowerCase(); if (funcName.includes('cluster')) { // Try to extract keyword count from INIT step const initStepLog = stepLogs.find(log => log.stepName === 'INIT'); if (initStepLog?.message) { const message = initStepLog.message; // Try to extract from "Validating keyword1, keyword2, keyword3 and 5 more keywords" const countMatch = message.match(/(\d+)\s+more keyword/i); if (countMatch) { const moreCount = parseInt(countMatch[1], 10); const shownCount = 3; // Typically shows 3 keywords const totalCount = shownCount + moreCount; return `Mapping ${totalCount} Keywords into Keyword Clusters`; } // Try to extract from "Validating X keywords" const simpleMatch = message.match(/(\d+)\s+keyword/i); if (simpleMatch) { return `Mapping ${simpleMatch[1]} Keywords into Keyword Clusters`; } } // Try to find keyword count in any step log for (const log of stepLogs) { const keywordCountMatch = log.message?.match(/(\d+)\s+keyword/i); if (keywordCountMatch) { return `Mapping ${keywordCountMatch[1]} Keywords into Keyword Clusters`; } } } else if (funcName.includes('idea')) { // For idea generation, use fixed heading return 'Generating Content Ideas & Outline'; } else if (funcName.includes('image') && funcName.includes('from')) { // For image generation from prompts return 'Generate Images'; } else if (funcName.includes('image') && (funcName.includes('prompt') || funcName.includes('extract'))) { // For image prompt generation return 'Smart Image Prompts'; } else if (funcName.includes('image')) { // Fallback for other image functions return 'Generate Images'; } return title; })()}

{/* Subtitle for image functions */} {(() => { const funcName = (functionId || title || '').toLowerCase(); if (funcName.includes('image') && funcName.includes('from')) { // Image generation from prompts return (

Generating images from prompts using AI

); } else if (funcName.includes('image') && (funcName.includes('prompt') || funcName.includes('extract'))) { // Image prompt generation return (

Powered by Igny8 Visual Intelligence

); } return null; })()} {!showSuccess && status !== 'completed' && (

{(() => { const funcName = (functionId || title || '').toLowerCase(); // For image generation, show a more specific message if (funcName.includes('image') && funcName.includes('from')) { // Get current step message from step logs const currentStepLog = stepLogs.find(log => log.stepName === currentPhase); if (currentStepLog?.message) { return currentStepLog.message; } // Fallback to step label const currentStep = steps.find(s => s.phase === currentPhase); return currentStep?.label || 'Generating images...'; } // For other functions, use the message prop return message; })()}

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

Processing...

)} {/* Spinner below heading - show when processing OR when completed but steps not all visually done */} {(status === 'processing' || (status === 'completed' && !allStepsVisuallyCompleted)) && (
)}
{/* 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 - only checkmark for completed, gray circle for pending */}
{item.completed ? ( ) : (
)}
{/* Step Text */} {item.label}
))}
{/* Footer */} {showSuccess && onClose && (
)} {onCancel && !showSuccess && status !== 'error' && (
)} {status === 'error' && onClose && (
)}
); }