Spaces:
Running
Running
Update src/App.tsx
Browse files- src/App.tsx +160 -148
src/App.tsx
CHANGED
@@ -1,4 +1,16 @@
|
|
1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
import React, { useEffect, useRef, useState } from "react";
|
3 |
import './App.scss';
|
4 |
import { LiveAPIProvider, useLiveAPIContext } from "./contexts/LiveAPIContext";
|
@@ -107,109 +119,119 @@ const SvgReferenceMicrophoneIcon = () => (
|
|
107 |
</svg>
|
108 |
);
|
109 |
|
110 |
-
const AppInternalLogic: React.FC<{
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
</div>
|
159 |
</div>
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
className=
|
165 |
-
|
166 |
-
"animate-popover-close-top-center": !isNotificationOpen && document.getElementById('notification-popover')?.classList.contains('open'),
|
167 |
-
})}
|
168 |
>
|
169 |
-
<
|
170 |
-
|
171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
</div>
|
173 |
</div>
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
/>
|
186 |
-
{isMicActive && !isCamActive && (
|
187 |
-
<div
|
188 |
-
id="large-logo-container"
|
189 |
-
className="absolute top-0 left-0 w-full h-full flex items-center justify-center pointer-events-none"
|
190 |
-
>
|
191 |
-
{createLogoFunction(false, true, 'human', false)}
|
192 |
-
</div>
|
193 |
)}
|
194 |
-
</div>
|
195 |
-
|
196 |
-
<ControlTray
|
197 |
-
videoRef={videoRef}
|
198 |
-
supportsVideo={true}
|
199 |
-
onVideoStreamChange={(stream) => { /* ... */ }}
|
200 |
-
isAppMicActive={isMicActive}
|
201 |
-
onAppMicToggle={setIsMicActive}
|
202 |
-
isAppCamActive={isCamActive}
|
203 |
-
onAppCamToggle={setIsCamActive}
|
204 |
-
createLogoFunction={createLogoFunction}
|
205 |
-
ReferenceMicrophoneIcon={SvgReferenceMicrophoneIcon}
|
206 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
207 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
208 |
</div>
|
209 |
-
|
|
|
210 |
};
|
211 |
|
212 |
-
const logoColorConfig = {
|
213 |
blue: {
|
214 |
ping: "bg-blue-200 dark:bg-blue-700",
|
215 |
outer: "bg-blue-200 dark:bg-blue-700",
|
@@ -230,51 +252,50 @@ const logoColorConfig = { /* ... (بدون تغییر) ... */
|
|
230 |
}
|
231 |
};
|
232 |
|
233 |
-
function App() {
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
}
|
249 |
-
const timer = setTimeout(() => {
|
250 |
-
setIsAllowedOrigin(true);
|
251 |
-
}, 100);
|
252 |
-
return () => clearTimeout(timer);
|
253 |
-
}, []);
|
254 |
-
|
255 |
-
useEffect(() => {
|
256 |
-
const handleClickOutside = (event: MouseEvent) => {
|
257 |
-
if (
|
258 |
-
isNotificationOpen &&
|
259 |
-
notificationPopoverRef.current &&
|
260 |
-
!notificationPopoverRef.current.contains(event.target as Node) &&
|
261 |
-
notificationButtonRef.current &&
|
262 |
-
!notificationButtonRef.current.contains(event.target as Node)
|
263 |
-
) {
|
264 |
-
setIsNotificationOpen(false);
|
265 |
-
}
|
266 |
-
};
|
267 |
-
document.addEventListener("mousedown", handleClickOutside);
|
268 |
-
return () => {
|
269 |
-
document.removeEventListener("mousedown", handleClickOutside);
|
270 |
-
};
|
271 |
-
}, [isNotificationOpen]);
|
272 |
-
|
273 |
-
if (isAllowedOrigin === null) {
|
274 |
-
return <div style={{ padding: '20px', textAlign: 'center' }}>در حال بررسی دسترسی...</div>;
|
275 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
276 |
|
277 |
-
// *** MODIFIED: createLogoFunction با دقت بیشتر در inset ها و کلاسها ***
|
278 |
const createLogoFunction = (isMini: boolean, isActive: boolean, type: 'human' | 'ai' = 'human', forFooter: boolean = false) => {
|
279 |
if (!isActive) return null;
|
280 |
|
@@ -285,13 +306,9 @@ function App() { /* ... (useEffect ها و state ها بدون تغییر) ... *
|
|
285 |
const iconDisplaySize = isMini ? 35 : 70;
|
286 |
const iconInset = (size - iconDisplaySize) / 2;
|
287 |
|
288 |
-
// مقادیر inset برای حلقهها از HTML مرجع شما:
|
289 |
-
// بزرگ: ping: 40, outer: 0, mid: 20, inner: 50, icon: 65
|
290 |
-
// کوچک: ping: 10, outer: 0, mid: 5, inner: 12, icon: 22
|
291 |
-
// ما iconInset را خودمان محاسبه کردیم، بقیه را از HTML میگیریم.
|
292 |
const insetValues = {
|
293 |
ping: isMini ? 10 : 40,
|
294 |
-
outer: 0,
|
295 |
mid: isMini ? 5 : 20,
|
296 |
inner: isMini ? 12 : 50,
|
297 |
icon: iconInset
|
@@ -301,15 +318,10 @@ function App() { /* ... (useEffect ها و state ها بدون تغییر) ... *
|
|
301 |
|
302 |
return (
|
303 |
<div className={cn("logo-animation-wrapper", {"for-footer": forFooter})} style={{ width: `${size}px`, height: `${size}px` }}>
|
304 |
-
{/* حلقه پینگ */}
|
305 |
<div className={`absolute rounded-full opacity-50 animate-ping ${currentColors.ping}`} style={{ inset: `${insetValues.ping}px` }}></div>
|
306 |
-
{/* حلقه بیرونی */}
|
307 |
<div className={`absolute rounded-full opacity-50 ${currentColors.outer}`} style={{ inset: `${insetValues.outer}px` }}></div>
|
308 |
-
{/* حلقه میانی */}
|
309 |
<div className={`absolute rounded-full opacity-50 ${currentColors.mid}`} style={{ inset: `${insetValues.mid}px` }}></div>
|
310 |
-
{/* حلقه داخلی */}
|
311 |
<div className={`absolute rounded-full opacity-50 ${currentColors.inner}`} style={{ inset: `${insetValues.inner}px` }}></div>
|
312 |
-
{/* کانتینر آیکون */}
|
313 |
<div className="z-10 absolute flex items-center justify-center" style={{ inset: `${insetValues.icon}px`, width: `${iconDisplaySize}px`, height: `${iconDisplaySize}px` }}>
|
314 |
{IconComponent && <IconComponent />}
|
315 |
</div>
|
@@ -317,7 +329,7 @@ function App() { /* ... (useEffect ها و state ها بدون تغییر) ... *
|
|
317 |
);
|
318 |
};
|
319 |
|
320 |
-
return (
|
321 |
<LiveAPIProvider initialConfig={initialAppConfig}>
|
322 |
<AppInternalLogic
|
323 |
isMicActive={isMicActive}
|
|
|
1 |
+
/**
|
2 |
+
Copyright 2024 Google LLC
|
3 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4 |
+
you may not use this file except in compliance with the License.
|
5 |
+
You may obtain a copy of the License at
|
6 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
7 |
+
Unless required by applicable law or agreed to in writing, software
|
8 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
9 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
10 |
+
See the License for the specific language governing permissions and
|
11 |
+
limitations under the License.
|
12 |
+
*/
|
13 |
+
|
14 |
import React, { useEffect, useRef, useState } from "react";
|
15 |
import './App.scss';
|
16 |
import { LiveAPIProvider, useLiveAPIContext } from "./contexts/LiveAPIContext";
|
|
|
119 |
</svg>
|
120 |
);
|
121 |
|
122 |
+
const AppInternalLogic: React.FC<{
|
123 |
+
isMicActive: boolean;
|
124 |
+
isCamActive: boolean;
|
125 |
+
setIsMicActive: React.Dispatch<React.SetStateAction<boolean>>;
|
126 |
+
setIsCamActive: React.Dispatch<React.SetStateAction<boolean>>;
|
127 |
+
createLogoFunction: (isMini: boolean, isActive: boolean, type?: 'human' | 'ai', forFooter?: boolean) => React.ReactNode;
|
128 |
+
videoRef: React.RefObject<HTMLVideoElement>;
|
129 |
+
notificationPopoverRef: React.RefObject<HTMLDivElement>;
|
130 |
+
notificationButtonRef: React.RefObject<HTMLButtonElement>;
|
131 |
+
isNotificationOpen: boolean;
|
132 |
+
setIsNotificationOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
133 |
+
}> = ({
|
134 |
+
isMicActive,
|
135 |
+
isCamActive,
|
136 |
+
setIsMicActive,
|
137 |
+
setIsCamActive,
|
138 |
+
createLogoFunction,
|
139 |
+
videoRef,
|
140 |
+
notificationPopoverRef,
|
141 |
+
notificationButtonRef,
|
142 |
+
isNotificationOpen,
|
143 |
+
setIsNotificationOpen // اطمینان از وجود کاما در صورت نیاز در کپی پیستهای قبلی
|
144 |
+
}) => {
|
145 |
+
const { connected, disconnect } = useLiveAPIContext();
|
146 |
+
|
147 |
+
useEffect(() => {
|
148 |
+
if (!isMicActive && !isCamActive && connected) {
|
149 |
+
disconnect();
|
150 |
+
}
|
151 |
+
}, [isMicActive, isCamActive, connected, disconnect]);
|
152 |
+
|
153 |
+
return (
|
154 |
+
<div className="w-full flex flex-col items-center justify-center min-h-screen text-foreground antialiased">
|
155 |
+
<div className="main-wrapper max-w-3xl w-full flex flex-col items-center justify-center h-full relative">
|
156 |
+
<div className="header-controls">
|
157 |
+
<div id="notification-trigger-container">
|
158 |
+
<button
|
159 |
+
ref={notificationButtonRef}
|
160 |
+
id="notification-button"
|
161 |
+
aria-label="Notifications"
|
162 |
+
className="header-button"
|
163 |
+
onClick={(e) => {
|
164 |
+
e.stopPropagation();
|
165 |
+
setIsNotificationOpen(!isNotificationOpen);
|
166 |
+
}}
|
167 |
+
>
|
168 |
+
<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>
|
169 |
+
</button>
|
|
|
170 |
</div>
|
171 |
+
<div className="back-button-container">
|
172 |
+
<button
|
173 |
+
id="back-button"
|
174 |
+
aria-label="Go back"
|
175 |
+
className="header-button"
|
176 |
+
onClick={() => alert('Back clicked (implement navigation)')}
|
|
|
|
|
177 |
>
|
178 |
+
<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"><path d="m15 18-6-6 6-6"></path></svg>
|
179 |
+
</button>
|
180 |
+
</div>
|
181 |
+
</div>
|
182 |
+
|
183 |
+
<div ref={notificationPopoverRef} id="notification-popover-wrapper" className="notification-popover-wrapper">
|
184 |
+
<div
|
185 |
+
id="notification-popover"
|
186 |
+
className={cn("popover-content", {
|
187 |
+
"open animate-popover-open-top-center": isNotificationOpen,
|
188 |
+
"animate-popover-close-top-center": !isNotificationOpen && document.getElementById('notification-popover')?.classList.contains('open'),
|
189 |
+
})}
|
190 |
+
>
|
191 |
+
<div className="notification-popover-text-content">
|
192 |
+
مدلهای هوش مصنوعی میتوانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از وارد کردن اطلاعات حساس بپرهیزید.
|
193 |
</div>
|
194 |
</div>
|
195 |
+
</div>
|
196 |
+
|
197 |
+
<div className="media-area w-full flex flex-col items-center justify-center flex-grow relative">
|
198 |
+
<video
|
199 |
+
id="video-feed"
|
200 |
+
ref={videoRef}
|
201 |
+
autoPlay
|
202 |
+
playsInline
|
203 |
+
className={cn(
|
204 |
+
"absolute top-0 left-0 w-full h-full object-cover scale-x-[-1]",
|
205 |
+
{ "hidden": !isCamActive }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
206 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
207 |
/>
|
208 |
+
{isMicActive && !isCamActive && (
|
209 |
+
<div
|
210 |
+
id="large-logo-container"
|
211 |
+
className="absolute top-0 left-0 w-full h-full flex items-center justify-center pointer-events-none"
|
212 |
+
>
|
213 |
+
{createLogoFunction(false, true, 'human', false)}
|
214 |
+
</div>
|
215 |
+
)}
|
216 |
</div>
|
217 |
+
|
218 |
+
<ControlTray
|
219 |
+
videoRef={videoRef}
|
220 |
+
supportsVideo={true}
|
221 |
+
onVideoStreamChange={(stream) => { /* ... */ }}
|
222 |
+
isAppMicActive={isMicActive}
|
223 |
+
onAppMicToggle={setIsMicActive}
|
224 |
+
isAppCamActive={isCamActive}
|
225 |
+
onAppCamToggle={setIsCamActive}
|
226 |
+
createLogoFunction={createLogoFunction}
|
227 |
+
ReferenceMicrophoneIcon={SvgReferenceMicrophoneIcon}
|
228 |
+
/>
|
229 |
</div>
|
230 |
+
</div>
|
231 |
+
);
|
232 |
};
|
233 |
|
234 |
+
const logoColorConfig = {
|
235 |
blue: {
|
236 |
ping: "bg-blue-200 dark:bg-blue-700",
|
237 |
outer: "bg-blue-200 dark:bg-blue-700",
|
|
|
252 |
}
|
253 |
};
|
254 |
|
255 |
+
function App() {
|
256 |
+
const videoRef = useRef<HTMLVideoElement>(null);
|
257 |
+
const [showIOSModal, setShowIOSModal] = useState(false);
|
258 |
+
const [isAllowedOrigin, setIsAllowedOrigin] = useState<boolean | null>(null);
|
259 |
+
|
260 |
+
const [isMicActive, setIsMicActive] = useState(false);
|
261 |
+
const [isCamActive, setIsCamActive] = useState(false);
|
262 |
+
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
|
263 |
+
|
264 |
+
const notificationPopoverRef = useRef<HTMLDivElement>(null);
|
265 |
+
const notificationButtonRef = useRef<HTMLButtonElement>(null);
|
266 |
+
|
267 |
+
useEffect(() => {
|
268 |
+
if (isIOS()) {
|
269 |
+
setShowIOSModal(true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
270 |
}
|
271 |
+
const timer = setTimeout(() => {
|
272 |
+
setIsAllowedOrigin(true);
|
273 |
+
}, 100);
|
274 |
+
return () => clearTimeout(timer);
|
275 |
+
}, []);
|
276 |
+
|
277 |
+
useEffect(() => {
|
278 |
+
const handleClickOutside = (event: MouseEvent) => {
|
279 |
+
if (
|
280 |
+
isNotificationOpen &&
|
281 |
+
notificationPopoverRef.current &&
|
282 |
+
!notificationPopoverRef.current.contains(event.target as Node) &&
|
283 |
+
notificationButtonRef.current &&
|
284 |
+
!notificationButtonRef.current.contains(event.target as Node)
|
285 |
+
) {
|
286 |
+
setIsNotificationOpen(false);
|
287 |
+
}
|
288 |
+
};
|
289 |
+
document.addEventListener("mousedown", handleClickOutside);
|
290 |
+
return () => {
|
291 |
+
document.removeEventListener("mousedown", handleClickOutside);
|
292 |
+
};
|
293 |
+
}, [isNotificationOpen]);
|
294 |
+
|
295 |
+
if (isAllowedOrigin === null) {
|
296 |
+
return <div style={{ padding: '20px', textAlign: 'center' }}>در حال بررسی دسترسی...</div>;
|
297 |
+
}
|
298 |
|
|
|
299 |
const createLogoFunction = (isMini: boolean, isActive: boolean, type: 'human' | 'ai' = 'human', forFooter: boolean = false) => {
|
300 |
if (!isActive) return null;
|
301 |
|
|
|
306 |
const iconDisplaySize = isMini ? 35 : 70;
|
307 |
const iconInset = (size - iconDisplaySize) / 2;
|
308 |
|
|
|
|
|
|
|
|
|
309 |
const insetValues = {
|
310 |
ping: isMini ? 10 : 40,
|
311 |
+
outer: 0,
|
312 |
mid: isMini ? 5 : 20,
|
313 |
inner: isMini ? 12 : 50,
|
314 |
icon: iconInset
|
|
|
318 |
|
319 |
return (
|
320 |
<div className={cn("logo-animation-wrapper", {"for-footer": forFooter})} style={{ width: `${size}px`, height: `${size}px` }}>
|
|
|
321 |
<div className={`absolute rounded-full opacity-50 animate-ping ${currentColors.ping}`} style={{ inset: `${insetValues.ping}px` }}></div>
|
|
|
322 |
<div className={`absolute rounded-full opacity-50 ${currentColors.outer}`} style={{ inset: `${insetValues.outer}px` }}></div>
|
|
|
323 |
<div className={`absolute rounded-full opacity-50 ${currentColors.mid}`} style={{ inset: `${insetValues.mid}px` }}></div>
|
|
|
324 |
<div className={`absolute rounded-full opacity-50 ${currentColors.inner}`} style={{ inset: `${insetValues.inner}px` }}></div>
|
|
|
325 |
<div className="z-10 absolute flex items-center justify-center" style={{ inset: `${insetValues.icon}px`, width: `${iconDisplaySize}px`, height: `${iconDisplaySize}px` }}>
|
326 |
{IconComponent && <IconComponent />}
|
327 |
</div>
|
|
|
329 |
);
|
330 |
};
|
331 |
|
332 |
+
return (
|
333 |
<LiveAPIProvider initialConfig={initialAppConfig}>
|
334 |
<AppInternalLogic
|
335 |
isMicActive={isMicActive}
|