@vettly/react 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,878 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ModeratedImageUpload: () => ModeratedImageUpload,
24
+ ModeratedTextarea: () => ModeratedTextarea,
25
+ ModeratedVideoUpload: () => ModeratedVideoUpload,
26
+ useModeration: () => useModeration
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/useModeration.ts
31
+ var import_react = require("react");
32
+ var import_sdk = require("@vettly/sdk");
33
+ function useModeration(options) {
34
+ const {
35
+ apiKey,
36
+ policyId,
37
+ debounceMs = 500,
38
+ enabled = true,
39
+ onCheck,
40
+ onError
41
+ } = options;
42
+ const [result, setResult] = (0, import_react.useState)({
43
+ safe: true,
44
+ flagged: false,
45
+ action: "allow",
46
+ categories: [],
47
+ isChecking: false,
48
+ error: null
49
+ });
50
+ const clientRef = (0, import_react.useRef)(null);
51
+ const timeoutRef = (0, import_react.useRef)(null);
52
+ const abortControllerRef = (0, import_react.useRef)(null);
53
+ (0, import_react.useEffect)(() => {
54
+ if (!apiKey) return;
55
+ clientRef.current = new import_sdk.ModerationClient({ apiKey });
56
+ }, [apiKey]);
57
+ const check = (0, import_react.useCallback)(
58
+ async (content) => {
59
+ if (!enabled || !clientRef.current) return;
60
+ if (abortControllerRef.current) {
61
+ abortControllerRef.current.abort();
62
+ }
63
+ if (timeoutRef.current) {
64
+ clearTimeout(timeoutRef.current);
65
+ }
66
+ if (typeof content === "string" && !content.trim()) {
67
+ setResult({
68
+ safe: true,
69
+ flagged: false,
70
+ action: "allow",
71
+ categories: [],
72
+ isChecking: false,
73
+ error: null
74
+ });
75
+ return;
76
+ }
77
+ setResult((prev) => ({ ...prev, isChecking: true, error: null }));
78
+ timeoutRef.current = setTimeout(async () => {
79
+ try {
80
+ abortControllerRef.current = new AbortController();
81
+ const checkRequest = typeof content === "string" ? { content, policyId, contentType: "text" } : { ...content, policyId };
82
+ const response = await clientRef.current.check(checkRequest);
83
+ setResult({
84
+ safe: response.safe,
85
+ flagged: response.flagged,
86
+ action: response.action,
87
+ categories: response.categories || [],
88
+ isChecking: false,
89
+ error: null
90
+ });
91
+ if (onCheck) {
92
+ onCheck(response);
93
+ }
94
+ } catch (err) {
95
+ const error = err;
96
+ if (error.name === "AbortError") return;
97
+ setResult((prev) => ({
98
+ ...prev,
99
+ isChecking: false,
100
+ error: error.message || "Failed to check content"
101
+ }));
102
+ if (onError) {
103
+ onError(error);
104
+ }
105
+ }
106
+ }, debounceMs);
107
+ },
108
+ [enabled, policyId, debounceMs, onCheck, onError]
109
+ );
110
+ (0, import_react.useEffect)(() => {
111
+ return () => {
112
+ if (timeoutRef.current) {
113
+ clearTimeout(timeoutRef.current);
114
+ }
115
+ if (abortControllerRef.current) {
116
+ abortControllerRef.current.abort();
117
+ }
118
+ };
119
+ }, []);
120
+ return {
121
+ result,
122
+ check
123
+ };
124
+ }
125
+
126
+ // src/ModeratedTextarea.tsx
127
+ var import_react2 = require("react");
128
+ var import_jsx_runtime = require("react/jsx-runtime");
129
+ var ModeratedTextarea = (0, import_react2.forwardRef)(
130
+ ({
131
+ apiKey,
132
+ policyId,
133
+ value = "",
134
+ onChange,
135
+ debounceMs = 500,
136
+ showFeedback = true,
137
+ blockUnsafe = false,
138
+ customFeedback,
139
+ onModerationResult,
140
+ onModerationError,
141
+ className = "",
142
+ disabled,
143
+ ...props
144
+ }, ref) => {
145
+ const [internalValue, setInternalValue] = (0, import_react2.useState)(value);
146
+ const { result, check } = useModeration({
147
+ apiKey,
148
+ policyId,
149
+ debounceMs,
150
+ enabled: !disabled,
151
+ onCheck: onModerationResult,
152
+ onError: onModerationError
153
+ });
154
+ (0, import_react2.useEffect)(() => {
155
+ setInternalValue(value);
156
+ }, [value]);
157
+ (0, import_react2.useEffect)(() => {
158
+ if (internalValue) {
159
+ check(internalValue);
160
+ }
161
+ }, [internalValue, check]);
162
+ const handleChange = (e) => {
163
+ const newValue = e.target.value;
164
+ if (blockUnsafe && !result.safe && newValue.length > internalValue.length) {
165
+ return;
166
+ }
167
+ setInternalValue(newValue);
168
+ if (onChange) {
169
+ onChange(newValue, {
170
+ safe: result.safe,
171
+ flagged: result.flagged,
172
+ action: result.action
173
+ });
174
+ }
175
+ };
176
+ const getBorderColor = () => {
177
+ if (result.isChecking) return "border-blue-300";
178
+ if (result.error) return "border-red-400";
179
+ if (result.action === "block") return "border-red-400";
180
+ if (result.action === "flag") return "border-yellow-400";
181
+ if (result.action === "warn") return "border-orange-400";
182
+ return "border-green-400";
183
+ };
184
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "moderated-textarea-wrapper", children: [
185
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
186
+ "textarea",
187
+ {
188
+ ref,
189
+ value: internalValue,
190
+ onChange: handleChange,
191
+ disabled,
192
+ className: `moderated-textarea ${getBorderColor()} ${className}`,
193
+ ...props
194
+ }
195
+ ),
196
+ showFeedback && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "moderation-feedback", children: customFeedback ? customFeedback(result) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DefaultFeedback, { result }) })
197
+ ] });
198
+ }
199
+ );
200
+ ModeratedTextarea.displayName = "ModeratedTextarea";
201
+ function DefaultFeedback({
202
+ result
203
+ }) {
204
+ if (result.isChecking) {
205
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "feedback-checking", children: [
206
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "spinner" }),
207
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Checking content..." })
208
+ ] });
209
+ }
210
+ if (result.error) {
211
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "feedback-error", children: [
212
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u26A0\uFE0F" }),
213
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: result.error })
214
+ ] });
215
+ }
216
+ if (result.action === "block") {
217
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "feedback-block", children: [
218
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u{1F6AB}" }),
219
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "This content violates community guidelines" })
220
+ ] });
221
+ }
222
+ if (result.action === "flag") {
223
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "feedback-flag", children: [
224
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u26A0\uFE0F" }),
225
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "This content has been flagged for review" })
226
+ ] });
227
+ }
228
+ if (result.action === "warn") {
229
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "feedback-warn", children: [
230
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u26A1" }),
231
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Please be mindful of community guidelines" })
232
+ ] });
233
+ }
234
+ if (result.safe) {
235
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "feedback-safe", children: [
236
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u2705" }),
237
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Content looks good" })
238
+ ] });
239
+ }
240
+ return null;
241
+ }
242
+
243
+ // src/ModeratedImageUpload.tsx
244
+ var import_react3 = require("react");
245
+ var import_jsx_runtime2 = require("react/jsx-runtime");
246
+ function ModeratedImageUpload({
247
+ apiKey,
248
+ policyId,
249
+ onUpload,
250
+ onReject,
251
+ maxSizeMB = 10,
252
+ acceptedFormats = ["image/jpeg", "image/png", "image/gif", "image/webp"],
253
+ showPreview = true,
254
+ blockUnsafe = true,
255
+ customPreview,
256
+ className = "",
257
+ disabled = false,
258
+ onModerationResult,
259
+ onModerationError
260
+ }) {
261
+ const [file, setFile] = (0, import_react3.useState)(null);
262
+ const [preview, setPreview] = (0, import_react3.useState)(null);
263
+ const [uploadError, setUploadError] = (0, import_react3.useState)(null);
264
+ const fileInputRef = (0, import_react3.useRef)(null);
265
+ const { result, check } = useModeration({
266
+ apiKey,
267
+ policyId,
268
+ debounceMs: 0,
269
+ // No debounce for images
270
+ enabled: !disabled,
271
+ onCheck: onModerationResult,
272
+ onError: onModerationError
273
+ });
274
+ const validateFile = (0, import_react3.useCallback)(
275
+ (selectedFile) => {
276
+ if (!acceptedFormats.includes(selectedFile.type)) {
277
+ return `Invalid file type. Accepted formats: ${acceptedFormats.join(", ")}`;
278
+ }
279
+ const fileSizeMB = selectedFile.size / (1024 * 1024);
280
+ if (fileSizeMB > maxSizeMB) {
281
+ return `File size exceeds ${maxSizeMB}MB limit`;
282
+ }
283
+ return null;
284
+ },
285
+ [acceptedFormats, maxSizeMB]
286
+ );
287
+ const handleFileSelect = async (e) => {
288
+ setUploadError(null);
289
+ const selectedFile = e.target.files?.[0];
290
+ if (!selectedFile) return;
291
+ const validationError = validateFile(selectedFile);
292
+ if (validationError) {
293
+ setUploadError(validationError);
294
+ if (onReject) {
295
+ onReject(selectedFile, validationError);
296
+ }
297
+ return;
298
+ }
299
+ const reader = new FileReader();
300
+ reader.onloadend = () => {
301
+ const previewUrl = reader.result;
302
+ setPreview(previewUrl);
303
+ setFile(selectedFile);
304
+ check({
305
+ content: previewUrl,
306
+ // Send base64 for moderation
307
+ contentType: "image",
308
+ policyId
309
+ });
310
+ };
311
+ reader.readAsDataURL(selectedFile);
312
+ };
313
+ const handleRemove = () => {
314
+ setFile(null);
315
+ setPreview(null);
316
+ setUploadError(null);
317
+ if (fileInputRef.current) {
318
+ fileInputRef.current.value = "";
319
+ }
320
+ };
321
+ const handleConfirm = () => {
322
+ if (!file) return;
323
+ if (blockUnsafe && !result.safe) {
324
+ if (onReject) {
325
+ onReject(file, "Content violates community guidelines");
326
+ }
327
+ return;
328
+ }
329
+ if (onUpload) {
330
+ onUpload(file, {
331
+ safe: result.safe,
332
+ flagged: result.flagged,
333
+ action: result.action
334
+ });
335
+ }
336
+ handleRemove();
337
+ };
338
+ const triggerFileInput = () => {
339
+ fileInputRef.current?.click();
340
+ };
341
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `moderated-image-upload ${className}`, children: [
342
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
343
+ "input",
344
+ {
345
+ ref: fileInputRef,
346
+ type: "file",
347
+ accept: acceptedFormats.join(","),
348
+ onChange: handleFileSelect,
349
+ disabled,
350
+ style: { display: "none" }
351
+ }
352
+ ),
353
+ !file ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "upload-area", onClick: triggerFileInput, children: [
354
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "upload-icon", children: "\u{1F4F7}" }),
355
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "upload-text", children: "Click to upload an image" }),
356
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "upload-hint", children: [
357
+ acceptedFormats.map((f) => f.split("/")[1]).join(", "),
358
+ " up to ",
359
+ maxSizeMB,
360
+ "MB"
361
+ ] }),
362
+ uploadError && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "upload-error", children: [
363
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "\u26A0\uFE0F" }),
364
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: uploadError })
365
+ ] })
366
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: customPreview ? customPreview({
367
+ file,
368
+ preview,
369
+ result,
370
+ onRemove: handleRemove
371
+ }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
372
+ DefaultPreview,
373
+ {
374
+ file,
375
+ preview,
376
+ result,
377
+ onRemove: handleRemove,
378
+ onConfirm: handleConfirm,
379
+ blockUnsafe
380
+ }
381
+ ) })
382
+ ] });
383
+ }
384
+ function DefaultPreview({
385
+ file,
386
+ preview,
387
+ result,
388
+ onRemove,
389
+ onConfirm,
390
+ blockUnsafe
391
+ }) {
392
+ const canConfirm = !result.isChecking && (result.safe || !blockUnsafe);
393
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "image-preview", children: [
394
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "preview-container", children: [
395
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("img", { src: preview, alt: file.name, className: "preview-image" }),
396
+ result.isChecking && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "preview-overlay", children: [
397
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "spinner" }),
398
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Checking image..." })
399
+ ] })
400
+ ] }),
401
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "preview-info", children: [
402
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "file-name", children: file.name }),
403
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "file-size", children: [
404
+ (file.size / 1024).toFixed(2),
405
+ " KB"
406
+ ] })
407
+ ] }),
408
+ result.error && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "moderation-error", children: [
409
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "\u26A0\uFE0F" }),
410
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: result.error })
411
+ ] }),
412
+ !result.isChecking && !result.error && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `moderation-status status-${result.action}`, children: [
413
+ result.action === "block" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
414
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "\u{1F6AB}" }),
415
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "This image violates community guidelines" })
416
+ ] }),
417
+ result.action === "flag" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
418
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "\u26A0\uFE0F" }),
419
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "This image has been flagged for review" })
420
+ ] }),
421
+ result.action === "warn" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
422
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "\u26A1" }),
423
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "This image may not be appropriate" })
424
+ ] }),
425
+ result.safe && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
426
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "\u2705" }),
427
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Image looks good" })
428
+ ] })
429
+ ] }),
430
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "preview-actions", children: [
431
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", onClick: onRemove, className: "btn-remove", children: "Remove" }),
432
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
433
+ "button",
434
+ {
435
+ type: "button",
436
+ onClick: onConfirm,
437
+ disabled: !canConfirm,
438
+ className: "btn-confirm",
439
+ children: blockUnsafe && !result.safe ? "Cannot Upload" : "Upload"
440
+ }
441
+ )
442
+ ] })
443
+ ] });
444
+ }
445
+
446
+ // src/ModeratedVideoUpload.tsx
447
+ var import_react4 = require("react");
448
+ var import_jsx_runtime3 = require("react/jsx-runtime");
449
+ function ModeratedVideoUpload({
450
+ apiKey,
451
+ policyId,
452
+ onUpload,
453
+ onReject,
454
+ maxSizeMB = 100,
455
+ maxDurationSeconds = 300,
456
+ acceptedFormats = ["video/mp4", "video/webm", "video/quicktime"],
457
+ showPreview = true,
458
+ blockUnsafe = true,
459
+ extractFramesCount = 3,
460
+ customPreview,
461
+ className = "",
462
+ disabled = false,
463
+ onModerationResult,
464
+ onModerationError
465
+ }) {
466
+ const [file, setFile] = (0, import_react4.useState)(null);
467
+ const [preview, setPreview] = (0, import_react4.useState)(null);
468
+ const [thumbnail, setThumbnail] = (0, import_react4.useState)(null);
469
+ const [duration, setDuration] = (0, import_react4.useState)(0);
470
+ const [uploadError, setUploadError] = (0, import_react4.useState)(null);
471
+ const [frameExtractionProgress, setFrameExtractionProgress] = (0, import_react4.useState)(0);
472
+ const [isDragOver, setIsDragOver] = (0, import_react4.useState)(false);
473
+ const [processingStage, setProcessingStage] = (0, import_react4.useState)("");
474
+ const fileInputRef = (0, import_react4.useRef)(null);
475
+ const videoRef = (0, import_react4.useRef)(null);
476
+ const canvasRef = (0, import_react4.useRef)(null);
477
+ const { result, check } = useModeration({
478
+ apiKey,
479
+ policyId,
480
+ debounceMs: 0,
481
+ enabled: !disabled,
482
+ onCheck: onModerationResult,
483
+ onError: onModerationError
484
+ });
485
+ const validateFile = (0, import_react4.useCallback)(
486
+ (selectedFile) => {
487
+ if (!acceptedFormats.includes(selectedFile.type)) {
488
+ return `Invalid file type. Accepted formats: ${acceptedFormats.map((f) => f.split("/")[1]?.toUpperCase() || "").join(", ")}`;
489
+ }
490
+ const fileSizeMB = selectedFile.size / (1024 * 1024);
491
+ if (fileSizeMB > maxSizeMB) {
492
+ return `File size exceeds ${maxSizeMB}MB limit`;
493
+ }
494
+ return null;
495
+ },
496
+ [acceptedFormats, maxSizeMB]
497
+ );
498
+ const generateThumbnail = (0, import_react4.useCallback)(async (video) => {
499
+ const canvas = canvasRef.current || document.createElement("canvas");
500
+ const ctx = canvas.getContext("2d");
501
+ if (!ctx) throw new Error("Canvas context not available");
502
+ canvas.width = video.videoWidth;
503
+ canvas.height = video.videoHeight;
504
+ const seekTime = Math.min(1, video.duration * 0.1);
505
+ return new Promise((resolve, reject) => {
506
+ video.currentTime = seekTime;
507
+ video.onseeked = () => {
508
+ try {
509
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
510
+ const thumbnailUrl = canvas.toDataURL("image/jpeg", 0.8);
511
+ resolve(thumbnailUrl);
512
+ } catch (error) {
513
+ reject(error);
514
+ }
515
+ };
516
+ video.onerror = () => reject(new Error("Failed to generate thumbnail"));
517
+ });
518
+ }, []);
519
+ const extractFrames = async (video) => {
520
+ const canvas = document.createElement("canvas");
521
+ const ctx = canvas.getContext("2d");
522
+ if (!ctx) throw new Error("Failed to get canvas context");
523
+ canvas.width = video.videoWidth;
524
+ canvas.height = video.videoHeight;
525
+ const frames = [];
526
+ const interval = video.duration / extractFramesCount;
527
+ setProcessingStage("Extracting frames for analysis...");
528
+ for (let i = 0; i < extractFramesCount; i++) {
529
+ const time = i * interval;
530
+ await new Promise((resolve) => {
531
+ video.currentTime = time;
532
+ video.onseeked = () => {
533
+ ctx.drawImage(video, 0, 0);
534
+ frames.push(canvas.toDataURL("image/jpeg", 0.8));
535
+ const progress = (i + 1) / extractFramesCount * 100;
536
+ setFrameExtractionProgress(progress);
537
+ setProcessingStage(`Extracting frames... ${Math.round(progress)}%`);
538
+ resolve();
539
+ };
540
+ });
541
+ }
542
+ setProcessingStage("Analyzing video content...");
543
+ return frames;
544
+ };
545
+ const handleDragOver = (e) => {
546
+ e.preventDefault();
547
+ setIsDragOver(true);
548
+ };
549
+ const handleDragLeave = (e) => {
550
+ e.preventDefault();
551
+ setIsDragOver(false);
552
+ };
553
+ const handleDrop = (e) => {
554
+ e.preventDefault();
555
+ setIsDragOver(false);
556
+ const droppedFile = e.dataTransfer.files[0];
557
+ if (droppedFile) {
558
+ processFile(droppedFile);
559
+ }
560
+ };
561
+ const processFile = async (selectedFile) => {
562
+ setUploadError(null);
563
+ setFrameExtractionProgress(0);
564
+ setProcessingStage("");
565
+ const validationError = validateFile(selectedFile);
566
+ if (validationError) {
567
+ setUploadError(validationError);
568
+ if (onReject) {
569
+ onReject(selectedFile, validationError);
570
+ }
571
+ return;
572
+ }
573
+ const videoUrl = URL.createObjectURL(selectedFile);
574
+ setPreview(videoUrl);
575
+ setFile(selectedFile);
576
+ const video = document.createElement("video");
577
+ video.src = videoUrl;
578
+ video.preload = "metadata";
579
+ video.crossOrigin = "anonymous";
580
+ video.onloadedmetadata = async () => {
581
+ const videoDuration = video.duration;
582
+ setDuration(videoDuration);
583
+ if (videoDuration > maxDurationSeconds) {
584
+ const error = `Video duration (${Math.round(videoDuration)}s) exceeds ${maxDurationSeconds}s limit`;
585
+ setUploadError(error);
586
+ if (onReject) {
587
+ onReject(selectedFile, error);
588
+ }
589
+ return;
590
+ }
591
+ try {
592
+ setProcessingStage("Generating thumbnail...");
593
+ const thumbnailUrl = await generateThumbnail(video);
594
+ setThumbnail(thumbnailUrl);
595
+ const frames = await extractFrames(video);
596
+ if (frames.length > 0) {
597
+ check({
598
+ content: frames[0] || "",
599
+ contentType: "video",
600
+ policyId,
601
+ metadata: {
602
+ totalFrames: frames.length,
603
+ videoDuration,
604
+ filename: selectedFile.name,
605
+ fileSize: selectedFile.size,
606
+ demoMode: true
607
+ }
608
+ });
609
+ }
610
+ setProcessingStage("Moderation complete");
611
+ } catch (error) {
612
+ const err = error;
613
+ setUploadError(err.message);
614
+ setProcessingStage("");
615
+ if (onReject) {
616
+ onReject(selectedFile, err.message);
617
+ }
618
+ }
619
+ };
620
+ video.onerror = () => {
621
+ const error = "Failed to load video metadata";
622
+ setUploadError(error);
623
+ setProcessingStage("");
624
+ if (onReject) {
625
+ onReject(selectedFile, error);
626
+ }
627
+ };
628
+ };
629
+ const handleFileSelect = async (e) => {
630
+ const selectedFile = e.target.files?.[0];
631
+ if (selectedFile) {
632
+ await processFile(selectedFile);
633
+ }
634
+ };
635
+ const handleRemove = () => {
636
+ if (preview) {
637
+ URL.revokeObjectURL(preview);
638
+ }
639
+ setFile(null);
640
+ setPreview(null);
641
+ setThumbnail(null);
642
+ setDuration(0);
643
+ setUploadError(null);
644
+ setFrameExtractionProgress(0);
645
+ setProcessingStage("");
646
+ setIsDragOver(false);
647
+ if (fileInputRef.current) {
648
+ fileInputRef.current.value = "";
649
+ }
650
+ };
651
+ const handleConfirm = () => {
652
+ if (!file) return;
653
+ if (blockUnsafe && !result.safe) {
654
+ if (onReject) {
655
+ onReject(file, "Content violates community guidelines");
656
+ }
657
+ return;
658
+ }
659
+ if (onUpload) {
660
+ onUpload(file, {
661
+ safe: result.safe,
662
+ flagged: result.flagged,
663
+ action: result.action
664
+ });
665
+ }
666
+ handleRemove();
667
+ };
668
+ const triggerFileInput = () => {
669
+ fileInputRef.current?.click();
670
+ };
671
+ const formatDuration = (seconds) => {
672
+ const mins = Math.floor(seconds / 60);
673
+ const secs = Math.floor(seconds % 60);
674
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
675
+ };
676
+ (0, import_react4.useEffect)(() => {
677
+ return () => {
678
+ if (preview) {
679
+ URL.revokeObjectURL(preview);
680
+ }
681
+ };
682
+ }, [preview]);
683
+ const formatFileSize = (bytes) => {
684
+ const mb = bytes / (1024 * 1024);
685
+ return mb >= 1 ? `${mb.toFixed(1)} MB` : `${(bytes / 1024).toFixed(0)} KB`;
686
+ };
687
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `moderated-video-upload ${className}`, children: [
688
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
689
+ "input",
690
+ {
691
+ ref: fileInputRef,
692
+ type: "file",
693
+ accept: acceptedFormats.join(","),
694
+ onChange: handleFileSelect,
695
+ disabled,
696
+ style: { display: "none" }
697
+ }
698
+ ),
699
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("canvas", { ref: canvasRef, style: { display: "none" } }),
700
+ !file ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
701
+ "div",
702
+ {
703
+ className: `upload-area ${isDragOver ? "drag-over" : ""} ${disabled ? "disabled" : ""}`,
704
+ onClick: !disabled ? triggerFileInput : void 0,
705
+ onDragOver: !disabled ? handleDragOver : void 0,
706
+ onDragLeave: !disabled ? handleDragLeave : void 0,
707
+ onDrop: !disabled ? handleDrop : void 0,
708
+ role: "button",
709
+ tabIndex: disabled ? -1 : 0,
710
+ onKeyPress: (e) => {
711
+ if (!disabled && (e.key === "Enter" || e.key === " ")) {
712
+ triggerFileInput();
713
+ }
714
+ },
715
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "upload-content", children: [
716
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "upload-icon", children: "\u{1F3A5}" }),
717
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "upload-title", children: isDragOver ? "Drop your video here" : "Upload Video for Moderation" }),
718
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "upload-text", children: isDragOver ? "Release to upload" : "Click to browse or drag & drop your video here" }),
719
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "upload-specs", children: [
720
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "spec-item", children: [
721
+ "\u{1F4C4} ",
722
+ acceptedFormats.map((f) => f.split("/")[1]?.toUpperCase() || "").join(", ")
723
+ ] }),
724
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "spec-item", children: [
725
+ "\u{1F4E6} Max ",
726
+ maxSizeMB,
727
+ " MB"
728
+ ] }),
729
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "spec-item", children: [
730
+ "\u23F1\uFE0F Max ",
731
+ formatDuration(maxDurationSeconds)
732
+ ] })
733
+ ] }),
734
+ uploadError && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "upload-error", children: [
735
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "error-icon", children: "\u26A0\uFE0F" }),
736
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "error-message", children: uploadError })
737
+ ] })
738
+ ] })
739
+ }
740
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: customPreview ? customPreview({
741
+ file,
742
+ preview,
743
+ duration,
744
+ result,
745
+ onRemove: handleRemove
746
+ }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
747
+ DefaultPreview2,
748
+ {
749
+ file,
750
+ preview,
751
+ thumbnail,
752
+ duration,
753
+ result,
754
+ frameExtractionProgress,
755
+ processingStage,
756
+ onRemove: handleRemove,
757
+ onConfirm: handleConfirm,
758
+ blockUnsafe,
759
+ formatDuration,
760
+ formatFileSize
761
+ }
762
+ ) })
763
+ ] });
764
+ }
765
+ function DefaultPreview2({
766
+ file,
767
+ preview,
768
+ thumbnail,
769
+ duration,
770
+ result,
771
+ frameExtractionProgress,
772
+ processingStage,
773
+ onRemove,
774
+ onConfirm,
775
+ blockUnsafe,
776
+ formatDuration,
777
+ formatFileSize
778
+ }) {
779
+ const [showVideo, setShowVideo] = (0, import_react4.useState)(false);
780
+ const isProcessing = result.isChecking || frameExtractionProgress < 100 || processingStage;
781
+ const canConfirm = !isProcessing && (result.safe || !blockUnsafe);
782
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "video-preview", children: [
783
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "preview-container", children: [
784
+ showVideo ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
785
+ "video",
786
+ {
787
+ src: preview,
788
+ controls: true,
789
+ className: "preview-video",
790
+ onError: () => setShowVideo(false)
791
+ }
792
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "thumbnail-container", onClick: () => setShowVideo(true), children: [
793
+ thumbnail ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("img", { src: thumbnail, alt: "Video thumbnail", className: "video-thumbnail" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "thumbnail-placeholder", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "video-icon", children: "\u{1F3A5}" }) }),
794
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "play-overlay", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "play-button", children: "\u25B6" }) }),
795
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "duration-badge", children: formatDuration(duration) })
796
+ ] }),
797
+ isProcessing && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "processing-overlay", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "processing-content", children: [
798
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "processing-spinner" }),
799
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "processing-text", children: processingStage || (frameExtractionProgress < 100 ? `Extracting frames... ${frameExtractionProgress.toFixed(0)}%` : "Analyzing video content...") }),
800
+ frameExtractionProgress > 0 && frameExtractionProgress < 100 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "progress-bar", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
801
+ "div",
802
+ {
803
+ className: "progress-fill",
804
+ style: { width: `${frameExtractionProgress}%` }
805
+ }
806
+ ) })
807
+ ] }) })
808
+ ] }),
809
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "preview-info", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "file-details", children: [
810
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h4", { className: "file-name", children: file.name }),
811
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "file-metadata", children: [
812
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "metadata-item", children: [
813
+ "\u{1F4E6} ",
814
+ formatFileSize(file.size)
815
+ ] }),
816
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "metadata-item", children: [
817
+ "\u23F1\uFE0F ",
818
+ formatDuration(duration)
819
+ ] }),
820
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "metadata-item", children: "\u{1F3AC} Video File" })
821
+ ] })
822
+ ] }) }),
823
+ result.error && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "moderation-error", children: [
824
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "error-icon", children: "\u26A0\uFE0F" }),
825
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "error-message", children: result.error })
826
+ ] }),
827
+ !isProcessing && !result.error && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `moderation-status status-${result.action}`, children: [
828
+ result.action === "block" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
829
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "status-icon", children: "\u{1F6AB}" }),
830
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "status-content", children: [
831
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "status-text", children: "Video blocked" }),
832
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "status-detail", children: "Contains content that violates community guidelines" })
833
+ ] })
834
+ ] }),
835
+ result.action === "flag" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
836
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "status-icon", children: "\u26A0\uFE0F" }),
837
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "status-content", children: [
838
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "status-text", children: "Video flagged for review" }),
839
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "status-detail", children: "Contains potentially inappropriate content" })
840
+ ] })
841
+ ] }),
842
+ result.action === "warn" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
843
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "status-icon", children: "\u26A1" }),
844
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "status-content", children: [
845
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "status-text", children: "Video may need attention" }),
846
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "status-detail", children: "Some content may not be appropriate" })
847
+ ] })
848
+ ] }),
849
+ result.safe && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
850
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "status-icon", children: "\u2705" }),
851
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "status-content", children: [
852
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "status-text", children: "Video approved" }),
853
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "status-detail", children: "No policy violations detected" })
854
+ ] })
855
+ ] })
856
+ ] }),
857
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "preview-actions", children: [
858
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: onRemove, className: "btn-remove", children: "Remove Video" }),
859
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
860
+ "button",
861
+ {
862
+ type: "button",
863
+ onClick: onConfirm,
864
+ disabled: !canConfirm,
865
+ className: "btn-confirm",
866
+ children: isProcessing ? "Processing..." : blockUnsafe && !result.safe ? "Cannot Upload" : "Upload Video"
867
+ }
868
+ )
869
+ ] })
870
+ ] });
871
+ }
872
+ // Annotate the CommonJS export names for ESM import in node:
873
+ 0 && (module.exports = {
874
+ ModeratedImageUpload,
875
+ ModeratedTextarea,
876
+ ModeratedVideoUpload,
877
+ useModeration
878
+ });