Skip to content

Commit 2ff3dff

Browse files
committed
add missing modal
1 parent 89c259a commit 2ff3dff

File tree

3 files changed

+172
-1
lines changed

3 files changed

+172
-1
lines changed

ui/src/components/ActionBar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index";
1919
import MountPopopover from "@/components/popovers/MountPopover";
2020
import ExtensionPopover from "@/components/popovers/ExtensionPopover";
2121
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
22+
2223
import OCRModal from "./popovers/OCRModal";
2324

2425
export default function Actionbar({
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { useCallback, useMemo, useState } from "react";
2+
import { LuCornerDownLeft } from "react-icons/lu";
3+
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
4+
import { type LoggerMessage } from "tesseract.js";
5+
6+
import { Button } from "@components/Button";
7+
import { GridCard } from "@components/Card";
8+
import { TextAreaWithLabel } from "@components/TextArea";
9+
import { SettingsPageHeader } from "@components/SettingsPageheader";
10+
import { useUiStore } from "@/hooks/stores";
11+
import useOCR from "@/hooks/useOCR";
12+
import { cx } from "@/cva.config";
13+
14+
export default function OCRModal({ videoElmRef }: { videoElmRef?: React.RefObject<HTMLVideoElement | null> }) {
15+
const { setDisableVideoFocusTrap } = useUiStore();
16+
const [ocrStatus, setOcrStatus] = useState<LoggerMessage>();
17+
const [ocrError, setOcrError] = useState<string | null>(null);
18+
const handleOcrError = useCallback((error: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
19+
if (typeof error === "string") {
20+
setOcrError(error);
21+
} else {
22+
setOcrError(error.message);
23+
}
24+
}, [setOcrError]);
25+
const [ocrText, setOcrText] = useState<string | null>(null);
26+
27+
const { ocrImage } = useOCR();
28+
29+
const onConfirmOCR = useCallback(async () => {
30+
setDisableVideoFocusTrap(true);
31+
32+
setOcrText(null);
33+
setOcrError(null);
34+
setOcrStatus(undefined);
35+
36+
if (!videoElmRef?.current) {
37+
setOcrError("Video element not found");
38+
return;
39+
}
40+
41+
setOcrStatus({
42+
status: "Capturing image",
43+
progress: 0,
44+
jobId: "",
45+
userJobId: "",
46+
workerId: "",
47+
});
48+
49+
// create a canvas from the video element then capture the image from the canvas
50+
const video = videoElmRef?.current;
51+
const canvas = document.createElement("canvas");
52+
canvas.width = video.videoWidth;
53+
canvas.height = video.videoHeight;
54+
55+
const ctx = canvas.getContext("2d");
56+
ctx?.drawImage(video, 0, 0, canvas.width, canvas.height);
57+
58+
const text = await ocrImage(["eng"], canvas, { logger: setOcrStatus, errorHandler: handleOcrError });
59+
setOcrText(text);
60+
61+
setOcrStatus(undefined);
62+
setOcrError(null);
63+
}, [
64+
videoElmRef,
65+
setDisableVideoFocusTrap,
66+
ocrImage,
67+
setOcrStatus,
68+
setOcrError,
69+
handleOcrError,
70+
]);
71+
72+
const ocrProgress = useMemo(() => {
73+
if (!ocrStatus?.progress) return 0;
74+
return Math.round(ocrStatus?.progress * 100);
75+
}, [ocrStatus]);
76+
77+
return (
78+
<GridCard>
79+
<div className="space-y-4 p-4 py-3">
80+
<div className="grid h-full grid-rows-(--grid-headerBody)">
81+
<div className="h-full space-y-4">
82+
<div className="space-y-4">
83+
<SettingsPageHeader
84+
title="OCR"
85+
description="OCR text from the video"
86+
/>
87+
88+
<div
89+
className={cx("animate-fadeIn space-y-2 opacity-0", ocrText === null ? "hidden" : "")}
90+
style={{
91+
animationDuration: "0.7s",
92+
animationDelay: "0.1s",
93+
}}
94+
>
95+
<div>
96+
<div
97+
className="w-full"
98+
onKeyUp={e => e.stopPropagation()}
99+
onKeyDown={e => e.stopPropagation()}
100+
>
101+
<TextAreaWithLabel
102+
value={ocrText || ""}
103+
label="Text"
104+
rows={4}
105+
readOnly
106+
spellCheck={false}
107+
data-lt="false"
108+
data-gram="false"
109+
className="font-mono"
110+
/>
111+
</div>
112+
</div>
113+
</div>
114+
115+
{ocrStatus && <div className={cx("animate-fadeIn space-y-2 opacity-0")}
116+
style={{
117+
animationDuration: "0.7s",
118+
animationDelay: "0.1s",
119+
}}
120+
>
121+
<div className="space-y-1 flex justify-between">
122+
<p className="text-xs text-slate-600 dark:text-slate-400 capitalize">
123+
{ocrStatus?.status}
124+
</p>
125+
<span className="text-xs text-slate-600 dark:text-slate-400">{ocrProgress}%</span>
126+
</div>
127+
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300">
128+
<div
129+
style={{ width: ocrProgress + "%" }}
130+
className="h-2.5 bg-blue-700 transition-all duration-1000 ease-in-out"
131+
></div>
132+
</div>
133+
</div>}
134+
135+
{ocrError && (
136+
<div className="flex items-center gap-x-2">
137+
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
138+
<span className="text-xs text-red-500 dark:text-red-400">
139+
{ocrError}
140+
</span>
141+
</div>
142+
)}
143+
</div>
144+
</div>
145+
</div>
146+
<div className="gap-y-4">
147+
<p className="text-xs text-slate-600 dark:text-slate-400">
148+
Internet connectivity might be required to download Tesseract OCR trained data.
149+
</p>
150+
</div>
151+
<div className="flex animate-fadeIn items-center justify-end gap-x-2 opacity-0"
152+
style={{
153+
animationDuration: "0.7s",
154+
animationDelay: "0.2s",
155+
}}
156+
>
157+
<Button
158+
size="SM"
159+
theme="primary"
160+
text="Start OCR"
161+
onClick={onConfirmOCR}
162+
LeadingIcon={LuCornerDownLeft}
163+
/>
164+
</div>
165+
</div>
166+
</GridCard>
167+
);
168+
}

ui/src/hooks/useOCR.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ async function ocrImage(
99
image: ImageLike,
1010
options?: Partial<WorkerOptions>,
1111
) {
12-
const { createWorker } = await import('tesseract.js')
12+
const tesseract = await import('tesseract.js')
13+
const createWorker = tesseract.createWorker || tesseract.default.createWorker
14+
1315
const worker = await createWorker(language, undefined, options)
1416
const { data: { text } } = await worker.recognize(image)
1517
await worker.terminate()

0 commit comments

Comments
 (0)