feat: ricetta persistente + quantità corretta per scalare ingredienti
- Ricetta cachata in localStorage: riaprendo mostra la stessa ricetta
- Si rigenera solo se cambia tipo pasto (colazione/pranzo/cena) o bottone 'Generane un'altra'
- Prompt Gemini aggiornato: richiede qty_number nella stessa unità di misura dell'inventario
- Bottone 'Usa' ora scala la quantità effettiva della ricetta (non più sempre 1)
- Estratta renderRecipe() per riutilizzo con cache
- Aggiunto bottone '🔄 Generane un'altra' nel risultato ricetta
This commit is contained in:
+3
-2
@@ -741,6 +741,7 @@ REGOLE IMPORTANTI:
|
||||
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. Esempio: se in dispensa c'è "Farina: 1000 g" e la ricetta richiede 200g, qty_number = 200. Se "Riso: 2 kg" e servono 300g, qty_number = 0.3. Per ingredienti non dalla dispensa, qty_number = 0.
|
||||
|
||||
INGREDIENTI DISPONIBILI IN DISPENSA:
|
||||
$ingredientsText
|
||||
@@ -755,8 +756,8 @@ Rispondi SOLO con un JSON valido in questo formato esatto (senza markdown, senza
|
||||
"tags": ["sano", "veloce", "..."],
|
||||
"expiry_note": "Nota sugli ingredienti in scadenza usati (o stringa vuota)",
|
||||
"ingredients": [
|
||||
{"name": "nome ingrediente", "qty": "quantità per $persons persone", "from_pantry": true},
|
||||
{"name": "sale", "qty": "q.b.", "from_pantry": false}
|
||||
{"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",
|
||||
|
||||
+81
-47
@@ -2637,11 +2637,25 @@ const MEAL_LABELS = {
|
||||
function openRecipeDialog() {
|
||||
const meal = getMealType();
|
||||
document.getElementById('recipe-meal-title').textContent = MEAL_LABELS[meal] || '🍳 Ricetta';
|
||||
document.getElementById('recipe-overlay').style.display = 'flex';
|
||||
|
||||
// Check for cached recipe matching current meal type
|
||||
try {
|
||||
const cached = JSON.parse(localStorage.getItem('cachedRecipe') || 'null');
|
||||
if (cached && cached.meal === meal && cached.recipe) {
|
||||
document.getElementById('recipe-ask').style.display = 'none';
|
||||
document.getElementById('recipe-loading').style.display = 'none';
|
||||
renderRecipe(cached.recipe);
|
||||
document.getElementById('recipe-result').style.display = '';
|
||||
return;
|
||||
}
|
||||
} catch (e) { /* ignore parse errors */ }
|
||||
|
||||
// No valid cache — show ask form
|
||||
document.getElementById('recipe-persons').value = 1;
|
||||
document.getElementById('recipe-ask').style.display = '';
|
||||
document.getElementById('recipe-loading').style.display = 'none';
|
||||
document.getElementById('recipe-result').style.display = 'none';
|
||||
document.getElementById('recipe-overlay').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeRecipeDialog() {
|
||||
@@ -2655,15 +2669,16 @@ function adjustRecipePersons(delta) {
|
||||
input.value = val;
|
||||
}
|
||||
|
||||
async function useRecipeIngredient(idx, productId, location, btn) {
|
||||
async function useRecipeIngredient(idx, productId, location, qtyNumber, btn) {
|
||||
if (btn.disabled) return;
|
||||
if (!qtyNumber || qtyNumber <= 0) qtyNumber = 1;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳...';
|
||||
|
||||
try {
|
||||
const result = await api('inventory_use', {}, 'POST', {
|
||||
product_id: productId,
|
||||
quantity: 1,
|
||||
quantity: qtyNumber,
|
||||
use_all: false,
|
||||
location: location
|
||||
});
|
||||
@@ -2689,6 +2704,66 @@ async function useRecipeIngredient(idx, productId, location, btn) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderRecipe(r) {
|
||||
let html = `<h2>${r.title}</h2>`;
|
||||
|
||||
// Meta tags
|
||||
html += '<div class="recipe-meta">';
|
||||
html += `<span class="recipe-tag">${MEAL_LABELS[r.meal] || r.meal}</span>`;
|
||||
html += `<span class="recipe-tag">👥 ${r.persons} pers.</span>`;
|
||||
if (r.prep_time) html += `<span class="recipe-tag">🔪 ${r.prep_time}</span>`;
|
||||
if (r.cook_time) html += `<span class="recipe-tag">🔥 ${r.cook_time}</span>`;
|
||||
if (r.tags) r.tags.forEach(t => { html += `<span class="recipe-tag">${t}</span>`; });
|
||||
html += '</div>';
|
||||
|
||||
// Expiry note
|
||||
if (r.expiry_note) {
|
||||
html += `<div class="recipe-expiry-note">⚠️ ${r.expiry_note}</div>`;
|
||||
}
|
||||
|
||||
// Ingredients
|
||||
html += '<h3>🧾 Ingredienti</h3><ul class="recipe-ingredients">';
|
||||
(r.ingredients || []).forEach((ing, idx) => {
|
||||
if (ing.from_pantry && ing.product_id) {
|
||||
const qtyNum = ing.qty_number || 0;
|
||||
const loc = (ing.location || 'dispensa').replace(/'/g, "\\'");
|
||||
html += `<li class="recipe-ingredient" id="recipe-ing-${idx}">`;
|
||||
html += `<span class="recipe-ing-text"><strong>${ing.name}</strong>: ${ing.qty} ✅</span>`;
|
||||
html += `<button class="btn-use-ingredient" onclick="useRecipeIngredient(${idx}, ${ing.product_id}, '${loc}', ${qtyNum}, this)" title="Scala dalla dispensa">📦 Usa</button>`;
|
||||
html += `</li>`;
|
||||
} else {
|
||||
const pantryIcon = ing.from_pantry ? ' ✅' : ' 🛒';
|
||||
html += `<li class="recipe-ingredient"><span class="recipe-ing-text"><strong>${ing.name}</strong>: ${ing.qty}${pantryIcon}</span></li>`;
|
||||
}
|
||||
});
|
||||
html += '</ul>';
|
||||
|
||||
// Steps
|
||||
html += '<h3>👨🍳 Procedimento</h3><ol>';
|
||||
(r.steps || []).forEach(step => {
|
||||
const cleanStep = step.replace(/^Passo\s*\d+\s*:\s*/i, '');
|
||||
html += `<li>${cleanStep}</li>`;
|
||||
});
|
||||
html += '</ol>';
|
||||
|
||||
// Nutrition note
|
||||
if (r.nutrition_note) {
|
||||
html += `<p style="color:var(--text-muted);font-size:0.85rem;margin-top:12px">💡 ${r.nutrition_note}</p>`;
|
||||
}
|
||||
|
||||
document.getElementById('recipe-content').innerHTML = html;
|
||||
}
|
||||
|
||||
function regenerateRecipe() {
|
||||
localStorage.removeItem('cachedRecipe');
|
||||
document.getElementById('recipe-result').style.display = 'none';
|
||||
document.getElementById('recipe-loading').style.display = 'none';
|
||||
const meal = getMealType();
|
||||
document.getElementById('recipe-meal-title').textContent = MEAL_LABELS[meal] || '🍳 Ricetta';
|
||||
document.getElementById('recipe-persons').value = 1;
|
||||
document.getElementById('recipe-ask').style.display = '';
|
||||
}
|
||||
|
||||
async function generateRecipe() {
|
||||
const meal = getMealType();
|
||||
const persons = parseInt(document.getElementById('recipe-persons').value) || 1;
|
||||
@@ -2712,52 +2787,11 @@ async function generateRecipe() {
|
||||
}
|
||||
|
||||
const r = result.recipe;
|
||||
let html = `<h2>${r.title}</h2>`;
|
||||
renderRecipe(r);
|
||||
|
||||
// Meta tags
|
||||
html += '<div class="recipe-meta">';
|
||||
html += `<span class="recipe-tag">${MEAL_LABELS[r.meal] || r.meal}</span>`;
|
||||
html += `<span class="recipe-tag">👥 ${r.persons} pers.</span>`;
|
||||
if (r.prep_time) html += `<span class="recipe-tag">🔪 ${r.prep_time}</span>`;
|
||||
if (r.cook_time) html += `<span class="recipe-tag">🔥 ${r.cook_time}</span>`;
|
||||
if (r.tags) r.tags.forEach(t => { html += `<span class="recipe-tag">${t}</span>`; });
|
||||
html += '</div>';
|
||||
// Cache the recipe for this meal type
|
||||
localStorage.setItem('cachedRecipe', JSON.stringify({ meal, recipe: r }));
|
||||
|
||||
// Expiry note
|
||||
if (r.expiry_note) {
|
||||
html += `<div class="recipe-expiry-note">⚠️ ${r.expiry_note}</div>`;
|
||||
}
|
||||
|
||||
// Ingredients
|
||||
html += '<h3>🧾 Ingredienti</h3><ul class="recipe-ingredients">';
|
||||
(r.ingredients || []).forEach((ing, idx) => {
|
||||
if (ing.from_pantry && ing.product_id) {
|
||||
html += `<li class="recipe-ingredient" id="recipe-ing-${idx}">`;
|
||||
html += `<span class="recipe-ing-text"><strong>${ing.name}</strong>: ${ing.qty} ✅</span>`;
|
||||
html += `<button class="btn-use-ingredient" onclick="useRecipeIngredient(${idx}, ${ing.product_id}, '${(ing.location || 'dispensa').replace(/'/g, "\\'")}', this)" title="Scala dalla dispensa">📦 Usa</button>`;
|
||||
html += `</li>`;
|
||||
} else {
|
||||
const pantryIcon = ing.from_pantry ? ' ✅' : ' 🛒';
|
||||
html += `<li class="recipe-ingredient"><span class="recipe-ing-text"><strong>${ing.name}</strong>: ${ing.qty}${pantryIcon}</span></li>`;
|
||||
}
|
||||
});
|
||||
html += '</ul>';
|
||||
|
||||
// Steps
|
||||
html += '<h3>👨🍳 Procedimento</h3><ol>';
|
||||
(r.steps || []).forEach(step => {
|
||||
// Remove leading "Passo N:" if present
|
||||
const cleanStep = step.replace(/^Passo\s*\d+\s*:\s*/i, '');
|
||||
html += `<li>${cleanStep}</li>`;
|
||||
});
|
||||
html += '</ol>';
|
||||
|
||||
// Nutrition note
|
||||
if (r.nutrition_note) {
|
||||
html += `<p style="color:var(--text-muted);font-size:0.85rem;margin-top:12px">💡 ${r.nutrition_note}</p>`;
|
||||
}
|
||||
|
||||
document.getElementById('recipe-content').innerHTML = html;
|
||||
document.getElementById('recipe-loading').style.display = 'none';
|
||||
document.getElementById('recipe-result').style.display = '';
|
||||
|
||||
|
||||
@@ -522,6 +522,9 @@
|
||||
</div>
|
||||
<div id="recipe-result" style="display:none" class="recipe-result">
|
||||
<div id="recipe-content"></div>
|
||||
<button class="btn btn-large btn-secondary full-width mt-2" onclick="regenerateRecipe()">
|
||||
🔄 Generane un'altra
|
||||
</button>
|
||||
<button class="btn btn-large btn-primary full-width mt-2" onclick="closeRecipeDialog()">
|
||||
✅ Chiudi
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user