Files
igny8/frontend/src/components/common/ProgressModal.tsx
Desktop d1d2d768e5 Revert "dup remocal"
This reverts commit cfddf3d8fd.
2025-11-12 04:05:17 +05:00

822 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 `Clustering complete\n${keywordCount} keyword${keywordCount !== '1' ? 's' : ''} mapped and grouped into ${clusterCount} cluster${clusterCount !== '1' ? 's' : ''}`;
} else if (clusterCount) {
return `Clustering complete\n${clusterCount} cluster${clusterCount !== '1' ? 's' : ''} created`;
} else if (keywordCount) {
return `Clustering complete\n${keywordCount} keyword${keywordCount !== '1' ? 's' : ''} mapped and grouped into clusters`;
}
return 'Clustering complete\nKeywords mapped and grouped into clusters';
}
if (funcName.includes('idea')) {
const ideaCount = extractCount(/(\d+)\s+idea/i, stepLogs || []);
if (ideaCount) {
return `Content ideas & outlines created successfully`;
}
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 || []);
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.';
}
// 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 successfully`;
}
return 'Images generated successfully';
} 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 `Featured Image and ${inArticleCount} Inarticle Image Prompts ready for image generation`;
} else {
return `Featured Image Prompt ready for image generation`;
}
}
}
// 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 `Featured Image and ${inArticleCount} Inarticle Image Prompts ready for image generation`;
} else {
return `Featured Image Prompt ready for image generation`;
}
}
}
// 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 `Featured Image and ${inArticleCount} Inarticle Image Prompts ready for image generation`;
} else {
return `Featured Image Prompt ready for image generation`;
}
}
// Default message
return 'Featured Image and X Inarticle Image Prompts ready for image generation';
}
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 with Igny8 Semantic SEO Model' },
{ phase: 'PARSE', label: 'Organizing clusters' },
{ phase: 'SAVE', label: 'Saving clusters' },
];
}
if (funcName.includes('idea')) {
return [
{ phase: 'INIT', label: 'Verifying cluster integrity' },
{ phase: 'PREP', label: 'Loading cluster keywords' },
{ phase: 'AI_CALL', label: 'Generating ideas with Igny8 Semantic AI' },
{ phase: 'PARSE', label: 'High-opportunity ideas generated' },
{ phase: 'SAVE', label: 'Content Outline for Ideas generated' },
];
}
if (funcName.includes('content')) {
return [
{ phase: 'INIT', label: 'Validating task' },
{ phase: 'PREP', label: 'Preparing content idea' },
{ phase: 'AI_CALL', label: 'Writing article with Igny8 Semantic AI' },
{ phase: 'PARSE', label: 'Formatting content' },
{ phase: 'SAVE', label: 'Saving article' },
];
}
// Check for image generation from prompts FIRST (more specific)
if (funcName.includes('image') && funcName.includes('from')) {
// Image generation from prompts
return [
{ phase: 'INIT', label: 'Validating image prompts' },
{ phase: 'PREP', label: 'Preparing image generation queue' },
{ phase: 'AI_CALL', label: 'Generating images with AI' },
{ phase: 'PARSE', label: 'Processing image URLs' },
{ phase: 'SAVE', label: 'Saving image URLs' },
];
} else if (funcName.includes('image') && (funcName.includes('prompt') || funcName.includes('extract'))) {
// Image prompt generation
return [
{ phase: 'INIT', label: 'Checking content and image slots' },
{ phase: 'PREP', label: 'Mapping Content for X Image Prompts' },
{ phase: 'AI_CALL', label: 'Writing Featured Image Prompts' },
{ phase: 'PARSE', label: 'Writing X Inarticle Image Prompts' },
{ phase: 'SAVE', label: 'Assigning Prompts to Dedicated 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<Set<string>>(new Set());
const stepCompletionTimersRef = useRef<Map<string, NodeJS.Timeout>>(new Map());
const visuallyCompletedStepsRef = useRef<Set<string>>(new Set());
const lastProcessedStepLogsHashRef = useRef<string>('');
const lastProcessedPhaseRef = useRef<string>('');
const lastVisuallyCompletedCountRef = useRef<number>(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] : '';
};
if (funcName.includes('cluster')) {
if (stepPhase === 'INIT') {
// For INIT: Try to extract keyword count from message or stepLogs
// Backend message might include keyword names (e.g., "Validating keyword1, keyword2, keyword3 and 5 more keywords")
// Or we need to extract the total count
if (message && message !== defaultLabel && message.includes('Validating')) {
// Try to extract total count from message
const countMatch = message.match(/(\d+)\s+more keyword/i);
if (countMatch) {
const moreCount = parseInt(countMatch[1], 10);
// Count keywords before "and X more" - typically 3
const shownCount = 3;
const totalCount = shownCount + moreCount;
return `Validating ${totalCount} keyword${totalCount !== 1 ? 's' : ''}`;
}
// Try to find total keyword count in any step log
for (const log of allStepLogs) {
const keywordCountMatch = log.message?.match(/(\d+)\s+keyword/i);
if (keywordCountMatch) {
const totalCount = parseInt(keywordCountMatch[1], 10);
return `Validating ${totalCount} keyword${totalCount !== 1 ? 's' : ''}`;
}
}
// If message has keyword names but no count, return as-is
return message;
}
// Fallback: try to extract count from stepLogs
for (const log of allStepLogs) {
const keywordCountMatch = log.message?.match(/(\d+)\s+keyword/i);
if (keywordCountMatch) {
const totalCount = parseInt(keywordCountMatch[1], 10);
return `Validating ${totalCount} keyword${totalCount !== 1 ? 's' : ''}`;
}
}
// Final fallback: use default label
return defaultLabel;
} 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 "Generating clusters with Igny8 Semantic SEO Model"
return 'Generating clusters with Igny8 Semantic SEO Model';
} 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 === 'INIT') {
// For INIT: Show "Verifying cluster integrity"
return 'Verifying cluster integrity';
} else if (stepPhase === 'PREP') {
// For PREP: Show "Loading cluster keywords"
return 'Loading cluster keywords';
} else if (stepPhase === 'AI_CALL') {
// For AI_CALL: Show "Generating ideas with Igny8 Semantic AI"
return 'Generating ideas with Igny8 Semantic AI';
} else if (stepPhase === 'PARSE') {
// For PARSE: Show "X high-opportunity ideas generated"
const ideaCount = extractCount(/(\d+)\s+idea/i);
if (ideaCount) {
return `${ideaCount} high-opportunity idea${ideaCount !== '1' ? 's' : ''} generated`;
}
// 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 `${count[1]} high-opportunity idea${count[1] !== '1' ? 's' : ''} generated`;
}
}
return message;
} else if (stepPhase === 'SAVE') {
// For SAVE: Show "Content Outline for Ideas generated"
return 'Content Outline for Ideas generated';
}
} else if (funcName.includes('content')) {
if (stepPhase === 'AI_CALL') {
// For AI_CALL: Show "Writing article with Igny8 Semantic AI"
return 'Writing article with Igny8 Semantic AI';
} else if (stepPhase === 'PARSE') {
const articleCount = extractCount(/(\d+)\s+article/i);
if (articleCount) {
return `${articleCount} article${articleCount !== '1' ? 's' : ''} created`;
}
}
} else if (funcName.includes('image') && funcName.includes('from')) {
// Image generation from prompts
if (stepPhase === 'PREP') {
// Extract image count from PREP step message
const imageCount = extractCount(/(\d+)\s+image/i);
if (imageCount) {
return `Preparing to generate ${imageCount} image${imageCount !== '1' ? 's' : ''}`;
}
if (stepLog?.message) {
const match = stepLog.message.match(/Preparing to generate (\d+)\s+image/i);
if (match && match[1]) {
return `Preparing to generate ${match[1]} image${match[1] !== '1' ? 's' : ''}`;
}
}
return 'Preparing image generation queue';
} else if (stepPhase === 'AI_CALL') {
// Extract current image number from message
const match = stepLog?.message?.match(/Generating.*image (\d+)/i);
if (match && match[1]) {
return `Generating image ${match[1]} with AI`;
}
return 'Generating images with AI';
} else if (stepPhase === 'PARSE') {
// Extract image count from PARSE step
const imageCount = extractCount(/(\d+)\s+image/i);
if (imageCount) {
return `${imageCount} image${imageCount !== '1' ? 's' : ''} generated successfully`;
}
if (stepLog?.message) {
const match = stepLog.message.match(/(\d+)\s+image.*generated/i);
if (match && match[1]) {
return `${match[1]} image${match[1] !== '1' ? 's' : ''} generated successfully`;
}
}
return 'Processing image URLs';
} else if (stepPhase === 'SAVE') {
// Extract image count from SAVE step
const imageCount = extractCount(/(\d+)\s+image/i);
if (imageCount) {
return `Saving ${imageCount} image${imageCount !== '1' ? 's' : ''}`;
}
if (stepLog?.message) {
const match = stepLog.message.match(/Saved image (\d+)/i);
if (match && match[1]) {
return `Saving image ${match[1]}`;
}
}
return 'Saving image URLs';
}
} else if (funcName.includes('image') && (funcName.includes('prompt') || funcName.includes('extract'))) {
// Image prompt generation
if (stepPhase === 'PREP') {
// Extract total image count from PREP step message
// Look for "Mapping Content for X Image Prompts"
const totalCount = extractCount(/(\d+)\s+Image Prompts/i) || extractCount(/(\d+)\s+image/i);
if (totalCount) {
return `Mapping Content for ${totalCount} Image Prompts`;
}
// Try to extract from step log message
if (stepLog?.message) {
const match = stepLog.message.match(/Mapping Content for (\d+)\s+Image Prompts/i);
if (match && match[1]) {
return `Mapping Content for ${match[1]} Image Prompts`;
}
}
return 'Mapping Content for X Image Prompts';
} else if (stepPhase === 'AI_CALL') {
// For AI_CALL: Show "Writing Featured Image Prompts"
return 'Writing Featured Image Prompts';
} else if (stepPhase === 'PARSE') {
// Extract in-article image count from PARSE step
// Look for "Writing X Inarticle Image Prompts"
const inArticleCount = extractCount(/(\d+)\s+Inarticle/i) || extractCount(/(\d+)\s+In-article/i);
if (inArticleCount) {
return `Writing ${inArticleCount} Inarticle Image Prompts`;
}
// Try to extract from step log message
if (stepLog?.message) {
const match = stepLog.message.match(/Writing (\d+)\s+In[-]article Image Prompts/i);
if (match && match[1]) {
return `Writing ${match[1]} Inarticle Image Prompts`;
}
}
return 'Writing X Inarticle Image Prompts';
} else if (stepPhase === 'SAVE') {
// For SAVE: Extract prompt count from message
const promptCount = extractCount(/(\d+)\s+Prompts/i) || extractCount(/(\d+)\s+prompt/i);
if (promptCount) {
return `Assigning ${promptCount} Prompts to Dedicated Slots`;
}
// Try to extract from step log message
if (stepLog?.message) {
const match = stepLog.message.match(/Assigning (\d+)\s+Prompts/i);
if (match && match[1]) {
return `Assigning ${match[1]} Prompts to Dedicated Slots`;
}
}
return 'Assigning Prompts to Dedicated 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 (
<Modal
isOpen={isOpen}
onClose={onClose || (() => {})}
className="max-w-lg"
showCloseButton={false}
>
<div className="p-6 min-h-[200px]">
{/* Header */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-1 text-center">
{(() => {
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;
})()}
</h3>
{/* Subtitle for image functions */}
{(() => {
const funcName = (functionId || title || '').toLowerCase();
if (funcName.includes('image') && funcName.includes('from')) {
// Image generation from prompts
return (
<p className="text-sm text-gray-500 dark:text-gray-400 text-center mt-1">
Generating images from prompts using AI
</p>
);
} else if (funcName.includes('image') && (funcName.includes('prompt') || funcName.includes('extract'))) {
// Image prompt generation
return (
<p className="text-sm text-gray-500 dark:text-gray-400 text-center mt-1">
Powered by Igny8 Visual Intelligence
</p>
);
}
return null;
})()}
{!showSuccess && status !== 'completed' && (
<p className="text-sm text-gray-600 dark:text-gray-400 text-center">
{(() => {
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;
})()}
</p>
)}
{status === 'completed' && !allStepsVisuallyCompleted && (
<p className="text-sm text-gray-600 dark:text-gray-400 text-center">Processing...</p>
)}
{/* Spinner below heading - show when processing OR when completed but steps not all visually done */}
{(status === 'processing' || (status === 'completed' && !allStepsVisuallyCompleted)) && (
<div className="flex justify-center mt-4">
<svg className="w-8 h-8 text-brand-500 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
</div>
)}
</div>
{/* Success Alert (shown when all steps are visually completed) */}
{showSuccess && (
<div className="mb-6">
{/* Big centered check icon */}
<div className="flex justify-center mb-4">
<div className="w-16 h-16 rounded-full bg-green-600 dark:bg-green-700 flex items-center justify-center">
<svg className="w-10 h-10 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
</div>
{/* Dark success alert box with centered text */}
<div className="p-5 rounded-lg bg-green-600 dark:bg-green-700 border border-green-700 dark:border-green-600">
<div className="text-base font-semibold text-white text-center whitespace-pre-line">
{successMessage}
</div>
</div>
</div>
)}
{/* Checklist-style Progress Steps - Always visible */}
<div className="mb-6 space-y-3">
{checklistItems.map((item, index) => (
<div
key={index}
className={`flex items-center gap-3 p-3 rounded-lg border transition-all ${
item.completed
? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800'
: 'bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700 opacity-60'
}`}
>
{/* Icon - only checkmark for completed, gray circle for pending */}
<div className="flex-shrink-0">
{item.completed ? (
<svg className="w-5 h-5 text-green-600 dark:text-green-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
) : (
<div className="w-5 h-5 rounded-full border-2 border-gray-300 dark:border-gray-600" />
)}
</div>
{/* Step Text */}
<span
className={`flex-1 text-sm font-medium ${
item.completed
? 'text-green-800 dark:text-green-300'
: 'text-gray-500 dark:text-gray-400'
}`}
>
{item.label}
</span>
</div>
))}
</div>
{/* Footer */}
{showSuccess && onClose && (
<div className="flex justify-center mt-6">
<Button
variant="primary"
size="lg"
onClick={onClose}
className="bg-green-600 hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-800 text-white px-8 py-3 text-base font-semibold"
>
Close
</Button>
</div>
)}
{onCancel && !showSuccess && status !== 'error' && (
<div className="flex justify-end gap-3 mt-6">
<Button
variant="secondary"
size="sm"
onClick={onCancel}
disabled={status === 'processing'}
>
Cancel
</Button>
</div>
)}
{status === 'error' && onClose && (
<div className="flex justify-end gap-3 mt-6">
<Button variant="primary" size="sm" onClick={onClose}>
Dismiss
</Button>
</div>
)}
</div>
</Modal>
);
}