Spaces:
Running
Running
Update src/App.tsx
Browse files- src/App.tsx +171 -135
src/App.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
// ... (
|
2 |
import React, { useEffect, useRef, useState } from "react";
|
3 |
import './App.scss';
|
4 |
import { LiveAPIProvider, useLiveAPIContext } from "./contexts/LiveAPIContext";
|
@@ -107,145 +107,174 @@ const SvgReferenceMicrophoneIcon = () => (
|
|
107 |
</svg>
|
108 |
);
|
109 |
|
110 |
-
const AppInternalLogic: React.FC<{ /* ...props... */ }> = ({ /* ...props... */ }) => {
|
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 |
</div>
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
className="
|
144 |
-
|
|
|
|
|
145 |
>
|
146 |
-
<
|
147 |
-
|
148 |
-
|
149 |
-
</div>
|
150 |
-
|
151 |
-
<div ref={notificationPopoverRef} id="notification-popover-wrapper" className="notification-popover-wrapper">
|
152 |
-
<div
|
153 |
-
id="notification-popover"
|
154 |
-
className={cn("popover-content", {
|
155 |
-
"open animate-popover-open-top-center": isNotificationOpen,
|
156 |
-
"animate-popover-close-top-center": !isNotificationOpen && document.getElementById('notification-popover')?.classList.contains('open'),
|
157 |
-
})}
|
158 |
-
>
|
159 |
-
<div className="notification-popover-text-content">
|
160 |
-
مدلهای هوش مصنوعی میتوانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از وارد کردن اطلاعات حساس بپرهیزید.
|
161 |
</div>
|
162 |
</div>
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
174 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
175 |
/>
|
176 |
-
{isMicActive && !isCamActive && (
|
177 |
-
<div
|
178 |
-
id="large-logo-container"
|
179 |
-
className="absolute top-0 left-0 w-full h-full flex items-center justify-center pointer-events-none"
|
180 |
-
>
|
181 |
-
{createLogoFunction(false, true, 'human', false)}
|
182 |
-
</div>
|
183 |
-
)}
|
184 |
</div>
|
185 |
-
|
186 |
-
<ControlTray
|
187 |
-
videoRef={videoRef}
|
188 |
-
supportsVideo={true}
|
189 |
-
onVideoStreamChange={(stream) => { /* ... */ }}
|
190 |
-
isAppMicActive={isMicActive}
|
191 |
-
onAppMicToggle={setIsMicActive}
|
192 |
-
isAppCamActive={isCamActive}
|
193 |
-
onAppCamToggle={setIsCamActive}
|
194 |
-
createLogoFunction={createLogoFunction}
|
195 |
-
ReferenceMicrophoneIcon={SvgReferenceMicrophoneIcon}
|
196 |
-
/>
|
197 |
</div>
|
198 |
-
|
199 |
-
);
|
200 |
};
|
201 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
202 |
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
if (isIOS()) {
|
219 |
-
setShowIOSModal(true);
|
220 |
-
}
|
221 |
-
const timer = setTimeout(() => {
|
222 |
-
setIsAllowedOrigin(true);
|
223 |
-
}, 100);
|
224 |
-
return () => clearTimeout(timer);
|
225 |
-
}, []);
|
226 |
-
|
227 |
-
useEffect(() => {
|
228 |
-
const handleClickOutside = (event: MouseEvent) => {
|
229 |
-
if (
|
230 |
-
isNotificationOpen &&
|
231 |
-
notificationPopoverRef.current &&
|
232 |
-
!notificationPopoverRef.current.contains(event.target as Node) &&
|
233 |
-
notificationButtonRef.current &&
|
234 |
-
!notificationButtonRef.current.contains(event.target as Node)
|
235 |
-
) {
|
236 |
-
setIsNotificationOpen(false);
|
237 |
}
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
};
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
248 |
|
|
|
249 |
const createLogoFunction = (isMini: boolean, isActive: boolean, type: 'human' | 'ai' = 'human', forFooter: boolean = false) => {
|
250 |
if (!isActive) return null;
|
251 |
|
@@ -256,17 +285,23 @@ function App() {
|
|
256 |
const iconDisplaySize = isMini ? 35 : 70;
|
257 |
const iconInset = (size - iconDisplaySize) / 2;
|
258 |
|
259 |
-
// مقادیر inset برای حلقهها از HTML مرجع
|
260 |
-
//
|
261 |
-
|
262 |
-
|
263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
264 |
|
265 |
const IconComponent = type === 'human' ? SvgHumanIcon : null;
|
266 |
|
267 |
return (
|
268 |
<div className={cn("logo-animation-wrapper", {"for-footer": forFooter})} style={{ width: `${size}px`, height: `${size}px` }}>
|
269 |
-
{/* حلقه
|
270 |
<div className={`absolute rounded-full opacity-50 animate-ping ${currentColors.ping}`} style={{ inset: `${insetValues.ping}px` }}></div>
|
271 |
{/* حلقه بیرونی */}
|
272 |
<div className={`absolute rounded-full opacity-50 ${currentColors.outer}`} style={{ inset: `${insetValues.outer}px` }}></div>
|
@@ -275,14 +310,14 @@ function App() {
|
|
275 |
{/* حلقه داخلی */}
|
276 |
<div className={`absolute rounded-full opacity-50 ${currentColors.inner}`} style={{ inset: `${insetValues.inner}px` }}></div>
|
277 |
{/* کانتینر آیکون */}
|
278 |
-
<div className="z-10 absolute flex items-center justify-center" style={{ inset: `${
|
279 |
{IconComponent && <IconComponent />}
|
280 |
</div>
|
281 |
</div>
|
282 |
);
|
283 |
};
|
284 |
|
285 |
-
return (
|
286 |
<LiveAPIProvider initialConfig={initialAppConfig}>
|
287 |
<AppInternalLogic
|
288 |
isMicActive={isMicActive}
|
@@ -300,4 +335,5 @@ function App() {
|
|
300 |
</LiveAPIProvider>
|
301 |
);
|
302 |
}
|
|
|
303 |
export default App;
|
|
|
1 |
+
// ... (ایمپورتها، myCustomInstruction, initialAppConfig, SVG Icons, AppInternalLogic، و بخش اول تابع App بدون تغییر) ...
|
2 |
import React, { useEffect, useRef, useState } from "react";
|
3 |
import './App.scss';
|
4 |
import { LiveAPIProvider, useLiveAPIContext } from "./contexts/LiveAPIContext";
|
|
|
107 |
</svg>
|
108 |
);
|
109 |
|
110 |
+
const AppInternalLogic: React.FC<{ /* ... props ... */ }> = ({ /* ... props ... */ }) => {
|
111 |
+
// ... (بدون تغییر)
|
112 |
+
isMicActive,
|
113 |
+
isCamActive,
|
114 |
+
setIsMicActive,
|
115 |
+
setIsCamActive,
|
116 |
+
createLogoFunction,
|
117 |
+
videoRef,
|
118 |
+
notificationPopoverRef,
|
119 |
+
notificationButtonRef,
|
120 |
+
isNotificationOpen,
|
121 |
+
setIsNotificationOpen
|
122 |
+
}) => {
|
123 |
+
const { connected, disconnect } = useLiveAPIContext();
|
124 |
+
|
125 |
+
useEffect(() => {
|
126 |
+
if (!isMicActive && !isCamActive && connected) {
|
127 |
+
disconnect();
|
128 |
+
}
|
129 |
+
}, [isMicActive, isCamActive, connected, disconnect]);
|
130 |
+
|
131 |
+
return (
|
132 |
+
<div className="w-full flex flex-col items-center justify-center min-h-screen text-foreground antialiased">
|
133 |
+
<div className="main-wrapper max-w-3xl w-full flex flex-col items-center justify-center h-full relative">
|
134 |
+
<div className="header-controls">
|
135 |
+
<div id="notification-trigger-container">
|
136 |
+
<button
|
137 |
+
ref={notificationButtonRef}
|
138 |
+
id="notification-button"
|
139 |
+
aria-label="Notifications"
|
140 |
+
className="header-button"
|
141 |
+
onClick={(e) => {
|
142 |
+
e.stopPropagation();
|
143 |
+
setIsNotificationOpen(!isNotificationOpen);
|
144 |
+
}}
|
145 |
+
>
|
146 |
+
<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>
|
147 |
+
</button>
|
148 |
+
</div>
|
149 |
+
<div className="back-button-container">
|
150 |
+
<button
|
151 |
+
id="back-button"
|
152 |
+
aria-label="Go back"
|
153 |
+
className="header-button"
|
154 |
+
onClick={() => alert('Back clicked (implement navigation)')}
|
155 |
+
>
|
156 |
+
<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>
|
157 |
+
</button>
|
158 |
+
</div>
|
159 |
</div>
|
160 |
+
|
161 |
+
<div ref={notificationPopoverRef} id="notification-popover-wrapper" className="notification-popover-wrapper">
|
162 |
+
<div
|
163 |
+
id="notification-popover"
|
164 |
+
className={cn("popover-content", {
|
165 |
+
"open animate-popover-open-top-center": isNotificationOpen,
|
166 |
+
"animate-popover-close-top-center": !isNotificationOpen && document.getElementById('notification-popover')?.classList.contains('open'),
|
167 |
+
})}
|
168 |
>
|
169 |
+
<div className="notification-popover-text-content">
|
170 |
+
مدلهای هوش مصنوعی میتوانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از وارد کردن اطلاعات حساس بپرهیزید.
|
171 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
</div>
|
173 |
</div>
|
174 |
+
|
175 |
+
<div className="media-area w-full flex flex-col items-center justify-center flex-grow relative">
|
176 |
+
<video
|
177 |
+
id="video-feed"
|
178 |
+
ref={videoRef}
|
179 |
+
autoPlay
|
180 |
+
playsInline
|
181 |
+
className={cn(
|
182 |
+
"absolute top-0 left-0 w-full h-full object-cover scale-x-[-1]",
|
183 |
+
{ "hidden": !isCamActive }
|
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",
|
216 |
+
mid: "bg-blue-300 dark:bg-blue-600",
|
217 |
+
inner: "bg-blue-400 dark:bg-blue-500",
|
218 |
+
},
|
219 |
+
green: {
|
220 |
+
ping: "bg-green-200 dark:bg-green-700",
|
221 |
+
outer: "bg-green-200 dark:bg-green-700",
|
222 |
+
mid: "bg-green-300 dark:bg-green-600",
|
223 |
+
inner: "bg-green-400 dark:bg-green-500",
|
224 |
+
},
|
225 |
+
gray: {
|
226 |
+
ping: "bg-gray-200 dark:bg-gray-700",
|
227 |
+
outer: "bg-gray-200 dark:bg-gray-700",
|
228 |
+
mid: "bg-gray-300 dark:bg-gray-600",
|
229 |
+
inner: "bg-gray-400 dark:bg-gray-500",
|
230 |
+
}
|
231 |
+
};
|
232 |
|
233 |
+
function App() { /* ... (useEffect ها و state ها بدون تغییر) ... */
|
234 |
+
const videoRef = useRef<HTMLVideoElement>(null);
|
235 |
+
const [showIOSModal, setShowIOSModal] = useState(false);
|
236 |
+
const [isAllowedOrigin, setIsAllowedOrigin] = useState<boolean | null>(null);
|
237 |
+
|
238 |
+
const [isMicActive, setIsMicActive] = useState(false);
|
239 |
+
const [isCamActive, setIsCamActive] = useState(false);
|
240 |
+
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
|
241 |
+
|
242 |
+
const notificationPopoverRef = useRef<HTMLDivElement>(null);
|
243 |
+
const notificationButtonRef = useRef<HTMLButtonElement>(null);
|
244 |
+
|
245 |
+
useEffect(() => {
|
246 |
+
if (isIOS()) {
|
247 |
+
setShowIOSModal(true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
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, // در HTML شما برای outer از inset:0 استفاده شده
|
295 |
+
mid: isMini ? 5 : 20,
|
296 |
+
inner: isMini ? 12 : 50,
|
297 |
+
icon: iconInset
|
298 |
+
};
|
299 |
|
300 |
const IconComponent = type === 'human' ? SvgHumanIcon : null;
|
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>
|
|
|
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>
|
316 |
</div>
|
317 |
);
|
318 |
};
|
319 |
|
320 |
+
return ( /* ... (LiveAPIProvider و AppInternalLogic مثل قبل) ... */
|
321 |
<LiveAPIProvider initialConfig={initialAppConfig}>
|
322 |
<AppInternalLogic
|
323 |
isMicActive={isMicActive}
|
|
|
335 |
</LiveAPIProvider>
|
336 |
);
|
337 |
}
|
338 |
+
|
339 |
export default App;
|