118 lines
4.8 KiB
TypeScript
118 lines
4.8 KiB
TypeScript
import React, { useState } from 'react';
|
|
import axios from 'axios';
|
|
import { Loader2, Video, Play, Film } from 'lucide-react';
|
|
import { useAuth } from '../AuthContext';
|
|
|
|
interface VideoGeneratorProps {
|
|
project: any;
|
|
onVideoGenerated: () => void;
|
|
}
|
|
|
|
const VIDEO_PRESETS = [
|
|
{ id: 'cinematic_pan', label: 'Cinematic Pan', icon: '↔️', description: 'Slow horizontal pan across the artwork' },
|
|
{ id: 'slow_zoom', label: 'Slow Zoom', icon: '🔍', description: 'Gentle zoom in to highlight details' },
|
|
{ id: 'windy_atmosphere', label: 'Windy Atmosphere', icon: '🍃', description: 'Subtle movement suggesting a breeze' },
|
|
{ id: 'page_flip', label: 'Page Flip (Conceptual)', icon: '📖', description: 'Simulated page turning effect' },
|
|
];
|
|
|
|
export const VideoGenerator: React.FC<VideoGeneratorProps> = ({ project, onVideoGenerated }) => {
|
|
const [generating, setGenerating] = useState(false);
|
|
const [selectedPreset, setSelectedPreset] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const { refreshUser } = useAuth();
|
|
|
|
const handleGenerate = async () => {
|
|
if (!selectedPreset) return;
|
|
|
|
setGenerating(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
const apiKey = localStorage.getItem('gemini_api_key');
|
|
|
|
await axios.post(
|
|
`/api/projects/${project.id}/video-mockups`,
|
|
{ presetId: selectedPreset },
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'X-Gemini-API-Key': apiKey || ''
|
|
}
|
|
}
|
|
);
|
|
|
|
onVideoGenerated();
|
|
setSelectedPreset(null);
|
|
await refreshUser();
|
|
} catch (err: any) {
|
|
console.error("Video generation failed:", err);
|
|
const msg = err.response?.data?.error || "Failed to generate video";
|
|
if (err.response?.status === 402) alert(`⚠️ ${msg}`);
|
|
setError(msg);
|
|
} finally {
|
|
setGenerating(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="bg-white/50 backdrop-blur-sm rounded-xl p-6 border border-stone-200 shadow-sm mt-8">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<div className="p-2 bg-purple-100 rounded-lg">
|
|
<Film className="w-5 h-5 text-purple-600" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-stone-800">Video Studio (Beta)</h3>
|
|
<p className="text-sm text-stone-500">Transform static designs into cinematic video mockups</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
|
{VIDEO_PRESETS.map((preset) => (
|
|
<button
|
|
key={preset.id}
|
|
onClick={() => setSelectedPreset(preset.id)}
|
|
className={`p-4 rounded-xl border text-left transition-all ${selectedPreset === preset.id
|
|
? 'border-purple-500 bg-purple-50 shadow-md ring-1 ring-purple-200'
|
|
: 'border-stone-200 hover:border-purple-300 hover:bg-white'
|
|
}`}
|
|
>
|
|
<div className="text-2xl mb-2">{preset.icon}</div>
|
|
<div className="font-medium text-stone-800 mb-1">{preset.label}</div>
|
|
<div className="text-xs text-stone-500">{preset.description}</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mb-4 p-3 bg-red-50 text-red-600 rounded-lg text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-end">
|
|
<button
|
|
onClick={handleGenerate}
|
|
disabled={generating || !selectedPreset}
|
|
className={`flex items-center gap-2 px-6 py-2.5 rounded-lg font-medium transition-all ${generating || !selectedPreset
|
|
? 'bg-stone-200 text-stone-400 cursor-not-allowed'
|
|
: 'bg-gradient-to-r from-purple-600 to-indigo-600 text-white hover:shadow-lg hover:scale-[1.02]'
|
|
}`}
|
|
>
|
|
{generating ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
Renderizing...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Play className="w-4 h-4" />
|
|
Generate Video
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|