Merge branch 'develop'

This commit is contained in:
dadaloop82
2026-04-20 14:43:12 +00:00
2 changed files with 123 additions and 110 deletions
+80 -97
View File
@@ -1957,52 +1957,66 @@ function generateRecipe(PDO $db): void {
}); });
// Build ingredient list grouped by priority // Build ingredient list grouped by priority
$priorityHeaders = [ // ---- Build compact ingredient list for AI prompt ----
1 => '⚠️ PRODOTTI SCADUTI (PRIORITÀ MASSIMA - USA SUBITO SE ANCORA COMMESTIBILI)', // Skip common staples that are always assumed available (rule says: acqua, sale, pepe, olio)
2 => '🔴 SCADENZA IMMINENTE (entro 3 giorni - USA PER PRIMI)', $staplePatterns = '/\b(sale|pepe|olio d.oliva|olio di semi|olio extra|acqua|aceto balsamico|aceto di|sel marin)\b/i';
3 => '🟠 SCADENZA RAVVICINATA (entro 7 giorni)',
4 => '🟡 ALTRI PRODOTTI CON SCADENZA',
5 => '📦 PRODOTTI APERTI (già aperti/tagliati — da consumare prima delle confezioni chiuse)',
6 => '🟢 ALTRI PRODOTTI',
];
$priorityGroups = []; $priorityGroups = [];
foreach ($items as $item) { foreach ($items as $item) {
$line = "- {$item['name']}"; $group = $getItemPriority($item);
if ($item['brand']) $line .= " ({$item['brand']})"; // Skip always-available staples from category 6 (closed, no expiry concern)
$line .= ": {$item['quantity']} {$item['unit']}"; if ($group >= 5 && preg_match($staplePatterns, $item['name'])) continue;
if ($item['unit'] === 'conf' && !empty($item['package_unit']) && $item['default_quantity'] > 0) {
$line .= " (da {$item['default_quantity']} {$item['package_unit']} ciascuna, totale: " . ($item['quantity'] * $item['default_quantity']) . " {$item['package_unit']})";
}
if ($item['expiry_date']) {
$daysLeft = intval($item['days_left']);
if ($daysLeft < 0) {
$line .= " [SCADUTO da " . abs($daysLeft) . " giorni]";
} else {
$line .= " [scade tra $daysLeft giorni]";
}
}
if (strtolower($item['location']) === 'frigo') {
$line .= " [FRIGO]";
}
$qty = floatval($item['quantity']); $qty = floatval($item['quantity']);
$isOpen = !empty($item['opened_at']) || $isOpen = !empty($item['opened_at']) ||
($qty > 0 && $qty < 1 && $item['unit'] === 'conf'); ($qty > 0 && $qty < 1 && $item['unit'] === 'conf');
if ($isOpen) { $daysLeft = intval($item['days_left']);
$line .= ' [APERTO]';
// Compact line: name + qty (with conf expansion) + flags only when relevant
$line = "- {$item['name']}: {$item['quantity']} {$item['unit']}";
if ($item['unit'] === 'conf' && !empty($item['package_unit']) && $item['default_quantity'] > 0) {
$line .= " ({$item['default_quantity']}{$item['package_unit']}/conf)";
} }
$line .= " (in {$item['location']})"; // Add expiry info only for priority groups 1-4
if ($group <= 4 && $item['expiry_date']) {
$group = $getItemPriority($item); if ($daysLeft < 0) {
$line .= " ⚠️SCADUTO";
} elseif ($daysLeft <= 3) {
$line .= " 🔴{$daysLeft}gg";
} elseif ($daysLeft <= 7) {
$line .= " 🟠{$daysLeft}gg";
} else {
$line .= " {$daysLeft}gg";
}
}
if ($isOpen) $line .= ' [APERTO]';
$priorityGroups[$group][] = $line; $priorityGroups[$group][] = $line;
} }
// Build sections: detailed headers for urgent groups, brief for rest
$ingredientSections = []; $ingredientSections = [];
$priorityHeaders = [
1 => 'SCADUTI — usa subito',
2 => 'SCADENZA ≤3gg — priorità alta',
3 => 'SCADENZA ≤7gg',
4 => 'ALTRI CON SCADENZA',
5 => 'APERTI',
6 => 'DISPENSA',
];
// Limit groups to keep prompt compact:
// 1-3 (urgent): all items; 4 (has expiry): max 40; 5 (opened): all; 6 (pantry): max 20
foreach ($priorityHeaders as $g => $header) { foreach ($priorityHeaders as $g => $header) {
if (!empty($priorityGroups[$g])) { if (empty($priorityGroups[$g])) continue;
$ingredientSections[] = "=== {$header} ===\n" . implode("\n", $priorityGroups[$g]); $groupItems = $priorityGroups[$g];
if ($g === 4 && count($groupItems) > 40) {
$groupItems = array_slice($groupItems, 0, 40);
} elseif ($g === 6 && count($groupItems) > 20) {
$groupItems = array_slice($groupItems, 0, 20);
} }
$ingredientSections[] = "[$header]\n" . implode("\n", $groupItems);
} }
$ingredientsText = implode("\n\n", $ingredientSections); $ingredientsText = implode("\n", $ingredientSections);
// Build mandatory/recommended lists ONLY when user explicitly selected // Build mandatory/recommended lists ONLY when user explicitly selected
// 'scadenze' (expiry priority) or 'zerowaste' (zero waste) options. // 'scadenze' (expiry priority) or 'zerowaste' (zero waste) options.
@@ -2043,10 +2057,10 @@ function generateRecipe(PDO $db): void {
$mustUseText = ''; $mustUseText = '';
if (!empty($mandatoryItems)) { if (!empty($mandatoryItems)) {
$mustUseText .= "\n\n⚠️⚠️⚠️ INGREDIENTI OBBLIGATORI (SCADUTI O IN SCADENZA OGGI/DOMANI) ⚠️⚠️⚠️\nLa ricetta DEVE usare ALMENO uno (meglio se tutti) di questi ingredienti come ingrediente PRINCIPALE. Non sono opzionali!\n" . implode("\n", array_map(fn($n) => "$n", $mandatoryItems)); $mustUseText .= "\n\n⚠️ OBBLIGATORI (scaduti/imminenti — DEVE usarne almeno 1):\n" . implode("\n", array_map(fn($n) => "$n", $mandatoryItems));
} }
if (!empty($recommendedItems)) { if (!empty($recommendedItems)) {
$mustUseText .= "\n\n🔶 INGREDIENTI FORTEMENTE CONSIGLIATI (aperti e/o in scadenza a breve)\nSono già aperti e/o scadono presto — includi più di questi possibile nella ricetta:\n" . implode("\n", array_map(fn($n) => "· $n", $recommendedItems)); $mustUseText .= "\n\n🔶 CONSIGLIATI (aperti/in scadenza):\n" . implode("\n", array_map(fn($n) => "· $n", $recommendedItems));
} }
$mealLabels = [ $mealLabels = [
@@ -2079,18 +2093,18 @@ function generateRecipe(PDO $db): void {
if (!empty($subType) && isset($subTypeLabels[$mealType][$subType])) { if (!empty($subType) && isset($subTypeLabels[$mealType][$subType])) {
$subHint = $subTypeLabels[$mealType][$subType]; $subHint = $subTypeLabels[$mealType][$subType];
$mealLabel .= " — tipo: $subHint"; $mealLabel .= " — tipo: $subHint";
$subTypeText = "\n\n🎨 SOTTO-TIPO RICHIESTO:\nL'utente ha scelto specificamente: {$subHint}\nLa ricetta DEVE essere di questo tipo preciso. Non proporre un tipo diverso di {$mealType}."; $subTypeText = "\n\n🎨 SOTTO-TIPO: {$subHint}. La ricetta DEVE essere di questo tipo.";
} }
// Build extra rules from options // Build extra rules from options
$extraRules = []; $extraRules = [];
$optionLabels = [ $optionLabels = [
'veloce' => 'La ricetta deve essere VELOCE: massimo 15-20 minuti totali di preparazione e cottura.', 'veloce' => 'VELOCE: max 15-20 min totali.',
'pocafame' => 'L\'utente ha POCA FAME: proponi una porzione leggera, magari uno snack, un\'insalata o qualcosa di semplice e poco abbondante.', 'pocafame' => 'POCA FAME: porzione leggera, snack o insalata.',
'scadenze' => 'PRIORITÀ SCADENZE: usa ASSOLUTAMENTE per primi gli ingredienti più vicini alla scadenza o già scaduti (se ancora commestibili).', 'scadenze' => 'PRIORITÀ SCADENZE: usa per primi i prodotti in scadenza.',
'salutare' => 'Ricetta EXTRA SALUTARE: prediligi ingredienti integrali, tante verdure, pochi grassi, cotture leggere.', 'salutare' => 'SALUTARE: ingredienti integrali, verdure, pochi grassi.',
'opened' => 'PRIORITÀ COSE APERTE: dai la MASSIMA PRIORITÀ ai prodotti con confezione aperta (contrassegnati [APERTO]) e a quelli in FRIGO (contrassegnati [FRIGO]). Questi prodotti si deteriorano più in fretta e DEVONO essere usati per primi. Costruisci la ricetta attorno a questi ingredienti.', 'opened' => 'PRIORITÀ APERTI: usa per primi i prodotti [APERTO].',
'zerowaste' => 'ZERO SPRECHI: cerca di usare quanti più ingredienti in scadenza possibile, combina anche ingredienti insoliti pur di non sprecare nulla.' 'zerowaste' => 'ZERO SPRECHI: usa il più possibile ingredienti in scadenza.'
]; ];
foreach ($options as $opt) { foreach ($options as $opt) {
if (isset($optionLabels[$opt])) { if (isset($optionLabels[$opt])) {
@@ -2106,7 +2120,7 @@ function generateRecipe(PDO $db): void {
// Appliances // Appliances
$appliancesText = ''; $appliancesText = '';
if (!empty($appliances)) { if (!empty($appliances)) {
$appliancesText = "\n\nELETTRODOMESTICI DISPONIBILI:\nL'utente dispone di: " . implode(', ', $appliances) . ".\nPuoi usare SOLO questi elettrodomestici (più fornelli e forno che si presumono sempre disponibili). Non suggerire ricette che richiedano elettrodomestici non elencati."; $appliancesText = "\n\nELETTRODOMESTICI: " . implode(', ', $appliances) . " (+ fornelli e forno). Usa SOLO questi.";
} }
// Dietary restrictions // Dietary restrictions
@@ -2184,8 +2198,8 @@ function generateRecipe(PDO $db): void {
$matchingBlock = "Nessun ingrediente perfettamente corrispondente trovato — usa la cosa più affine disponibile e segnalalo in nutrition_note."; $matchingBlock = "Nessun ingrediente perfettamente corrispondente trovato — usa la cosa più affine disponibile e segnalalo in nutrition_note.";
} }
$mealPlanText = "\n\n🎯 TIPOLOGIA PASTO PIANIFICATA — OBBLIGATORIA:\nOggi questo pasto DEVE essere: {$hint}\nQuesta è una regola del piano alimentare personale dell'utente, NON un suggerimento.\n{$matchingBlock}"; $mealPlanText = "\n\n🎯 TIPO OBBLIGATORIO: {$hint}\n{$matchingBlock}";
$mealPlanRule = "0. TIPOLOGIA PASTO OBBLIGATORIA: la ricetta DEVE rispettare il tipo pianificato ({$hint}). Usa gli ingredienti compatibili evidenziati sopra come base principale del piatto. Non ignorare questa regola.\n "; $mealPlanRule = "0. La ricetta DEVE essere: {$hint}. Usa gli ingredienti compatibili come base.\n ";
} }
// Today's previous recipes from DB - avoid repetition // Today's previous recipes from DB - avoid repetition
@@ -2216,77 +2230,41 @@ function generateRecipe(PDO $db): void {
$varietyText = ''; $varietyText = '';
if (!empty($todayTitles)) { if (!empty($todayTitles)) {
$todayList = implode(', ', array_map(function($t) { return '"' . $t . '"'; }, $todayTitles)); $todayList = implode(', ', array_map(function($t) { return '"' . $t . '"'; }, $todayTitles));
$varietyText .= "\n\nRICETTE GIÀ PREPARATE OGGI:\n{$todayList}\nNON proporre una ricetta simile o con lo stesso concetto di quelle già fatte oggi. Varia il tipo di piatto, gli ingredienti principali e lo stile di cucina. Ad esempio se a pranzo c'era una piadina, a cena proponi pasta, riso, zuppa o altro — MAI un'altra piadina o wrap o piatto concettualmente simile."; $varietyText .= "\n\nGIÀ FATTO OGGI: {$todayList} — proponi qualcosa di DIVERSO.";
} }
// Weekly variety: list all recent recipes so AI avoids repetition // Weekly variety: list all recent recipes so AI avoids repetition
$weekOnly = array_diff($weekTitles, $todayTitles); $weekOnly = array_diff($weekTitles, $todayTitles);
if (!empty($weekOnly)) { if (!empty($weekOnly)) {
$weekList = implode(', ', array_map(function($t) { return '"' . $t . '"'; }, array_values($weekOnly))); $weekList = implode(', ', array_map(function($t) { return '"' . $t . '"'; }, array_values($weekOnly)));
$varietyText .= "\n\nRICETTE DEGLI ULTIMI 7 GIORNI:\n{$weekList}\nCerca di variare rispetto a queste ricette recenti: evita piatti troppo simili o con gli stessi ingredienti principali. Alterna pasta, riso, zuppe, carne, pesce, verdure, piatti freddi, ecc."; $varietyText .= "\n\nULTIMI 7GG: {$weekList} — varia.";
} }
// If this is a re-generation, stress the need for a truly different recipe // If this is a re-generation, stress the need for a truly different recipe
$regenText = ''; $regenText = '';
if ($variation > 0) { if ($variation > 0) {
$regenText = "\n\n🔁 RIGENERAZIONE #{$variation}: L'utente ha già visto e scartato le ricette precedenti. " . $regenText = "\n\n🔁 RIGENERA #{$variation}: proponi qualcosa di COMPLETAMENTE DIVERSO (altro stile, altro ingrediente principale, altra tecnica).";
"Devi proporre qualcosa di COMPLETAMENTE DIVERSO: stile di cucina diverso, ingrediente principale diverso, " .
"tecnica di cottura diversa, piatto di un'altra tradizione culinaria o di un'altra categoria. " .
"Non basta cambiare il nome della stessa idea. Sorprendi! Sii creativo!";
if (!empty($rejectedIngredients)) { if (!empty($rejectedIngredients)) {
$rejList = implode(', ', array_map(fn($n) => '"' . $n . '"', $rejectedIngredients)); $rejList = implode(', ', array_map(fn($n) => '"' . $n . '"', $rejectedIngredients));
$regenText .= "\n\n🚫 INGREDIENTI PRINCIPALI GIÀ RIFIUTATI DALL'UTENTE: {$rejList}\n" . $regenText .= " Evita come ingrediente principale: {$rejList}.";
"NON usare NESSUNO di questi come ingrediente PRINCIPALE della nuova ricetta. " .
"Puoi usarli come ingrediente secondario solo se indispensabile. " .
"Scegli ingredienti principali completamente diversi dalla lista della dispensa!";
} }
} }
$prompt = <<<PROMPT $prompt = <<<PROMPT
Sei un nutrizionista e chef italiano esperto. Genera UNA ricetta per $mealLabel per $persons persona/e usando PRINCIPALMENTE gli ingredienti disponibili nella dispensa dell'utente. Sei uno chef italiano esperto. Genera UNA ricetta per $mealLabel per $persons persona/e usando gli ingredienti disponibili sotto.
{$extraRulesText}{$appliancesText}{$dietaryText}{$subTypeText}{$mealPlanText}{$varietyText}{$regenText}{$mustUseText} {$extraRulesText}{$appliancesText}{$dietaryText}{$subTypeText}{$mealPlanText}{$varietyText}{$regenText}{$mustUseText}
REGOLE IMPORTANTI: REGOLE:
{$mealPlanRule}1. ORDINE DI PRIORITÀ INGREDIENTI (dal più urgente al meno urgente) — gli ingredienti nella lista sono già ordinati per priorità: {$mealPlanRule}1. PRIORITÀ: usa prima gli ingredienti scaduti/in scadenza (⚠️🔴🟠), poi quelli [APERTO], poi il resto.
a) PRODOTTI SCADUTI: se ancora commestibili, usali SUBITO — hanno la priorità massima assoluta 2. Usa SOLO ingredienti dalla lista + acqua/sale/pepe/olio (sempre disponibili).
b) PRODOTTI IN SCADENZA IMMINENTE (≤3 giorni): usa questi per primi dopo gli scaduti 3. Quantità per $persons persona/e. Se un ingrediente ha poca quantità, usalo TUTTO.
c) PRODOTTI IN SCADENZA RAVVICINATA (≤7 giorni): poi questi 4. "qty_number": valore NUMERICO nella STESSA unità della dispensa (g/ml/pz/conf, MAI kg o litri). Per non-dispensa: 0.
d) PRODOTTI CON SCADENZA PIÙ LONTANA: poi questi 5. "name": usa ESATTAMENTE il nome dalla lista (il sistema lo usa per scalare l'inventario).
e) CONFEZIONI APERTE (contrassegnate [APERTO]): preferiscile rispetto a quelle chiuse 6. Includi nella lista ingredienti TUTTI quelli citati nei passi (tranne acqua/sale/pepe/olio).
f) PRODOTTI CHIUSI SENZA SCADENZA: usa per ultimi
Costruisci la ricetta partendo dagli ingredienti delle categorie più urgenti! Usa il maggior numero possibile di ingredienti ad alta priorità.
*** OBBLIGO: se nella sezione "INGREDIENTI OBBLIGATORI" sopra ci sono prodotti, la ricetta DEVE contenere ALMENO UNO di quei prodotti come ingrediente principale. Se li ignori, la ricetta è SBAGLIATA. ***
*** CONSIGLIO: gli "INGREDIENTI FORTEMENTE CONSIGLIATI" vanno usati se si abbinano bene alla ricetta, ma puoi escluderli se non si adattano — spesso sono prodotti freschi come il latte che si usano comunque. ***
2. Prediligi una ricetta SANA, EQUILIBRATA e NUTRIENTE
3. Usa SOLO ingredienti dalla lista sotto, più al massimo acqua, sale, pepe e olio che si presumono sempre disponibili
4. Adatta le quantità per $persons persona/e
5. Se non ci sono abbastanza ingredienti per una ricetta completa, suggerisci la migliore combinazione possibile
6. La ricetta deve essere adatta al pasto: $mealLabel
7. IMPORTANTE - QUANTITÀ NUMERICHE: per ogni ingrediente dalla dispensa, il campo "qty_number" DEVE contenere il valore NUMERICO da scalare dall'inventario, espresso nella STESSA unità di misura della dispensa. Le unità ammesse sono SOLO: g (grammi), ml (millilitri), pz (pezzi), conf (confezioni). NON usare mai kg o litri. Esempio: se in dispensa c'è "Farina: 1000 g" e la ricetta richiede 200g, qty_number = 200. Se "Riso: 2000 g" e servono 300g, qty_number = 300. Per ingredienti non dalla dispensa, qty_number = 0.
8. GESTIONE SMART QUANTITÀ: NON lasciare rimasugli poco usabili in dispensa. Se un ingrediente ha una quantità piccola (es. 50g di formaggio, 1 uovo, 100ml di latte), preferisci usarlo TUTTO piuttosto che lasciarne una quantità inutilizzabile. Se invece la quantità è abbondante, usa solo il necessario lasciando abbastanza per un altro pasto. Pensa sempre: "quello che resta sarà sufficiente per un altro utilizzo?"
9. NOMI INGREDIENTI: nel campo "name" di ogni ingrediente dalla dispensa, usa ESATTAMENTE lo stesso nome riportato nella lista sotto (copia-incolla). NON riformulare, NON abbreviare, NON tradurre. Il sistema usa il nome per collegare l'ingrediente all'inventario. Se il nome non corrisponde, l'ingrediente non viene scalato correttamente.
10. COMPLETEZZA: la lista ingredienti DEVE includere TUTTI gli ingredienti necessari citati nei passi della ricetta. Se un passo dice "aggiungere il latte", il latte DEVE comparire nella lista ingredienti. Non dare per scontato nessun ingrediente tranne acqua, sale, pepe e olio.
INGREDIENTI DISPONIBILI IN DISPENSA: DISPENSA:
$ingredientsText $ingredientsText
Rispondi SOLO con un JSON valido in questo formato esatto (senza markdown, senza backtick): Rispondi SOLO JSON valido (no markdown):
{ {"title":"…","meal":"$mealType","persons":$persons,"prep_time":"…","cook_time":"…","tags":["…"],"expiry_note":"…","ingredients":[{"name":"…","qty":"200 g","qty_number":200,"from_pantry":true}],"steps":["Passo 1…"],"nutrition_note":"…"}
"title": "Nome della ricetta",
"meal": "$mealType",
"persons": $persons,
"prep_time": "tempo preparazione (es. 15 min)",
"cook_time": "tempo cottura (es. 20 min)",
"tags": ["sano", "veloce", "..."],
"expiry_note": "Nota sugli ingredienti in scadenza usati (o stringa vuota)",
"ingredients": [
{"name": "nome ingrediente", "qty": "quantità leggibile (es: 200 g)", "qty_number": 200, "from_pantry": true},
{"name": "sale", "qty": "q.b.", "qty_number": 0, "from_pantry": false}
],
"steps": [
"Passo 1: descrizione dettagliata",
"Passo 2: descrizione dettagliata"
],
"nutrition_note": "Breve nota nutrizionale sulla ricetta"
}
PROMPT; PROMPT;
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={$apiKey}"; $url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={$apiKey}";
@@ -2319,7 +2297,12 @@ PROMPT;
curl_close($ch); curl_close($ch);
if ($response === false || $httpCode !== 200) { if ($response === false || $httpCode !== 200) {
echo json_encode(['success' => false, 'error' => 'Errore API Gemini', 'http_code' => $httpCode]); $errDetail = '';
if ($response) {
$errData = json_decode($response, true);
$errDetail = $errData['error']['message'] ?? substr($response, 0, 300);
}
echo json_encode(['success' => false, 'error' => 'Errore API Gemini', 'http_code' => $httpCode, 'detail' => $errDetail]);
return; return;
} }
+43 -13
View File
@@ -76,6 +76,16 @@ let _scaleStabilityVal = null; // value we are currently timing for stabilit
let _scaleUserDismissed = false; // user tapped or edited → don't retrigger for same value let _scaleUserDismissed = false; // user tapped or edited → don't retrigger for same value
let _scaleRecipeAutoFillPaused = false; // pause flag for recipe-use modal only let _scaleRecipeAutoFillPaused = false; // pause flag for recipe-use modal only
let _scaleLastConfirmedGrams = null; // grams of last auto-confirmed weight (to detect product change) let _scaleLastConfirmedGrams = null; // grams of last auto-confirmed weight (to detect product change)
let _scaleLastStableGrams = null; // last accepted stable reading in grams (for jitter filtering)
function _scaleToGrams(value, unit) {
if (!isFinite(value)) return null;
const u = (unit || 'g').toLowerCase();
if (u === 'kg') return value * 1000;
if (u === 'lbs' || u === 'lb') return value * 453.592;
if (u === 'oz') return value * 28.3495;
return value; // g / ml treated as grams-equivalent for stability filtering
}
function scaleInit() { function scaleInit() {
const s = getSettings(); const s = getSettings();
@@ -121,34 +131,53 @@ function _scaleOnMessage(msg) {
updateScaleReadButtons(); updateScaleReadButtons();
} else if (msg.type === 'weight') { } else if (msg.type === 'weight') {
// Ignore negative weight values (tare artifacts, sensor noise) // Ignore negative weight values (tare artifacts, sensor noise)
if (parseFloat(msg.value) < 0) return; const rawValue = parseFloat(msg.value);
_scaleLatestWeight = msg; if (rawValue < 0) return;
// Ignore sub-gram jitter for stability decisions: only integer-gram changes matter.
let effectiveStable = !!msg.stable;
const grams = _scaleToGrams(rawValue, msg.unit);
if (grams !== null) {
if (effectiveStable) {
_scaleLastStableGrams = grams;
} else if (_scaleLastStableGrams !== null) {
if (Math.round(grams) === Math.round(_scaleLastStableGrams)) {
effectiveStable = true;
}
}
if (effectiveStable) {
_scaleLastStableGrams = grams;
}
}
const liveMsg = effectiveStable === msg.stable ? msg : { ...msg, stable: effectiveStable };
_scaleLatestWeight = liveMsg;
// Update live reading modal overlay if visible (scale-read modal) // Update live reading modal overlay if visible (scale-read modal)
const live = document.getElementById('scale-reading-live'); const live = document.getElementById('scale-reading-live');
if (live) live.textContent = `${msg.value} ${msg.unit || 'kg'}${msg.stable ? ' ✓' : ' …'}`; if (live) live.textContent = `${msg.value} ${msg.unit || 'kg'}${liveMsg.stable ? ' ✓' : ' …'}`;
// Also update edit-form inline scale reading if visible // Also update edit-form inline scale reading if visible
const editLive = document.getElementById('edit-scale-reading'); const editLive = document.getElementById('edit-scale-reading');
if (editLive) editLive.textContent = `${msg.value} ${msg.unit || 'kg'}${msg.stable ? ' ✓' : ' …'}`; if (editLive) editLive.textContent = `${msg.value} ${msg.unit || 'kg'}${liveMsg.stable ? ' ✓' : ' …'}`;
// Always update the persistent live box on the use page (every message, stable or not) // Always update the persistent live box on the use page (every message, stable or not)
_scaleUpdateLiveBox(msg); _scaleUpdateLiveBox(liveMsg);
// If weight is NOT stable: stop any running timer/bar but keep the sentinel value. // If weight is NOT stable: stop any running timer/bar but keep the sentinel value.
// The sentinel is reset only when a genuinely different stable value arrives. // The sentinel is reset only when a genuinely different stable value arrives.
if (!msg.stable) { if (!liveMsg.stable) {
_cancelScaleTimersOnly(); _cancelScaleTimersOnly();
} }
// Fulfil pending callback on stable reading // Fulfil pending callback on stable reading
if (msg.stable && _scaleWeightCallback) { if (liveMsg.stable && _scaleWeightCallback) {
const cb = _scaleWeightCallback; const cb = _scaleWeightCallback;
_scaleWeightCallback = null; _scaleWeightCallback = null;
cb(msg); cb(liveMsg);
} }
// Drive stability logic on use page // Drive stability logic on use page
if (msg.stable && _currentPageId === 'use') { if (liveMsg.stable && _currentPageId === 'use') {
_scaleAutoFillUse(msg); _scaleAutoFillUse(liveMsg);
} }
// Same for recipe-use modal // Same for recipe-use modal
if (msg.stable && document.getElementById('ruse-quantity') && !_scaleRecipeAutoFillPaused) { if (liveMsg.stable && document.getElementById('ruse-quantity') && !_scaleRecipeAutoFillPaused) {
_scaleAutoFillRecipeUse(msg); _scaleAutoFillRecipeUse(liveMsg);
} }
} }
} }
@@ -9652,7 +9681,8 @@ async function generateRecipe() {
if (result.error === 'no_api_key') { if (result.error === 'no_api_key') {
showToast('⚠️ Chiave API Gemini non configurata', 'warning'); showToast('⚠️ Chiave API Gemini non configurata', 'warning');
} else { } else {
showToast(result.error || 'Errore nella generazione', 'error'); const detail = result.detail ? ` (${result.detail})` : '';
showToast((result.error || 'Errore nella generazione') + detail, 'error');
} }
return; return;
} }