import React, { useState, useEffect, useRef } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Progress } from '@/components/ui/progress'; import { Button } from '@/components/ui/button'; import { CheckCircle, AlertCircle, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; type AnimationPhase = 'progressing' | 'waiting' | 'completing' | 'completed' | 'hiding'; interface ProgressModalProps { isOpen: boolean; onClose: () => void; // State from useCancellableGeneration hook isActive: boolean; isComplete: boolean; hasError: boolean; isCancelling: boolean; taskId: string | null; // Customization title: string; description: string; icon?: React.ReactNode; errorMessage?: string; // Callbacks onCancel?: () => void; onComplete?: () => void; // Optional operation-specific details currentItem?: string; completedCount?: number; totalCount?: number; } export const ProgressModal: React.FC = ({ isOpen, onClose, isActive, isComplete, hasError, isCancelling, taskId, title, description, icon, errorMessage, onCancel, onComplete, currentItem, completedCount, totalCount }) => { const [progress, setProgress] = useState(0); const [phase, setPhase] = useState('progressing'); const [internalComplete, setInternalComplete] = useState(false); const timerRef = useRef(null); const phaseTimerRef = useRef(null); const animationStartedRef = useRef(false); const progressRef = useRef(0); const stateRef = useRef({ isComplete, hasError, isCancelling }); stateRef.current = { isComplete, hasError, isCancelling }; // Clear all timers helper const clearAllTimers = () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } if (phaseTimerRef.current) { clearTimeout(phaseTimerRef.current); phaseTimerRef.current = null; } }; // Reset state when modal opens useEffect(() => { if (isOpen && isActive) { clearAllTimers(); setProgress(0); progressRef.current = 0; setPhase('progressing'); setInternalComplete(false); animationStartedRef.current = false; } }, [isOpen, isActive]); // Animate to completion from current progress const animateToCompletion = (fromProgress: number) => { clearAllTimers(); setPhase('completing'); // Animate quickly to 100% over 500ms const remainingProgress = 100 - fromProgress; const completionInterval = 50; const totalSteps = 500 / completionInterval; const progressPerStep = remainingProgress / totalSteps; let step = 0; timerRef.current = setInterval(() => { step++; const newProgress = fromProgress + (progressPerStep * step); if (newProgress >= 100 || step >= totalSteps) { setProgress(100); setPhase('completed'); setInternalComplete(true); clearAllTimers(); // Auto-dismiss after 2.5 seconds phaseTimerRef.current = setTimeout(() => { setPhase('hiding'); setTimeout(() => { onComplete?.(); onClose(); }, 300); }, 2500); } else { setProgress(newProgress); } }, completionInterval); }; // Main effect to handle progress animation useEffect(() => { // Only start animation once when conditions are met if (isOpen && isActive && phase === 'progressing' && !animationStartedRef.current) { animationStartedRef.current = true; // Start progress animation to 90% over 54 seconds const progressPerUpdate = 90 / 540; timerRef.current = setInterval(() => { // Check if we should stop if (stateRef.current.isComplete || stateRef.current.hasError || stateRef.current.isCancelling) { clearAllTimers(); return; } progressRef.current += progressPerUpdate; if (progressRef.current >= 90) { setProgress(90); progressRef.current = 90; setPhase('waiting'); clearAllTimers(); } else { setProgress(progressRef.current); } }, 100); } // Cleanup only on unmount or when modal closes return () => { if (!isOpen) { clearAllTimers(); animationStartedRef.current = false; } }; }, [isOpen, isActive, phase]); // Handle completion useEffect(() => { if (isComplete && !internalComplete && (phase === 'progressing' || phase === 'waiting')) { animateToCompletion(progressRef.current); } }, [isComplete, phase, internalComplete]); // Handle cancellation useEffect(() => { if (isCancelling && (phase === 'progressing' || phase === 'waiting')) { clearAllTimers(); } }, [isCancelling, phase]); // Handle cancellation completed (modal closes) useEffect(() => { if (!isCancelling && !isActive && isOpen && !isComplete && !hasError) { // Cancellation completed - close after brief delay setTimeout(() => { onClose(); }, 1000); } }, [isCancelling, isActive, isOpen, isComplete, hasError, onClose]); // Cleanup on unmount useEffect(() => { return () => { clearAllTimers(); }; }, []); const handleCancel = () => { if (onCancel && !isCancelling && taskId && !taskId.startsWith('temp-')) { onCancel(); } }; // Prevent closing during operation const handleOpenChange = (open: boolean) => { if (!open && (internalComplete || hasError)) { onClose(); } // Don't allow closing during active operation }; const getStatusLabel = () => { if (isCancelling) return 'Cancelling...'; if (hasError) return 'Operation failed'; if (internalComplete) return 'Completed successfully!'; if (phase === 'waiting') return 'Finalizing...'; if (currentItem) return currentItem; return 'Processing...'; }; const getIcon = () => { if (hasError) return ; if (internalComplete) return ; if (icon) return icon; return ; }; const canCancel = !internalComplete && !hasError && taskId && !taskId.startsWith('temp-') && !isCancelling; return ( { // Prevent closing by clicking outside during operation if (!internalComplete && !hasError) { e.preventDefault(); } }} onEscapeKeyDown={(e) => { // Prevent closing with Escape during operation if (!internalComplete && !hasError) { e.preventDefault(); } }} > {getIcon()} {title} {description}
{/* Progress Bar */}
div]:bg-green-500" )} />
{Math.round(progress)}% {totalCount !== undefined && completedCount !== undefined && ( {completedCount}/{totalCount} )}
{/* Current Status */}
{getStatusLabel()}
{/* Error Message */} {hasError && errorMessage && (
{errorMessage}
)} {/* Success Message */} {internalComplete && !hasError && (
Operation completed successfully!
)} {/* Action Buttons */}
{/* Cancel Button */} {canCancel && ( )} {isCancelling && (
Cancelling operation...
)}
{/* Close Button - only show when complete or error */} {(internalComplete || hasError) && ( )}
); }; export default ProgressModal;