# Project export: Snap Chef

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2025
- Tagline: Snap Chef: Click, Cook, Enjoy—transform your fridge's leftovers into delicious meals with a click of the button!
- Devpost: https://devpost.com/software/snap-chef-bfwep7
- GitHub: https://github.com/ryan-tock/snap-chef
- Video: https://www.youtube.com/embed/CqEZquajEXg?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — galacticism (51 commits), jae (49 commits), Nicole Mulvey (26 commits)

## Devpost submission (written by the team)

### Inspiration

With 40% of the entire US food supply ending up in the waste, helping to eliminate food waste—even at a small scale—can go a long way. We wanted to tackle the problem at a consumer level to help individual households use their leftovers more effectively. Furthermore, many people (our team included) spend parts of the day worrying about what they want to make for dinner with the ingredients in their fridge. We decided to chip away at both of these problems with a new app household cooks can use on a daily basis.

### What it does

Snap chef just requires a picture of your fridge, either taken live or previously taken and uploaded. With this, it is able to use Google Gemini to parse the image and create delicious recipe ideas with the data and other parameters specified by the user. Features The app allows the user to manually edit the number and name of each ingredient in case Gemini incorrectly counted or some items were hidden. It has several sorting and filtering modes, so you can make sure you see recipes that use specified ingredients, or make sure you see recipes that meet your dietary restrictions. Lastly, you can also save recipes Snap Chef generated for later use. App structure The foundation of the app is built upon react native, developed with expo. It has a back-end server built with flask to pass user input to the Gemini Generative AI API. Challenges The free trial plan of Gemini we were using has a rate limit of 1 query every 5 minutes, which forced us to get creative with our development process and testing. The Gemini API servers were also having severe connection issues by the end of the final day, making testing and recording a test take over two hours. Our group barely had any react experience, so doing an entire react project and integrating it with other software was very challenging for us. In particular, getting the image picker and camera to work along with flask and the Gemini API took a lot of troubleshooting.

### Accomplishments we're proud of

Above all, we are happy to have competed in our first hackathon! Honestly, we're extremely proud that we managed to get the whole thing working and talking to each other and learning entire frameworks on the fly, given our lack of experience in this area. We are very proud of how far we've learned and grown from this project.

### What we learned

Together, we all learned react-native, the Gemini GenerativeAI API, and flask for the first time! We also learned that it might not be the best idea to develop the frontend of a project before the backend.

### What's next

We hope to incorporate an actual recipe API, such as allrecipes or edamam, so we don't have to rely on Gemini for recipes. Furthermore, we hope to activate a paid plan for Gemini so we can get passed the 5 minute rate limit on requests. With these, it would be possible to put the app on the google play store and app store.

## README (from the GitHub repository)

