Enhance file upload functionality with document scanning support and improved error handling

This commit is contained in:
Mo Tarbin
2026-08-09 12:00:30 -04:00
parent 731c46b26a
commit 841f83d6ae
6 changed files with 426 additions and 181 deletions

View File

@@ -8,11 +8,13 @@ import {
import {
Box,
Button,
Checkbox,
CircularProgress,
LinearProgress,
Typography,
} from '@mui/joy'
import { useCallback, useEffect, useMemo } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useScanToTask } from './useScanToTask'
/**
@@ -27,34 +29,39 @@ import { useScanToTask } from './useScanToTask'
* belongs to the capture surface and drives a hidden input in this subtree.
*/
const ScanPanel = ({
open,
onTaskExtracted,
autoCapture,
canKeepImage = false,
initialImageUrl,
onClose,
onStateChange,
initialImageUrl,
autoCapture,
onTaskExtracted,
open,
}) => {
const {
isNativeScanner,
phase,
capturedImage,
ocrProgress,
taskResult,
errorMsg,
activate,
cameraAvailable,
videoRef,
canvasRef,
fileInputRef,
startCamera,
stopCamera,
capture,
capturedImage,
errorMsg,
fileInputRef,
handleFileSelect,
handleNativeScan,
retake,
activate,
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) {
@@ -82,7 +89,10 @@ const ScanPanel = ({
// Auto-close and populate when done
useEffect(() => {
if (phase === 'done' && taskResult) {
onTaskExtracted(taskResult)
onTaskExtracted({
...taskResult,
attachmentImage: canKeepImage && keepImage ? capturedImage : null,
})
onClose()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -137,6 +147,18 @@ const ScanPanel = ({
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 : (
<Checkbox
size='sm'
checked={keepImage}
onChange={e => setKeepImage(e.target.checked)}
label='Keep photo as attachment'
sx={{ '--Checkbox-size': '18px' }}
/>
)
return (
<Box>
{/* ── Capture phase ── */}
@@ -201,16 +223,18 @@ const ScanPanel = ({
)}
</Box>
{/* Hidden when Upload is already the footer's primary action */}
{(isNativeScanner || cameraAvailable) && (
<Box
sx={{
py: 1,
display: 'flex',
alignItems: 'center',
gap: 1,
}}
>
<Box
sx={{
py: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: 1,
}}
>
{/* Hidden when Upload is already the footer's primary action */}
{(isNativeScanner || cameraAvailable) && (
<Button
size='sm'
variant='plain'
@@ -220,8 +244,9 @@ const ScanPanel = ({
>
Upload
</Button>
</Box>
)}
)}
{keepImageToggle}
</Box>
</>
)}
@@ -285,6 +310,9 @@ const ScanPanel = ({
sx={{ width: '100%' }}
/>
)}
{/* Still editable here — the choice is only read once the task lands */}
{keepImageToggle}
</Box>
)}