Spaces:
Running
Running
Update src/App.tsx
Browse files- 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>اینجا فقط
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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;
|