Ezmary commited on
Commit
7295bbe
·
verified ·
1 Parent(s): 844f21c

Update src/App.tsx

Browse files
Files changed (1) hide show
  1. 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
- if (name.trim() && instructions.trim()) {
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; onSelect: (p: PersonalityType) => void; }> = ({ isOpen, onClose, onSelect }) => {
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"><div className="popover-content"><ul>{(Object.keys(personalityIcons) as PersonalityType[]).map(key => (<li key={key} className={cn({ active: selectedPersonality === key })} onClick={() => onSelect(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></div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>}