import React, { useState, useEffect, useRef, useMemo } from 'react'; import { Utensils, Sparkles, ChefHat, Timer, Flame, Wine, Code, CheckCircle2, Circle, Play, Pause, RotateCcw, Copy, Check, Dices, Award, Heart, BookOpen, ChevronRight, Sliders, RefreshCw, Info, ShoppingBag, Zap, ShieldAlert, Sparkle, Download, Star, ExternalLink } from 'lucide-react'; // Pre-loaded sample recipe to ensure immediate interactivity const SAMPLE_RECIPE = { titel: "Krämig Kantarellpasta med Vitlök & Västerbottensost", beskrivning: "En lyxig och doftande svensk pastarätt med smörstekta kantareller, färsk timjan, vitlök och fyllig Västerbottensost.", kök: "Svensk husmanskost / Italienskt fusion", diet: "Vegetarisk", tillagningstid_min: 25, portionsstorlek: 2, svårighetsgrad: "Medel", näringsvärde: { kalorier: 640, protein: 22, kolhydrater: 68, fett: 31 }, ingredienser: [ { namn: "Tagliatelle eller Pappardelle", mängd: 250, enhet: "g" }, { namn: "Färska eller frysta kantareller", mängd: 300, enhet: "g" }, { namn: "Schalottenlök (finhackad)", mängd: 2, enhet: "st" }, { namn: "Vitlöksklyftor (pressade)", mängd: 2, enhet: "st" }, { namn: "Vispgrädde", mängd: 2, enhet: "dl" }, { namn: "Riven Västerbottensost", mängd: 1.5, enhet: "dl" }, { namn: "Smör till stekning", mängd: 2, enhet: "msk" }, { namn: "Färsk timjan", mängd: 2, enhet: "kvistar" }, { namn: "Flingsalt & nymalen svartpeppar", mängd: 1, enhet: "nypa" } ], instruktioner: [ { steg_nummer: 1, rubrik: "Rensa och förbered kantarellerna", beskrivning: "Rensa svampen noga. Dela större kantareller i mindre bitar. Finhacka schalottenlöken och vitlöken.", timer_minuter: 5 }, { steg_nummer: 2, rubrik: "Torrstek svampen & tillsätt smör", beskrivning: "Lägg kantarellerna i en varm torr panna så att vätskan ångar bort. Tillsätt sedan smör, lök och vitlök. Fräs tills löken är mjuk och svampen fått fin gyllene färg.", timer_minuter: 7 }, { steg_nummer: 3, rubrik: "Koka pastan", beskrivning: "Koka pastan i rikligt med välsaltat vatten enligt anvisning på förpackningen tills den är 'al dente'. Spara 1 dl pastavatten innan du häller av.", timer_minuter: 9 }, { steg_nummer: 4, rubrik: "Skapa såsen och montera", beskrivning: "Häll i grädde och repad timjan i svamppannan. Låt sjuda ihop 2-3 minuter. Rör ner pastan tillsammans med riven Västerbottensost och lite pastavatten för perfekt krämighet.", timer_minuter: 3 }, { steg_nummer: 5, rubrik: "Smaka av och servera", beskrivning: "Smaka av med flingsalt och rikligt med nymalen svartpeppar. Garnera med färsk timjan och extra ost vid servering.", timer_minuter: 0 } ], kockens_tips: "För extra djup i smaken, tillsätt en skvätt torrt vitt vin i svampfräset precis innan grädden hälls på!", dryckestips: "Ett kylt glas torrt vitt vin som Chardonnay med fatkaraktär, eller en ljus svensk hantverksöl." }; // Helper to play notification sound for active step timer const playAlarmSound = () => { try { const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(587.33, audioCtx.currentTime); // D5 osc.frequency.setValueAtTime(880, audioCtx.currentTime + 0.15); // A5 gain.gain.setValueAtTime(0.3, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.6); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + 0.6); } catch (e) { console.log("Audio play blocked or unsupported"); } }; // Exponential backoff API fetch helper async function fetchWithRetry(url, options, retries = 3, backoff = 1000) { for (let i = 0; i < retries; i++) { try { const res = await fetch(url, options); if (res.ok) return res; const errText = await res.text(); console.warn(`Attempt ${i + 1} failed: ${res.status}`, errText); } catch (err) { if (i === retries - 1) throw err; } await new Promise(r => setTimeout(r, backoff * Math.pow(2, i))); } throw new Error("Kunde inte ansluta till AI-tjänsten. Kontrollera nätverket och försök igen."); } export default function App() { const [activeTab, setActiveTab] = useState('generator'); // 'generator', 'recipe', 'inspector', 'game', 'saved' const [recipeMode, setRecipeMode] = useState('visual'); // 'visual' | 'json' // Generator Form States const [cuisines, setCuisines] = useState(['Italienskt']); const [diet, setDiet] = useState('Alla (Ingen specifik)'); const [cookingTime, setCookingTime] = useState('30 minuter'); const [difficulty, setDifficulty] = useState('Medel'); const [servings, setServings] = useState(2); const [fridgeItems, setFridgeItems] = useState(['Vitlök', 'Grädde', 'Kantareller']); const [newFridgeItem, setNewFridgeItem] = useState(''); const [allergies, setAllergies] = useState(''); const [customPrompt, setCustomPrompt] = useState(''); // Generated Recipe & Meta State const [currentRecipe, setCurrentRecipe] = useState(SAMPLE_RECIPE); const [recipeImage, setRecipeImage] = useState(null); const [isGenerating, setIsGenerating] = useState(false); const [isGeneratingImage, setIsGeneratingImage] = useState(false); const [generatorError, setGeneratorError] = useState(null); const [rawJsonResponse, setRawJsonResponse] = useState(JSON.stringify(SAMPLE_RECIPE, null, 2)); const [lastApiPayload, setLastApiPayload] = useState(null); // Active Cooking Interactive State const [portionMultiplier, setPortionMultiplier] = useState(1); const [checkedIngredients, setCheckedIngredients] = useState({}); const [activeTimers, setActiveTimers] = useState({}); // { stepNum: { secondsLeft, isRunning } } // Saved Recipes Collection const [savedRecipes, setSavedRecipes] = useState(() => { try { const local = localStorage.getItem('gourmet_saved_recipes'); return local ? JSON.parse(local) : [SAMPLE_RECIPE]; } catch (e) { return [SAMPLE_RECIPE]; } }); // UI Toast State const [toastMessage, setToastMessage] = useState(null); const showToast = (msg) => { setToastMessage(msg); setTimeout(() => setToastMessage(null), 3000); }; useEffect(() => { const interval = setInterval(() => { setActiveTimers(prevTimers => { let updated = false; const newTimers = { ...prevTimers }; Object.keys(newTimers).forEach(stepKey => { const timer = newTimers[stepKey]; if (timer.isRunning && timer.secondsLeft > 0) { updated = true; newTimers[stepKey] = { ...timer, secondsLeft: timer.secondsLeft - 1 }; if (timer.secondsLeft - 1 === 0) { playAlarmSound(); showToast(` Timern för Steg ${stepKey} har slutat!`); } } }); return updated ? newTimers : prevTimers; }); }, 1000); return () => clearInterval(interval); }, []); const handleAddFridgeItem = (e) => { if (e) e.preventDefault(); if (!newFridgeItem.trim()) return; if (!fridgeItems.includes(newFridgeItem.trim())) { setFridgeItems([...fridgeItems, newFridgeItem.trim()]); } setNewFridgeItem(''); }; const handleRemoveFridgeItem = (item) => { setFridgeItems(fridgeItems.filter(i => i !== item)); }; const handleGenerateRecipe = async () => { setIsGenerating(true); setGeneratorError(null); setRecipeImage(null); const systemPrompt = `Du är en backend-tjänst för en recept-app. Din uppgift är att generera ett recept baserat på användarens inställningar och returnera det i exakt JSON-format. REGLER: 1. Svara ENBART med giltig JSON enligt angivet schema. Ingen extra text före eller efter. 2. Alla textfält ska vara på svenska. 3. Anpassa råvaror och tillagning strikt efter valda näringsmål och kök. 4. Generera aptitretande, realistiska och kulinariskt korrekta recept med tydliga steg.`; const userInstructions = ` Önskat Kök: ${cuisines.join(', ')} Näringsmål/Diet: ${diet} Tillagningstid max: ${cookingTime} Svårighetsgrad: ${difficulty} Antal portioner: ${servings} Råvaror i kylskåpet att prioritera: ${fridgeItems.length > 0 ? fridgeItems.join(', ') : 'Valfritt'} Allergier/Undvik: ${allergies || 'Inga'} Övrigt önskemål: ${customPrompt || 'Inget'} `; const jsonSchema = { type: "OBJECT", properties: { titel: { type: "STRING" }, beskrivning: { type: "STRING" }, kök: { type: "STRING" }, diet: { type: "STRING" }, tillagningstid_min: { type: "INTEGER" }, portionsstorlek: { type: "INTEGER" }, svårighetsgrad: { type: "STRING" }, näringsvärde: { type: "OBJECT", properties: { kalorier: { type: "INTEGER" }, protein: { type: "INTEGER" }, kolhydrater: { type: "INTEGER" }, fett: { type: "INTEGER" } }, required: ["kalorier", "protein", "kolhydrater", "fett"] }, ingredienser: { type: "ARRAY", items: { type: "OBJECT", properties: { namn: { type: "STRING" }, mängd: { type: "NUMBER" }, enhet: { type: "STRING" } }, required: ["namn", "mängd", "enhet"] } }, instruktioner: { type: "ARRAY", items: { type: "OBJECT", properties: { steg_nummer: { type: "INTEGER" }, rubrik: { type: "STRING" }, beskrivning: { type: "STRING" }, timer_minuter: { type: "INTEGER" } }, required: ["steg_nummer", "rubrik", "beskrivning"] } }, kockens_tips: { type: "STRING" }, dryckestips: { type: "STRING" } }, required: [ "titel", "beskrivning", "kök", "diet", "tillagningstid_min", "portionsstorlek", "svårighetsgrad", "näringsvärde", "ingredienser", "instruktioner", "kockens_tips", "dryckestips" ] }; const apiKey = ""; const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent?key=${apiKey}`; const payload = { contents: [{ parts: [{ text: userInstructions }] }], systemInstruction: { parts: [{ text: systemPrompt }] }, generationConfig: { responseMimeType: "application/json", responseSchema: jsonSchema } }; setLastApiPayload(payload); try { const response = await fetchWithRetry(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await response.json(); const rawText = data?.candidates?.[0]?.content?.parts?.[0]?.text; if (!rawText) { throw new Error("Inget svar mottogs från AI backend-tjänsten."); } const parsedRecipe = JSON.parse(rawText); setCurrentRecipe(parsedRecipe); setRawJsonResponse(JSON.stringify(parsedRecipe, null, 2)); setCheckedIngredients({}); setPortionMultiplier(1); setActiveTimers({}); setActiveTab('recipe'); showToast(" Nytt recept skapat av AI Backend!"); // Trigger parallel Image Generation generateDishImage(parsedRecipe.titel, parsedRecipe.beskrivning); } catch (err) { console.error("Receptgenerering misslyckades:", err); setGeneratorError(err.message || "Ett fel uppstod vid kommunikation med Gemini API."); } finally { setIsGenerating(false); } }; const generateDishImage = async (title, desc) => { setIsGeneratingImage(true); const apiKey = ""; const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-lite-image:generateContent?key=${apiKey}`; const imagePrompt = `A high quality, professional gourmet food photograph of ${title}: ${desc}. Elegant plating, realistic lighting, appetizing food photography on a dark rustic table background, cinematic resolution.`; const payload = { contents: [{ parts: [{ text: imagePrompt }] }], generationConfig: { responseModalities: ["IMAGE"], imageConfig: { aspectRatio: "16:9" } } }; try { const response = await fetchWithRetry(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await response.json(); const base64Data = result?.candidates?.[0]?.content?.parts?.find(p => p.inlineData)?.inlineData?.data; if (base64Data) { setRecipeImage(`data:image/png;base64,${base64Data}`); } } catch (e) { console.warn("Matbildgenerering misslyckades eller stöds ej i offline-läge:", e); } finally { setIsGeneratingImage(false); } }; const toggleTimer = (stepNum, defaultMinutes) => { setActiveTimers(prev => { const current = prev[stepNum]; if (!current) { return { ...prev, [stepNum]: { secondsLeft: defaultMinutes * 60, isRunning: true, totalSeconds: defaultMinutes * 60 } }; } return { ...prev, [stepNum]: { ...current, isRunning: !current.isRunning } }; }); }; const resetTimer = (stepNum, defaultMinutes) => { setActiveTimers(prev => ({ ...prev, [stepNum]: { secondsLeft: defaultMinutes * 60, isRunning: false, totalSeconds: defaultMinutes * 60 } })); }; const handleSaveRecipe = () => { if (savedRecipes.some(r => r.titel === currentRecipe.titel)) { showToast(" Receptet finns redan i din receptbok!"); return; } const updated = [currentRecipe, ...savedRecipes]; setSavedRecipes(updated); try { localStorage.setItem('gourmet_saved_recipes', JSON.stringify(updated)); } catch (e) { } showToast(" Sparat i din Receptbok!"); }; const copyToClipboard = (text, label = "Kopierat") => { navigator.clipboard.writeText(text); showToast(` ${label} till urklipp!`); }; return (
{/* Toast Notification Banner */} {toastMessage && (
{toastMessage}
)} {/* Top Application Header */}
setActiveTab('generator')}>

