Ezmary commited on
Commit
1b7a601
·
verified ·
1 Parent(s): 53398ed

Update src/App.tsx

Browse files
Files changed (1) hide show
  1. src/App.tsx +141 -199
src/App.tsx CHANGED
@@ -1,206 +1,148 @@
1
- // src/App.scss (نسخه نهایی و تصحیح شده برای رفع مشکل ظاهری)
 
 
 
 
 
 
 
 
 
2
 
3
- // 1. Import Tailwind's base, components, and utilities
4
- @import 'tailwindcss/base';
5
- @import 'tailwindcss/components';
6
- @import 'tailwindcss/utilities';
7
-
8
- // 2. Define CSS Variables (از کد شما)
9
- :root {
10
- --radius: 0.625rem;
11
- --radius-md: 0.5rem;
12
- --background: oklch(1 0 0);
13
- --foreground: oklch(0.145 0 0);
14
- --popover: oklch(1 0 0);
15
- --popover-foreground: oklch(0.145 0 0);
16
- --border: oklch(0.922 0 0);
17
- }
18
- .dark {
19
- --background: oklch(0.145 0 0);
20
- --foreground: oklch(0.985 0 0);
21
- --popover: oklch(0.205 0 0);
22
- --popover-foreground: oklch(0.985 0 0);
23
- --border: oklch(1 0 0 / 10%);
24
- }
25
-
26
- // 3. Apply base styles (از کد شما)
27
- @layer base {
28
- * {
29
- border-color: theme('colors.custom-border');
30
- box-sizing: border-box;
31
  }
32
- body {
33
- @apply bg-custom-background text-custom-foreground;
34
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
35
- margin: 0; padding: 0;
36
- }
37
- }
38
-
39
- /* --- START: استایل‌های اصلی و جدید برای چیدمان و عناصر --- */
40
- .loading-screen {
41
- display: flex; align-items: center; justify-content: center;
42
- height: 100vh; font-size: 1.2rem;
43
- color: theme('colors.custom-foreground');
44
- }
 
 
 
 
 
 
 
 
45
 
