full upgrade to dev
This commit is contained in:
240
app/components/admin/AIImageGenerator.tsx
Normal file
240
app/components/admin/AIImageGenerator.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
Sparkles,
|
||||
Loader2,
|
||||
Image as ImageIcon,
|
||||
RefreshCw,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
|
||||
interface AIImageGeneratorProps {
|
||||
projectId: number;
|
||||
projectTitle: string;
|
||||
currentImageUrl?: string | null;
|
||||
onImageGenerated?: (imageUrl: string) => void;
|
||||
}
|
||||
|
||||
export default function AIImageGenerator({
|
||||
projectId,
|
||||
projectTitle,
|
||||
currentImageUrl,
|
||||
onImageGenerated,
|
||||
}: AIImageGeneratorProps) {
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
|
||||
const [message, setMessage] = useState("");
|
||||
const [generatedImageUrl, setGeneratedImageUrl] = useState(
|
||||
currentImageUrl || null,
|
||||
);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
|
||||
const handleGenerate = async (regenerate: boolean = false) => {
|
||||
setIsGenerating(true);
|
||||
setStatus("idle");
|
||||
setMessage("Generating AI image...");
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/n8n/generate-image", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
projectId: projectId,
|
||||
regenerate: regenerate,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
setStatus("success");
|
||||
setMessage(data.message || "Image generated successfully!");
|
||||
setGeneratedImageUrl(data.imageUrl);
|
||||
setShowPreview(true);
|
||||
|
||||
if (onImageGenerated) {
|
||||
onImageGenerated(data.imageUrl);
|
||||
}
|
||||
} else {
|
||||
setStatus("error");
|
||||
setMessage(data.error || data.message || "Failed to generate image");
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus("error");
|
||||
setMessage(
|
||||
error instanceof Error ? error.message : "An unexpected error occurred",
|
||||
);
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-2xl border-2 border-stone-200 p-6 shadow-sm">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 bg-gradient-to-br from-purple-100 to-pink-100 rounded-lg">
|
||||
<Sparkles className="text-purple-600" size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-stone-900">AI Image Generator</h3>
|
||||
<p className="text-sm text-stone-600">
|
||||
Generate cover image for:{" "}
|
||||
<span className="font-semibold">{projectTitle}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current/Generated Image Preview */}
|
||||
<AnimatePresence mode="wait">
|
||||
{(generatedImageUrl || showPreview) && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="mb-4 relative group"
|
||||
>
|
||||
<div className="aspect-[4/3] rounded-xl overflow-hidden border-2 border-stone-200 bg-stone-50">
|
||||
{generatedImageUrl ? (
|
||||
<img
|
||||
src={generatedImageUrl}
|
||||
alt={projectTitle}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<ImageIcon className="text-stone-300" size={48} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{generatedImageUrl && (
|
||||
<div className="absolute top-2 right-2 bg-white/90 backdrop-blur-sm px-3 py-1 rounded-full text-xs font-medium text-stone-700 border border-stone-200">
|
||||
Current Image
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Status Message */}
|
||||
<AnimatePresence mode="wait">
|
||||
{message && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={`mb-4 p-3 rounded-xl border-2 flex items-center gap-2 ${
|
||||
status === "success"
|
||||
? "bg-green-50 border-green-200 text-green-800"
|
||||
: status === "error"
|
||||
? "bg-red-50 border-red-200 text-red-800"
|
||||
: "bg-blue-50 border-blue-200 text-blue-800"
|
||||
}`}
|
||||
>
|
||||
{status === "success" && <CheckCircle size={18} />}
|
||||
{status === "error" && <XCircle size={18} />}
|
||||
{status === "idle" && isGenerating && (
|
||||
<Loader2 size={18} className="animate-spin" />
|
||||
)}
|
||||
<span className="text-sm font-medium">{message}</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => handleGenerate(false)}
|
||||
disabled={isGenerating || (!regenerate && !!generatedImageUrl)}
|
||||
className={`flex-1 py-3 px-4 rounded-xl font-semibold text-white transition-all duration-300 flex items-center justify-center gap-2 ${
|
||||
isGenerating
|
||||
? "bg-stone-400 cursor-not-allowed"
|
||||
: generatedImageUrl
|
||||
? "bg-stone-300 cursor-not-allowed"
|
||||
: "bg-gradient-to-br from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 shadow-lg hover:shadow-xl"
|
||||
}`}
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<Loader2 size={18} className="animate-spin" />
|
||||
Generating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles size={18} />
|
||||
Generate Image
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
|
||||
{generatedImageUrl && (
|
||||
<motion.button
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => handleGenerate(true)}
|
||||
disabled={isGenerating}
|
||||
className={`py-3 px-4 rounded-xl font-semibold transition-all duration-300 flex items-center justify-center gap-2 border-2 ${
|
||||
isGenerating
|
||||
? "bg-stone-100 border-stone-300 text-stone-400 cursor-not-allowed"
|
||||
: "bg-white border-purple-300 text-purple-700 hover:bg-purple-50 hover:border-purple-400"
|
||||
}`}
|
||||
>
|
||||
<RefreshCw size={18} />
|
||||
Regenerate
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="mt-4 p-3 bg-gradient-to-br from-blue-50 to-purple-50 border border-blue-200 rounded-lg">
|
||||
<p className="text-xs text-stone-700 leading-relaxed">
|
||||
<span className="font-semibold">💡 How it works:</span> The AI
|
||||
analyzes your project&aposs title, description, category, and tech
|
||||
stack to create a unique cover image using Stable Diffusion.
|
||||
Generation takes 15-30 seconds.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Advanced Options (Optional) */}
|
||||
<details className="mt-4">
|
||||
<summary className="cursor-pointer text-sm font-semibold text-stone-700 hover:text-stone-900 transition-colors">
|
||||
Advanced Options
|
||||
</summary>
|
||||
<div className="mt-3 space-y-3 pl-4 border-l-2 border-stone-200">
|
||||
<div className="text-xs text-stone-600 space-y-1">
|
||||
<p>
|
||||
<strong>Image Size:</strong> 1024x768 (4:3 aspect ratio)
|
||||
</p>
|
||||
<p>
|
||||
<strong>Quality:</strong> High (30 steps, CFG 7)
|
||||
</p>
|
||||
<p>
|
||||
<strong>Sampler:</strong> DPM++ 2M Karras
|
||||
</p>
|
||||
<p>
|
||||
<strong>Model:</strong> SDXL Base / Category-specific
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() =>
|
||||
window.open("/docs/ai-image-generation/SETUP.md", "_blank")
|
||||
}
|
||||
className="text-xs text-purple-600 hover:text-purple-700 font-medium underline"
|
||||
>
|
||||
View Full Documentation →
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user