feat: zero-waste tips during cooking mode (#76)

This commit is contained in:
dadaloop82
2026-05-17 09:16:48 +00:00
parent 06f6d58fb5
commit a602726531
12 changed files with 169 additions and 10 deletions
+6
View File
@@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Recipe scraps tips** — During cooking steps, detect "waste" generated (peels, cores, bones, eggshells, coffee grounds, citrus zest, etc.) and surface AI-powered tips on how to reuse them (compost, natural cleaner, broth, candied peel, etc.). Could be shown as an optional collapsible hint card below the step that generates the scrap. - **Recipe scraps tips** — During cooking steps, detect "waste" generated (peels, cores, bones, eggshells, coffee grounds, citrus zest, etc.) and surface AI-powered tips on how to reuse them (compost, natural cleaner, broth, candied peel, etc.). Could be shown as an optional collapsible hint card below the step that generates the scrap.
## [1.7.19] - 2026-05-19
### Added
- **Zero-waste tips during cooking** — When cooking mode is active, a ♻️ card appears below each step that generates reusable scraps (peels, cooking water, egg whites, cheese rinds, bread crusts, vegetable tops, etc.). Gemini generates the tips as part of the recipe JSON at no extra API cost. Tips are dismissible per-step and reset on recipe restart. Opt-in toggle in Settings → Zero-waste tips (default OFF). Resolves [#76](https://github.com/dadaloop82/EverShelf/issues/76).
- New translation keys `cooking.zerowaste_*` and `settings.zerowaste.*` in all 5 languages (IT, EN, DE, FR, ES).
## [1.7.18] - 2026-05-19 ## [1.7.18] - 2026-05-19
### Added ### Added
+1 -1
View File
@@ -25,7 +25,7 @@
[![SQLite](https://img.shields.io/badge/SQLite-3-blue.svg)](https://www.sqlite.org/) [![SQLite](https://img.shields.io/badge/SQLite-3-blue.svg)](https://www.sqlite.org/)
[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED.svg)](Dockerfile) [![Docker](https://img.shields.io/badge/Docker-Ready-2496ED.svg)](Dockerfile)
[![i18n](https://img.shields.io/badge/i18n-IT%20%7C%20EN%20%7C%20DE%20%7C%20FR%20%7C%20ES-orange.svg)](translations/) [![i18n](https://img.shields.io/badge/i18n-IT%20%7C%20EN%20%7C%20DE%20%7C%20FR%20%7C%20ES-orange.svg)](translations/)
[![Version](https://img.shields.io/badge/version-1.7.18-brightgreen.svg)](CHANGELOG.md) [![Version](https://img.shields.io/badge/version-1.7.19-brightgreen.svg)](CHANGELOG.md)
[![GitHub stars](https://img.shields.io/github/stars/dadaloop82/EverShelf?style=social)](https://github.com/dadaloop82/EverShelf/stargazers) [![GitHub stars](https://img.shields.io/github/stars/dadaloop82/EverShelf?style=social)](https://github.com/dadaloop82/EverShelf/stargazers)
[![Last commit](https://img.shields.io/github/last-commit/dadaloop82/EverShelf/main)](https://github.com/dadaloop82/EverShelf/commits/main) [![Last commit](https://img.shields.io/github/last-commit/dadaloop82/EverShelf/main)](https://github.com/dadaloop82/EverShelf/commits/main)
[![Contributors](https://img.shields.io/github/contributors/dadaloop82/EverShelf)](https://github.com/dadaloop82/EverShelf/graphs/contributors) [![Contributors](https://img.shields.io/github/contributors/dadaloop82/EverShelf)](https://github.com/dadaloop82/EverShelf/graphs/contributors)
+2 -1
View File
@@ -4458,13 +4458,14 @@ REGOLE:
6. Includi nella lista ingredienti TUTTI quelli citati nei passi (tranne acqua/sale/pepe/olio). 6. Includi nella lista ingredienti TUTTI quelli citati nei passi (tranne acqua/sale/pepe/olio).
7. Language rule: {$recipeLangName} only for all textual fields (`title`, `tags`, `expiry_note`, `ingredients.qty`, `steps`, `nutrition_note`, `tools_needed`). Keep `meal` unchanged. 7. Language rule: {$recipeLangName} only for all textual fields (`title`, `tags`, `expiry_note`, `ingredients.qty`, `steps`, `nutrition_note`, `tools_needed`). Keep `meal` unchanged.
8. `tools_needed`: array of kitchen tools/appliances actually required by this recipe (e.g. ["Forno","Frullatore"]). Use the same language as all other text fields. Empty array [] if only stovetop/knife/pan needed. 8. `tools_needed`: array of kitchen tools/appliances actually required by this recipe (e.g. ["Forno","Frullatore"]). Use the same language as all other text fields. Empty array [] if only stovetop/knife/pan needed.
9. `zero_waste_tips`: array of zero-waste tips for steps that generate reusable scraps (peels, leftover cooking water, egg whites, cheese rinds, bread crusts, vegetable tops, etc.). Each entry: {"step": 0-based_step_index, "scrap": "scrap name", "tip": "short practical reuse tip (max 20 words)"}. Use the same language as other text fields. Empty array [] if no reusable scraps are generated.
DISPENSA: DISPENSA:
$ingredientsText $ingredientsText
Rispondi SOLO JSON valido (no markdown): Rispondi SOLO JSON valido (no markdown):
{$promptLanguageRule} {$promptLanguageRule}
{"title":"","meal":"$mealType","persons":$persons,"prep_time":"","cook_time":"","tags":[""],"expiry_note":"","tools_needed":[""],"ingredients":[{"name":"","qty":"200 g","qty_number":200,"from_pantry":true}],"steps":["{$promptStepExample}"],"nutrition_note":""} {"title":"","meal":"$mealType","persons":$persons,"prep_time":"","cook_time":"","tags":[""],"expiry_note":"","tools_needed":[""],"ingredients":[{"name":"","qty":"200 g","qty_number":200,"from_pantry":true}],"steps":["{$promptStepExample}"],"nutrition_note":"","zero_waste_tips":[{"step":0,"scrap":"","tip":""}]}
PROMPT; PROMPT;
$genConfig = [ $genConfig = [
+58
View File
@@ -6875,6 +6875,64 @@ body.cooking-mode-active .app-header {
opacity: 0.8; opacity: 0.8;
} }
/* ===== ZERO-WASTE TIP CARD (cooking mode) ===== */
.cooking-zerowaste-tip {
display: flex;
align-items: flex-start;
gap: 10px;
background: rgba(16, 185, 129, 0.10);
border: 1.5px solid rgba(16, 185, 129, 0.35);
border-radius: 12px;
padding: 12px 14px;
margin: 10px 16px 0;
position: relative;
animation: zwFadeIn 0.3s ease;
flex-direction: column;
}
@keyframes zwFadeIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.cooking-zerowaste-label {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #059669;
}
.cooking-zerowaste-scrap {
font-size: 0.85rem;
font-weight: 700;
color: #065f46;
margin-left: 4px;
}
.cooking-zerowaste-text {
font-size: 0.85rem;
color: var(--text);
margin: 4px 0 0;
line-height: 1.45;
}
.cooking-zerowaste-close {
position: absolute;
top: 8px;
right: 10px;
background: none;
border: none;
font-size: 0.85rem;
cursor: pointer;
color: #6b7280;
padding: 2px 4px;
line-height: 1;
}
.cooking-zerowaste-close:hover { color: #374151; }
[data-theme="dark"] .cooking-zerowaste-tip {
background: rgba(16, 185, 129, 0.08);
border-color: rgba(16, 185, 129, 0.25);
}
[data-theme="dark"] .cooking-zerowaste-scrap { color: #6ee7b7; }
[data-theme="dark"] .cooking-zerowaste-label { color: #34d399; }
[data-theme="dark"] .cooking-zerowaste-close { color: #9ca3af; }
/* ===== DARK MODE ===== */ /* ===== DARK MODE ===== */
[data-theme="dark"] { [data-theme="dark"] {
--bg: #0f172a; --bg: #0f172a;
+40
View File
@@ -2538,6 +2538,9 @@ async function loadSettingsUI() {
// Dark mode setting // Dark mode setting
const dmEl = document.getElementById('setting-dark-mode'); const dmEl = document.getElementById('setting-dark-mode');
if (dmEl) dmEl.value = s.dark_mode || 'auto'; if (dmEl) dmEl.value = s.dark_mode || 'auto';
// Zero-waste tips setting
const zwEl = document.getElementById('setting-zerowaste-tips');
if (zwEl) zwEl.checked = s.zerowaste_tips_enabled === true;
// Populate About section version // Populate About section version
_loadAboutSection(); _loadAboutSection();
@@ -2864,6 +2867,9 @@ async function saveSettings() {
// Dark mode // Dark mode
const dmSaveEl = document.getElementById('setting-dark-mode'); const dmSaveEl = document.getElementById('setting-dark-mode');
if (dmSaveEl) { s.dark_mode = dmSaveEl.value; _applyTheme(); } if (dmSaveEl) { s.dark_mode = dmSaveEl.value; _applyTheme(); }
// Zero-waste tips
const zwSaveEl = document.getElementById('setting-zerowaste-tips');
if (zwSaveEl) s.zerowaste_tips_enabled = zwSaveEl.checked;
// Meal plan enabled toggle // Meal plan enabled toggle
const mpEnabledEl = document.getElementById('setting-meal-plan-enabled'); const mpEnabledEl = document.getElementById('setting-meal-plan-enabled');
if (mpEnabledEl) s.meal_plan_enabled = mpEnabledEl.checked; if (mpEnabledEl) s.meal_plan_enabled = mpEnabledEl.checked;
@@ -12429,6 +12435,7 @@ function startCookingMode() {
_cookingRecipe = JSON.parse(JSON.stringify(recipe)); _cookingRecipe = JSON.parse(JSON.stringify(recipe));
_cookingStep = 0; _cookingStep = 0;
_cookingVisited = new Set(); _cookingVisited = new Set();
_dismissedZeroWasteTips = new Set();
clearAllCookingTimers(); clearAllCookingTimers();
} }
_cookingTTS = true; _cookingTTS = true;
@@ -12478,6 +12485,7 @@ function restartCookingMode() {
_cookingStep = 0; _cookingStep = 0;
_cookingWheelLastDelta = 0; _cookingWheelLastDelta = 0;
_cookingVisited = new Set(); _cookingVisited = new Set();
_dismissedZeroWasteTips = new Set();
clearAllCookingTimers(); clearAllCookingTimers();
renderCookingStep(); renderCookingStep();
} }
@@ -12699,10 +12707,42 @@ function renderCookingStep() {
// Timer: detect duration in step text and show suggestion // Timer: detect duration in step text and show suggestion
setupCookingTimerSuggestion(cleanStep); setupCookingTimerSuggestion(cleanStep);
// Zero-waste tip for this step
_renderZeroWasteTip(_cookingStep);
// TTS: auto-speak is handled by navigateCookingStep() and startCookingMode() callers. // TTS: auto-speak is handled by navigateCookingStep() and startCookingMode() callers.
// Use replayCookingTTS() to re-read the current step manually ("Rileggi" button). // Use replayCookingTTS() to re-read the current step manually ("Rileggi" button).
} }
// ===== ZERO-WASTE TIPS =====
let _dismissedZeroWasteTips = new Set(); // dismissed tip indices for this cooking session
function _renderZeroWasteTip(stepIdx) {
const tipEl = document.getElementById('cooking-zerowaste-tip');
if (!tipEl) return;
// Check setting
const s = getSettings();
if (!s.zerowaste_tips_enabled) { tipEl.style.display = 'none'; return; }
// Already dismissed for this step in this session
if (_dismissedZeroWasteTips.has(stepIdx)) { tipEl.style.display = 'none'; return; }
// Find tip for current step
const tips = (_cookingRecipe && _cookingRecipe.zero_waste_tips) || [];
const tip = tips.find(t => t.step === stepIdx);
if (!tip) { tipEl.style.display = 'none'; return; }
// Populate and show
const scrapEl = document.getElementById('cooking-zerowaste-scrap');
const textEl = document.getElementById('cooking-zerowaste-text');
if (scrapEl) scrapEl.textContent = tip.scrap || '';
if (textEl) textEl.textContent = tip.tip || '';
tipEl.style.display = 'flex';
}
function _dismissZeroWasteTip() {
_dismissedZeroWasteTips.add(_cookingStep);
const tipEl = document.getElementById('cooking-zerowaste-tip');
if (tipEl) tipEl.style.display = 'none';
}
function _buildTtsRequest(text, s) { function _buildTtsRequest(text, s) {
const url = s.tts_url || ''; const url = s.tts_url || '';
const method = s.tts_method || 'POST'; const method = s.tts_method || 'POST';
+21 -2
View File
@@ -11,7 +11,7 @@
<title>EverShelf</title> <title>EverShelf</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png"> <link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png">
<link rel="stylesheet" href="assets/css/style.css?v=20260519b"> <link rel="stylesheet" href="assets/css/style.css?v=20260519c">
<!-- QuaggaJS for barcode scanning --> <!-- QuaggaJS for barcode scanning -->
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
<!-- @xenova/transformers: ES-module bootstrap that exposes a lazy category-classifier as window._categoryPipelinePromise --> <!-- @xenova/transformers: ES-module bootstrap that exposes a lazy category-classifier as window._categoryPipelinePromise -->
@@ -1300,6 +1300,19 @@
</select> </select>
</div> </div>
</div> </div>
<div class="settings-card">
<h4 data-i18n="settings.zerowaste.card_title">♻️ Suggerimenti zero-waste</h4>
<p class="settings-hint" data-i18n="settings.zerowaste.card_hint">Durante la cottura, mostra consigli su come riutilizzare gli scarti prodotti in ogni passo (bucce, acqua di cottura, ecc.). Disattivo per impostazione predefinita.</p>
<div class="form-group">
<label class="toggle-row">
<span data-i18n="settings.zerowaste.label">Mostra suggerimenti durante la cottura</span>
<span class="toggle-switch">
<input type="checkbox" id="setting-zerowaste-tips">
<span class="toggle-slider"></span>
</span>
</label>
</div>
</div>
<div class="settings-card"> <div class="settings-card">
<h4 data-i18n="export.title">📤 Esporta inventario</h4> <h4 data-i18n="export.title">📤 Esporta inventario</h4>
<p class="settings-hint" data-i18n="export.hint">Scarica l'inventario corrente in CSV o apri una versione stampabile (PDF).</p> <p class="settings-hint" data-i18n="export.hint">Scarica l'inventario corrente in CSV o apri una versione stampabile (PDF).</p>
@@ -1578,6 +1591,12 @@
</button> </button>
</div> </div>
<div class="cooking-step-ings" id="cooking-step-ings" style="display:none"></div> <div class="cooking-step-ings" id="cooking-step-ings" style="display:none"></div>
<div id="cooking-zerowaste-tip" class="cooking-zerowaste-tip" style="display:none">
<span class="cooking-zerowaste-label" data-i18n="cooking.zerowaste_label">♻️ Scarto</span>
<span id="cooking-zerowaste-scrap" class="cooking-zerowaste-scrap"></span>
<p id="cooking-zerowaste-text" class="cooking-zerowaste-text"></p>
<button class="cooking-zerowaste-close" onclick="_dismissZeroWasteTip()" aria-label="Chiudi"></button>
</div>
</div> </div>
<div class="cooking-nav"> <div class="cooking-nav">
<button class="cooking-nav-btn cooking-prev-btn" id="cooking-prev" onclick="navigateCookingStep(-1)">◀ Precedente</button> <button class="cooking-nav-btn cooking-prev-btn" id="cooking-prev" onclick="navigateCookingStep(-1)">◀ Precedente</button>
@@ -1585,6 +1604,6 @@
</div> </div>
</div> </div>
<script src="assets/js/app.js?v=20260519b"></script> <script src="assets/js/app.js?v=20260519c"></script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "EverShelf", "name": "EverShelf",
"short_name": "EverShelf", "short_name": "EverShelf",
"description": "Gestione completa della dispensa di casa con scansione barcode", "description": "Gestione completa della dispensa di casa con scansione barcode",
"version": "1.7.18", "version": "1.7.19",
"start_url": "/evershelf/", "start_url": "/evershelf/",
"display": "standalone", "display": "standalone",
"background_color": "#f0f4e8", "background_color": "#f0f4e8",
+8 -1
View File
@@ -540,7 +540,9 @@
"recipe_done_tts": "Rezept abgeschlossen! Guten Appetit!", "recipe_done_tts": "Rezept abgeschlossen! Guten Appetit!",
"expires_chip": "läuft ab {date}", "expires_chip": "läuft ab {date}",
"finish": "✅ Fertig", "finish": "✅ Fertig",
"step_fallback": "Schritt {n}" "step_fallback": "Schritt {n}",
"zerowaste_label": "♻️ Abfall",
"zerowaste_tip_title": "Zero-Waste-Tipp"
}, },
"settings": { "settings": {
"title": "⚙️ Einstellungen", "title": "⚙️ Einstellungen",
@@ -752,6 +754,11 @@
"off": "☀️ Hell", "off": "☀️ Hell",
"on": "🌙 Dunkel", "on": "🌙 Dunkel",
"auto": "🔄 Automatisch (System)" "auto": "🔄 Automatisch (System)"
},
"zerowaste": {
"card_title": "♻️ Zero-Waste-Tipps",
"card_hint": "Zeige während des Kochens Tipps zur Wiederverwendung von Abfällen (Schalen, Kochwasser usw.). Standardmäßig deaktiviert.",
"label": "Tipps beim Kochen anzeigen"
} }
}, },
"expiry": { "expiry": {
+8 -1
View File
@@ -540,7 +540,9 @@
"recipe_done_tts": "Recipe complete! Enjoy your meal!", "recipe_done_tts": "Recipe complete! Enjoy your meal!",
"expires_chip": "exp. {date}", "expires_chip": "exp. {date}",
"finish": "✅ Finish", "finish": "✅ Finish",
"step_fallback": "Step {n}" "step_fallback": "Step {n}",
"zerowaste_label": "♻️ Scrap",
"zerowaste_tip_title": "Zero-waste tip"
}, },
"settings": { "settings": {
"title": "⚙️ Settings", "title": "⚙️ Settings",
@@ -752,6 +754,11 @@
"off": "☀️ Light", "off": "☀️ Light",
"on": "🌙 Dark", "on": "🌙 Dark",
"auto": "🔄 Auto (system)" "auto": "🔄 Auto (system)"
},
"zerowaste": {
"card_title": "♻️ Zero-waste tips",
"card_hint": "During cooking, show tips on how to reuse scraps generated in each step (peels, cooking water, etc.). Disabled by default.",
"label": "Show tips during cooking"
} }
}, },
"expiry": { "expiry": {
+8 -1
View File
@@ -540,7 +540,9 @@
"recipe_done_tts": "¡Receta completada! ¡Buen provecho!", "recipe_done_tts": "¡Receta completada! ¡Buen provecho!",
"expires_chip": "cad. {date}", "expires_chip": "cad. {date}",
"finish": "✅ Finalizar", "finish": "✅ Finalizar",
"step_fallback": "Paso {n}" "step_fallback": "Paso {n}",
"zerowaste_label": "♻️ Desperdicio",
"zerowaste_tip_title": "Consejo sin desperdicios"
}, },
"settings": { "settings": {
"title": "⚙️ Ajustes", "title": "⚙️ Ajustes",
@@ -752,6 +754,11 @@
"off": "☀️ Claro", "off": "☀️ Claro",
"on": "🌙 Oscuro", "on": "🌙 Oscuro",
"auto": "🔄 Automático (sistema)" "auto": "🔄 Automático (sistema)"
},
"zerowaste": {
"card_title": "♻️ Consejos sin desperdicios",
"card_hint": "Durante la cocción, muestra consejos sobre cómo reutilizar los restos generados en cada paso (peladuras, agua de cocción, etc.). Desactivado por defecto.",
"label": "Mostrar consejos durante la cocción"
} }
}, },
"expiry": { "expiry": {
+8 -1
View File
@@ -540,7 +540,9 @@
"recipe_done_tts": "Recette terminée ! Bon appétit !", "recipe_done_tts": "Recette terminée ! Bon appétit !",
"expires_chip": "exp. {date}", "expires_chip": "exp. {date}",
"finish": "✅ Terminer", "finish": "✅ Terminer",
"step_fallback": "Étape {n}" "step_fallback": "Étape {n}",
"zerowaste_label": "♻️ Déchet",
"zerowaste_tip_title": "Conseil zéro déchet"
}, },
"settings": { "settings": {
"title": "⚙️ Paramètres", "title": "⚙️ Paramètres",
@@ -752,6 +754,11 @@
"off": "☀️ Clair", "off": "☀️ Clair",
"on": "🌙 Sombre", "on": "🌙 Sombre",
"auto": "🔄 Automatique (système)" "auto": "🔄 Automatique (système)"
},
"zerowaste": {
"card_title": "♻️ Conseils zéro déchet",
"card_hint": "Pendant la cuisson, affichez des conseils pour réutiliser les déchets produits à chaque étape (épluchures, eau de cuisson, etc.). Désactivé par défaut.",
"label": "Afficher les conseils pendant la cuisson"
} }
}, },
"expiry": { "expiry": {
+8 -1
View File
@@ -540,7 +540,9 @@
"recipe_done_tts": "Ricetta completata! Buon appetito!", "recipe_done_tts": "Ricetta completata! Buon appetito!",
"expires_chip": "scade {date}", "expires_chip": "scade {date}",
"finish": "✅ Fine", "finish": "✅ Fine",
"step_fallback": "Passo {n}" "step_fallback": "Passo {n}",
"zerowaste_label": "♻️ Scarto",
"zerowaste_tip_title": "Consiglio anti-spreco"
}, },
"settings": { "settings": {
"title": "⚙️ Configurazione", "title": "⚙️ Configurazione",
@@ -752,6 +754,11 @@
"off": "☀️ Chiaro", "off": "☀️ Chiaro",
"on": "🌙 Scuro", "on": "🌙 Scuro",
"auto": "🔄 Automatico (sistema)" "auto": "🔄 Automatico (sistema)"
},
"zerowaste": {
"card_title": "♻️ Suggerimenti zero-waste",
"card_hint": "Durante la cottura, mostra consigli su come riutilizzare gli scarti prodotti in ogni passo (bucce, acqua di cottura, ecc.). Disattivo per impostazione predefinita.",
"label": "Mostra suggerimenti durante la cottura"
} }
}, },
"expiry": { "expiry": {