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 = () => (
);
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) => (
))}
{/* 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 && (
)}
)}
{/* Fridge Mode UI */}
{activeTab === 'fridge' && (
)}
{/* Action Button */}
{error && (
{error}
)}
{/* Results Section */}
{recipes.length > 0 && (
Here's what I cooked up!
{recipes.map((recipe, idx) => (
))}
)}
{/* Empty State / Welcome */}
{recipes.length === 0 && !loading && !imagePreview && ingredientsInput === '' && (
)}
);
}
Enjoying this? A quick like helps keep it online longer.
Like what you see?
Create your own