import React, { useState, useRef, useEffect } from 'react'; import { Camera, Upload, Leaf, Share2, Minus, Plus, X, Loader2, Utensils, Zap, Clock, Flame, ChevronRight } from 'lucide-react'; // --- API Helper --- const apiKey = ""; // Set by environment const generateRecipes = async (mode, input, isVegetarian) => { const promptText = ` You are Chef Whiskers, a helpful cat chef assistant. ${mode === 'image' ? "Identify the food or ingredients in this image." : `The user has these ingredients: ${input}.`} Generate 3 distinct recipes based on this input. Constraint 1: At least one recipe MUST be "No-Oven" (stovetop, raw, microwave, etc.) and very easy. Constraint 2: ${isVegetarian ? "ALL recipes must be VEGETARIAN." : "Suggest a mix, but prioritize the input ingredients."} Return ONLY a valid JSON array of objects with this structure: [ { "id": "unique_id", "title": "Recipe Title", "description": "A short, appetizing description.", "difficulty": "Easy", "calories": "550 kcal", "time": "20 mins", "ingredients": [ {"item": "Flour", "amount": 2, "unit": "cups"}, {"item": "Sugar", "amount": 1, "unit": "tbsp"} ], "steps": ["Step 1 description", "Step 2 description"] } ] `; try { const payload = { contents: [], generationConfig: { responseMimeType: "application/json" } }; if (mode === 'image') { payload.contents = [{ parts: [ { text: promptText }, { inlineData: { mimeType: "image/jpeg", data: input.split(',')[1] } } ] }]; } else { payload.contents = [{ parts: [{ text: promptText }] }]; } const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await response.json(); const text = result.candidates?.[0]?.content?.parts?.[0]?.text; if (!text) { throw new Error("No data returned from Chef Whiskers."); } // Robust JSON extraction: Find the array brackets to ignore any potential whitespace/markdown const startIndex = text.indexOf('['); const endIndex = text.lastIndexOf(']'); if (startIndex !== -1 && endIndex !== -1) { const jsonString = text.substring(startIndex, endIndex + 1); return JSON.parse(jsonString); } else { // Fallback for non-array responses (though prompt requests array) return JSON.parse(text); } } catch (error) { console.error("API Error:", error); throw new Error("Meow! I couldn't cook up a response. Please try again."); } }; // --- Components --- const BlackCatLogo = () => ( {/* Black Head */} {/* Ears */} {/* Inner Ears */} {/* Eyes */} {/* Pupils (Teal) */} {/* Eye Shine */} {/* Nose */} {/* Whiskers */} ); const Header = () => (

Chef Whiskers

Your Purr-sonal Sous Chef

Ready to cook!
); const RecipeCard = ({ recipe }) => { const [servings, setServings] = useState(2); const handleShare = async () => { const text = `Check out this recipe for ${recipe.title} I found on Chef Whiskers!\n\n${recipe.description}`; if (navigator.share) { try { await navigator.share({ title: recipe.title, text: text }); } catch (err) { console.log('Share canceled'); } } else { navigator.clipboard.writeText(text); alert('Recipe copied to clipboard!'); } }; const scaleAmount = (amount) => { if (!amount) return ""; const scaled = (amount / 2) * servings; return Number.isInteger(scaled) ? scaled : scaled.toFixed(1); }; // Modern difficulty badge styles const difficultyColor = recipe.difficulty === "Easy" ? "text-green-700 bg-green-100" : recipe.difficulty === "Medium" ? "text-orange-700 bg-orange-100" : "text-red-700 bg-red-100"; return (
{/* Header Section */}

{recipe.title}

{recipe.difficulty || "Easy"}

{recipe.description}

{/* Metadata Grid - Matches the "Neat" reference image */}
{recipe.time}
{recipe.calories && (
{recipe.calories}
)}
{/* Servings Control */}
{servings}
{/* Ingredients Section */}

Ingredients

    {recipe.ingredients.map((ing, idx) => (
  • {scaleAmount(ing.amount)} {ing.unit} {ing.item}
  • ))}
{/* Instructions Section */}

Instructions

{recipe.steps.map((step, idx) => (
{idx + 1}.

{step}

))}
{/* Footer */}
); }; // --- Main App Component --- export default function App() { const [activeTab, setActiveTab] = useState('photo'); // 'photo' or 'fridge' const [isVegetarian, setIsVegetarian] = useState(false); const [ingredientsInput, setIngredientsInput] = useState(''); const [imagePreview, setImagePreview] = useState(null); const [recipes, setRecipes] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [cameraActive, setCameraActive] = useState(false); const videoRef = useRef(null); const fileInputRef = useRef(null); // Camera handling const startCamera = async () => { try { setCameraActive(true); const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); if (videoRef.current) { videoRef.current.srcObject = stream; } } catch (err) { setError("Unable to access camera. Please upload a file instead."); setCameraActive(false); } }; const stopCamera = () => { if (videoRef.current && videoRef.current.srcObject) { videoRef.current.srcObject.getTracks().forEach(track => track.stop()); } setCameraActive(false); }; const capturePhoto = () => { if (!videoRef.current) return; const canvas = document.createElement('canvas'); canvas.width = videoRef.current.videoWidth; canvas.height = videoRef.current.videoHeight; canvas.getContext('2d').drawImage(videoRef.current, 0, 0); const dataUrl = canvas.toDataURL('image/jpeg'); setImagePreview(dataUrl); stopCamera(); }; const handleFileUpload = (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onloadend = () => setImagePreview(reader.result); reader.readAsDataURL(file); } }; const handleSubmit = async () => { if (activeTab === 'photo' && !imagePreview) { setError("Please take a photo or upload an image first!"); return; } if (activeTab === 'fridge' && !ingredientsInput.trim()) { setError("Please enter some ingredients!"); return; } setLoading(true); setError(null); setRecipes([]); try { const input = activeTab === 'photo' ? imagePreview : ingredientsInput; const result = await generateRecipes(activeTab === 'image' ? 'image' : (activeTab === 'photo' ? 'image' : 'text'), input, isVegetarian); setRecipes(result); } catch (err) { setError(err.message); } finally { setLoading(false); } }; const clearAll = () => { setImagePreview(null); setIngredientsInput(''); setRecipes([]); setError(null); stopCamera(); }; return (
{/* Main Input Card */}
{/* Mode Switcher */}
{/* Vegetarian Toggle - Styled as a pill */}
{/* Photo Mode UI */} {activeTab === 'photo' && (
{!imagePreview && !cameraActive && (
)} {cameraActive && (
)} {imagePreview && (
Preview
)}
)} {/* Fridge Mode UI */} {activeTab === 'fridge' && (