Ezmary commited on
Commit
a70e3bb
·
verified ·
1 Parent(s): f29f422

Update src/App.tsx

Browse files
Files changed (1) hide show
  1. src/App.tsx +29 -26
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";
@@ -39,7 +39,7 @@ const CustomModal: FC<CustomModalProps> = ({ isOpen, onClose, onSave, initialNam
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>
@@ -60,18 +60,7 @@ const PersonalityMenu: React.FC<{ isOpen: boolean; onClose: () => void; onSelect
60
  }, [isOpen, onClose]);
61
  if (!isOpen) return null;
62
  return (
63
- <div ref={menuRef} className="personality-popover-wrapper open">
64
- <div className="popover-content">
65
- <ul>
66
- {(Object.keys(personalityIcons) as PersonalityType[]).map(key => (
67
- <li key={key} className={cn({ active: selectedPersonality === key })} onClick={() => onSelect(key)}>
68
- <div><span className="material-symbols-outlined">{personalityIcons[key]}</span>{personalityLabels[key]}</div>
69
- {selectedPersonality === key && <span className="material-symbols-outlined tick">done</span>}
70
- </li>
71
- ))}
72
- </ul>
73
- </div>
74
- </div>
75
  );
76
  };
77
 
@@ -86,30 +75,49 @@ const AppInternal: React.FC = () => {
86
  const [isNotificationOpen, setIsNotificationOpen] = useState(false);
87
  const [isPersonalityMenuOpen, setIsPersonalityMenuOpen] = useState(false);
88
  const [isCustomModalOpen, setIsCustomModalOpen] = useState(false);
 
 
 
89
  const handleSelectPersonality = (p: PersonalityType) => {
90
  setIsPersonalityMenuOpen(false);
91
  if (p === 'custom') setIsCustomModalOpen(true);
92
  else changePersonality(p);
93
  };
 
 
 
 
 
 
 
 
 
 
 
94
  return (
95
  <>
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">psychology</span>
100
  </button>
101
- <button aria-label="اطلاعات" className="header-icon-button" onClick={() => setIsNotificationOpen(v => !v)}>
102
  <span className="material-symbols-outlined">info</span>
103
  </button>
104
  </div>
105
- {isNotificationOpen && <div className="notification-popover-wrapper open"><div className="popover-content">مدل‌های هوش مصنوعی می‌توانند اشتباه کنند.</div></div>}
 
 
 
 
 
 
106
  <PersonalityMenu isOpen={isPersonalityMenuOpen} onClose={() => setIsPersonalityMenuOpen(false)} onSelect={handleSelectPersonality} />
107
 
108
- <div className="media-area"> {/* ✅ این بخش برای نمایش ویدیو و لوگو ضروری است */}
109
  <video id="video-feed" ref={videoRef} autoPlay playsInline className={cn({ hidden: !isCamActive }, { "scale-x-[-1]": currentFacingMode === 'user' })} />
110
  {isMicActive && !isCamActive && <div id="large-logo-container"><Logo isMini={false} isActive={true} isAi={false} speakingVolume={volume} isUserSpeaking={isUserSpeaking} /></div>}
111
  </div>
112
-
113
  <ControlTray videoRef={videoRef} onUserSpeakingChange={setIsUserSpeaking} isAppMicActive={isMicActive} onAppMicToggle={setIsMicActive} isAppCamActive={isCamActive} onAppCamToggle={setIsCamActive} currentFacingMode={currentFacingMode} onFacingModeChange={setCurrentFacingMode} />
114
  </div>
115
  <CustomModal isOpen={isCustomModalOpen} onClose={() => setIsCustomModalOpen(false)} onSave={(name, instructions) => changePersonality('custom', { name, instructions })} initialName={customUserName} initialInstructions={customInstructions} />
@@ -129,9 +137,7 @@ function App() {
129
  const res = await fetch('/api/instructions');
130
  if (!res.ok) throw new Error(`Network error: ${res.status}`);
131
  setPersonalityInstructions(await res.json());
132
- } catch (e) {
133
- setLoadingError("امکان دریافت تنظیمات شخصیت‌ها وجود ندارد. لطفاً صفحه را رفرش کنید.");
134
- }
135
  };
136
  fetchInstructions();
137
  }, []);
