fix: step.replace is not a function when Gemini returns steps as objects
- 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
This commit is contained in:
+8
-1
@@ -5571,7 +5571,14 @@ PROMPT;
|
|||||||
return;
|
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'])) {
|
if (!empty($recipe['ingredients'])) {
|
||||||
$itemsLookup = [];
|
$itemsLookup = [];
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
|
|||||||
+13
-9
@@ -13161,8 +13161,7 @@ function renderRecipe(r) {
|
|||||||
// Steps
|
// Steps
|
||||||
html += `<h3>${t('recipes.steps_title')}</h3><ol>`;
|
html += `<h3>${t('recipes.steps_title')}</h3><ol>`;
|
||||||
(r.steps || []).forEach(step => {
|
(r.steps || []).forEach(step => {
|
||||||
const cleanStep = step.replace(/^Passo\s*\d+\s*:\s*/i, '');
|
html += `<li>${_stepStr(step)}</li>`;
|
||||||
html += `<li>${cleanStep}</li>`;
|
|
||||||
});
|
});
|
||||||
html += '</ol>';
|
html += '</ol>';
|
||||||
|
|
||||||
@@ -13179,6 +13178,11 @@ let _cookingRecipe = null;
|
|||||||
let _cookingStep = 0;
|
let _cookingStep = 0;
|
||||||
let _cookingTTS = true;
|
let _cookingTTS = true;
|
||||||
let _cookingVisited = new Set(); // indices of steps already seen
|
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 _cookingWheelBound = false;
|
||||||
let _cookingWheelTouchStartY = null;
|
let _cookingWheelTouchStartY = null;
|
||||||
let _cookingWheelLastNavTs = 0;
|
let _cookingWheelLastNavTs = 0;
|
||||||
@@ -13281,7 +13285,7 @@ function startCookingMode() {
|
|||||||
try { screen.orientation?.lock('portrait').catch(() => {}); } catch (_) { /* ignore */ }
|
try { screen.orientation?.lock('portrait').catch(() => {}); } catch (_) { /* ignore */ }
|
||||||
renderCookingStep();
|
renderCookingStep();
|
||||||
if (_cookingTTS) {
|
if (_cookingTTS) {
|
||||||
const text = ((_cookingRecipe.steps || [])[_cookingStep] || '').replace(/^Passo\s*\d+\s*[:.]\s*/i, '');
|
const text = _stepStr((_cookingRecipe.steps || [])[_cookingStep]);
|
||||||
speakCookingStep(text);
|
speakCookingStep(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13432,7 +13436,7 @@ function renderCookingStep() {
|
|||||||
if (!_cookingRecipe) return;
|
if (!_cookingRecipe) return;
|
||||||
const steps = _cookingRecipe.steps || [];
|
const steps = _cookingRecipe.steps || [];
|
||||||
const step = steps[_cookingStep] || '';
|
const step = steps[_cookingStep] || '';
|
||||||
const cleanStep = step.replace(/^Passo\s*\d+\s*[:.]\s*/i, '');
|
const cleanStep = _stepStr(step);
|
||||||
const total = steps.length;
|
const total = steps.length;
|
||||||
|
|
||||||
// Mark current step as visited
|
// Mark current step as visited
|
||||||
@@ -13445,7 +13449,7 @@ function renderCookingStep() {
|
|||||||
const nextEl = document.getElementById('cooking-step-next');
|
const nextEl = document.getElementById('cooking-step-next');
|
||||||
if (prevEl) {
|
if (prevEl) {
|
||||||
if (_cookingStep > 0) {
|
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');
|
prevEl.classList.remove('is-empty');
|
||||||
} else {
|
} else {
|
||||||
prevEl.textContent = '';
|
prevEl.textContent = '';
|
||||||
@@ -13454,7 +13458,7 @@ function renderCookingStep() {
|
|||||||
}
|
}
|
||||||
if (nextEl) {
|
if (nextEl) {
|
||||||
if (_cookingStep < total - 1) {
|
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');
|
nextEl.classList.remove('is-empty');
|
||||||
} else {
|
} else {
|
||||||
nextEl.textContent = '';
|
nextEl.textContent = '';
|
||||||
@@ -13617,7 +13621,7 @@ async function speakCookingStep(text) {
|
|||||||
function replayCookingTTS() {
|
function replayCookingTTS() {
|
||||||
if (!_cookingRecipe) return;
|
if (!_cookingRecipe) return;
|
||||||
const steps = _cookingRecipe.steps || [];
|
const steps = _cookingRecipe.steps || [];
|
||||||
const text = (steps[_cookingStep] || '').replace(/^Passo\s*\d+\s*[:.]\s*/i, '');
|
const text = _stepStr(steps[_cookingStep]);
|
||||||
if (text) speakCookingStep(text);
|
if (text) speakCookingStep(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14180,7 +14184,7 @@ function toggleCookingTTS() {
|
|||||||
btn.textContent = _cookingTTS ? '🔊' : '🔇';
|
btn.textContent = _cookingTTS ? '🔊' : '🔇';
|
||||||
if (_cookingTTS) {
|
if (_cookingTTS) {
|
||||||
const steps = _cookingRecipe?.steps || [];
|
const steps = _cookingRecipe?.steps || [];
|
||||||
const text = (steps[_cookingStep] || '').replace(/^Passo\s*\d+\s*[:.]\s*/i, '');
|
const text = _stepStr(steps[_cookingStep]);
|
||||||
speakCookingStep(text);
|
speakCookingStep(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14205,7 +14209,7 @@ function navigateCookingStep(delta) {
|
|||||||
renderCookingStep();
|
renderCookingStep();
|
||||||
_cookingStepFeedback();
|
_cookingStepFeedback();
|
||||||
if (_cookingTTS) {
|
if (_cookingTTS) {
|
||||||
const text = ((_cookingRecipe.steps || [])[_cookingStep] || '').replace(/^Passo\s*\d+\s*[:.]\s*/i, '');
|
const text = _stepStr((_cookingRecipe.steps || [])[_cookingStep]);
|
||||||
speakCookingStep(text);
|
speakCookingStep(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user