From 2d9ec4e25855e9d281176d6b127df67d44bafdf2 Mon Sep 17 00:00:00 2001 From: Siddharth Kumar Sah Date: Sun, 12 Apr 2026 18:37:09 +0800 Subject: [PATCH] feat(ocr): rewrite UI with quality tiers, enhance toggle, editable results, download --- .../web/src/components/tools/ocr-settings.tsx | 222 ++++++++++++------ 1 file changed, 152 insertions(+), 70 deletions(-) diff --git a/apps/web/src/components/tools/ocr-settings.tsx b/apps/web/src/components/tools/ocr-settings.tsx index 442f3eca..25d9ba15 100644 --- a/apps/web/src/components/tools/ocr-settings.tsx +++ b/apps/web/src/components/tools/ocr-settings.tsx @@ -1,13 +1,20 @@ -import { Check, Copy } from "lucide-react"; +import { Check, ChevronDown, ChevronRight, Copy, Download, Info } from "lucide-react"; import { useRef, useState } from "react"; import { ProgressCard } from "@/components/common/progress-card"; import { formatHeaders } from "@/lib/api"; import { copyToClipboard, generateId } from "@/lib/utils"; import { useFileStore } from "@/stores/file-store"; -type OcrEngine = "tesseract" | "paddleocr"; +type OcrQuality = "fast" | "balanced" | "best"; + +const QUALITY_OPTIONS: { value: OcrQuality; label: string }[] = [ + { value: "fast", label: "Fast" }, + { value: "balanced", label: "Balanced" }, + { value: "best", label: "Best" }, +]; const LANGUAGES = [ + { code: "auto", label: "Auto-detect" }, { code: "en", label: "English" }, { code: "de", label: "German" }, { code: "fr", label: "French" }, @@ -17,13 +24,30 @@ const LANGUAGES = [ { code: "ko", label: "Korean" }, ]; +const ENHANCE_DEFAULTS: Record = { + fast: true, + balanced: true, + best: false, +}; + +function SectionLabel({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +

+ ); +} + export function OcrSettings() { const { files, processing, error, setProcessing, setError } = useFileStore(); - const [engine, setEngine] = useState("tesseract"); - const [language, setLanguage] = useState("en"); + const [quality, setQuality] = useState("balanced"); + const [language, setLanguage] = useState("auto"); + const [enhance, setEnhance] = useState(true); + const [enhanceManuallySet, setEnhanceManuallySet] = useState(false); + const [langOpen, setLangOpen] = useState(false); + const [text, setText] = useState(null); - const [detectedEngine, setDetectedEngine] = useState(""); const [copied, setCopied] = useState(false); const [progressPhase, setProgressPhase] = useState<"idle" | "uploading" | "processing">("idle"); const [progressPercent, setProgressPercent] = useState(0); @@ -31,6 +55,19 @@ export function OcrSettings() { const [elapsed, setElapsed] = useState(0); const elapsedRef = useRef | null>(null); + const handleQualityChange = (q: OcrQuality) => { + setQuality(q); + // Update enhance default unless user has manually toggled it + if (!enhanceManuallySet) { + setEnhance(ENHANCE_DEFAULTS[q]); + } + }; + + const handleEnhanceToggle = (checked: boolean) => { + setEnhance(checked); + setEnhanceManuallySet(true); + }; + const handleProcess = async () => { if (files.length === 0) return; @@ -49,7 +86,6 @@ export function OcrSettings() { const clientJobId = generateId(); - // Open SSE for server-side progress const es = new EventSource(`/api/v1/jobs/${clientJobId}/progress`); es.onmessage = (event) => { try { @@ -65,7 +101,7 @@ export function OcrSettings() { const formData = new FormData(); formData.append("file", files[0]); - formData.append("settings", JSON.stringify({ engine, language })); + formData.append("settings", JSON.stringify({ quality, language, enhance })); formData.append("clientJobId", clientJobId); const xhr = new XMLHttpRequest(); @@ -85,8 +121,7 @@ export function OcrSettings() { if (xhr.status >= 200 && xhr.status < 300) { try { const data = JSON.parse(xhr.responseText); - setText(data.text || ""); - setDetectedEngine(data.engine || engine); + setText(data.text ?? ""); } catch { setError("Invalid response"); } @@ -116,7 +151,7 @@ export function OcrSettings() { }; const handleCopy = async () => { - if (text) { + if (text !== null) { const ok = await copyToClipboard(text); if (ok) { setCopied(true); @@ -125,62 +160,93 @@ export function OcrSettings() { } }; + const handleDownload = () => { + if (text === null) return; + const blob = new Blob([text], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + const baseName = files[0]?.name?.replace(/\.[^.]+$/, "") ?? "extracted"; + a.href = url; + a.download = `${baseName}_ocr.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + const hasFile = files.length > 0; + const langLabel = LANGUAGES.find((l) => l.code === language)?.label ?? "Auto-detect"; return ( -
- {/* Engine selector */} -
-

OCR Engine

-
+
+ {/* Quality selector */} + Quality +
+ {QUALITY_OPTIONS.map((opt) => ( - -
+ ))}
- {/* Language selector */} -
- - handleEnhanceToggle(e.target.checked)} + className="rounded border-border accent-primary" + /> + Enhance before scanning + - {LANGUAGES.map((lang) => ( - - ))} - + + + + + {/* Language (collapsible) */} +
+ + {langOpen && ( + + )}
{/* Error */} {error &&

{error}

} - {/* Process button */} + {/* Process button / progress */} {processing ? (
- - + Extracted Text +
+ {text.length > 0 && ( + + )} + +
-