import {
CameraAlt,
DocumentScanner,
PhotoCamera,
Replay,
WarningAmber,
} from '@mui/icons-material'
import {
Box,
Button,
CircularProgress,
LinearProgress,
Typography,
} from '@mui/joy'
import { useCallback, useEffect, useMemo } 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 = ({
open,
onTaskExtracted,
onClose,
onStateChange,
initialImageUrl,
autoCapture,
}) => {
const {
isNativeScanner,
phase,
capturedImage,
ocrProgress,
taskResult,
errorMsg,
cameraAvailable,
videoRef,
canvasRef,
fileInputRef,
startCamera,
stopCamera,
capture,
handleFileSelect,
handleNativeScan,
retake,
activate,
reset,
} = useScanToTask()
// 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)
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'
return (
{/* ── Capture phase ── */}
{phase === 'capture' && (
<>
{isNativeScanner && (
Tap "Scan Document" to open the scanner
)}
{!isNativeScanner && cameraAvailable && (
)}
{!isNativeScanner && !cameraAvailable && (
Camera not available — use Upload instead
)}
{/* Hidden when Upload is already the footer's primary action */}
{(isNativeScanner || cameraAvailable) && (
}
onClick={openFilePicker}
>
Upload
)}
>
)}
{/* ── Processing phase ── */}
{isProcessing && (
{capturedImage && (
)}
{ocrProgress > 0 && ocrProgress < 100
? `Reading text… ${ocrProgress}%`
: ocrProgress >= 100
? 'Identifying task with AI…'
: 'Starting…'}
{ocrProgress > 0 && ocrProgress < 100 && (
)}
)}
{/* ── Error phase ── */}
{phase === 'error' && (
{capturedImage && (
)}
{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