# ⭐ Snap Chef App - TreeHacks 2025 ⭐
For many people, making the decision for what to cook each night is a source of stress. With Snap Chef, decisions can be made for you while taking into account the contents of your fridge.
### Click, Cook, Enjoy!
Transforming your fridge's leftovers into a delicious meal has never been easier! First, Point the camera at your fridge, or upload a previously taken photo. Then, input any specific ingredients you want to make sure are used as well as any dietary restrictions. Snap chef will instantly respond with several great ideas for any meal that day!
### Using Snap Chef
First, upload a photo to the app or take a live photo
Second, hit the analyze fridge button
Third, edit any ingredients (names, amounts) or add new ingredients entirely
Fourth, view recipes, feel free to bookmark!
### Building Snap Chef
Snap Chef is currently very early in it's development and so is not on the Apple app store or the Google play store. Thus, Snap Chef needs to be compiled directly from source on a computer and then transferred to a phone.
###### Step 1
Install the [Expo Go](https://expo.dev/go) app on your phone
###### Step 2
Install [homebrew](https://brew.sh/) (MacOS only) running the following in terminal`
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
###### Step 3
Clone this github repository with
`git clone https://github.com/ryan-tock/snap-chef.git`
###### Step 4
Install Node.js by running
`brew install node`
###### Step 5
Navigate to the source directory with
`cd snap-chef`
###### Step 6
Install dependencies with
`npm i`
###### Step 7
start the expo server with
`npx expo start`
###### Step 8
Scan the qr code on your phone to launch the app on phone


## Detected evidence (automated analysis)

Indexed codebase: 34 recognized source files, 122 KB.
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- Flask (technology) — claimed on Devpost, not found in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (43 of 43)

```
.gitignore
app.json
app.py
app/_layout.tsx
app/.DS_Store
app/(tabs)/_layout.tsx
app/(tabs)/index.tsx
app/(tabs)/ingredients.tsx
app/(tabs)/recipes.tsx
app/(tabs)/user_settings.tsx
app/+not-found.tsx
app/contexts/global_saved_recipes.tsx
app/contexts/SavedRecipesContext.tsx
app/saved_recipes.tsx
complete-recipes-list.json
components/__tests__/__snapshots__/ThemedText-test.tsx.snap
components/__tests__/ThemedText-test.tsx
components/AuthContext.tsx
components/Collapsible.tsx
components/ExternalLink.tsx
components/HapticTab.tsx
components/HelloWave.tsx
components/ParallaxScrollView.tsx
components/ThemedText.tsx
components/ThemedView.tsx
components/ThemeToggleContext.tsx
components/ui/IconSymbol.ios.tsx
components/ui/IconSymbol.tsx
components/ui/TabBarBackground.ios.tsx
components/ui/TabBarBackground.tsx
constants/Colors.ts
contexts/SavedRecipesContext.tsx
eas.json
flask_app.py
gemini_api.py
hooks/useColorScheme.ts
hooks/useColorScheme.web.ts
hooks/useThemeColor.ts
LICENSE
package.json
README.md
scripts/reset-project.js
tsconfig.json
```

### Dependencies

- package.json: @babel/core@^7.25.2, @expo/vector-icons@^14.0.2, @react-native-async-storage/async-storage@11.1.0, @react-native-picker/picker@^2.11.0, @react-navigation/bottom-tabs@^7.2.0, @react-navigation/native@^7.0.14, @types/jest@^29.5.12, @types/react@~18.3.12, @types/react-test-renderer@^18.3.0, axios@^1.7.9, expo@~52.0.35, expo-blur@~14.0.3, expo-camera@~16.0.16, expo-constants@~17.0.6, expo-font@~13.0.3, expo-haptics@~14.0.1, expo-image-picker@~16.0.6, expo-linking@~7.0.5, expo-router@~4.0.17, expo-splash-screen@~0.29.22, expo-status-bar@~2.0.1, expo-symbols@~0.2.2, expo-system-ui@~4.0.8, expo-web-browser@~14.0.2, jest@^29.2.1, jest-expo@~52.0.4, react@18.3.1, react-dom@18.3.1, react-native@0.76.7, react-native-dropdown-picker@^5.4.6, react-native-gesture-handler@~2.20.2, react-native-reanimated@~3.16.1, react-native-safe-area-context@4.12.0, react-native-screens@~4.4.0, react-native-vision-camera@^4.6.3, react-native-web@~0.19.13, react-native-webview@13.12.5, react-test-renderer@18.3.1, typescript@^5.3.3

### Recent commits (newest first)

- Merge remote-tracking branch 'origin/main'
- updated readme
- Merge branch 'app'
- Merge remote-tracking branch 'origin/app' into app
- Merge branch 'app' of https://github.com/ryan-tock/snap-chef into app
- p
- p
- updated package.json
- updated package.json
- Merge pull request #2 from ryan-tock/testtesttest
- Merge remote-tracking branch 'origin/main' into testtesttest
- Merge branch 'app' into testtesttest
- bookmark working
- Merge branch 'app' of https://github.com/ryan-tock/snap-chef into app
- fixed recipe autogetch
- woriasdkfian
- wrokgin?
- j
- ingredients buttons fix
- changes to readme

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "snap-chef",
  "main": "expo-router/entry",
  "version": "1.0.0",
  "scripts": {
    "start": "expo start",
    "reset-project": "node ./scripts/reset-project.js",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "test": "jest --watchAll",
    "lint": "expo lint"
  },
  "jest": {
    "preset": "jest-expo"
  },
  "dependencies": {
    "@expo/vector-icons": "^14.0.2",
    "@react-native-picker/picker": "^2.11.0",
    "@react-navigation/bottom-tabs": "^7.2.0",
    "@react-navigation/native": "^7.0.14",
    "axios": "^1.7.9",
    "expo": "~52.0.35",
    "expo-blur": "~14.0.3",
    "expo-camera": "~16.0.16",
    "expo-constants": "~17.0.6",
    "expo-font": "~13.0.3",
    "expo-haptics": "~14.0.1",
    "expo-image-picker": "~16.0.6",
    "expo-linking": "~7.0.5",
    "expo-router": "~4.0.17",
    "expo-splash-screen": "~0.29.22",
    "expo-status-bar": "~2.0.1",
    "expo-symbols": "~0.2.2",
    "expo-system-ui": "~4.0.8",
    "expo-web-browser": "~14.0.2",
    "react": "18.3.1",
    "react-dom": "18.3.1",
    "react-native": "0.76.7",
    "@react-native-async-storage/async-storage": "11.1.0",
    "react-native-dropdown-picker": "^5.4.6",
    "react-native-gesture-handler": "~2.20.2",
    "react-native-reanimated": "~3.16.1",
    "react-native-safe-area-context": "4.12.0",
    "react-native-screens": "~4.4.0",
    "react-native-vision-camera": "^4.6.3",
    "react-native-web": "~0.19.13",
    "react-native-webview": "13.12.5"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@types/jest": "^29.5.12",
    "@types/react": "~18.3.12",
    "@types/react-test-renderer": "^18.3.0",
    "jest": "^29.2.1",
    "jest-expo": "~52.0.4",
    "react-test-renderer": "18.3.1",
    "typescript": "^5.3.3"
  },
  "private": true
}

```

### app.py

```python
from flask import Flask, jsonify, request
from flask_cors import CORS
from flask_jwt_extended import JWTManager, create_access_token

app = Flask(__name__)
CORS(app)  # Allow cross-origin requests

app.config["JWT_SECRET_KEY"] = "your_secret_key"
jwt = JWTManager(app)

@app.route("/api/hello", methods=["GET"])
def hello():
    return jsonify({"message": "Hello from Flask Backend!"})

@app.route("/api/login", methods=["POST"])
def login():
    data = request.json
    if data["username"] == "admin" and data["password"] == "password":
        token = create_access_token(identity=data["username"])
        return jsonify({"token": token})
    return jsonify({"error": "Invalid credentials"}), 401

@app.route("/", methods=["GET"])
def index():
    return jsonify("Welcome to the Flask API")

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)
```

### app/_layout.tsx

```typescript
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { StatusBar } from 'expo-status-bar';
import { useEffect } from 'react';
import 'react-native-reanimated';

