|
|
"use client"; |
|
|
|
|
|
import React, { useState, useEffect } from "react"; |
|
|
import { Card, CardContent } from "@/components/ui/Card"; |
|
|
import { ProgressBar } from "@/components/ui/ProgressBar"; |
|
|
import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; |
|
|
import { |
|
|
Sparkles, |
|
|
Image as ImageIcon, |
|
|
Database, |
|
|
CheckCircle2, |
|
|
AlertCircle, |
|
|
Wand2, |
|
|
Zap, |
|
|
X |
|
|
} from "lucide-react"; |
|
|
import type { GenerationProgress } from "@/types"; |
|
|
import { useGenerationStore } from "@/store/generationStore"; |
|
|
|
|
|
interface GenerationProgressProps { |
|
|
progress: GenerationProgress; |
|
|
generationStartTime?: number | null; |
|
|
} |
|
|
|
|
|
const STEPS = [ |
|
|
{ |
|
|
key: "copy", label: "Crafting Copy", icon: Sparkles, color: "from-blue-500 to-cyan-500", messages: [ |
|
|
"Brainstorming compelling headlines...", |
|
|
"Writing persuasive ad copy...", |
|
|
"Polishing the perfect message...", |
|
|
"Adding psychological triggers...", |
|
|
] |
|
|
}, |
|
|
{ |
|
|
key: "image", label: "Generating Images", icon: ImageIcon, color: "from-cyan-500 to-pink-500", messages: [ |
|
|
"Creating stunning visuals...", |
|
|
"Bringing your vision to life...", |
|
|
"Rendering high-quality images...", |
|
|
"Adding creative flair...", |
|
|
"Generation may take a while...", |
|
|
"Almost there!", |
|
|
"Working on the perfect image...", |
|
|
"This is worth the wait...", |
|
|
"Crafting something amazing...", |
|
|
"Just a few more moments...", |
|
|
] |
|
|
}, |
|
|
{ |
|
|
key: "saving", label: "Saving", icon: Database, color: "from-pink-500 to-purple-500", messages: [ |
|
|
"Storing your creative...", |
|
|
"Securing your masterpiece...", |
|
|
"Almost done...", |
|
|
"Finalizing everything...", |
|
|
] |
|
|
}, |
|
|
] as const; |
|
|
|
|
|
|
|
|
const ENGAGING_MESSAGES = [ |
|
|
"Generation may take a while, but great things are worth waiting for!", |
|
|
"Almost there! We're putting the finishing touches on your ad.", |
|
|
"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 ad is almost ready!", |
|
|
"We're working hard to make this perfect for you!", |
|
|
"Great things take time - we're crafting your masterpiece!", |
|
|
"Almost done! We're making sure everything is just right.", |
|
|
] as const; |
|
|
|
|
|
export const GenerationProgressComponent: React.FC<GenerationProgressProps> = ({ |
|
|
progress, |
|
|
generationStartTime, |
|
|
}) => { |
|
|
const [currentMessageIndex, setCurrentMessageIndex] = useState(0); |
|
|
const [elapsedTime, setElapsedTime] = useState(0); |
|
|
const reset = useGenerationStore((state) => state.reset); |
|
|
|
|
|
const stepProgress = { |
|
|
idle: 0, |
|
|
copy: 33, |
|
|
image: 66, |
|
|
saving: 90, |
|
|
complete: 100, |
|
|
error: 0, |
|
|
}; |
|
|
|
|
|
|
|
|
const currentProgress = progress.progress ?? stepProgress[progress.step]; |
|
|
const currentStepIndex = STEPS.findIndex(s => s.key === progress.step); |
|
|
const isComplete = progress.step === "complete"; |
|
|
const isError = progress.step === "error"; |
|
|
const isStuckAtHighProgress = currentProgress >= 85 && !isComplete && !isError; |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (generationStartTime && !isComplete && !isError) { |
|
|
const interval = setInterval(() => { |
|
|
setElapsedTime(Math.floor((Date.now() - generationStartTime) / 1000)); |
|
|
}, 1000); |
|
|
return () => clearInterval(interval); |
|
|
} |
|
|
}, [generationStartTime, isComplete, isError]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (isStuckAtHighProgress) { |
|
|
const interval = setInterval(() => { |
|
|
setCurrentMessageIndex((prev) => (prev + 1) % ENGAGING_MESSAGES.length); |
|
|
}, 5000); |
|
|
return () => clearInterval(interval); |
|
|
} |
|
|
}, [isStuckAtHighProgress]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (!isComplete && !isError && !isStuckAtHighProgress) { |
|
|
const step = STEPS.find(s => s.key === progress.step); |
|
|
if (step && step.messages.length > 1) { |
|
|
const interval = setInterval(() => { |
|
|
setCurrentMessageIndex((prev) => (prev + 1) % step.messages.length); |
|
|
}, 4000); |
|
|
return () => clearInterval(interval); |
|
|
} |
|
|
} |
|
|
}, [progress.step, isComplete, isError, isStuckAtHighProgress]); |
|
|
|
|
|
|
|
|
const getStepMessage = () => { |
|
|
if (progress.message) return progress.message; |
|
|
|
|
|
|
|
|
if (isStuckAtHighProgress) { |
|
|
return ENGAGING_MESSAGES[currentMessageIndex]; |
|
|
} |
|
|
|
|
|
const step = STEPS.find(s => s.key === progress.step); |
|
|
if (step && step.messages.length > 0) { |
|
|
return step.messages[currentMessageIndex % step.messages.length]; |
|
|
} |
|
|
return "Processing..."; |
|
|
}; |
|
|
|
|
|
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> |
|
|
) : isError ? ( |
|
|
<AlertCircle className="h-8 w-8 text-red-500 animate-pulse" /> |
|
|
) : ( |
|
|
<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"> |
|
|
<Wand2 className="h-5 w-5 text-white animate-spin-slow" /> |
|
|
</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 ? "Generation Complete!" : isError ? "Generation Failed" : "Creating Your Ad"} |
|
|
</h3> |
|
|
<p className="text-sm text-gray-600 mt-0.5 transition-all duration-500"> |
|
|
{isComplete ? "Your ad is ready!" : isError ? "Something went wrong" : getStepMessage()} |
|
|
</p> |
|
|
{elapsedTime > 30 && !isComplete && !isError && ( |
|
|
<p className="text-xs text-gray-500 mt-1"> |
|
|
{Math.floor(elapsedTime / 60)}m {elapsedTime % 60}s elapsed |
|
|
</p> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
{progress.estimatedTimeRemaining && !isComplete && !isError && ( |
|
|
<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>~{Math.ceil(progress.estimatedTimeRemaining)}s</span> |
|
|
</div> |
|
|
<p className="text-xs text-gray-500">remaining</p> |
|
|
</div> |
|
|
)} |
|
|
{!isComplete && !isError && ( |
|
|
<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> |
|
|
|
|
|
{/* Step Indicators */} |
|
|
{!isComplete && !isError && ( |
|
|
<div className="flex items-center justify-between relative"> |
|
|
{/* Progress line */} |
|
|
<div className="absolute top-5 left-0 right-0 h-0.5 bg-gray-200 -z-10"> |
|
|
<div |
|
|
className="h-full bg-gradient-to-r from-blue-500 via-cyan-500 to-pink-500 transition-all duration-500 ease-out" |
|
|
style={{ width: `${currentProgress}%` }} |
|
|
/> |
|
|
</div> |
|
|
|
|
|
{STEPS.map((step, index) => { |
|
|
const StepIcon = step.icon; |
|
|
const isActive = progress.step === step.key; |
|
|
const isCompleted = currentStepIndex > index; |
|
|
const isUpcoming = currentStepIndex < index; |
|
|
|
|
|
return ( |
|
|
<div key={step.key} className="flex flex-col items-center flex-1"> |
|
|
<div className={`relative w-10 h-10 rounded-full flex items-center justify-center transition-all duration-300 ${isActive |
|
|
? `bg-gradient-to-r ${step.color} shadow-lg scale-110 animate-pulse` |
|
|
: isCompleted |
|
|
? "bg-gradient-to-r from-green-500 to-emerald-500 shadow-md" |
|
|
: "bg-gray-200" |
|
|
}`}> |
|
|
{isActive ? ( |
|
|
<div className="text-white"> |
|
|
<LoadingSpinner size="sm" /> |
|
|
</div> |
|
|
) : isCompleted ? ( |
|
|
<CheckCircle2 className="h-5 w-5 text-white" /> |
|
|
) : ( |
|
|
<StepIcon className={`h-5 w-5 ${isUpcoming ? "text-gray-400" : "text-white"}`} /> |
|
|
)} |
|
|
{isActive && ( |
|
|
<div className={`absolute inset-0 rounded-full bg-gradient-to-r ${step.color} animate-ping opacity-75`}></div> |
|
|
)} |
|
|
</div> |
|
|
<p className={`text-xs font-medium mt-2 text-center ${isActive |
|
|
? "text-gray-900 font-bold" |
|
|
: isCompleted |
|
|
? "text-green-600" |
|
|
: "text-gray-400" |
|
|
}`}> |
|
|
{step.label} |
|
|
</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(currentProgress)}% |
|
|
</span> |
|
|
</div> |
|
|
<ProgressBar |
|
|
progress={currentProgress} |
|
|
showPercentage={false} |
|
|
/> |
|
|
</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"> |
|
|
Ad generated successfully! |
|
|
</p> |
|
|
<p className="text-xs text-green-700 mt-0.5"> |
|
|
Your creative is ready to use |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{/* Error State */} |
|
|
{isError && ( |
|
|
<div className="mt-4 p-4 bg-gradient-to-r from-red-50 to-pink-50 border-2 border-red-200 rounded-xl animate-scale-in"> |
|
|
<div className="flex items-center space-x-3"> |
|
|
<AlertCircle className="h-6 w-6 text-red-600 flex-shrink-0" /> |
|
|
<div> |
|
|
<p className="text-sm font-semibold text-red-900"> |
|
|
Generation failed |
|
|
</p> |
|
|
<p className="text-xs text-red-700 mt-0.5"> |
|
|
{progress.message || "An error occurred. Please try again."} |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
</CardContent> |
|
|
</Card> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|