GourmetAI

JSON Backend v3.1

Interaktiv Svensk Receptgenerator & Kockutmaning

{/* Navigation Bar */}
{/* Main Body Content View Router */}
{/* ========================================================================= */} {/* TAB 1: AI RECIPE GENERATOR FORM */} {/* ========================================================================= */} {activeTab === 'generator' && (
Drivs av Gemini 3 Flash & JSON Schema

Designa ditt skräddarsydda recept

Välj dina preferenser, vad du har i kylskåpet och låt backend-AI:n komponera ett perfekt recept med exakt näringsinnehåll och tider.

{/* Cuisine Selection */} {}
{[ 'Italienskt', 'Asiatiskt', 'Svensk husmanskost', 'Mexikanskt', 'Indiskt', 'Medelhavet', 'Franskt Gourmet', 'Street Food', 'Mellanöstern' ].map(c => { const isSelected = cuisines.includes(c); return ( ); })}
{/* Diet & Goal */} {}
{/* Cooking Time & Difficulty */}
{/* Fridge Contents / Pantry */} {}
Slipp matsvinn! AI:n bygger receptet runt dessa.
setNewFridgeItem(e.target.value)} placeholder="T.ex. Lax, Avokado, Kokosmjölk..." className="flex-1 bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-sm text-slate-200 placeholder-slate-600 focus:outline-none focus:border-amber-500" />
{fridgeItems.map(item => ( {item} ))} {fridgeItems.length === 0 && (

Inga specifika råvaror valda.

)}
{/* Servings, Allergies & Extra Instructions */}
{servings} portioner
setAllergies(e.target.value)} placeholder="T.ex. Gluten, Laktos, Nötter" className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2.5 text-sm text-slate-200 focus:outline-none focus:border-amber-500" />
setCustomPrompt(e.target.value)} placeholder="T.ex. 'Extra såsigt', 'Ugnstillagad'" className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2.5 text-sm text-slate-200 focus:outline-none focus:border-amber-500" />
{/* Error Message if Generation Fails */} {generatorError && (
Kunde inte skapa receptet: {generatorError}
)} {/* Submit / Generate Button */} {}
)} {/* ========================================================================= */} {/* TAB 2: RECIPE VIEW (VISUAL & STEP-BY-STEP COOKING MODE) */} {/* ========================================================================= */} {activeTab === 'recipe' && (
{/* Top Toolbar */}
{/* VISUAL RECIPE CARD */} {recipeMode === 'visual' ? (
{/* Hero Header Banner */} {}
{recipeImage ? ( {currentRecipe.titel} ) : (
)}
{currentRecipe.kök} {currentRecipe.diet} Svårighet: {currentRecipe.svårighetsgrad} {isGeneratingImage && ( Genererar matbild med AI... )}

{currentRecipe.titel}

{currentRecipe.beskrivning}

{/* Meta stats bar */}
{currentRecipe.tillagningstid_min} min tillagning
{currentRecipe.näringsvärde?.kalorier} kcal / portion
Standard: {currentRecipe.portionsstorlek} portioner
{/* Recipe Body Split */}
{/* LEFT COLUMN: Ingredients Checklist & Portion Scaler */} {}

Ingredienser

{/* Portion Multiplier */}
Portioner: {(currentRecipe.portionsstorlek * portionMultiplier).toFixed(1).replace('.0', '')}
{currentRecipe.ingredienser?.map((ing, idx) => { const scaledAmount = (ing.mängd * portionMultiplier).toFixed(1).replace('.0', ''); const isChecked = checkedIngredients[idx]; return (
setCheckedIngredients({ ...checkedIngredients, [idx]: !isChecked })} className={`flex items-center justify-between p-3 rounded-xl border transition-all cursor-pointer select-none ${ isChecked ? 'bg-emerald-950/20 border-emerald-500/30 text-slate-500 line-through' : 'bg-slate-900/60 border-slate-800 text-slate-200 hover:border-slate-700' }`} >
{isChecked ? ( ) : ( )} {ing.namn}
{scaledAmount} {ing.enhet}
); })}
{/* Nutrition Breakdown Graphic */} {}

Näringsvärde per portion

Kcal {currentRecipe.näringsvärde?.kalorier}
Protein {currentRecipe.näringsvärde?.protein}g
Kolhydr. {currentRecipe.näringsvärde?.kolhydrater}g
Fett {currentRecipe.näringsvärde?.fett}g
{/* Macro Bar Visual */}
{/* Wine & Beverage Pairing Box */}

Sommelierens Dryckestips

"{currentRecipe.dryckestips}"

{/* RIGHT COLUMN: Step-by-Step Instructions & Interactive Timers */} {}

Tillagning & Instruktioner

{currentRecipe.instruktioner?.map((steg) => { const stepNum = steg.steg_nummer; const timer = activeTimers[stepNum]; const defaultMin = steg.timer_minuter || 0; return (
{stepNum}

{steg.rubrik}

{/* Step Timer Trigger if minutes specified */} {defaultMin > 0 && (
{timer ? (
{Math.floor(timer.secondsLeft / 60)}:{(timer.secondsLeft % 60).toString().padStart(2, '0')}
) : ( )}
)}

{steg.beskrivning}

); })}
{/* Chef's Pro Tip */} {currentRecipe.kockens_tips && (
Kockens Hemliga Knep

{currentRecipe.kockens_tips}

)}
) : ( /* RAW JSON MODE VIEW */
Gemini API Backend Return Payload (JSON)
                  {rawJsonResponse}
                
)}
)} {/* ========================================================================= */} {/* TAB 3: MINIGAME - KOCKUTMANINGEN (MYSTERY BOX) */} {/* ========================================================================= */} {activeTab === 'game' && ( )} {/* ========================================================================= */} {/* TAB 4: SAVED RECIPES BOOK */} {/* ========================================================================= */} {activeTab === 'saved' && (

Din Receptbok

Sparade AI-genererade recept

{savedRecipes.length === 0 ? (

Inga sparade recept ännu

Generera ett recept i skaparläget och klicka på "Spara" för att bygga din personliga kokbok.

) : (
{savedRecipes.map((rec, i) => (
{rec.kök} {rec.tillagningstid_min} min

{rec.titel}

{rec.beskrivning}

{rec.ingredienser?.length} ingredienser
))}
)}
)} {/* ========================================================================= */} {/* TAB 5: BACKEND JSON INSPECTOR */} {/* ========================================================================= */} {activeTab === 'inspector' && (

Backend JSON Developer Inspector

Inspektera den råa datastrukturen och Gemini API-anropet. Perfekt för utvecklare och testning.

{/* Validation Badge */}
JSON Schema Validering: PASS Schema: gemini-3-flash-preview strict mode
{/* JSON Payload Viewer */}
                  {rawJsonResponse}
                
{/* Request Config Viewer */} {lastApiPayload && (
                    {JSON.stringify(lastApiPayload, null, 2)}
                  
)}
)}
{/* Footer */}
); } // ============================================================================ // MINIGAME COMPONENT: KOCKUTMANINGEN - MYSTERY BOX & FLAVOR BALANCER // ============================================================================ function MysteryBoxGame({ showToast }) { const MYSTERY_DISHES = [ { title: "Glacerad Anka med Apelsin & Rödvin", ingredients: ["Ankbröst", "Färsk Apelsin", "Timjan", "Rödvin", "Salladslök"], targetFlavors: { salta: 60, syra: 75, sotma: 70, hetta: 20, umami: 85 } }, { title: "Rökig Mexikansk Mole med Kyckling", ingredients: ["Kycklinglår", "Mörk Choklad (85%)", "Chili Chipotle", "Spiskummin", "Lime"], targetFlavors: { salta: 55, syra: 50, sotma: 40, hetta: 80, umami: 90 } }, { title: "Svensk Smörstekt Gös med Sandefjordsås & Forellrom", ingredients: ["Gösfilé", "Vispgrädde", "Citron", "Dill", "Forellrom"], targetFlavors: { salta: 70, syra: 80, sotma: 25, hetta: 10, umami: 75 } } ]; const [currentDishIndex, setCurrentDishIndex] = useState(0); const dish = MYSTERY_DISHES[currentDishIndex]; // Player Flavor Sliders (0 - 100) const [flavors, setFlavors] = useState({ salta: 40, syra: 30, sotma: 30, hetta: 20, umami: 50 }); const [isJudging, setIsJudging] = useState(false); const [criticReview, setCriticReview] = useState(null); const handleNewMysteryBox = () => { const nextIdx = (currentDishIndex + 1) % MYSTERY_DISHES.length; setCurrentDishIndex(nextIdx); setFlavors({ salta: 30 + Math.floor(Math.random() * 30), syra: 20 + Math.floor(Math.random() * 30), sotma: 20 + Math.floor(Math.random() * 30), hetta: 10 + Math.floor(Math.random() * 30), umami: 40 + Math.floor(Math.random() * 30) }); setCriticReview(null); showToast(" Ny Mysterielåda öppnad!"); }; const applySeasoning = (type, delta) => { setFlavors(prev => ({ ...prev, [type]: Math.min(100, Math.max(0, prev[type] + delta)) })); }; const handleJudgeDish = async () => { setIsJudging(true); setCriticReview(null); const apiKey = ""; const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent?key=${apiKey}`; const prompt = `Du är den hårda men underhållande stjärnkrogen-matkritikern "Monsieur Gusteau". Kocken har tillagat rätten "${dish.title}" med ingredienserna: ${dish.ingredients.join(', ')}. Kockens Smakbalans (0-100): - Sälta: ${flavors.salta} (Mål: ${dish.targetFlavors.salta}) - Syra: ${flavors.syra} (Mål: ${dish.targetFlavors.syra}) - Sötma: ${flavors.sotma} (Mål: ${dish.targetFlavors.sotma}) - Hetta: ${flavors.hetta} (Mål: ${dish.targetFlavors.hetta}) - Umami: ${flavors.umami} (Mål: ${dish.targetFlavors.umami}) Generera ett betyg i exakt JSON-format med följande felder: - "score": heltal 0-100 baserat på hur nära smakbalansen är målet - "michelinStars": heltal 0 till 3 - "title": en dramatisk rubrik på svensk gourmet-franska/svenska - "quote": ett underhållande citat från kritikern - "feedback": en kort förklaring vad som var bra eller saknades i smakerna`; const jsonSchema = { type: "OBJECT", properties: { score: { type: "INTEGER" }, michelinStars: { type: "INTEGER" }, title: { type: "STRING" }, quote: { type: "STRING" }, feedback: { type: "STRING" } }, required: ["score", "michelinStars", "title", "quote", "feedback"] }; const payload = { contents: [{ parts: [{ text: prompt }] }], generationConfig: { responseMimeType: "application/json", responseSchema: jsonSchema } }; try { const response = await fetchWithRetry(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await response.json(); const rawText = data?.candidates?.[0]?.content?.parts?.[0]?.text; if (rawText) { setCriticReview(JSON.parse(rawText)); } } catch (e) { // Fallback offline critique math const diff = Math.abs(flavors.salta - dish.targetFlavors.salta) + Math.abs(flavors.syra - dish.targetFlavors.syra) + Math.abs(flavors.sotma - dish.targetFlavors.sotma); const calculatedScore = Math.max(20, 100 - Math.round(diff / 3)); setCriticReview({ score: calculatedScore, michelinStars: calculatedScore > 85 ? 3 : calculatedScore > 65 ? 2 : 1, title: "Kritikerns Omdöme", quote: calculatedScore > 80 ? "Magnifique! En symfoni i gommen." : "Bra försök, men balansen behöver finjusteras.", feedback: "En fin insats med mysterielådans råvaror." }); } finally { setIsJudging(false); } }; return (
{/* Game Header */}
Mini-spel: Kockutmaningen

