Spaces:
Running
Running
Update src/App.tsx
Browse files- src/App.tsx +194 -288
src/App.tsx
CHANGED
@@ -1,77 +1,41 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
(لایسنس مثل قبل)
|
4 |
-
*/
|
5 |
-
|
6 |
-
import React, { useEffect, useRef, useState, useCallback } from "react";
|
7 |
-
import "./App.scss"; // استایلهای جدید اینجا خواهند بود
|
8 |
import { LiveAPIProvider } from "./contexts/LiveAPIContext";
|
9 |
-
|
10 |
import { Altair } from "./components/altair/Altair";
|
11 |
-
import ControlTray from "./components/control-tray/ControlTray";
|
12 |
import { IOSModal } from "./components/ios-modal/IOSModal";
|
13 |
import { isIOS } from "./lib/platform";
|
14 |
import cn from "classnames";
|
15 |
import { LiveConfig } from "./multimodal-live-types";
|
16 |
-
import { useLiveAPIContext } from "./contexts/LiveAPIContext"; // برای دسترسی به توابع اتصال و ...
|
17 |
|
18 |
// --- 👇 دامنه مجاز خودتان را اینجا قرار دهید (با https یا http) 👇 ---
|
19 |
-
const ALLOWED_ORIGIN = "https://www.aisada.ir";
|
20 |
// --- 👆 ---
|
21 |
|
22 |
-
//
|
23 |
-
const
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
const SvgSwitchCameraIcon = () => <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <path d="M11 19H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5"/><path d="M13 5h7a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-5"/><path d="m17 12-3-3 3-3"/><path d="m7 12 3 3-3 3"/></svg>;
|
29 |
-
|
30 |
-
const LogoDisplay = ({ isMini, isHumanActive, isAiActive }: { isMini: boolean; isHumanActive: boolean; isAiActive: boolean; }) => {
|
31 |
-
if (!isHumanActive && !isAiActive) return null;
|
32 |
-
const size = isMini ? 80 : 200;
|
33 |
-
const iconSize = isMini ? 35 : 70;
|
34 |
-
const insetBase = isMini ? { ping: 10, outer: 0, mid: 5, inner: 12, icon: 22 }
|
35 |
-
: { ping: 40, outer: 0, mid: 20, inner: 50, icon: 65 };
|
36 |
-
const bgColorBase = isHumanActive ? 'blue' : (isAiActive ? 'green' : 'gray');
|
37 |
-
|
38 |
-
return (
|
39 |
-
<>
|
40 |
-
{/* Dummy divs for Tailwind JIT compiler to pick up classes */}
|
41 |
-
<div className="hidden bg-green-200 bg-green-300 bg-green-400 bg-blue-200 bg-blue-300 bg-blue-400"></div>
|
42 |
-
<div className="relative" style={{ width: `${size}px`, height: `${size}px` }}>
|
43 |
-
<div className={`absolute rounded-full opacity-50 animate-ping bg-${bgColorBase}-200`} style={{ inset: `${insetBase.ping}px` }}></div>
|
44 |
-
<div className={`absolute inset-0 rounded-full opacity-50 bg-${bgColorBase}-200`}></div>
|
45 |
-
<div className={`absolute rounded-full opacity-50 bg-${bgColorBase}-300`} style={{ inset: `${insetBase.mid}px` }}></div>
|
46 |
-
<div className={`absolute rounded-full opacity-50 bg-${bgColorBase}-400`} style={{ inset: `${insetBase.inner}px` }}></div>
|
47 |
-
<div className="z-10 absolute" style={{ inset: `${insetBase.icon}px`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
48 |
-
{ (isHumanActive || isAiActive) && <SvgHumanIcon /> /* Placeholder for AI icon if needed */ }
|
49 |
-
</div>
|
50 |
-
</div>
|
51 |
-
</>
|
52 |
-
);
|
53 |
-
};
|
54 |
|
55 |
|
56 |
-
function
|
57 |
const videoRef = useRef<HTMLVideoElement>(null);
|
58 |
-
const [
|
59 |
const [showIOSModal, setShowIOSModal] = useState(false);
|
60 |
const [isAllowedOrigin, setIsAllowedOrigin] = useState<boolean | null>(null);
|
61 |
-
|
62 |
-
// States from new HTML/JS
|
63 |
-
const [isMicActive, setIsMicActive] = useState(false);
|
64 |
-
const [isCamActive, setIsCamActive] = useState(false);
|
65 |
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
|
66 |
-
const
|
|
|
67 |
|
68 |
-
const { connected, connect, disconnect, setMuted, client, currentFacingMode, rotateWebcam, changeStreams } = useLiveAPIContext();
|
69 |
|
70 |
useEffect(() => {
|
71 |
if (isIOS()) {
|
72 |
setShowIOSModal(true);
|
73 |
}
|
74 |
-
|
75 |
try {
|
76 |
if (window.self !== window.top) {
|
77 |
if (window.location.ancestorOrigins && window.location.ancestorOrigins.length > 0) {
|
@@ -88,245 +52,36 @@ function AppInternal() {
|
|
88 |
setIsAllowedOrigin(false);
|
89 |
}
|
90 |
} else {
|
91 |
-
//
|
92 |
-
//
|
93 |
-
|
94 |
-
|
95 |
-
console.warn("App loaded directly or on hf.space. Allowing for dev/test.");
|
96 |
-
setIsAllowedOrigin(true);
|
97 |
-
} else {
|
98 |
-
console.warn("App loaded directly, not in an iframe from allowed origin. Blocking.");
|
99 |
-
setIsAllowedOrigin(false);
|
100 |
-
}
|
101 |
}
|
102 |
} catch (e) {
|
103 |
console.error("Cross-origin access error, cannot verify parent. Blocking.", e);
|
104 |
setIsAllowedOrigin(false);
|
105 |
}
|
106 |
-
}, []);
|
107 |
-
|
108 |
-
// Sync mic active state with Gemini's muted state
|
109 |
-
useEffect(() => {
|
110 |
-
setIsMicActive(connected && !client?.isMuted); // Assuming client has an isMuted property or similar
|
111 |
-
}, [connected, client?.isMuted]);
|
112 |
-
|
113 |
-
// Sync cam active state with appVideoStream
|
114 |
-
useEffect(() => {
|
115 |
-
setIsCamActive(!!appVideoStream);
|
116 |
-
}, [appVideoStream]);
|
117 |
-
|
118 |
-
|
119 |
-
const handleToggleMic = () => {
|
120 |
-
if (!connected) {
|
121 |
-
// Try to connect first if not connected
|
122 |
-
handleConnectToggle().then(() => {
|
123 |
-
setMuted(isMicActive); // Toggle mute state after connection attempt
|
124 |
-
setIsMicActive(!isMicActive);
|
125 |
-
});
|
126 |
-
} else {
|
127 |
-
setMuted(isMicActive); // If already connected, just toggle mute
|
128 |
-
setIsMicActive(!isMicActive);
|
129 |
-
}
|
130 |
-
};
|
131 |
-
|
132 |
-
const handleToggleCam = async () => {
|
133 |
-
if (!isCamActive) {
|
134 |
-
if (!connected) await handleConnectToggle(); // Ensure connection before starting cam
|
135 |
-
// Use the changeStreams function from context/ControlTray
|
136 |
-
if (changeStreams) changeStreams('webcam')();
|
137 |
-
} else {
|
138 |
-
if (changeStreams) changeStreams('none')();
|
139 |
-
}
|
140 |
-
// The isCamActive state will be updated by the useEffect watching appVideoStream
|
141 |
-
};
|
142 |
-
|
143 |
-
const handleSwitchCamera = () => {
|
144 |
-
if (isCamActive && rotateWebcam) {
|
145 |
-
rotateWebcam();
|
146 |
-
}
|
147 |
-
};
|
148 |
-
|
149 |
-
const handleConnectToggle = async () => {
|
150 |
-
try {
|
151 |
-
if (connected) {
|
152 |
-
await disconnect();
|
153 |
-
setIsMicActive(false); // Deactivate mic on disconnect
|
154 |
-
// Camera will be stopped by ControlTray's logic on disconnect
|
155 |
-
} else {
|
156 |
-
await connect();
|
157 |
-
// Mic/Cam can be enabled after connection if desired, or let user do it
|
158 |
-
}
|
159 |
-
} catch (err) {
|
160 |
-
console.error('Connection/Disconnection error:', err);
|
161 |
-
}
|
162 |
-
};
|
163 |
-
|
164 |
-
const handleNotificationToggle = (event?: React.MouseEvent) => {
|
165 |
-
event?.stopPropagation();
|
166 |
-
setIsNotificationOpen(prev => !prev);
|
167 |
-
};
|
168 |
|
169 |
-
|
170 |
const handleClickOutside = (event: MouseEvent) => {
|
171 |
-
|
172 |
-
|
173 |
-
|
|
|
|
|
|
|
174 |
setIsNotificationOpen(false);
|
175 |
}
|
176 |
};
|
177 |
-
|
178 |
-
|
179 |
-
}
|
180 |
return () => {
|
181 |
document.removeEventListener('click', handleClickOutside);
|
182 |
};
|
183 |
-
}, [isNotificationOpen]);
|
184 |
-
|
185 |
-
// Dark mode
|
186 |
-
useEffect(() => {
|
187 |
-
if (isDarkMode) {
|
188 |
-
document.documentElement.classList.add('dark');
|
189 |
-
} else {
|
190 |
-
document.documentElement.classList.remove('dark');
|
191 |
-
}
|
192 |
-
}, [isDarkMode]);
|
193 |
-
|
194 |
-
|
195 |
-
if (isAllowedOrigin === null) {
|
196 |
-
return <div style={{ padding: '20px', textAlign: 'center' }}>در حال بررسی دسترسی...</div>;
|
197 |
-
}
|
198 |
-
|
199 |
-
if (isAllowedOrigin === false) {
|
200 |
-
return <div style={{ padding: '20px', textAlign: 'center', color: 'red' }}>دسترسی غیرمجاز! اگر چت صوتی و تصویری برای شما باز نمیشود این لینک رو با مرورگر کروم باز کنید و همچنین مرورگر کروم رو به عنوان مرورگر پیشفرض گوشی خود قرار دهید تا هر بار زدن روی دکمه شروع داخل برنامه لینک با مرورگر کروم باز بشه، برای پیشفرض قرار دادن مرورگر کروم وارد تنظیمات گوشی خود شوید قسمت برنامه ها ، مدیریت برنامه ها رو کلیک کنید بالای صفحه روی سه نقطه بزنید و تنظیمات بیشتر رو انتخاب کنید بعدا وارد قسمت برنامه های پیش فرض شوید و مرورگر کروم رو به عنوان مرورگر پیشفرض خود قرار دهید، اگر مشکلی بود حتماً به پشتیبانی برنامه پیام بفرستید</div>;
|
201 |
-
}
|
202 |
-
|
203 |
-
return (
|
204 |
-
<div className={`App w-full flex flex-col items-center justify-center min-h-[90dvh] md:min-h-screen antialiased ${isDarkMode ? 'dark' : ''}`}>
|
205 |
-
<div className="max-w-3xl w-full flex flex-col items-center justify-center h-full relative">
|
206 |
-
{/* Header */}
|
207 |
-
<div className="header-controls">
|
208 |
-
<div id="notification-trigger-container" className="notification-trigger-container">
|
209 |
-
<button id="notification-button" aria-label="Notifications" className="header-button" onClick={handleNotificationToggle}>
|
210 |
-
<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>
|
211 |
-
</button>
|
212 |
-
</div>
|
213 |
-
<div className="back-button-container">
|
214 |
-
<div className="header-button" onClick={() => alert('Back clicked (or implement navigation)')}>
|
215 |
-
<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>
|
216 |
-
</div>
|
217 |
-
</div>
|
218 |
-
</div>
|
219 |
-
|
220 |
-
{/* Notification Popover */}
|
221 |
-
<div id="notification-popover-wrapper" className="notification-popover-wrapper">
|
222 |
-
<div
|
223 |
-
id="notification-popover"
|
224 |
-
className={cn("popover-content", {
|
225 |
-
'open animate-popover-open-top-center': isNotificationOpen,
|
226 |
-
'animate-popover-close-top-center': !isNotificationOpen && document.getElementById('notification-popover')?.classList.contains('open') // Only animate close if it was open
|
227 |
-
})}
|
228 |
-
>
|
229 |
-
<div className="notification-popover-text-content">
|
230 |
-
مدلهای هوش مصنوعی میتوانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از وارد کردن اطلاعات حساس بپرهیزید.
|
231 |
-
</div>
|
232 |
-
</div>
|
233 |
-
</div>
|
234 |
-
|
235 |
-
{/* Main Media Area */}
|
236 |
-
<div className="w-full flex flex-col items-center justify-center h-[90dvh] bg-background top-0 left-0 relative">
|
237 |
-
<video
|
238 |
-
id="video-feed"
|
239 |
-
ref={videoRef}
|
240 |
-
autoPlay
|
241 |
-
playsInline
|
242 |
-
className={cn("absolute top-0 left-0 w-full h-full object-cover", {
|
243 |
-
"scale-x-[-1]": currentFacingMode === "user", // Mirror if user facing
|
244 |
-
"hidden": !isCamActive
|
245 |
-
})}
|
246 |
-
/>
|
247 |
-
|
248 |
-
{/* Altair Chat UI - Position and style as needed */}
|
249 |
-
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
250 |
-
{connected && <Altair className="w-full max-w-md p-4 pointer-events-auto" />} {/* Adjust styling and conditional rendering */}
|
251 |
-
</div>
|
252 |
-
|
253 |
-
|
254 |
-
{(!isCamActive && isMicActive) && (
|
255 |
-
<div id="large-logo-container" className="flex items-center justify-center w-full h-full absolute top-0 left-0">
|
256 |
-
<LogoDisplay isMini={false} isHumanActive={true} isAiActive={false} />
|
257 |
-
</div>
|
258 |
-
)}
|
259 |
-
|
260 |
-
{/* Footer Controls */}
|
261 |
-
<div
|
262 |
-
id="footer-controls"
|
263 |
-
className={cn("footer-controls", {
|
264 |
-
"layout-default": !isCamActive,
|
265 |
-
"layout-with-small-logo": isCamActive
|
266 |
-
})}
|
267 |
-
>
|
268 |
-
<div id="cam-button-wrapper" className="relative flex justify-center">
|
269 |
-
<button id="cam-button" className="control-button cam-button-color" onClick={handleToggleCam} disabled={!connected && !isCamActive /* Allow turning off if active but disconnected */}>
|
270 |
-
{isCamActive ? <SvgStopCamIcon /> : <SvgCameraIcon />}
|
271 |
-
</button>
|
272 |
-
<div id="switch-camera-button-container" className={cn("switch-camera-button-container", { "visible": isCamActive && connected })}>
|
273 |
-
<button id="switch-camera-button" aria-label="Switch Camera" className="switch-camera-button-content" onClick={handleSwitchCamera} disabled={!isCamActive || !connected}>
|
274 |
-
<SvgSwitchCameraIcon/>
|
275 |
-
</button>
|
276 |
-
</div>
|
277 |
-
</div>
|
278 |
-
|
279 |
-
{isCamActive && (
|
280 |
-
<div id="small-logo-container" className="flex items-center justify-center">
|
281 |
-
<LogoDisplay isMini={true} isHumanActive={true} isAiActive={false} />
|
282 |
-
</div>
|
283 |
-
)}
|
284 |
-
|
285 |
-
<button id="mic-button" className="control-button mic-button-color" onClick={handleToggleMic} disabled={!connected && !isMicActive}>
|
286 |
-
{isMicActive ? <SvgPauseIcon /> : <SvgMicrophoneIcon />}
|
287 |
-
</button>
|
288 |
-
</div>
|
289 |
-
</div>
|
290 |
-
</div>
|
291 |
-
|
292 |
-
{/* ControlTray is now mostly for logic, not UI, but needs videoRef and stream changes */}
|
293 |
-
<ControlTray
|
294 |
-
videoRef={videoRef}
|
295 |
-
supportsVideo={true} // You can make this dynamic if needed
|
296 |
-
onVideoStreamChange={setAppVideoStream} // Pass the renamed state setter
|
297 |
-
isUiHidden={true} // A new prop to hide its default UI
|
298 |
-
/>
|
299 |
-
|
300 |
-
<IOSModal
|
301 |
-
isOpen={showIOSModal}
|
302 |
-
onClose={() => setShowIOSModal(false)}
|
303 |
-
/>
|
304 |
-
|
305 |
-
{/* Example Dark Mode Toggle - remove or place elsewhere */}
|
306 |
-
<button
|
307 |
-
onClick={() => setIsDarkMode(!isDarkMode)}
|
308 |
-
className="absolute top-4 right-20 p-2 bg-gray-500 text-white rounded z-50"
|
309 |
-
>
|
310 |
-
Toggle Dark
|
311 |
-
</button>
|
312 |
-
{/* Connect/Disconnect Button (from original ControlTray, adapted) */}
|
313 |
-
<button
|
314 |
-
onClick={handleConnectToggle}
|
315 |
-
className="absolute bottom-28 left-1/2 transform -translate-x-1/2 z-50 p-3 rounded-full shadow-lg"
|
316 |
-
style={{ backgroundColor: connected ? 'var(--accent-red)' : 'var(--Blue-500)', color: 'white' }}
|
317 |
-
title={connected ? "Disconnect Stream" : "Connect Stream"}
|
318 |
-
>
|
319 |
-
<span className="material-symbols-outlined filled" style={{ fontSize: '30px' }}>
|
320 |
-
{connected ? "pause" : "play_arrow"}
|
321 |
-
</span>
|
322 |
-
</button>
|
323 |
-
</div>
|
324 |
-
);
|
325 |
-
}
|
326 |
|
|
|
327 |
|
328 |
-
|
329 |
-
const myCustomInstruction = `
|
330 |
ت1. هویت دستیار:
|
331 |
|
332 |
فقط خود را به عنوان "دستیار صوتی و تصویری اپلیکیشن زبانفلای" معرفی کن.
|
@@ -396,22 +151,173 @@ const myCustomInstruction = `
|
|
396 |
|
397 |
سطح سختی واژگان و جملات را بر اساس سطح کاربر (مبتدی، متوسط، پیشرفته) تنظیم کن.".
|
398 |
`.trim();
|
399 |
-
// --- 👆 ---
|
400 |
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
406 |
}
|
407 |
-
};
|
408 |
-
// --- 👆 ---
|
409 |
|
410 |
-
function App() {
|
411 |
return (
|
412 |
-
|
413 |
-
<
|
414 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
415 |
);
|
416 |
}
|
417 |
|
|
|
1 |
+
import React, { useEffect, useRef, useState } from "react";
|
2 |
+
// import "./App.scss"; // ممکن است بخواهید این را کامنت یا حذف کنید اگر با استایلهای جدید تداخل دارد
|
|
|
|
|
|
|
|
|
|
|
3 |
import { LiveAPIProvider } from "./contexts/LiveAPIContext";
|
4 |
+
import SidePanel from "./components/side-panel/SidePanel";
|
5 |
import { Altair } from "./components/altair/Altair";
|
6 |
+
import ControlTray from "./components/control-tray/ControlTray";
|
7 |
import { IOSModal } from "./components/ios-modal/IOSModal";
|
8 |
import { isIOS } from "./lib/platform";
|
9 |
import cn from "classnames";
|
10 |
import { LiveConfig } from "./multimodal-live-types";
|
|
|
11 |
|
12 |
// --- 👇 دامنه مجاز خودتان را اینجا قرار دهید (با https یا http) 👇 ---
|
13 |
+
const ALLOWED_ORIGIN = "https://www.aisada.ir"; // یا "http://www.aisada.ir"; // یا "https://aisada.ir"; // یا "http://aisada.ir" اگر سایتتان http است
|
14 |
// --- 👆 ---
|
15 |
|
16 |
+
// Helper component for SVGs to avoid repetition (or you can use an SVG loader)
|
17 |
+
const SvgIcon = ({ path, viewBox = "0 0 24 24", width = "24", height = "24", className = "" }) => (
|
18 |
+
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox={viewBox} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
19 |
+
{path}
|
20 |
+
</svg>
|
21 |
+
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
|
23 |
|
24 |
+
function App() {
|
25 |
const videoRef = useRef<HTMLVideoElement>(null);
|
26 |
+
const [videoStream, setVideoStream] = useState<MediaStream | null>(null);
|
27 |
const [showIOSModal, setShowIOSModal] = useState(false);
|
28 |
const [isAllowedOrigin, setIsAllowedOrigin] = useState<boolean | null>(null);
|
|
|
|
|
|
|
|
|
29 |
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
|
30 |
+
const notificationPopoverRef = useRef<HTMLDivElement>(null);
|
31 |
+
const notificationButtonRef = useRef<HTMLButtonElement>(null);
|
32 |
|
|
|
33 |
|
34 |
useEffect(() => {
|
35 |
if (isIOS()) {
|
36 |
setShowIOSModal(true);
|
37 |
}
|
38 |
+
|
39 |
try {
|
40 |
if (window.self !== window.top) {
|
41 |
if (window.location.ancestorOrigins && window.location.ancestorOrigins.length > 0) {
|
|
|
52 |
setIsAllowedOrigin(false);
|
53 |
}
|
54 |
} else {
|
55 |
+
// اگر میخواهید مستقیما هم کار کند این خط را true کنید، برای هاگینگ فیس معمولا false بهتر است
|
56 |
+
// setIsAllowedOrigin(true); // For direct loading if needed
|
57 |
+
console.warn("App loaded directly, not in an iframe. Blocking.");
|
58 |
+
setIsAllowedOrigin(false); // Default behavior
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
}
|
60 |
} catch (e) {
|
61 |
console.error("Cross-origin access error, cannot verify parent. Blocking.", e);
|
62 |
setIsAllowedOrigin(false);
|
63 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
|
65 |
+
// Click outside handler for notification
|
66 |
const handleClickOutside = (event: MouseEvent) => {
|
67 |
+
if (
|
68 |
+
notificationPopoverRef.current &&
|
69 |
+
!notificationPopoverRef.current.contains(event.target as Node) &&
|
70 |
+
notificationButtonRef.current &&
|
71 |
+
!notificationButtonRef.current.contains(event.target as Node)
|
72 |
+
) {
|
73 |
setIsNotificationOpen(false);
|
74 |
}
|
75 |
};
|
76 |
+
|
77 |
+
document.addEventListener('click', handleClickOutside);
|
|
|
78 |
return () => {
|
79 |
document.removeEventListener('click', handleClickOutside);
|
80 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
|
82 |
+
}, []);
|
83 |
|
84 |
+
const myCustomInstruction = `
|
|
|
85 |
ت1. هویت دستیار:
|
86 |
|
87 |
فقط خود را به عنوان "دستیار صوتی و تصویری اپلیکیشن زبانفلای" معرفی کن.
|
|
|
151 |
|
152 |
سطح سختی واژگان و جملات را بر اساس سطح کاربر (مبتدی، متوسط، پیشرفته) تنظیم کن.".
|
153 |
`.trim();
|
|
|
154 |
|
155 |
+
const initialAppConfig: LiveConfig = {
|
156 |
+
model: "models/gemini-2.0-flash-exp",
|
157 |
+
systemInstruction: {
|
158 |
+
parts: [{ text: myCustomInstruction }]
|
159 |
+
}
|
160 |
+
};
|
161 |
+
|
162 |
+
const newStyles = `
|
163 |
+
:root {
|
164 |
+
--radius: 0.625rem; /* 10px */
|
165 |
+
--radius-md: 0.5rem; /* 8px */
|
166 |
+
--background: oklch(1 0 0);
|
167 |
+
--foreground: oklch(0.145 0 0);
|
168 |
+
--popover: oklch(1 0 0);
|
169 |
+
--popover-foreground: oklch(0.145 0 0);
|
170 |
+
--border: oklch(0.922 0 0);
|
171 |
+
}
|
172 |
+
.dark {
|
173 |
+
--background: oklch(0.145 0 0);
|
174 |
+
--foreground: oklch(0.985 0 0);
|
175 |
+
--popover: oklch(0.205 0 0);
|
176 |
+
--popover-foreground: oklch(0.985 0 0);
|
177 |
+
--border: oklch(1 0 0 / 10%);
|
178 |
+
}
|
179 |
+
body { background-color: var(--background); color: var(--foreground); overflow-x: hidden; }
|
180 |
+
* { border-color: var(--border); /* Tailwind applies border-border */ }
|
181 |
+
|
182 |
+
.notification-popover-wrapper {
|
183 |
+
position: fixed; top: 1rem; left: 50%;
|
184 |
+
transform: translateX(-50%); z-index: 100;
|
185 |
+
width: calc(100% - 2rem); max-width: 28rem;
|
186 |
+
display: flex; justify-content: center; pointer-events: none;
|
187 |
+
}
|
188 |
+
.popover-content {
|
189 |
+
width: 100%; border-radius: var(--radius-md, 0.5rem);
|
190 |
+
border-width: 1px; border-color: var(--border);
|
191 |
+
background-color: var(--popover); color: var(--popover-foreground);
|
192 |
+
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
193 |
+
outline: none;
|
194 |
+
transition: opacity 0.3s ease-out, transform 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
195 |
+
opacity: 0; transform: translateY(-100%) scale(0.9); pointer-events: none;
|
196 |
+
}
|
197 |
+
.popover-content.open {
|
198 |
+
opacity: 1; transform: translateY(0) scale(1); pointer-events: auto;
|
199 |
+
}
|
200 |
+
.notification-popover-text-content {
|
201 |
+
background-color: #eff6ff; font-size: 0.875rem; line-height: 1.5rem; direction: rtl;
|
202 |
+
padding: 1rem; border-radius: var(--radius-md, 0.5rem);
|
203 |
+
color: oklch(0.145 0 0);
|
204 |
+
}
|
205 |
+
.dark .notification-popover-text-content {
|
206 |
+
background-color: oklch(0.25 0.05 230); color: oklch(0.95 0.01 230);
|
207 |
+
}
|
208 |
+
.header-controls {
|
209 |
+
display: flex; padding: 1rem; justify-content: space-between; align-items: center;
|
210 |
+
width: 100%; position: absolute; top: 0; left: 0; z-index: 10;
|
211 |
+
}
|
212 |
+
.header-button {
|
213 |
+
display: flex; align-items: center; justify-content: center;
|
214 |
+
padding: 0.5rem; border-radius: var(--radius-lg, 0.625rem);
|
215 |
+
background-color: #e5e7eb; /* bg-gray-200 */
|
216 |
+
cursor: pointer; transition: background-color 0.2s;
|
217 |
+
}
|
218 |
+
.header-button:hover { background-color: #d1d5db; /* bg-gray-300 */ }
|
219 |
+
.header-button svg { opacity: 0.7; stroke: #374151 /* gray-700 */; }
|
220 |
+
.dark .header-button { background-color: oklch(0.28 0 0); }
|
221 |
+
.dark .header-button:hover { background-color: oklch(0.35 0 0); }
|
222 |
+
.dark .header-button svg { opacity: 0.8; stroke: oklch(0.85 0 0); }
|
223 |
+
|
224 |
+
/* Styles for ControlTray will be in ControlTray.tsx or its own CSS if preferred */
|
225 |
+
/* Ensure .App container itself doesn't constrain width too much if SidePanel is present */
|
226 |
+
.App { display: flex; flex-direction: column; min-height: 100vh; }
|
227 |
+
.streaming-console { flex-grow: 1; display: flex; } /* Assuming SidePanel and main are siblings */
|
228 |
+
main { flex-grow: 1; position: relative; /* For positioning ControlTray */ }
|
229 |
+
.main-app-area { position: relative; width: 100%; height: 100%; /* Or specific height like 90dvh */ }
|
230 |
+
|
231 |
+
/* Hide original Altair styling if it conflicts */
|
232 |
+
/* .user-query-container, .system-response-container { display: none !important; } */
|
233 |
+
`;
|
234 |
+
|
235 |
+
if (isAllowedOrigin === null) {
|
236 |
+
return <div style={{ padding: '20px', textAlign: 'center' }}>در حال بررسی دسترسی...</div>;
|
237 |
+
}
|
238 |
+
|
239 |
+
if (isAllowedOrigin === false) {
|
240 |
+
return <div style={{ padding: '20px', textAlign: 'center', color: 'red' }}>دسترسی غیرمجاز! اگر چت صوتی و تصویری برای شما باز نمیشود این لینک رو با مرورگر کروم باز کنید و همچنین مرورگر کروم رو به عنوان مرورگر پیشفرض گوشی خود قرار دهید تا هر بار زدن روی دکمه شروع داخل برنامه لینک با مرورگر کروم باز بشه، برای پیشفرض قرار دادن مرورگر کروم وارد تنظیمات گوشی خود شوید قسمت برنامه ها ، مدیریت برنامه ها رو کلیک کنید بالای صفحه روی سه نقطه بزنید و تنظیمات بیشتر رو انتخاب کنید بعدا وارد قسمت برنامه های پیش فرض شوید و مرورگر کروم رو به عنوان مرورگر پیشفرض خود قرار دهید، اگر مشکلی بود حتماً به پشتیبانی برنامه پیام بفرستید</div>;
|
241 |
}
|
|
|
|
|
242 |
|
|
|
243 |
return (
|
244 |
+
<>
|
245 |
+
<style>{newStyles}</style> {/* Injecting new styles */}
|
246 |
+
<div className="App antialiased"> {/* Added antialiased from body */}
|
247 |
+
<LiveAPIProvider initialConfig={initialAppConfig}>
|
248 |
+
{/* Header Controls from new HTML */}
|
249 |
+
<div className="header-controls">
|
250 |
+
<div className="notification-trigger-container">
|
251 |
+
<button
|
252 |
+
ref={notificationButtonRef}
|
253 |
+
id="notification-button"
|
254 |
+
aria-label="Notifications"
|
255 |
+
className="header-button"
|
256 |
+
onClick={(e) => {
|
257 |
+
e.stopPropagation();
|
258 |
+
setIsNotificationOpen(!isNotificationOpen);
|
259 |
+
}}
|
260 |
+
>
|
261 |
+
<SvgIcon path={<><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></>} />
|
262 |
+
</button>
|
263 |
+
</div>
|
264 |
+
<div className="back-button-container">
|
265 |
+
<div className="header-button" onClick={() => alert('Back clicked (ZabanFly)')}> {/* Replace alert with actual back logic */}
|
266 |
+
<SvgIcon path={<path d="m15 18-6-6 6-6"></path>} width="20" height="20" />
|
267 |
+
</div>
|
268 |
+
</div>
|
269 |
+
</div>
|
270 |
+
|
271 |
+
{/* Notification Popover */}
|
272 |
+
<div id="notification-popover-wrapper" className="notification-popover-wrapper">
|
273 |
+
<div
|
274 |
+
ref={notificationPopoverRef}
|
275 |
+
id="notification-popover"
|
276 |
+
className={cn("popover-content", { open: isNotificationOpen })}
|
277 |
+
// Animation classes can be controlled by 'open' state via Tailwind or CSS transitions
|
278 |
+
>
|
279 |
+
<div className="notification-popover-text-content">
|
280 |
+
مدلهای هوش مصنوعی میتوانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از وارد کردن اطلاعات حساس بپرهیزید.
|
281 |
+
</div>
|
282 |
+
</div>
|
283 |
+
</div>
|
284 |
+
|
285 |
+
<div className="streaming-console">
|
286 |
+
<SidePanel /> {/* Kept SidePanel, might need style adjustments */}
|
287 |
+
<main>
|
288 |
+
<div className={cn("main-app-area w-full flex flex-col items-center justify-center min-h-[90dvh] md:min-h-screen bg-background top-0 left-0 relative")}>
|
289 |
+
<Altair /> {/* This is where conversation happens */}
|
290 |
+
<video
|
291 |
+
id="video-feed" // From new HTML
|
292 |
+
className={cn(
|
293 |
+
"absolute top-0 left-0 w-full h-full object-cover scale-x-[-1]",
|
294 |
+
{ hidden: !videoStream } // Controlled by videoStream state
|
295 |
+
)}
|
296 |
+
ref={videoRef}
|
297 |
+
autoPlay
|
298 |
+
playsInline
|
299 |
+
/>
|
300 |
+
{/* large-logo-container might be managed by ControlTray or here based on cam/mic state */}
|
301 |
+
<div id="large-logo-container" className="hidden absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
302 |
+
{/* Content for large logo will be dynamic based on mic state if cam is off */}
|
303 |
+
</div>
|
304 |
+
</div>
|
305 |
+
<ControlTray
|
306 |
+
videoRef={videoRef}
|
307 |
+
supportsVideo={true}
|
308 |
+
onVideoStreamChange={setVideoStream}
|
309 |
+
// Pass any other necessary props from new HTML logic if ControlTray needs them
|
310 |
+
/>
|
311 |
+
</main>
|
312 |
+
</div>
|
313 |
+
</LiveAPIProvider>
|
314 |
+
|
315 |
+
<IOSModal
|
316 |
+
isOpen={showIOSModal}
|
317 |
+
onClose={() => setShowIOSModal(false)}
|
318 |
+
/>
|
319 |
+
</div>
|
320 |
+
</>
|
321 |
);
|
322 |
}
|
323 |
|