46
- .main-wrapper {
47
- min-height: 100vh;
48
- display: flex;
49
- flex-direction: column;
50
- position: relative; /* برای جای‌گیری درست عناصر داخلی */
51
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
- .header-controls {
54
- display: flex; padding: 0.75rem 1rem; justify-content: space-between; align-items: center;
55
- width: 100%; position: absolute; top: 0; left: 0; z-index: 50;
56
- pointer-events: none;
57
- > div, > button { pointer-events: auto; }
58
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- .header-icon-button {
61
- display: flex; align-items: center; justify-content: center; padding: 0.5rem;
62
- width: 44px; height: 44px; border-radius: 9999px;
63
- background-color: rgba(44, 44, 46, 0.7);
64
- border: 1px solid rgba(255, 255, 255, 0.1);
65
- backdrop-filter: blur(8px);
66
- cursor: pointer; transition: all 0.2s ease-out;
67
- .material-symbols-outlined {
68
- opacity: 0.9;
69
- color: oklch(0.95 0 0);
70
- font-size: 26px;
71
- }
72
- &:hover { background-color: rgba(60, 60, 62, 0.8); }
73
- &:active { transform: scale(0.95); }
74
- }
75
 
76
- .notification-popover-wrapper, .personality-popover-wrapper {
77
- position: absolute; z-index: 100; opacity: 0; pointer-events: none;
78
- transform: translateY(-10px) scale(0.95); transition: all 150ms ease-in-out;
79
- &.open { opacity: 1; pointer-events: auto; transform: translateY(0) scale(1); }
80
- .popover-content {
81
- background: rgba(44,44,46,0.95); backdrop-filter: blur(10px);
82
- border-radius: 12px; padding: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.4);
83
- border: 1px solid rgba(255, 255, 255, 0.1); color: oklch(0.95 0 0); font-size: 14px;
84
-
85
- ul { list-style: none; padding: 0; margin: 0; width: 220px; }
86
- li {
87
- display: flex; align-items: center; justify-content: space-between;
88
- padding: 10px 12px; border-radius: 8px; cursor: pointer; transition: background-color 150ms ease;
89
- &:hover { background: #3a3a3c; }
90
- &.active { color: #fff; font-weight: 500; }
91
- div { display: flex; align-items: center; gap: 12px; }
92
- .material-symbols-outlined { font-size: 22px; &.tick { color: #34c759; } }
93
- }
94
- }
95
- }
96
- .notification-popover-wrapper {
97
- top: calc(1rem + 44px + 8px); right: 1rem;
98
- .popover-content { padding: 12px 16px; width: 280px; }
99
- }
100
- .personality-popover-wrapper {
101
- top: calc(1rem + 44px + 8px); left: 1rem;
102
- }
103
- /* --- END: استایل‌های جدید --- */
104
-
105
-
106
- /* بخش‌های دیگر استایل که از کد شما حفظ شده‌اند */
107
- .media-area {
108
- flex-grow: 1; position: relative; width: 100%; height: 100%;
109
- }
110
- #large-logo-container {
111
- display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;
112
- position: absolute; top: 0; left: 0; pointer-events: none;
113
- }
114
- .footer-controls-html-like {
115
- width: 100%; display: flex; align-items: center; position: absolute;
116
- bottom: 2rem; padding: 0.5rem 2.5rem; box-sizing: border-box; z-index: 20; justify-content: space-between;
117
- }
118
- .small-logo-footer-html-like {
119
- position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
120
- z-index: 1; display: flex; align-items: center; justify-content: center;
121
- }
122
- .control-button-wrapper { position: relative; display: flex; justify-content: center; }
123
- .control-button {
124
- height: 80px; width: 80px; border-radius: 9999px; padding: 0;
125
- display: flex; align-items: center; justify-content: center;
126
- border-width: 1px; border-color: theme('colors.custom-border');
127
- box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06);
128
- cursor: pointer; transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
129
- flex-shrink: 0; z-index: 2; overflow: hidden;
130
- &:hover { transform: scale(1.05); }
131
- svg.reference-mic-svg { width: 75%; height: 75%; }
132
- }
133
- .mic-button-color { background-color: #fecdd3; }
134
- .cam-button-color { background-color: #E0ECFF; }
135
- .dark .mic-button-color { background-color: #5C2129; }
136
- .dark .cam-button-color { background-color: #223355; }
137
- .switch-camera-button-container {
138
- position: absolute; bottom: calc(100% + 0.65rem); left: 50%;
139
- transform: translateX(-50%) translateY(15px) scale(0.7); z-index: 5;
140
- opacity: 0; pointer-events: none; transition: opacity 0.35s, transform 0.35s;
141
- transform-origin: center bottom;
142
- &.visible { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); pointer-events: auto; }
143
- }
144
- .switch-camera-button-content {
145
- width: 48px; height: 48px; background-color: theme('colors.custom-background');
146
- border: 1px solid theme('colors.custom-border'); border-radius: 9999px;
147
- display: flex; align-items: center; justify-content: center;
148
- box-shadow: 0 5px 10px rgba(0,0,0,0.12); cursor: pointer;
149
- transition: transform 0.2s ease-out;
150
- &:hover { transform: scale(1.12); }
151
- &:active { transform: scale(1.03); }
152
- svg { width: 22px; height: 22px; stroke: theme('colors.custom-foreground'); }
153
- }
154
-
155
- /* استایل‌های مودال */
156
- .modal-overlay {
157
- position: fixed; top: 0; left: 0; right: 0; bottom: 0;
158
- background-color: rgba(0, 0, 0, 0.7);
159
- display: flex; align-items: center; justify-content: center;
160
- z-index: 1000; backdrop-filter: blur(5px);
161
- }
162
- .modal-content {
163
- background: #2c2c2e; padding: 24px; border-radius: 16px;
164
- width: 90%; max-width: 500px;
165
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
166
- border: 1px solid rgba(255, 255, 255, 0.15);
167
- animation: modal-fade-in 0.3s ease-out;
168
- .modal-header {
169
- display: flex; justify-content: space-between; align-items: center;
170
- margin-bottom: 20px; border-bottom: 1px solid #444; padding-bottom: 16px;
171
- h3 { margin: 0; font-size: 20px; color: #fff; }
172
- .close-button {
173
- background: none; border: none; color: #aaa; font-size: 28px;
174
- cursor: pointer; line-height: 1; padding: 0;
175
- &:hover { color: #fff; }
176
- }
177
- }
178
- .modal-body .form-group {
179
- margin-bottom: 20px;
180
- label { display: block; margin-bottom: 8px; font-weight: 500; color: #e0e0e0; }
181
- input, textarea {
182
- width: 100%; padding: 12px; border-radius: 8px;
183
- border: 1px solid #555; background-color: #3a3a3c;
184
- color: #fff; font-size: 15px;
185
- &:focus {
186
- outline: none; border-color: #0a84ff;
187
- box-shadow: 0 0 0 2px rgba(10, 132, 255, 0.5);
188
  }
189
- }
190
- textarea { resize: vertical; min-height: 100px; }
191
- small { display: block; margin-top: 8px; color: #999; font-size: 12px; }
192
- }
193
- .modal-footer {
194
- display: flex; justify-content: flex-end; margin-top: 24px;
195
- .save-button {
196
- background-color: #0a84ff; color: #fff; border: none;
197
- padding: 12px 24px; border-radius: 8px; font-size: 16px;
198
- font-weight: 600; cursor: pointer; transition: background-color 0.2s;
199
- &:hover { background-color: #3499ff; }
200
- }
201
- }
202
- }
203
- @keyframes modal-fade-in {
204
- from { opacity: 0; transform: scale(0.9); }
205
- to { opacity: 1; transform: scale(1); }
206
- }
 
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";
7
+ 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">
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
 
78
+ // کامپوننت داخلی اصلی برنامه
79
+ const AppInternal: React.FC = () => {
80
+ const videoRef = useRef<HTMLVideoElement>(null);
81
+ const { volume, changePersonality, customUserName, customInstructions } = useAppContext();
82
+ const [isUserSpeaking, setIsUserSpeaking] = useState(false);
83
+ const [isMicActive, setIsMicActive] = useState(false);
84
+ const [isCamActive, setIsCamActive] = useState(false);
85
+ const [currentFacingMode, setCurrentFacingMode] = useState<'user' | 'environment'>('user');
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} />
116
+ </>
117
+ );
118
+ };
 
 
 
 
 
 
 
 
 
119
 
120
+ // کامپوننت ریشه App
121
+ function App() {
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 () => {
128
+ try {
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
+ }, []);
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 />
144
+ <IOSModal isOpen={showIOSModal} onClose={() => setShowIOSModal(false)} />
145
+ </AppProvider>
146
+ );
147
+ }
148
+ export default App;