@@ -139,10 +145,7 @@ function App() {
139
  if (!personalityInstructions) return <div className="loading-screen">در حال بارگذاری...</div>;
140
  const initialAppConfig: LiveConfig = { model: "models/gemini-2.0-flash-exp" };
141
  return (
142
- <AppProvider initialConfig={initialAppConfig} personalityInstructions={personalityInstructions}>
143
- <AppInternal />
144
- <IOSModal isOpen={showIOSModal} onClose={() => setShowIOSModal(false)} />
145
- </AppProvider>
146
  );
147
  }
148
  export default App;
 
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";
 
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>
 
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
 
 
75
  const [isNotificationOpen, setIsNotificationOpen] = useState(false);
76
  const [isPersonalityMenuOpen, setIsPersonalityMenuOpen] = useState(false);
77
  const [isCustomModalOpen, setIsCustomModalOpen] = useState(false);
78
+ const notificationButtonRef = useRef<HTMLButtonElement>(null);
79
+ const notificationPopoverRef = useRef<HTMLDivElement>(null);
80
+
81
  const handleSelectPersonality = (p: PersonalityType) => {
82
  setIsPersonalityMenuOpen(false);
83
  if (p === 'custom') setIsCustomModalOpen(true);
84
  else changePersonality(p);
85
  };
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);
95
+ }, [isNotificationOpen]);
96
+
97
  return (
98
  <>
99
+ <div className="main-wrapper">
100
  <div className="header-controls">
101
  <button aria-label="انتخاب شخصیت" className="header-icon-button" onClick={() => setIsPersonalityMenuOpen(v => !v)}>
102
  <span className="material-symbols-outlined">psychology</span>
103
  </button>
104
+ <button ref={notificationButtonRef} aria-label="اطلاعات" className="header-icon-button" onClick={() => setIsNotificationOpen(v => !v)}>
105
  <span className="material-symbols-outlined">info</span>
106
  </button>
107
  </div>
108
+
109
+ <div ref={notificationPopoverRef} id="notification-popover-wrapper" className="notification-popover-wrapper">
110
+ <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'), })}>
111
+ <div className="notification-popover-text-content">مدل‌های هوش مصنوعی می‌توانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از بیان اطلاعات حساس بپرهیزید.</div>
112
+ </div>
113
+ </div>
114
+
115
  <PersonalityMenu isOpen={isPersonalityMenuOpen} onClose={() => setIsPersonalityMenuOpen(false)} onSelect={handleSelectPersonality} />
116
 
117
+ <div className="media-area">
118
  <video id="video-feed" ref={videoRef} autoPlay playsInline className={cn({ hidden: !isCamActive }, { "scale-x-[-1]": currentFacingMode === 'user' })} />
119
  {isMicActive && !isCamActive && <div id="large-logo-container"><Logo isMini={false} isActive={true} isAi={false} speakingVolume={volume} isUserSpeaking={isUserSpeaking} /></div>}
120
  </div>
 
121
  <ControlTray videoRef={videoRef} onUserSpeakingChange={setIsUserSpeaking} isAppMicActive={isMicActive} onAppMicToggle={setIsMicActive} isAppCamActive={isCamActive} onAppCamToggle={setIsCamActive} currentFacingMode={currentFacingMode} onFacingModeChange={setCurrentFacingMode} />
122
  </div>
123
  <CustomModal isOpen={isCustomModalOpen} onClose={() => setIsCustomModalOpen(false)} onSave={(name, instructions) => changePersonality('custom', { name, instructions })} initialName={customUserName} initialInstructions={customInstructions} />
 
137
  const res = await fetch('/api/instructions');
138
  if (!res.ok) throw new Error(`Network error: ${res.status}`);
139
  setPersonalityInstructions(await res.json());
140
+ } catch (e) { setLoadingError("امکان دریافت تنظیمات شخصیت‌ها وجود ندارد. لطفاً صفحه را رفرش کنید."); }
 
 
141
  };
142
  fetchInstructions();
143
  }, []);
 
145
  if (!personalityInstructions) return <div className="loading-screen">در حال بارگذاری...</div>;
146
  const initialAppConfig: LiveConfig = { model: "models/gemini-2.0-flash-exp" };
147
  return (
148
+ <AppProvider initialConfig={initialAppConfig} personalityInstructions={personalityInstructions}><AppInternal /><IOSModal isOpen={showIOSModal} onClose={() => setShowIOSModal(false)} /></AppProvider>
 
 
 
149
  );
150
  }
151
  export default App;