Spaces:
Running
Running
Update src/App.tsx
Browse files- src/App.tsx +56 -11
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";
|
@@ -8,7 +8,7 @@ import { isIOS } from "./lib/platform";
|
|
8 |
import cn from "classnames";
|
9 |
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; }
|
@@ -22,12 +22,14 @@ const CustomModal: FC<CustomModalProps> = ({ isOpen, onClose, onSave, initialNam
|
|
22 |
};
|
23 |
|
24 |
// کامپوننت منوی شخصیتها
|
25 |
-
const PersonalityMenu: React.FC<{ isOpen: boolean; onClose: () => void; onSelectPersonality: (p: PersonalityType) => void; }> = ({ isOpen, onClose, onSelectPersonality }) => {
|
26 |
const menuRef = useRef<HTMLDivElement>(null);
|
27 |
const { selectedPersonality, selectedVoice, changeVoice } = useAppContext();
|
28 |
const personalityIcons: Record<PersonalityType, string> = { default: "person", teacher: "school", poetic: "auto_awesome", funny: "sentiment_satisfied", custom: "tune" };
|
29 |
const personalityLabels: Record<PersonalityType, string> = { default: 'دستیار پیشفرض', teacher: 'استاد زبان', poetic: 'حس خوب', funny: 'شوخطبع', custom: 'شخصیت اختصاصی' };
|
30 |
|
|
|
|
|
31 |
useEffect(() => {
|
32 |
const handleClickOutside = (e: MouseEvent) => menuRef.current && !menuRef.current.contains(e.target as Node) && onClose();
|
33 |
if (isOpen) document.addEventListener('mousedown', handleClickOutside);
|
@@ -39,6 +41,17 @@ const PersonalityMenu: React.FC<{ isOpen: boolean; onClose: () => void; onSelect
|
|
39 |
return (
|
40 |
<div ref={menuRef} className="personality-popover-wrapper open">
|
41 |
<div className="popover-content">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
<ul>
|
43 |
{(Object.keys(personalityIcons) as PersonalityType[]).map(key => (
|
44 |
<li key={key} className={cn({ active: selectedPersonality === key })} onClick={() => onSelectPersonality(key)}>
|
@@ -47,7 +60,6 @@ const PersonalityMenu: React.FC<{ isOpen: boolean; onClose: () => void; onSelect
|
|
47 |
</li>
|
48 |
))}
|
49 |
</ul>
|
50 |
-
{/* ✅ تغییر اصلی: اضافه شدن گالری گویندگان */}
|
51 |
<hr className="menu-divider" />
|
52 |
<h4 className="menu-subtitle">انتخاب گوینده</h4>
|
53 |
<div className="voice-grid">
|
@@ -66,7 +78,7 @@ const PersonalityMenu: React.FC<{ isOpen: boolean; onClose: () => void; onSelect
|
|
66 |
// کامپوننت داخلی اصلی برنامه
|
67 |
const AppInternal: React.FC = () => {
|
68 |
const videoRef = useRef<HTMLVideoElement>(null);
|
69 |
-
const { volume, changePersonality, customUserName, customInstructions } = useAppContext();
|
70 |
const [isUserSpeaking, setIsUserSpeaking] = useState(false);
|
71 |
const [isMicActive, setIsMicActive] = useState(false);
|
72 |
const [isCamActive, setIsCamActive] = useState(false);
|
@@ -76,13 +88,40 @@ const AppInternal: React.FC = () => {
|
|
76 |
const [isCustomModalOpen, setIsCustomModalOpen] = useState(false);
|
77 |
const notificationButtonRef = useRef<HTMLButtonElement>(null);
|
78 |
const notificationPopoverRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
|
80 |
const handleSelectPersonality = (p: PersonalityType) => {
|
81 |
setIsPersonalityMenuOpen(false);
|
82 |
-
if (p === 'custom')
|
83 |
-
|
|
|
|
|
|
|
|
|
|
|
84 |
};
|
85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
useEffect(() => {
|
87 |
const handleClickOutside = (event: MouseEvent) => {
|
88 |
if (isNotificationOpen && notificationPopoverRef.current && !notificationPopoverRef.current.contains(event.target as Node) && notificationButtonRef.current && !notificationButtonRef.current.contains(event.target as Node)) setIsNotificationOpen(false);
|
@@ -107,19 +146,25 @@ const AppInternal: React.FC = () => {
|
|
107 |
<div className="notification-popover-text-content">مدلهای هوش مصنوعی میتوانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از بیان اطلاعات حساس بپرهیزید.</div>
|
108 |
</div>
|
109 |
</div>
|
110 |
-
<PersonalityMenu isOpen={isPersonalityMenuOpen} onClose={() => setIsPersonalityMenuOpen(false)} onSelectPersonality={handleSelectPersonality} />
|
111 |
<div className="media-area">
|
112 |
<video id="video-feed" ref={videoRef} autoPlay playsInline className={cn({ hidden: !isCamActive }, { "scale-x-[-1]": currentFacingMode === 'user' })} />
|
113 |
{isMicActive && !isCamActive && <div id="large-logo-container"><Logo isMini={false} isActive={true} isAi={false} speakingVolume={volume} isUserSpeaking={isUserSpeaking} /></div>}
|
114 |
</div>
|
115 |
<ControlTray videoRef={videoRef} onUserSpeakingChange={setIsUserSpeaking} isAppMicActive={isMicActive} onAppMicToggle={setIsMicActive} isAppCamActive={isCamActive} onAppCamToggle={setIsCamActive} currentFacingMode={currentFacingMode} onFacingModeChange={setCurrentFacingMode} />
|
116 |
</div>
|
117 |
-
<CustomModal isOpen={isCustomModalOpen} onClose={() => setIsCustomModalOpen(false)} onSave={
|
|
|
|
|
|
|
|
|
|
|
|
|
118 |
</>
|
119 |
);
|
120 |
};
|
121 |
|
122 |
-
// کامپوننت ریشه App
|
123 |
function App() {
|
124 |
const [showIOSModal, setShowIOSModal] = useState(false);
|
125 |
const [personalityInstructions, setPersonalityInstructions] = useState<PersonalityInstructions | null>(null);
|
|
|
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";
|
|
|
8 |
import cn from "classnames";
|
9 |
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; }
|
|
|
22 |
};
|
23 |
|
24 |
// کامپوننت منوی شخصیتها
|
25 |
+
const PersonalityMenu: React.FC<{ isOpen: boolean; onClose: () => void; onSelectPersonality: (p: PersonalityType) => void; volume: number; }> = ({ isOpen, onClose, onSelectPersonality, volume }) => {
|
26 |
const menuRef = useRef<HTMLDivElement>(null);
|
27 |
const { selectedPersonality, selectedVoice, changeVoice } = useAppContext();
|
28 |
const personalityIcons: Record<PersonalityType, string> = { default: "person", teacher: "school", poetic: "auto_awesome", funny: "sentiment_satisfied", custom: "tune" };
|
29 |
const personalityLabels: Record<PersonalityType, string> = { default: 'دستیار پیشفرض', teacher: 'استاد زبان', poetic: 'حس خوب', funny: 'شوخطبع', custom: 'شخصیت اختصاصی' };
|
30 |
|
31 |
+
const selectedSpeaker = speakers.find(s => s.id === selectedVoice) || speakers[0];
|
32 |
+
|
33 |
useEffect(() => {
|
34 |
const handleClickOutside = (e: MouseEvent) => menuRef.current && !menuRef.current.contains(e.target as Node) && onClose();
|
35 |
if (isOpen) document.addEventListener('mousedown', handleClickOutside);
|
|
|
41 |
return (
|
42 |
<div ref={menuRef} className="personality-popover-wrapper open">
|
43 |
<div className="popover-content">
|
44 |
+
{/* ✅ بخش جدید: پیشنمایش گوینده منتخب */}
|
45 |
+
<div className={cn("selected-voice-display", { speaking: volume > 0.01 })}>
|
46 |
+
<div className="voice-image-wrapper">
|
47 |
+
<img src={selectedSpeaker.imgUrl} alt={selectedSpeaker.name} />
|
48 |
+
</div>
|
49 |
+
<h3>{selectedSpeaker.name}</h3>
|
50 |
+
<p>{selectedSpeaker.desc}</p>
|
51 |
+
</div>
|
52 |
+
<hr className="menu-divider" />
|
53 |
+
|
54 |
+
<h4 className="menu-subtitle">انتخاب شخصیت</h4>
|
55 |
<ul>
|
56 |
{(Object.keys(personalityIcons) as PersonalityType[]).map(key => (
|
57 |
<li key={key} className={cn({ active: selectedPersonality === key })} onClick={() => onSelectPersonality(key)}>
|
|
|
60 |
</li>
|
61 |
))}
|
62 |
</ul>
|
|
|
63 |
<hr className="menu-divider" />
|
64 |
<h4 className="menu-subtitle">انتخاب گوینده</h4>
|
65 |
<div className="voice-grid">
|
|
|
78 |
// کامپوننت داخلی اصلی برنامه
|
79 |
const AppInternal: React.FC = () => {
|
80 |
const videoRef = useRef<HTMLVideoElement>(null);
|
81 |
+
const { volume, changePersonality, customUserName, customInstructions, selectedVoice } = useAppContext();
|
82 |
const [isUserSpeaking, setIsUserSpeaking] = useState(false);
|
83 |
const [isMicActive, setIsMicActive] = useState(false);
|
84 |
const [isCamActive, setIsCamActive] = useState(false);
|
|
|
88 |
const [isCustomModalOpen, setIsCustomModalOpen] = useState(false);
|
89 |
const notificationButtonRef = useRef<HTMLButtonElement>(null);
|
90 |
const notificationPopoverRef = useRef<HTMLDivElement>(null);
|
91 |
+
const [confirmationMessage, setConfirmationMessage] = useState<string | null>(null);
|
92 |
+
|
93 |
+
// ✅ منطق جدید برای پیغام تایید
|
94 |
+
const showConfirmation = (message: string) => {
|
95 |
+
setConfirmationMessage(message);
|
96 |
+
setTimeout(() => setConfirmationMessage(null), 3000); // پیغام بعد از ۳ ثانیه محو میشود
|
97 |
+
};
|
98 |
|
99 |
const handleSelectPersonality = (p: PersonalityType) => {
|
100 |
setIsPersonalityMenuOpen(false);
|
101 |
+
if (p === 'custom') {
|
102 |
+
setIsCustomModalOpen(true);
|
103 |
+
} else {
|
104 |
+
changePersonality(p);
|
105 |
+
const personalityLabels: Record<PersonalityType, string> = { default: 'دستیار پیشفرض', teacher: 'استاد زبان', poetic: 'حس خوب', funny: 'شوخطبع', custom: 'شخصیت اختصاصی' };
|
106 |
+
showConfirmation(`شخصیت به ${personalityLabels[p]} تغییر کرد`);
|
107 |
+
}
|
108 |
};
|
109 |
+
|
110 |
+
const handleSaveCustom = (name: string, instructions: string) => {
|
111 |
+
changePersonality('custom', { name, instructions });
|
112 |
+
showConfirmation("شخصیت اختصاصی با موفقیت ذخیره شد");
|
113 |
+
};
|
114 |
+
|
115 |
+
// ✅ هوک برای نمایش پیغام تایید هنگام تغییر گوینده
|
116 |
+
useEffect(() => {
|
117 |
+
if(isPersonalityMenuOpen) return; // جلوگیری از نمایش هنگام باز بودن منو
|
118 |
+
const speaker = speakers.find(s => s.id === selectedVoice);
|
119 |
+
if (speaker) {
|
120 |
+
showConfirmation(`گوینده به ${speaker.name} تغییر کرد`);
|
121 |
+
}
|
122 |
+
}, [selectedVoice]);
|
123 |
+
|
124 |
+
|
125 |
useEffect(() => {
|
126 |
const handleClickOutside = (event: MouseEvent) => {
|
127 |
if (isNotificationOpen && notificationPopoverRef.current && !notificationPopoverRef.current.contains(event.target as Node) && notificationButtonRef.current && !notificationButtonRef.current.contains(event.target as Node)) setIsNotificationOpen(false);
|
|
|
146 |
<div className="notification-popover-text-content">مدلهای هوش مصنوعی میتوانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از بیان اطلاعات حساس بپرهیزید.</div>
|
147 |
</div>
|
148 |
</div>
|
149 |
+
<PersonalityMenu isOpen={isPersonalityMenuOpen} onClose={() => setIsPersonalityMenuOpen(false)} onSelectPersonality={handleSelectPersonality} volume={volume} />
|
150 |
<div className="media-area">
|
151 |
<video id="video-feed" ref={videoRef} autoPlay playsInline className={cn({ hidden: !isCamActive }, { "scale-x-[-1]": currentFacingMode === 'user' })} />
|
152 |
{isMicActive && !isCamActive && <div id="large-logo-container"><Logo isMini={false} isActive={true} isAi={false} speakingVolume={volume} isUserSpeaking={isUserSpeaking} /></div>}
|
153 |
</div>
|
154 |
<ControlTray videoRef={videoRef} onUserSpeakingChange={setIsUserSpeaking} isAppMicActive={isMicActive} onAppMicToggle={setIsMicActive} isAppCamActive={isCamActive} onAppCamToggle={setIsCamActive} currentFacingMode={currentFacingMode} onFacingModeChange={setCurrentFacingMode} />
|
155 |
</div>
|
156 |
+
<CustomModal isOpen={isCustomModalOpen} onClose={() => setIsCustomModalOpen(false)} onSave={handleSaveCustom} initialName={customUserName} initialInstructions={customInstructions} />
|
157 |
+
{/* ✅ JSX برای نمایش پیغام تایید */}
|
158 |
+
{confirmationMessage && (
|
159 |
+
<div className="confirmation-toast">
|
160 |
+
{confirmationMessage}
|
161 |
+
</div>
|
162 |
+
)}
|
163 |
</>
|
164 |
);
|
165 |
};
|
166 |
|
167 |
+
// کامپوننت ریشه App (بدون تغییر)
|
168 |
function App() {
|
169 |
const [showIOSModal, setShowIOSModal] = useState(false);
|
170 |
const [personalityInstructions, setPersonalityInstructions] = useState<PersonalityInstructions | null>(null);
|