From 4e8b586201ad05ab14806136afea9910880a8f89 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Mon, 6 Apr 2026 09:23:41 +0000 Subject: [PATCH] feat: AI photo identification from product form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When creating a new product (manual entry), a 'πŸ“· Scatta foto e identifica con AI' button appears at the top of the form. Tapping it: 1. Opens a camera modal (same pattern as expiry scanner) 2. User takes photo of product/label 3. Sends to gemini_identify β€” returns name, brand, category + OpenFoodFacts matches 4. User can pick a specific OFF match (fills barcode + full details via lookup_barcode) or tap 'Usa dati AI' to fill just name/brand/category from Gemini 5. All matching fields are auto-filled: name, brand, category, barcode, image, unit/qty 6. Button hidden when editing an existing product (not needed) --- assets/js/app.js | 198 +++++++++++++++++++++++++++++++++++++++++++++++ index.html | 6 ++ 2 files changed, 204 insertions(+) diff --git a/assets/js/app.js b/assets/js/app.js index 1b7b015..8acf872 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -2423,6 +2423,8 @@ function startManualEntry(barcode = '') { document.getElementById('pf-image').value = ''; document.getElementById('pf-image-preview').style.display = 'none'; document.getElementById('product-form-title').textContent = 'Nuovo Prodotto'; + const pfAiRow = document.getElementById('pf-ai-fill-row'); + if (pfAiRow) pfAiRow.style.display = 'block'; // Remove datalist/autocomplete suggestions for new products (they cause confusion) document.getElementById('pf-name').removeAttribute('list'); @@ -2866,6 +2868,8 @@ function editProductFromAction() { document.getElementById('pf-unit').value = currentProduct.unit || 'pz'; document.getElementById('pf-defqty').value = currentProduct.default_quantity || 1; document.getElementById('product-form-title').textContent = 'Modifica Prodotto'; + const pfAiRow = document.getElementById('pf-ai-fill-row'); + if (pfAiRow) pfAiRow.style.display = 'none'; // Restore datalist for editing (was removed for new products) document.getElementById('pf-name').setAttribute('list', 'common-products'); @@ -4351,6 +4355,200 @@ async function saveAIProductDirect() { } } +// ===== AI PHOTO FILL FOR PRODUCT FORM ===== +let _pfAiStream = null; + +async function captureForAIFormFill() { + document.getElementById('modal-content').innerHTML = ` + +
+
+ + +
+
+ + + +

Inquadra l'etichetta del prodotto

+
+ + +
+
+ `; + document.getElementById('modal-overlay').style.display = 'flex'; + + try { + _pfAiStream = await navigator.mediaDevices.getUserMedia(getCameraConstraints()); + const video = document.getElementById('pfai-video'); + video.srcObject = _pfAiStream; + await video.play(); + } catch (err) { + document.getElementById('pfai-cam-container').innerHTML = + `

⚠️ Impossibile accedere alla fotocamera

`; + } +} + +function closePfAiScanner() { + if (_pfAiStream) { _pfAiStream.getTracks().forEach(t => t.stop()); _pfAiStream = null; } + closeModal(); +} + +function pfAiCapture() { + const video = document.getElementById('pfai-video'); + const canvas = document.getElementById('pfai-canvas'); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + canvas.getContext('2d').drawImage(video, 0, 0); + const dataUrl = canvas.toDataURL('image/jpeg', 0.85); + document.getElementById('pfai-preview-img').src = dataUrl; + + if (_pfAiStream) { _pfAiStream.getTracks().forEach(t => t.stop()); _pfAiStream = null; } + video.srcObject = null; + + document.getElementById('pfai-cam-container').style.display = 'none'; + document.getElementById('pfai-preview-container').style.display = 'block'; + document.getElementById('pfai-capture-btn').style.display = 'none'; + document.getElementById('pfai-retake-btn').style.display = 'inline-flex'; + document.getElementById('pfai-hint').style.display = 'none'; + + _pfAiAnalyze(canvas.toDataURL('image/jpeg', 0.7).split(',')[1]); +} + +function pfAiRetake() { + document.getElementById('pfai-cam-container').style.display = 'block'; + document.getElementById('pfai-preview-container').style.display = 'none'; + document.getElementById('pfai-capture-btn').style.display = 'inline-flex'; + document.getElementById('pfai-retake-btn').style.display = 'none'; + document.getElementById('pfai-status').style.display = 'none'; + document.getElementById('pfai-result').style.display = 'none'; + document.getElementById('pfai-hint').style.display = 'block'; + + navigator.mediaDevices.getUserMedia(getCameraConstraints()).then(stream => { + _pfAiStream = stream; + const video = document.getElementById('pfai-video'); + video.srcObject = stream; + video.play(); + }); +} + +async function _pfAiAnalyze(base64) { + const statusEl = document.getElementById('pfai-status'); + const resultEl = document.getElementById('pfai-result'); + statusEl.style.display = 'block'; + resultEl.style.display = 'none'; + + try { + const result = await api('gemini_identify', {}, 'POST', { image: base64 }); + + statusEl.style.display = 'none'; + resultEl.style.display = 'block'; + + if (!result.success) { + resultEl.innerHTML = `

❌ ${escapeHtml(result.error || 'Errore identificazione')}

+ `; + return; + } + + const id = result.identified; + const matches = result.off_matches || []; + + let html = `
+ ${escapeHtml(id.name)}`; + if (id.brand) html += ` β€” ${escapeHtml(id.brand)}`; + if (id.description) html += `

${escapeHtml(id.description)}

`; + html += `
`; + + if (matches.length > 0) { + html += `

Seleziona la variante esatta o usa i dati AI:

`; + html += `
`; + matches.forEach((m, idx) => { + html += `
`; + if (m.image_url) html += ``; + html += `
${escapeHtml(m.name)}`; + if (m.brand) html += `
${escapeHtml(m.brand)}`; + if (m.quantity_info) html += `
${escapeHtml(m.quantity_info)}`; + html += `
${escapeHtml(m.barcode)}
`; + }); + html += `
`; + } + + html += ``; + resultEl.innerHTML = html; + + window._pfAiIdentified = id; + window._pfAiMatches = matches; + + } catch (err) { + statusEl.style.display = 'none'; + resultEl.style.display = 'block'; + resultEl.innerHTML = `

❌ Errore di connessione

+ `; + } +} + +function _pfAiFillFields(name, brand, category, barcode, imageUrl, quantityInfo) { + if (name) document.getElementById('pf-name').value = name; + if (brand) document.getElementById('pf-brand').value = brand; + if (category) { + const cat = mapToLocalCategory(category, name || ''); + document.getElementById('pf-category').value = cat; + document.getElementById('pf-category').dataset.manuallySet = 'true'; + onCategoryChange(true); + } + if (barcode) document.getElementById('pf-barcode').value = barcode; + if (imageUrl) { + document.getElementById('pf-image').value = imageUrl; + const preview = document.getElementById('pf-image-preview'); + document.getElementById('pf-image-img').src = imageUrl; + preview.style.display = 'block'; + } + if (quantityInfo) { + const detected = detectUnitAndQuantity(quantityInfo); + document.getElementById('pf-unit').value = detected.unit; + document.getElementById('pf-defqty').value = detected.quantity; + document.getElementById('pf-defqty').dataset.manuallySet = 'true'; + onPfUnitChange(); + } + // Trigger auto-detect for remaining empty fields + if (name && !category) autoDetectCategory(); + closePfAiScanner(); + showToast('βœ… Campi compilati dall\'AI', 'success'); +} + +function _pfAiFillFromAI() { + const id = window._pfAiIdentified; + if (!id) return; + _pfAiFillFields(id.name, id.brand, id.category, '', '', ''); +} + +async function _pfAiFillFromMatch(idx) { + const match = window._pfAiMatches[idx]; + if (!match) return; + closePfAiScanner(); + showLoading(true); + try { + const lookupResult = await api('lookup_barcode', { barcode: match.barcode }); + if (lookupResult.found && lookupResult.product) { + const p = lookupResult.product; + _pfAiFillFields(p.name || match.name, p.brand || match.brand, p.category || '', match.barcode, p.image_url || match.image_url, p.quantity_info || ''); + showLoading(false); + return; + } + } catch (e) {} + showLoading(false); + _pfAiFillFields(match.name, match.brand, match.category, match.barcode, match.image_url, ''); +} + // ===== ALL PRODUCTS ===== async function loadAllProducts() { try { diff --git a/index.html b/index.html index 1271893..e486d80 100644 --- a/index.html +++ b/index.html @@ -306,6 +306,12 @@
+
+ +

L'AI compilerΓ  automaticamente i campi del prodotto

+