import { useColorScheme } from '@/hooks/useColorScheme';
import { ThemeToggleProvider } from '@/components/ThemeToggleContext';
import SavedRecipesProvider from '@/app/contexts/SavedRecipesContext';
import { AuthProvider } from '@/components/AuthContext';


// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
  const colorScheme = useColorScheme();
  const [loaded] = useFonts({
    SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
  });

  useEffect(() => {
    if (loaded) {
      SplashScreen.hideAsync();
    }
  }, [loaded]);

  if (!loaded) {
    return null;
  }

  return (
    <AuthProvider>
      <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
        <ThemeToggleProvider>
          <SavedRecipesProvider>
            <Stack>
              <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
              <Stack.Screen name="+not-found" />
            </Stack>
            <StatusBar style="auto" />
          </SavedRecipesProvider>
        </ThemeToggleProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

```

### app/(tabs)/_layout.tsx

```typescript
import { Tabs } from 'expo-router';
import React from 'react';
import { Platform } from 'react-native';

import { HapticTab } from '@/components/HapticTab';
import TabBarBackground from '@/components/ui/TabBarBackground';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useThemeToggle } from '@/components/ThemeToggleContext';

import Entypo from '@expo/vector-icons/Entypo';

export default function TabLayout() {
  const { isDark } = useThemeToggle();
  const currentColorScheme = isDark ? 'dark' : 'light';

  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: Colors[currentColorScheme].tint,
        headerShown: false,
        tabBarButton: HapticTab,
        tabBarBackground: TabBarBackground,
        tabBarStyle: Platform.select({
          ios: {
            // Use a transparent background on iOS to show the blur effect
            position: 'absolute',
          },
          default: {},
        }),
      }}>
      <Tabs.Screen
        name="index"
        options={{
          title: 'Upload',
          tabBarIcon: ({ color }) => <Entypo name="camera" size={24} color={color}/>,
        }}
      />
      <Tabs.Screen
        name="ingredients"
        options={{
          title: 'Ingredients',
          tabBarIcon: ({ color }) => <Entypo name="shopping-basket" size={24} color={color} />,
        }}
      />
      <Tabs.Screen
        name="recipes"
        options={{
          title: 'Recipes',
          tabBarIcon: ({ color }) => <Entypo name="book" size={24} color={color} />,
        }}
      />
      <Tabs.Screen
        name="saved"
        options={{
          title: 'Saved Recipes',
          tabBarIcon: ({ color }) => <Entypo name="save" size={24} color={color} />,
        }}
      />
    </Tabs>
  );
}

