Ezmary commited on
Commit
5f81b28
·
verified ·
1 Parent(s): 25cd3bd

Update src/App.tsx

Browse files
Files changed (1) hide show
  1. src/App.tsx +199 -141
src/App.tsx CHANGED
@@ -1,148 +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;
 
 
 
 
 
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
+ }