Add PDF/HEIC/image upload support and improve receipt extraction
- Add backend image conversion endpoint (POST /api/convert-image) supporting PDF, HEIC, PNG, WebP via Pillow, PyMuPDF, and pillow-heif - Add separate "Upload file" button in UI while keeping camera-first behavior for the photo area and + button - Improve Haiku extraction prompt for hotel receipts (parenthesized total) - Increase max image resolution from 1024px to 2048px for better OCR accuracy - Add libheif-dev system dependency in Dockerfile Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -542,6 +542,8 @@
|
||||
<span>Tap to take photo</span>
|
||||
</div>
|
||||
<input type="file" id="photo-input" accept="image/*" capture="environment" hidden>
|
||||
<input type="file" id="file-input" accept="image/*,.pdf,.heic,.heif,application/pdf" hidden>
|
||||
<button type="button" id="upload-file-btn" style="margin-top:8px;width:100%;padding:10px;border:1.5px dashed #aaa;border-radius:8px;background:#fafafa;color:#666;font-size:0.9rem;cursor:pointer;">Upload file (PDF, HEIC, image)</button>
|
||||
<div class="extract-status" id="extract-status"></div>
|
||||
</div>
|
||||
|
||||
@@ -695,6 +697,8 @@
|
||||
const modalTitle = document.getElementById("modal-title");
|
||||
const photoArea = document.getElementById("photo-area");
|
||||
const photoInput = document.getElementById("photo-input");
|
||||
const fileInput = document.getElementById("file-input");
|
||||
const uploadFileBtn = document.getElementById("upload-file-btn");
|
||||
const dateInput = document.getElementById("date-input");
|
||||
const amountInput = document.getElementById("amount-input");
|
||||
const categoryInput = document.getElementById("category-input");
|
||||
@@ -866,6 +870,7 @@
|
||||
function closeModal() {
|
||||
overlay.classList.remove("open");
|
||||
photoInput.value = "";
|
||||
fileInput.value = "";
|
||||
}
|
||||
|
||||
function updatePhotoArea() {
|
||||
@@ -876,7 +881,7 @@
|
||||
photoArea.classList.remove("has-photo");
|
||||
photoArea.innerHTML = `
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"/><circle cx="12" cy="13" r="4"/></svg>
|
||||
<span>Tap to take photo</span>`;
|
||||
<span>Tap to add photo or file</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1055,6 +1060,7 @@
|
||||
});
|
||||
|
||||
photoArea.addEventListener("click", () => photoInput.click());
|
||||
uploadFileBtn.addEventListener("click", () => fileInput.click());
|
||||
|
||||
// --- Receipt extraction via Claude ---
|
||||
const extractStatus = document.getElementById("extract-status");
|
||||
@@ -1290,11 +1296,50 @@
|
||||
}, "image/jpeg", 0.85);
|
||||
});
|
||||
|
||||
async function convertViaBackend(file) {
|
||||
const buf = await file.arrayBuffer();
|
||||
const r = await fetch("/api/convert-image", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": file.type || "application/octet-stream" },
|
||||
body: buf
|
||||
});
|
||||
if (!r.ok) throw new Error("Conversion failed");
|
||||
return await r.blob();
|
||||
}
|
||||
|
||||
function needsBackendConversion(file) {
|
||||
const type = (file.type || "").toLowerCase();
|
||||
const name = (file.name || "").toLowerCase();
|
||||
if (type === "application/pdf" || name.endsWith(".pdf")) return true;
|
||||
if (type === "image/heic" || type === "image/heif" || name.endsWith(".heic") || name.endsWith(".heif")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
photoInput.addEventListener("change", async e => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const resized = await resizeImage(file, 1024);
|
||||
openCropOverlay(resized);
|
||||
const blob = await resizeImage(file, 2048);
|
||||
openCropOverlay(blob);
|
||||
});
|
||||
|
||||
fileInput.addEventListener("change", async e => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
let blob;
|
||||
if (needsBackendConversion(file)) {
|
||||
try {
|
||||
extractStatus.textContent = "Converting file...";
|
||||
blob = await convertViaBackend(file);
|
||||
} catch (err) {
|
||||
extractStatus.textContent = "Conversion failed";
|
||||
return;
|
||||
} finally {
|
||||
if (extractStatus.textContent === "Converting file...") extractStatus.textContent = "";
|
||||
}
|
||||
} else {
|
||||
blob = await resizeImage(file, 2048);
|
||||
}
|
||||
openCropOverlay(blob);
|
||||
});
|
||||
|
||||
btnSave.addEventListener("click", async () => {
|
||||
|
||||
Reference in New Issue
Block a user