Files
ai-app-skr/views/CreativeStudio.tsx
2025-11-21 00:24:18 +08:00

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;