taghirsado / components /ModelModal.tsx
Opera10's picture
Update components/ModelModal.tsx
4ed4d62 verified
Raw
History Blame Contribute Delete
15.2 kB
import React, { useState, useRef, useEffect } from 'react';
import { Model } from '../types';
interface ModelModalProps {
model: Model | null;
isOpen: boolean;
onClose: () => void;
onConfirm: (data: { file?: File; text?: string; mode: 'vc' | 'tts'; pitch: number }) => void;
isLoading: boolean;
subscriptionStatus?: 'free' | 'paid';
onUpgrade?: () => void;
}
const ModelModal: React.FC<ModelModalProps> = ({
model,
isOpen,
onClose,
onConfirm,
isLoading,
subscriptionStatus = 'free',
onUpgrade
}) => {
const [activeMode, setActiveMode] = useState<'vc' | 'tts'>('vc');
const [file, setFile] = useState<File | null>(null);
const [fileUrl, setFileUrl] = useState<string | null>(null);
const [text, setText] = useState('');
const [pitch, setPitch] = useState<number | null>(null); // Null initially to force selection
const inputRef = useRef<HTMLInputElement>(null);
// Reset state when modal opens
useEffect(() => {
if (isOpen) {
setFile(null);
setFileUrl(null);
setText('');
setPitch(null); // Reset pitch
}
}, [isOpen]);
// Clean up object URL
useEffect(() => {
return () => {
if (fileUrl) URL.revokeObjectURL(fileUrl);
};
}, [fileUrl]);
const handleFileChange = (selectedFile: File) => {
if (fileUrl) URL.revokeObjectURL(fileUrl);
setFile(selectedFile);
setFileUrl(URL.createObjectURL(selectedFile));
};
if (!isOpen || !model) return null;
const handleConfirm = () => {
if (activeMode === 'vc' && file) {
onConfirm({ file, mode: 'vc', pitch: pitch ?? 0 });
} else if (activeMode === 'tts' && text.trim().length > 0) {
onConfirm({ text, mode: 'tts', pitch: pitch ?? 0 });
}
};
const isLegacy = model.type === 'legacy_rvc';
// Logic for pitch selection based on TARGET GENDER
const targetIsFemale = model.targetGender === 'female';
const setPitchForGender = (inputGender: 'male' | 'female') => {
if (targetIsFemale) {
if (inputGender === 'male') setPitch(12);
else setPitch(0);
} else {
if (inputGender === 'male') setPitch(0);
else setPitch(-12);
}
};
const isMaleSelected = targetIsFemale ? pitch === 12 : pitch === 0;
const isFemaleSelected = targetIsFemale ? pitch === 0 : pitch === -12;
const isReady = activeMode === 'vc'
? (!!file && (!isLegacy || pitch !== null))
: (text.trim().length > 0 && (subscriptionStatus !== 'free' || text.length <= 500));
const activeColor = activeMode === 'vc' ? 'bg-gradient-to-r from-blue-500 to-indigo-600' : 'bg-gradient-to-r from-teal-400 to-teal-600';
const containerBorder = activeMode === 'vc' ? 'border-blue-100 bg-blue-50/30' : 'border-teal-100 bg-teal-50/30';
// Handle Model Image for Custom Models (Handle File/Blob)
const getModelImage = () => {
if (model.isCustom && model.image && typeof model.image !== 'string') {
try {
return URL.createObjectURL(model.image as any);
} catch (e) {
return '';
}
}
return (model.image as string) || '';
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity"
onClick={onClose}
></div>
<div className="relative bg-white rounded-3xl w-full max-w-[350px] shadow-2xl animate-[fadeIn_0.3s_ease-out] overflow-hidden flex flex-col max-h-[90vh]">
<div className="overflow-y-auto p-5 custom-scrollbar">
<button
onClick={onClose}
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 w-8 h-8 flex items-center justify-center rounded-full hover:bg-gray-100 transition-colors z-10"
>
<i className="fas fa-times text-xl"></i>
</button>
<div className="text-center mb-6 mt-2">
<div className="w-16 h-16 mx-auto rounded-full p-1 bg-gradient-to-tr from-primary to-secondary mb-3 shadow-lg overflow-hidden">
<img
src={getModelImage()}
alt={model.name}
className="w-full h-full object-cover rounded-full border-2 border-white"
/>
</div>
<h3 className="text-xl font-black text-gray-800">{model.name}</h3>
</div>
<div className="relative flex bg-gray-100 p-1 rounded-xl mb-6 h-14 shadow-inner">
<div
className={`absolute top-1 bottom-1 w-[calc(50%-4px)] rounded-lg shadow-sm transition-all duration-300 ease-[cubic-bezier(0.25,0.1,0.25,1)]
${activeMode === 'vc'
? 'right-1 translate-x-0 bg-gradient-to-r from-blue-500 to-indigo-600'
: 'right-1 -translate-x-[100%] -ml-2 bg-gradient-to-r from-teal-400 to-emerald-500'
}`}
></div>
<button
onClick={() => setActiveMode('vc')}
className={`flex-1 relative z-10 text-sm font-bold text-center transition-colors duration-200 flex items-center justify-center gap-2
${activeMode === 'vc' ? 'text-white' : 'text-gray-600 hover:text-gray-800'}`}
>
<i className="fas fa-microphone-alt text-lg"></i> تغییر صدا
</button>
<button
onClick={() => setActiveMode('tts')}
className={`flex-1 relative z-10 text-sm font-bold text-center transition-colors duration-200 flex items-center justify-center gap-2
${activeMode === 'tts' ? 'text-white' : 'text-gray-600 hover:text-gray-800'}`}
>
<i className="fas fa-keyboard text-lg"></i> متن به صدا
</button>
</div>
<div className={`min-h-[160px] rounded-2xl p-2 border-2 transition-all duration-300 ${containerBorder}`}>
{activeMode === 'vc' ? (
<div className="animate-[fadeIn_0.3s_ease-out] h-full flex flex-col justify-center">
{!file && (
<div
onClick={() => inputRef.current?.click()}
className="flex-1 border-2 border-dashed border-blue-200 rounded-xl p-5 text-center cursor-pointer hover:border-blue-400 hover:bg-white/50 transition-all flex flex-col items-center justify-center min-h-[140px]"
>
<input
type="file"
ref={inputRef}
hidden
accept="audio/*"
onChange={(e) => e.target.files && e.target.files[0] && handleFileChange(e.target.files[0])}
/>
<div className="w-12 h-12 rounded-full mb-3 flex items-center justify-center bg-blue-100 text-blue-500">
<i className="fas fa-cloud-upload-alt text-xl"></i>
</div>
<p className="font-bold text-gray-700 text-sm mb-1 text-center leading-relaxed">برای انتخاب فایل صوتی کلیک کنید</p>
<p className="text-[10px] text-gray-400">یا صدای خود را ضبط کنید</p>
</div>
)}
{file && (
<div className="w-full animate-[fadeIn_0.3s_ease-out]">
<div className="text-center py-2">
<p className="text-xs text-gray-500 mb-2 flex items-center justify-center gap-2">
<i className="fas fa-headphones-alt text-primary"></i> پیش‌نمایش صدای شما
</p>
<audio
controls
src={fileUrl || ''}
className="w-full h-8 rounded-lg shadow-sm"
/>
<div className="flex items-center justify-between mt-2 px-1">
<span className="text-[10px] text-gray-400 truncate max-w-[150px] dir-ltr">{file.name}</span>
<button
onClick={() => { setFile(null); setPitch(null); }}
className="text-[10px] text-red-500 hover:text-red-700 font-bold"
>
<i className="fas fa-sync-alt mr-1"></i> تعویض فایل
</button>
</div>
</div>
{isLegacy && (
<div className="mt-2 animate-[fadeIn_0.3s_ease-out]">
<hr className="border-blue-200/50 mb-3" />
<div>
<p className="text-xs text-center text-gray-600 mb-3 font-bold">جنسیت صدای این فایل چیست؟</p>
<div className="flex gap-3">
<div
onClick={() => setPitchForGender('male')}
className={`flex-1 p-3 rounded-xl border-2 cursor-pointer transition-all flex flex-col items-center gap-2 shadow-sm
${isMaleSelected
? 'border-blue-500 bg-blue-600 text-white transform scale-105 shadow-blue-200'
: 'border-gray-200 bg-white text-gray-500 hover:border-blue-200 hover:bg-gray-50'}`}
>
<i className="fas fa-male text-2xl"></i>
<span className="text-xs font-bold">مرد</span>
</div>
<div
onClick={() => setPitchForGender('female')}
className={`flex-1 p-3 rounded-xl border-2 cursor-pointer transition-all flex flex-col items-center gap-2 shadow-sm
${isFemaleSelected
? 'border-pink-500 bg-pink-500 text-white transform scale-105 shadow-pink-200'
: 'border-gray-200 bg-white text-gray-500 hover:border-pink-200 hover:bg-gray-50'}`}
>
<i className="fas fa-female text-2xl"></i>
<span className="text-xs font-bold">زن</span>
</div>
</div>
</div>
</div>
)}
{!isLegacy && (
<p className="text-center text-[11px] text-gray-400 mt-4 animate-pulse">
برای شروع پردازش دکمه پایین را بزنید
</p>
)}
</div>
)}
</div>
) : (
<div className="animate-[fadeIn_0.3s_ease-out] h-full flex flex-col">
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="متن را اینجا بنویسید..."
className="flex-1 w-full p-4 rounded-xl border border-teal-200 focus:border-teal-500 focus:ring-2 focus:ring-teal-500/20 outline-none resize-none bg-white text-gray-700 text-sm leading-relaxed shadow-sm min-h-[140px]"
></textarea>
{/* شمارشگر و وضعیت کاراکترها */}
<div className="flex justify-between items-center mt-1.5 px-1 text-[10px]">
<span className={text.length > 500 && subscriptionStatus === 'free' ? 'text-red-500 font-bold' : 'text-gray-400'}>
تعداد حروف: {text.length}
</span>
{subscriptionStatus === 'free' && <span className="text-gray-400">حداکثر ۵۰۰ حرف برای نسخه رایگان</span>}
</div>
{/* باکس پیغام زیبای محدودیت کاراکتر و ارتقا */}
{text.length > 500 && subscriptionStatus === 'free' && (
<div className="mt-3 p-3 bg-amber-50 border border-amber-200 rounded-xl text-right animate-[slideUp_0.3s_ease-out]">
<div className="flex items-start gap-2">
<i className="fas fa-exclamation-triangle text-amber-500 mt-0.5 text-xs"></i>
<div className="flex-1">
<p className="text-[11px] font-black text-amber-900 leading-relaxed">تعداد حروف بیشتر از حد مجاز است!</p>
<p className="text-[10px] text-amber-700 mt-1 leading-relaxed">در نسخه رایگان حداکثر می‌توانید ۵۰۰ حرف را به گفتار تبدیل کنید. برای خروجی‌های طولانی‌تر لطفاً حساب خود را ارتقا دهید.</p>
</div>
</div>
<button
onClick={onUpgrade}
className="mt-2.5 w-full py-2.5 bg-gradient-to-r from-amber-500 via-yellow-500 to-orange-500 text-gray-950 rounded-lg font-bold text-[10px] flex items-center justify-center gap-1.5 shadow-sm hover:shadow-md transition-all active:scale-95"
>
<i className="fas fa-crown"></i> ارتقای حساب کاربری و دسترسی نامحدود
</button>
</div>
)}
</div>
)}
</div>
<button
onClick={handleConfirm}
disabled={!isReady || isLoading}
className={`w-full py-4 rounded-2xl font-bold text-white text-base shadow-md transition-all mt-6 transform active:scale-95
${(!isReady || isLoading)
? 'bg-gray-300 cursor-not-allowed shadow-none'
: `${activeColor} hover:shadow-lg shadow-blue-200`}`}
>
{isLoading ? (
<span className="flex items-center justify-center gap-2">
<i className="fas fa-circle-notch fa-spin"></i> در حال پردازش...
</span>
) : (
<span className="flex items-center justify-center gap-2">
<span>شروع پردازش</span>
<i className={`fas ${activeMode === 'vc' ? 'fa-magic' : 'fa-play'}`}></i>
</span>
)}
</button>
</div>
</div>
</div>
);
};
export default ModelModal;