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