sushilideaclan01's picture
feat: add cancel button to generation progress and fix stuck loading state
3c7508e
"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;
// Engaging messages for when stuck at high progress
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,
};
// Use nullish coalescing (??) instead of || to properly handle 0 as a valid progress value
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;
// Calculate elapsed time
useEffect(() => {
if (generationStartTime && !isComplete && !isError) {
const interval = setInterval(() => {
setElapsedTime(Math.floor((Date.now() - generationStartTime) / 1000));
}, 1000);
return () => clearInterval(interval);
}
}, [generationStartTime, isComplete, isError]);
// Rotate messages when stuck at high progress
useEffect(() => {
if (isStuckAtHighProgress) {
const interval = setInterval(() => {
setCurrentMessageIndex((prev) => (prev + 1) % ENGAGING_MESSAGES.length);
}, 5000); // Change message every 5 seconds
return () => clearInterval(interval);
}
}, [isStuckAtHighProgress]);
// Rotate step messages periodically
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); // Change message every 4 seconds
return () => clearInterval(interval);
}
}
}, [progress.step, isComplete, isError, isStuckAtHighProgress]);
// Get message for current step
const getStepMessage = () => {
if (progress.message) return progress.message;
// If stuck at high progress, show engaging messages
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>
);
};