202 lines
10 KiB
TypeScript
202 lines
10 KiB
TypeScript
|
|
import React, { useState, useRef } from 'react';
|
|
import { geminiService } from '../services/geminiService';
|
|
import { Image as ImageIcon, Video, Wand2, Download, Loader2, Sparkles } from 'lucide-react';
|
|
import { Language } from '../types';
|
|
import { translations } from '../utils/localization';
|
|
|
|
interface CreativeStudioProps {
|
|
language: Language;
|
|
addToast: (type: 'success' | 'error' | 'info', msg: string) => void;
|
|
}
|
|
|
|
type Mode = 'image-gen' | 'image-edit' | 'video-gen';
|
|
|
|
const CreativeStudio: React.FC<CreativeStudioProps> = ({ language, addToast }) => {
|
|
const t = translations[language].creative;
|
|
const tCommon = translations[language].common;
|
|
|
|
const [mode, setMode] = useState<Mode>('image-gen');
|
|
const [prompt, setPrompt] = useState('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [resultUrl, setResultUrl] = useState<string | null>(null);
|
|
const [statusMessage, setStatusMessage] = useState('');
|
|
const [uploadedImage, setUploadedImage] = useState<string | null>(null);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const handleGenerate = async () => {
|
|
if (!prompt.trim()) return;
|
|
setIsLoading(true);
|
|
setResultUrl(null);
|
|
setStatusMessage(t.creatingBtn);
|
|
|
|
try {
|
|
if (mode === 'image-gen') {
|
|
const url = await geminiService.generateImage(prompt);
|
|
setResultUrl(url);
|
|
} else if (mode === 'video-gen') {
|
|
const url = await geminiService.generateVideo(prompt, (status) => setStatusMessage(status));
|
|
setResultUrl(url);
|
|
} else if (mode === 'image-edit') {
|
|
if (!uploadedImage) {
|
|
addToast('error', t.uploadAlert);
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
const url = await geminiService.editImage(uploadedImage, prompt);
|
|
setResultUrl(url);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
addToast('error', "Generation failed.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
setUploadedImage(reader.result as string);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
};
|
|
|
|
// ... rest of UI (same as before) ...
|
|
const ModeButton = ({ id, icon: Icon, label }: { id: Mode, icon: React.ElementType, label: string }) => {
|
|
const isActive = mode === id;
|
|
return (
|
|
<button
|
|
onClick={() => { setMode(id); setResultUrl(null); }}
|
|
className={`relative flex-1 px-4 py-3 rounded-xl text-sm font-bold flex items-center justify-center gap-2 transition-all duration-200 ${
|
|
isActive
|
|
? 'bg-white text-indigo-600 shadow-md shadow-slate-200 ring-1 ring-black/5 z-10 scale-105'
|
|
: 'text-slate-500 hover:bg-slate-100/50'
|
|
}`}
|
|
>
|
|
<Icon size={18} className={isActive ? 'stroke-[2.5px]' : ''} /> {label}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="h-full flex flex-col p-4 md:p-8 bg-slate-50/50 overflow-y-auto animate-fade-in">
|
|
<div className="max-w-5xl mx-auto w-full">
|
|
{/* ... Header & Controls (same) ... */}
|
|
<div className="mb-8 animate-fade-in-up">
|
|
<h2 className="text-3xl font-extrabold text-slate-800 tracking-tight">{t.title}</h2>
|
|
<p className="text-slate-500 mt-1">{tCommon.poweredBy}</p>
|
|
</div>
|
|
|
|
<div className="bg-slate-200/60 p-1.5 rounded-2xl flex mb-8 w-full max-w-2xl mx-auto shadow-inner animate-fade-in-up delay-100">
|
|
<ModeButton id="image-gen" icon={ImageIcon} label={t.genImage} />
|
|
<ModeButton id="image-edit" icon={Wand2} label={t.editImage} />
|
|
<ModeButton id="video-gen" icon={Video} label={t.genVideo} />
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* Input Section */}
|
|
<div className="bg-white p-6 md:p-8 rounded-3xl shadow-sm border border-slate-100 h-fit relative overflow-hidden animate-fade-in-up delay-200">
|
|
{/* Decor */}
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-indigo-50 to-transparent -mr-10 -mt-10 rounded-full" />
|
|
|
|
{mode === 'image-edit' && (
|
|
<div className="mb-8 relative z-10 animate-scale-in">
|
|
<label className="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-3">{t.editLabel1}</label>
|
|
<div
|
|
onClick={() => fileInputRef.current?.click()}
|
|
className="border-2 border-dashed border-slate-200 rounded-2xl h-48 flex flex-col items-center justify-center cursor-pointer hover:bg-indigo-50 hover:border-indigo-200 transition-all duration-300 overflow-hidden relative group"
|
|
>
|
|
{uploadedImage ? (
|
|
<img src={uploadedImage} alt="Base" className="w-full h-full object-cover" />
|
|
) : (
|
|
<div className="text-center text-slate-400 group-hover:text-indigo-500 transition-colors">
|
|
<div className="w-12 h-12 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-3 group-hover:bg-white group-hover:scale-110 transition-all">
|
|
<ImageIcon size={24} />
|
|
</div>
|
|
<span className="text-sm font-medium">{t.uploadPlaceholder}</span>
|
|
</div>
|
|
)}
|
|
<input
|
|
type="file"
|
|
ref={fileInputRef}
|
|
className="hidden"
|
|
accept="image/*"
|
|
onChange={handleImageUpload}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mb-8 relative z-10">
|
|
<label className="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-3">
|
|
{mode === 'image-edit' ? t.editLabel2 : t.promptLabel}
|
|
</label>
|
|
<textarea
|
|
value={prompt}
|
|
onChange={(e) => setPrompt(e.target.value)}
|
|
className="w-full bg-slate-50 border border-slate-200 rounded-2xl p-4 text-slate-700 focus:bg-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent focus:outline-none h-32 resize-none shadow-inner text-lg transition-all"
|
|
placeholder={
|
|
mode === 'video-gen' ? t.videoPrompt :
|
|
mode === 'image-edit' ? t.editPrompt :
|
|
t.imagePrompt
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleGenerate}
|
|
disabled={isLoading || !prompt}
|
|
className="w-full py-4 bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-500 hover:to-violet-500 text-white rounded-2xl font-bold shadow-lg shadow-indigo-200 transform active:scale-95 transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
>
|
|
{isLoading ? <Loader2 className="animate-spin" /> : <Sparkles size={20} />}
|
|
{isLoading ? t.creatingBtn : t.generateBtn}
|
|
</button>
|
|
|
|
{mode === 'video-gen' && (
|
|
<p className="text-[10px] text-slate-400 mt-4 text-center uppercase tracking-wide font-medium animate-pulse">
|
|
{t.videoWarning}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Output Section (Simplified for brevity, logic same as before) */}
|
|
<div className="bg-white rounded-3xl border border-slate-100 shadow-sm flex flex-col items-center justify-center min-h-[400px] p-4 relative overflow-hidden group animate-fade-in-up delay-300">
|
|
<div className="absolute inset-0 opacity-[0.03] pointer-events-none bg-[radial-gradient(#4f46e5_1px,transparent_1px)] [background-size:16px_16px]"></div>
|
|
{isLoading ? (
|
|
<div className="text-center relative z-10">
|
|
<div className="relative">
|
|
<div className="absolute inset-0 bg-indigo-500 blur-xl opacity-20 animate-pulse rounded-full"></div>
|
|
<Loader2 size={64} className="text-indigo-600 animate-spin mx-auto mb-6 relative z-10" />
|
|
</div>
|
|
<p className="text-slate-800 font-bold text-lg animate-pulse">{statusMessage}</p>
|
|
</div>
|
|
) : resultUrl ? (
|
|
<div className="w-full h-full flex flex-col items-center justify-center relative z-10 animate-scale-in">
|
|
<div className="relative rounded-2xl overflow-hidden shadow-2xl ring-4 ring-white transition-transform hover:scale-[1.02]">
|
|
{mode === 'video-gen' ? (
|
|
<video controls autoPlay loop className="max-h-[400px] w-auto bg-black"><source src={resultUrl} type="video/mp4" /></video>
|
|
) : (
|
|
<img src={resultUrl} alt="Result" className="max-h-[400px] w-auto object-contain bg-white" />
|
|
)}
|
|
</div>
|
|
<a href={resultUrl} download="creation" className="mt-6 flex items-center gap-2 px-6 py-3 bg-slate-900 text-white rounded-full font-bold hover:bg-slate-800 transition-colors shadow-lg hover:scale-105 transform"><Download size={18} /> {t.download}</a>
|
|
</div>
|
|
) : (
|
|
<div className="text-slate-300 text-center relative z-10">
|
|
<Sparkles size={64} className="mx-auto mb-4 opacity-50" />
|
|
<p className="font-medium text-lg">{t.emptyState}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CreativeStudio;
|