diff --git a/docs/user-guide.md b/docs/user-guide.md index 3ddb25f..d076b78 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -20,7 +20,7 @@ The sign-in image is a current capture from the deployed page. The dashboard, hi *Upload a receipt* -Upload a JPEG, PNG or bounded PDF through the web workspace. Telegram receipts reach the same authenticated backend through the OpenClaw relay. +Upload a JPEG, PNG or bounded PDF through the web workspace. The selected file appears beside the form, and its preview remains visible after processing. Select **Open processed receipt** to inspect extracted fields and any review reasons, or **Upload another** to clear the form for the next file. This local preview is a convenience; the saved receipt detail shows the retained original evidence. Telegram receipts reach the same authenticated backend through the OpenClaw relay. ![Pending reviews](assets/screenshots/2026-09-23/03-pending-reviews.jpg) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6fdeaa2..8a94d98 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -955,6 +955,8 @@ function UploadForm({ open: (id: string) => void; }) { const [file, setFile] = useState(null), + [previewUrl, setPreviewUrl] = useState(""), + [uploadedId, setUploadedId] = useState(null), [selectedPurpose, setSelectedPurpose] = useState(""), [otherPurpose, setOtherPurpose] = useState(""), [busy, setBusy] = useState(false), @@ -968,9 +970,14 @@ function UploadForm({ active.current = false; }; }, []); + useEffect(() => { + return () => { + if (previewUrl) URL.revokeObjectURL(previewUrl); + }; + }, [previewUrl]); async function submit(e: React.FormEvent) { e.preventDefault(); - if (submitting.current) return; + if (submitting.current || uploadedId) return; setError(""); setFailedId(null); if ( @@ -998,7 +1005,7 @@ function UploadForm({ token, { method: "POST", body, timeoutMs: 330000 }, ); - if (active.current) open(result.receipt_id); + if (active.current) setUploadedId(result.receipt_id); } catch (err) { if (!active.current) return; setError( @@ -1012,19 +1019,29 @@ function UploadForm({ } } return ( -
+
+
setFile(e.target.files?.[0] || null)} + disabled={busy || !!uploadedId} + onChange={(e) => { + const chosen = e.target.files?.[0] || null; + setFile(chosen); + setPreviewUrl( + chosen && ["image/jpeg", "image/png", "application/pdf"].includes(chosen.type) + ? URL.createObjectURL(chosen) + : "", + ); + }} />

JPEG, PNG, or PDF · Up to 5 MB · PDF up to 3 pages · One receipt per @@ -1053,7 +1070,7 @@ function UploadForm({ name="business-purpose" className="sr-only" required - disabled={busy} + disabled={busy || !!uploadedId} checked={selected} onChange={() => { setSelectedPurpose(value); @@ -1076,7 +1093,7 @@ function UploadForm({ minLength={3} maxLength={500} value={otherPurpose} - disabled={busy} + disabled={busy || !!uploadedId} onChange={(event) => setOtherPurpose(event.target.value)} placeholder="For example: cleaning supplies for the office pantry" /> @@ -1093,21 +1110,77 @@ function UploadForm({ )} )} - + {uploadedId && ( +

+

Receipt uploaded. Check the extracted fields and routing decision in the saved record.

+
+ + +
+
+ )} + {!uploadedId && ( + + )}

OCR runs on the server. Extraction and classification may use gateway credits. Keep this page open while processing.

- + +