zurich_kotak/src/components/FileField.jsx
Yashas e32916cad3 Read the extract response correctly, and fail visibly
/ocr-extract answers { extracted: {...}, raw: "..." }. The client checked
`fields` first and `extracted` second, which was right by luck — but an
extraction that returned nothing, or threw, did so silently: the button
went back to idle and the operator had no way to tell a read from a
no-read.

Now an empty or failed extraction says so under the field, and either
way the UPLOAD survives. The file is already stored and referenced; a
failed read only means the fields are not pre-filled, which someone can
recover by typing. Losing the upload because the read failed would not
be recoverable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 15:23:35 +05:30

94 lines
3.5 KiB
JavaScript

import { useRef, useState } from 'react'
import { useZino } from '../api/provider.jsx'
/**
* A file or OCR field.
*
* Uploads ON PICK rather than on submit: the backend's extract endpoint takes
* a REFERENCE to a stored file, so the document crosses the wire once, survives
* a reload, and re-extracting costs no second upload.
*
* For an `ocr` field it then calls /ocr-extract, which resolves the field's own
* extraction config server-side and returns the values keyed by extraction key.
* Those are handed up so the form can fill the fields the document answers —
* which is the entire point: nobody should be typing an engine capacity that is
* printed on the RC.
*/
export default function FileField({ field, value, instanceId, activityUid, onChange, onExtract }) {
const { client } = useZino()
const inputRef = useRef(null)
const [busy, setBusy] = useState('')
const [err, setErr] = useState(null)
const [extracted, setExtracted] = useState(null)
const isOcr = field.data_type === 'ocr'
const files = Array.isArray(value) ? value : []
async function pick(e) {
const file = e.target.files?.[0]
if (!file) return
setErr(null); setExtracted(null)
const ctx = { activityId: activityUid, fieldId: field.id, instanceId }
try {
setBusy('Uploading…')
const ref = await client.uploadFile(file, ctx)
onChange([ref])
if (isOcr) {
setBusy('Reading the document…')
// Extraction failing must NOT lose the upload. The file is already
// stored and referenced; a failed read just means the fields are not
// pre-filled, which is recoverable by typing. Swallowing the upload
// because the OCR errored would not be.
try {
const out = await client.ocrExtract(ref, ctx)
// The endpoint answers { extracted: {...}, raw: "..." }.
const fields = out?.extracted ?? out?.fields ?? out?.data ?? null
if (fields && typeof fields === 'object' && Object.keys(fields).length) {
setExtracted(fields)
onExtract?.(fields)
} else {
setErr({ status: '', message: 'Uploaded, but nothing could be read from this document.' })
}
} catch (ox) {
setErr({ status: ox.status ?? '', message: 'Uploaded, but reading it failed — ' + (ox.message || 'unknown error') })
}
}
} catch (ex) {
setErr(ex)
} finally {
setBusy('')
}
}
return (
<div className="ff">
<input
ref={inputRef} type="file" className="ff__input"
accept=".pdf,.png,.jpg,.jpeg" onChange={pick} disabled={Boolean(busy)}
/>
<button type="button" className="ff__btn" disabled={Boolean(busy)}
onClick={() => inputRef.current?.click()}>
{busy || (files.length ? 'Replace file' : isOcr ? 'Upload — we read it for you' : 'Upload')}
</button>
{files.length ? (
<div className="ff__got">{files[0].original_name || files[0].uuid}</div>
) : null}
{extracted ? (
<div className="ff__read">
<span className="ff__readlabel">Read from the document</span>
{Object.entries(extracted)
.filter(([, v]) => v !== null && v !== undefined && String(v) !== '')
.map(([k, v]) => (
<div key={k}><em>{k.replace(/_/g, ' ')}</em> {String(v)}</div>
))}
</div>
) : null}
{err ? <div className="ff__err">{err.status} {err.message}</div> : null}
</div>
)
}