feat: zero-waste tips during cooking mode (#76)
This commit is contained in:
@@ -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.
|
||||
|
||||
## [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
|
||||
|
||||
### Added
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
[](https://www.sqlite.org/)
|
||||
[](Dockerfile)
|
||||
[](translations/)
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](https://github.com/dadaloop82/EverShelf/stargazers)
|
||||
[](https://github.com/dadaloop82/EverShelf/commits/main)
|
||||
[](https://github.com/dadaloop82/EverShelf/graphs/contributors)
|
||||
|
||||
+2
-1
@@ -4458,13 +4458,14 @@ REGOLE:
|
||||
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.
|
||||
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:
|
||||
$ingredientsText
|
||||
|
||||
Rispondi SOLO JSON valido (no markdown):
|
||||
{$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;
|
||||
|
||||
$genConfig = [
|
||||
|
||||
@@ -6875,6 +6875,64 @@ body.cooking-mode-active .app-header {
|
||||
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 ===== */
|
||||
[data-theme="dark"] {
|
||||
--bg: #0f172a;
|
||||
|
||||
@@ -2538,6 +2538,9 @@ async function loadSettingsUI() {
|
||||
// Dark mode setting
|
||||
const dmEl = document.getElementById('setting-dark-mode');
|
||||
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
|
||||
_loadAboutSection();
|
||||
@@ -2864,6 +2867,9 @@ async function saveSettings() {
|
||||
// Dark mode
|
||||
const dmSaveEl = document.getElementById('setting-dark-mode');
|
||||
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
|
||||
const mpEnabledEl = document.getElementById('setting-meal-plan-enabled');
|
||||
if (mpEnabledEl) s.meal_plan_enabled = mpEnabledEl.checked;
|
||||
@@ -12429,6 +12435,7 @@ function startCookingMode() {
|
||||
_cookingRecipe = JSON.parse(JSON.stringify(recipe));
|
||||
_cookingStep = 0;
|
||||
_cookingVisited = new Set();
|
||||
_dismissedZeroWasteTips = new Set();
|
||||
clearAllCookingTimers();
|
||||
}
|
||||
_cookingTTS = true;
|
||||
@@ -12478,6 +12485,7 @@ function restartCookingMode() {
|
||||
_cookingStep = 0;
|
||||
_cookingWheelLastDelta = 0;
|
||||
_cookingVisited = new Set();
|
||||
_dismissedZeroWasteTips = new Set();
|
||||
clearAllCookingTimers();
|
||||
renderCookingStep();
|
||||
}
|
||||
@@ -12699,10 +12707,42 @@ function renderCookingStep() {
|
||||
// Timer: detect duration in step text and show suggestion
|
||||
setupCookingTimerSuggestion(cleanStep);
|
||||
|
||||
// Zero-waste tip for this step
|
||||
_renderZeroWasteTip(_cookingStep);
|
||||
|
||||
// TTS: auto-speak is handled by navigateCookingStep() and startCookingMode() callers.
|
||||
// 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) {
|
||||
const url = s.tts_url || '';
|
||||
const method = s.tts_method || 'POST';
|
||||
|
||||
+21
-2
@@ -11,7 +11,7 @@
|
||||
<title>EverShelf</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<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 -->
|
||||
<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 -->
|
||||
@@ -1300,6 +1300,19 @@
|
||||
</select>
|
||||
</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">
|
||||
<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>
|
||||
@@ -1578,6 +1591,12 @@
|
||||
</button>
|
||||
</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 class="cooking-nav">
|
||||
<button class="cooking-nav-btn cooking-prev-btn" id="cooking-prev" onclick="navigateCookingStep(-1)">◀ Precedente</button>
|
||||
@@ -1585,6 +1604,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/app.js?v=20260519b"></script>
|
||||
<script src="assets/js/app.js?v=20260519c"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "EverShelf",
|
||||
"short_name": "EverShelf",
|
||||
"description": "Gestione completa della dispensa di casa con scansione barcode",
|
||||
"version": "1.7.18",
|
||||
"version": "1.7.19",
|
||||
"start_url": "/evershelf/",
|
||||
"display": "standalone",
|
||||
"background_color": "#f0f4e8",
|
||||
|
||||
@@ -540,7 +540,9 @@
|
||||
"recipe_done_tts": "Rezept abgeschlossen! Guten Appetit!",
|
||||
"expires_chip": "läuft ab {date}",
|
||||
"finish": "✅ Fertig",
|
||||
"step_fallback": "Schritt {n}"
|
||||
"step_fallback": "Schritt {n}",
|
||||
"zerowaste_label": "♻️ Abfall",
|
||||
"zerowaste_tip_title": "Zero-Waste-Tipp"
|
||||
},
|
||||
"settings": {
|
||||
"title": "⚙️ Einstellungen",
|
||||
@@ -752,6 +754,11 @@
|
||||
"off": "☀️ Hell",
|
||||
"on": "🌙 Dunkel",
|
||||
"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": {
|
||||
|
||||
@@ -540,7 +540,9 @@
|
||||
"recipe_done_tts": "Recipe complete! Enjoy your meal!",
|
||||
"expires_chip": "exp. {date}",
|
||||
"finish": "✅ Finish",
|
||||
"step_fallback": "Step {n}"
|
||||
"step_fallback": "Step {n}",
|
||||
"zerowaste_label": "♻️ Scrap",
|
||||
"zerowaste_tip_title": "Zero-waste tip"
|
||||
},
|
||||
"settings": {
|
||||
"title": "⚙️ Settings",
|
||||
@@ -752,6 +754,11 @@
|
||||
"off": "☀️ Light",
|
||||
"on": "🌙 Dark",
|
||||
"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": {
|
||||
|
||||
@@ -540,7 +540,9 @@
|
||||
"recipe_done_tts": "¡Receta completada! ¡Buen provecho!",
|
||||
"expires_chip": "cad. {date}",
|
||||
"finish": "✅ Finalizar",
|
||||
"step_fallback": "Paso {n}"
|
||||
"step_fallback": "Paso {n}",
|
||||
"zerowaste_label": "♻️ Desperdicio",
|
||||
"zerowaste_tip_title": "Consejo sin desperdicios"
|
||||
},
|
||||
"settings": {
|
||||
"title": "⚙️ Ajustes",
|
||||
@@ -752,6 +754,11 @@
|
||||
"off": "☀️ Claro",
|
||||
"on": "🌙 Oscuro",
|
||||
"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": {
|
||||
|
||||
@@ -540,7 +540,9 @@
|
||||
"recipe_done_tts": "Recette terminée ! Bon appétit !",
|
||||
"expires_chip": "exp. {date}",
|
||||
"finish": "✅ Terminer",
|
||||
"step_fallback": "Étape {n}"
|
||||
"step_fallback": "Étape {n}",
|
||||
"zerowaste_label": "♻️ Déchet",
|
||||
"zerowaste_tip_title": "Conseil zéro déchet"
|
||||
},
|
||||
"settings": {
|
||||
"title": "⚙️ Paramètres",
|
||||
@@ -752,6 +754,11 @@
|
||||
"off": "☀️ Clair",
|
||||
"on": "🌙 Sombre",
|
||||
"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": {
|
||||
|
||||
@@ -540,7 +540,9 @@
|
||||
"recipe_done_tts": "Ricetta completata! Buon appetito!",
|
||||
"expires_chip": "scade {date}",
|
||||
"finish": "✅ Fine",
|
||||
"step_fallback": "Passo {n}"
|
||||
"step_fallback": "Passo {n}",
|
||||
"zerowaste_label": "♻️ Scarto",
|
||||
"zerowaste_tip_title": "Consiglio anti-spreco"
|
||||
},
|
||||
"settings": {
|
||||
"title": "⚙️ Configurazione",
|
||||
@@ -752,6 +754,11 @@
|
||||
"off": "☀️ Chiaro",
|
||||
"on": "🌙 Scuro",
|
||||
"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": {
|
||||
|
||||
Reference in New Issue
Block a user