feat: enable Subject upload for Meta AI video generation
Some checks are pending
CI / build (18.x) (push) Waiting to run
CI / build (20.x) (push) Waiting to run

- Subject button now enabled for Meta AI (Scene/Style still disabled)
- Video button appears when Subject reference is uploaded
- Uses Subject image for image-to-video generation
- Added handleGenerateVideo() in PromptHero
- Tooltip: 'Upload image to animate into video'
This commit is contained in:
Khoa.vo 2026-01-06 14:51:08 +07:00
parent 7aaa4c8166
commit c2ee01b7b7

View file

@ -3,7 +3,7 @@
import React, { useRef, useState, useEffect } from "react";
import { useStore, ReferenceCategory } from "@/lib/store";
import { cn } from "@/lib/utils";
import { Sparkles, Maximize2, X, Hash, AlertTriangle, Upload, Zap, Brain, Settings, Settings2 } from "lucide-react";
import { Sparkles, Maximize2, X, Hash, AlertTriangle, Upload, Zap, Brain, Settings, Settings2, Video } from "lucide-react";
const IMAGE_COUNTS = [1, 2, 4];
@ -19,6 +19,8 @@ export function PromptHero() {
} = useStore();
const [isGenerating, setLocalIsGenerating] = useState(false);
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
const { addVideo } = useStore();
const [uploadingRefs, setUploadingRefs] = useState<Record<string, boolean>>({});
const [errorNotification, setErrorNotification] = useState<{ message: string; type: 'error' | 'warning' } | null>(null);
@ -205,6 +207,80 @@ export function PromptHero() {
}
};
// Handle video generation (Meta AI only) - uses Subject reference image
const handleGenerateVideo = async () => {
const subjectRefs = references.subject || [];
if (subjectRefs.length === 0) {
setErrorNotification({ message: '📷 Please upload a Subject image first', type: 'warning' });
setTimeout(() => setErrorNotification(null), 4000);
return;
}
const finalPrompt = prompt.trim() || "Animate this image with natural, cinematic movement";
if (!settings.metaCookies) {
setShowCookieExpired(true);
return;
}
setIsGeneratingVideo(true);
setIsGenerating(true);
try {
const imageBase64 = subjectRefs[0].thumbnail;
console.log('[PromptHero] Starting Meta AI image-to-video...');
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 data = await res.json();
if (data.error) throw new Error(data.error);
if (data.success && data.videos?.length > 0) {
for (const video of data.videos) {
addVideo({
id: crypto.randomUUID(),
url: video.url,
prompt: video.prompt || finalPrompt,
thumbnail: imageBase64,
createdAt: Date.now()
});
}
setErrorNotification({
message: `🎬 Video generated! Check the gallery.`,
type: 'warning'
});
setTimeout(() => setErrorNotification(null), 5000);
} else {
throw new Error('No videos generated');
}
} catch (e: any) {
console.error('[Video Gen]', e);
const errorMessage = e.message || 'Video generation failed';
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) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
@ -488,18 +564,26 @@ export function PromptHero() {
<div className="flex flex-col md:flex-row items-center justify-between gap-3 pt-1">
{/* Left Controls: References */}
{/* For Meta AI: References are disabled (generate images first, video from gallery) */}
<div className={cn("flex flex-wrap gap-2", settings.provider === 'meta' && "opacity-30 pointer-events-none grayscale")}>
{/* For Meta AI: Only Subject is enabled (for video generation), Scene/Style disabled */}
<div className="flex flex-wrap gap-2">
{(['subject', 'scene', 'style'] as ReferenceCategory[]).map((cat) => {
const refs = references[cat] || [];
const hasRefs = refs.length > 0;
const isUploading = uploadingRefs[cat];
// For Meta AI: only Subject is enabled (for image-to-video), Scene/Style disabled
const isDisabledForMeta = settings.provider === 'meta' && cat !== 'subject';
return (
<div key={cat} className="relative group">
<div key={cat} className={cn(
"relative group",
isDisabledForMeta && "opacity-30 pointer-events-none grayscale"
)}>
<button
onClick={() => toggleReference(cat)}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, cat)}
title={settings.provider === 'meta' && cat === 'subject'
? "Upload image to animate into video"
: undefined}
className={cn(
"flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[10px] font-medium transition-all border relative overflow-hidden",
hasRefs
@ -645,6 +729,35 @@ export function PromptHero() {
)}
</div>
</button>
{/* Generate Video Button - Only for Meta AI when Subject is uploaded */}
{settings.provider === 'meta' && (references.subject?.length ?? 0) > 0 && (
<button
onClick={handleGenerateVideo}
disabled={isGenerating}
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"
: "bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-500 hover:to-cyan-500 hover:shadow-cyan-500/25"
)}
title="Animate the subject image into video"
>
<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>Video</span>
</>
)}
</div>
</button>
)}
</div>
</div>