Spaces:
Running
Running
Update src/App.tsx
Browse files- src/App.tsx +37 -44
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,59 +8,58 @@ 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 |
|
12 |
-
// کامپوننت مودال (
|
13 |
-
interface CustomModalProps {
|
14 |
-
isOpen: boolean;
|
15 |
-
onClose: () => void;
|
16 |
-
onSave: (name: string, instructions: string) => void;
|
17 |
-
initialName: string;
|
18 |
-
initialInstructions: string;
|
19 |
-
}
|
20 |
const CustomModal: FC<CustomModalProps> = ({ isOpen, onClose, onSave, initialName, initialInstructions }) => {
|
21 |
const [name, setName] = useState(initialName);
|
22 |
const [instructions, setInstructions] = useState(initialInstructions);
|
23 |
-
useEffect(() => {
|
24 |
-
if (isOpen) {
|
25 |
-
setName(initialName);
|
26 |
-
setInstructions(initialInstructions);
|
27 |
-
}
|
28 |
-
}, [isOpen, initialName, initialInstructions]);
|
29 |
if (!isOpen) return null;
|
30 |
-
const handleSave = () => {
|
31 |
-
|
32 |
-
onSave(name.trim(), instructions.trim());
|
33 |
-
onClose();
|
34 |
-
} else { alert("لطفا نام و دستورالعملها را وارد کنید."); }
|
35 |
-
};
|
36 |
-
return (
|
37 |
-
<div className="modal-overlay" onClick={onClose}>
|
38 |
-
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
39 |
-
<div className="modal-header"><h3>ساخت شخصیت اختصاصی</h3><button className="close-button" onClick={onClose}>×</button></div>
|
40 |
-
<div className="modal-body">
|
41 |
-
<div className="form-group"><label htmlFor="name">نام شما (برای صدا زدن)</label><input id="name" type="text" placeholder="مثلا: علی" value={name} onChange={(e) => setName(e.target.value)} /></div>
|
42 |
-
<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>
|
43 |
-
</div>
|
44 |
-
<div className="modal-footer"><button className="save-button" onClick={handleSave}>ذخیره و فعالسازی</button></div>
|
45 |
-
</div>
|
46 |
-
</div>
|
47 |
-
);
|
48 |
};
|
49 |
|
50 |
// کامپوننت منوی شخصیتها
|
51 |
-
const PersonalityMenu: React.FC<{ isOpen: boolean; onClose: () => void;
|
52 |
const menuRef = useRef<HTMLDivElement>(null);
|
53 |
-
const { selectedPersonality } = useAppContext();
|
54 |
const personalityIcons: Record<PersonalityType, string> = { default: "person", teacher: "school", poetic: "auto_awesome", funny: "sentiment_satisfied", custom: "tune" };
|
55 |
const personalityLabels: Record<PersonalityType, string> = { default: 'دستیار پیشفرض', teacher: 'استاد زبان', poetic: 'حس خوب', funny: 'شوخطبع', custom: 'شخصیت اختصاصی' };
|
|
|
56 |
useEffect(() => {
|
57 |
const handleClickOutside = (e: MouseEvent) => menuRef.current && !menuRef.current.contains(e.target as Node) && onClose();
|
58 |
if (isOpen) document.addEventListener('mousedown', handleClickOutside);
|
59 |
return () => document.removeEventListener('mousedown', handleClickOutside);
|
60 |
}, [isOpen, onClose]);
|
|
|
61 |
if (!isOpen) return null;
|
|
|
62 |
return (
|
63 |
-
<div ref={menuRef} className="personality-popover-wrapper open"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
);
|
65 |
};
|
66 |
|
@@ -86,9 +85,7 @@ const AppInternal: React.FC = () => {
|
|
86 |
|
87 |
useEffect(() => {
|
88 |
const handleClickOutside = (event: MouseEvent) => {
|
89 |
-
if (isNotificationOpen && notificationPopoverRef.current && !notificationPopoverRef.current.contains(event.target as Node) && notificationButtonRef.current && !notificationButtonRef.current.contains(event.target as Node))
|
90 |
-
setIsNotificationOpen(false);
|
91 |
-
}
|
92 |
};
|
93 |
document.addEventListener("mousedown", handleClickOutside);
|
94 |
return () => document.removeEventListener("mousedown", handleClickOutside);
|
@@ -99,22 +96,18 @@ const AppInternal: React.FC = () => {
|
|
99 |
<div className="main-wrapper">
|
100 |
<div className="header-controls">
|
101 |
<button aria-label="انتخاب شخصیت" className="header-icon-button" onClick={() => setIsPersonalityMenuOpen(v => !v)}>
|
102 |
-
{/* ✅ تغییر اصلی اینجاست */}
|
103 |
<span className="material-symbols-outlined">account_circle</span>
|
104 |
</button>
|
105 |
<button ref={notificationButtonRef} aria-label="اطلاعات" className="header-icon-button" onClick={() => setIsNotificationOpen(v => !v)}>
|
106 |
<span className="material-symbols-outlined">info</span>
|
107 |
</button>
|
108 |
</div>
|
109 |
-
|
110 |
<div ref={notificationPopoverRef} id="notification-popover-wrapper" className="notification-popover-wrapper">
|
111 |
<div id="notification-popover" className={cn("popover-content", { "open animate-popover-open-top-center": isNotificationOpen, "animate-popover-close-top-center": !isNotificationOpen && document.getElementById('notification-popover')?.classList.contains('open'), })}>
|
112 |
<div className="notification-popover-text-content">مدلهای هوش مصنوعی میتوانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از بیان اطلاعات حساس بپرهیزید.</div>
|
113 |
</div>
|
114 |
</div>
|
115 |
-
|
116 |
-
<PersonalityMenu isOpen={isPersonalityMenuOpen} onClose={() => setIsPersonalityMenuOpen(false)} onSelect={handleSelectPersonality} />
|
117 |
-
|
118 |
<div className="media-area">
|
119 |
<video id="video-feed" ref={videoRef} autoPlay playsInline className={cn({ hidden: !isCamActive }, { "scale-x-[-1]": currentFacingMode === 'user' })} />
|
120 |
{isMicActive && !isCamActive && <div id="large-logo-container"><Logo isMini={false} isActive={true} isAi={false} speakingVolume={volume} isUserSpeaking={isUserSpeaking} /></div>}
|
|
|
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; }
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
const CustomModal: FC<CustomModalProps> = ({ isOpen, onClose, onSave, initialName, initialInstructions }) => {
|
16 |
const [name, setName] = useState(initialName);
|
17 |
const [instructions, setInstructions] = useState(initialInstructions);
|
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}><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 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);
|
34 |
return () => document.removeEventListener('mousedown', handleClickOutside);
|
35 |
}, [isOpen, onClose]);
|
36 |
+
|
37 |
if (!isOpen) return null;
|
38 |
+
|
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)}>
|
45 |
+
<div><span className="material-symbols-outlined">{personalityIcons[key]}</span>{personalityLabels[key]}</div>
|
46 |
+
{selectedPersonality === key && <span className="material-symbols-outlined tick">done</span>}
|
47 |
+
</li>
|
48 |
+
))}
|
49 |
+
</ul>
|
50 |
+
{/* ✅ تغییر اصلی: اضافه شدن گالری گویندگان */}
|
51 |
+
<hr className="menu-divider" />
|
52 |
+
<h4 className="menu-subtitle">انتخاب گوینده</h4>
|
53 |
+
<div className="voice-grid">
|
54 |
+
{speakers.map(speaker => (
|
55 |
+
<div key={speaker.id} className={cn("voice-card", { active: selectedVoice === speaker.id })} onClick={() => changeVoice(speaker.id)} title={speaker.name}>
|
56 |
+
<img src={speaker.imgUrl} alt={speaker.name} loading="lazy" />
|
57 |
+
<div className="voice-name">{speaker.name.split(' ')[0]}</div>
|
58 |
+
</div>
|
59 |
+
))}
|
60 |
+
</div>
|
61 |
+
</div>
|
62 |
+
</div>
|
63 |
);
|
64 |
};
|
65 |
|
|
|
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);
|
|
|
|
|
89 |
};
|
90 |
document.addEventListener("mousedown", handleClickOutside);
|
91 |
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
|
96 |
<div className="main-wrapper">
|
97 |
<div className="header-controls">
|
98 |
<button aria-label="انتخاب شخصیت" className="header-icon-button" onClick={() => setIsPersonalityMenuOpen(v => !v)}>
|
|
|
99 |
<span className="material-symbols-outlined">account_circle</span>
|
100 |
</button>
|
101 |
<button ref={notificationButtonRef} aria-label="اطلاعات" className="header-icon-button" onClick={() => setIsNotificationOpen(v => !v)}>
|
102 |
<span className="material-symbols-outlined">info</span>
|
103 |
</button>
|
104 |
</div>
|
|
|
105 |
<div ref={notificationPopoverRef} id="notification-popover-wrapper" className="notification-popover-wrapper">
|
106 |
<div id="notification-popover" className={cn("popover-content", { "open animate-popover-open-top-center": isNotificationOpen, "animate-popover-close-top-center": !isNotificationOpen && document.getElementById('notification-popover')?.classList.contains('open'), })}>
|
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>}
|