Spaces:
Running
Running
Update src/App.tsx
Browse files- src/App.tsx +12 -51
src/App.tsx
CHANGED
@@ -1,8 +1,6 @@
|
|
1 |
-
// src/App.tsx (نسخه
|
2 |
-
|
3 |
import React, { useEffect, useRef, useState, FC } from "react";
|
4 |
import './App.scss';
|
5 |
-
// AppContext را از مسیر درست ایمپورت میکنیم
|
6 |
import { AppProvider, useAppContext, PersonalityType, PersonalityInstructions } from "./contexts/AppContext";
|
7 |
import ControlTray from "./components/control-tray/ControlTray";
|
8 |
import { IOSModal } from "./components/ios-modal/IOSModal";
|
@@ -11,9 +9,7 @@ import cn from "classnames";
|
|
11 |
import Logo from "./components/logo/Logo";
|
12 |
import { LiveConfig } from "./multimodal-live-types";
|
13 |
|
14 |
-
//
|
15 |
-
// START: کامپوننت مودال مستقیماً اینجا تعریف شده است
|
16 |
-
// ===================================================================
|
17 |
interface CustomModalProps {
|
18 |
isOpen: boolean;
|
19 |
onClose: () => void;
|
@@ -21,74 +17,48 @@ interface CustomModalProps {
|
|
21 |
initialName: string;
|
22 |
initialInstructions: string;
|
23 |
}
|
24 |
-
|
25 |
const CustomModal: FC<CustomModalProps> = ({ isOpen, onClose, onSave, initialName, initialInstructions }) => {
|
26 |
const [name, setName] = useState(initialName);
|
27 |
const [instructions, setInstructions] = useState(initialInstructions);
|
28 |
-
|
29 |
useEffect(() => {
|
30 |
if (isOpen) {
|
31 |
setName(initialName);
|
32 |
setInstructions(initialInstructions);
|
33 |
}
|
34 |
}, [isOpen, initialName, initialInstructions]);
|
35 |
-
|
36 |
if (!isOpen) return null;
|
37 |
-
|
38 |
const handleSave = () => {
|
39 |
if (name.trim() && instructions.trim()) {
|
40 |
onSave(name.trim(), instructions.trim());
|
41 |
onClose();
|
42 |
-
} else {
|
43 |
-
alert("لطفا نام و دستورالعملها را وارد کنید.");
|
44 |
-
}
|
45 |
};
|
46 |
-
|
47 |
return (
|
48 |
<div className="modal-overlay" onClick={onClose}>
|
49 |
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
50 |
-
<div className="modal-header">
|
51 |
-
<h3>ساخت شخصیت اختصاصی</h3>
|
52 |
-
<button className="close-button" onClick={onClose}>×</button>
|
53 |
-
</div>
|
54 |
<div className="modal-body">
|
55 |
-
<div className="form-group">
|
56 |
-
|
57 |
-
<input id="name" type="text" placeholder="مثلا: علی" value={name} onChange={(e) => setName(e.target.value)} />
|
58 |
-
</div>
|
59 |
-
<div className="form-group">
|
60 |
-
<label htmlFor="instructions">دستورالعملهای شخصیت</label>
|
61 |
-
<textarea id="instructions" placeholder="مثلا: خیلی خودمونی و بامزه صحبت کن..." value={instructions} onChange={(e) => setInstructions(e.target.value)} rows={6} />
|
62 |
-
<small>اینجا فقط رفتار و سبک صحبت کردن دستیار را مشخص کنید.</small>
|
63 |
-
</div>
|
64 |
-
</div>
|
65 |
-
<div className="modal-footer">
|
66 |
-
<button className="save-button" onClick={handleSave}>ذخیره و فعالسازی</button>
|
67 |
</div>
|
|
|
68 |
</div>
|
69 |
</div>
|
70 |
);
|
71 |
};
|
72 |
-
// ===================================================================
|
73 |
-
// END: تعریف کامپوننت مودال
|
74 |
-
// ===================================================================
|
75 |
|
76 |
-
|
77 |
-
// کامپوننت منوی شخصیتها (به صورت یک کامپوننت داخلی ساده)
|
78 |
const PersonalityMenu: React.FC<{ isOpen: boolean; onClose: () => void; onSelect: (p: PersonalityType) => void; }> = ({ isOpen, onClose, onSelect }) => {
|
79 |
const menuRef = useRef<HTMLDivElement>(null);
|
80 |
const { selectedPersonality } = useAppContext();
|
81 |
const personalityIcons: Record<PersonalityType, string> = { default: "person", teacher: "school", poetic: "auto_awesome", funny: "sentiment_satisfied", custom: "tune" };
|
82 |
const personalityLabels: Record<PersonalityType, string> = { default: 'دستیار پیشفرض', teacher: 'استاد زبان', poetic: 'حس خوب', funny: 'شوخطبع', custom: 'شخصیت اختصاصی' };
|
83 |
-
|
84 |
useEffect(() => {
|
85 |
const handleClickOutside = (e: MouseEvent) => menuRef.current && !menuRef.current.contains(e.target as Node) && onClose();
|
86 |
if (isOpen) document.addEventListener('mousedown', handleClickOutside);
|
87 |
return () => document.removeEventListener('mousedown', handleClickOutside);
|
88 |
}, [isOpen, onClose]);
|
89 |
-
|
90 |
if (!isOpen) return null;
|
91 |
-
|
92 |
return (
|
93 |
<div ref={menuRef} className="personality-popover-wrapper open">
|
94 |
<div className="popover-content">
|
@@ -116,30 +86,26 @@ const AppInternal: React.FC = () => {
|
|
116 |
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
|
117 |
const [isPersonalityMenuOpen, setIsPersonalityMenuOpen] = useState(false);
|
118 |
const [isCustomModalOpen, setIsCustomModalOpen] = useState(false);
|
119 |
-
|
120 |
const handleSelectPersonality = (p: PersonalityType) => {
|
121 |
setIsPersonalityMenuOpen(false);
|
122 |
if (p === 'custom') setIsCustomModalOpen(true);
|
123 |
else changePersonality(p);
|
124 |
};
|
125 |
-
|
126 |
return (
|
127 |
<>
|
128 |
-
<div className="main-wrapper">
|
129 |
<div className="header-controls">
|
130 |
-
<button aria-label="انتخاب شخصیت" className="header-icon-button" onClick={() => setIsPersonalityMenuOpen(
|
131 |
<span className="material-symbols-outlined">psychology</span>
|
132 |
</button>
|
133 |
<button aria-label="اطلاعات" className="header-icon-button" onClick={() => setIsNotificationOpen(v => !v)}>
|
134 |
<span className="material-symbols-outlined">info</span>
|
135 |
</button>
|
136 |
</div>
|
137 |
-
|
138 |
{isNotificationOpen && <div className="notification-popover-wrapper open"><div className="popover-content">مدلهای هوش مصنوعی میتوانند اشتباه کنند.</div></div>}
|
139 |
-
|
140 |
<PersonalityMenu isOpen={isPersonalityMenuOpen} onClose={() => setIsPersonalityMenuOpen(false)} onSelect={handleSelectPersonality} />
|
141 |
-
|
142 |
-
<div className="media-area">
|
143 |
<video id="video-feed" ref={videoRef} autoPlay playsInline className={cn({ hidden: !isCamActive }, { "scale-x-[-1]": currentFacingMode === 'user' })} />
|
144 |
{isMicActive && !isCamActive && <div id="large-logo-container"><Logo isMini={false} isActive={true} isAi={false} speakingVolume={volume} isUserSpeaking={isUserSpeaking} /></div>}
|
145 |
</div>
|
@@ -156,7 +122,6 @@ function App() {
|
|
156 |
const [showIOSModal, setShowIOSModal] = useState(false);
|
157 |
const [personalityInstructions, setPersonalityInstructions] = useState<PersonalityInstructions | null>(null);
|
158 |
const [loadingError, setLoadingError] = useState<string | null>(null);
|
159 |
-
|
160 |
useEffect(() => {
|
161 |
if (isIOS()) setShowIOSModal(true);
|
162 |
const fetchInstructions = async () => {
|
@@ -170,12 +135,9 @@ function App() {
|
|
170 |
};
|
171 |
fetchInstructions();
|
172 |
}, []);
|
173 |
-
|
174 |
if (loadingError) return <div className="loading-screen">{loadingError}</div>;
|
175 |
if (!personalityInstructions) return <div className="loading-screen">در حال بارگذاری...</div>;
|
176 |
-
|
177 |
const initialAppConfig: LiveConfig = { model: "models/gemini-2.0-flash-exp" };
|
178 |
-
|
179 |
return (
|
180 |
<AppProvider initialConfig={initialAppConfig} personalityInstructions={personalityInstructions}>
|
181 |
<AppInternal />
|
@@ -183,5 +145,4 @@ function App() {
|
|
183 |
</AppProvider>
|
184 |
);
|
185 |
}
|
186 |
-
|
187 |
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";
|
5 |
import ControlTray from "./components/control-tray/ControlTray";
|
6 |
import { IOSModal } from "./components/ios-modal/IOSModal";
|
|
|
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;
|
|
|
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">
|
64 |
<div className="popover-content">
|
|
|
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>
|
|
|
122 |
const [showIOSModal, setShowIOSModal] = useState(false);
|
123 |
const [personalityInstructions, setPersonalityInstructions] = useState<PersonalityInstructions | null>(null);
|
124 |
const [loadingError, setLoadingError] = useState<string | null>(null);
|
|
|
125 |
useEffect(() => {
|
126 |
if (isIOS()) setShowIOSModal(true);
|
127 |
const fetchInstructions = async () => {
|
|
|
135 |
};
|
136 |
fetchInstructions();
|
137 |
}, []);
|
|
|
138 |
if (loadingError) return <div className="loading-screen">{loadingError}</div>;
|
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 />
|
|
|
145 |
</AppProvider>
|
146 |
);
|
147 |
}
|
|
|
148 |
export default App;
|