Abdullahrasheed45 commited on
Commit
763440c
·
verified ·
1 Parent(s): da4009c

Create WebcamPermissionDialog.tsx

Browse files
src/components/WebcamPermissionDialog.tsx ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useCallback, useMemo } from "react";
2
+ import Button from "./Button";
3
+ import { THEME } from "../constants";
4
+
5
+ const ERROR_TYPES = {
6
+ HTTPS: "https",
7
+ NOT_SUPPORTED: "not-supported",
8
+ PERMISSION: "permission",
9
+ GENERAL: "general",
10
+ } as const;
11
+
12
+ const VIDEO_CONSTRAINTS = {
13
+ video: {
14
+ width: { ideal: 1920, max: 1920 },
15
+ height: { ideal: 1080, max: 1080 },
16
+ facingMode: "user",
17
+ },
18
+ };
19
+
20
+ interface ErrorInfo {
21
+ type: (typeof ERROR_TYPES)[keyof typeof ERROR_TYPES];
22
+ message: string;
23
+ }
24
+
25
+ interface WebcamPermissionDialogProps {
26
+ onPermissionGranted: (stream: MediaStream) => void;
27
+ }
28
+
29
+ export default function WebcamPermissionDialog({
30
+ onPermissionGranted,
31
+ }: WebcamPermissionDialogProps) {
32
+ const [isRequesting, setIsRequesting] = useState(false);
33
+ const [error, setError] = useState<ErrorInfo | null>(null);
34
+
35
+ const [mounted, setMounted] = useState(false);
36
+ useEffect(() => setMounted(true), []);
37
+
38
+ const getErrorInfo = (err: unknown): ErrorInfo => {
39
+ if (!navigator.mediaDevices) {
40
+ return {
41
+ type: ERROR_TYPES.HTTPS,
42
+ message: "Camera access requires a secure connection (HTTPS)",
43
+ };
44
+ }
45
+
46
+ if (!navigator.mediaDevices.getUserMedia) {
47
+ return {
48
+ type: ERROR_TYPES.NOT_SUPPORTED,
49
+ message: "Camera access not supported in this browser",
50
+ };
51
+ }
52
+
53
+ if (err instanceof DOMException) {
54
+ switch (err.name) {
55
+ case "NotAllowedError":
56
+ return {
57
+ type: ERROR_TYPES.PERMISSION,
58
+ message: "Camera access denied",
59
+ };
60
+ case "NotFoundError":
61
+ return {
62
+ type: ERROR_TYPES.GENERAL,
63
+ message: "No camera found",
64
+ };
65
+ case "NotReadableError":
66
+ return {
67
+ type: ERROR_TYPES.GENERAL,
68
+ message: "Camera is in use by another application",
69
+ };
70
+ case "OverconstrainedError":
71
+ return {
72
+ type: ERROR_TYPES.GENERAL,
73
+ message: "Camera doesn't meet requirements",
74
+ };
75
+ case "SecurityError":
76
+ return {
77
+ type: ERROR_TYPES.HTTPS,
78
+ message: "Security error accessing camera",
79
+ };
80
+ default:
81
+ return {
82
+ type: ERROR_TYPES.GENERAL,
83
+ message: `Camera error: ${err.name}`,
84
+ };
85
+ }
86
+ }
87
+
88
+ return {
89
+ type: ERROR_TYPES.GENERAL,
90
+ message: "Failed to access camera",
91
+ };
92
+ };
93
+
94
+ const requestWebcamAccess = useCallback(async () => {
95
+ setIsRequesting(true);
96
+ setError(null);
97
+
98
+ try {
99
+ if (!navigator.mediaDevices?.getUserMedia) {
100
+ throw new Error("NOT_SUPPORTED");
101
+ }
102
+
103
+ const stream =
104
+ await navigator.mediaDevices.getUserMedia(VIDEO_CONSTRAINTS);
105
+ onPermissionGranted(stream);
106
+ } catch (err) {
107
+ const errorInfo = getErrorInfo(err);
108
+ setError(errorInfo);
109
+ console.error("Error accessing webcam:", err, errorInfo);
110
+ } finally {
111
+ setIsRequesting(false);
112
+ }
113
+ }, [onPermissionGranted]);
114
+
115
+ useEffect(() => {
116
+ requestWebcamAccess();
117
+ }, [requestWebcamAccess]);
118
+
119
+ const troubleshootingData = useMemo(
120
+ () => ({
121
+ [ERROR_TYPES.HTTPS]: {
122
+ title: "HTTPS Required",
123
+ items: [
124
+ "Access this app via https:// instead of http://",
125
+ "If developing locally, use localhost",
126
+ "Deploy to a secure hosting service (Hugging Face spaces)",
127
+ ],
128
+ },
129
+ [ERROR_TYPES.NOT_SUPPORTED]: {
130
+ title: "Browser Compatibility",
131
+ items: [
132
+ "Update your browser to the latest version",
133
+ "Try Chrome, Edge, or Firefox",
134
+ "Enable JavaScript if disabled",
135
+ ],
136
+ },
137
+ [ERROR_TYPES.PERMISSION]: {
138
+ title: "Permission Issues",
139
+ items: [
140
+ "Click the camera icon in your address bar",
141
+ 'Select "Always allow" for camera access',
142
+ "Check System Preferences → Security & Privacy",
143
+ "Refresh the page after changing settings",
144
+ ],
145
+ },
146
+ [ERROR_TYPES.GENERAL]: {
147
+ title: "General Troubleshooting",
148
+ items: [
149
+ "Ensure no other apps (Zoom, Teams) are using the camera",
150
+ "Try unplugging and replugging your webcam",
151
+ "Try using a different browser",
152
+ ],
153
+ },
154
+ }),
155
+ [],
156
+ );
157
+
158
+ const getTroubleshootingContent = () => {
159
+ if (!error) return null;
160
+ const content = troubleshootingData[error.type];
161
+
162
+ return (
163
+ <div
164
+ className="bg-white border p-5 mt-6"
165
+ style={{ borderColor: THEME.beigeDark }}
166
+ >
167
+ <h4 className="text-sm font-bold uppercase tracking-wider text-gray-500 mb-3 flex items-center gap-2">
168
+ <span
169
+ className="w-2 h-2 rounded-full"
170
+ style={{ backgroundColor: THEME.mistralOrange }}
171
+ ></span>
172
+ Troubleshooting
173
+ </h4>
174
+ <div className="space-y-2">
175
+ <p className="font-semibold" style={{ color: THEME.textBlack }}>
176
+ {content.title}
177
+ </p>
178
+ <ul className="space-y-2">
179
+ {content.items.map((item, index) => (
180
+ <li
181
+ key={index}
182
+ className="text-sm text-gray-600 flex items-start"
183
+ >
184
+ <span
185
+ className="mr-2 font-mono"
186
+ style={{ color: THEME.beigeDark }}
187
+ >
188
+ /
189
+ </span>{" "}
190
+ {item}
191
+ </li>
192
+ ))}
193
+ </ul>
194
+ </div>
195
+ {/* Technical Details Footer */}
196
+ <div className="mt-4 pt-4 border-t border-gray-100 flex flex-col gap-1">
197
+ <p className="text-xs text-gray-400 font-mono uppercase">
198
+ Diagnostics
199
+ </p>
200
+ <div className="bg-gray-50 p-2 rounded text-xs font-mono text-gray-600 break-all border border-gray-100">
201
+ LOC: {window.location.protocol}//{window.location.host}
202
+ <br />
203
+ ERR: {error.type.toUpperCase()}
204
+ </div>
205
+ </div>
206
+ </div>
207
+ );
208
+ };
209
+
210
+ const getTitle = () => {
211
+ if (isRequesting) return "Initialize Camera";
212
+ if (error) return "Connection Failed";
213
+ return "Permission Required";
214
+ };
215
+
216
+ const getDescription = () => {
217
+ if (isRequesting) return "Requesting access to video input device...";
218
+ if (error) return error.message;
219
+ return "Ministral WebGPU requires local camera access for real-time inference.";
220
+ };
221
+
222
+ return (
223
+ <>
224
+ <div
225
+ className="fixed inset-0 flex items-center justify-center p-4 z-50"
226
+ style={{
227
+ backgroundColor: THEME.beigeLight,
228
+ backgroundImage: `
229
+ linear-gradient(${THEME.beigeDark} 1px, transparent 1px),
230
+ linear-gradient(90deg, ${THEME.beigeDark} 1px, transparent 1px)
231
+ `,
232
+ backgroundSize: "40px 40px",
233
+ }}
234
+ role="dialog"
235
+ aria-labelledby="webcam-dialog-title"
236
+ >
237
+ <div
238
+ className={`max-w-lg w-full backdrop-blur-sm border shadow-2xl transition-all duration-700 ${mounted ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"}`}
239
+ style={{
240
+ backgroundColor: `${THEME.beigeLight}F2`,
241
+ borderColor: THEME.beigeDark,
242
+ }}
243
+ >
244
+ {/* Header Bar */}
245
+ <div
246
+ className="h-1 w-full"
247
+ style={{ backgroundColor: THEME.mistralOrange }}
248
+ ></div>
249
+ <div className="p-8 md:p-10">
250
+ {/* Icon Area */}
251
+ <div className="flex justify-center mb-8">
252
+ {isRequesting ? (
253
+ <div className="relative">
254
+ <div
255
+ className="absolute inset-0 blur-lg opacity-20 rounded-full animate-pulse"
256
+ style={{ backgroundColor: THEME.mistralOrange }}
257
+ ></div>
258
+ <div
259
+ className="w-16 h-16 border-4 rounded-full animate-spin"
260
+ style={{
261
+ borderColor: THEME.beigeDark,
262
+ borderTopColor: THEME.mistralOrange,
263
+ }}
264
+ ></div>
265
+ </div>
266
+ ) : (
267
+ <div
268
+ className={`w-16 h-16 flex items-center justify-center border-2`}
269
+ style={{
270
+ borderColor: error ? THEME.errorRed : THEME.mistralOrange,
271
+ backgroundColor: error
272
+ ? `${THEME.errorRed}0D`
273
+ : `${THEME.mistralOrange}1A`,
274
+ }}
275
+ >
276
+ {error ? (
277
+ <svg
278
+ className="w-8 h-8"
279
+ style={{ color: THEME.errorRed }}
280
+ fill="none"
281
+ viewBox="0 0 24 24"
282
+ stroke="currentColor"
283
+ strokeWidth={2}
284
+ >
285
+ <path
286
+ strokeLinecap="square"
287
+ strokeLinejoin="miter"
288
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
289
+ />
290
+ </svg>
291
+ ) : (
292
+ <svg
293
+ className="w-8 h-8"
294
+ style={{ color: THEME.mistralOrange }}
295
+ fill="none"
296
+ viewBox="0 0 24 24"
297
+ stroke="currentColor"
298
+ strokeWidth={1.5}
299
+ >
300
+ <path
301
+ strokeLinecap="square"
302
+ strokeLinejoin="miter"
303
+ d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
304
+ />
305
+ </svg>
306
+ )}
307
+ </div>
308
+ )}
309
+ </div>
310
+ {/* Text Content */}
311
+ <div className="text-center space-y-4 mb-8">
312
+ <h2
313
+ id="webcam-dialog-title"
314
+ className="text-2xl font-bold tracking-tight"
315
+ style={{ color: THEME.textBlack }}
316
+ >
317
+ {getTitle()}
318
+ </h2>
319
+ <p className="text-gray-600 leading-relaxed font-light text-lg">
320
+ {getDescription()}
321
+ </p>
322
+ </div>
323
+ {/* Error Actions */}
324
+ {error && (
325
+ <div className="animate-enter">
326
+ <div className="flex justify-center mb-6">
327
+ <Button
328
+ onClick={requestWebcamAccess}
329
+ disabled={isRequesting}
330
+ className="px-8 py-3 text-white shadow-lg hover:shadow-xl transition-all font-semibold tracking-wide hover:bg-[var(--mistral-orange-dark)]"
331
+ >
332
+ Try Again
333
+ </Button>
334
+ </div>
335
+ {getTroubleshootingContent()}
336
+ </div>
337
+ )}
338
+ {/* Loading State Helper */}
339
+ {isRequesting && (
340
+ <p className="text-center text-sm text-gray-400 font-mono animate-pulse">
341
+ Waiting for browser permission...
342
+ </p>
343
+ )}
344
+ </div>
345
+ </div>
346
+ </div>
347
+ </>
348
+ );
349
+ }