refactor: simplify Meta AI video workflow
- Remove Video button from PromptHero (generate images first, video from gallery) - Add Meta AI image-to-video in Gallery component - Redesign lightbox with split panel layout (image left, controls right) - Add handleGenerateMetaVideo() for Meta AI video from gallery - Fix reference buttons (all disabled for Meta AI) - Lightbox now shows: Download, Generate Video, Remix/Edit, Copy Prompt, Delete
This commit is contained in:
parent
bae4c487da
commit
7aaa4c8166
3 changed files with 179 additions and 164 deletions
|
|
@ -37,6 +37,59 @@ export function Gallery() {
|
||||||
setEditModalOpen(true);
|
setEditModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [isGeneratingMetaVideo, setIsGeneratingMetaVideo] = React.useState(false);
|
||||||
|
|
||||||
|
// Handle Meta AI video generation (image-to-video)
|
||||||
|
const handleGenerateMetaVideo = async (img: { data: string; prompt: string }) => {
|
||||||
|
if (!settings.metaCookies) {
|
||||||
|
alert("Please set your Meta AI Cookies in Settings first!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsGeneratingMetaVideo(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("[Gallery] Starting Meta AI image-to-video...");
|
||||||
|
|
||||||
|
const res = await fetch('/api/meta/video', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: img.prompt || "Animate this image with natural movement",
|
||||||
|
cookies: settings.metaCookies,
|
||||||
|
imageBase64: img.data
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
console.log("[Gallery] Meta video response:", data);
|
||||||
|
|
||||||
|
if (data.success && data.videos?.length > 0) {
|
||||||
|
for (const video of data.videos) {
|
||||||
|
addVideo({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
url: video.url,
|
||||||
|
prompt: video.prompt || img.prompt,
|
||||||
|
thumbnail: img.data,
|
||||||
|
createdAt: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
alert('🎬 Video generation complete! Scroll up to see your video.');
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || 'No videos generated');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("[Gallery] Meta video error:", error);
|
||||||
|
let errorMessage = error.message || 'Video generation failed';
|
||||||
|
if (errorMessage.includes('401') || errorMessage.includes('cookies')) {
|
||||||
|
errorMessage = '🔐 Your Meta AI cookies have expired. Please go to Settings and update them.';
|
||||||
|
}
|
||||||
|
alert(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setIsGeneratingMetaVideo(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleGenerateVideo = async (prompt: string) => {
|
const handleGenerateVideo = async (prompt: string) => {
|
||||||
if (!videoSource) return;
|
if (!videoSource) return;
|
||||||
|
|
||||||
|
|
@ -359,93 +412,175 @@ export function Gallery() {
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Lightbox Modal */}
|
{/* Lightbox Modal - Split Panel Design */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{selectedIndex !== null && selectedImage && (
|
{selectedIndex !== null && selectedImage && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/90 backdrop-blur-sm p-4 md:p-8"
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/95 backdrop-blur-md p-4 md:p-6"
|
||||||
onClick={() => setSelectedIndex(null)}
|
onClick={() => setSelectedIndex(null)}
|
||||||
>
|
>
|
||||||
{/* Close Button */}
|
{/* Close Button */}
|
||||||
<button
|
<button
|
||||||
className="absolute top-4 right-4 p-3 bg-white/10 hover:bg-white/20 rounded-full text-white transition-colors z-50"
|
className="absolute top-4 right-4 p-2 bg-white/10 hover:bg-white/20 rounded-full text-white transition-colors z-50"
|
||||||
onClick={() => setSelectedIndex(null)}
|
onClick={() => setSelectedIndex(null)}
|
||||||
>
|
>
|
||||||
<X className="h-6 w-6" />
|
<X className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Navigation Buttons */}
|
{/* Navigation Buttons */}
|
||||||
{selectedIndex > 0 && (
|
{selectedIndex > 0 && (
|
||||||
<button
|
<button
|
||||||
className="absolute left-4 top-1/2 -translate-y-1/2 p-3 bg-white/10 hover:bg-white/20 rounded-full text-white transition-colors z-50 hidden md:block"
|
className="absolute left-2 md:left-4 top-1/2 -translate-y-1/2 p-2 md:p-3 bg-white/10 hover:bg-white/20 rounded-full text-white transition-colors z-50"
|
||||||
onClick={(e) => { e.stopPropagation(); setSelectedIndex(prev => prev! - 1); }}
|
onClick={(e) => { e.stopPropagation(); setSelectedIndex(prev => prev! - 1); }}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-8 w-8" />
|
<ChevronLeft className="h-6 w-6 md:h-8 md:w-8" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{selectedIndex < gallery.length - 1 && (
|
{selectedIndex < gallery.length - 1 && (
|
||||||
<button
|
<button
|
||||||
className="absolute right-4 top-1/2 -translate-y-1/2 p-3 bg-white/10 hover:bg-white/20 rounded-full text-white transition-colors z-50 hidden md:block"
|
className="absolute right-2 md:right-4 top-1/2 -translate-y-1/2 p-2 md:p-3 bg-white/10 hover:bg-white/20 rounded-full text-white transition-colors z-50"
|
||||||
onClick={(e) => { e.stopPropagation(); setSelectedIndex(prev => prev! + 1); }}
|
onClick={(e) => { e.stopPropagation(); setSelectedIndex(prev => prev! + 1); }}
|
||||||
>
|
>
|
||||||
<ChevronRight className="h-8 w-8" />
|
<ChevronRight className="h-6 w-6 md:h-8 md:w-8" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Image Container */}
|
{/* Split Panel Container */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0.9, opacity: 0 }}
|
initial={{ scale: 0.95, opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
exit={{ scale: 0.9, opacity: 0 }}
|
exit={{ scale: 0.95, opacity: 0 }}
|
||||||
className="relative max-w-7xl max-h-full flex flex-col items-center"
|
className="relative w-full max-w-6xl max-h-[90vh] flex flex-col md:flex-row gap-4 md:gap-6 overflow-hidden"
|
||||||
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
{/* Left: Image */}
|
||||||
|
<div className="flex-1 flex items-center justify-center min-h-0">
|
||||||
|
<img
|
||||||
|
src={getImageSrc(selectedImage.data)}
|
||||||
|
alt={selectedImage.prompt}
|
||||||
|
className="max-w-full max-h-[50vh] md:max-h-[85vh] object-contain rounded-xl shadow-2xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<img
|
{/* Right: Controls Panel */}
|
||||||
src={getImageSrc(selectedImage.data)}
|
<div className="w-full md:w-80 lg:w-96 flex flex-col gap-4 bg-white/5 rounded-xl p-4 md:p-5 border border-white/10 backdrop-blur-sm max-h-[40vh] md:max-h-[85vh] overflow-y-auto">
|
||||||
alt={selectedImage.prompt}
|
{/* Provider Badge */}
|
||||||
className="max-w-full max-h-[85vh] object-contain rounded-lg shadow-2xl"
|
{selectedImage.provider && (
|
||||||
/>
|
<div className={cn(
|
||||||
|
"self-start px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider",
|
||||||
|
selectedImage.provider === 'meta' ? "bg-blue-500/20 text-blue-300 border border-blue-500/30" :
|
||||||
|
selectedImage.provider === 'grok' ? "bg-yellow-500/20 text-yellow-300 border border-yellow-500/30" :
|
||||||
|
"bg-amber-500/20 text-amber-300 border border-amber-500/30"
|
||||||
|
)}>
|
||||||
|
{selectedImage.provider}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-4 flex flex-col items-center gap-2 max-w-2xl text-center">
|
{/* Prompt Section */}
|
||||||
<p className="text-white/90 text-sm md:text-base font-medium line-clamp-2">
|
<div className="space-y-2">
|
||||||
{selectedImage.prompt}
|
<h3 className="text-xs font-medium text-white/50 uppercase tracking-wider">Prompt</h3>
|
||||||
</p>
|
<p className="text-white/90 text-sm leading-relaxed">
|
||||||
<div className="flex gap-3">
|
{selectedImage.prompt || "No prompt available"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="border-t border-white/10" />
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-xs font-medium text-white/50 uppercase tracking-wider">Actions</h3>
|
||||||
|
|
||||||
|
{/* Download */}
|
||||||
<a
|
<a
|
||||||
href={getImageSrc(selectedImage.data)}
|
href={getImageSrc(selectedImage.data)}
|
||||||
download={"generated-" + selectedIndex + "-" + Date.now() + ".png"}
|
download={"generated-" + selectedIndex + "-" + Date.now() + ".png"}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90 rounded-full font-medium transition-colors"
|
className="flex items-center gap-3 w-full px-4 py-3 bg-white/10 hover:bg-white/15 rounded-lg text-white font-medium transition-colors"
|
||||||
>
|
>
|
||||||
<Download className="h-4 w-4" />
|
<Download className="h-5 w-5 text-green-400" />
|
||||||
Download Current
|
<span>Download Image</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
{/* Generate Video - Show for all providers */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedImage.provider === 'meta') {
|
||||||
|
handleGenerateMetaVideo(selectedImage);
|
||||||
|
} else {
|
||||||
|
openVideoModal(selectedImage);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isGeneratingMetaVideo}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 w-full px-4 py-3 rounded-lg text-white font-medium transition-all",
|
||||||
|
isGeneratingMetaVideo
|
||||||
|
? "bg-gray-600 cursor-wait"
|
||||||
|
: selectedImage.provider === 'meta'
|
||||||
|
? "bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-500 hover:to-cyan-500"
|
||||||
|
: "bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isGeneratingMetaVideo ? (
|
||||||
|
<>
|
||||||
|
<div className="h-5 w-5 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||||
|
<span>Generating Video...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Film className="h-5 w-5" />
|
||||||
|
<span>Generate Video</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Remix/Edit - Only for Whisk */}
|
||||||
{(!selectedImage.provider || selectedImage.provider === 'whisk') && (
|
{(!selectedImage.provider || selectedImage.provider === 'whisk') && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => openEditModal(selectedImage)}
|
||||||
if (selectedImage) openVideoModal(selectedImage);
|
className="flex items-center gap-3 w-full px-4 py-3 bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-500 hover:to-orange-500 rounded-lg text-white font-medium transition-all"
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-full font-medium transition-colors"
|
|
||||||
>
|
>
|
||||||
<Film className="h-4 w-4" />
|
<Wand2 className="h-5 w-5" />
|
||||||
Generate Video
|
<span>Remix / Edit</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Use Prompt */}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPrompt(selectedImage.prompt);
|
setPrompt(selectedImage.prompt);
|
||||||
navigator.clipboard.writeText(selectedImage.prompt);
|
navigator.clipboard.writeText(selectedImage.prompt);
|
||||||
setSelectedIndex(null); // Close lightbox? Or keep open? User said "reuse", likely wants to edit.
|
setSelectedIndex(null);
|
||||||
// Let's close it so they can see the input updating.
|
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-secondary text-secondary-foreground hover:bg-secondary/80 rounded-full font-medium transition-colors"
|
className="flex items-center gap-3 w-full px-4 py-3 bg-white/10 hover:bg-white/15 rounded-lg text-white font-medium transition-colors"
|
||||||
>
|
>
|
||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-5 w-5 text-purple-400" />
|
||||||
Use Prompt
|
<span>Copy & Use Prompt</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Delete */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedImage.id) {
|
||||||
|
removeFromGallery(selectedImage.id);
|
||||||
|
setSelectedIndex(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-3 w-full px-4 py-3 bg-red-500/10 hover:bg-red-500/20 rounded-lg text-red-400 font-medium transition-colors border border-red-500/20"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-5 w-5" />
|
||||||
|
<span>Delete Image</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image Info */}
|
||||||
|
<div className="mt-auto pt-3 border-t border-white/10 text-xs text-white/40 space-y-1">
|
||||||
|
{selectedImage.aspectRatio && (
|
||||||
|
<p>Aspect Ratio: {selectedImage.aspectRatio}</p>
|
||||||
|
)}
|
||||||
|
<p>Image {selectedIndex + 1} of {gallery.length}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import React, { useRef, useState, useEffect } from "react";
|
import React, { useRef, useState, useEffect } from "react";
|
||||||
import { useStore, ReferenceCategory } from "@/lib/store";
|
import { useStore, ReferenceCategory } from "@/lib/store";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Sparkles, Maximize2, X, Hash, AlertTriangle, Upload, Zap, Brain, Settings, Settings2, Video } from "lucide-react";
|
import { Sparkles, Maximize2, X, Hash, AlertTriangle, Upload, Zap, Brain, Settings, Settings2 } from "lucide-react";
|
||||||
|
|
||||||
const IMAGE_COUNTS = [1, 2, 4];
|
const IMAGE_COUNTS = [1, 2, 4];
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ export function PromptHero() {
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
const [isGenerating, setLocalIsGenerating] = useState(false);
|
const [isGenerating, setLocalIsGenerating] = useState(false);
|
||||||
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
|
|
||||||
const [uploadingRefs, setUploadingRefs] = useState<Record<string, boolean>>({});
|
const [uploadingRefs, setUploadingRefs] = useState<Record<string, boolean>>({});
|
||||||
const [errorNotification, setErrorNotification] = useState<{ message: string; type: 'error' | 'warning' } | null>(null);
|
const [errorNotification, setErrorNotification] = useState<{ message: string; type: 'error' | 'warning' } | null>(null);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
@ -205,85 +205,6 @@ export function PromptHero() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle video generation (Meta AI only)
|
|
||||||
// If a subject reference is set, it will use image-to-video
|
|
||||||
// Otherwise, it will use text-to-video
|
|
||||||
const handleGenerateVideo = async () => {
|
|
||||||
let finalPrompt = prompt.trim();
|
|
||||||
if (!finalPrompt || isGeneratingVideo || settings.provider !== 'meta') return;
|
|
||||||
|
|
||||||
setIsGeneratingVideo(true);
|
|
||||||
setIsGenerating(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if we have a subject reference for image-to-video
|
|
||||||
const subjectRefs = references.subject || [];
|
|
||||||
const imageBase64 = subjectRefs.length > 0 ? subjectRefs[0].thumbnail : undefined;
|
|
||||||
const mode = imageBase64 ? 'image-to-video' : 'text-to-video';
|
|
||||||
|
|
||||||
console.log(`[PromptHero] Starting Meta AI ${mode}...`);
|
|
||||||
|
|
||||||
const res = await fetch('/api/meta/video', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: finalPrompt,
|
|
||||||
cookies: settings.metaCookies,
|
|
||||||
imageBase64: imageBase64
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseText = await res.text();
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = JSON.parse(responseText);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("API Error (Non-JSON response):", responseText.substring(0, 500));
|
|
||||||
throw new Error(`Server Error: ${res.status} ${res.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.error) throw new Error(data.error);
|
|
||||||
|
|
||||||
if (data.videos && data.videos.length > 0) {
|
|
||||||
// Add videos to store
|
|
||||||
for (const video of data.videos) {
|
|
||||||
useStore.getState().addVideo({
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
url: video.url,
|
|
||||||
prompt: video.prompt || finalPrompt,
|
|
||||||
thumbnail: imageBase64, // Store the source image as thumbnail
|
|
||||||
createdAt: Date.now()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Show success notification
|
|
||||||
setErrorNotification({
|
|
||||||
message: `🎬 Success! Generated ${data.videos.length} video(s) via ${mode}. Check the gallery.`,
|
|
||||||
type: 'warning' // Using warning for visibility (amber color)
|
|
||||||
});
|
|
||||||
setTimeout(() => setErrorNotification(null), 5000);
|
|
||||||
} else {
|
|
||||||
throw new Error('No videos were generated');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error('[Video Gen]', e);
|
|
||||||
const errorMessage = e.message || '';
|
|
||||||
|
|
||||||
if (errorMessage.includes('401') || errorMessage.includes('cookies')) {
|
|
||||||
setShowCookieExpired(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrorNotification({
|
|
||||||
message: `🎬 Video Error: ${errorMessage}`,
|
|
||||||
type: 'error'
|
|
||||||
});
|
|
||||||
setTimeout(() => setErrorNotification(null), 8000);
|
|
||||||
} finally {
|
|
||||||
setIsGeneratingVideo(false);
|
|
||||||
setIsGenerating(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -567,19 +488,14 @@ export function PromptHero() {
|
||||||
<div className="flex flex-col md:flex-row items-center justify-between gap-3 pt-1">
|
<div className="flex flex-col md:flex-row items-center justify-between gap-3 pt-1">
|
||||||
|
|
||||||
{/* Left Controls: References */}
|
{/* Left Controls: References */}
|
||||||
{/* For Meta AI: Only subject is enabled (for image-to-video), scene/style are disabled */}
|
{/* For Meta AI: References are disabled (generate images first, video from gallery) */}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className={cn("flex flex-wrap gap-2", settings.provider === 'meta' && "opacity-30 pointer-events-none grayscale")}>
|
||||||
{(['subject', 'scene', 'style'] as ReferenceCategory[]).map((cat) => {
|
{(['subject', 'scene', 'style'] as ReferenceCategory[]).map((cat) => {
|
||||||
const refs = references[cat] || [];
|
const refs = references[cat] || [];
|
||||||
const hasRefs = refs.length > 0;
|
const hasRefs = refs.length > 0;
|
||||||
const isUploading = uploadingRefs[cat];
|
const isUploading = uploadingRefs[cat];
|
||||||
// For Meta AI: only enable subject (for image-to-video), disable scene/style
|
|
||||||
const isDisabledForMeta = settings.provider === 'meta' && cat !== 'subject';
|
|
||||||
return (
|
return (
|
||||||
<div key={cat} className={cn(
|
<div key={cat} className="relative group">
|
||||||
"relative group",
|
|
||||||
isDisabledForMeta && "opacity-30 pointer-events-none grayscale"
|
|
||||||
)}>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleReference(cat)}
|
onClick={() => toggleReference(cat)}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
|
|
@ -591,9 +507,6 @@ export function PromptHero() {
|
||||||
: "bg-white/5 text-white/40 border-white/5 hover:bg-white/10 hover:text-white/70 hover:border-white/10",
|
: "bg-white/5 text-white/40 border-white/5 hover:bg-white/10 hover:text-white/70 hover:border-white/10",
|
||||||
isUploading && "animate-pulse cursor-wait"
|
isUploading && "animate-pulse cursor-wait"
|
||||||
)}
|
)}
|
||||||
title={settings.provider === 'meta' && cat === 'subject'
|
|
||||||
? "Upload an image to animate into video"
|
|
||||||
: undefined}
|
|
||||||
>
|
>
|
||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||||
|
|
@ -715,7 +628,7 @@ export function PromptHero() {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative z-10 flex items-center gap-1.5">
|
<div className="relative z-10 flex items-center gap-1.5">
|
||||||
{isGenerating && !isGeneratingVideo ? (
|
{isGenerating ? (
|
||||||
<>
|
<>
|
||||||
<div className="h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
<div className="h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||||
<span className="animate-pulse">Dreaming...</span>
|
<span className="animate-pulse">Dreaming...</span>
|
||||||
|
|
@ -732,39 +645,6 @@ export function PromptHero() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Generate Video Button - Only for Meta AI */}
|
|
||||||
{settings.provider === 'meta' && (
|
|
||||||
<button
|
|
||||||
onClick={handleGenerateVideo}
|
|
||||||
disabled={isGenerating || !prompt.trim()}
|
|
||||||
className={cn(
|
|
||||||
"relative overflow-hidden px-4 py-1.5 rounded-lg font-bold text-sm text-white shadow-lg transition-all active:scale-95 group border border-white/10",
|
|
||||||
isGenerating
|
|
||||||
? "bg-gray-700 cursor-not-allowed"
|
|
||||||
: references.subject?.length
|
|
||||||
? "bg-gradient-to-r from-pink-600 to-purple-600 hover:from-pink-500 hover:to-purple-500 hover:shadow-purple-500/25"
|
|
||||||
: "bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-500 hover:to-cyan-500 hover:shadow-cyan-500/25"
|
|
||||||
)}
|
|
||||||
title={references.subject?.length
|
|
||||||
? "Animate the subject image (30-60+ seconds)"
|
|
||||||
: "Generate video from text prompt (30-60+ seconds)"}
|
|
||||||
>
|
|
||||||
<div className="relative z-10 flex items-center gap-1.5">
|
|
||||||
{isGeneratingVideo ? (
|
|
||||||
<>
|
|
||||||
<div className="h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
|
||||||
<span className="animate-pulse">Creating...</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Video className="h-3 w-3 group-hover:scale-110 transition-transform" />
|
|
||||||
<span>{references.subject?.length ? "Animate" : "Video"}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"last_updated": "2026-01-06T04:34:41.435Z",
|
"last_updated": "2026-01-06T07:32:15.350Z",
|
||||||
"lastSync": 1767674081435,
|
"lastSync": 1767684735350,
|
||||||
"categories": {
|
"categories": {
|
||||||
"style": [
|
"style": [
|
||||||
"Illustration",
|
"Illustration",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue