Files
ai-app-skg/components/Tools.tsx
2025-12-26 16:03:17 +08:00

168 lines
8.0 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, Sparkles, Key } from 'lucide-react';
interface ToolsProps {
language: AppLanguage;
hasCustomKey?: boolean;
}
const Tools: React.FC<ToolsProps> = ({ language, hasCustomKey }) => {
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;
// Users must select their own API key for image/video generation tasks
// Skip this check if user has manually configured a key in settings
if (!hasCustomKey && typeof (window as any).aistudio !== 'undefined') {
const hasKey = await (window as any).aistudio.hasSelectedApiKey();
if (!hasKey) {
await (window as any).aistudio.openSelectKey();
// Proceeding assuming selection was successful per guidelines
}
}
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) {
if (!hasCustomKey && e.message && e.message.includes("Requested entity was not found.")) {
// Handle race condition or invalid key by prompting re-selection, ONLY if no custom key
if (typeof (window as any).aistudio !== 'undefined') {
await (window as any).aistudio.openSelectKey();
}
}
setError(e.message || t.genError);
} finally {
setLoading(false);
}
};
return (
<div className="max-w-4xl mx-auto p-4 md:p-8 space-y-6">
<div className="flex space-x-1 bg-slate-200 dark:bg-slate-800 p-1 rounded-xl w-fit animate-fade-in shadow-inner">
<button
onClick={() => { setActiveTab('image'); setResultUrl(null); }}
className={`flex items-center space-x-2 px-6 py-2 rounded-lg transition-all active:scale-95 ${activeTab === 'image' ? 'bg-white dark:bg-slate-700 shadow text-blue-600 dark:text-blue-400' : 'text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200'}`}
>
<ImageIcon size={18} />
<span className="font-medium">{t.imageGen}</span>
</button>
<button
onClick={() => { setActiveTab('video'); setResultUrl(null); }}
className={`flex items-center space-x-2 px-6 py-2 rounded-lg transition-all active:scale-95 ${activeTab === 'video' ? 'bg-white dark:bg-slate-700 shadow text-purple-600 dark:text-purple-400' : 'text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200'}`}
>
<Video size={18} />
<span className="font-medium">{t.videoGen}</span>
</button>
</div>
<div className="bg-white dark:bg-slate-900 rounded-3xl shadow-xl border border-slate-100 dark:border-slate-800 p-6 md:p-8 animate-slide-up space-y-6">
<div className="relative">
<textarea
className="w-full p-5 bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:outline-none focus:bg-white dark:focus:bg-slate-900 transition-all resize-none min-h-[120px] text-slate-700 dark:text-slate-200"
rows={4}
placeholder={activeTab === 'image' ? t.imagePromptPlaceholder : t.videoPromptPlaceholder}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
/>
<Sparkles className="absolute right-4 bottom-4 text-slate-300 dark:text-slate-600 pointer-events-none" size={20} />
</div>
<div className="flex flex-wrap gap-6 items-center justify-between">
<div className="flex gap-6">
{activeTab === 'image' ? (
<div className="flex flex-col gap-1">
<span className="text-xs font-semibold text-slate-400 uppercase tracking-tight ml-1">{t.imageSize}</span>
<select
value={imageSize}
onChange={(e) => setImageSize(e.target.value as any)}
className="px-4 py-2 bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-200 border border-transparent rounded-xl text-sm font-medium focus:bg-white dark:focus:bg-slate-700 focus:border-blue-200 transition-all"
>
<option value="1K">1K (HD)</option>
<option value="2K">2K (QHD)</option>
<option value="4K">4K (UHD)</option>
</select>
</div>
) : (
<div className="flex flex-col gap-1">
<span className="text-xs font-semibold text-slate-400 uppercase tracking-tight ml-1">{t.aspectRatio}</span>
<select
value={videoRatio}
onChange={(e) => setVideoRatio(e.target.value as any)}
className="px-4 py-2 bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-200 border border-transparent rounded-xl text-sm font-medium focus:bg-white dark:focus:bg-slate-700 focus:border-blue-200 transition-all"
>
<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-8 py-3 bg-blue-600 text-white font-bold rounded-2xl hover:bg-blue-700 active:scale-95 disabled:opacity-50 flex items-center space-x-2 transition-all shadow-lg shadow-blue-200 dark:shadow-none"
>
{loading ? <Loader2 className="animate-spin" size={20} /> : null}
<span>{loading ? t.generating : t.generate}</span>
</button>
</div>
{error && (
<div className="p-4 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm rounded-xl border border-red-100 dark:border-red-900/30 animate-slide-up">
{error}
</div>
)}
{loading && activeTab === 'video' && (
<div className="text-center py-4 text-sm text-slate-500 dark:text-slate-400 animate-breathe">
{t.videoDuration}
</div>
)}
{resultUrl && (
<div className="mt-8 border-t border-slate-100 dark:border-slate-800 pt-8 animate-slide-up">
<div className="relative rounded-2xl overflow-hidden bg-slate-900 flex justify-center items-center shadow-2xl group">
{activeTab === 'image' ? (
<img src={resultUrl} alt="Generated" className="max-h-[600px] w-auto object-contain transition-transform duration-500 group-hover:scale-[1.02]" />
) : (
<video src={resultUrl} controls autoPlay loop className="max-h-[600px] w-auto" />
)}
<a
href={resultUrl}
download={`sociopal-${activeTab}-${Date.now()}.${activeTab === 'image' ? 'png' : 'mp4'}`}
className="absolute top-4 right-4 bg-white/90 backdrop-blur p-3 rounded-full shadow-lg hover:bg-white text-slate-800 transition-all opacity-0 group-hover:opacity-100 translate-y-2 group-hover:translate-y-0 active:scale-90"
title={t.download}
>
<Download size={24} />
</a>
</div>
</div>
)}
</div>
</div>
);
};
export default Tools;