|
|
"use client"; |
|
|
|
|
|
import React, { useState, useEffect } from "react"; |
|
|
import { Card, CardContent } from "@/components/ui/Card"; |
|
|
import { ProgressBar } from "@/components/ui/ProgressBar"; |
|
|
import { |
|
|
Sparkles, |
|
|
CheckCircle2, |
|
|
Zap, |
|
|
Package, |
|
|
X |
|
|
} from "lucide-react"; |
|
|
import { useGenerationStore } from "@/store/generationStore"; |
|
|
|
|
|
interface BatchProgressProps { |
|
|
progress: number; |
|
|
currentIndex?: number; |
|
|
totalCount?: number; |
|
|
generationStartTime?: number | null; |
|
|
message?: string; |
|
|
} |
|
|
|
|
|
const BATCH_MESSAGES = [ |
|
|
"Generating your ad set...", |
|
|
"Crafting compelling visuals...", |
|
|
"Building your ad collection...", |
|
|
"Almost there! Finalizing your ads...", |
|
|
"Perfecting each creative...", |
|
|
"Lining up scroll-stopping ads...", |
|
|
"Great things take time - we're crafting perfection!", |
|
|
] as const; |
|
|
|
|
|
const ENGAGING_MESSAGES = [ |
|
|
"Batch generation may take a while, but great things are worth waiting for!", |
|
|
"Almost there! We're putting the finishing touches on your batch.", |
|
|
"Hang tight! We're creating something amazing for you.", |
|
|
"This is taking a bit longer, but we're ensuring top quality!", |
|
|
"Just a few more moments... Your batch is almost ready!", |
|
|
"We're working hard to make this perfect for you!", |
|
|
"Great things take time - we're crafting your batch!", |
|
|
"Almost done! We're making sure everything is just right.", |
|
|
] as const; |
|
|
|
|
|
export const BatchProgressComponent: React.FC<BatchProgressProps> = ({ |
|
|
progress, |
|
|
currentIndex = 0, |
|
|
totalCount = 0, |
|
|
generationStartTime, |
|
|
message, |
|
|
}) => { |
|
|
const [currentMessageIndex, setCurrentMessageIndex] = useState(0); |
|
|
const [elapsedTime, setElapsedTime] = useState(0); |
|
|
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState<number | null>(null); |
|
|
const reset = useGenerationStore((state) => state.reset); |
|
|
|
|
|
const clampedProgress = Math.min(100, Math.max(0, progress)); |
|
|
const isComplete = clampedProgress >= 100; |
|
|
const isStuckAtHighProgress = clampedProgress >= 85 && !isComplete; |
|
|
const currentAdNumber = currentIndex + 1; |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (generationStartTime && !isComplete) { |
|
|
const interval = setInterval(() => { |
|
|
const elapsed = Math.floor((Date.now() - generationStartTime) / 1000); |
|
|
setElapsedTime(elapsed); |
|
|
|
|
|
|
|
|
if (clampedProgress > 5 && clampedProgress < 100) { |
|
|
const rate = clampedProgress / elapsed; |
|
|
const remaining = (100 - clampedProgress) / rate; |
|
|
setEstimatedTimeRemaining(Math.max(0, Math.ceil(remaining))); |
|
|
} |
|
|
}, 1000); |
|
|
return () => clearInterval(interval); |
|
|
} |
|
|
}, [generationStartTime, isComplete, clampedProgress]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (isStuckAtHighProgress) { |
|
|
const interval = setInterval(() => { |
|
|
setCurrentMessageIndex((prev) => (prev + 1) % ENGAGING_MESSAGES.length); |
|
|
}, 5000); |
|
|
return () => clearInterval(interval); |
|
|
} |
|
|
}, [isStuckAtHighProgress]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (!isComplete && !isStuckAtHighProgress) { |
|
|
const interval = setInterval(() => { |
|
|
setCurrentMessageIndex((prev) => (prev + 1) % BATCH_MESSAGES.length); |
|
|
}, 4000); |
|
|
return () => clearInterval(interval); |
|
|
} |
|
|
}, [isComplete, isStuckAtHighProgress]); |
|
|
|
|
|
|
|
|
const getCurrentMessage = () => { |
|
|
if (message) return message; |
|
|
if (isStuckAtHighProgress) { |
|
|
return ENGAGING_MESSAGES[currentMessageIndex]; |
|
|
} |
|
|
if (totalCount > 0 && currentIndex >= 0) { |
|
|
return `Generating ad ${currentAdNumber} of ${totalCount}...`; |
|
|
} |
|
|
return BATCH_MESSAGES[currentMessageIndex]; |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<div className="sticky top-20 z-30 mb-6 animate-scale-in"> |
|
|
<Card variant="glass" className="overflow-hidden shadow-xl border-2 border-blue-200/50 backdrop-blur-xl"> |
|
|
<CardContent className="pt-6"> |
|
|
<div className="space-y-6"> |
|
|
{/* Header with animated icon */} |
|
|
<div className="flex items-center justify-between"> |
|
|
<div className="flex items-center space-x-4"> |
|
|
{isComplete ? ( |
|
|
<div className="relative"> |
|
|
<div className="absolute inset-0 bg-green-500 rounded-full animate-ping opacity-75"></div> |
|
|
<CheckCircle2 className="h-8 w-8 text-green-500 relative z-10" /> |
|
|
</div> |
|
|
) : ( |
|
|
<div className="relative"> |
|
|
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-full animate-ping opacity-20"></div> |
|
|
<div className="relative bg-gradient-to-r from-blue-500 to-cyan-500 rounded-full p-2"> |
|
|
<Package className="h-5 w-5 text-white animate-pulse" /> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
<div> |
|
|
<h3 className="text-lg font-bold bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent"> |
|
|
{isComplete ? "Batch Generation Complete!" : "Generating Batch Ads"} |
|
|
</h3> |
|
|
<p className="text-sm text-gray-600 mt-0.5 transition-all duration-500"> |
|
|
{isComplete ? "All ads are ready!" : getCurrentMessage()} |
|
|
</p> |
|
|
{elapsedTime > 30 && !isComplete && ( |
|
|
<p className="text-xs text-gray-500 mt-1"> |
|
|
{Math.floor(elapsedTime / 60)}m {elapsedTime % 60}s elapsed |
|
|
</p> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
{estimatedTimeRemaining !== null && !isComplete && ( |
|
|
<div className="text-right"> |
|
|
<div className="flex items-center space-x-1 text-sm font-semibold text-gray-700"> |
|
|
<Zap className="h-4 w-4 text-yellow-500 animate-pulse" /> |
|
|
<span>~{estimatedTimeRemaining}s</span> |
|
|
</div> |
|
|
<p className="text-xs text-gray-500">remaining</p> |
|
|
</div> |
|
|
)} |
|
|
{!isComplete && ( |
|
|
<button |
|
|
onClick={() => reset()} |
|
|
className="p-2 hover:bg-red-50 text-gray-400 hover:text-red-500 rounded-full transition-colors group" |
|
|
title="Cancel Generation" |
|
|
> |
|
|
<X className="h-5 w-5 group-hover:rotate-90 transition-transform duration-300" /> |
|
|
</button> |
|
|
)} |
|
|
</div> |
|
|
|
|
|
{/* Batch Stats */} |
|
|
{totalCount > 0 && ( |
|
|
<div className="grid grid-cols-3 gap-4"> |
|
|
<div className="bg-gradient-to-br from-blue-50 to-cyan-50 rounded-xl p-4 border border-blue-200"> |
|
|
<div className="flex items-center space-x-2 mb-1"> |
|
|
<Package className="h-4 w-4 text-blue-600" /> |
|
|
<p className="text-xs font-semibold text-gray-600">Total Ads</p> |
|
|
</div> |
|
|
<p className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent"> |
|
|
{totalCount} |
|
|
</p> |
|
|
</div> |
|
|
<div className="bg-gradient-to-br from-pink-50 to-purple-50 rounded-xl p-4 border border-pink-200"> |
|
|
<div className="flex items-center space-x-2 mb-1"> |
|
|
<Sparkles className="h-4 w-4 text-pink-600" /> |
|
|
<p className="text-xs font-semibold text-gray-600">Current</p> |
|
|
</div> |
|
|
<p className="text-2xl font-bold bg-gradient-to-r from-pink-600 to-purple-600 bg-clip-text text-transparent"> |
|
|
{currentAdNumber}/{totalCount} |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{/* Progress Bar */} |
|
|
<div className="space-y-2"> |
|
|
<div className="flex justify-between items-center"> |
|
|
<span className="text-sm font-semibold text-gray-700">Overall Progress</span> |
|
|
<span className="text-sm font-bold bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent"> |
|
|
{Math.round(clampedProgress)}% |
|
|
</span> |
|
|
</div> |
|
|
<ProgressBar |
|
|
progress={clampedProgress} |
|
|
showPercentage={false} |
|
|
/> |
|
|
{totalCount > 0 && currentIndex >= 0 && ( |
|
|
<p className="text-xs text-gray-500 text-center mt-2"> |
|
|
{currentAdNumber} of {totalCount} ads completed |
|
|
</p> |
|
|
)} |
|
|
</div> |
|
|
|
|
|
{/* Success State */} |
|
|
{isComplete && ( |
|
|
<div className="mt-4 p-4 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-200 rounded-xl animate-scale-in"> |
|
|
<div className="flex items-center space-x-3"> |
|
|
<CheckCircle2 className="h-6 w-6 text-green-600 flex-shrink-0" /> |
|
|
<div> |
|
|
<p className="text-sm font-semibold text-green-900"> |
|
|
Batch generation completed successfully! |
|
|
</p> |
|
|
<p className="text-xs text-green-700 mt-0.5"> |
|
|
{totalCount > 0 ? `${totalCount} ads are ready!` : "All ads are ready to use"} |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
</CardContent> |
|
|
</Card> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|