import { CameraAlt, DocumentScanner, PhotoCamera, Replay, WarningAmber, } from '@mui/icons-material' import { Box, Button, Checkbox, CircularProgress, LinearProgress, Typography, } from '@mui/joy' import { useCallback, useEffect, useMemo, useState } from 'react' import { useScanToTask } from './useScanToTask' /** * Inline scan-to-task panel. Mounts inside AddTaskModal — no second modal. * * Flow: capture → (auto) processing → done [calls onTaskExtracted + onClose] * → error [retake or cancel] * * The primary action (Capture / Scan Document / Retake) lives in the modal * footer alongside Cancel — the panel reports it up through onStateChange * rather than rendering its own button row. Upload stays inline because it * belongs to the capture surface and drives a hidden input in this subtree. */ const ScanPanel = ({ autoCapture, canKeepImage = false, initialImageUrl, onClose, onStateChange, onTaskExtracted, open, }) => { const { activate, cameraAvailable, canvasRef, capture, capturedImage, errorMsg, fileInputRef, handleFileSelect, handleNativeScan, isNativeScanner, ocrProgress, phase, reset, retake, startCamera, stopCamera, taskResult, videoRef, } = useScanToTask() // The scanned page is usually the task's source of truth (the bill, the // notice), so keeping it is the default — the OCR text alone loses it. const [keepImage, setKeepImage] = useState(false) // Start/stop based on open state useEffect(() => { if (open) { activate(initialImageUrl) if (autoCapture && isNativeScanner && !initialImageUrl) { handleNativeScan() } } else { reset() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, initialImageUrl]) // Start camera when entering capture phase on web useEffect(() => { if (phase === 'capture' && !isNativeScanner) { startCamera() } if (phase !== 'capture') { stopCamera() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [phase, isNativeScanner]) // Auto-close and populate when done useEffect(() => { if (phase === 'done' && taskResult) { onTaskExtracted({ ...taskResult, attachmentImage: canKeepImage && keepImage ? capturedImage : null, }) onClose() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [phase, taskResult]) const openFilePicker = useCallback( () => fileInputRef.current?.click(), [fileInputRef], ) // The one action the footer renders for the current phase; null while // processing (nothing to do but wait) and when done (the panel closes) const primaryAction = useMemo(() => { if (phase === 'capture') { if (isNativeScanner) { return { label: 'Scan Document', icon: , onClick: handleNativeScan, } } if (cameraAvailable) { return { label: 'Capture', icon: , onClick: capture } } // No camera on this device — Upload is the only way forward, so it // graduates from the inline secondary to the footer's primary return { label: 'Upload Photo', icon: , onClick: openFilePicker, } } if (phase === 'error') { return { label: 'Retake', icon: , onClick: retake } } return null }, [ phase, isNativeScanner, cameraAvailable, capture, handleNativeScan, retake, openFilePicker, ]) useEffect(() => { onStateChange?.({ phase, primaryAction }) }, [phase, primaryAction, onStateChange]) if (!open) return null const isProcessing = phase === 'processing' // Attachments are a Plus feature; without it the upload would only ever // surface an upgrade error, so the choice isn't offered at all. const keepImageToggle = !canKeepImage ? null : ( setKeepImage(e.target.checked)} label='Keep photo as attachment' sx={{ '--Checkbox-size': '18px' }} /> ) return ( {/* ── Capture phase ── */} {phase === 'capture' && ( <> {isNativeScanner && ( Tap "Scan Document" to open the scanner )} {!isNativeScanner && cameraAvailable && ( {/* Hidden when Upload is already the footer's primary action */} {(isNativeScanner || cameraAvailable) && ( )} {keepImageToggle} )} {/* ── Processing phase ── */} {isProcessing && ( {capturedImage && ( Processing )} {ocrProgress > 0 && ocrProgress < 100 ? `Reading text… ${ocrProgress}%` : ocrProgress >= 100 ? 'Identifying task with AI…' : 'Starting…'} {ocrProgress > 0 && ocrProgress < 100 && ( )} {/* Still editable here — the choice is only read once the task lands */} {keepImageToggle} )} {/* ── Error phase ── */} {phase === 'error' && ( {capturedImage && ( Failed scan )} {errorMsg} )} {/* Kept outside the phase branches so the footer's Upload action can reach it even when no capture surface is rendered */} ) } export default ScanPanel