From 52afdd6bfa96fd41d84072b857b79a255f499979 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Mon, 25 May 2026 10:01:10 +0000 Subject: [PATCH] fix(recipes): steps shown as raw JSON when AI uses instruction/appliance_function objects - _stepStr: parse JSON-string steps; handle s.instruction key (backward-compat with already-saved recipes) - _stepAppliance: new helper to extract appliance_function hint; returns null for 'Nessuno'/'None' - renderRecipe steps list: shows appliance badge inline after step text when present - CSS: .recipe-step-appliance badge (green chip, dark-mode variant) - Prompt (both generateRecipe + generateRecipeStream): rule 9/10 explicitly forbids step objects; appliance info must be embedded in the step text string directly --- api/index.php | 4 +++- assets/css/style.css | 14 ++++++++++++++ assets/js/app.js | 29 +++++++++++++++++++++++++---- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/api/index.php b/api/index.php index 7326559..254c1bb 100644 --- a/api/index.php +++ b/api/index.php @@ -5152,7 +5152,8 @@ REGOLE: 5. "name": usa ESATTAMENTE il nome dalla lista (il sistema lo usa per scalare l'inventario). 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. +8. `tools_needed`: array of kitchen tools/appliances actually required by this recipe (e.g. ["Forno","Frullateur"]). Use the same language as all other text fields. Empty array [] if only stovetop/knife/pan needed. +9. `steps`: array of PLAIN TEXT STRINGS only — no objects, no JSON, no sub-fields. Each step is a single readable string. If appliances are used, include the appliance/mode information directly in the step text (e.g. "Nel Cookeo, modalità Rosolare: aggiungere la cipolla…"). NEVER output steps as objects like {"instruction":…, "appliance_function":…}. DISPENSA: $ingredientsText @@ -6099,6 +6100,7 @@ REGOLE: 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. +10. `steps`: array of PLAIN TEXT STRINGS only — no objects, no JSON, no sub-fields. Each step is a single readable string. If appliances are used, include the appliance/mode information directly in the step text (e.g. "Nel Cookeo, modalità Rosolare: aggiungere la cipolla…"). NEVER output steps as objects like {"instruction":…, "appliance_function":…}. DISPENSA: $ingredientsText diff --git a/assets/css/style.css b/assets/css/style.css index 3d2880d..d953de5 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -4276,6 +4276,19 @@ body.server-offline .bottom-nav { color: #3730a3; white-space: nowrap; } +/* Appliance/mode badge shown inline next to a step text */ +.recipe-step-appliance { + display: inline-block; + margin-left: 6px; + background: #f0fdf4; + border: 1px solid #bbf7d0; + border-radius: 12px; + padding: 1px 8px; + font-size: 0.72rem; + color: #15803d; + vertical-align: middle; + white-space: nowrap; +} /* Recipe ingredient use buttons */ .recipe-ingredients { @@ -7653,6 +7666,7 @@ body.cooking-mode-active .app-header { [data-theme="dark"] .recipe-expiry-note { background: #2a1e00; color: #fde68a; } [data-theme="dark"] .recipe-tools-banner { background: #1a1040; border-color: #3730a3; color: #c4b5fd; } [data-theme="dark"] .recipe-tool-chip { background: #2e1a4a; color: #c4b5fd; } +[data-theme="dark"] .recipe-step-appliance { background: #052e16; border-color: #166534; color: #4ade80; } [data-theme="dark"] .recipe-subtype-chip { background: #1c1300; border-color: #78350f; color: var(--text); } [data-theme="dark"] .recipe-subtype-chip:has(input:checked) { background: #2a1e00; border-color: #d97706; } diff --git a/assets/js/app.js b/assets/js/app.js index 2f3fd57..160de47 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -13273,7 +13273,8 @@ function renderRecipe(r) { // Steps html += `

${t('recipes.steps_title')}

    `; (r.steps || []).forEach(step => { - html += `
  1. ${_stepStr(step)}
  2. `; + const appliance = _stepAppliance(step); + html += `
  3. ${_stepStr(step)}${appliance ? ` ${appliance}` : ''}
  4. `; }); html += '
'; @@ -13291,9 +13292,29 @@ let _cookingStep = 0; let _cookingTTS = true; let _cookingVisited = new Set(); // indices of steps already seen -// Safely extract step text regardless of whether it's a string or an object -// (Gemini sometimes returns [{text:"..."}, ...] instead of ["...", ...]) -const _stepStr = s => String((s !== null && typeof s === 'object') ? (s.text ?? s.description ?? s.step ?? '') : (s ?? '')).replace(/^Passo\s*\d+\s*[:.]\s*/i, ''); +// Safely extract step text regardless of whether it's a string or an object. +// Also handles JSON-encoded step objects emitted by older AI generations +// (e.g. {"instruction":"…","appliance_function":"…"}). +const _stepStr = s => { + if (typeof s === 'string' && s.trimStart().startsWith('{')) { + try { s = JSON.parse(s); } catch(e) {} + } + const text = (s !== null && typeof s === 'object') + ? (s.instruction ?? s.text ?? s.description ?? s.step ?? '') + : (s ?? ''); + return String(text).replace(/^Passo\s*\d+\s*[:.]\s*/i, '').replace(/^Step\s*\d+\s*[:.]\s*/i, ''); +}; +// Returns the appliance/function hint for a step, or null if absent/Nessuno. +const _stepAppliance = s => { + if (typeof s === 'string' && s.trimStart().startsWith('{')) { + try { s = JSON.parse(s); } catch(e) {} + } + if (s !== null && typeof s === 'object' && s.appliance_function) { + const a = s.appliance_function.trim(); + if (a && a.toLowerCase() !== 'nessuno' && a.toLowerCase() !== 'none') return a; + } + return null; +}; let _cookingWheelBound = false; let _cookingWheelTouchStartY = null;