147 lines
5.6 KiB
TypeScript
147 lines
5.6 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { TRANSLATIONS } from '../constants';
|
|
import { AppLanguage } from '../types';
|
|
import { generateImage, generateVideo } from '../services/geminiService';
|
|
import { Loader2, Image as ImageIcon, Video, Download } from 'lucide-react';
|
|
|
|
interface ToolsProps {
|
|
language: AppLanguage;
|
|
}
|
|
|
|
const Tools: React.FC<ToolsProps> = ({ language }) => {
|
|
const t = TRANSLATIONS[language];
|
|
const [activeTab, setActiveTab] = useState<'image' | 'video'>('image');
|
|
const [prompt, setPrompt] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [resultUrl, setResultUrl] = useState<string | null>(null);
|
|
const [imageSize, setImageSize] = useState<"1K" | "2K" | "4K">("1K");
|
|
const [videoRatio, setVideoRatio] = useState<"16:9" | "9:16">("16:9");
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const handleGenerate = async () => {
|
|
if (!prompt.trim()) return;
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
setResultUrl(null);
|
|
|
|
try {
|
|
if (activeTab === 'image') {
|
|
const images = await generateImage(prompt, imageSize);
|
|
if (images.length > 0) setResultUrl(images[0]);
|
|
} else {
|
|
const video = await generateVideo(prompt, videoRatio);
|
|
setResultUrl(video);
|
|
}
|
|
} catch (e: any) {
|
|
setError(e.message || "Generation failed");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto p-4 space-y-6">
|
|
<div className="flex space-x-2 bg-slate-200 p-1 rounded-lg w-fit">
|
|
<button
|
|
onClick={() => setActiveTab('image')}
|
|
className={`flex items-center space-x-2 px-4 py-2 rounded-md transition ${activeTab === 'image' ? 'bg-white shadow text-blue-600' : 'text-slate-600'}`}
|
|
>
|
|
<ImageIcon size={18} />
|
|
<span>{t.imageGen}</span>
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('video')}
|
|
className={`flex items-center space-x-2 px-4 py-2 rounded-md transition ${activeTab === 'video' ? 'bg-white shadow text-purple-600' : 'text-slate-600'}`}
|
|
>
|
|
<Video size={18} />
|
|
<span>{t.videoGen}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-2xl shadow-sm border border-slate-100 p-6">
|
|
<textarea
|
|
className="w-full p-4 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:outline-none resize-none"
|
|
rows={4}
|
|
placeholder={activeTab === 'image' ? t.imagePromptPlaceholder : t.videoPromptPlaceholder}
|
|
value={prompt}
|
|
onChange={(e) => setPrompt(e.target.value)}
|
|
/>
|
|
|
|
<div className="mt-4 flex flex-wrap gap-4 items-center justify-between">
|
|
<div className="flex gap-4">
|
|
{activeTab === 'image' ? (
|
|
<div className="flex items-center space-x-2">
|
|
<span className="text-sm text-slate-500">{t.imageSize}:</span>
|
|
<select
|
|
value={imageSize}
|
|
onChange={(e) => setImageSize(e.target.value as any)}
|
|
className="p-2 bg-slate-50 border border-slate-200 rounded-lg text-sm"
|
|
>
|
|
<option value="1K">1K</option>
|
|
<option value="2K">2K</option>
|
|
<option value="4K">4K</option>
|
|
</select>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center space-x-2">
|
|
<span className="text-sm text-slate-500">{t.aspectRatio}:</span>
|
|
<select
|
|
value={videoRatio}
|
|
onChange={(e) => setVideoRatio(e.target.value as any)}
|
|
className="p-2 bg-slate-50 border border-slate-200 rounded-lg text-sm"
|
|
>
|
|
<option value="16:9">{t.landscape}</option>
|
|
<option value="9:16">{t.portrait}</option>
|
|
</select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleGenerate}
|
|
disabled={loading || !prompt.trim()}
|
|
className="px-6 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700 disabled:opacity-50 flex items-center space-x-2"
|
|
>
|
|
{loading ? <Loader2 className="animate-spin" size={18} /> : null}
|
|
<span>{loading ? t.generating : t.generate}</span>
|
|
</button>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mt-4 p-3 bg-red-50 text-red-600 text-sm rounded-lg">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{loading && activeTab === 'video' && (
|
|
<div className="mt-4 text-center text-sm text-slate-500 animate-pulse">
|
|
{t.videoDuration}
|
|
</div>
|
|
)}
|
|
|
|
{resultUrl && (
|
|
<div className="mt-8 border-t pt-6">
|
|
<div className="relative rounded-xl overflow-hidden bg-black flex justify-center items-center">
|
|
{activeTab === 'image' ? (
|
|
<img src={resultUrl} alt="Generated" className="max-h-[500px] w-auto object-contain" />
|
|
) : (
|
|
<video src={resultUrl} controls autoPlay loop className="max-h-[500px] w-auto" />
|
|
)}
|
|
<a
|
|
href={resultUrl}
|
|
download={`generated-${activeTab}-${Date.now()}.${activeTab === 'image' ? 'png' : 'mp4'}`}
|
|
className="absolute top-4 right-4 bg-white/90 p-2 rounded-full shadow hover:bg-white text-slate-800"
|
|
title={t.download}
|
|
>
|
|
<Download size={20} />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Tools; |