From 3989d1109400119d8977f3485d228286deb84276 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Sat, 23 May 2026 11:55:55 +0000 Subject: [PATCH] fix: step.replace is not a function when Gemini returns steps as objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PHP generateRecipeStream: normalize recipe.steps to plain strings after parsing Gemini JSON (handles [{text:'...'}, ...] objects gracefully) - JS: add _stepStr(s) helper near cooking mode — safely extracts text from a step regardless of type (string or object {text/description/step key}) and strips leading 'Passo N:' prefix in one place - JS: replace all 7 manual step.replace(/^Passo.../) calls with _stepStr() across renderRecipe, renderCookingStep, startCookingMode, replayCookingTTS, toggleCookingTTS, navigateCookingStep — no more crash if Gemini schema drifts --- api/index.php | 9 ++++++++- assets/js/app.js | 22 +++++++++++++--------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/api/index.php b/api/index.php index fec784a..dddc0ee 100644 --- a/api/index.php +++ b/api/index.php @@ -5571,7 +5571,14 @@ PROMPT; return; } - // ── Post-process: fuzzy-match ingredients → inventory (same as generateRecipe) ── + // Normalize steps: Gemini sometimes returns [{"text":"..."}, ...] instead of ["...", ...] + if (!empty($recipe['steps']) && is_array($recipe['steps'])) { + $recipe['steps'] = array_values(array_map(function($s) { + if (is_string($s)) return $s; + if (is_array($s)) return $s['text'] ?? $s['description'] ?? $s['step'] ?? json_encode($s, JSON_UNESCAPED_UNICODE); + return (string)$s; + }, $recipe['steps'])); + } if (!empty($recipe['ingredients'])) { $itemsLookup = []; foreach ($items as $item) { diff --git a/assets/js/app.js b/assets/js/app.js index 58de99b..8fbc44e 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -13161,8 +13161,7 @@ function renderRecipe(r) { // Steps html += `

${t('recipes.steps_title')}

    `; (r.steps || []).forEach(step => { - const cleanStep = step.replace(/^Passo\s*\d+\s*:\s*/i, ''); - html += `
  1. ${cleanStep}
  2. `; + html += `
  3. ${_stepStr(step)}
  4. `; }); html += '
'; @@ -13179,6 +13178,11 @@ let _cookingRecipe = null; 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, ''); + let _cookingWheelBound = false; let _cookingWheelTouchStartY = null; let _cookingWheelLastNavTs = 0; @@ -13281,7 +13285,7 @@ function startCookingMode() { try { screen.orientation?.lock('portrait').catch(() => {}); } catch (_) { /* ignore */ } renderCookingStep(); if (_cookingTTS) { - const text = ((_cookingRecipe.steps || [])[_cookingStep] || '').replace(/^Passo\s*\d+\s*[:.]\s*/i, ''); + const text = _stepStr((_cookingRecipe.steps || [])[_cookingStep]); speakCookingStep(text); } } @@ -13432,7 +13436,7 @@ function renderCookingStep() { if (!_cookingRecipe) return; const steps = _cookingRecipe.steps || []; const step = steps[_cookingStep] || ''; - const cleanStep = step.replace(/^Passo\s*\d+\s*[:.]\s*/i, ''); + const cleanStep = _stepStr(step); const total = steps.length; // Mark current step as visited @@ -13445,7 +13449,7 @@ function renderCookingStep() { const nextEl = document.getElementById('cooking-step-next'); if (prevEl) { if (_cookingStep > 0) { - prevEl.textContent = (steps[_cookingStep - 1] || '').replace(/^Passo\s*\d+\s*[:.]\s*/i, ''); + prevEl.textContent = _stepStr(steps[_cookingStep - 1]); prevEl.classList.remove('is-empty'); } else { prevEl.textContent = ''; @@ -13454,7 +13458,7 @@ function renderCookingStep() { } if (nextEl) { if (_cookingStep < total - 1) { - nextEl.textContent = (steps[_cookingStep + 1] || '').replace(/^Passo\s*\d+\s*[:.]\s*/i, ''); + nextEl.textContent = _stepStr(steps[_cookingStep + 1]); nextEl.classList.remove('is-empty'); } else { nextEl.textContent = ''; @@ -13617,7 +13621,7 @@ async function speakCookingStep(text) { function replayCookingTTS() { if (!_cookingRecipe) return; const steps = _cookingRecipe.steps || []; - const text = (steps[_cookingStep] || '').replace(/^Passo\s*\d+\s*[:.]\s*/i, ''); + const text = _stepStr(steps[_cookingStep]); if (text) speakCookingStep(text); } @@ -14180,7 +14184,7 @@ function toggleCookingTTS() { btn.textContent = _cookingTTS ? '🔊' : '🔇'; if (_cookingTTS) { const steps = _cookingRecipe?.steps || []; - const text = (steps[_cookingStep] || '').replace(/^Passo\s*\d+\s*[:.]\s*/i, ''); + const text = _stepStr(steps[_cookingStep]); speakCookingStep(text); } } @@ -14205,7 +14209,7 @@ function navigateCookingStep(delta) { renderCookingStep(); _cookingStepFeedback(); if (_cookingTTS) { - const text = ((_cookingRecipe.steps || [])[_cookingStep] || '').replace(/^Passo\s*\d+\s*[:.]\s*/i, ''); + const text = _stepStr((_cookingRecipe.steps || [])[_cookingStep]); speakCookingStep(text); } }