Mysterielådan & Smakbalansen

Öppna mysterielådan, använd kryddstängerna för att balansera de 5 grundsmakerna och imponera på AI-kritikern Gusteau!

{/* LEFT COLUMN: Mystery Box Dish & Ingredients */}
Mysterielåda #{currentDishIndex + 1}

{dish.title}

{dish.ingredients.map(ing => ( {ing} ))}
{/* Quick Seasoning Action Buttons */}
{/* RIGHT COLUMN: Flavor Sliders & Critic Evaluation */} {}

Balansera de 5 Grundsmakerna

{/* Flavor Sliders */}
{[ { key: 'salta', label: 'Sälta', color: 'bg-slate-300', text: 'text-slate-300' }, { key: 'syra', label: 'Syra', color: 'bg-amber-400', text: 'text-amber-400' }, { key: 'sotma', label: 'Sötma', color: 'bg-orange-400', text: 'text-orange-400' }, { key: 'hetta', label: 'Hetta', color: 'bg-red-500', text: 'text-red-400' }, { key: 'umami', label: 'Umami', color: 'bg-amber-600', text: 'text-amber-500' }, ].map(item => (
{item.label} {flavors[item.key]} / 100
setFlavors({ ...flavors, [item.key]: parseInt(e.target.value) })} className="w-full accent-amber-500 bg-slate-950 h-2 rounded-lg cursor-pointer" />
))}
{/* Submit to Critic Button */}
{/* Critic Review Output */} {criticReview && (
Monsieur Gusteaus Betyg

{criticReview.title}

{/* Score Circle */}
{criticReview.score}
{/* Michelin Stars */}
{[...Array(3)].map((_, i) => ( ))} ({criticReview.michelinStars} Michelin-stjärnor)
"{criticReview.quote}"

{criticReview.feedback}

)}
); }