Spaces:
Running
Running
Update src/App.tsx
Browse files- src/App.tsx +50 -39
src/App.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
// src/App.tsx (نسخه نهایی با رفع کامل مشکل ترجمه
|
2 |
import React, { useEffect, useRef, useState, FC } from "react";
|
3 |
import './App.scss';
|
4 |
import { AppProvider, useAppContext, PersonalityType, PersonalityInstructions } from "./contexts/AppContext";
|
@@ -10,7 +10,7 @@ import Logo from "./components/logo/Logo";
|
|
10 |
import { LiveConfig } from "./multimodal-live-types";
|
11 |
import { speakers } from "./data/speakers";
|
12 |
|
13 |
-
// کامپوننت مودال
|
14 |
interface CustomModalProps { isOpen: boolean; onClose: () => void; onSave: (name: string, instructions: string) => void; initialName: string; initialInstructions: string; }
|
15 |
const CustomModal: FC<CustomModalProps> = ({ isOpen, onClose, onSave, initialName, initialInstructions }) => {
|
16 |
const [name, setName] = useState(initialName);
|
@@ -18,17 +18,26 @@ const CustomModal: FC<CustomModalProps> = ({ isOpen, onClose, onSave, initialNam
|
|
18 |
useEffect(() => { if (isOpen) { setName(initialName); setInstructions(initialInstructions); } }, [isOpen, initialName, initialInstructions]);
|
19 |
if (!isOpen) return null;
|
20 |
const handleSave = () => { if (name.trim() && instructions.trim()) { onSave(name.trim(), instructions.trim()); onClose(); } else { alert("لطفا نام و دستورالعملها را وارد کنید."); } };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
return (
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
<div className="modal-header"><h3>ساخت شخصیت اختصاصی</h3><button className="close-button" onClick={onClose}>×</button></div>
|
26 |
-
<div className="modal-body">
|
27 |
-
<div className="form-group"><label htmlFor="name">نام شما</label><input id="name" type="text" placeholder="مثلا: حامد" value={name} onChange={(e) => setName(e.target.value)} /></div>
|
28 |
-
<div className="form-group"><label htmlFor="instructions">دستورالعملها</label><textarea id="instructions" placeholder="مثلا: خیلی خودمونی صحبت کن..." value={instructions} onChange={(e) => setInstructions(e.target.value)} rows={6} /><small>اینجا فقط سبک صحبت کردن را مشخص کنید.</small></div>
|
29 |
-
</div>
|
30 |
-
<div className="modal-footer"><button className="save-button" onClick={handleSave}>ذخیره</button></div>
|
31 |
-
</div>
|
32 |
</div>
|
33 |
);
|
34 |
};
|
@@ -49,39 +58,23 @@ const PersonalityMenu: React.FC<{ isOpen: boolean; onClose: () => void; onSelect
|
|
49 |
return (
|
50 |
<div ref={menuRef} className="personality-popover-wrapper open" translate="no">
|
51 |
<div className="popover-content">
|
52 |
-
<div className={cn("selected-voice-display", { speaking: volume > 0.01 })}>
|
53 |
-
<div className="voice-image-wrapper"><img src={selectedSpeaker.imgUrl} alt={selectedSpeaker.name} /></div>
|
54 |
-
<h3>{selectedSpeaker.name}</h3><p>{selectedSpeaker.desc}</p>
|
55 |
-
</div>
|
56 |
<hr className="menu-divider" />
|
57 |
<h4 className="menu-subtitle">انتخاب شخصیت</h4>
|
58 |
-
<ul>
|
59 |
-
|
60 |
-
<li key={key} className={cn({ active: selectedPersonality === key })} onClick={() => onSelectPersonality(key)}>
|
61 |
-
<div><span className="material-symbols-outlined">{personalityIcons[key]}</span>{personalityLabels[key]}</div>
|
62 |
-
{selectedPersonality === key && <span className="material-symbols-outlined tick">done</span>}
|
63 |
-
</li>
|
64 |
-
))}
|
65 |
-
</ul>
|
66 |
<hr className="menu-divider" />
|
67 |
<h4 className="menu-subtitle">انتخاب گوینده</h4>
|
68 |
-
<div className="voice-grid">
|
69 |
-
{speakers.map(speaker => (
|
70 |
-
<div key={speaker.id} className={cn("voice-card", { active: selectedVoice === speaker.id })} onClick={() => changeVoice(speaker.id)} title={speaker.name}>
|
71 |
-
<img src={speaker.imgUrl} alt={speaker.name} loading="lazy" />
|
72 |
-
<div className="voice-name">{speaker.name.split(' ')[0]}</div>
|
73 |
-
</div>
|
74 |
-
))}
|
75 |
-
</div>
|
76 |
</div>
|
77 |
</div>
|
78 |
);
|
79 |
};
|
80 |
|
81 |
-
// کامپوننت داخلی اصلی برنامه
|
82 |
const AppInternal: React.FC = () => {
|
83 |
const videoRef = useRef<HTMLVideoElement>(null);
|
84 |
-
const { volume, changePersonality, customUserName, customInstructions } = useAppContext();
|
85 |
const [isUserSpeaking, setIsUserSpeaking] = useState(false);
|
86 |
const [isMicActive, setIsMicActive] = useState(false);
|
87 |
const [isCamActive, setIsCamActive] = useState(false);
|
@@ -98,6 +91,10 @@ const AppInternal: React.FC = () => {
|
|
98 |
const handleSaveCustom = (name: string, instructions: string) => {
|
99 |
changePersonality('custom', { name, instructions });
|
100 |
};
|
|
|
|
|
|
|
|
|
101 |
useEffect(() => {
|
102 |
const handleClickOutside = (event: MouseEvent) => {
|
103 |
if (isNotificationOpen && notificationPopoverRef.current && !notificationPopoverRef.current.contains(event.target as Node) && notificationButtonRef.current && !notificationButtonRef.current.contains(event.target as Node)) setIsNotificationOpen(false);
|
@@ -105,6 +102,7 @@ const AppInternal: React.FC = () => {
|
|
105 |
document.addEventListener("mousedown", handleClickOutside);
|
106 |
return () => document.removeEventListener("mousedown", handleClickOutside);
|
107 |
}, [isNotificationOpen]);
|
|
|
108 |
return (
|
109 |
<>
|
110 |
<div className="main-wrapper">
|
@@ -118,11 +116,24 @@ const AppInternal: React.FC = () => {
|
|
118 |
</div>
|
119 |
</div>
|
120 |
<PersonalityMenu isOpen={isPersonalityMenuOpen} onClose={() => setIsPersonalityMenuOpen(false)} onSelectPersonality={handleSelectPersonality} volume={volume} />
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
126 |
</div>
|
127 |
<CustomModal isOpen={isCustomModalOpen} onClose={() => setIsCustomModalOpen(false)} onSave={handleSaveCustom} initialName={customUserName} initialInstructions={customInstructions} />
|
128 |
</>
|
|
|
1 |
+
// src/App.tsx (نسخه نهایی با رفع کامل مشکل ترجمه همه بخشها)
|
2 |
import React, { useEffect, useRef, useState, FC } from "react";
|
3 |
import './App.scss';
|
4 |
import { AppProvider, useAppContext, PersonalityType, PersonalityInstructions } from "./contexts/AppContext";
|
|
|
10 |
import { LiveConfig } from "./multimodal-live-types";
|
11 |
import { speakers } from "./data/speakers";
|
12 |
|
13 |
+
// کامپوننت مودال (بدون تغییر)
|
14 |
interface CustomModalProps { isOpen: boolean; onClose: () => void; onSave: (name: string, instructions: string) => void; initialName: string; initialInstructions: string; }
|
15 |
const CustomModal: FC<CustomModalProps> = ({ isOpen, onClose, onSave, initialName, initialInstructions }) => {
|
16 |
const [name, setName] = useState(initialName);
|
|
|
18 |
useEffect(() => { if (isOpen) { setName(initialName); setInstructions(initialInstructions); } }, [isOpen, initialName, initialInstructions]);
|
19 |
if (!isOpen) return null;
|
20 |
const handleSave = () => { if (name.trim() && instructions.trim()) { onSave(name.trim(), instructions.trim()); onClose(); } else { alert("لطفا نام و دستورالعملها را وارد کنید."); } };
|
21 |
+
return ( <div className="modal-overlay" onClick={onClose} translate="no"><div className="modal-content" onClick={(e) => e.stopPropagation()}><div className="modal-header"><h3>ساخت شخصیت اختصاصی</h3><button className="close-button" onClick={onClose}>×</button></div><div className="modal-body"><div className="form-group"><label htmlFor="name">نام شما</label><input id="name" type="text" placeholder="مثلا: حامد" value={name} onChange={(e) => setName(e.target.value)} /></div><div className="form-group"><label htmlFor="instructions">دستورالعملها</label><textarea id="instructions" placeholder="مثلا: خیلی خودمونی صحبت کن..." value={instructions} onChange={(e) => setInstructions(e.target.value)} rows={6} /><small>اینجا فقط سبک صحبت کردن را مشخص کنید.</small></div></div><div className="modal-footer"><button className="save-button" onClick={handleSave}>ذخیره</button></div></div></div> );
|
22 |
+
};
|
23 |
+
|
24 |
+
// کامپوننت تایمر
|
25 |
+
const TimerDisplay: React.FC = () => {
|
26 |
+
const { remainingTime, isTimeUp, isTimerActive } = useAppContext();
|
27 |
+
const minutes = Math.floor(remainingTime / 60);
|
28 |
+
const seconds = remainingTime % 60;
|
29 |
+
const progressPercent = (remainingTime / 900) * 100;
|
30 |
+
if (isTimeUp) {
|
31 |
+
return (
|
32 |
+
<div className="timer-section time-up">
|
33 |
+
<p>برای استفاده نامحدود، از نسخه پیشرفته استفاده کنید.</p>
|
34 |
+
</div>
|
35 |
+
);
|
36 |
+
}
|
37 |
return (
|
38 |
+
<div className="timer-section">
|
39 |
+
<div className="timer-text"><span>زمان باقیمانده روزانه</span><span>{minutes}:{seconds < 10 ? `0${seconds}` : seconds}</span></div>
|
40 |
+
<div className="timer-bar"><div className={cn("timer-bar-fill", { active: isTimerActive })} style={{ width: `${progressPercent}%` }}/></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
</div>
|
42 |
);
|
43 |
};
|
|
|
58 |
return (
|
59 |
<div ref={menuRef} className="personality-popover-wrapper open" translate="no">
|
60 |
<div className="popover-content">
|
61 |
+
<div className={cn("selected-voice-display", { speaking: volume > 0.01 })}><div className="voice-image-wrapper"><img src={selectedSpeaker.imgUrl} alt={selectedSpeaker.name} /></div><h3>{selectedSpeaker.name}</h3><p>{selectedSpeaker.desc}</p></div>
|
|
|
|
|
|
|
62 |
<hr className="menu-divider" />
|
63 |
<h4 className="menu-subtitle">انتخاب شخصیت</h4>
|
64 |
+
<ul>{(Object.keys(personalityIcons) as PersonalityType[]).map(key => (<li key={key} className={cn({ active: selectedPersonality === key })} onClick={() => onSelectPersonality(key)}><div><span className="material-symbols-outlined">{personalityIcons[key]}</span>{personalityLabels[key]}</div>{selectedPersonality === key && <span className="material-symbols-outlined tick">done</span>}</li>))}</ul>
|
65 |
+
<TimerDisplay />
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
<hr className="menu-divider" />
|
67 |
<h4 className="menu-subtitle">انتخاب گوینده</h4>
|
68 |
+
<div className="voice-grid">{speakers.map(speaker => (<div key={speaker.id} className={cn("voice-card", { active: selectedVoice === speaker.id })} onClick={() => changeVoice(speaker.id)} title={speaker.name}><img src={speaker.imgUrl} alt={speaker.name} loading="lazy" /><div className="voice-name">{speaker.name.split(' ')[0]}</div></div>))}</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
</div>
|
70 |
</div>
|
71 |
);
|
72 |
};
|
73 |
|
74 |
+
// کامپوننت داخلی اصلی برنامه
|
75 |
const AppInternal: React.FC = () => {
|
76 |
const videoRef = useRef<HTMLVideoElement>(null);
|
77 |
+
const { volume, changePersonality, customUserName, customInstructions, startTimer, stopTimer, connected, isTimeUp } = useAppContext();
|
78 |
const [isUserSpeaking, setIsUserSpeaking] = useState(false);
|
79 |
const [isMicActive, setIsMicActive] = useState(false);
|
80 |
const [isCamActive, setIsCamActive] = useState(false);
|
|
|
91 |
const handleSaveCustom = (name: string, instructions: string) => {
|
92 |
changePersonality('custom', { name, instructions });
|
93 |
};
|
94 |
+
useEffect(() => {
|
95 |
+
if (connected && isMicActive && !isTimeUp) { startTimer(); } else { stopTimer(); }
|
96 |
+
return () => stopTimer();
|
97 |
+
}, [connected, isMicActive, isTimeUp, startTimer, stopTimer]);
|
98 |
useEffect(() => {
|
99 |
const handleClickOutside = (event: MouseEvent) => {
|
100 |
if (isNotificationOpen && notificationPopoverRef.current && !notificationPopoverRef.current.contains(event.target as Node) && notificationButtonRef.current && !notificationButtonRef.current.contains(event.target as Node)) setIsNotificationOpen(false);
|
|
|
102 |
document.addEventListener("mousedown", handleClickOutside);
|
103 |
return () => document.removeEventListener("mousedown", handleClickOutside);
|
104 |
}, [isNotificationOpen]);
|
105 |
+
|
106 |
return (
|
107 |
<>
|
108 |
<div className="main-wrapper">
|
|
|
116 |
</div>
|
117 |
</div>
|
118 |
<PersonalityMenu isOpen={isPersonalityMenuOpen} onClose={() => setIsPersonalityMenuOpen(false)} onSelectPersonality={handleSelectPersonality} volume={volume} />
|
119 |
+
|
120 |
+
{isTimeUp ? (
|
121 |
+
// ✅ تغییر اصلی اینجاست: اضافه کردن translate="no" به کل صفحه اتمام زمان
|
122 |
+
<div className="time-up-overlay" translate="no">
|
123 |
+
<div className="time-up-card">
|
124 |
+
<span className="material-symbols-outlined medal-icon" translate="no">workspace_premium</span>
|
125 |
+
<h2>زمان شما به پایان رسید</h2>
|
126 |
+
<p>زمان استفاده رایگان 15 دقیقه روزانه به پایان رسیده است. برای استفاده نامحدود، حساب خود را ارتقا دهید و از نسخه پیشرفته بصورت نامحدود استفاده کنید.</p>
|
127 |
+
</div>
|
128 |
+
</div>
|
129 |
+
) : (
|
130 |
+
<div className="media-area">
|
131 |
+
<video id="video-feed" ref={videoRef} autoPlay playsInline className={cn({ hidden: !isCamActive }, { "scale-x-[-1]": currentFacingMode === 'user' })} />
|
132 |
+
{isMicActive && !isCamActive && <div id="large-logo-container"><Logo isMini={false} isActive={true} isAi={false} speakingVolume={volume} isUserSpeaking={isUserSpeaking} /></div>}
|
133 |
+
</div>
|
134 |
+
)}
|
135 |
+
|
136 |
+
<ControlTray videoRef={videoRef} onUserSpeakingChange={setIsUserSpeaking} isAppMicActive={isMicActive} onAppMicToggle={setIsMicActive} isAppCamActive={isCamActive} onAppCamToggle={setIsCamActive} currentFacingMode={currentFacingMode} onFacingModeChange={setCurrentFacingMode} isTimeUp={isTimeUp} />
|
137 |
</div>
|
138 |
<CustomModal isOpen={isCustomModalOpen} onClose={() => setIsCustomModalOpen(false)} onSave={handleSaveCustom} initialName={customUserName} initialInstructions={customInstructions} />
|
139 |
</>
|