```

### app/(tabs)/index.tsx

```typescript
import React, { useEffect, useState, useRef } from 'react';
import {
  View,
  Text,
  Alert,
  ScrollView,
  Image,
  ActivityIndicator,
  StyleSheet,
  TouchableOpacity,
  SafeAreaView,
  Dimensions,
} from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import { MaterialIcons } from '@expo/vector-icons';
import { CameraView, CameraType, useCameraPermissions, Camera } from 'expo-camera';
import { useRouter } from 'expo-router';

import { Colors } from '@/constants/Colors';
import { useThemeToggle } from '@/components/ThemeToggleContext';
const screenHeight = Dimensions.get('window').height;
const cameraHeight = screenHeight * 0.7;

interface Recipe {
  title: string;
  ingredients: string[];
  instructions: string[];
}

interface Ingredient {
  id: string;
  name: string;
  amount: number;
  unit?: string;
}

const API_URL = 'http://10.37.163.63:5000'; // Adjust to your server IP

export default function ApiTest() {
  const router = useRouter();
  const { isDark } = useThemeToggle();
  // Make sure TypeScript knows these two strings:
  const currentColorScheme: 'light' | 'dark' = isDark ? 'dark' : 'light';

  // State
  const [image, setImage] = useState<string | null>(null);
  const [fridgeContents, setFridgeContents] = useState('');
  const [matchedRecipes, setMatchedRecipes] = useState<Recipe[]>([]);
  const [aiSuggestions, setAiSuggestions] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [ingredients, setIngredients] = useState<Ingredient[]>([]);

  // Camera
  const [facing, setFacing] = useState<CameraType>('back');
  const cameraRef = useRef<CameraView>(null);
  const [cameraShown, setCameraShown] = useState(false);

  // 1) Camera Permission
  useEffect(() => {
    (async () => {
      const { status } = await Camera.requestCameraPermissionsAsync();
      if (status !== 'granted') {
        Alert.alert('Sorry, we need camera permissions to make this work!');
      }
    })();
  }, []);

  // 2) Media Library Permission
  useEffect(() => {
    (async () => {
      const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
      if (status !== 'granted') {
        Alert.alert('Sorry, we need camera roll permissions to make this work!');
      }
    })();
  }, []);

  // 3) Parse Ingredients
  const parseIngredients = (contents: string) => {
    const ingredientsList: Ingredient[] = [];
    const lines = contents.split('\n');

    lines.forEach((line, index) => {
      if (line.includes(':')) {
        const [name, quantity] = line.split(':').map((s) => s.trim());
        const cleanName = name.replace(/\*/g, '').trim();

        if (cleanName && quantity) {
          const numbers = quantity.match(/\d+/g);
          let amount = 0;
          let unit = '';

          // Extract number
          if (numbers) {
            if (numbers.length === 2) {
              amount = Math.round((parseInt(numbers[0]) + parseInt(numbers[1])) / 2);
            } else {
              amount = parseInt(numbers[0]);
            }
          } else {
            amount = 1;
          }

          // Determine unit
          const lowerQuantity = quantity.toLowerCase();
          if (lowerQuantity.includes('bag') || lowerQuantity.includes('container')) {
            unit = amount > 1 ? 'piece' : 'bag';
          } else if (lowerQuantity.includes('cup')) {
            unit = 'cup';
          } else if (lowerQuantity.includes('slice')) {
            unit = 'slice';
          } else if (lowerQuantity.includes('pack')) {
            unit = amount > 1 ? 'piece' : 'pack';
          } else if (lowerQuantity.includes('bottle')) {
            unit = amount > 1 ? 'piece' : 'bottle';
          } else if (lowerQuantity.includes('can')) {
            unit = amount > 1 ? 'piece' : 'can';
          } else {
            unit = 'piece';
          }

          ingredientsList.push({
            id: `ingredient-${index}-${Date.now()}`,
            name: cleanName,
            amount,
            unit,
          });
        }
      }
    });

    return ingredientsList;
  };

  // 4) Camera Logic
  const takePicture = async () => {
    if (cameraRef.current) {
      const photoData = await cameraRef.current.takePictureAsync();
      if (!photoData) return;
      setImage(photoData.uri);
    } else {
      console.log('No camera ref');
    }
  };

  // 5) Image Upload
  const handleImageUpload = async () => {
    if (!image) return;

    setIsLoading(true);
    try {
      const formData = new FormData();
      formData.append('image', {
        uri: image,
        type: 'image/jpeg',
        name: 'fridge.jpg',
      } as any);

      console.log('Sending request to:', `${API_URL}/api/analyze-fridge`);

      const response = await fetch(`${API_URL}/api/analyze-fridge`, {
        method: 'POST',
        body: formData,
        headers: {
          'Content-Type': 'multipart/form-data',
        },
      });

      console.log('Response status:', response.status);
      const data = await response.json();
      console.log('Response data:', data);

      if (response.ok) {
        setFridgeContents(data.fridge_contents || '');
        const parsedIngredients = parseIngredients(data.fridge_contents || '');
        setIngredients(parsedIngredients);

        setMatchedRecipes(Array.isArray(data.matched_recipes) ? data.matched_recipes : []);
        setAiSuggestions(data.ai_suggestions || '');

        // Navigate to ingredients screen with the new data
        router.push({
          pathname: '/ingredients',
          params: { ingredients: JSON.stringify(parsedIngredients) },
        });
      } else {
        Alert.alert('Error', data.error || 'Failed to analyze image');
      }
    } catch (error) {
      console.error('Detailed error:', error);
      Alert.alert('Error', 'Failed to connect to server');
    } finally {
      setIsLoading(false);
    }
  };

  // 6) Pick Image
  const pickImage = async () => {
    try {
      const result = await ImagePicker.
[truncated — 8840 more characters]
```

### gemini_api.py

```python
from google import genai
import json
from flask import Blueprint, request, jsonify
import time

app = Blueprint('recipes', __name__)
model = "gemini-pro"  # Changed to gemini-pro since we're only doing text
client = genai.Client(api_key="AIzaSyC5Q2H6J1XhSKgvIDJa31H4vuAN9CE6HRo")

def generate_recipes(ingredients):
    try:
        # Check if there are any expiring ingredients
        expiring_ingredients = [ing for ing in ingredients if ing.get('isExpiring')]
        
        ingredients_prompt = "Generate recipes using these ingredients:\n"
        for ing in ingredients:
            expiring = "(Expiring)" if ing.get('isExpiring') else ""
            ingredients_prompt += f"- {ing['name']} (Quantity: {ing['amount']}) {expiring}\n"
        
        if not expiring_ingredients:
            ingredients_prompt += "\nCreate 3 recipes using any of the available ingredients. "
        else:
            ingredients_prompt += "\nCreate 3 recipes prioritizing ingredients marked as (Expiring). "
            
        ingredients_prompt += (
            "Return ONLY a JSON array of recipes with this exact structure:\n"
            "[\n"
            "  {\n"
            '    "id": "recipe-1",\n'
            '    "name": "Recipe Name",\n'
            '    "category": "Breakfast/Lunch/Dinner",\n'
            '    "description": "Brief description",\n'
            '    "ingredients": [\n'
            '      {"name": "Ingredient", "amount": "100", "unit": "g"}\n'
            '    ],\n'
            '    "instructions": ["step 1", "step 2"],\n'
            '    "prepTime": 30,\n'
            '    "cookTime": 20,\n'
            '    "servings": 4,\n'
            '    "dietary": ["Vegetarian", "Gluten-Free"],\n'
            '    "requiredItems": ["item1", "item2"],\n'
            '    "expiringSoon": false,\n'
            '    "matchingPercentage": 75\n'
            "  }\n"
            "]\n"
        )

        print("Sending prompt to Gemini:", ingredients_prompt)  # Debug log

        response = client.models.generate_content(
            model=model,
            contents=ingredients_prompt
        )
        
        return response.text

    except Exception as e:
        print(f"Gemini API Error: {str(e)}")
        return json.dumps([{
            "id": f"error-{int(time.time())}",
            "name": "Error Recipe",
            "category": "Error",
            "description": str(e),
            "ingredients": [{"name": ing["name"], "amount": str(ing["amount"]), "unit": "piece"} for ing in ingredients],
            "instructions": ["Could not generate recipe"],
            "prepTime": 0,
            "cookTime": 0,
            "servings": 0,
            "dietary": [],
            "requiredItems": [],
            "expiringSoon": False,
            "matchingPercentage": 0
        }])

@app.route('/generate_recipes', methods=['POST'])
def recipe_endpoint():
    try:
        data = request.json
        ingredients = data.get('ingredients', [])
        
        print(f"Received ingredients: {ingredients}")
        
        recipes_response = generate_recipes(ingredients)
        print(f"Raw Gemini response: {recipes_response}")

        try:
            # Parse the response to ensure it's valid JSON
            parsed_recipes = json.loads(recipes_response)
            
            # Ensure the response matches the expected format
            formatted_recipes = []
            for recipe in parsed_recipes:
                formatted_recipe = {
                    "id": recipe.get("id", f"recipe-{int(time.time())}"),
                    "name": recipe.get("name", "Untitled Recipe"),
                    "category": recipe.get("category", "Other"),
                    "description": recipe.get("description", "No description available"),
                    "ingredients": recipe.get("ingredients", []),
                    "instructions": recipe.get("instructions", []),
                    "prepTime": recipe.get("prepTime", 0),
                    "cookTime": recipe.get("cookTime", 0),
                    "servings": recipe.get("servings", 4),
                    "dietary": recipe.get("dietary", []),
                    "requiredItems": recipe.get("requiredItems", []),
                    "expiringSoon": recipe.get("expiringSoon", False),
                    "matchingPercentage": recipe.get("matchingPercentage", 0)
                }
                formatted_recipes.append(formatted_recipe)

            return jsonify({
                'success': True,
                'recipes': formatted_recipes
            })

        except json.JSONDecodeError as e:
            print(f"JSON parse error: {e}")
            print(f"Invalid JSON response: {recipes_response}")
            
            fallback_recipe = {
                "id": f"error-{int(time.time())}",
                "name": "Sample Recipe",
                "category": "Dinner",
                "description": "Failed to parse recipe data",
                "ingredients": [{"name": ing["name"], "amount": str(ing["amount"]), "unit": "piece"} for ing in ingredients],
                "instructions": ["Could not generate instructions"],
                "prepTime": 0,
                "cookTime": 0,
                "servings": 0,
                "dietary": [],
                "requiredItems": [],
                "expiringSoon": False,
                "matchingPercentage": 0
            }
            
            return jsonify({
                'success': True,
                'recipes': [fallback_recipe]
            })
        
    except Exception as e:
        print(f"Server error: {str(e)}")
        return jsonify({
            'success': False,
            'error': str(e)
        })

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)
```

### flask_app.py

```python
from flask import Flask, request, jsonify, render_template
from flask_cors import CORS
import PIL.Image
import io
import base64
from google import genai
import json
import time

app = Flask(__name__)
CORS(app)

model = "gemini-pro"  # For text generation
vision_model = "gemini-2.0-flash"  # For image analysis
client = genai.Client(api_key="AIzaSyBG764Zn4BcmFCw2ZqkF8VEUwLYmUEZYvE")

def parse_foods(foods_txt):
    return [item.strip().lower() for item in foods_txt.split(',')]

def generate_recipes(ingredients):
    try:
        # Check if there are any expiring ingredients
        expiring_ingredients = [ing for ing in ingredients if ing.get('isExpiring')]
        
        ingredients_prompt = "Generate recipes using these ingredients. Count the quantities in individual pieces, not in bunches or in groups, or other non-individual units.:\n"
        for ing in ingredients:
            expiring = "(Expiring)" if ing.get('isExpiring') else ""
            ingredients_prompt += f"- {ing['name']} (Quantity: {ing['amount']}) {expiring}\n"
        
        if not expiring_ingredients:
            ingredients_prompt += "\nCreate 3 recipes using any of the available ingredients. "
        else:
            ingredients_prompt += "\nCreate 3 recipes prioritizing ingredients marked as (Expiring). "
            
        ingredients_prompt += (
            "Return ONLY a JSON array of recipes with this exact structure:\n"
            "[\n"
            "  {\n"
            '    "id": "recipe-1",\n'
            '    "name": "Recipe Name",\n'
            '    "category": "Breakfast/Lunch/Dinner",\n'
            '    "description": "Brief description",\n'
            '    "ingredients": [\n'
            '      {"name": "Ingredient", "amount": "100", "unit": "g"}\n'
            '    ],\n'
            '    "instructions": ["step 1", "step 2"],\n'
            '    "prepTime": 30,\n'
            '    "cookTime": 20,\n'
            '    "servings": 4,\n'
            '    "dietary": ["Vegetarian", "Gluten-Free"],\n'
            '    "requiredItems": ["item1", "item2"],\n'
            '    "expiringSoon": false,\n'
            '    "matchingPercentage": 75\n'
            "  }\n"
            "]\n"
        )

        print("Sending prompt to Gemini:", ingredients_prompt)

        response = client.models.generate_content(
            model=model,
            contents=ingredients_prompt
        )
        
        return response.text

    except Exception as e:
        print(f"Gemini API Error: {str(e)}")
        return json.dumps([{
            "id": f"error-{int(time.time())}",
            "name": "Error Recipe",
            "category": "Error",
            "description": str(e),
            "ingredients": [{"name": ing["name"], "amount": str(ing["amount"]), "unit": "piece"} for ing in ingredients],
            "instructions": ["Could not generate recipe"],
            "prepTime": 0,
            "cookTime": 0,
            "servings": 0,
            "dietary": [],
            "requiredItems": [],
            "expiringSoon": False,
            "matchingPercentage": 0
        }])

@app.route('/generate_recipes', methods=['POST'])
def recipe_endpoint():
    try:
        data = request.json
        ingredients = data.get('ingredients', [])
        
        print(f"Received ingredients: {ingredients}")
        
        # Generate recipes using the ingredients
        recipes_response = generate_recipes(ingredients)
        print(f"Raw Gemini response: {recipes_response}")

        try:
            # Parse the response to ensure it's valid JSON
            parsed_recipes = json.loads(recipes_response)
            
            # Ensure the response matches the expected format
            formatted_recipes = []
            for recipe in parsed_recipes:
                formatted_recipe = {
                    "id": recipe.get("id", f"recipe-{int(time.time())}"),
                    "name": recipe.get("name", "Untitled Recipe"),
                    "category": recipe.get("category", "Other"),
                    "description": recipe.get("description", "No description available"),
                    "ingredients": recipe.get("ingredients", []),
                    "instructions": recipe.get("instructions", []),
                    "prepTime": recipe.get("prepTime", 0),
                    "cookTime": recipe.get("cookTime", 0),
                    "servings": recipe.get("servings", 4),
                    "dietary": recipe.get("dietary", []),
                    "requiredItems": recipe.get("requiredItems", []),
                    "expiringSoon": recipe.get("expiringSoon", False),
                    "matchingPercentage": recipe.get("matchingPercentage", 0)
                }
                formatted_recipes.append(formatted_recipe)

            return jsonify({
                'success': True,
                'recipes': formatted_recipes
            })

        except json.JSONDecodeError as e:
            print(f"JSON parse error: {e}")
            print(f"Invalid JSON response: {recipes_response}")
            
            fallback_recipe = {
                "id": f"error-{int(time.time())}",
                "name": "Sample Recipe",
                "category": "Dinner",
                "description": "Failed to parse recipe data. Please try again.",
                "ingredients": [{"name": ing["name"], "amount": str(ing["amount"]), "unit": "piece"} for ing in ingredients],
                "instructions": ["Could not generate instructions"],
                "prepTime": 0,
                "cookTime": 0,
                "servings": 0,
                "dietary": [],
                "requiredItems": [],
                "expiringSoon": False,
                "matchingPercentage": 0
            }
            
            return jsonify({
                'success': True,
                'recipes': [fallback_recipe]
            })
        
    except Exception as e:
        print(f"Server error: {str(e)}")
        return jsonify({
 
[truncated — 7170 more characters]
```

### hooks/useColorScheme.ts

```typescript
export { useColorScheme } from 'react-native';

```

### components/ThemedView.tsx

```typescript
import { View, type ViewProps } from 'react-native';

import { useThemeColor } from '@/hooks/useThemeColor';

export type ThemedViewProps = ViewProps & {
  lightColor?: string;
  darkColor?: string;
};

export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
  const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');

  return <View style={[{ backgroundColor }, style]} {...otherProps} />;
}

```

### hooks/useColorScheme.web.ts

```typescript
import { useEffect, useState } from 'react';
import { useColorScheme as useRNColorScheme } from 'react-native';

/**
 * To support static rendering, this value needs to be re-calculated on the client side for web
 */
export function useColorScheme() {
  const [hasHydrated, setHasHydrated] = useState(false);

  useEffect(() => {
    setHasHydrated(true);
  }, []);

  const colorScheme = useRNColorScheme();

  if (hasHydrated) {
    return colorScheme;
  }

  return 'light';
}

```

[24 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]