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.
{fridgeItems.map(item => (
{item}
))}
{fridgeItems.length === 0 && (
Inga specifika råvaror valda.
)}
{/* Servings, Allergies & Extra Instructions */}
{/* 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.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 => (
))}
{/* 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}
)}
);
}