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
This commit is contained in:
+3
-1
@@ -5152,7 +5152,8 @@ REGOLE:
|
|||||||
5. "name": usa ESATTAMENTE il nome dalla lista (il sistema lo usa per scalare l'inventario).
|
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).
|
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","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:
|
DISPENSA:
|
||||||
$ingredientsText
|
$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.
|
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.
|
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:
|
DISPENSA:
|
||||||
$ingredientsText
|
$ingredientsText
|
||||||
|
|||||||
@@ -4276,6 +4276,19 @@ body.server-offline .bottom-nav {
|
|||||||
color: #3730a3;
|
color: #3730a3;
|
||||||
white-space: nowrap;
|
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 ingredient use buttons */
|
||||||
.recipe-ingredients {
|
.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-expiry-note { background: #2a1e00; color: #fde68a; }
|
||||||
[data-theme="dark"] .recipe-tools-banner { background: #1a1040; border-color: #3730a3; color: #c4b5fd; }
|
[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-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 { background: #1c1300; border-color: #78350f; color: var(--text); }
|
||||||
[data-theme="dark"] .recipe-subtype-chip:has(input:checked) { background: #2a1e00; border-color: #d97706; }
|
[data-theme="dark"] .recipe-subtype-chip:has(input:checked) { background: #2a1e00; border-color: #d97706; }
|
||||||
|
|
||||||
|
|||||||
+25
-4
@@ -13273,7 +13273,8 @@ 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 => {
|
||||||
html += `<li>${_stepStr(step)}</li>`;
|
const appliance = _stepAppliance(step);
|
||||||
|
html += `<li>${_stepStr(step)}${appliance ? ` <span class="recipe-step-appliance">${appliance}</span>` : ''}</li>`;
|
||||||
});
|
});
|
||||||
html += '</ol>';
|
html += '</ol>';
|
||||||
|
|
||||||
@@ -13291,9 +13292,29 @@ 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
|
// Safely extract step text regardless of whether it's a string or an object.
|
||||||
// (Gemini sometimes returns [{text:"..."}, ...] instead of ["...", ...])
|
// Also handles JSON-encoded step objects emitted by older AI generations
|
||||||
const _stepStr = s => String((s !== null && typeof s === 'object') ? (s.text ?? s.description ?? s.step ?? '') : (s ?? '')).replace(/^Passo\s*\d+\s*[:.]\s*/i, '');
|
// (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 _cookingWheelBound = false;
|
||||||
let _cookingWheelTouchStartY = null;
|
let _cookingWheelTouchStartY = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user