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