'use client'; import { useEffect, useRef, useState } from 'react'; import { Html5Qrcode, Html5QrcodeSupportedFormats } from 'html5-qrcode'; import { Camera, RefreshCw, XCircle, Type, Search } from 'lucide-react'; import { createWorker } from 'tesseract.js'; import { toast } from 'react-hot-toast'; interface ScannerProps { onScanSuccess: (decodedText: string) => void; onOCRMatch?: (text: string) => void; paused?: boolean; } export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerProps) { const html5QrCodeRef = useRef(null); const [isStarted, setIsStarted] = useState(false); const [error, setError] = useState(null); const [isOCRMode, setIsOCRMode] = useState(false); const [ocrProcessing, setOcrProcessing] = useState(false); const [zoom, setZoom] = useState(1); const [maxZoom, setMaxZoom] = useState(1); const [hasZoom, setHasZoom] = useState(false); const [detectedWords, setDetectedWords] = useState<{ text: string, bbox: { x0: number, y0: number, x1: number, y1: number } }[]>([]); const [isSelecting, setIsSelecting] = useState(false); const [capturedImage, setCapturedImage] = useState(null); const isBusy = useRef(false); const scannerId = "reader-container-unique"; useEffect(() => { if (!html5QrCodeRef.current) { html5QrCodeRef.current = new Html5Qrcode(scannerId); } const startScanner = async () => { if (isBusy.current) return; isBusy.current = true; try { if (html5QrCodeRef.current?.isScanning) { await html5QrCodeRef.current.stop(); } const config = { fps: 15, // Lower FPS for stability qrbox: { width: 280, height: 280 }, aspectRatio: 1.0, formatsToSupport: [ Html5QrcodeSupportedFormats.QR_CODE, Html5QrcodeSupportedFormats.CODE_128, Html5QrcodeSupportedFormats.CODE_39, Html5QrcodeSupportedFormats.EAN_13, Html5QrcodeSupportedFormats.UPC_A, Html5QrcodeSupportedFormats.DATAMATRIX ], // Simplified constraints for maximum compatibility videoConstraints: { facingMode: "environment" } }; await html5QrCodeRef.current?.start( { facingMode: "environment" }, config, (decodedText) => { onScanSuccess(decodedText); }, () => {} // Ignore frame errors ); // Check for zoom capability via video track const video = document.querySelector(`#${scannerId} video`) as HTMLVideoElement; const track = (video?.srcObject as MediaStream)?.getVideoTracks()[0]; const caps = track?.getCapabilities() as any; if (track && caps?.zoom) { setHasZoom(true); setMaxZoom(caps.zoom.max || 3); } setIsStarted(true); setError(null); } catch (err: any) { const errorMsg = String(err); if (!errorMsg.includes("is already starting") && !errorMsg.includes("already under transition")) { console.error("Scanner failed", err); setError(errorMsg || "Failed to access camera"); setIsStarted(false); } } finally { isBusy.current = false; } }; if (!paused) { startScanner(); } return () => { if (html5QrCodeRef.current?.isScanning) { html5QrCodeRef.current.stop() .then(() => { setIsStarted(false); }) .catch(e => console.error("Stop failed", e)); } }; }, [paused, onScanSuccess]); // Minimal deps // Separate effect for OCR interval to avoid restarting scanner useEffect(() => { let intervalId: any; if (isOCRMode && isStarted && !paused) { intervalId = setInterval(() => { handleOCR(); }, 5000); // 5 seconds is safer } return () => { if (intervalId) clearInterval(intervalId); }; }, [isOCRMode, isStarted, paused]); const handleOCR = async () => { if (ocrProcessing) return; setOcrProcessing(true); try { const video = document.querySelector(`#${scannerId} video`) as HTMLVideoElement; if (!video) return; const canvas = document.createElement('canvas'); const vWidth = video.videoWidth || 1280; const vHeight = video.videoHeight || 720; // Digital Zoom/Crop: 60% of center const cropFactor = 0.6; const sw = vWidth * cropFactor; const sh = vHeight * cropFactor; const sx = (vWidth - sw) / 2; const sy = (vHeight - sh) / 2; canvas.width = 1200; canvas.height = (sh / sw) * 1200; console.log(`OCR Capture: ${vWidth}x${vHeight} -> ${canvas.width}x${canvas.height}`); const ctx = canvas.getContext('2d'); if (ctx) { // More balanced filter for both paper and metal labels ctx.filter = 'grayscale(100%) contrast(180%) brightness(105%)'; ctx.drawImage(video, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height); } // Timeout protection for worker initialization (especially when offline for first run) const workerPromise = createWorker('eng'); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("OCR Engine timeout - check internet for first run")), 8000)); const worker = await Promise.race([workerPromise, timeoutPromise]) as any; const dataUrl = canvas.toDataURL('image/jpeg', 0.85); // DIAGNOSTIC: Set captured image early so user can see what was sent to AI setCapturedImage(dataUrl); const result = await worker.recognize(dataUrl); console.log("OCR Result Size:", result.data?.text?.length); const data = result.data; // FALLBACK: If we have text but no word coordinates if (data && data.text && (!data.words || data.words.length === 0)) { console.log("Fallback to direct match"); onOCRMatch(data.text); await worker.terminate(); setCapturedImage(null); return; } if (!data || !data.words || data.words.length === 0) { console.warn("OCR produced no text or words."); toast.error("Label not readable. Try better light or zoom."); await worker.terminate(); setCapturedImage(null); return; } // Store words with their bounding boxes const words = data.words.map((w: any) => ({ text: w.text, bbox: w.bbox })); setDetectedWords(words); setIsSelecting(true); await worker.terminate(); } catch (err) { console.error("OCR failed", err); toast.error("OCR selection failed"); } finally { setOcrProcessing(false); } }; const handleWordSelect = (text: string) => { onOCRMatch(text); // Reset selection state setIsSelecting(false); setCapturedImage(null); setDetectedWords([]); }; const cn = (...classes: any[]) => classes.filter(Boolean).join(' '); return (
{/* Selection UI */} {isSelecting && capturedImage && (
{/* Render Bounding Boxes */}
{detectedWords.map((w, i) => (

Tap the correct text on the label

)} {isOCRMode && (
)} {!isStarted && !error && (

Initializing camera...

)} {error && (

Camera Error

{error}

)}
{hasZoom && ( )} {isOCRMode ? ( ) : (

Center barcode for auto-scan

)}
); }