diff --git a/apps/mobile/src/app/barcode.tsx b/apps/mobile/src/app/barcode.tsx index 92c0d87..0b6cfb1 100644 --- a/apps/mobile/src/app/barcode.tsx +++ b/apps/mobile/src/app/barcode.tsx @@ -29,6 +29,8 @@ type Nutrition = { carbsG: number; fatG: number; saturatedFatG: number; + monounsaturatedFatG?: number; + polyunsaturatedFatG?: number; fiberG: number; sugarG: number; saltG: number; @@ -87,6 +89,7 @@ export default function BarcodeScreen() { const [product, setProduct] = useState(null); const [unknown, setUnknown] = useState(false); const [contributing, setContributing] = useState(false); + const [justContributed, setJustContributed] = useState(false); const [errorMsg, setErrorMsg] = useState(null); // Synkron lås: hindrar att kamerans upprepade träffar startar parallella uppslag. const locked = useRef(false); @@ -101,6 +104,7 @@ export default function BarcodeScreen() { lastGtin.current = gtin; setBusy(true); setErrorMsg(null); + setJustContributed(false); try { const result = await api("/v1/scans", { method: "POST", @@ -177,6 +181,7 @@ export default function BarcodeScreen() { const found = await pollScan(created.scan.id); if (found) { setUnknown(false); + setJustContributed(true); setProduct(found); } else { // Behåll nuvarande vy (okänd/produkt) och visa felet som dialog. @@ -197,6 +202,7 @@ export default function BarcodeScreen() { setProduct(null); setUnknown(false); setErrorMsg(null); + setJustContributed(false); locked.current = false; lastGtin.current = null; }; @@ -248,6 +254,11 @@ export default function BarcodeScreen() { {product && !contributing && ( + {justContributed && ( + + {t("barcode.contributing.saved")} + + )} {product.name} {(product.brand || product.packageSizeValue) && ( @@ -269,6 +280,12 @@ export default function BarcodeScreen() { + {n.monounsaturatedFatG != null && ( + + )} + {n.polyunsaturatedFatG != null && ( + + )} @@ -426,4 +443,12 @@ const styles = StyleSheet.create({ muted: { color: colors.textMuted }, strong: { fontWeight: "700" }, source: { color: colors.textMuted, fontStyle: "italic" }, + savedBanner: { + backgroundColor: colors.primarySoft, + borderRadius: 10, + paddingVertical: spacing.xs, + paddingHorizontal: spacing.sm, + marginBottom: spacing.sm, + }, + savedText: { color: colors.primary, fontWeight: "700" }, }); diff --git a/apps/mobile/src/locales/da/common.json b/apps/mobile/src/locales/da/common.json index e185d8a..0ffdb4b 100644 --- a/apps/mobile/src/locales/da/common.json +++ b/apps/mobile/src/locales/da/common.json @@ -33,6 +33,7 @@ "barcode.contributing.title": "Aflæser emballagen…", "barcode.contributing.body": "Vi finder næring, allergener og indhold – og gemmer produktet i det delte katalog, så den næste slipper for at fotografere det.", "barcode.contributing.failed": "Kunne ikke aflæse emballagen. Prøv igen med et skarpt billede og godt lys.", + "barcode.contributing.saved": "✓ Gemt i det delte katalog – tak fordi du bidrager!", "barcode.contributing.frontDone": "Forside taget", "barcode.contributing.backPrompt": "Tag nu et billede af bagsiden med næringsdeklaration og indhold – så bliver produktet komplet.", "barcode.contributing.skipBack": "Færdig, analysér", @@ -463,6 +464,8 @@ "barcode.info.sugar": "– heraf sukkerarter", "barcode.info.fat": "Fedt", "barcode.info.satfat": "– heraf mættede", + "barcode.info.monofat": "– heraf enkeltumættede", + "barcode.info.polyfat": "– heraf flerumættede", "barcode.info.fiber": "Fibre", "barcode.info.salt": "Salt", "barcode.info.allergens": "Allergener", diff --git a/apps/mobile/src/locales/de/common.json b/apps/mobile/src/locales/de/common.json index bd1827e..db0cb63 100644 --- a/apps/mobile/src/locales/de/common.json +++ b/apps/mobile/src/locales/de/common.json @@ -33,6 +33,7 @@ "barcode.contributing.title": "Verpackung wird gelesen…", "barcode.contributing.body": "Wir ermitteln Nährwerte, Allergene und Zutaten – und speichern das Produkt im gemeinsamen Katalog, damit es der Nächste nicht fotografieren muss.", "barcode.contributing.failed": "Die Verpackung konnte nicht gelesen werden. Versuche es mit einem scharfen Foto und gutem Licht erneut.", + "barcode.contributing.saved": "✓ Im gemeinsamen Katalog gespeichert – danke fürs Beitragen!", "barcode.contributing.frontDone": "Vorderseite aufgenommen", "barcode.contributing.backPrompt": "Fotografiere jetzt die Rückseite mit Nährwerttabelle und Zutaten – so wird das Produkt vollständig.", "barcode.contributing.skipBack": "Fertig, analysieren", @@ -463,6 +464,8 @@ "barcode.info.sugar": "– davon Zucker", "barcode.info.fat": "Fett", "barcode.info.satfat": "– davon gesättigte", + "barcode.info.monofat": "– davon einfach ungesättigt", + "barcode.info.polyfat": "– davon mehrfach ungesättigt", "barcode.info.fiber": "Ballaststoffe", "barcode.info.salt": "Salz", "barcode.info.allergens": "Allergene", diff --git a/apps/mobile/src/locales/en/common.json b/apps/mobile/src/locales/en/common.json index 37fda44..d3f112c 100644 --- a/apps/mobile/src/locales/en/common.json +++ b/apps/mobile/src/locales/en/common.json @@ -33,6 +33,7 @@ "barcode.contributing.title": "Reading the package…", "barcode.contributing.body": "We're extracting nutrition, allergens and ingredients – and saving the product to the shared catalogue so the next person doesn't have to photograph it.", "barcode.contributing.failed": "Couldn't read the package. Try again with a sharp photo and good lighting.", + "barcode.contributing.saved": "✓ Saved to the shared catalogue – thanks for contributing!", "barcode.contributing.frontDone": "Front photo taken", "barcode.contributing.backPrompt": "Now photograph the back with the nutrition label and ingredients – that completes the product.", "barcode.contributing.skipBack": "Done, analyse", @@ -463,6 +464,8 @@ "barcode.info.sugar": "– of which sugars", "barcode.info.fat": "Fat", "barcode.info.satfat": "– of which saturates", + "barcode.info.monofat": "– of which monounsaturated", + "barcode.info.polyfat": "– of which polyunsaturated", "barcode.info.fiber": "Fibre", "barcode.info.salt": "Salt", "barcode.info.allergens": "Allergens", diff --git a/apps/mobile/src/locales/es/common.json b/apps/mobile/src/locales/es/common.json index 2c5c053..879d2ae 100644 --- a/apps/mobile/src/locales/es/common.json +++ b/apps/mobile/src/locales/es/common.json @@ -33,6 +33,7 @@ "barcode.contributing.title": "Leyendo el envase…", "barcode.contributing.body": "Estamos extrayendo la información nutricional, los alérgenos y los ingredientes, y guardando el producto en el catálogo compartido para que la próxima persona no tenga que fotografiarlo.", "barcode.contributing.failed": "No se pudo leer el envase. Inténtalo de nuevo con una foto nítida y buena luz.", + "barcode.contributing.saved": "✓ Guardado en el catálogo compartido: ¡gracias por contribuir!", "barcode.contributing.frontDone": "Foto del frente tomada", "barcode.contributing.backPrompt": "Ahora fotografía la parte de atrás con la información nutricional y los ingredientes: así el producto queda completo.", "barcode.contributing.skipBack": "Listo, analizar", @@ -463,6 +464,8 @@ "barcode.info.sugar": "– de los cuales azúcares", "barcode.info.fat": "Grasas", "barcode.info.satfat": "– de las cuales saturadas", + "barcode.info.monofat": "– de las cuales monoinsaturadas", + "barcode.info.polyfat": "– de las cuales poliinsaturadas", "barcode.info.fiber": "Fibra", "barcode.info.salt": "Sal", "barcode.info.allergens": "Alérgenos", diff --git a/apps/mobile/src/locales/fi/common.json b/apps/mobile/src/locales/fi/common.json index 78cf858..05ed464 100644 --- a/apps/mobile/src/locales/fi/common.json +++ b/apps/mobile/src/locales/fi/common.json @@ -33,6 +33,7 @@ "barcode.contributing.title": "Luetaan pakkausta…", "barcode.contributing.body": "Poimimme ravintoarvot, allergeenit ja ainesosat – ja tallennamme tuotteen jaettuun luetteloon, jottei seuraavan tarvitse kuvata sitä.", "barcode.contributing.failed": "Pakkausta ei voitu lukea. Yritä uudelleen terävällä kuvalla ja hyvässä valossa.", + "barcode.contributing.saved": "✓ Tallennettu jaettuun luetteloon – kiitos osallistumisesta!", "barcode.contributing.frontDone": "Etupuoli kuvattu", "barcode.contributing.backPrompt": "Kuvaa nyt takapuoli, jossa on ravintosisältö ja ainesosat – näin tuote täydentyy.", "barcode.contributing.skipBack": "Valmis, analysoi", @@ -463,6 +464,8 @@ "barcode.info.sugar": "– josta sokereita", "barcode.info.fat": "Rasva", "barcode.info.satfat": "– josta tyydyttynyttä", + "barcode.info.monofat": "– josta kertatyydyttymätöntä", + "barcode.info.polyfat": "– josta monityydyttymätöntä", "barcode.info.fiber": "Kuitu", "barcode.info.salt": "Suola", "barcode.info.allergens": "Allergeenit", diff --git a/apps/mobile/src/locales/fr/common.json b/apps/mobile/src/locales/fr/common.json index f8004bb..58a718d 100644 --- a/apps/mobile/src/locales/fr/common.json +++ b/apps/mobile/src/locales/fr/common.json @@ -33,6 +33,7 @@ "barcode.contributing.title": "Lecture de l'emballage…", "barcode.contributing.body": "Nous extrayons les valeurs nutritionnelles, les allergènes et les ingrédients, et enregistrons le produit dans le catalogue partagé pour que la prochaine personne n'ait pas à le photographier.", "barcode.contributing.failed": "Impossible de lire l'emballage. Réessayez avec une photo nette et un bon éclairage.", + "barcode.contributing.saved": "✓ Enregistré dans le catalogue partagé – merci de contribuer !", "barcode.contributing.frontDone": "Photo de l'avant prise", "barcode.contributing.backPrompt": "Photographiez maintenant l'arrière avec la déclaration nutritionnelle et les ingrédients – le produit sera ainsi complet.", "barcode.contributing.skipBack": "Terminé, analyser", @@ -463,6 +464,8 @@ "barcode.info.sugar": "– dont sucres", "barcode.info.fat": "Matières grasses", "barcode.info.satfat": "– dont saturées", + "barcode.info.monofat": "– dont monoinsaturés", + "barcode.info.polyfat": "– dont polyinsaturés", "barcode.info.fiber": "Fibres", "barcode.info.salt": "Sel", "barcode.info.allergens": "Allergènes", diff --git a/apps/mobile/src/locales/it/common.json b/apps/mobile/src/locales/it/common.json index 22a6ceb..0401649 100644 --- a/apps/mobile/src/locales/it/common.json +++ b/apps/mobile/src/locales/it/common.json @@ -33,6 +33,7 @@ "barcode.contributing.title": "Lettura della confezione…", "barcode.contributing.body": "Stiamo estraendo valori nutrizionali, allergeni e ingredienti e salvando il prodotto nel catalogo condiviso, così il prossimo non dovrà fotografarlo.", "barcode.contributing.failed": "Impossibile leggere la confezione. Riprova con una foto nitida e una buona illuminazione.", + "barcode.contributing.saved": "✓ Salvato nel catalogo condiviso – grazie per il contributo!", "barcode.contributing.frontDone": "Foto del fronte scattata", "barcode.contributing.backPrompt": "Ora fotografa il retro con la tabella nutrizionale e gli ingredienti: così il prodotto è completo.", "barcode.contributing.skipBack": "Fatto, analizza", @@ -463,6 +464,8 @@ "barcode.info.sugar": "– di cui zuccheri", "barcode.info.fat": "Grassi", "barcode.info.satfat": "– di cui saturi", + "barcode.info.monofat": "– di cui monoinsaturi", + "barcode.info.polyfat": "– di cui polinsaturi", "barcode.info.fiber": "Fibre", "barcode.info.salt": "Sale", "barcode.info.allergens": "Allergeni", diff --git a/apps/mobile/src/locales/nb/common.json b/apps/mobile/src/locales/nb/common.json index e0e8cf7..af8aad8 100644 --- a/apps/mobile/src/locales/nb/common.json +++ b/apps/mobile/src/locales/nb/common.json @@ -33,6 +33,7 @@ "barcode.contributing.title": "Leser av emballasjen…", "barcode.contributing.body": "Vi henter næring, allergener og innhold – og lagrer produktet i den delte katalogen, så den neste slipper å fotografere det.", "barcode.contributing.failed": "Kunne ikke lese av emballasjen. Prøv igjen med et skarpt bilde og godt lys.", + "barcode.contributing.saved": "✓ Lagret i den delte katalogen – takk for at du bidrar!", "barcode.contributing.frontDone": "Forside tatt", "barcode.contributing.backPrompt": "Ta nå bilde av baksiden med næringsinnhold og ingredienser – da blir produktet komplett.", "barcode.contributing.skipBack": "Ferdig, analyser", @@ -463,6 +464,8 @@ "barcode.info.sugar": "– hvorav sukkerarter", "barcode.info.fat": "Fett", "barcode.info.satfat": "– hvorav mettede", + "barcode.info.monofat": "– hvorav enumettede", + "barcode.info.polyfat": "– hvorav flerumettede", "barcode.info.fiber": "Fiber", "barcode.info.salt": "Salt", "barcode.info.allergens": "Allergener", diff --git a/apps/mobile/src/locales/nl/common.json b/apps/mobile/src/locales/nl/common.json index c5ac465..9516871 100644 --- a/apps/mobile/src/locales/nl/common.json +++ b/apps/mobile/src/locales/nl/common.json @@ -33,6 +33,7 @@ "barcode.contributing.title": "Verpakking wordt gelezen…", "barcode.contributing.body": "We halen voedingswaarde, allergenen en ingrediënten op – en slaan het product op in de gedeelde catalogus, zodat de volgende het niet hoeft te fotograferen.", "barcode.contributing.failed": "Kon de verpakking niet lezen. Probeer het opnieuw met een scherpe foto en goed licht.", + "barcode.contributing.saved": "✓ Opgeslagen in de gedeelde catalogus – bedankt voor je bijdrage!", "barcode.contributing.frontDone": "Voorkant gefotografeerd", "barcode.contributing.backPrompt": "Fotografeer nu de achterkant met de voedingswaarde en ingrediënten – dan is het product compleet.", "barcode.contributing.skipBack": "Klaar, analyseren", @@ -463,6 +464,8 @@ "barcode.info.sugar": "– waarvan suikers", "barcode.info.fat": "Vet", "barcode.info.satfat": "– waarvan verzadigd", + "barcode.info.monofat": "– waarvan enkelvoudig onverzadigd", + "barcode.info.polyfat": "– waarvan meervoudig onverzadigd", "barcode.info.fiber": "Vezels", "barcode.info.salt": "Zout", "barcode.info.allergens": "Allergenen", diff --git a/apps/mobile/src/locales/pl/common.json b/apps/mobile/src/locales/pl/common.json index be678cf..5be691f 100644 --- a/apps/mobile/src/locales/pl/common.json +++ b/apps/mobile/src/locales/pl/common.json @@ -33,6 +33,7 @@ "barcode.contributing.title": "Odczytywanie opakowania…", "barcode.contributing.body": "Odczytujemy wartości odżywcze, alergeny i składniki – i zapisujemy produkt we wspólnym katalogu, aby następna osoba nie musiała go fotografować.", "barcode.contributing.failed": "Nie udało się odczytać opakowania. Spróbuj ponownie z ostrym zdjęciem i dobrym oświetleniem.", + "barcode.contributing.saved": "✓ Zapisano we wspólnym katalogu – dziękujemy za wkład!", "barcode.contributing.frontDone": "Zrobiono zdjęcie przodu", "barcode.contributing.backPrompt": "Teraz sfotografuj tył z tabelą wartości odżywczych i składnikami – dzięki temu produkt będzie kompletny.", "barcode.contributing.skipBack": "Gotowe, analizuj", @@ -477,6 +478,8 @@ "barcode.info.sugar": "– w tym cukry", "barcode.info.fat": "Tłuszcz", "barcode.info.satfat": "– w tym nasycone", + "barcode.info.monofat": "– w tym jednonienasycone", + "barcode.info.polyfat": "– w tym wielonienasycone", "barcode.info.fiber": "Błonnik", "barcode.info.salt": "Sól", "barcode.info.allergens": "Alergeny", diff --git a/apps/mobile/src/locales/pt/common.json b/apps/mobile/src/locales/pt/common.json index d7ac534..1639d81 100644 --- a/apps/mobile/src/locales/pt/common.json +++ b/apps/mobile/src/locales/pt/common.json @@ -33,6 +33,7 @@ "barcode.contributing.title": "A ler a embalagem…", "barcode.contributing.body": "Estamos a extrair informação nutricional, alergénios e ingredientes – e a guardar o produto no catálogo partilhado, para que a próxima pessoa não precise de o fotografar.", "barcode.contributing.failed": "Não foi possível ler a embalagem. Tente novamente com uma foto nítida e boa iluminação.", + "barcode.contributing.saved": "✓ Guardado no catálogo partilhado – obrigado por contribuir!", "barcode.contributing.frontDone": "Foto da frente tirada", "barcode.contributing.backPrompt": "Agora fotografe a parte de trás com a declaração nutricional e os ingredientes – assim o produto fica completo.", "barcode.contributing.skipBack": "Pronto, analisar", @@ -463,6 +464,8 @@ "barcode.info.sugar": "– dos quais açúcares", "barcode.info.fat": "Gordura", "barcode.info.satfat": "– das quais saturadas", + "barcode.info.monofat": "– das quais monoinsaturadas", + "barcode.info.polyfat": "– das quais poli-insaturadas", "barcode.info.fiber": "Fibra", "barcode.info.salt": "Sal", "barcode.info.allergens": "Alergénios", diff --git a/apps/mobile/src/locales/sv/common.json b/apps/mobile/src/locales/sv/common.json index 2bb3d55..597a781 100644 --- a/apps/mobile/src/locales/sv/common.json +++ b/apps/mobile/src/locales/sv/common.json @@ -33,6 +33,7 @@ "barcode.contributing.title": "Läser av förpackningen…", "barcode.contributing.body": "Vi tar fram näring, allergener och innehåll – och sparar produkten i den delade katalogen så nästa person slipper fota den.", "barcode.contributing.failed": "Kunde inte läsa av förpackningen. Försök igen med skarp bild och bra ljus.", + "barcode.contributing.saved": "✓ Sparad i delade katalogen – tack för att du bidrar!", "barcode.contributing.frontDone": "Framsida tagen", "barcode.contributing.backPrompt": "Fota nu baksidan med näringsdeklaration och innehåll – då blir produkten komplett.", "barcode.contributing.skipBack": "Klar, analysera", @@ -463,6 +464,8 @@ "barcode.info.sugar": "– varav socker", "barcode.info.fat": "Fett", "barcode.info.satfat": "– varav mättat", + "barcode.info.monofat": "– varav enkelomättat", + "barcode.info.polyfat": "– varav fleromättat", "barcode.info.fiber": "Fiber", "barcode.info.salt": "Salt", "barcode.info.allergens": "Allergener", diff --git a/apps/worker/src/processors/scans.ts b/apps/worker/src/processors/scans.ts index 0c9a1c1..b00b261 100644 --- a/apps/worker/src/processors/scans.ts +++ b/apps/worker/src/processors/scans.ts @@ -83,33 +83,64 @@ export async function processScanJob(ctx: WorkerContext, scanJobId: string): Pro discountByName.set(k, (discountByName.get(k) ?? 0) + l.totalPriceMinor); // negativt belopp } } - (output as Record).items = lines - .filter((l) => l.isDiscount !== true && (l.normalizedName ?? l.rawText)) - .map((l, idx) => { - const qty = typeof l.quantity === "number" ? l.quantity : null; - const gross = - typeof l.totalPriceMinor === "number" - ? l.totalPriceMinor - : typeof l.unitPriceMinor === "number" && qty != null - ? Math.round(l.unitPriceMinor * qty) - : null; - const disc = l.normalizedName - ? (discountByName.get(String(l.normalizedName).toLowerCase()) ?? 0) - : 0; - const priceMinor = gross != null ? Math.max(0, gross + disc) : null; - return { - tempId: String(idx), - detectedName: String(l.normalizedName ?? l.rawText ?? ""), + // Slå ihop rader som är SAMMA vara (samma normaliserade namn + enhet) så att + // t.ex. "2 st mjölk" på kvittot blir EN vara med antal 2, inte två rader à 1 st. + // (Många butiker skriver ut varje köpt exemplar på egen rad.) + type MergedLine = { + detectedName: string; + canonicalIngredientId: string | null; + unit: string | null; + quantity: number; + gross: number | null; + confidence: number; + }; + const mergedItems = new Map(); + for (const l of lines) { + if (l.isDiscount === true) continue; + const name = (l.normalizedName ?? l.rawText) as string | undefined; + if (!name) continue; + const unit = (l.unit as string | null) ?? null; + const key = `${String(name).toLowerCase().trim()}|${unit ?? ""}`; + // En kvittorad är minst 1 exemplar även om antal saknas. + const qty = typeof l.quantity === "number" && l.quantity > 0 ? l.quantity : 1; + const lineGross = + typeof l.totalPriceMinor === "number" + ? l.totalPriceMinor + : typeof l.unitPriceMinor === "number" + ? Math.round(l.unitPriceMinor * qty) + : null; + const cur = mergedItems.get(key); + if (cur) { + cur.quantity += qty; + cur.gross = + cur.gross == null && lineGross == null ? null : (cur.gross ?? 0) + (lineGross ?? 0); + } else { + mergedItems.set(key, { + detectedName: String(name), canonicalIngredientId: (l.canonicalIngredientId as string | null) ?? null, - brand: null, - estimatedQuantity: qty, - unit: (l.unit as string | null) ?? null, - bestBeforeDate: null, + unit, + quantity: qty, + gross: lineGross, confidence: typeof l.confidence === "number" ? l.confidence : 0.5, - requiresConfirmation: true, - priceMinor, // kvittots (netto)pris i ören, förs vidare till lagret vid bekräftelse - }; - }); + }); + } + } + (output as Record).items = [...mergedItems.values()].map((m, idx) => { + const disc = discountByName.get(m.detectedName.toLowerCase()) ?? 0; + const priceMinor = m.gross != null ? Math.max(0, m.gross + disc) : null; + return { + tempId: String(idx), + detectedName: m.detectedName, + canonicalIngredientId: m.canonicalIngredientId, + brand: null, + estimatedQuantity: m.quantity, + unit: m.unit, + bestBeforeDate: null, + confidence: m.confidence, + requiresConfirmation: true, + priceMinor, // kvittots (netto)pris i ören, förs vidare till lagret vid bekräftelse + }; + }); output = await mapDetectedItemsToCanonical(ctx, output); } @@ -142,6 +173,8 @@ export async function processScanJob(ctx: WorkerContext, scanJobId: string): Pro carbsG: v.carbsG ?? 0, fatG: v.fatG ?? 0, saturatedFatG: v.saturatedFatG ?? 0, + monounsaturatedFatG: v.monounsaturatedFatG ?? undefined, + polyunsaturatedFatG: v.polyunsaturatedFatG ?? undefined, fiberG: v.fiberG ?? 0, sugarG: v.sugarG ?? 0, saltG: v.saltG ?? 0, diff --git a/packages/ai-contracts/src/gemini.ts b/packages/ai-contracts/src/gemini.ts index f85dab0..14fb369 100644 --- a/packages/ai-contracts/src/gemini.ts +++ b/packages/ai-contracts/src/gemini.ts @@ -972,7 +972,7 @@ Språk: ${lang}.`; Läs av exakt det som står på förpackningen. Gissa aldrig värden som inte syns. Returnera: - basis: "per_100_g", "per_100_ml" eller "per_portion" – vad gäller näringstabellen? -- values: { kcal, proteinG, carbsG, fatG, saturatedFatG, fiberG, sugarG, saltG } – null om ej synligt. +- values: { kcal, proteinG, carbsG, fatG, saturatedFatG, monounsaturatedFatG, polyunsaturatedFatG, fiberG, sugarG, saltG } – null om ej synligt. Läs ALLA "varav"-rader under Fett: varav mättat (saturatedFatG), varav enkelomättat (monounsaturatedFatG), varav fleromättat (polyunsaturatedFatG). - ingredientsText: rå text med ingredienser (från baksidan), eller null. - allergensDeclared: lista med allergener som explicit nämns (t.ex. ["gluten", "mjölk", "nötter"]). - gtin: streckkod/EAN om synlig, annars null. @@ -983,7 +983,7 @@ Läs av exakt det som står på förpackningen. Gissa aldrig värden som inte sy Svara ENDAST med giltig JSON som matchar detta schema: { "basis": "per_100_g" | "per_100_ml" | "per_portion" | null, - "values": { "kcal": number|null, "proteinG": number|null, "carbsG": number|null, "fatG": number|null, "saturatedFatG": number|null, "fiberG": number|null, "sugarG": number|null, "saltG": number|null } | null, + "values": { "kcal": number|null, "proteinG": number|null, "carbsG": number|null, "fatG": number|null, "saturatedFatG": number|null, "monounsaturatedFatG": number|null, "polyunsaturatedFatG": number|null, "fiberG": number|null, "sugarG": number|null, "saltG": number|null } | null, "ingredientsText": string | null, "allergensDeclared": string[], "gtin": string | null, diff --git a/packages/ai-contracts/src/tasks.ts b/packages/ai-contracts/src/tasks.ts index 300ac50..debb26a 100644 --- a/packages/ai-contracts/src/tasks.ts +++ b/packages/ai-contracts/src/tasks.ts @@ -222,6 +222,8 @@ export const readNutritionLabelOutput = z.object({ carbsG: z.number().nullable(), fatG: z.number().nullable(), saturatedFatG: z.number().nullable(), + monounsaturatedFatG: z.number().nullable().default(null), + polyunsaturatedFatG: z.number().nullable().default(null), fiberG: z.number().nullable(), sugarG: z.number().nullable(), saltG: z.number().nullable(), diff --git a/packages/shared-types/src/nutrition.ts b/packages/shared-types/src/nutrition.ts index a03a986..8a4d14d 100644 --- a/packages/shared-types/src/nutrition.ts +++ b/packages/shared-types/src/nutrition.ts @@ -16,6 +16,10 @@ export interface NutritionValues { fatG: number; /** gram */ saturatedFatG: number; + /** gram – enkelomättat fett. Valfritt (står oftast bara på vissa etiketter, t.ex. olja/nötter). */ + monounsaturatedFatG?: number; + /** gram – fleromättat fett. Valfritt (som ovan). */ + polyunsaturatedFatG?: number; /** gram */ fiberG: number; /** gram */