Spaces:
Running
Running
Update src/App.tsx
Browse files- src/App.tsx +67 -61
src/App.tsx
CHANGED
@@ -19,8 +19,11 @@ import { IOSModal } from "./components/ios-modal/IOSModal";
|
|
19 |
import { isIOS } from "./lib/platform";
|
20 |
import cn from "classnames";
|
21 |
import { LiveConfig } from "./multimodal-live-types";
|
|
|
|
|
22 |
|
23 |
-
|
|
|
24 |
const myCustomInstruction = `
|
25 |
ت1. هویت دستیار:
|
26 |
|
@@ -89,7 +92,13 @@ const myCustomInstruction = `
|
|
89 |
|
90 |
در صورت درخواست کاربر، سرعت مکالمه را کند یا تند کن.
|
91 |
|
92 |
-
سطح سختی واژگان و جملات را بر اساس سطح کاربر (مبتدی، متوسط، پیشرفته) تنظیم کن.
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
`.trim();
|
94 |
// --- ---
|
95 |
|
@@ -108,9 +117,10 @@ const SvgHumanIcon = () => (
|
|
108 |
|
109 |
function App() {
|
110 |
const videoRef = useRef<HTMLVideoElement>(null);
|
111 |
-
|
|
|
112 |
const [showIOSModal, setShowIOSModal] = useState(false);
|
113 |
-
const [isAllowedOrigin, setIsAllowedOrigin] = useState<boolean | null>(null);
|
114 |
|
115 |
const [isMicActive, setIsMicActive] = useState(false);
|
116 |
const [isCamActive, setIsCamActive] = useState(false);
|
@@ -124,15 +134,12 @@ function App() {
|
|
124 |
if (isIOS()) {
|
125 |
setShowIOSModal(true);
|
126 |
}
|
127 |
-
// برای اجازه دادن به برنامه در همه جا، isAllowedOrigin را مستقیماً true قرار میدهیم.
|
128 |
-
// نمایش اولیه "در حال بررسی دسترسی..." همچنان مفید است.
|
129 |
-
// بنابراین با یک تاخیر کوچک true میکنیم تا کاربر آن را ببیند.
|
130 |
const timer = setTimeout(() => {
|
131 |
setIsAllowedOrigin(true);
|
132 |
-
console.log("Access allowed globally.");
|
133 |
-
}, 100);
|
134 |
|
135 |
-
return () => clearTimeout(timer);
|
136 |
}, []);
|
137 |
|
138 |
useEffect(() => {
|
@@ -155,16 +162,9 @@ function App() {
|
|
155 |
|
156 |
|
157 |
if (isAllowedOrigin === null) {
|
158 |
-
// این پیام برای مدت کوتاهی نمایش داده میشود و سپس به true تغییر میکند.
|
159 |
return <div style={{ padding: '20px', textAlign: 'center' }}>در حال بررسی دسترسی...</div>;
|
160 |
}
|
161 |
-
|
162 |
-
// این بخش دیگر لازم نیست چون isAllowedOrigin همیشه true خواهد شد:
|
163 |
-
// if (isAllowedOrigin === false) {
|
164 |
-
// return <div style={{ padding: '20px', textAlign: 'center', color: 'red' }}>دسترسی غیرمجاز! ... </div>;
|
165 |
-
// }
|
166 |
|
167 |
-
// تابع برای ساخت لوگو (ساده شده)
|
168 |
const createLogoHTML = (isMini: boolean, isActive: boolean, type: 'human' | 'ai' = 'human') => {
|
169 |
if (!isActive) return null;
|
170 |
const size = isMini ? 80 : 200;
|
@@ -172,18 +172,16 @@ function App() {
|
|
172 |
const insetBase = isMini
|
173 |
? { ping: 10, outer: 0, mid: 5, inner: 12, icon: 22 }
|
174 |
: { ping: 40, outer: 0, mid: 20, inner: 50, icon: 65 };
|
175 |
-
const bgColorBase = type === 'human' ? 'blue' : 'green';
|
176 |
|
177 |
return (
|
178 |
<div className="relative" style={{ width: `${size}px`, height: `${size}px` }}>
|
179 |
-
{
|
180 |
-
<div className={`absolute rounded-full opacity-50
|
181 |
-
<div className={`absolute
|
182 |
-
<div className={`absolute rounded-full opacity-50 bg-${bgColorBase}-
|
183 |
-
<div className={`absolute rounded-full opacity-50 bg-${bgColorBase}-400`} style={{ inset: `${insetBase.inner}px` }}></div>
|
184 |
<div className="z-10 absolute" style={{ inset: `${insetBase.icon}px`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
185 |
{type === 'human' && <SvgHumanIcon />}
|
186 |
-
{/* Add AI icon if needed */}
|
187 |
</div>
|
188 |
</div>
|
189 |
);
|
@@ -191,10 +189,9 @@ function App() {
|
|
191 |
|
192 |
|
193 |
return (
|
194 |
-
// تگ <style jsx global> از اینجا حذف شده و استایلها از App.scss میآیند
|
195 |
<LiveAPIProvider initialConfig={initialAppConfig}>
|
196 |
-
<div className="w-full flex flex-col items-center justify-center min-h-
|
197 |
-
<div className="max-w-3xl w-full flex flex-col items-center justify-center h-full relative">
|
198 |
{/* Header */}
|
199 |
<div className="header-controls">
|
200 |
<button
|
@@ -202,11 +199,14 @@ function App() {
|
|
202 |
id="notification-button"
|
203 |
aria-label="Notifications"
|
204 |
className="header-button"
|
205 |
-
onClick={() =>
|
|
|
|
|
|
|
206 |
>
|
207 |
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
|
208 |
</button>
|
209 |
-
<div className="header-button" onClick={() => alert('Back clicked (
|
210 |
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m15 18-6-6 6-6"></path></svg>
|
211 |
</div>
|
212 |
</div>
|
@@ -215,7 +215,7 @@ function App() {
|
|
215 |
<div ref={notificationPopoverRef} id="notification-popover-wrapper" className="notification-popover-wrapper">
|
216 |
<div
|
217 |
id="notification-popover"
|
218 |
-
className={cn("popover-content", {
|
219 |
"open animate-popover-open-top-center": isNotificationOpen,
|
220 |
"animate-popover-close-top-center": !isNotificationOpen && document.getElementById('notification-popover')?.classList.contains('open'),
|
221 |
})}
|
@@ -226,39 +226,45 @@ function App() {
|
|
226 |
</div>
|
227 |
</div>
|
228 |
|
229 |
-
{/* Main Media Area */}
|
230 |
-
<div className="
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
className={cn("absolute top-0 left-0 w-full h-full object-cover scale-x-[-1]", {
|
237 |
-
"hidden": !isCamActive,
|
238 |
-
})}
|
239 |
-
/>
|
240 |
-
<div id="large-logo-container" className={cn("items-center justify-center w-full h-full absolute top-0 left-0", {
|
241 |
-
"flex": !isCamActive && isMicActive,
|
242 |
-
"hidden": isCamActive || !isMicActive
|
243 |
-
})}>
|
244 |
-
{createLogoHTML(false, !isCamActive && isMicActive)}
|
245 |
-
</div>
|
246 |
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
|
|
|
|
|
|
|
|
261 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
262 |
</div>
|
263 |
</div>
|
264 |
<IOSModal isOpen={showIOSModal} onClose={() => setShowIOSModal(false)} />
|
|
|
19 |
import { isIOS } from "./lib/platform";
|
20 |
import cn from "classnames";
|
21 |
import { LiveConfig } from "./multimodal-live-types";
|
22 |
+
import { Altair } from "./components/altair/Altair"; // اگر میخواهید نمایش چت را داشته باشید
|
23 |
+
import SidePanel from "./components/side-panel/SidePanel"; // اگر میخواهید پنل کناری را داشته باشید
|
24 |
|
25 |
+
|
26 |
+
// --- دستورالعمل شخصیسازی شما (با بخش تعامل تصویری) ---
|
27 |
const myCustomInstruction = `
|
28 |
ت1. هویت دستیار:
|
29 |
|
|
|
92 |
|
93 |
در صورت درخواست کاربر، سرعت مکالمه را کند یا تند کن.
|
94 |
|
95 |
+
سطح سختی واژگان و جملات را بر اساس سطح کاربر (مبتدی، متوسط، پیشرفته) تنظیم کن.
|
96 |
+
|
97 |
+
**تعامل تصویری:**
|
98 |
+
- به تصویر زندهای که از کاربر دریافت میکنی توجه کن.
|
99 |
+
- اگر در تصویر نکته قابل توجهی وجود دارد (مانند حالت چهره، اشیاء خاص، یا محیط اطراف کاربر)، میتوانی به آن در مکالمه اشاره کنی، البته فقط اگر مرتبط با موضوع صحبت باشد یا کاربر از تو بخواهد.
|
100 |
+
- اگر کاربر سوالی در مورد چیزی که در تصویر میبیند پرسید، سعی کن بر اساس تصویر پاسخ دهی.
|
101 |
+
- هدف اصلی، کمک به یادگیری زبان است، پس تعامل تصویری باید در خدمت این هدف باشد.
|
102 |
`.trim();
|
103 |
// --- ---
|
104 |
|
|
|
117 |
|
118 |
function App() {
|
119 |
const videoRef = useRef<HTMLVideoElement>(null);
|
120 |
+
// videoStream دیگر مستقیماً توسط App مدیریت نمیشود، بلکه توسط ControlTray از طریق onVideoStreamChange
|
121 |
+
// const [videoStream, setVideoStream] = useState<MediaStream | null>(null);
|
122 |
const [showIOSModal, setShowIOSModal] = useState(false);
|
123 |
+
const [isAllowedOrigin, setIsAllowedOrigin] = useState<boolean | null>(null);
|
124 |
|
125 |
const [isMicActive, setIsMicActive] = useState(false);
|
126 |
const [isCamActive, setIsCamActive] = useState(false);
|
|
|
134 |
if (isIOS()) {
|
135 |
setShowIOSModal(true);
|
136 |
}
|
|
|
|
|
|
|
137 |
const timer = setTimeout(() => {
|
138 |
setIsAllowedOrigin(true);
|
139 |
+
console.log("Access allowed globally by default in App.tsx.");
|
140 |
+
}, 100);
|
141 |
|
142 |
+
return () => clearTimeout(timer);
|
143 |
}, []);
|
144 |
|
145 |
useEffect(() => {
|
|
|
162 |
|
163 |
|
164 |
if (isAllowedOrigin === null) {
|
|
|
165 |
return <div style={{ padding: '20px', textAlign: 'center' }}>در حال بررسی دسترسی...</div>;
|
166 |
}
|
|
|
|
|
|
|
|
|
|
|
167 |
|
|
|
168 |
const createLogoHTML = (isMini: boolean, isActive: boolean, type: 'human' | 'ai' = 'human') => {
|
169 |
if (!isActive) return null;
|
170 |
const size = isMini ? 80 : 200;
|
|
|
172 |
const insetBase = isMini
|
173 |
? { ping: 10, outer: 0, mid: 5, inner: 12, icon: 22 }
|
174 |
: { ping: 40, outer: 0, mid: 20, inner: 50, icon: 65 };
|
175 |
+
const bgColorBase = type === 'human' ? 'blue' : 'green';
|
176 |
|
177 |
return (
|
178 |
<div className="relative" style={{ width: `${size}px`, height: `${size}px` }}>
|
179 |
+
<div className={`absolute rounded-full opacity-50 animate-ping bg-${bgColorBase}-200 dark:bg-${bgColorBase}-700`} style={{ inset: `${insetBase.ping}px` }}></div>
|
180 |
+
<div className={`absolute inset-0 rounded-full opacity-50 bg-${bgColorBase}-200 dark:bg-${bgColorBase}-700`} style={{ inset: `${insetBase.outer}px` }}></div>
|
181 |
+
<div className={`absolute rounded-full opacity-50 bg-${bgColorBase}-300 dark:bg-${bgColorBase}-600`} style={{ inset: `${insetBase.mid}px` }}></div>
|
182 |
+
<div className={`absolute rounded-full opacity-50 bg-${bgColorBase}-400 dark:bg-${bgColorBase}-500`} style={{ inset: `${insetBase.inner}px` }}></div>
|
|
|
183 |
<div className="z-10 absolute" style={{ inset: `${insetBase.icon}px`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
184 |
{type === 'human' && <SvgHumanIcon />}
|
|
|
185 |
</div>
|
186 |
</div>
|
187 |
);
|
|
|
189 |
|
190 |
|
191 |
return (
|
|
|
192 |
<LiveAPIProvider initialConfig={initialAppConfig}>
|
193 |
+
<div className="w-full flex flex-col items-center justify-center min-h-screen bg-background text-foreground antialiased"> {/* min-h-screen for full height */}
|
194 |
+
<div className="max-w-3xl w-full flex flex-col items-center justify-center h-full relative"> {/* h-full */}
|
195 |
{/* Header */}
|
196 |
<div className="header-controls">
|
197 |
<button
|
|
|
199 |
id="notification-button"
|
200 |
aria-label="Notifications"
|
201 |
className="header-button"
|
202 |
+
onClick={(e) => {
|
203 |
+
e.stopPropagation(); // برای جلوگیری از بسته شدن بلافاصله توسط کلیک بیرون
|
204 |
+
setIsNotificationOpen(!isNotificationOpen);
|
205 |
+
}}
|
206 |
>
|
207 |
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
|
208 |
</button>
|
209 |
+
<div className="header-button" onClick={() => alert('Back clicked (navigation logic)')}>
|
210 |
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m15 18-6-6 6-6"></path></svg>
|
211 |
</div>
|
212 |
</div>
|
|
|
215 |
<div ref={notificationPopoverRef} id="notification-popover-wrapper" className="notification-popover-wrapper">
|
216 |
<div
|
217 |
id="notification-popover"
|
218 |
+
className={cn("popover-content", {
|
219 |
"open animate-popover-open-top-center": isNotificationOpen,
|
220 |
"animate-popover-close-top-center": !isNotificationOpen && document.getElementById('notification-popover')?.classList.contains('open'),
|
221 |
})}
|
|
|
226 |
</div>
|
227 |
</div>
|
228 |
|
229 |
+
{/* Main Media Area and Chat Area */}
|
230 |
+
<div className="flex flex-col md:flex-row w-full h-[calc(100vh-100px)] mt-[60px] mb-[40px]"> {/* Adjust height and margins as needed */}
|
231 |
+
{/* SidePanel - اگر میخواهید پنل کناری چت را داشته باشید */}
|
232 |
+
<div className="w-full md:w-1/3 p-2 overflow-y-auto">
|
233 |
+
<SidePanel />
|
234 |
+
<Altair />
|
235 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
236 |
|
237 |
+
{/* Video Area */}
|
238 |
+
<div className="w-full md:w-2/3 flex flex-col items-center justify-center bg-background relative"> {/* Removed h-[90dvh] to fit within flex layout */}
|
239 |
+
<video
|
240 |
+
id="video-feed"
|
241 |
+
ref={videoRef}
|
242 |
+
autoPlay
|
243 |
+
playsInline
|
244 |
+
className={cn("absolute top-0 left-0 w-full h-full object-cover scale-x-[-1]", {
|
245 |
+
"hidden": !isCamActive,
|
246 |
+
})}
|
247 |
+
/>
|
248 |
+
<div id="large-logo-container" className={cn("items-center justify-center w-full h-full absolute top-0 left-0", {
|
249 |
+
"flex": !isCamActive && isMicActive,
|
250 |
+
"hidden": isCamActive || !isMicActive
|
251 |
+
})}>
|
252 |
+
{createLogoHTML(false, !isCamActive && isMicActive)}
|
253 |
+
</div>
|
254 |
+
</div>
|
255 |
</div>
|
256 |
+
|
257 |
+
<ControlTray
|
258 |
+
videoRef={videoRef}
|
259 |
+
supportsVideo={true}
|
260 |
+
onVideoStreamChange={(stream) => { /* App.tsx no longer needs direct videoStream state */ }}
|
261 |
+
isAppMicActive={isMicActive}
|
262 |
+
onAppMicToggle={setIsMicActive}
|
263 |
+
isAppCamActive={isAppCamActive}
|
264 |
+
onAppCamToggle={setIsCamActive}
|
265 |
+
createLogoFunction={createLogoHTML}
|
266 |
+
/>
|
267 |
+
|
268 |
</div>
|
269 |
</div>
|
270 |
<IOSModal isOpen={showIOSModal} onClose={() => setShowIOSModal(false)} />
|