@indora-labs/redaction-react 0.1.0

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,1079 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ DocumentRedactionViewer: () => DocumentRedactionViewer,
34
+ FileViewer: () => FileViewer,
35
+ PdfRedactionViewer: () => PdfRedactionViewer,
36
+ RedactionInspector: () => RedactionInspector,
37
+ RedactionReviewStatus: () => RedactionReviewStatus,
38
+ ViewerIngestionMode: () => ViewerIngestionMode
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // src/components/DocumentRedactionViewer.tsx
43
+ var import_react = require("react");
44
+ var import_jsx_runtime = require("react/jsx-runtime");
45
+ function clamp(v, min, max) {
46
+ return Math.max(min, Math.min(max, v));
47
+ }
48
+ function rectNormalize(x1, y1, x2, y2) {
49
+ const x = Math.min(x1, x2);
50
+ const y = Math.min(y1, y2);
51
+ const w = Math.abs(x2 - x1);
52
+ const h = Math.abs(y2 - y1);
53
+ return { x, y, w, h };
54
+ }
55
+ function unionBoxes(boxes) {
56
+ if (boxes.length === 0) return null;
57
+ let minX = boxes[0].x;
58
+ let minY = boxes[0].y;
59
+ let maxX = boxes[0].x + boxes[0].w;
60
+ let maxY = boxes[0].y + boxes[0].h;
61
+ for (let i = 1; i < boxes.length; i++) {
62
+ minX = Math.min(minX, boxes[i].x);
63
+ minY = Math.min(minY, boxes[i].y);
64
+ maxX = Math.max(maxX, boxes[i].x + boxes[i].w);
65
+ maxY = Math.max(maxY, boxes[i].y + boxes[i].h);
66
+ }
67
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
68
+ }
69
+ function hitTestWord(pt, w) {
70
+ return pt.x >= w.x && pt.x <= w.x + w.w && pt.y >= w.y && pt.y <= w.y + w.h;
71
+ }
72
+ function hitTestRect(pt, r) {
73
+ return pt.x >= r.x && pt.x <= r.x + r.w && pt.y >= r.y && pt.y <= r.y + r.h;
74
+ }
75
+ function hitTestHandle(pt, r, handleSize) {
76
+ const hs = handleSize;
77
+ const left = r.x;
78
+ const right = r.x + r.w;
79
+ const top = r.y;
80
+ const bottom = r.y + r.h;
81
+ const cx = r.x + r.w / 2;
82
+ const cy = r.y + r.h / 2;
83
+ const handles = [
84
+ { h: "nw", x: left, y: top },
85
+ { h: "n", x: cx, y: top },
86
+ { h: "ne", x: right, y: top },
87
+ { h: "e", x: right, y: cy },
88
+ { h: "se", x: right, y: bottom },
89
+ { h: "s", x: cx, y: bottom },
90
+ { h: "sw", x: left, y: bottom },
91
+ { h: "w", x: left, y: cy }
92
+ ];
93
+ for (const hh of handles) {
94
+ const dx = Math.abs(pt.x - hh.x);
95
+ const dy = Math.abs(pt.y - hh.y);
96
+ if (dx <= hs && dy <= hs) return hh.h;
97
+ }
98
+ return null;
99
+ }
100
+ var DocumentRedactionViewer = ({
101
+ pages,
102
+ ocrByPage,
103
+ rects,
104
+ onRectsChange,
105
+ onRectSelect,
106
+ selectedRectId,
107
+ defaultRectLabel = "REDACT",
108
+ allowCreate = true,
109
+ allowEdit = true,
110
+ zoom = 1,
111
+ className,
112
+ style,
113
+ pageFilter,
114
+ allowFreeformDragCreate = false
115
+ }) => {
116
+ const pageRefs = (0, import_react.useRef)({});
117
+ const pointerIdRef = (0, import_react.useRef)(null);
118
+ const selectedId = selectedRectId ?? null;
119
+ const isAnySelected = selectedId !== null;
120
+ const [drag, setDrag] = (0, import_react.useState)({ kind: "none" });
121
+ const visiblePages = (0, import_react.useMemo)(() => {
122
+ const filtered = pageFilter ? pages.filter(pageFilter) : pages;
123
+ return filtered.slice().sort((a, b) => a.page - b.page);
124
+ }, [pages, pageFilter]);
125
+ const selectedRect = (0, import_react.useMemo)(
126
+ () => rects.find((r) => r.id === selectedId) ?? null,
127
+ [rects, selectedId]
128
+ );
129
+ (0, import_react.useEffect)(() => {
130
+ if (!onRectSelect) return;
131
+ onRectSelect(selectedRect);
132
+ }, [selectedId, selectedRect, onRectSelect]);
133
+ const setPageRef = (0, import_react.useCallback)((page, el) => {
134
+ pageRefs.current[page] = el;
135
+ }, []);
136
+ const clientToPagePoint = (0, import_react.useCallback)(
137
+ (page, clientX, clientY) => {
138
+ const el = pageRefs.current[page.page];
139
+ if (!el) return null;
140
+ const b = el.getBoundingClientRect();
141
+ const x = (clientX - b.left) / zoom;
142
+ const y = (clientY - b.top) / zoom;
143
+ return {
144
+ x: clamp(x, 0, page.width),
145
+ y: clamp(y, 0, page.height)
146
+ };
147
+ },
148
+ [zoom]
149
+ );
150
+ const commitRects = (0, import_react.useCallback)(
151
+ (next) => {
152
+ onRectsChange(next);
153
+ },
154
+ [onRectsChange]
155
+ );
156
+ function generateRectId() {
157
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
158
+ return crypto.randomUUID();
159
+ }
160
+ return `rect_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
161
+ }
162
+ const trySelectRect = (0, import_react.useCallback)(
163
+ (pageNum, pt) => {
164
+ for (let i = rects.length - 1; i >= 0; i--) {
165
+ const r = rects[i];
166
+ if (r.page !== pageNum) continue;
167
+ const handle = hitTestHandle(pt, r, 6);
168
+ if (handle) return { index: i, handle };
169
+ if (hitTestRect(pt, r)) return { index: i, handle: null };
170
+ }
171
+ return { index: null, handle: null };
172
+ },
173
+ [rects]
174
+ );
175
+ const beginPointer = (0, import_react.useCallback)(
176
+ (e, page) => {
177
+ if (pointerIdRef.current !== null) return;
178
+ pointerIdRef.current = e.pointerId;
179
+ const pt = clientToPagePoint(page, e.clientX, e.clientY);
180
+ if (!pt) return;
181
+ if (allowEdit) {
182
+ const hit = trySelectRect(page.page, pt);
183
+ if (hit.index !== null) {
184
+ onRectSelect?.(rects[hit.index]);
185
+ if (hit.handle) {
186
+ const startRect = rects[hit.index];
187
+ setDrag({ kind: "resizing", index: hit.index, page: page.page, handle: hit.handle, startPt: pt, startRect });
188
+ } else {
189
+ const startRect = rects[hit.index];
190
+ setDrag({ kind: "moving", index: hit.index, page: page.page, startPt: pt, startRect });
191
+ }
192
+ e.currentTarget.setPointerCapture(e.pointerId);
193
+ return;
194
+ }
195
+ }
196
+ if (!allowCreate) {
197
+ onRectSelect?.(null);
198
+ setDrag({ kind: "none" });
199
+ return;
200
+ }
201
+ onRectSelect?.(null);
202
+ const words = ocrByPage?.[page.page] ?? [];
203
+ if (words.length > 0) {
204
+ const wordIds = [];
205
+ for (let i = 0; i < words.length; i++) {
206
+ if (hitTestWord(pt, words[i])) {
207
+ wordIds.push(i);
208
+ break;
209
+ }
210
+ }
211
+ setDrag({ kind: "selecting", page: page.page, startPt: pt, curPt: pt, wordIds });
212
+ } else if (allowFreeformDragCreate) {
213
+ setDrag({ kind: "freeform", page: page.page, startPt: pt, curPt: pt });
214
+ } else {
215
+ setDrag({ kind: "none" });
216
+ }
217
+ e.currentTarget.setPointerCapture(e.pointerId);
218
+ },
219
+ [allowCreate, allowEdit, allowFreeformDragCreate, clientToPagePoint, ocrByPage, rects, trySelectRect]
220
+ );
221
+ const movePointer = (0, import_react.useCallback)(
222
+ (e, page) => {
223
+ if (pointerIdRef.current !== e.pointerId) return;
224
+ const pt = clientToPagePoint(page, e.clientX, e.clientY);
225
+ if (!pt) return;
226
+ setDrag((cur) => {
227
+ if (cur.kind === "selecting" && cur.page === page.page) {
228
+ const words = ocrByPage?.[page.page] ?? [];
229
+ const { x, y, w, h } = rectNormalize(cur.startPt.x, cur.startPt.y, pt.x, pt.y);
230
+ const nextIds = [];
231
+ for (let i = 0; i < words.length; i++) {
232
+ const ww = words[i];
233
+ const intersects = ww.x <= x + w && ww.x + ww.w >= x && ww.y <= y + h && ww.y + ww.h >= y;
234
+ if (intersects) nextIds.push(i);
235
+ }
236
+ return { ...cur, curPt: pt, wordIds: nextIds };
237
+ }
238
+ if (cur.kind === "freeform" && cur.page === page.page) {
239
+ return { ...cur, curPt: pt };
240
+ }
241
+ if (cur.kind === "moving" && cur.page === page.page) {
242
+ const dx = pt.x - cur.startPt.x;
243
+ const dy = pt.y - cur.startPt.y;
244
+ const nr = {
245
+ ...cur.startRect,
246
+ x: clamp(cur.startRect.x + dx, 0, page.width - cur.startRect.w),
247
+ y: clamp(cur.startRect.y + dy, 0, page.height - cur.startRect.h)
248
+ };
249
+ const next = rects.slice();
250
+ next[cur.index] = nr;
251
+ commitRects(next);
252
+ return cur;
253
+ }
254
+ if (cur.kind === "resizing" && cur.page === page.page) {
255
+ const dx = pt.x - cur.startPt.x;
256
+ const dy = pt.y - cur.startPt.y;
257
+ const r0 = cur.startRect;
258
+ let x = r0.x;
259
+ let y = r0.y;
260
+ let w = r0.w;
261
+ let h = r0.h;
262
+ const minSize = 4;
263
+ const applyW = (delta) => {
264
+ w = clamp(r0.w + delta, minSize, page.width);
265
+ };
266
+ const applyH = (delta) => {
267
+ h = clamp(r0.h + delta, minSize, page.height);
268
+ };
269
+ const applyX = (delta) => {
270
+ x = clamp(r0.x + delta, 0, page.width);
271
+ };
272
+ const applyY = (delta) => {
273
+ y = clamp(r0.y + delta, 0, page.height);
274
+ };
275
+ switch (cur.handle) {
276
+ case "e":
277
+ applyW(dx);
278
+ break;
279
+ case "s":
280
+ applyH(dy);
281
+ break;
282
+ case "se":
283
+ applyW(dx);
284
+ applyH(dy);
285
+ break;
286
+ case "w": {
287
+ const newX = clamp(r0.x + dx, 0, r0.x + r0.w - minSize);
288
+ const newW = r0.x + r0.w - newX;
289
+ x = newX;
290
+ w = newW;
291
+ break;
292
+ }
293
+ case "n": {
294
+ const newY = clamp(r0.y + dy, 0, r0.y + r0.h - minSize);
295
+ const newH = r0.y + r0.h - newY;
296
+ y = newY;
297
+ h = newH;
298
+ break;
299
+ }
300
+ case "nw": {
301
+ const newX = clamp(r0.x + dx, 0, r0.x + r0.w - minSize);
302
+ const newY = clamp(r0.y + dy, 0, r0.y + r0.h - minSize);
303
+ x = newX;
304
+ y = newY;
305
+ w = r0.x + r0.w - newX;
306
+ h = r0.y + r0.h - newY;
307
+ break;
308
+ }
309
+ case "ne": {
310
+ const newY = clamp(r0.y + dy, 0, r0.y + r0.h - minSize);
311
+ y = newY;
312
+ h = r0.y + r0.h - newY;
313
+ applyW(dx);
314
+ break;
315
+ }
316
+ case "sw": {
317
+ const newX = clamp(r0.x + dx, 0, r0.x + r0.w - minSize);
318
+ x = newX;
319
+ w = r0.x + r0.w - newX;
320
+ applyH(dy);
321
+ break;
322
+ }
323
+ }
324
+ w = clamp(w, minSize, page.width - x);
325
+ h = clamp(h, minSize, page.height - y);
326
+ const nr = { ...r0, x, y, w, h };
327
+ const next = rects.slice();
328
+ next[cur.index] = nr;
329
+ commitRects(next);
330
+ return cur;
331
+ }
332
+ return cur;
333
+ });
334
+ },
335
+ [clientToPagePoint, commitRects, ocrByPage, rects, zoom]
336
+ );
337
+ const endPointer = (0, import_react.useCallback)(
338
+ (e, page) => {
339
+ if (pointerIdRef.current !== e.pointerId) return;
340
+ pointerIdRef.current = null;
341
+ setDrag((cur) => {
342
+ if (cur.kind === "selecting" && cur.page === page.page) {
343
+ const words = ocrByPage?.[page.page] ?? [];
344
+ const selected = cur.wordIds.map((i) => words[i]).filter(Boolean);
345
+ const u = unionBoxes(selected);
346
+ if (u && u.w > 2 && u.h > 2) {
347
+ const id = generateRectId();
348
+ const next = rects.concat([
349
+ { id, page: page.page, x: u.x, y: u.y, w: u.w, h: u.h, label: defaultRectLabel }
350
+ ]);
351
+ commitRects(next);
352
+ onRectSelect?.(
353
+ { id, page: page.page, x: u.x, y: u.y, w: u.w, h: u.h, label: defaultRectLabel }
354
+ );
355
+ }
356
+ return { kind: "none" };
357
+ }
358
+ if (cur.kind === "freeform" && cur.page === page.page) {
359
+ const r = rectNormalize(cur.startPt.x, cur.startPt.y, cur.curPt.x, cur.curPt.y);
360
+ if (r.w > 2 && r.h > 2) {
361
+ const id = generateRectId();
362
+ const next = rects.concat([
363
+ { id, page: page.page, x: r.x, y: r.y, w: r.w, h: r.h, label: defaultRectLabel }
364
+ ]);
365
+ commitRects(next);
366
+ onRectSelect?.(
367
+ { id, page: page.page, x: r.x, y: r.y, w: r.w, h: r.h, label: defaultRectLabel }
368
+ );
369
+ }
370
+ return { kind: "none" };
371
+ }
372
+ if (cur.kind === "moving" || cur.kind === "resizing") {
373
+ return { kind: "none" };
374
+ }
375
+ return { kind: "none" };
376
+ });
377
+ },
378
+ [commitRects, defaultRectLabel, ocrByPage, rects]
379
+ );
380
+ const cancelPointer = (0, import_react.useCallback)(() => {
381
+ pointerIdRef.current = null;
382
+ setDrag({ kind: "none" });
383
+ }, []);
384
+ (0, import_react.useEffect)(() => {
385
+ const onKeyDown = (ev) => {
386
+ if (ev.key !== "Backspace" && ev.key !== "Delete") return;
387
+ if (!selectedId) return;
388
+ const target = ev.target;
389
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) {
390
+ return;
391
+ }
392
+ ev.preventDefault();
393
+ commitRects(rects.filter((r) => r.id !== selectedId));
394
+ onRectSelect?.(null);
395
+ };
396
+ window.addEventListener("keydown", onKeyDown);
397
+ return () => window.removeEventListener("keydown", onKeyDown);
398
+ }, [commitRects, rects, selectedId]);
399
+ const selectionOverlay = (0, import_react.useMemo)(() => {
400
+ if (drag.kind === "selecting") {
401
+ const r = rectNormalize(drag.startPt.x, drag.startPt.y, drag.curPt.x, drag.curPt.y);
402
+ return { page: drag.page, rect: r };
403
+ }
404
+ if (drag.kind === "freeform") {
405
+ const r = rectNormalize(drag.startPt.x, drag.startPt.y, drag.curPt.x, drag.curPt.y);
406
+ return { page: drag.page, rect: r };
407
+ }
408
+ return null;
409
+ }, [drag]);
410
+ const handleStyles = (0, import_react.useMemo)(() => {
411
+ const base = {
412
+ position: "absolute",
413
+ width: 10,
414
+ height: 10,
415
+ background: "#fff",
416
+ border: "1px solid rgba(59,130,246,0.9)",
417
+ borderRadius: 2,
418
+ boxShadow: "0 0 0 2px rgba(59,130,246,0.25)"
419
+ };
420
+ return {
421
+ nw: { ...base, left: -5, top: -5, cursor: "nwse-resize" },
422
+ n: { ...base, left: "50%", top: -5, transform: "translateX(-50%)", cursor: "ns-resize" },
423
+ ne: { ...base, right: -5, top: -5, cursor: "nesw-resize" },
424
+ e: { ...base, right: -5, top: "50%", transform: "translateY(-50%)", cursor: "ew-resize" },
425
+ se: { ...base, right: -5, bottom: -5, cursor: "nwse-resize" },
426
+ s: { ...base, left: "50%", bottom: -5, transform: "translateX(-50%)", cursor: "ns-resize" },
427
+ sw: { ...base, left: -5, bottom: -5, cursor: "nesw-resize" },
428
+ w: { ...base, left: -5, top: "50%", transform: "translateY(-50%)", cursor: "ew-resize" }
429
+ };
430
+ }, []);
431
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, style: { ...style, display: "flex", flexDirection: "column", gap: 16 }, children: visiblePages.map((page) => {
432
+ const pageRects = rects.filter((r) => r.page === page.page);
433
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
434
+ "div",
435
+ {
436
+ ref: (el) => setPageRef(page.page, el),
437
+ style: {
438
+ position: "relative",
439
+ width: page.width,
440
+ height: page.height,
441
+ transform: `scale(${zoom})`,
442
+ transformOrigin: "top left",
443
+ touchAction: "none",
444
+ userSelect: "none",
445
+ border: "1px solid rgba(0,0,0,0.08)",
446
+ borderRadius: 8,
447
+ overflow: "hidden",
448
+ background: "#f8f8f8"
449
+ },
450
+ onPointerDown: (e) => beginPointer(e, page),
451
+ onPointerMove: (e) => movePointer(e, page),
452
+ onPointerUp: (e) => endPointer(e, page),
453
+ onPointerCancel: cancelPointer,
454
+ onPointerLeave: (e) => {
455
+ if (pointerIdRef.current === null) return;
456
+ },
457
+ onClick: (e) => {
458
+ const pt = clientToPagePoint(page, e.clientX, e.clientY);
459
+ if (!pt) return;
460
+ const hit = trySelectRect(page.page, pt);
461
+ if (hit.index === null) onRectSelect?.(null);
462
+ },
463
+ children: [
464
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
465
+ "img",
466
+ {
467
+ src: page.imageUrl,
468
+ alt: `Page ${page.page}`,
469
+ draggable: false,
470
+ style: {
471
+ width: page.width,
472
+ display: "block"
473
+ }
474
+ }
475
+ ),
476
+ selectionOverlay && selectionOverlay.page === page.page ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
477
+ "div",
478
+ {
479
+ style: {
480
+ position: "absolute",
481
+ left: selectionOverlay.rect.x,
482
+ top: selectionOverlay.rect.y,
483
+ width: selectionOverlay.rect.w,
484
+ height: selectionOverlay.rect.h,
485
+ background: "rgba(59, 130, 246, 0.15)",
486
+ border: "1px dashed rgba(59, 130, 246, 0.8)",
487
+ pointerEvents: "none"
488
+ }
489
+ }
490
+ ) : null,
491
+ pageRects.map((r) => {
492
+ const isSelected = selectedId === r.id;
493
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
494
+ "div",
495
+ {
496
+ style: {
497
+ position: "absolute",
498
+ zIndex: isSelected ? 10 : 1,
499
+ left: r.x,
500
+ top: r.y,
501
+ width: r.w,
502
+ height: r.h,
503
+ background: isSelected ? "rgba(0,0,0,0.95)" : isAnySelected ? "rgba(0,0,0,0.55)" : "rgba(0,0,0,0.85)",
504
+ outline: "none",
505
+ boxShadow: isSelected ? `
506
+ 0 0 0 2px rgba(59,130,246,1),
507
+ 0 0 0 6px rgba(59,130,246,0.25)
508
+ ` : "0 0 0 1px rgba(255,255,255,0.15)",
509
+ cursor: allowEdit ? isSelected ? "move" : "pointer" : "default",
510
+ transition: "box-shadow 120ms ease, background 120ms ease"
511
+ },
512
+ onMouseDown: (ev) => {
513
+ ev.preventDefault();
514
+ },
515
+ onClick: (ev) => {
516
+ ev.stopPropagation();
517
+ onRectSelect?.(r);
518
+ },
519
+ children: [
520
+ r.label ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
521
+ "div",
522
+ {
523
+ style: {
524
+ position: "absolute",
525
+ left: "50%",
526
+ top: "50%",
527
+ transform: "translate(-50%, -50%)",
528
+ maxWidth: "90%",
529
+ // keep inside box
530
+ overflow: "hidden",
531
+ textOverflow: "ellipsis",
532
+ whiteSpace: "nowrap",
533
+ textTransform: "uppercase",
534
+ background: "rgba(255,255,255,0.9)",
535
+ color: "#111",
536
+ fontSize: 8,
537
+ padding: "2px 4px",
538
+ borderRadius: 999,
539
+ pointerEvents: "none"
540
+ },
541
+ children: r.type ?? r.label
542
+ }
543
+ ) : null,
544
+ allowEdit && isSelected ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: Object.keys(handleStyles).map((h) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: handleStyles[h] }, h)) }) : null
545
+ ]
546
+ },
547
+ `${page.page}-${r.id}`
548
+ );
549
+ })
550
+ ]
551
+ }
552
+ ) }, page.page);
553
+ }) });
554
+ };
555
+
556
+ // src/components/FileViewer.tsx
557
+ var import_react4 = __toESM(require("react"));
558
+ var import_lucide_react = require("lucide-react");
559
+
560
+ // src/components/PdfRedactionViewer.tsx
561
+ var import_react2 = require("react");
562
+ var pdfjs = __toESM(require("pdfjs-dist"));
563
+ var import_jsx_runtime2 = require("react/jsx-runtime");
564
+ pdfjs.GlobalWorkerOptions.workerSrc = "";
565
+ var PdfRedactionViewer = ({
566
+ pdfUrl,
567
+ rects,
568
+ onRectsChange,
569
+ onRectSelect,
570
+ selectedRectId,
571
+ defaultRectLabel,
572
+ allowCreate,
573
+ allowEdit,
574
+ zoom,
575
+ className,
576
+ style,
577
+ pageFilter,
578
+ allowFreeformDragCreate
579
+ }) => {
580
+ const [pages, setPages] = (0, import_react2.useState)([]);
581
+ const [loading, setLoading] = (0, import_react2.useState)(true);
582
+ const [error, setError] = (0, import_react2.useState)(null);
583
+ (0, import_react2.useEffect)(() => {
584
+ let cancelled = false;
585
+ async function loadPdf() {
586
+ setLoading(true);
587
+ setError(null);
588
+ try {
589
+ const loadingTask = pdfjs.getDocument({
590
+ url: pdfUrl,
591
+ withCredentials: false,
592
+ disableWorker: true
593
+ });
594
+ const pdf = await loadingTask.promise;
595
+ const nextPages = [];
596
+ for (let i = 1; i <= pdf.numPages; i++) {
597
+ const page = await pdf.getPage(i);
598
+ const viewport = page.getViewport({ scale: 2 });
599
+ const canvas = document.createElement("canvas");
600
+ const ctx = canvas.getContext("2d");
601
+ if (!ctx) {
602
+ throw new Error("Could not acquire 2D canvas context");
603
+ }
604
+ canvas.width = viewport.width;
605
+ canvas.height = viewport.height;
606
+ await page.render({
607
+ canvas,
608
+ viewport
609
+ }).promise;
610
+ nextPages.push({
611
+ page: i,
612
+ imageUrl: canvas.toDataURL("image/png"),
613
+ width: viewport.width,
614
+ height: viewport.height
615
+ });
616
+ }
617
+ if (!cancelled) {
618
+ setPages(nextPages);
619
+ setLoading(false);
620
+ }
621
+ } catch (err) {
622
+ if (!cancelled) {
623
+ setError("Failed to load or render PDF");
624
+ setLoading(false);
625
+ }
626
+ }
627
+ }
628
+ if (pdfUrl) {
629
+ loadPdf();
630
+ } else {
631
+ setLoading(false);
632
+ setError("No PDF URL provided");
633
+ }
634
+ return () => {
635
+ cancelled = true;
636
+ };
637
+ }, [pdfUrl]);
638
+ if (loading) {
639
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex items-center justify-center h-64 text-sm text-gray-500", children: "Loading PDF\u2026" });
640
+ }
641
+ if (error) {
642
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex items-center justify-center h-64 text-sm text-red-600", children: error });
643
+ }
644
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
645
+ DocumentRedactionViewer,
646
+ {
647
+ pages,
648
+ rects,
649
+ onRectsChange,
650
+ selectedRectId,
651
+ allowCreate,
652
+ defaultRectLabel,
653
+ allowEdit,
654
+ zoom,
655
+ className,
656
+ style,
657
+ pageFilter,
658
+ allowFreeformDragCreate,
659
+ onRectSelect
660
+ }
661
+ );
662
+ };
663
+
664
+ // src/components/RedactionInspector.tsx
665
+ var import_react3 = require("react");
666
+ var import_jsx_runtime3 = require("react/jsx-runtime");
667
+ var DEFAULT_REASON_BY_TYPE = {
668
+ PII: "Personally identifiable information",
669
+ SSN: "Social Security number",
670
+ Address: "Residential or mailing address",
671
+ Phone: "Phone number",
672
+ Custom: ""
673
+ };
674
+ var RedactionInspector = ({
675
+ rect,
676
+ rules,
677
+ onUpdate,
678
+ onDelete
679
+ }) => {
680
+ const [query, setQuery] = (0, import_react3.useState)("");
681
+ const [open, setOpen] = (0, import_react3.useState)(false);
682
+ (0, import_react3.useEffect)(() => {
683
+ if (!rect) return;
684
+ if (rect.type) {
685
+ setQuery(rect.type);
686
+ } else {
687
+ setQuery("");
688
+ }
689
+ setOpen(false);
690
+ }, [rect?.id]);
691
+ const ruleOptions = (0, import_react3.useMemo)(() => {
692
+ return rules.map((r) => ({
693
+ id: r.id,
694
+ label: r.target,
695
+ type: r.target,
696
+ reason: r.source_text
697
+ }));
698
+ }, [rules]);
699
+ const filteredRules = (0, import_react3.useMemo)(() => {
700
+ const q = query.toLowerCase();
701
+ return ruleOptions.filter(
702
+ (r) => r.label.toLowerCase().includes(q)
703
+ );
704
+ }, [query, ruleOptions]);
705
+ if (!rect) {
706
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "text-sm text-gray-500", children: "Select a redaction box to edit its properties" });
707
+ }
708
+ const applyRule = (rule) => {
709
+ const prevType = rect.type;
710
+ const prevDefault = prevType ? DEFAULT_REASON_BY_TYPE[prevType] : void 0;
711
+ const isAutoReason = !rect.reason || rect.reason === prevDefault;
712
+ onUpdate({
713
+ ...rect,
714
+ type: rule.type,
715
+ reason: isAutoReason ? rule.reason : rect.reason
716
+ });
717
+ setQuery(rule.type);
718
+ setOpen(false);
719
+ };
720
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "space-y-4", children: [
721
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "relative", children: [
722
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "text-xs font-medium text-gray-600", children: "Policy Rule" }),
723
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
724
+ "input",
725
+ {
726
+ className: "mt-1 w-full border rounded px-2 py-1 text-sm",
727
+ placeholder: "Search policy rules\u2026",
728
+ value: query,
729
+ onChange: (e) => {
730
+ setQuery(e.target.value);
731
+ setOpen(true);
732
+ },
733
+ onFocus: () => setOpen(true)
734
+ }
735
+ ),
736
+ open && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "absolute z-10 mt-1 w-full max-h-48 overflow-auto rounded border bg-white shadow", children: [
737
+ filteredRules.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "px-3 py-2 text-xs text-gray-500", children: "No matching rules" }) : filteredRules.map((rule) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
738
+ "div",
739
+ {
740
+ className: "px-3 py-2 text-sm cursor-pointer hover:bg-gray-100",
741
+ onMouseDown: () => applyRule(rule),
742
+ children: rule.label
743
+ },
744
+ rule.id
745
+ )),
746
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
747
+ "div",
748
+ {
749
+ className: "px-3 py-2 text-sm cursor-pointer border-t hover:bg-gray-100",
750
+ onMouseDown: () => applyRule({
751
+ type: "Custom",
752
+ reason: ""
753
+ }),
754
+ children: "Custom"
755
+ }
756
+ )
757
+ ] })
758
+ ] }),
759
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
760
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "text-xs font-medium text-gray-600", children: "Reason" }),
761
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
762
+ "textarea",
763
+ {
764
+ className: "mt-1 w-full border rounded px-2 py-1 text-sm",
765
+ rows: 3,
766
+ value: rect.reason ?? "",
767
+ onChange: (e) => onUpdate({ ...rect, reason: e.target.value })
768
+ }
769
+ )
770
+ ] }),
771
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
772
+ "button",
773
+ {
774
+ className: "w-full bg-red-600 text-white text-sm py-1.5 rounded",
775
+ onClick: () => onDelete(rect.id),
776
+ children: "Delete Redaction"
777
+ }
778
+ )
779
+ ] });
780
+ };
781
+
782
+ // src/components/FileViewer.tsx
783
+ var import_jsx_runtime4 = require("react/jsx-runtime");
784
+ var ViewerIngestionMode = /* @__PURE__ */ ((ViewerIngestionMode2) => {
785
+ ViewerIngestionMode2["Default"] = "default";
786
+ ViewerIngestionMode2["Policy"] = "policy";
787
+ return ViewerIngestionMode2;
788
+ })(ViewerIngestionMode || {});
789
+ var RedactionReviewStatus = {
790
+ Pending: "pending",
791
+ Approved: "approved",
792
+ Rejected: "rejected"
793
+ };
794
+ var isSupportedDocument = (type) => type.includes("pdf") || type.includes("text") || type.includes("document");
795
+ var FileViewer = ({
796
+ file,
797
+ documentUrl,
798
+ rects,
799
+ rules,
800
+ hideFileDetails = true,
801
+ hideAICaseFindings = true,
802
+ onRectsChange,
803
+ onClose,
804
+ onFinalizeRedaction,
805
+ onMarkRelevant,
806
+ onDeleteRedactionBox
807
+ }) => {
808
+ const [zoom, setZoom] = (0, import_react4.useState)(100);
809
+ const [, setSelectedBox] = (0, import_react4.useState)(null);
810
+ const [selectedRect, setSelectedRect] = (0, import_react4.useState)(null);
811
+ const unsupported = !isSupportedDocument(file.type);
812
+ const [expandedPolicyKey, setExpandedPolicyKey] = (0, import_react4.useState)(null);
813
+ const enforcedPolicies = import_react4.default.useMemo(() => {
814
+ const seen = /* @__PURE__ */ new Set();
815
+ return rects.filter((r) => r.type && r.reason).filter((r) => {
816
+ const key = `${r.type}::${r.reason}`;
817
+ if (seen.has(key)) return false;
818
+ seen.add(key);
819
+ return true;
820
+ }).map((r) => ({
821
+ type: r.type,
822
+ reason: r.reason
823
+ }));
824
+ }, [rects]);
825
+ const formatFileSize = (bytes) => {
826
+ if (bytes === 0) return "0 Bytes";
827
+ const k = 1024;
828
+ const sizes = ["Bytes", "KB", "MB", "GB"];
829
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
830
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
831
+ };
832
+ const formatDuration = (seconds) => {
833
+ const hours = Math.floor(seconds / 3600);
834
+ const minutes = Math.floor(seconds % 3600 / 60);
835
+ const secs = seconds % 60;
836
+ if (hours > 0) {
837
+ return `${hours}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
838
+ }
839
+ return `${minutes}:${secs.toString().padStart(2, "0")}`;
840
+ };
841
+ const renderMetadataPanel = () => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "bg-white rounded-lg border p-6", children: [
842
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h4", { className: "font-semibold text-gray-900 mb-4", children: "File Details" }),
843
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "space-y-3 text-sm", children: [
844
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex justify-between", children: [
845
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-gray-600", children: "File Type" }),
846
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "font-medium", children: file.type })
847
+ ] }),
848
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex justify-between", children: [
849
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-gray-600", children: "Size" }),
850
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "font-medium", children: formatFileSize(file.size) })
851
+ ] }),
852
+ file.pages && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex justify-between", children: [
853
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-gray-600", children: "Pages" }),
854
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "font-medium", children: file.pages })
855
+ ] }),
856
+ file.duration && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex justify-between", children: [
857
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-gray-600", children: "Duration" }),
858
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "font-medium", children: formatDuration(file.duration) })
859
+ ] }),
860
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex justify-between", children: [
861
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-gray-600", children: "Uploaded" }),
862
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "font-medium", children: new Date(file.uploadedAt).toLocaleDateString() })
863
+ ] }),
864
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex justify-between", children: [
865
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-gray-600", children: "Department" }),
866
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "font-medium", children: file.department })
867
+ ] }),
868
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex justify-between", children: [
869
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-gray-600", children: "Relevancy Score" }),
870
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "font-semibold text-blue-600", children: [
871
+ file.relevancyScore,
872
+ "%"
873
+ ] })
874
+ ] })
875
+ ] }),
876
+ file.tags.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "mt-4", children: [
877
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h5", { className: "font-medium text-gray-900 mb-2", children: "Detected Tags" }),
878
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex flex-wrap gap-2", children: file.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
879
+ "span",
880
+ {
881
+ className: "px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded-full hover:bg-gray-200 cursor-pointer",
882
+ title: "Click to highlight in document",
883
+ children: tag
884
+ },
885
+ tag
886
+ )) })
887
+ ] })
888
+ ] });
889
+ const renderCaseFindings = () => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "bg-blue-50 rounded-lg border border-blue-200 p-6", children: [
890
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h4", { className: "font-semibold text-blue-900 mb-3", children: "AI Case Findings" }),
891
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-sm text-blue-800 leading-relaxed", children: "This document contains critical information related to the incident. Key entities identified include officer badge numbers, suspect information, and timeline details. Several areas require redaction before public release, including personal identifiers and sensitive location details." }),
892
+ file.redactionFlags.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "mt-4", children: [
893
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h5", { className: "font-medium text-blue-900 mb-2", children: "Redaction Flags" }),
894
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "space-y-2", children: file.redactionFlags.map((flag) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-2 text-sm", children: [
895
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "w-2 h-2 bg-red-400 rounded-full" }),
896
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "text-blue-800", children: [
897
+ flag.type,
898
+ ": ",
899
+ flag.reason
900
+ ] })
901
+ ] }, flag.id)) })
902
+ ] })
903
+ ] });
904
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "bg-white rounded-xl shadow-2xl w-full max-w-7xl max-h-[95vh] overflow-hidden flex flex-col", children: [
905
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center justify-between p-6 border-b border-gray-200", children: [
906
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-3", children: [
907
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "p-2 bg-gray-100 rounded-lg", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react.FileText, { className: "w-6 h-6 text-gray-600" }) }),
908
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
909
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h2", { className: "text-xl font-semibold text-gray-900", children: file.name }),
910
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-4 text-sm text-gray-500 mt-1", children: [
911
+ file.size !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex items-center gap-1", children: [
912
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react.HardDrive, { className: "w-4 h-4" }),
913
+ formatFileSize(file.size)
914
+ ] }),
915
+ file.uploadedAt && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex items-center gap-1", children: [
916
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react.Calendar, { className: "w-4 h-4" }),
917
+ new Date(file.uploadedAt).toLocaleDateString()
918
+ ] }),
919
+ file.department && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex items-center gap-1", children: [
920
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react.Building, { className: "w-4 h-4" }),
921
+ file.department
922
+ ] })
923
+ ] })
924
+ ] })
925
+ ] }),
926
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: onClose, className: "p-2 hover:bg-gray-100 rounded-lg transition-colors", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react.X, { size: 20 }) })
927
+ ] }),
928
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
929
+ "div",
930
+ {
931
+ className: "p-6 pb-0 overflow-hidden",
932
+ style: { height: "calc(95vh - 160px)" },
933
+ children: unsupported ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex items-center justify-center h-full bg-gray-50 rounded-lg border", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "text-center", children: [
934
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react.File, { className: "w-16 h-16 mx-auto mb-4 text-gray-400" }),
935
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-lg font-medium text-gray-700", children: "This file type is not supported" }),
936
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-sm text-gray-500 mt-1", children: "Only document and PDF files can be reviewed and redacted." })
937
+ ] }) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "grid grid-cols-1 lg:grid-cols-3 gap-6 h-full min-h-0 grid-rows-[minmax(0,1fr)]", children: [
938
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "lg:col-span-2 bg-gray-50 rounded-lg h-full", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "h-full overflow-auto", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
939
+ PdfRedactionViewer,
940
+ {
941
+ pdfUrl: documentUrl,
942
+ rects,
943
+ onRectsChange,
944
+ onRectSelect: (redactionBox) => setSelectedRect(redactionBox),
945
+ selectedRectId: selectedRect?.id ?? null,
946
+ zoom: zoom / 100,
947
+ allowCreate: true,
948
+ allowEdit: true,
949
+ allowFreeformDragCreate: true
950
+ }
951
+ ) }) }),
952
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "h-full", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "h-full overflow-y-auto pr-1 space-y-6", children: [
953
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "bg-white rounded-lg border p-4", children: [
954
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h4", { className: "font-semibold text-gray-900 mb-3", children: "View Controls" }),
955
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-2", children: [
956
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
957
+ "button",
958
+ {
959
+ onClick: () => setZoom(Math.max(50, zoom - 5)),
960
+ className: "p-2 bg-gray-100 hover:bg-gray-200 rounded",
961
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react.ZoomOut, { size: 16 })
962
+ }
963
+ ),
964
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
965
+ "input",
966
+ {
967
+ type: "range",
968
+ min: "50",
969
+ max: "200",
970
+ step: "5",
971
+ value: zoom,
972
+ onChange: (e) => setZoom(Number(e.target.value)),
973
+ className: "flex-1"
974
+ }
975
+ ),
976
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
977
+ "button",
978
+ {
979
+ onClick: () => setZoom(Math.min(200, zoom + 5)),
980
+ className: "p-2 bg-gray-100 hover:bg-gray-200 rounded",
981
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react.ZoomIn, { size: 16 })
982
+ }
983
+ ),
984
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "w-12 text-center text-sm", children: [
985
+ zoom,
986
+ "%"
987
+ ] })
988
+ ] }),
989
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
990
+ "button",
991
+ {
992
+ onClick: () => setZoom(100),
993
+ className: "mt-2 w-full px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded text-sm text-gray-700",
994
+ children: "Reset Zoom"
995
+ }
996
+ )
997
+ ] }),
998
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "bg-white rounded-lg border p-4", children: [
999
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h4", { className: "font-semibold text-gray-900 mb-3", children: "Redaction Box Properties" }),
1000
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1001
+ RedactionInspector,
1002
+ {
1003
+ rect: selectedRect,
1004
+ rules,
1005
+ onUpdate: (updated) => {
1006
+ onRectsChange(rects.map((r) => r.id === updated.id ? updated : r));
1007
+ },
1008
+ onDelete: (id) => {
1009
+ onDeleteRedactionBox(id);
1010
+ setSelectedRect(null);
1011
+ setSelectedBox(null);
1012
+ }
1013
+ }
1014
+ )
1015
+ ] }),
1016
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "bg-white rounded-lg border p-4", children: [
1017
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h4", { className: "font-semibold text-gray-900 mb-3", children: "Enforced Policies" }),
1018
+ enforcedPolicies.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "text-sm text-gray-500 leading-relaxed", children: [
1019
+ "No policy-based redactions have been enforced yet.",
1020
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("br", {}),
1021
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-xs text-gray-400", children: "This section will populate automatically when a redaction is created based on a policy rule." })
1022
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "space-y-2", children: enforcedPolicies.map((p) => {
1023
+ const key = `${p.type}::${p.reason}`;
1024
+ const isExpanded = expandedPolicyKey === key;
1025
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1026
+ "div",
1027
+ {
1028
+ className: "rounded border border-gray-200 bg-gray-50 px-3 py-2 cursor-pointer transition",
1029
+ onClick: () => setExpandedPolicyKey(isExpanded ? null : key),
1030
+ children: [
1031
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center justify-between", children: [
1032
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-xs font-medium text-gray-900", children: p.type }),
1033
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-[10px] text-blue-600", children: isExpanded ? "Collapse" : "Expand" })
1034
+ ] }),
1035
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1036
+ "div",
1037
+ {
1038
+ className: `mt-1 text-xs text-gray-600 leading-snug transition-all ${isExpanded ? "max-h-[500px] whitespace-normal" : "max-h-10 overflow-hidden text-ellipsis whitespace-nowrap"}`,
1039
+ children: p.reason
1040
+ }
1041
+ )
1042
+ ]
1043
+ },
1044
+ key
1045
+ );
1046
+ }) })
1047
+ ] }),
1048
+ !hideFileDetails && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: renderMetadataPanel() }),
1049
+ !hideAICaseFindings && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: renderCaseFindings() })
1050
+ ] }) })
1051
+ ] })
1052
+ }
1053
+ ),
1054
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center justify-between p-6 border-t border-gray-200", children: [
1055
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "text-sm text-gray-600", children: "Redaction" }),
1056
+ onMarkRelevant && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex items-center gap-3", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1057
+ "button",
1058
+ {
1059
+ onClick: () => onFinalizeRedaction,
1060
+ className: "flex items-center gap-2 px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700",
1061
+ children: [
1062
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react.Check, { size: 18 }),
1063
+ "Finalize Redaction"
1064
+ ]
1065
+ }
1066
+ ) })
1067
+ ] })
1068
+ ] }) });
1069
+ };
1070
+ // Annotate the CommonJS export names for ESM import in node:
1071
+ 0 && (module.exports = {
1072
+ DocumentRedactionViewer,
1073
+ FileViewer,
1074
+ PdfRedactionViewer,
1075
+ RedactionInspector,
1076
+ RedactionReviewStatus,
1077
+ ViewerIngestionMode
1078
+ });
1079
+ //# sourceMappingURL=index.js.map