Spaces:
Running
Running
Update src/App.tsx
Browse files- src/App.tsx +58 -68
src/App.tsx
CHANGED
@@ -12,20 +12,17 @@ limitations under the License.
|
|
12 |
*/
|
13 |
|
14 |
import React, { useEffect, useRef, useState, useCallback } from "react";
|
15 |
-
import "./App.scss";
|
16 |
import { LiveAPIProvider, useLiveAPIContext } from "./contexts/LiveAPIContext";
|
17 |
-
// SidePanel and Altair are part of the original Gemini UI.
|
18 |
-
// You need to decide if/how they fit into the new UI.
|
19 |
-
// For now, we'll include them but they might need styling or to be conditionally rendered.
|
20 |
// import SidePanel from "./components/side-panel/SidePanel"; // اگر استفاده نمیکنید کامنت کنید
|
21 |
import { Altair } from "./components/altair/Altair";
|
22 |
-
import ControlTray from "./components/control-tray/ControlTray";
|
23 |
import { IOSModal } from "./components/ios-modal/IOSModal";
|
24 |
import { isIOS } from "./lib/platform";
|
25 |
import { LiveConfig } from "./multimodal-live-types";
|
26 |
|
27 |
-
// --- 👇 دامنه
|
28 |
-
const
|
29 |
// --- 👆 ---
|
30 |
|
31 |
// --- 👇 دستورالعمل شخصیسازی شما (بدون تغییر) 👇 ---
|
@@ -101,16 +98,13 @@ const myCustomInstruction = `
|
|
101 |
`.trim();
|
102 |
// --- 👆 ---
|
103 |
|
104 |
-
// --- 👇 تنظیمات اولیه (بدون تغییر) 👇 ---
|
105 |
const initialAppConfig: LiveConfig = {
|
106 |
-
model: "models/gemini-2.0-flash-exp",
|
107 |
systemInstruction: {
|
108 |
parts: [{ text: myCustomInstruction }]
|
109 |
}
|
110 |
};
|
111 |
-
// --- 👆 ---
|
112 |
|
113 |
-
// SVG Icons (as React components or functions returning JSX)
|
114 |
const NotificationIcon = () => (
|
115 |
<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>
|
116 |
);
|
@@ -124,19 +118,16 @@ const HumanIconSVG = ({ width = 70, height = 70 }: { width?: number, height?: nu
|
|
124 |
);
|
125 |
|
126 |
const LogoDisplay = ({ isMini, isHumanActive }: { isMini: boolean, isHumanActive: boolean }) => {
|
127 |
-
if (!isHumanActive) return null;
|
128 |
-
|
129 |
const size = isMini ? 80 : 200;
|
130 |
const iconSize = isMini ? 35 : 70;
|
131 |
const insetBase = isMini
|
132 |
-
? { ping: 10, outer: 0, mid: 5, inner: 12, icon: 22.5 }
|
133 |
-
: { ping: 40, outer: 0, mid: 20, inner: 50, icon: 65 };
|
134 |
-
const bgColorBase = 'blue';
|
135 |
|
136 |
return (
|
137 |
<div className="relative" style={{ width: `${size}px`, height: `${size}px` }}>
|
138 |
-
{/* These divs are for Tailwind JIT, not strictly needed if classes are used directly */}
|
139 |
-
{/* <div className="hidden bg-blue-200 bg-blue-300 bg-blue-400"></div> */}
|
140 |
<div className={`absolute rounded-full opacity-50 animate-ping bg-${bgColorBase}-200 dark:bg-${bgColorBase}-700`} style={{ inset: `${insetBase.ping}px` }}></div>
|
141 |
<div className={`absolute rounded-full opacity-50 bg-${bgColorBase}-200 dark:bg-${bgColorBase}-700`} style={{ inset: `${insetBase.outer}px` }}></div>
|
142 |
<div className={`absolute rounded-full opacity-50 bg-${bgColorBase}-300 dark:bg-${bgColorBase}-600`} style={{ inset: `${insetBase.mid}px` }}></div>
|
@@ -148,54 +139,57 @@ const LogoDisplay = ({ isMini, isHumanActive }: { isMini: boolean, isHumanActive
|
|
148 |
);
|
149 |
};
|
150 |
|
151 |
-
|
152 |
function AppContent() {
|
153 |
const videoRef = useRef<HTMLVideoElement>(null);
|
154 |
-
const [videoStream, setVideoStream] = useState<MediaStream | null>(null);
|
155 |
const [showIOSModal, setShowIOSModal] = useState(false);
|
156 |
-
const [isAllowedOrigin, setIsAllowedOrigin] = useState<boolean | null>(null);
|
157 |
-
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
|
158 |
const notificationPopoverRef = useRef<HTMLDivElement>(null);
|
159 |
const notificationButtonRef = useRef<HTMLButtonElement>(null);
|
160 |
|
161 |
-
|
162 |
-
const [
|
163 |
-
const [isCamActive, setIsCamActive] = useState(false); // Is camera capturing
|
164 |
|
165 |
-
const { connected, volume } = useLiveAPIContext();
|
166 |
|
167 |
useEffect(() => {
|
168 |
if (isIOS()) {
|
169 |
setShowIOSModal(true);
|
170 |
}
|
171 |
|
|
|
172 |
try {
|
173 |
-
if (window.self !== window.top) {
|
174 |
if (window.location.ancestorOrigins && window.location.ancestorOrigins.length > 0) {
|
175 |
const parentOrigin = window.location.ancestorOrigins[0];
|
176 |
-
|
177 |
-
|
|
|
|
|
178 |
} else {
|
179 |
-
console.
|
180 |
-
setIsAllowedOrigin(
|
181 |
}
|
182 |
} else {
|
183 |
-
|
184 |
-
|
|
|
|
|
185 |
}
|
186 |
-
} else {
|
187 |
-
console.
|
188 |
-
setIsAllowedOrigin(
|
189 |
}
|
190 |
} catch (e) {
|
191 |
-
|
192 |
-
|
|
|
|
|
|
|
|
|
193 |
}
|
|
|
194 |
|
195 |
-
// Dark mode toggle for testing (can be removed or integrated with a theme switcher)
|
196 |
-
// document.documentElement.classList.add('dark');
|
197 |
-
|
198 |
-
// Click outside listener for notification popover
|
199 |
const handleClickOutside = (event: MouseEvent) => {
|
200 |
if (isNotificationOpen &&
|
201 |
notificationPopoverRef.current &&
|
@@ -213,10 +207,8 @@ function AppContent() {
|
|
213 |
|
214 |
}, [isNotificationOpen]);
|
215 |
|
216 |
-
|
217 |
const handleVideoStreamChange = useCallback((stream: MediaStream | null) => {
|
218 |
-
|
219 |
-
setIsCamActive(!!stream); // Update cam active state based on stream
|
220 |
if (videoRef.current) {
|
221 |
videoRef.current.srcObject = stream;
|
222 |
if (stream) {
|
@@ -224,32 +216,35 @@ function AppContent() {
|
|
224 |
}
|
225 |
}
|
226 |
}, []);
|
227 |
-
|
228 |
useEffect(() => {
|
229 |
-
// This
|
230 |
-
// but ControlTray.tsx now handles its own inVolume for that.
|
231 |
-
// This 'volume' from useLiveAPIContext is usually output volume.
|
232 |
-
// If you need to visualize output volume elsewhere, you can use it.
|
233 |
}, [volume]);
|
234 |
|
235 |
-
|
236 |
if (isAllowedOrigin === null) {
|
237 |
return <div style={{ padding: '20px', textAlign: 'center' }}>در حال بررسی دسترسی...</div>;
|
238 |
}
|
239 |
|
|
|
240 |
if (isAllowedOrigin === false) {
|
241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
242 |
}
|
|
|
243 |
|
|
|
244 |
return (
|
245 |
-
<div className="App">
|
246 |
-
{/* <SidePanel />
|
247 |
-
<main>
|
248 |
-
<div className="main-app-area">
|
249 |
-
{/* Altair is the chat bubbles. Position it as needed. */}
|
250 |
<Altair />
|
251 |
-
|
252 |
-
{/* Header */}
|
253 |
<div className="header-controls">
|
254 |
<button
|
255 |
id="notification-button"
|
@@ -268,7 +263,6 @@ function AppContent() {
|
|
268 |
</button>
|
269 |
</div>
|
270 |
|
271 |
-
{/* Notification Popover */}
|
272 |
<div id="notification-popover-wrapper" className="notification-popover-wrapper">
|
273 |
<div
|
274 |
id="notification-popover"
|
@@ -281,7 +275,6 @@ function AppContent() {
|
|
281 |
</div>
|
282 |
</div>
|
283 |
|
284 |
-
{/* Media Area (Video feed and Logos) */}
|
285 |
<div className="media-toggle-area">
|
286 |
<video
|
287 |
id="video-feed"
|
@@ -290,23 +283,20 @@ function AppContent() {
|
|
290 |
playsInline
|
291 |
className={`video-feed ${!isCamActive ? 'hidden' : ''}`}
|
292 |
/>
|
293 |
-
{!isCamActive && isMicActive && (
|
294 |
<div id="large-logo-container" className="large-logo-container">
|
295 |
<LogoDisplay isMini={false} isHumanActive={true} />
|
296 |
</div>
|
297 |
)}
|
298 |
-
{/* The small logo is part of the footer logic in ControlTray */}
|
299 |
</div>
|
300 |
</div>
|
301 |
|
302 |
-
{/* Footer Controls */}
|
303 |
<ControlTray
|
304 |
-
videoRef={videoRef}
|
305 |
onVideoStreamChange={handleVideoStreamChange}
|
306 |
-
// Pass setters for App to know mic/cam state for UI logic (e.g., logos)
|
307 |
setIsMicActive={setIsMicActive}
|
308 |
-
setIsCamActiveExternal={setIsCamActive}
|
309 |
-
isCamActiveApp={isCamActive}
|
310 |
/>
|
311 |
</main>
|
312 |
|
|
|
12 |
*/
|
13 |
|
14 |
import React, { useEffect, useRef, useState, useCallback } from "react";
|
15 |
+
import "./App.scss";
|
16 |
import { LiveAPIProvider, useLiveAPIContext } from "./contexts/LiveAPIContext";
|
|
|
|
|
|
|
17 |
// import SidePanel from "./components/side-panel/SidePanel"; // اگر استفاده نمیکنید کامنت کنید
|
18 |
import { Altair } from "./components/altair/Altair";
|
19 |
+
import ControlTray from "./components/control-tray/ControlTray";
|
20 |
import { IOSModal } from "./components/ios-modal/IOSModal";
|
21 |
import { isIOS } from "./lib/platform";
|
22 |
import { LiveConfig } from "./multimodal-live-types";
|
23 |
|
24 |
+
// --- 👇 دامنه **ممنوع** شما 👇 ---
|
25 |
+
const FORBIDDEN_ORIGIN = "https://www.aisada.ir"; // یا "http://www.aisada.ir";
|
26 |
// --- 👆 ---
|
27 |
|
28 |
// --- 👇 دستورالعمل شخصیسازی شما (بدون تغییر) 👇 ---
|
|
|
98 |
`.trim();
|
99 |
// --- 👆 ---
|
100 |
|
|
|
101 |
const initialAppConfig: LiveConfig = {
|
102 |
+
model: "models/gemini-2.0-flash-exp",
|
103 |
systemInstruction: {
|
104 |
parts: [{ text: myCustomInstruction }]
|
105 |
}
|
106 |
};
|
|
|
107 |
|
|
|
108 |
const NotificationIcon = () => (
|
109 |
<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>
|
110 |
);
|
|
|
118 |
);
|
119 |
|
120 |
const LogoDisplay = ({ isMini, isHumanActive }: { isMini: boolean, isHumanActive: boolean }) => {
|
121 |
+
if (!isHumanActive) return null;
|
|
|
122 |
const size = isMini ? 80 : 200;
|
123 |
const iconSize = isMini ? 35 : 70;
|
124 |
const insetBase = isMini
|
125 |
+
? { ping: 10, outer: 0, mid: 5, inner: 12, icon: 22.5 }
|
126 |
+
: { ping: 40, outer: 0, mid: 20, inner: 50, icon: 65 };
|
127 |
+
const bgColorBase = 'blue';
|
128 |
|
129 |
return (
|
130 |
<div className="relative" style={{ width: `${size}px`, height: `${size}px` }}>
|
|
|
|
|
131 |
<div className={`absolute rounded-full opacity-50 animate-ping bg-${bgColorBase}-200 dark:bg-${bgColorBase}-700`} style={{ inset: `${insetBase.ping}px` }}></div>
|
132 |
<div className={`absolute rounded-full opacity-50 bg-${bgColorBase}-200 dark:bg-${bgColorBase}-700`} style={{ inset: `${insetBase.outer}px` }}></div>
|
133 |
<div className={`absolute rounded-full opacity-50 bg-${bgColorBase}-300 dark:bg-${bgColorBase}-600`} style={{ inset: `${insetBase.mid}px` }}></div>
|
|
|
139 |
);
|
140 |
};
|
141 |
|
|
|
142 |
function AppContent() {
|
143 |
const videoRef = useRef<HTMLVideoElement>(null);
|
|
|
144 |
const [showIOSModal, setShowIOSModal] = useState(false);
|
145 |
+
const [isAllowedOrigin, setIsAllowedOrigin] = useState<boolean | null>(null); // true: allowed, false: blocked
|
146 |
+
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
|
147 |
const notificationPopoverRef = useRef<HTMLDivElement>(null);
|
148 |
const notificationButtonRef = useRef<HTMLButtonElement>(null);
|
149 |
|
150 |
+
const [isMicActive, setIsMicActive] = useState(false);
|
151 |
+
const [isCamActive, setIsCamActive] = useState(false);
|
|
|
152 |
|
153 |
+
const { connected, volume } = useLiveAPIContext();
|
154 |
|
155 |
useEffect(() => {
|
156 |
if (isIOS()) {
|
157 |
setShowIOSModal(true);
|
158 |
}
|
159 |
|
160 |
+
// --- 👇 منطق بررسی دامنه مجاز (معکوس شده) 👇 ---
|
161 |
try {
|
162 |
+
if (window.self !== window.top) { // App is in an iframe
|
163 |
if (window.location.ancestorOrigins && window.location.ancestorOrigins.length > 0) {
|
164 |
const parentOrigin = window.location.ancestorOrigins[0];
|
165 |
+
console.log("Parent Origin:", parentOrigin);
|
166 |
+
if (parentOrigin === FORBIDDEN_ORIGIN) {
|
167 |
+
console.warn(`Blocked load from forbidden origin: ${parentOrigin}`);
|
168 |
+
setIsAllowedOrigin(false); // Block if from forbidden origin
|
169 |
} else {
|
170 |
+
console.log(`Allowed load from origin: ${parentOrigin}`);
|
171 |
+
setIsAllowedOrigin(true); // Allow if from any other origin
|
172 |
}
|
173 |
} else {
|
174 |
+
// Cannot verify parent origin, but it's in an iframe.
|
175 |
+
// For "allow everywhere except specific site", we can assume allow here.
|
176 |
+
console.warn("Cannot verify parent origin (ancestorOrigins not available/empty), but in iframe. Allowing.");
|
177 |
+
setIsAllowedOrigin(true);
|
178 |
}
|
179 |
+
} else { // App loaded directly, not in an iframe
|
180 |
+
console.log("App loaded directly. Allowing.");
|
181 |
+
setIsAllowedOrigin(true); // Allow if loaded directly
|
182 |
}
|
183 |
} catch (e) {
|
184 |
+
// Cross-origin access error. This usually happens if the iframe has sandbox attributes
|
185 |
+
// that prevent accessing ancestorOrigins. In this "allow everywhere except" scenario,
|
186 |
+
// it's safer to allow if we can't determine, or block if you prefer higher security.
|
187 |
+
// Let's allow for wider compatibility, assuming it's not the forbidden one.
|
188 |
+
console.error("Cross-origin access error, cannot verify parent. Allowing by default.", e);
|
189 |
+
setIsAllowedOrigin(true);
|
190 |
}
|
191 |
+
// --- 👆 ---
|
192 |
|
|
|
|
|
|
|
|
|
193 |
const handleClickOutside = (event: MouseEvent) => {
|
194 |
if (isNotificationOpen &&
|
195 |
notificationPopoverRef.current &&
|
|
|
207 |
|
208 |
}, [isNotificationOpen]);
|
209 |
|
|
|
210 |
const handleVideoStreamChange = useCallback((stream: MediaStream | null) => {
|
211 |
+
setIsCamActive(!!stream);
|
|
|
212 |
if (videoRef.current) {
|
213 |
videoRef.current.srcObject = stream;
|
214 |
if (stream) {
|
|
|
216 |
}
|
217 |
}
|
218 |
}, []);
|
219 |
+
|
220 |
useEffect(() => {
|
221 |
+
// This effect is for potential output volume visualization, not currently used for mic pulse
|
|
|
|
|
|
|
222 |
}, [volume]);
|
223 |
|
|
|
224 |
if (isAllowedOrigin === null) {
|
225 |
return <div style={{ padding: '20px', textAlign: 'center' }}>در حال بررسی دسترسی...</div>;
|
226 |
}
|
227 |
|
228 |
+
// --- 👇 رندر شرطی با پیام خطای جدید 👇 ---
|
229 |
if (isAllowedOrigin === false) {
|
230 |
+
// این پیام زمانی نمایش داده میشود که برنامه در iframe سایت ممنوعه (aisada.ir) بارگذاری شود.
|
231 |
+
return (
|
232 |
+
<div style={{ padding: '20px', textAlign: 'center', color: 'red', direction: 'rtl' }}>
|
233 |
+
دسترسی به این محتوا از دامنه شما ({FORBIDDEN_ORIGIN}) مجاز نمیباشد.
|
234 |
+
<br />
|
235 |
+
لطفا این اسپیس را مستقیما یا از طریق دامنههای مجاز دیگر باز کنید.
|
236 |
+
</div>
|
237 |
+
);
|
238 |
}
|
239 |
+
// --- 👆 ---
|
240 |
|
241 |
+
// اگر isAllowedOrigin === true بود، برنامه اصلی رندر میشود
|
242 |
return (
|
243 |
+
<div className="App">
|
244 |
+
{/* <SidePanel /> */}
|
245 |
+
<main>
|
246 |
+
<div className="main-app-area">
|
|
|
247 |
<Altair />
|
|
|
|
|
248 |
<div className="header-controls">
|
249 |
<button
|
250 |
id="notification-button"
|
|
|
263 |
</button>
|
264 |
</div>
|
265 |
|
|
|
266 |
<div id="notification-popover-wrapper" className="notification-popover-wrapper">
|
267 |
<div
|
268 |
id="notification-popover"
|
|
|
275 |
</div>
|
276 |
</div>
|
277 |
|
|
|
278 |
<div className="media-toggle-area">
|
279 |
<video
|
280 |
id="video-feed"
|
|
|
283 |
playsInline
|
284 |
className={`video-feed ${!isCamActive ? 'hidden' : ''}`}
|
285 |
/>
|
286 |
+
{!isCamActive && isMicActive && (
|
287 |
<div id="large-logo-container" className="large-logo-container">
|
288 |
<LogoDisplay isMini={false} isHumanActive={true} />
|
289 |
</div>
|
290 |
)}
|
|
|
291 |
</div>
|
292 |
</div>
|
293 |
|
|
|
294 |
<ControlTray
|
295 |
+
videoRef={videoRef}
|
296 |
onVideoStreamChange={handleVideoStreamChange}
|
|
|
297 |
setIsMicActive={setIsMicActive}
|
298 |
+
setIsCamActiveExternal={setIsCamActive}
|
299 |
+
isCamActiveApp={isCamActive}
|
300 |
/>
|
301 |
</main>
|
302 |
|