diff --git a/PRIVACY_SETUP.md b/PRIVACY_SETUP.md
deleted file mode 100644
index 0dc55e8..0000000
--- a/PRIVACY_SETUP.md
+++ /dev/null
@@ -1,88 +0,0 @@
-# Privacy Configuration for SousChefAI
-
-## Camera Permission Setup (Required)
-
-The app needs camera access to scan ingredients and monitor cooking. Follow these steps to add the required privacy descriptions:
-
-### Method 1: Using Xcode Target Settings (Recommended)
-
-1. Open the project in Xcode
-2. Select the **SousChefAI** target in the project navigator
-3. Go to the **Info** tab
-4. Under "Custom iOS Target Properties", click the **+** button
-5. Add the following keys with their values:
-
-**Camera Permission:**
-- **Key**: `Privacy - Camera Usage Description`
-- **Value**: `SousChefAI needs camera access to scan your fridge for ingredients and monitor your cooking progress in real-time.`
-
-**Microphone Permission (for voice guidance):**
-- **Key**: `Privacy - Microphone Usage Description`
-- **Value**: `SousChefAI uses the microphone to provide voice-guided cooking instructions.`
-
-### Method 2: Manual Info.plist (Alternative)
-
-If you prefer to manually edit the Info.plist:
-
-1. In Xcode, right-click on the SousChefAI folder
-2. Select **New File** → **Property List**
-3. Name it `Info.plist`
-4. Add these entries:
-
-```xml
-NSCameraUsageDescription
-SousChefAI needs camera access to scan your fridge for ingredients and monitor your cooking progress in real-time.
-
-NSMicrophoneUsageDescription
-SousChefAI uses the microphone to provide voice-guided cooking instructions.
-```
-
-## Verifying the Setup
-
-After adding the privacy descriptions:
-
-1. Clean the build folder: **Product → Clean Build Folder** (⌘ + Shift + K)
-2. Rebuild the project: **Product → Build** (⌘ + B)
-3. Run on a device or simulator
-4. When you first open the Scanner view, you should see a permission dialog
-
-## Troubleshooting
-
-### "App crashed when accessing camera"
-- Ensure you added `NSCameraUsageDescription` to the target's Info settings
-- Clean and rebuild the project
-- Restart Xcode if the permission isn't taking effect
-
-### "Permission dialog not appearing"
-- Check that the Info settings were saved
-- Try deleting the app from the simulator/device and reinstalling
-- Reset privacy settings on the simulator: **Device → Erase All Content and Settings**
-
-### "Multiple Info.plist errors"
-- Modern Xcode projects use automatic Info.plist generation
-- Use Method 1 (Target Settings) instead of creating a manual file
-- If you created Info.plist manually, make sure to configure the build settings to use it
-
-## Privacy Manifest
-
-The `PrivacyInfo.xcprivacy` file is included for App Store compliance. This declares:
-- No tracking
-- No third-party SDK tracking domains
-- Camera access is for app functionality only
-
-## Testing Camera Permissions
-
-1. Build and run the app
-2. Navigate to the **Scan** tab
-3. You should see a permission dialog
-4. Grant camera access
-5. The camera preview should appear
-
-If permission is denied:
-- Go to **Settings → Privacy & Security → Camera**
-- Find **SousChefAI** and enable it
-- Relaunch the app
-
----
-
-**Note**: These privacy descriptions are required by Apple's App Store guidelines. Apps that access camera without proper usage descriptions will be rejected.
diff --git a/PROJECT_SUMMARY.md b/PROJECT_SUMMARY.md
deleted file mode 100644
index 7657ab9..0000000
--- a/PROJECT_SUMMARY.md
+++ /dev/null
@@ -1,256 +0,0 @@
-# SousChefAI - Project Summary
-
-## 📱 Project Overview
-
-**SousChefAI** is a production-ready iOS application that leverages multimodal AI to transform cooking. Users can scan their fridge to detect ingredients, receive personalized recipe suggestions, and get real-time cooking guidance through computer vision.
-
-## 🎯 Key Features
-
-### 1. AI-Powered Ingredient Scanner
-- Real-time video inference using Overshoot API
-- Confidence scoring for each detected item
-- Manual entry fallback
-- Low-confidence item highlighting
-
-### 2. Intelligent Recipe Generation
-- Google Gemini 2.0 for complex reasoning
-- "The Scavenger" mode: uses only available ingredients
-- "The Upgrader" mode: requires 1-2 additional items
-- Recipe scaling based on limiting ingredients
-- Match score prioritization (0.0-1.0)
-
-### 3. Live Cooking Assistant
-- Step-by-step guidance with progress tracking
-- Real-time visual monitoring of cooking progress
-- Text-to-speech announcements for hands-free operation
-- AI feedback when steps are complete
-- Haptic feedback for completion events
-
-### 4. User Profiles & Preferences
-- Dietary restrictions (Vegan, Keto, Gluten-Free, etc.)
-- Nutrition goals
-- Pantry staples management
-- Firebase cloud sync (optional)
-
-## 🏗️ Architecture
-
-### Design Pattern
-**MVVM (Model-View-ViewModel) + Repository Pattern**
-
-```
-┌─────────────┐
-│ Views │ (SwiftUI)
-└─────┬───────┘
- │
-┌─────▼───────┐
-│ ViewModels │ (@MainActor, ObservableObject)
-└─────┬───────┘
- │
-┌─────▼───────┐
-│ Services │ (Protocol-based)
-└─────┬───────┘
- │
-┌─────▼───────┐
-│ APIs/Cloud │ (Overshoot, Gemini, Firebase)
-└─────────────┘
-```
-
-### Protocol-Oriented Design
-
-**Vision Service:**
-```swift
-protocol VisionService: Sendable {
- func detectIngredients(from: AsyncStream) async throws -> [Ingredient]
- func analyzeCookingProgress(from: AsyncStream, for: String) async throws -> CookingProgress
-}
-```
-
-**Recipe Service:**
-```swift
-protocol RecipeService: Sendable {
- func generateRecipes(inventory: [Ingredient], profile: UserProfile) async throws -> [Recipe]
- func scaleRecipe(_: Recipe, for: Ingredient, quantity: String) async throws -> Recipe
-}
-```
-
-This design allows easy swapping of AI providers (e.g., OpenAI, Anthropic, etc.) without changing business logic.
-
-## 📁 Complete File Structure
-
-```
-SousChefAI/
-├── SousChefAI/
-│ ├── Config/
-│ │ └── AppConfig.swift # API keys and feature flags
-│ │
-│ ├── Models/
-│ │ ├── Ingredient.swift # Ingredient data model
-│ │ ├── UserProfile.swift # User preferences and restrictions
-│ │ └── Recipe.swift # Recipe with categorization
-│ │
-│ ├── Services/
-│ │ ├── VisionService.swift # Vision protocol definition
-│ │ ├── OvershootVisionService.swift # Overshoot implementation
-│ │ ├── RecipeService.swift # Recipe protocol definition
-│ │ ├── GeminiRecipeService.swift # Gemini implementation
-│ │ ├── FirestoreRepository.swift # Firebase data layer
-│ │ └── CameraManager.swift # AVFoundation camera handling
-│ │
-│ ├── ViewModels/
-│ │ ├── ScannerViewModel.swift # Scanner business logic
-│ │ ├── RecipeGeneratorViewModel.swift # Recipe generation logic
-│ │ └── CookingModeViewModel.swift # Cooking guidance logic
-│ │
-│ ├── Views/
-│ │ ├── ScannerView.swift # Camera scanning UI
-│ │ ├── InventoryView.swift # Ingredient management UI
-│ │ ├── RecipeGeneratorView.swift # Recipe browsing UI
-│ │ └── CookingModeView.swift # Step-by-step cooking UI
-│ │
-│ ├── ContentView.swift # Tab-based navigation
-│ ├── SousChefAIApp.swift # App entry point
-│ └── Assets.xcassets # App icons and images
-│
-├── Documentation/
-│ ├── README.md # Complete documentation
-│ ├── QUICKSTART.md # 5-minute setup checklist
-│ ├── SETUP_GUIDE.md # Detailed setup instructions
-│ ├── PRIVACY_SETUP.md # Camera permission guide
-│ └── PROJECT_SUMMARY.md # This file
-│
-├── PrivacyInfo.xcprivacy # Privacy manifest
-└── Tests/
- ├── SousChefAITests/
- └── SousChefAIUITests/
-```
-
-## 🛠️ Technology Stack
-
-| Category | Technology | Purpose |
-|----------|-----------|---------|
-| Language | Swift 6 | Type-safe, concurrent programming |
-| UI Framework | SwiftUI | Declarative, modern UI |
-| Concurrency | async/await | Native Swift concurrency |
-| Camera | AVFoundation | Video capture and processing |
-| Vision AI | Overshoot API | Real-time video inference |
-| Reasoning AI | Google Gemini 2.0 | Recipe generation and logic |
-| Backend | Firebase | Authentication and Firestore |
-| Persistence | Firestore | Cloud-synced data storage |
-| Architecture | MVVM | Separation of concerns |
-
-## 📊 Code Statistics
-
-- **Total Swift Files**: 17
-- **Lines of Code**: ~8,000+
-- **Models**: 3 (Ingredient, UserProfile, Recipe)
-- **Services**: 6 (protocols + implementations)
-- **ViewModels**: 3
-- **Views**: 4 main views + supporting components
-
-## 🔑 Configuration Requirements
-
-### Required (for full functionality)
-1. **Camera Privacy Description** - App will crash without this
-2. **Overshoot API Key** - For ingredient detection
-3. **Gemini API Key** - For recipe generation
-
-### Optional
-1. **Firebase Configuration** - For cloud sync
-2. **Microphone Privacy** - For voice features
-
-## 🚀 Build Status
-
-✅ **Project builds successfully** with Xcode 15.0+
-✅ **Swift 6 compliant** with strict concurrency
-✅ **iOS 17.0+ compatible**
-✅ **No compiler warnings**
-
-## 📱 User Flow
-
-1. **Launch** → Tab bar with 4 sections
-2. **Scan Tab** → Point camera at fridge → Detect ingredients
-3. **Inventory** → Review & edit items → Set preferences
-4. **Generate** → AI creates recipe suggestions
-5. **Cook** → Step-by-step with live monitoring
-
-## 🎨 UI Highlights
-
-- **Clean Apple HIG compliance**
-- **Material blur overlays** for camera views
-- **Confidence indicators** (green/yellow/red)
-- **Real-time progress bars**
-- **Haptic feedback** for important events
-- **Dark mode support** (automatic)
-
-## 🔒 Privacy & Security
-
-- **Privacy Manifest** included (PrivacyInfo.xcprivacy)
-- **Camera usage clearly described**
-- **No tracking or analytics**
-- **API keys marked for replacement** (not committed)
-- **Local-first architecture** (works offline for inventory)
-
-## 🧪 Testing Strategy
-
-### Unit Tests
-- Model encoding/decoding
-- Service protocol conformance
-- ViewModel business logic
-
-### UI Tests
-- Tab navigation
-- Camera permission flow
-- Recipe filtering
-- Step progression in cooking mode
-
-## 🔄 Future Enhancements
-
-Potential features for future versions:
-
-- [ ] Nutrition tracking and calorie counting
-- [ ] Shopping list generation with store integration
-- [ ] Social features (recipe sharing)
-- [ ] Meal planning calendar
-- [ ] Apple Watch companion app
-- [ ] Widgets for quick recipe access
-- [ ] Offline mode with Core ML models
-- [ ] Multi-language support
-- [ ] Voice commands during cooking
-- [ ] Smart appliance integration
-
-## 📚 Documentation Files
-
-1. **README.md** - Complete feature documentation
-2. **QUICKSTART.md** - 5-minute setup checklist
-3. **SETUP_GUIDE.md** - Step-by-step configuration
-4. **PRIVACY_SETUP.md** - Camera permission details
-5. **PROJECT_SUMMARY.md** - Architecture overview (this file)
-
-## 🤝 Contributing
-
-The codebase follows these principles:
-
-1. **Protocol-oriented design** for service abstractions
-2. **Async/await** for all asynchronous operations
-3. **@MainActor** for UI-related classes
-4. **Sendable** conformance for concurrency safety
-5. **SwiftUI best practices** with MVVM
-6. **Clear separation** between layers
-
-## 📄 License
-
-MIT License - See LICENSE file for details
-
-## 👏 Acknowledgments
-
-- **Overshoot AI** - Low-latency video inference
-- **Google Gemini** - Advanced reasoning capabilities
-- **Firebase** - Scalable backend infrastructure
-- **Apple** - SwiftUI and AVFoundation frameworks
-
----
-
-**Built with Swift 6 + SwiftUI**
-**Production-ready for iOS 17.0+**
-
-Last Updated: February 2026
diff --git a/PrivacyInfo.xcprivacy b/PrivacyInfo.xcprivacy
deleted file mode 100644
index 5397adc..0000000
--- a/PrivacyInfo.xcprivacy
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- NSPrivacyAccessedAPITypes
-
- NSPrivacyCollectedDataTypes
-
- NSPrivacyTracking
-
- NSPrivacyTrackingDomains
-
-
-
diff --git a/QUICKSTART.md b/QUICKSTART.md
deleted file mode 100644
index 9bc924f..0000000
--- a/QUICKSTART.md
+++ /dev/null
@@ -1,139 +0,0 @@
-# SousChefAI - Quick Start Checklist ✅
-
-Get up and running in 5 minutes!
-
-## Prerequisites Check
-
-- [ ] macOS 14.0+ with Xcode 15.0+
-- [ ] iOS 17.0+ device or simulator
-- [ ] Internet connection
-
-## Step-by-Step Setup
-
-### 1️⃣ Configure Privacy (CRITICAL - App will crash without this!)
-
-**In Xcode:**
-1. Select the **SousChefAI** target
-2. Go to **Info** tab
-3. Click **+** under "Custom iOS Target Properties"
-4. Add:
- - Key: `Privacy - Camera Usage Description`
- - Value: `SousChefAI needs camera access to scan your fridge for ingredients and monitor your cooking progress in real-time.`
-5. Click **+** again and add:
- - Key: `Privacy - Microphone Usage Description`
- - Value: `SousChefAI uses the microphone to provide voice-guided cooking instructions.`
-
-✅ **Status**: [ ] Privacy descriptions added
-
-### 2️⃣ Add API Keys
-
-**File**: `SousChefAI/Config/AppConfig.swift`
-
-Replace:
-```swift
-static let overshootAPIKey = "INSERT_KEY_HERE"
-static let geminiAPIKey = "INSERT_KEY_HERE"
-```
-
-With your actual API keys from:
-- **Overshoot**: [Your Overshoot Provider]
-- **Gemini**: https://makersuite.google.com/app/apikey
-
-✅ **Status**:
-- [ ] Overshoot API key added
-- [ ] Gemini API key added
-
-### 3️⃣ Add Firebase (Optional - for cloud sync)
-
-**Add Package:**
-1. File → Add Package Dependencies
-2. URL: `https://github.com/firebase/firebase-ios-sdk`
-3. Add products: `FirebaseAuth`, `FirebaseFirestore`
-
-**Configure:**
-1. Download `GoogleService-Info.plist` from Firebase Console
-2. Drag into Xcode (ensure it's added to target)
-3. Uncomment in `SousChefAIApp.swift`:
-```swift
-import FirebaseCore
-
-init() {
- FirebaseApp.configure()
-}
-```
-
-✅ **Status**:
-- [ ] Firebase package added
-- [ ] GoogleService-Info.plist added
-- [ ] Firebase initialized
-
-### 4️⃣ Build & Run
-
-1. Open `SousChefAI.xcodeproj`
-2. Select target device (iOS 17.0+)
-3. Press **⌘ + R**
-4. Grant camera permission when prompted
-
-✅ **Status**: [ ] App running successfully
-
-## Minimum Viable Setup (Test Mode)
-
-Want to just see the UI without external services?
-
-**Required:**
-- ✅ Privacy descriptions (Step 1)
-
-**Optional:**
-- ⚠️ API keys (will show errors but UI works)
-- ⚠️ Firebase (uses local data only)
-
-## Verification
-
-After setup, test these features:
-
-- [ ] Scanner tab opens camera
-- [ ] Can add manual ingredients
-- [ ] Inventory view displays items
-- [ ] Profile tab shows configuration status
-- [ ] No crash when opening camera
-
-## Common Issues
-
-### ❌ App crashes immediately when opening Scanner
-→ **Fix**: Add camera privacy description (Step 1)
-
-### ❌ "API Key Missing" errors
-→ **Fix**: Replace "INSERT_KEY_HERE" in AppConfig.swift (Step 2)
-
-### ❌ "Module 'Firebase' not found"
-→ **Fix**: Add Firebase package via SPM (Step 3)
-
-### ❌ Camera permission dialog doesn't appear
-→ **Fix**: Delete app, clean build (⌘+Shift+K), rebuild, reinstall
-
-## Next Steps
-
-Once running:
-
-1. **Scan Mode**: Point camera at ingredients → tap "Scan Fridge"
-2. **Inventory**: Review detected items → edit quantities → set preferences
-3. **Generate Recipes**: Tap "Generate Recipes" → browse suggestions
-4. **Cook**: Select recipe → "Start Cooking" → enable AI monitoring
-
-## Documentation
-
-- **Full Guide**: [SETUP_GUIDE.md](SETUP_GUIDE.md)
-- **Privacy**: [PRIVACY_SETUP.md](PRIVACY_SETUP.md)
-- **Architecture**: [README.md](README.md)
-
-## Support
-
-Issues? Check:
-1. Privacy descriptions are added ✓
-2. API keys are valid strings (not "INSERT_KEY_HERE") ✓
-3. Target is iOS 17.0+ ✓
-4. Clean build folder and rebuild ✓
-
----
-
-**Ready to cook with AI! 🍳**
diff --git a/README.md b/README.md
deleted file mode 100644
index df25687..0000000
--- a/README.md
+++ /dev/null
@@ -1,256 +0,0 @@
-Note - this is an AI generated readme, and will be updated in the future.
-# SousChefAI
-A production-ready iOS app that uses multimodal AI to scan ingredients, generate personalized recipes, and provide real-time cooking guidance.
-
-## Features
-
-### 🎥 Intelligent Fridge Scanner
-- Real-time ingredient detection using Overshoot API
-- Camera-based scanning with live preview
-- Confidence scoring for each detected ingredient
-- Manual ingredient entry and editing
-
-### 🍳 AI-Powered Recipe Generation
-- Personalized recipe suggestions based on available ingredients
-- Google Gemini AI for complex reasoning and recipe creation
-- Filtering by "Scavenger" (use only what you have) or "Upgrader" (minimal shopping)
-- Recipe scaling based on limiting ingredients
-- Match scoring to prioritize best recipes
-
-### 👨🍳 Live Cooking Mode
-- Step-by-step guided cooking
-- Real-time visual monitoring of cooking progress
-- Text-to-speech announcements for hands-free cooking
-- AI feedback when steps are complete
-- Progress tracking and navigation
-
-### 🔐 User Profiles & Persistence
-- Firebase Firestore for cloud data sync
-- Dietary restrictions (Vegan, Keto, Gluten-Free, etc.)
-- Nutrition goals
-- Saved recipes and pantry staples
-
-## Architecture
-
-The app follows **MVVM (Model-View-ViewModel)** with a **Repository Pattern** for clean separation of concerns:
-
-```
-├── Models/ # Core data models (Codable, Identifiable)
-│ ├── Ingredient.swift
-│ ├── UserProfile.swift
-│ └── Recipe.swift
-│
-├── Services/ # Business logic & external APIs
-│ ├── VisionService.swift # Protocol for vision AI
-│ ├── OvershootVisionService.swift # Overshoot implementation
-│ ├── RecipeService.swift # Protocol for recipe generation
-│ ├── GeminiRecipeService.swift # Gemini implementation
-│ ├── FirestoreRepository.swift # Firebase data layer
-│ └── CameraManager.swift # AVFoundation camera handling
-│
-├── ViewModels/ # Business logic for views
-│ ├── ScannerViewModel.swift
-│ ├── RecipeGeneratorViewModel.swift
-│ └── CookingModeViewModel.swift
-│
-├── Views/ # SwiftUI views
-│ ├── ScannerView.swift
-│ ├── InventoryView.swift
-│ ├── RecipeGeneratorView.swift
-│ └── CookingModeView.swift
-│
-└── Config/ # App configuration
- └── AppConfig.swift
-```
-
-## Setup Instructions
-
-### 1. Clone the Repository
-```bash
-git clone https://github.com/yourusername/souschef.git
-cd souschef
-```
-
-### 2. Configure API Keys
-
-Open `SousChefAI/Config/AppConfig.swift` and replace the placeholder values:
-
-```swift
-// Overshoot Vision API
-static let overshootAPIKey = "YOUR_OVERSHOOT_API_KEY"
-
-// Google Gemini API
-static let geminiAPIKey = "YOUR_GEMINI_API_KEY"
-```
-
-**Getting API Keys:**
-- **Overshoot API**: Visit [overshoot.ai](https://overshoot.ai) (or the actual provider URL) and sign up
-- **Gemini API**: Visit [Google AI Studio](https://makersuite.google.com/app/apikey) and create an API key
-
-### 3. Add Firebase
-
-#### Add Firebase SDK via Swift Package Manager:
-1. In Xcode: `File` > `Add Package Dependencies`
-2. Enter URL: `https://github.com/firebase/firebase-ios-sdk`
-3. Select version: `10.0.0` or later
-4. Add the following products:
- - `FirebaseAuth`
- - `FirebaseFirestore`
-
-#### Add GoogleService-Info.plist:
-1. Go to [Firebase Console](https://console.firebase.google.com/)
-2. Create a new project or select existing
-3. Add an iOS app with bundle ID: `com.yourcompany.SousChefAI`
-4. Download `GoogleService-Info.plist`
-5. Drag it into your Xcode project (ensure it's added to the SousChefAI target)
-
-#### Enable Firebase in App:
-1. Open `SousChefAI/SousChefAIApp.swift`
-2. Uncomment the Firebase imports and initialization:
-```swift
-import FirebaseCore
-
-init() {
- FirebaseApp.configure()
-}
-```
-
-### 4. Add Google Generative AI SDK (Optional)
-
-For better Gemini integration, add the official SDK:
-
-```swift
-// In Xcode: File > Add Package Dependencies
-// URL: https://github.com/google/generative-ai-swift
-```
-
-Then update `GeminiRecipeService.swift` to use the SDK instead of REST API.
-
-### 5. Configure Camera Permissions
-
-The app requires camera access. Permissions are already handled in code, but ensure your `Info.plist` includes:
-
-```xml
-NSCameraUsageDescription
-We need camera access to scan your fridge and monitor cooking progress
-```
-
-### 6. Build and Run
-
-1. Open `SousChefAI.xcodeproj` in Xcode
-2. Select your target device or simulator
-3. Press `Cmd + R` to build and run
-
-## Usage Guide
-
-### Scanning Your Fridge
-1. Tap the **Scan** tab
-2. Point your camera at your fridge or ingredients
-3. Tap **Scan Fridge** to start detection
-4. Review detected ingredients (yellow = low confidence)
-5. Tap **Continue to Inventory**
-
-### Managing Inventory
-1. Edit quantities by tapping an ingredient
-2. Swipe left to delete items
-3. Add manual entries with the `+` button
-4. Set dietary preferences before generating recipes
-5. Tap **Generate Recipes** when ready
-
-### Generating Recipes
-1. Browse suggested recipes sorted by match score
-2. Filter by:
- - **All Recipes**: Show everything
- - **The Scavenger**: Only use what you have
- - **The Upgrader**: Need 1-2 items max
- - **High Match**: 80%+ ingredient match
-3. Tap a recipe to view details
-4. Save favorites with the heart icon
-5. Start cooking with **Start Cooking** button
-
-### Cooking Mode
-1. Enable **AI Monitoring** to watch your cooking
-2. The AI will analyze your progress visually
-3. Navigate steps with Previous/Next
-4. Use **Read Aloud** for hands-free guidance
-5. The AI will announce when steps are complete
-6. View all steps with the list icon
-
-## Tech Stack
-
-- **Language**: Swift 6
-- **UI Framework**: SwiftUI
-- **Architecture**: MVVM + Repository Pattern
-- **Concurrency**: Swift Async/Await (no completion handlers)
-- **Camera**: AVFoundation
-- **Vision AI**: Overshoot API (real-time video inference)
-- **Reasoning AI**: Google Gemini 2.0 Flash
-- **Backend**: Firebase (Auth + Firestore)
-- **Persistence**: Firebase Firestore (cloud sync)
-
-## Protocol-Oriented Design
-
-The app uses protocols for AI services to enable easy provider swapping:
-
-```swift
-protocol VisionService {
- func detectIngredients(from: AsyncStream) async throws -> [Ingredient]
-}
-
-protocol RecipeService {
- func generateRecipes(inventory: [Ingredient], profile: UserProfile) async throws -> [Recipe]
-}
-```
-
-To swap providers, simply create a new implementation:
-
-```swift
-final class OpenAIVisionService: VisionService {
- // Implementation using OpenAI Vision API
-}
-
-final class AnthropicRecipeService: RecipeService {
- // Implementation using Claude API
-}
-```
-
-## Future Enhancements
-
-- [ ] Nutrition tracking and calorie counting
-- [ ] Shopping list generation
-- [ ] Recipe sharing and social features
-- [ ] Meal planning calendar
-- [ ] Voice commands during cooking
-- [ ] Multi-language support
-- [ ] Apple Watch companion app
-- [ ] Widget for quick recipe access
-- [ ] Offline mode with local ML models
-- [ ] Integration with smart kitchen appliances
-
-## Contributing
-
-Contributions are welcome! Please follow these guidelines:
-
-1. Fork the repository
-2. Create a feature branch: `git checkout -b feature/amazing-feature`
-3. Follow Swift style guide and existing architecture
-4. Write unit tests for new features
-5. Update documentation as needed
-6. Submit a pull request
-
-## License
-
-This project is licensed under the MIT License - see the LICENSE file for details.
-
-## Acknowledgments
-
-- Overshoot AI for low-latency video inference
-- Google Gemini for powerful reasoning capabilities
-- Firebase for robust backend infrastructure
-- Apple for SwiftUI and AVFoundation frameworks
-
-## Support
-
-For issues, questions, or feature requests, please open an issue on GitHub.
-
----
diff --git a/SETUP_GUIDE.md b/SETUP_GUIDE.md
deleted file mode 100644
index c7cec04..0000000
--- a/SETUP_GUIDE.md
+++ /dev/null
@@ -1,203 +0,0 @@
-# SousChefAI - Quick Setup Guide
-
-This guide will help you get SousChefAI up and running.
-
-## Prerequisites
-
-- macOS 14.0 or later
-- Xcode 15.0 or later
-- iOS 17.0+ device or simulator
-- Active internet connection for API calls
-
-## Step 1: Configure API Keys
-
-### Overshoot Vision API
-
-1. Visit the Overshoot API provider website and create an account
-2. Generate an API key for video inference
-3. Open `SousChefAI/Config/AppConfig.swift`
-4. Replace `INSERT_KEY_HERE` with your Overshoot API key:
-
-```swift
-static let overshootAPIKey = "your_overshoot_api_key_here"
-```
-
-### Google Gemini API
-
-1. Visit [Google AI Studio](https://makersuite.google.com/app/apikey)
-2. Sign in with your Google account
-3. Create a new API key
-4. In `SousChefAI/Config/AppConfig.swift`, replace:
-
-```swift
-static let geminiAPIKey = "your_gemini_api_key_here"
-```
-
-## Step 2: Add Firebase (Optional but Recommended)
-
-### Add Firebase SDK
-
-1. In Xcode, go to `File` > `Add Package Dependencies`
-2. Enter: `https://github.com/firebase/firebase-ios-sdk`
-3. Select version `10.0.0` or later
-4. Add these products to your target:
- - FirebaseAuth
- - FirebaseFirestore
-
-### Configure Firebase Project
-
-1. Go to [Firebase Console](https://console.firebase.google.com/)
-2. Create a new project or select existing
-3. Click "Add app" and select iOS
-4. Enter bundle identifier: `com.yourcompany.SousChefAI`
-5. Download `GoogleService-Info.plist`
-6. Drag the file into your Xcode project (ensure it's added to the SousChefAI target)
-
-### Enable Firebase in Code
-
-1. Open `SousChefAI/SousChefAIApp.swift`
-2. Uncomment these lines:
-
-```swift
-import FirebaseCore
-
-init() {
- FirebaseApp.configure()
-}
-```
-
-### Configure Firestore Database
-
-1. In Firebase Console, go to Firestore Database
-2. Click "Create database"
-3. Start in test mode (or production mode with proper rules)
-4. Choose a location close to your users
-
-## Step 3: Configure Camera Permissions (CRITICAL)
-
-⚠️ **The app will crash without this step!**
-
-### Add Privacy Descriptions in Xcode
-
-1. In Xcode, select the **SousChefAI** target
-2. Go to the **Info** tab
-3. Under "Custom iOS Target Properties", click the **+** button
-4. Add these two keys:
-
-**Camera Permission:**
-- **Key**: `Privacy - Camera Usage Description` (or `NSCameraUsageDescription`)
-- **Value**: `SousChefAI needs camera access to scan your fridge for ingredients and monitor your cooking progress in real-time.`
-
-**Microphone Permission:**
-- **Key**: `Privacy - Microphone Usage Description` (or `NSMicrophoneUsageDescription`)
-- **Value**: `SousChefAI uses the microphone to provide voice-guided cooking instructions.`
-
-📖 See [PRIVACY_SETUP.md](PRIVACY_SETUP.md) for detailed step-by-step instructions with screenshots.
-
-## Step 4: Build and Run
-
-1. Open `SousChefAI.xcodeproj` in Xcode
-2. Select your target device (iOS 17.0+ required)
-3. Press `⌘ + R` to build and run
-4. Allow camera permissions when prompted
-
-## Testing Without API Keys
-
-If you want to test the UI without API keys:
-
-1. The app will show placeholder data and errors for API calls
-2. You can still navigate through the UI
-3. Manual ingredient entry will work
-4. Recipe generation will fail gracefully
-
-## Troubleshooting
-
-### Build Errors
-
-**"Missing GoogleService-Info.plist"**
-- Ensure the file is in your project and added to the target
-- Check that it's not in a subdirectory
-
-**"Module 'Firebase' not found"**
-- Make sure you added the Firebase package correctly
-- Clean build folder: `⌘ + Shift + K`
-- Rebuild: `⌘ + B`
-
-**"API Key Missing" errors**
-- Check that you replaced "INSERT_KEY_HERE" in AppConfig.swift
-- API keys should be strings without quotes inside the quotes
-
-### Runtime Errors
-
-**"Camera access denied"**
-- Go to Settings > Privacy & Security > Camera
-- Enable camera access for SousChefAI
-
-**"Network request failed"**
-- Check internet connection
-- Verify API keys are valid
-- Check API endpoint URLs in AppConfig.swift
-
-**"Firebase configuration error"**
-- Ensure GoogleService-Info.plist is properly added
-- Verify Firebase initialization is uncommented
-- Check Firestore is enabled in Firebase Console
-
-## Architecture Overview
-
-The app follows MVVM architecture with clean separation:
-
-```
-Views → ViewModels → Services → APIs/Firebase
- ↓ ↓ ↓
-Models ← Repository ← Firestore
-```
-
-## Next Steps
-
-Once the app is running:
-
-1. **Test the Scanner**: Point camera at ingredients and scan
-2. **Review Inventory**: Edit quantities and add items manually
-3. **Set Preferences**: Configure dietary restrictions
-4. **Generate Recipes**: Get AI-powered recipe suggestions
-5. **Cooking Mode**: Try the live cooking assistant
-
-## Optional Enhancements
-
-### Add Google Generative AI SDK
-
-For better Gemini integration:
-
-1. Add package: `https://github.com/google/generative-ai-swift`
-2. Update `GeminiRecipeService.swift` to use the SDK
-3. Uncomment the SDK-based code in the service
-
-### Configure Overshoot WebSocket
-
-If using WebSocket for real-time detection:
-
-1. Update `overshootWebSocketURL` in AppConfig.swift
-2. Verify the WebSocket endpoint with Overshoot documentation
-3. Test real-time detection in Scanner view
-
-## Support
-
-For issues or questions:
-- Check the main [README.md](README.md)
-- Open an issue on GitHub
-- Review the inline documentation in code files
-
-## Security Notes
-
-⚠️ **Important**: Never commit API keys to version control!
-
-Consider:
-- Using environment variables for keys
-- Adding `AppConfig.swift` to `.gitignore` (but keep a template)
-- Using a secrets management service in production
-- Rotating keys regularly
-
----
-
-**You're all set! Happy cooking with SousChefAI! 🍳**
diff --git a/SWIFT6_WARNINGS.md b/SWIFT6_WARNINGS.md
deleted file mode 100644
index c1d8723..0000000
--- a/SWIFT6_WARNINGS.md
+++ /dev/null
@@ -1,136 +0,0 @@
-# Swift 6 Concurrency Warnings - Explained
-
-## Summary
-
-The SousChefAI project builds successfully with **only 4 unavoidable Swift 6 concurrency warnings**. These warnings are related to Core Video framework types that haven't been updated for Swift 6 Sendable conformance yet.
-
-## Remaining Warnings (4 total)
-
-### 1-3. CVPixelBuffer / AsyncStream Not Sendable
-
-**Files**: `OvershootVisionService.swift` (lines 36, 79, 88)
-
-**Warning Messages**:
-- "Non-Sendable parameter type 'AsyncStream' cannot be sent..."
-- "Non-Sendable parameter type 'CVPixelBuffer' cannot be sent..."
-
-**Why This Happens**:
-- Core Video's `CVPixelBuffer` (aka `CVBuffer`) hasn't been marked as `Sendable` by Apple yet
-- This is a framework limitation, not a code issue
-
-**Why It's Safe**:
-- `CVPixelBuffer` is **thread-safe** and **immutable** by design
-- The underlying C API uses reference counting and atomic operations
-- We use `@preconcurrency import CoreVideo` to acknowledge this
-- The service is marked `@unchecked Sendable` which tells Swift we've verified thread safety
-
-**Resolution**:
-✅ **These warnings are expected and safe to ignore**
-- They will be resolved when Apple updates Core Video for Swift 6
-- The code is correct and thread-safe
-
-### 4. Configuration Warning
-
-**File**: `SousChefAI.xcodeproj`
-
-**Warning**: "Update to recommended settings"
-
-**Why This Happens**:
-- Xcode periodically suggests updating project settings to latest recommendations
-
-**Resolution**:
-⚠️ **Optional** - You can update project settings in Xcode:
-1. Click on the warning in Issue Navigator
-2. Click "Update to Recommended Settings"
-3. Review and accept the changes
-
-This won't affect functionality - it just updates build settings to Apple's latest recommendations.
-
-## What We Fixed ✅
-
-During the warning cleanup, we successfully resolved:
-
-1. ✅ **CameraManager concurrency issues**
- - Added `nonisolated(unsafe)` for AVFoundation types
- - Fixed capture session isolation
- - Resolved frame continuation thread safety
-
-2. ✅ **Service initialization warnings**
- - Made service initializers `nonisolated`
- - Fixed ViewModel initialization context
-
-3. ✅ **FirestoreRepository unused variable warnings**
- - Changed `guard let userId = userId` to `guard userId != nil`
- - Removed 8 unnecessary variable bindings
-
-4. ✅ **Unnecessary await warnings**
- - Removed `await` from synchronous function calls
- - Fixed in ScannerViewModel and CookingModeViewModel
-
-5. ✅ **AppConfig isolation**
- - Verified String constants are properly Sendable
-
-## Build Status
-
-- **Build Result**: ✅ **SUCCESS**
-- **Error Count**: 0
-- **Warning Count**: 4 (all unavoidable Core Video framework issues)
-- **Swift 6 Mode**: ✅ Enabled and passing
-- **Strict Concurrency**: ✅ Enabled
-
-## Recommendations
-
-### For Development
-The current warnings can be safely ignored. The code is production-ready and follows Swift 6 best practices.
-
-### For Production
-These warnings do **not** indicate runtime issues:
-- CVPixelBuffer is thread-safe
-- All actor isolation is properly handled
-- Sendable conformance is correctly applied
-
-### Future Updates
-These warnings will automatically resolve when:
-- Apple updates Core Video to conform to Sendable
-- Expected in a future iOS SDK release
-
-## Technical Details
-
-### Why @preconcurrency?
-
-We use `@preconcurrency import CoreVideo` because:
-1. Core Video was written before Swift Concurrency
-2. Apple hasn't retroactively added Sendable conformance
-3. The types are inherently thread-safe but not marked as such
-4. This suppresses warnings while maintaining safety
-
-### Why @unchecked Sendable?
-
-`OvershootVisionService` is marked `@unchecked Sendable` because:
-1. It uses Core Video types that aren't marked Sendable
-2. We've manually verified thread safety
-3. All mutable state is properly synchronized
-4. URLSession and other types used are thread-safe
-
-## Verification
-
-To verify warnings yourself:
-
-```bash
-# Build the project
-xcodebuild -scheme SousChefAI build
-
-# Count warnings
-xcodebuild -scheme SousChefAI build 2>&1 | grep "warning:" | wc -l
-```
-
-Expected result: 4 warnings (all Core Video related)
-
----
-
-**Status**: ✅ Production Ready
-**Swift 6**: ✅ Fully Compatible
-**Concurrency**: ✅ Thread-Safe
-**Action Required**: None
-
-These warnings are framework limitations, not code issues. The app is safe to deploy.
diff --git a/SousChefAI.xcodeproj/project.pbxproj b/SousChefAI.xcodeproj/project.pbxproj
deleted file mode 100644
index 4271788..0000000
--- a/SousChefAI.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,624 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 77;
- objects = {
-
-/* Begin PBXBuildFile section */
- 865424082F3D142A00B4257E /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 865424072F3D142A00B4257E /* README.md */; };
- 8654240A2F3D151800B4257E /* SETUP_GUIDE.md in Resources */ = {isa = PBXBuildFile; fileRef = 865424092F3D151800B4257E /* SETUP_GUIDE.md */; };
- 8654240E2F3D17FE00B4257E /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 8654240D2F3D17FE00B4257E /* PrivacyInfo.xcprivacy */; };
- 865424102F3D181000B4257E /* PRIVACY_SETUP.md in Resources */ = {isa = PBXBuildFile; fileRef = 8654240F2F3D181000B4257E /* PRIVACY_SETUP.md */; };
- 865424122F3D185100B4257E /* QUICKSTART.md in Resources */ = {isa = PBXBuildFile; fileRef = 865424112F3D185100B4257E /* QUICKSTART.md */; };
- 865424142F3D188500B4257E /* PROJECT_SUMMARY.md in Resources */ = {isa = PBXBuildFile; fileRef = 865424132F3D188500B4257E /* PROJECT_SUMMARY.md */; };
- 865424162F3D1A7100B4257E /* SWIFT6_WARNINGS.md in Resources */ = {isa = PBXBuildFile; fileRef = 865424152F3D1A7100B4257E /* SWIFT6_WARNINGS.md */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXContainerItemProxy section */
- 86FE8EEB2F3CF75900A1BEA6 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 86FE8ED52F3CF75800A1BEA6 /* Project object */;
- proxyType = 1;
- remoteGlobalIDString = 86FE8EDC2F3CF75800A1BEA6;
- remoteInfo = SousChefAI;
- };
- 86FE8EF52F3CF75900A1BEA6 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 86FE8ED52F3CF75800A1BEA6 /* Project object */;
- proxyType = 1;
- remoteGlobalIDString = 86FE8EDC2F3CF75800A1BEA6;
- remoteInfo = SousChefAI;
- };
-/* End PBXContainerItemProxy section */
-
-/* Begin PBXFileReference section */
- 865424072F3D142A00B4257E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
- 865424092F3D151800B4257E /* SETUP_GUIDE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SETUP_GUIDE.md; sourceTree = ""; };
- 8654240D2F3D17FE00B4257E /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
- 8654240F2F3D181000B4257E /* PRIVACY_SETUP.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = PRIVACY_SETUP.md; sourceTree = ""; };
- 865424112F3D185100B4257E /* QUICKSTART.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = QUICKSTART.md; sourceTree = ""; };
- 865424132F3D188500B4257E /* PROJECT_SUMMARY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = PROJECT_SUMMARY.md; sourceTree = ""; };
- 865424152F3D1A7100B4257E /* SWIFT6_WARNINGS.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SWIFT6_WARNINGS.md; sourceTree = ""; };
- 86FE8EDD2F3CF75800A1BEA6 /* SousChefAI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SousChefAI.app; sourceTree = BUILT_PRODUCTS_DIR; };
- 86FE8EEA2F3CF75900A1BEA6 /* SousChefAITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SousChefAITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
- 86FE8EF42F3CF75900A1BEA6 /* SousChefAIUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SousChefAIUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
-/* End PBXFileReference section */
-
-/* Begin PBXFileSystemSynchronizedRootGroup section */
- 86FE8EDF2F3CF75800A1BEA6 /* SousChefAI */ = {
- isa = PBXFileSystemSynchronizedRootGroup;
- path = SousChefAI;
- sourceTree = "";
- };
- 86FE8EED2F3CF75900A1BEA6 /* SousChefAITests */ = {
- isa = PBXFileSystemSynchronizedRootGroup;
- path = SousChefAITests;
- sourceTree = "";
- };
- 86FE8EF72F3CF75900A1BEA6 /* SousChefAIUITests */ = {
- isa = PBXFileSystemSynchronizedRootGroup;
- path = SousChefAIUITests;
- sourceTree = "";
- };
-/* End PBXFileSystemSynchronizedRootGroup section */
-
-/* Begin PBXFrameworksBuildPhase section */
- 86FE8EDA2F3CF75800A1BEA6 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 86FE8EE72F3CF75900A1BEA6 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 86FE8EF12F3CF75900A1BEA6 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
- 86FE8ED42F3CF75800A1BEA6 = {
- isa = PBXGroup;
- children = (
- 86FE8EDF2F3CF75800A1BEA6 /* SousChefAI */,
- 86FE8EED2F3CF75900A1BEA6 /* SousChefAITests */,
- 86FE8EF72F3CF75900A1BEA6 /* SousChefAIUITests */,
- 86FE8EDE2F3CF75800A1BEA6 /* Products */,
- 865424072F3D142A00B4257E /* README.md */,
- 865424092F3D151800B4257E /* SETUP_GUIDE.md */,
- 8654240D2F3D17FE00B4257E /* PrivacyInfo.xcprivacy */,
- 8654240F2F3D181000B4257E /* PRIVACY_SETUP.md */,
- 865424112F3D185100B4257E /* QUICKSTART.md */,
- 865424132F3D188500B4257E /* PROJECT_SUMMARY.md */,
- 865424152F3D1A7100B4257E /* SWIFT6_WARNINGS.md */,
- );
- sourceTree = "";
- };
- 86FE8EDE2F3CF75800A1BEA6 /* Products */ = {
- isa = PBXGroup;
- children = (
- 86FE8EDD2F3CF75800A1BEA6 /* SousChefAI.app */,
- 86FE8EEA2F3CF75900A1BEA6 /* SousChefAITests.xctest */,
- 86FE8EF42F3CF75900A1BEA6 /* SousChefAIUITests.xctest */,
- );
- name = Products;
- sourceTree = "";
- };
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
- 86FE8EDC2F3CF75800A1BEA6 /* SousChefAI */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 86FE8EFE2F3CF75900A1BEA6 /* Build configuration list for PBXNativeTarget "SousChefAI" */;
- buildPhases = (
- 86FE8ED92F3CF75800A1BEA6 /* Sources */,
- 86FE8EDA2F3CF75800A1BEA6 /* Frameworks */,
- 86FE8EDB2F3CF75800A1BEA6 /* Resources */,
- );
- buildRules = (
- );
- dependencies = (
- );
- fileSystemSynchronizedGroups = (
- 86FE8EDF2F3CF75800A1BEA6 /* SousChefAI */,
- );
- name = SousChefAI;
- packageProductDependencies = (
- );
- productName = SousChefAI;
- productReference = 86FE8EDD2F3CF75800A1BEA6 /* SousChefAI.app */;
- productType = "com.apple.product-type.application";
- };
- 86FE8EE92F3CF75900A1BEA6 /* SousChefAITests */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 86FE8F012F3CF75900A1BEA6 /* Build configuration list for PBXNativeTarget "SousChefAITests" */;
- buildPhases = (
- 86FE8EE62F3CF75900A1BEA6 /* Sources */,
- 86FE8EE72F3CF75900A1BEA6 /* Frameworks */,
- 86FE8EE82F3CF75900A1BEA6 /* Resources */,
- );
- buildRules = (
- );
- dependencies = (
- 86FE8EEC2F3CF75900A1BEA6 /* PBXTargetDependency */,
- );
- fileSystemSynchronizedGroups = (
- 86FE8EED2F3CF75900A1BEA6 /* SousChefAITests */,
- );
- name = SousChefAITests;
- packageProductDependencies = (
- );
- productName = SousChefAITests;
- productReference = 86FE8EEA2F3CF75900A1BEA6 /* SousChefAITests.xctest */;
- productType = "com.apple.product-type.bundle.unit-test";
- };
- 86FE8EF32F3CF75900A1BEA6 /* SousChefAIUITests */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 86FE8F042F3CF75900A1BEA6 /* Build configuration list for PBXNativeTarget "SousChefAIUITests" */;
- buildPhases = (
- 86FE8EF02F3CF75900A1BEA6 /* Sources */,
- 86FE8EF12F3CF75900A1BEA6 /* Frameworks */,
- 86FE8EF22F3CF75900A1BEA6 /* Resources */,
- );
- buildRules = (
- );
- dependencies = (
- 86FE8EF62F3CF75900A1BEA6 /* PBXTargetDependency */,
- );
- fileSystemSynchronizedGroups = (
- 86FE8EF72F3CF75900A1BEA6 /* SousChefAIUITests */,
- );
- name = SousChefAIUITests;
- packageProductDependencies = (
- );
- productName = SousChefAIUITests;
- productReference = 86FE8EF42F3CF75900A1BEA6 /* SousChefAIUITests.xctest */;
- productType = "com.apple.product-type.bundle.ui-testing";
- };
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
- 86FE8ED52F3CF75800A1BEA6 /* Project object */ = {
- isa = PBXProject;
- attributes = {
- BuildIndependentTargetsInParallel = 1;
- LastSwiftUpdateCheck = 2620;
- LastUpgradeCheck = 2620;
- TargetAttributes = {
- 86FE8EDC2F3CF75800A1BEA6 = {
- CreatedOnToolsVersion = 26.2;
- };
- 86FE8EE92F3CF75900A1BEA6 = {
- CreatedOnToolsVersion = 26.2;
- TestTargetID = 86FE8EDC2F3CF75800A1BEA6;
- };
- 86FE8EF32F3CF75900A1BEA6 = {
- CreatedOnToolsVersion = 26.2;
- TestTargetID = 86FE8EDC2F3CF75800A1BEA6;
- };
- };
- };
- buildConfigurationList = 86FE8ED82F3CF75800A1BEA6 /* Build configuration list for PBXProject "SousChefAI" */;
- developmentRegion = en;
- hasScannedForEncodings = 0;
- knownRegions = (
- en,
- Base,
- );
- mainGroup = 86FE8ED42F3CF75800A1BEA6;
- minimizedProjectReferenceProxies = 1;
- preferredProjectObjectVersion = 77;
- productRefGroup = 86FE8EDE2F3CF75800A1BEA6 /* Products */;
- projectDirPath = "";
- projectRoot = "";
- targets = (
- 86FE8EDC2F3CF75800A1BEA6 /* SousChefAI */,
- 86FE8EE92F3CF75900A1BEA6 /* SousChefAITests */,
- 86FE8EF32F3CF75900A1BEA6 /* SousChefAIUITests */,
- );
- };
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
- 86FE8EDB2F3CF75800A1BEA6 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 865424142F3D188500B4257E /* PROJECT_SUMMARY.md in Resources */,
- 865424082F3D142A00B4257E /* README.md in Resources */,
- 865424122F3D185100B4257E /* QUICKSTART.md in Resources */,
- 8654240A2F3D151800B4257E /* SETUP_GUIDE.md in Resources */,
- 865424162F3D1A7100B4257E /* SWIFT6_WARNINGS.md in Resources */,
- 865424102F3D181000B4257E /* PRIVACY_SETUP.md in Resources */,
- 8654240E2F3D17FE00B4257E /* PrivacyInfo.xcprivacy in Resources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 86FE8EE82F3CF75900A1BEA6 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 86FE8EF22F3CF75900A1BEA6 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
- 86FE8ED92F3CF75800A1BEA6 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 86FE8EE62F3CF75900A1BEA6 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 86FE8EF02F3CF75900A1BEA6 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXSourcesBuildPhase section */
-
-/* Begin PBXTargetDependency section */
- 86FE8EEC2F3CF75900A1BEA6 /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- target = 86FE8EDC2F3CF75800A1BEA6 /* SousChefAI */;
- targetProxy = 86FE8EEB2F3CF75900A1BEA6 /* PBXContainerItemProxy */;
- };
- 86FE8EF62F3CF75900A1BEA6 /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- target = 86FE8EDC2F3CF75800A1BEA6 /* SousChefAI */;
- targetProxy = 86FE8EF52F3CF75900A1BEA6 /* PBXContainerItemProxy */;
- };
-/* End PBXTargetDependency section */
-
-/* Begin XCBuildConfiguration section */
- 86FE8EFC2F3CF75900A1BEA6 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_ENABLE_OBJC_WEAK = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = dwarf;
- DEVELOPMENT_TEAM = YK2DB9NT3S;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_TESTABILITY = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = YES;
- GCC_C_LANGUAGE_STANDARD = gnu17;
- GCC_DYNAMIC_NO_PIC = NO;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_OPTIMIZATION_LEVEL = 0;
- GCC_PREPROCESSOR_DEFINITIONS = (
- "DEBUG=1",
- "$(inherited)",
- );
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 26.2;
- LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
- MTL_FAST_MATH = YES;
- ONLY_ACTIVE_ARCH = YES;
- SDKROOT = iphoneos;
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- SWIFT_VERSION = 6.0;
- };
- name = Debug;
- };
- 86FE8EFD2F3CF75900A1BEA6 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_ENABLE_OBJC_WEAK = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- DEVELOPMENT_TEAM = YK2DB9NT3S;
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = YES;
- GCC_C_LANGUAGE_STANDARD = gnu17;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 26.2;
- LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MTL_ENABLE_DEBUG_INFO = NO;
- MTL_FAST_MATH = YES;
- SDKROOT = iphoneos;
- SWIFT_COMPILATION_MODE = wholemodule;
- SWIFT_VERSION = 6.0;
- VALIDATE_PRODUCT = YES;
- };
- name = Release;
- };
- 86FE8EFF2F3CF75900A1BEA6 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = YK2DB9NT3S;
- ENABLE_PREVIEWS = YES;
- GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_FILE = "";
- INFOPLIST_KEY_LSApplicationCategoryType = "";
- INFOPLIST_KEY_NSCameraUsageDescription = "SousChefAI needs camera access to scan your fridge for ingredients and monitor your cooking progress in real-time.";
- INFOPLIST_KEY_NSMicrophoneUsageDescription = "SousChefAI uses the microphone to provide voice-guided cooking instructions.";
- INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
- INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
- INFOPLIST_KEY_UILaunchScreen_Generation = YES;
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.adipu.SousChefAI;
- PRODUCT_NAME = "$(TARGET_NAME)";
- STRING_CATALOG_GENERATE_SYMBOLS = YES;
- SWIFT_APPROACHABLE_CONCURRENCY = YES;
- SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
- SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Debug;
- };
- 86FE8F002F3CF75900A1BEA6 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = YK2DB9NT3S;
- ENABLE_PREVIEWS = YES;
- GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_FILE = "";
- INFOPLIST_KEY_LSApplicationCategoryType = "";
- INFOPLIST_KEY_NSCameraUsageDescription = "SousChefAI needs camera access to scan your fridge for ingredients and monitor your cooking progress in real-time.";
- INFOPLIST_KEY_NSMicrophoneUsageDescription = "SousChefAI uses the microphone to provide voice-guided cooking instructions.";
- INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
- INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
- INFOPLIST_KEY_UILaunchScreen_Generation = YES;
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.adipu.SousChefAI;
- PRODUCT_NAME = "$(TARGET_NAME)";
- STRING_CATALOG_GENERATE_SYMBOLS = YES;
- SWIFT_APPROACHABLE_CONCURRENCY = YES;
- SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
- SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Release;
- };
- 86FE8F022F3CF75900A1BEA6 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- BUNDLE_LOADER = "$(TEST_HOST)";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = YK2DB9NT3S;
- GENERATE_INFOPLIST_FILE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 26.2;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.adipu.SousChefAITests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- STRING_CATALOG_GENERATE_SYMBOLS = NO;
- SWIFT_APPROACHABLE_CONCURRENCY = YES;
- SWIFT_EMIT_LOC_STRINGS = NO;
- SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SousChefAI.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SousChefAI";
- };
- name = Debug;
- };
- 86FE8F032F3CF75900A1BEA6 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- BUNDLE_LOADER = "$(TEST_HOST)";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = YK2DB9NT3S;
- GENERATE_INFOPLIST_FILE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 26.2;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.adipu.SousChefAITests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- STRING_CATALOG_GENERATE_SYMBOLS = NO;
- SWIFT_APPROACHABLE_CONCURRENCY = YES;
- SWIFT_EMIT_LOC_STRINGS = NO;
- SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SousChefAI.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SousChefAI";
- };
- name = Release;
- };
- 86FE8F052F3CF75900A1BEA6 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = YK2DB9NT3S;
- GENERATE_INFOPLIST_FILE = YES;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.adipu.SousChefAIUITests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- STRING_CATALOG_GENERATE_SYMBOLS = NO;
- SWIFT_APPROACHABLE_CONCURRENCY = YES;
- SWIFT_EMIT_LOC_STRINGS = NO;
- SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- TEST_TARGET_NAME = SousChefAI;
- };
- name = Debug;
- };
- 86FE8F062F3CF75900A1BEA6 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = YK2DB9NT3S;
- GENERATE_INFOPLIST_FILE = YES;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.adipu.SousChefAIUITests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- STRING_CATALOG_GENERATE_SYMBOLS = NO;
- SWIFT_APPROACHABLE_CONCURRENCY = YES;
- SWIFT_EMIT_LOC_STRINGS = NO;
- SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- TEST_TARGET_NAME = SousChefAI;
- };
- name = Release;
- };
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
- 86FE8ED82F3CF75800A1BEA6 /* Build configuration list for PBXProject "SousChefAI" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 86FE8EFC2F3CF75900A1BEA6 /* Debug */,
- 86FE8EFD2F3CF75900A1BEA6 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 86FE8EFE2F3CF75900A1BEA6 /* Build configuration list for PBXNativeTarget "SousChefAI" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 86FE8EFF2F3CF75900A1BEA6 /* Debug */,
- 86FE8F002F3CF75900A1BEA6 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 86FE8F012F3CF75900A1BEA6 /* Build configuration list for PBXNativeTarget "SousChefAITests" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 86FE8F022F3CF75900A1BEA6 /* Debug */,
- 86FE8F032F3CF75900A1BEA6 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 86FE8F042F3CF75900A1BEA6 /* Build configuration list for PBXNativeTarget "SousChefAIUITests" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 86FE8F052F3CF75900A1BEA6 /* Debug */,
- 86FE8F062F3CF75900A1BEA6 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
-/* End XCConfigurationList section */
- };
- rootObject = 86FE8ED52F3CF75800A1BEA6 /* Project object */;
-}
diff --git a/SousChefAI.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SousChefAI.xcodeproj/project.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 919434a..0000000
--- a/SousChefAI.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/SousChefAI.xcodeproj/xcuserdata/adipu.xcuserdatad/xcschemes/xcschememanagement.plist b/SousChefAI.xcodeproj/xcuserdata/adipu.xcuserdatad/xcschemes/xcschememanagement.plist
deleted file mode 100644
index d80415a..0000000
--- a/SousChefAI.xcodeproj/xcuserdata/adipu.xcuserdatad/xcschemes/xcschememanagement.plist
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- SchemeUserState
-
- SousChefAI.xcscheme_^#shared#^_
-
- orderHint
- 0
-
-
-
-
diff --git a/SousChefAI/Assets.xcassets/AccentColor.colorset/Contents.json b/SousChefAI/Assets.xcassets/AccentColor.colorset/Contents.json
deleted file mode 100644
index eb87897..0000000
--- a/SousChefAI/Assets.xcassets/AccentColor.colorset/Contents.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "colors" : [
- {
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/SousChefAI/Assets.xcassets/AppIcon.appiconset/Contents.json b/SousChefAI/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index 2305880..0000000
--- a/SousChefAI/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "platform" : "ios",
- "size" : "1024x1024"
- },
- {
- "appearances" : [
- {
- "appearance" : "luminosity",
- "value" : "dark"
- }
- ],
- "idiom" : "universal",
- "platform" : "ios",
- "size" : "1024x1024"
- },
- {
- "appearances" : [
- {
- "appearance" : "luminosity",
- "value" : "tinted"
- }
- ],
- "idiom" : "universal",
- "platform" : "ios",
- "size" : "1024x1024"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/SousChefAI/Assets.xcassets/Contents.json b/SousChefAI/Assets.xcassets/Contents.json
deleted file mode 100644
index 73c0059..0000000
--- a/SousChefAI/Assets.xcassets/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/SousChefAI/Config/AppConfig.swift b/SousChefAI/Config/AppConfig.swift
deleted file mode 100644
index 87bd1c9..0000000
--- a/SousChefAI/Config/AppConfig.swift
+++ /dev/null
@@ -1,50 +0,0 @@
-//
-// AppConfig.swift
-// SousChefAI
-//
-// Centralized configuration for API keys and service endpoints
-//
-
-import Foundation
-
-/// Scanning mode for ingredient detection
-enum ScanningMode: String, CaseIterable {
- case geminiVision // Uses Gemini API for image analysis (recommended)
- case arKit // Uses ARKit for spatial scanning (future implementation)
-}
-
-enum AppConfig: Sendable {
- // MARK: - Google Gemini API
- /// Google Gemini API key for recipe generation and reasoning
- /// [INSERT_GEMINI_API_KEY_HERE]
- static let geminiAPIKey = "INSERT_KEY_HERE"
-
- // MARK: - Firebase Configuration
- /// Firebase configuration will be loaded from GoogleService-Info.plist
- /// [INSERT_FIREBASE_GOOGLESERVICE-INFO.PLIST_SETUP_HERE]
- /// Instructions:
- /// 1. Download GoogleService-Info.plist from Firebase Console
- /// 2. Add it to the Xcode project root
- /// 3. Ensure it's added to the target
-
- // MARK: - Scanning Configuration
-
- /// Current scanning mode - change this to switch between vision implementations
- /// Options: .geminiVision (uses Gemini API), .arKit (uses ARKit - future)
- static let scanningMode: ScanningMode = .geminiVision
-
- /// Enable AR-based scanning features (legacy flag, use scanningMode instead)
- static let enableARScanning = false
-
- // MARK: - Feature Flags
- static let enableRealTimeDetection = true
- static let enableCookingMode = true
- static let maxIngredientsPerScan = 50
- static let minConfidenceThreshold = 0.5
-
- // MARK: - Scanning Settings
- /// How often to send frames to Gemini (in seconds)
- static let geminiFrameInterval: Double = 1.0
- /// Maximum scan duration before auto-stop (in seconds)
- static let maxScanDuration: Double = 60.0
-}
diff --git a/SousChefAI/ContentView.swift b/SousChefAI/ContentView.swift
deleted file mode 100644
index 65c9aca..0000000
--- a/SousChefAI/ContentView.swift
+++ /dev/null
@@ -1,242 +0,0 @@
-//
-// ContentView.swift
-// SousChefAI
-//
-// Created by Aditya Pulipaka on 2/11/26.
-//
-
-import SwiftUI
-
-struct ContentView: View {
- @EnvironmentObject var repository: FirestoreRepository
- @State private var selectedTab = 0
-
- var body: some View {
- TabView(selection: $selectedTab) {
- // Scanner Tab
- ScannerView()
- .tabItem {
- Label("Scan", systemImage: "camera.fill")
- }
- .tag(0)
-
- // Inventory Tab
- NavigationStack {
- inventoryPlaceholder
- }
- .tabItem {
- Label("Inventory", systemImage: "square.grid.2x2")
- }
- .tag(1)
-
- // Saved Recipes Tab
- NavigationStack {
- savedRecipesPlaceholder
- }
- .tabItem {
- Label("Recipes", systemImage: "book.fill")
- }
- .tag(2)
-
- // Profile Tab
- NavigationStack {
- profileView
- }
- .tabItem {
- Label("Profile", systemImage: "person.fill")
- }
- .tag(3)
- }
- }
-
- // MARK: - Placeholder Views
-
- private var inventoryPlaceholder: some View {
- List {
- if repository.currentInventory.isEmpty {
- ContentUnavailableView(
- "No Ingredients",
- systemImage: "refrigerator",
- description: Text("Scan your fridge to get started")
- )
- } else {
- ForEach(repository.currentInventory) { ingredient in
- HStack {
- VStack(alignment: .leading) {
- Text(ingredient.name)
- .font(.headline)
- Text(ingredient.estimatedQuantity)
- .font(.caption)
- .foregroundStyle(.secondary)
- }
- Spacer()
- Text("\(Int(ingredient.confidence * 100))%")
- .font(.caption)
- .foregroundStyle(.secondary)
- }
- }
- }
- }
- .navigationTitle("My Inventory")
- .toolbar {
- ToolbarItem(placement: .primaryAction) {
- Button {
- selectedTab = 0
- } label: {
- Label("Scan", systemImage: "camera")
- }
- }
- }
- }
-
- private var savedRecipesPlaceholder: some View {
- List {
- if repository.savedRecipes.isEmpty {
- ContentUnavailableView(
- "No Saved Recipes",
- systemImage: "book",
- description: Text("Save recipes from the recipe generator")
- )
- } else {
- ForEach(repository.savedRecipes) { recipe in
- VStack(alignment: .leading, spacing: 8) {
- Text(recipe.title)
- .font(.headline)
- Text(recipe.description)
- .font(.caption)
- .foregroundStyle(.secondary)
- .lineLimit(2)
- }
- .padding(.vertical, 4)
- }
- }
- }
- .navigationTitle("Saved Recipes")
- }
-
- private var profileView: some View {
- Form {
- Section("About") {
- HStack {
- Text("Version")
- Spacer()
- Text("1.0.0")
- .foregroundStyle(.secondary)
- }
- }
-
- Section("Preferences") {
- NavigationLink {
- dietaryPreferencesView
- } label: {
- HStack {
- Label("Dietary Restrictions", systemImage: "leaf")
- Spacer()
- if let profile = repository.currentUser,
- !profile.dietaryRestrictions.isEmpty {
- Text("\(profile.dietaryRestrictions.count)")
- .foregroundStyle(.secondary)
- }
- }
- }
-
- NavigationLink {
- nutritionGoalsView
- } label: {
- Label("Nutrition Goals", systemImage: "heart")
- }
- }
-
- Section("API Configuration") {
- VStack(alignment: .leading, spacing: 8) {
- Text("AR Scanning")
- .font(.headline)
- Text(AppConfig.enableARScanning ? "Enabled" : "Disabled")
- .font(.caption)
- .foregroundStyle(AppConfig.enableARScanning ? .green : .red)
- }
-
- VStack(alignment: .leading, spacing: 8) {
- Text("Gemini API")
- .font(.headline)
- Text(AppConfig.geminiAPIKey == "INSERT_KEY_HERE" ? "Not configured" : "Configured")
- .font(.caption)
- .foregroundStyle(AppConfig.geminiAPIKey == "INSERT_KEY_HERE" ? .red : .green)
- }
- }
-
- Section {
- Link(destination: URL(string: "https://github.com/yourusername/souschef")!) {
- Label("View on GitHub", systemImage: "link")
- }
- }
- }
- .navigationTitle("Profile")
- }
-
- private var dietaryPreferencesView: some View {
- Form {
- Section {
- ForEach(UserProfile.commonRestrictions, id: \.self) { restriction in
- HStack {
- Text(restriction)
- Spacer()
- if repository.currentUser?.dietaryRestrictions.contains(restriction) ?? false {
- Image(systemName: "checkmark")
- .foregroundStyle(.blue)
- }
- }
- .contentShape(Rectangle())
- .onTapGesture {
- toggleRestriction(restriction)
- }
- }
- }
- }
- .navigationTitle("Dietary Restrictions")
- }
-
- private var nutritionGoalsView: some View {
- Form {
- Section {
- TextField("Enter your nutrition goals",
- text: Binding(
- get: { repository.currentUser?.nutritionGoals ?? "" },
- set: { newValue in
- Task {
- try? await repository.updateNutritionGoals(newValue)
- }
- }
- ),
- axis: .vertical)
- .lineLimit(5...10)
- } header: {
- Text("Goals")
- } footer: {
- Text("e.g., High protein, Low carb, Balanced diet")
- }
- }
- .navigationTitle("Nutrition Goals")
- }
-
- // MARK: - Actions
-
- private func toggleRestriction(_ restriction: String) {
- Task {
- guard var profile = repository.currentUser else { return }
-
- if profile.dietaryRestrictions.contains(restriction) {
- profile.dietaryRestrictions.removeAll { $0 == restriction }
- } else {
- profile.dietaryRestrictions.append(restriction)
- }
-
- try? await repository.updateDietaryRestrictions(profile.dietaryRestrictions)
- }
- }
-}
-
-#Preview {
- ContentView()
- .environmentObject(FirestoreRepository())
-}
diff --git a/SousChefAI/Models/Ingredient.swift b/SousChefAI/Models/Ingredient.swift
deleted file mode 100644
index 33ca98b..0000000
--- a/SousChefAI/Models/Ingredient.swift
+++ /dev/null
@@ -1,47 +0,0 @@
-//
-// Ingredient.swift
-// SousChefAI
-//
-// Core data model for ingredients detected or managed by the user
-//
-
-import Foundation
-
-/// Represents an alternative guess for what an ingredient might be
-struct IngredientGuess: Identifiable, Codable, Equatable {
- var id: String { name }
- let name: String
- let confidence: Double
-}
-
-struct Ingredient: Identifiable, Codable, Equatable {
- let id: String
- var name: String
- var estimatedQuantity: String
- var confidence: Double
-
- /// Top 3 guesses for what this ingredient might be (from AI detection)
- var guesses: [IngredientGuess]
-
- init(id: String = UUID().uuidString,
- name: String,
- estimatedQuantity: String,
- confidence: Double = 1.0,
- guesses: [IngredientGuess] = []) {
- self.id = id
- self.name = name
- self.estimatedQuantity = estimatedQuantity
- self.confidence = confidence
- self.guesses = guesses
- }
-
- /// Indicates if the detection confidence is low and requires user verification
- var needsVerification: Bool {
- confidence < 0.7
- }
-
- /// Returns the best guess name, or the current name if no guesses available
- var bestGuessName: String {
- guesses.first?.name ?? name
- }
-}
diff --git a/SousChefAI/Models/Recipe.swift b/SousChefAI/Models/Recipe.swift
deleted file mode 100644
index d2034bf..0000000
--- a/SousChefAI/Models/Recipe.swift
+++ /dev/null
@@ -1,70 +0,0 @@
-//
-// Recipe.swift
-// SousChefAI
-//
-// Recipe model generated by AI based on available ingredients
-//
-
-import Foundation
-
-struct Recipe: Identifiable, Codable {
- let id: String
- var title: String
- var description: String
- var missingIngredients: [Ingredient]
- var steps: [String]
- var matchScore: Double
- var estimatedTime: String?
- var servings: Int?
-
- init(id: String = UUID().uuidString,
- title: String,
- description: String,
- missingIngredients: [Ingredient] = [],
- steps: [String],
- matchScore: Double,
- estimatedTime: String? = nil,
- servings: Int? = nil) {
- self.id = id
- self.title = title
- self.description = description
- self.missingIngredients = missingIngredients
- self.steps = steps
- self.matchScore = matchScore
- self.estimatedTime = estimatedTime
- self.servings = servings
- }
-
- /// Indicates if recipe can be made with only available ingredients
- var canMakeNow: Bool {
- missingIngredients.isEmpty
- }
-
- /// Category based on missing ingredients
- var category: RecipeCategory {
- if canMakeNow {
- return .scavenger
- } else if missingIngredients.count <= 2 {
- return .upgrader
- } else {
- return .shopping
- }
- }
-}
-
-enum RecipeCategory: String, CaseIterable {
- case scavenger = "The Scavenger"
- case upgrader = "The Upgrader"
- case shopping = "Shopping Required"
-
- var description: String {
- switch self {
- case .scavenger:
- return "Uses only your current ingredients"
- case .upgrader:
- return "Needs 1-2 additional items"
- case .shopping:
- return "Requires shopping trip"
- }
- }
-}
diff --git a/SousChefAI/Models/UserProfile.swift b/SousChefAI/Models/UserProfile.swift
deleted file mode 100644
index af0f3c8..0000000
--- a/SousChefAI/Models/UserProfile.swift
+++ /dev/null
@@ -1,37 +0,0 @@
-//
-// UserProfile.swift
-// SousChefAI
-//
-// User profile model for dietary preferences and pantry staples
-//
-
-import Foundation
-
-struct UserProfile: Identifiable, Codable {
- let id: String
- var dietaryRestrictions: [String]
- var nutritionGoals: String
- var pantryStaples: [Ingredient]
-
- init(id: String = UUID().uuidString,
- dietaryRestrictions: [String] = [],
- nutritionGoals: String = "",
- pantryStaples: [Ingredient] = []) {
- self.id = id
- self.dietaryRestrictions = dietaryRestrictions
- self.nutritionGoals = nutritionGoals
- self.pantryStaples = pantryStaples
- }
-
- /// Common dietary restrictions for quick selection
- static let commonRestrictions = [
- "Vegan",
- "Vegetarian",
- "Gluten-Free",
- "Dairy-Free",
- "Keto",
- "Paleo",
- "Nut Allergy",
- "Shellfish Allergy"
- ]
-}
diff --git a/SousChefAI/Services/ARVisionService.swift b/SousChefAI/Services/ARVisionService.swift
deleted file mode 100644
index d57265d..0000000
--- a/SousChefAI/Services/ARVisionService.swift
+++ /dev/null
@@ -1,222 +0,0 @@
-//
-// ARVisionService.swift
-// SousChefAI
-//
-// AR-based vision service using RealityKit and ARKit
-// Provides real-time plane detection and raycasting capabilities
-//
-
-import Foundation
-import SwiftUI
-import RealityKit
-import ARKit
-@preconcurrency import CoreVideo
-
-/// AR-based implementation for vision and spatial scanning
-final class ARVisionService: VisionService, @unchecked Sendable {
-
- nonisolated init() {}
-
- // MARK: - VisionService Protocol Implementation
-
- nonisolated func detectIngredients(from stream: AsyncStream) async throws -> [Ingredient] {
- // Mock implementation - in a real app, this would use ML models
- // to detect ingredients from AR camera frames
- var detectedIngredients: [Ingredient] = []
- var frameCount = 0
-
- for await pixelBuffer in stream {
- frameCount += 1
-
- // Process every 30th frame to reduce processing load
- if frameCount % 30 == 0 {
- let ingredients = try await processARFrame(pixelBuffer)
-
- // Merge results
- for ingredient in ingredients {
- if !detectedIngredients.contains(where: { $0.name == ingredient.name }) {
- detectedIngredients.append(ingredient)
- }
- }
-
- // Stop after collecting enough ingredients
- if detectedIngredients.count >= AppConfig.maxIngredientsPerScan {
- break
- }
- }
- }
-
- return detectedIngredients
- .filter { $0.confidence >= AppConfig.minConfidenceThreshold }
- .sorted { $0.confidence > $1.confidence }
- }
-
- nonisolated func detectIngredients(from pixelBuffer: CVPixelBuffer) async throws -> [Ingredient] {
- return try await processARFrame(pixelBuffer)
- }
-
- nonisolated func analyzeCookingProgress(from stream: AsyncStream, for step: String) async throws -> CookingProgress {
- // Mock implementation for cooking progress monitoring
- return CookingProgress(
- isComplete: false,
- confidence: 0.5,
- feedback: "Monitoring cooking progress..."
- )
- }
-
- // MARK: - Private Helper Methods
-
- nonisolated private func processARFrame(_ pixelBuffer: CVPixelBuffer) async throws -> [Ingredient] {
- // Mock ingredient detection
- // In a real implementation, this would use Vision framework or ML models
- // to detect objects in the AR camera feed
-
- // For now, return empty array - actual detection would happen here
- return []
- }
-}
-
-/// SwiftUI wrapper for ARView with plane detection and raycasting
-struct ARViewContainer: UIViewRepresentable {
- @Binding var detectedPlanes: Int
- @Binding var lastRaycastResult: String
-
- func makeUIView(context: Context) -> ARView {
- let arView = ARView(frame: .zero)
-
- // Configure AR session
- let configuration = ARWorldTrackingConfiguration()
-
- // Enable plane detection for horizontal and vertical surfaces
- configuration.planeDetection = [.horizontal, .vertical]
-
- // Enable scene reconstruction for better spatial understanding
- if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) {
- configuration.sceneReconstruction = .mesh
- }
-
- // Enable debug options to visualize detected planes
- arView.debugOptions = [.showSceneUnderstanding, .showWorldOrigin]
-
- // Set the coordinator as the session delegate
- arView.session.delegate = context.coordinator
-
- // Run the AR session
- arView.session.run(configuration)
-
- // Add tap gesture for raycasting
- let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(_:)))
- arView.addGestureRecognizer(tapGesture)
-
- context.coordinator.arView = arView
-
- return arView
- }
-
- func updateUIView(_ uiView: ARView, context: Context) {
- // Update UI if needed
- }
-
- func makeCoordinator() -> Coordinator {
- Coordinator(detectedPlanes: $detectedPlanes, lastRaycastResult: $lastRaycastResult)
- }
-
- // MARK: - Coordinator
-
- class Coordinator: NSObject, ARSessionDelegate {
- @Binding var detectedPlanes: Int
- @Binding var lastRaycastResult: String
- weak var arView: ARView?
- private var detectedPlaneAnchors: Set = []
-
- init(detectedPlanes: Binding, lastRaycastResult: Binding) {
- _detectedPlanes = detectedPlanes
- _lastRaycastResult = lastRaycastResult
- }
-
- // MARK: - ARSessionDelegate Methods
-
- func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
- for anchor in anchors {
- if let planeAnchor = anchor as? ARPlaneAnchor {
- detectedPlaneAnchors.insert(planeAnchor.identifier)
- DispatchQueue.main.async {
- self.detectedPlanes = self.detectedPlaneAnchors.count
- }
- }
- }
- }
-
- func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
- // Planes are being updated as AR refines understanding
- }
-
- func session(_ session: ARSession, didRemove anchors: [ARAnchor]) {
- for anchor in anchors {
- if let planeAnchor = anchor as? ARPlaneAnchor {
- detectedPlaneAnchors.remove(planeAnchor.identifier)
- DispatchQueue.main.async {
- self.detectedPlanes = self.detectedPlaneAnchors.count
- }
- }
- }
- }
-
- func session(_ session: ARSession, didFailWithError error: Error) {
- print("AR Session failed: \(error.localizedDescription)")
- }
-
- // MARK: - Raycasting
-
- /// Performs a raycast from screen center to detect planes
- func performRaycast(from point: CGPoint, in view: ARView) -> ARRaycastResult? {
- // Create raycast query targeting estimated planes
- guard let query = view.makeRaycastQuery(
- from: point,
- allowing: .estimatedPlane,
- alignment: .any
- ) else {
- return nil
- }
-
- // Perform the raycast
- let results = view.session.raycast(query)
- return results.first
- }
-
- @objc func handleTap(_ gesture: UITapGestureRecognizer) {
- guard let arView = arView else { return }
-
- let location = gesture.location(in: arView)
-
- if let result = performRaycast(from: location, in: arView) {
- let position = result.worldTransform.columns.3
- let resultString = String(format: "Hit at: (%.2f, %.2f, %.2f)", position.x, position.y, position.z)
-
- DispatchQueue.main.async {
- self.lastRaycastResult = resultString
- }
-
- // Place a visual marker at the hit location
- placeMarker(at: result.worldTransform, in: arView)
- } else {
- DispatchQueue.main.async {
- self.lastRaycastResult = "No surface detected"
- }
- }
- }
-
- private func placeMarker(at transform: simd_float4x4, in arView: ARView) {
- // Create a small sphere to visualize the raycast hit
- let sphere = MeshResource.generateSphere(radius: 0.02)
- let material = SimpleMaterial(color: .green, isMetallic: false)
- let modelEntity = ModelEntity(mesh: sphere, materials: [material])
-
- // Create an anchor at the hit position
- let anchorEntity = AnchorEntity(world: transform)
- anchorEntity.addChild(modelEntity)
-
- arView.scene.addAnchor(anchorEntity)
- }
- }
-}
diff --git a/SousChefAI/Services/CameraManager.swift b/SousChefAI/Services/CameraManager.swift
deleted file mode 100644
index ed2ad77..0000000
--- a/SousChefAI/Services/CameraManager.swift
+++ /dev/null
@@ -1,290 +0,0 @@
-//
-// CameraManager.swift
-// SousChefAI
-//
-// Camera management using AVFoundation for real-time video streaming
-//
-
-@preconcurrency import AVFoundation
-@preconcurrency import CoreVideo
-import UIKit
-import Combine
-
-/// Manages camera capture and provides async stream of video frames
-@MainActor
-final class CameraManager: NSObject, ObservableObject {
-
- @Published var isAuthorized = false
- @Published var error: CameraError?
- @Published var isRunning = false
-
- enum SessionState {
- case idle
- case configuring
- case configured
- case starting
- case running
- case stopping
- }
- @Published private(set) var sessionState: SessionState = .idle
-
- nonisolated(unsafe) private let captureSession = AVCaptureSession()
- nonisolated(unsafe) private var videoOutput: AVCaptureVideoDataOutput?
- private let videoQueue = DispatchQueue(label: "com.souschef.video", qos: .userInitiated)
-
- nonisolated(unsafe) private var frameContinuation: AsyncStream.Continuation?
- private let continuationQueue = DispatchQueue(label: "com.souschef.continuation")
-
- private var isConfigured = false
- private var cachedPreviewLayer: AVCaptureVideoPreviewLayer?
-
- nonisolated override init() {
- super.init()
- print("🎥 CameraManager.init() - Instance created at \(Date())")
- }
-
- // MARK: - Authorization
-
- func checkAuthorization() async {
- switch AVCaptureDevice.authorizationStatus(for: .video) {
- case .authorized:
- isAuthorized = true
-
- case .notDetermined:
- isAuthorized = await AVCaptureDevice.requestAccess(for: .video)
-
- case .denied, .restricted:
- isAuthorized = false
- error = .notAuthorized
-
- @unknown default:
- isAuthorized = false
- }
- }
-
- // MARK: - Session Setup
-
- func setupSession() async throws {
- print("🎥 CameraManager.setupSession() - STARTED at \(Date()), current state: \(sessionState)")
-
- // Wait if session is stopping
- if sessionState == .stopping {
- print("🎥 CameraManager.setupSession() - ⏳ Waiting for session to finish stopping...")
- await waitForSessionToStop()
- }
-
- // Only configure once
- guard !isConfigured else {
- print("🎥 CameraManager.setupSession() - Already configured, returning")
- return
- }
-
- sessionState = .configuring
-
- // Ensure authorization is checked first
- await checkAuthorization()
-
- guard isAuthorized else {
- throw CameraError.notAuthorized
- }
-
- print("🎥 CameraManager.setupSession() - Calling beginConfiguration()")
- captureSession.beginConfiguration()
-
- // Set session preset
- captureSession.sessionPreset = .high
- print("🎥 CameraManager.setupSession() - Set preset to .high")
-
- // Add video input
- print("🎥 CameraManager.setupSession() - About to get video device (LINE 72)")
-
- #if targetEnvironment(simulator)
- print("🎥 CameraManager.setupSession() - ⚠️ RUNNING ON SIMULATOR - Camera may not work properly")
- #endif
-
- guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
- let videoInput = try? AVCaptureDeviceInput(device: videoDevice),
- captureSession.canAddInput(videoInput) else {
- print("🎥 CameraManager.setupSession() - ❌ FAILED to get video device or add input")
- captureSession.commitConfiguration()
- throw CameraError.setupFailed
- }
-
- print("🎥 CameraManager.setupSession() - ✅ Successfully got video device and input")
- captureSession.addInput(videoInput)
-
- // Add video output
- let output = AVCaptureVideoDataOutput()
- output.setSampleBufferDelegate(self, queue: videoQueue)
- output.alwaysDiscardsLateVideoFrames = true
- output.videoSettings = [
- kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA
- ]
-
- guard captureSession.canAddOutput(output) else {
- captureSession.commitConfiguration()
- throw CameraError.setupFailed
- }
-
- captureSession.addOutput(output)
- self.videoOutput = output
-
- captureSession.commitConfiguration()
- isConfigured = true
- sessionState = .configured
- print("🎥 CameraManager.setupSession() - ✅ COMPLETED successfully at \(Date())")
- }
-
- // MARK: - Session Control
-
- func startSession() {
- guard !captureSession.isRunning else {
- print("🎥 CameraManager.startSession() - Session already running")
- return
- }
-
- print("🎥 CameraManager.startSession() - Starting session")
- sessionState = .starting
-
- let session = captureSession
- Task.detached { [weak self] in
- session.startRunning()
-
- await MainActor.run { [weak self] in
- self?.isRunning = true
- self?.sessionState = .running
- print("🎥 CameraManager.startSession() - ✅ Session running")
- }
- }
- }
-
- func stopSession() {
- guard captureSession.isRunning else {
- print("🎥 CameraManager.stopSession() - Session not running")
- return
- }
-
- print("🎥 CameraManager.stopSession() - Stopping session")
- sessionState = .stopping
-
- let session = captureSession
- Task.detached { [weak self] in
- session.stopRunning()
-
- await MainActor.run { [weak self] in
- self?.isRunning = false
- self?.sessionState = .idle
- print("🎥 CameraManager.stopSession() - ✅ Session stopped")
- }
- }
- }
-
- // MARK: - Helper Methods
-
- private func waitForSessionToStop() async {
- // Wait for session to reach idle state
- while sessionState == .stopping {
- try? await Task.sleep(for: .milliseconds(100))
- }
- print("🎥 CameraManager.waitForSessionToStop() - ✅ Session is now idle")
- }
-
- /// Cleanup method that ensures session is fully stopped before returning
- func cleanup() async {
- print("🎥 CameraManager.cleanup() - Starting cleanup")
-
- if captureSession.isRunning {
- stopSession()
- }
-
- // Wait for session to fully stop
- await waitForSessionToStop()
-
- print("🎥 CameraManager.cleanup() - ✅ Cleanup complete")
- }
-
- // MARK: - Frame Stream
-
- func frameStream() -> AsyncStream {
- AsyncStream { [weak self] continuation in
- guard let self = self else { return }
-
- self.continuationQueue.async {
- Task { @MainActor in
- self.frameContinuation = continuation
- }
- }
-
- continuation.onTermination = { [weak self] _ in
- guard let self = self else { return }
- self.continuationQueue.async {
- Task { @MainActor in
- self.frameContinuation = nil
- }
- }
- }
- }
- }
-
- // MARK: - Preview Layer
-
- func previewLayer() -> AVCaptureVideoPreviewLayer? {
- print("🎥 CameraManager.previewLayer() - ⚠️ CALLED at \(Date()) - isConfigured: \(isConfigured)")
-
- // Only create preview layer after session is configured
- if !isConfigured {
- print("🎥 CameraManager.previewLayer() - ❌ Session not configured yet, returning nil")
- return nil
- }
-
- // Return cached layer if available
- if let cached = cachedPreviewLayer {
- print("🎥 CameraManager.previewLayer() - ✅ Returning cached preview layer")
- return cached
- }
-
- // Create and cache new preview layer
- print("🎥 CameraManager.previewLayer() - ✅ Creating new preview layer")
- let layer = AVCaptureVideoPreviewLayer(session: captureSession)
- layer.videoGravity = .resizeAspectFill
- cachedPreviewLayer = layer
- return layer
- }
-}
-
-// MARK: - AVCaptureVideoDataOutputSampleBufferDelegate
-
-extension CameraManager: AVCaptureVideoDataOutputSampleBufferDelegate {
- nonisolated func captureOutput(
- _ output: AVCaptureOutput,
- didOutput sampleBuffer: CMSampleBuffer,
- from connection: AVCaptureConnection
- ) {
- guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
- return
- }
-
- // CVPixelBuffer is thread-safe and immutable, safe to pass across isolation boundaries
- // Using nonisolated(unsafe) for continuation since we manage synchronization manually
- frameContinuation?.yield(pixelBuffer)
- }
-}
-
-// MARK: - Error Handling
-
-enum CameraError: Error, LocalizedError {
- case notAuthorized
- case setupFailed
- case captureSessionFailed
-
- var errorDescription: String? {
- switch self {
- case .notAuthorized:
- return "Camera access not authorized. Please enable camera access in Settings."
- case .setupFailed:
- return "Failed to setup camera session"
- case .captureSessionFailed:
- return "Camera capture session failed"
- }
- }
-}
diff --git a/SousChefAI/Services/FirestoreRepository.swift b/SousChefAI/Services/FirestoreRepository.swift
deleted file mode 100644
index 6195252..0000000
--- a/SousChefAI/Services/FirestoreRepository.swift
+++ /dev/null
@@ -1,248 +0,0 @@
-//
-// FirestoreRepository.swift
-// SousChefAI
-//
-// Repository pattern for Firebase Firestore data persistence
-// Note: Requires Firebase SDK to be added via Swift Package Manager
-//
-
-import Foundation
-import Combine
-
-/// Repository for managing user data in Firestore
-@MainActor
-final class FirestoreRepository: ObservableObject {
-
- // Uncomment when Firebase package is added
- // private let db = Firestore.firestore()
-
- @Published var currentUser: UserProfile?
- @Published var currentInventory: [Ingredient] = []
- @Published var savedRecipes: [Recipe] = []
-
- private var userId: String?
-
- nonisolated init() {
- // Initialize with current user ID from Firebase Auth
- // self.userId = Auth.auth().currentUser?.uid
- }
-
- // MARK: - User Profile
-
- /// Fetches the user profile from Firestore
- func fetchUserProfile(userId: String) async throws {
- self.userId = userId
-
- // When Firebase is added, use this:
- /*
- let document = try await db.collection("users").document(userId).getDocument()
-
- if let data = document.data() {
- currentUser = try Firestore.Decoder().decode(UserProfile.self, from: data)
- } else {
- // Create default profile
- let newProfile = UserProfile(id: userId)
- try await saveUserProfile(newProfile)
- currentUser = newProfile
- }
- */
-
- // Temporary fallback
- currentUser = UserProfile(id: userId)
- }
-
- /// Saves the user profile to Firestore
- func saveUserProfile(_ profile: UserProfile) async throws {
- guard userId != nil else { return }
-
- // When Firebase is added, use this:
- /*
- let data = try Firestore.Encoder().encode(profile)
- try await db.collection("users").document(userId).setData(data)
- */
-
- currentUser = profile
- }
-
- /// Updates dietary restrictions
- func updateDietaryRestrictions(_ restrictions: [String]) async throws {
- guard var profile = currentUser else { return }
- profile.dietaryRestrictions = restrictions
- try await saveUserProfile(profile)
- }
-
- /// Updates nutrition goals
- func updateNutritionGoals(_ goals: String) async throws {
- guard var profile = currentUser else { return }
- profile.nutritionGoals = goals
- try await saveUserProfile(profile)
- }
-
- // MARK: - Inventory Management
-
- /// Fetches current inventory from Firestore
- func fetchInventory() async throws {
- guard userId != nil else { return }
-
- // When Firebase is added, use this:
- /*
- let snapshot = try await db.collection("users")
- .document(userId)
- .collection("inventory")
- .getDocuments()
-
- currentInventory = try snapshot.documents.compactMap { document in
- try Firestore.Decoder().decode(Ingredient.self, from: document.data())
- }
- */
-
- // Temporary fallback
- currentInventory = []
- }
-
- /// Saves inventory to Firestore
- func saveInventory(_ ingredients: [Ingredient]) async throws {
- guard userId != nil else { return }
-
- // When Firebase is added, use this:
- /*
- let batch = db.batch()
- let inventoryRef = db.collection("users").document(userId).collection("inventory")
-
- // Delete existing inventory
- let existingDocs = try await inventoryRef.getDocuments()
- for doc in existingDocs.documents {
- batch.deleteDocument(doc.reference)
- }
-
- // Add new inventory
- for ingredient in ingredients {
- let docRef = inventoryRef.document(ingredient.id)
- let data = try Firestore.Encoder().encode(ingredient)
- batch.setData(data, forDocument: docRef)
- }
-
- try await batch.commit()
- */
-
- currentInventory = ingredients
- }
-
- /// Adds a single ingredient to inventory
- func addIngredient(_ ingredient: Ingredient) async throws {
- guard userId != nil else { return }
-
- // When Firebase is added, use this:
- /*
- let data = try Firestore.Encoder().encode(ingredient)
- try await db.collection("users")
- .document(userId)
- .collection("inventory")
- .document(ingredient.id)
- .setData(data)
- */
-
- currentInventory.append(ingredient)
- }
-
- /// Removes an ingredient from inventory
- func removeIngredient(_ ingredientId: String) async throws {
- guard userId != nil else { return }
-
- // When Firebase is added, use this:
- /*
- try await db.collection("users")
- .document(userId)
- .collection("inventory")
- .document(ingredientId)
- .delete()
- */
-
- currentInventory.removeAll { $0.id == ingredientId }
- }
-
- /// Updates an ingredient in inventory
- func updateIngredient(_ ingredient: Ingredient) async throws {
- guard userId != nil else { return }
-
- // When Firebase is added, use this:
- /*
- let data = try Firestore.Encoder().encode(ingredient)
- try await db.collection("users")
- .document(userId)
- .collection("inventory")
- .document(ingredient.id)
- .updateData(data)
- */
-
- if let index = currentInventory.firstIndex(where: { $0.id == ingredient.id }) {
- currentInventory[index] = ingredient
- }
- }
-
- // MARK: - Recipe Management
-
- /// Saves a recipe to user's favorites
- func saveRecipe(_ recipe: Recipe) async throws {
- guard userId != nil else { return }
-
- // When Firebase is added, use this:
- /*
- let data = try Firestore.Encoder().encode(recipe)
- try await db.collection("users")
- .document(userId)
- .collection("savedRecipes")
- .document(recipe.id)
- .setData(data)
- */
-
- if !savedRecipes.contains(where: { $0.id == recipe.id }) {
- savedRecipes.append(recipe)
- }
- }
-
- /// Fetches saved recipes
- func fetchSavedRecipes() async throws {
- guard userId != nil else { return }
-
- // When Firebase is added, use this:
- /*
- let snapshot = try await db.collection("users")
- .document(userId)
- .collection("savedRecipes")
- .getDocuments()
-
- savedRecipes = try snapshot.documents.compactMap { document in
- try Firestore.Decoder().decode(Recipe.self, from: document.data())
- }
- */
-
- // Temporary fallback
- savedRecipes = []
- }
-
- /// Deletes a saved recipe
- func deleteRecipe(_ recipeId: String) async throws {
- guard userId != nil else { return }
-
- // When Firebase is added, use this:
- /*
- try await db.collection("users")
- .document(userId)
- .collection("savedRecipes")
- .document(recipeId)
- .delete()
- */
-
- savedRecipes.removeAll { $0.id == recipeId }
- }
-
- // MARK: - Pantry Staples
-
- /// Updates pantry staples (ingredients always available)
- func updatePantryStaples(_ staples: [Ingredient]) async throws {
- guard var profile = currentUser else { return }
- profile.pantryStaples = staples
- try await saveUserProfile(profile)
- }
-}
diff --git a/SousChefAI/Services/GeminiRecipeService.swift b/SousChefAI/Services/GeminiRecipeService.swift
deleted file mode 100644
index 0ab8ee9..0000000
--- a/SousChefAI/Services/GeminiRecipeService.swift
+++ /dev/null
@@ -1,327 +0,0 @@
-//
-// GeminiRecipeService.swift
-// SousChefAI
-//
-// Concrete implementation using Google Gemini API for recipe generation
-// Note: Requires GoogleGenerativeAI SDK to be added via Swift Package Manager
-//
-
-import Foundation
-
-/// Google Gemini implementation for recipe generation and cooking guidance
-final class GeminiRecipeService: RecipeService, @unchecked Sendable {
-
- private let apiKey: String
-
- // Note: Uncomment when GoogleGenerativeAI package is added
- // private let model: GenerativeModel
-
- nonisolated init(apiKey: String = AppConfig.geminiAPIKey) {
- self.apiKey = apiKey
-
- // Initialize Gemini model when package is available
- // self.model = GenerativeModel(name: "gemini-2.0-flash-exp", apiKey: apiKey)
- }
-
- // MARK: - RecipeService Protocol Implementation
-
- func generateRecipes(inventory: [Ingredient], profile: UserProfile) async throws -> [Recipe] {
- guard apiKey != "INSERT_KEY_HERE" else {
- throw RecipeServiceError.apiKeyMissing
- }
-
- let prompt = buildRecipeGenerationPrompt(inventory: inventory, profile: profile)
-
- // When GoogleGenerativeAI is added, use this:
- // let response = try await model.generateContent(prompt)
- // return try parseRecipesFromResponse(response.text ?? "")
-
- // Temporary fallback using REST API
- return try await generateRecipesViaREST(prompt: prompt)
- }
-
- func scaleRecipe(_ recipe: Recipe, for ingredient: Ingredient, quantity: String) async throws -> Recipe {
- guard apiKey != "INSERT_KEY_HERE" else {
- throw RecipeServiceError.apiKeyMissing
- }
-
- let prompt = buildScalingPrompt(recipe: recipe, ingredient: ingredient, quantity: quantity)
-
- return try await scaleRecipeViaREST(prompt: prompt, originalRecipe: recipe)
- }
-
- func provideCookingGuidance(for step: String, context: String?) async throws -> String {
- guard apiKey != "INSERT_KEY_HERE" else {
- throw RecipeServiceError.apiKeyMissing
- }
-
- let prompt = buildGuidancePrompt(step: step, context: context)
-
- return try await generateGuidanceViaREST(prompt: prompt)
- }
-
- // MARK: - Prompt Building
-
- private func buildRecipeGenerationPrompt(inventory: [Ingredient], profile: UserProfile) -> String {
- let inventoryList = inventory.map { "- \($0.name): \($0.estimatedQuantity)" }.joined(separator: "\n")
-
- let restrictions = profile.dietaryRestrictions.isEmpty
- ? "None"
- : profile.dietaryRestrictions.joined(separator: ", ")
-
- let nutritionGoals = profile.nutritionGoals.isEmpty
- ? "No specific goals"
- : profile.nutritionGoals
-
- return """
- You are a professional chef AI assistant. Generate creative, practical recipes based on available ingredients.
-
- AVAILABLE INGREDIENTS:
- \(inventoryList)
-
- USER PREFERENCES:
- - Dietary Restrictions: \(restrictions)
- - Nutrition Goals: \(nutritionGoals)
-
- INSTRUCTIONS:
- 1. Generate 5-7 recipe ideas that can be made with these ingredients
- 2. Categorize recipes as:
- - "The Scavenger": Uses ONLY available ingredients (no shopping needed)
- - "The Upgrader": Requires 1-2 additional common ingredients
- 3. For each recipe, provide:
- - Title (creative and appetizing)
- - Brief description
- - List of missing ingredients (if any)
- - Step-by-step cooking instructions
- - Match score (0.0-1.0) based on ingredient availability
- - Estimated time
- - Servings
- 4. Respect ALL dietary restrictions strictly
- 5. Prioritize recipes with higher match scores
-
- RESPOND ONLY WITH VALID JSON in this exact format:
- {
- "recipes": [
- {
- "title": "Recipe Name",
- "description": "Brief description",
- "missingIngredients": [
- {
- "name": "ingredient name",
- "estimatedQuantity": "quantity",
- "confidence": 1.0
- }
- ],
- "steps": ["Step 1", "Step 2", ...],
- "matchScore": 0.95,
- "estimatedTime": "30 minutes",
- "servings": 4
- }
- ]
- }
- """
- }
-
- private func buildScalingPrompt(recipe: Recipe, ingredient: Ingredient, quantity: String) -> String {
- """
- Scale this recipe based on a limiting ingredient quantity.
-
- ORIGINAL RECIPE:
- Title: \(recipe.title)
- Servings: \(recipe.servings ?? 4)
-
- STEPS:
- \(recipe.steps.enumerated().map { "\($0 + 1). \($1)" }.joined(separator: "\n"))
-
- LIMITING INGREDIENT:
- \(ingredient.name): I only have \(quantity)
-
- INSTRUCTIONS:
- 1. Calculate the scaled portions for all ingredients
- 2. Adjust cooking times if necessary
- 3. Update servings count
- 4. Maintain the same step structure but update quantities
-
- RESPOND WITH JSON:
- {
- "title": "Recipe Name",
- "description": "Updated description with new servings",
- "missingIngredients": [...],
- "steps": ["Updated steps with scaled quantities"],
- "matchScore": 0.95,
- "estimatedTime": "updated time",
- "servings": updated_count
- }
- """
- }
-
- private func buildGuidancePrompt(step: String, context: String?) -> String {
- var prompt = """
- You are a cooking assistant providing real-time guidance.
-
- CURRENT STEP: \(step)
- """
-
- if let context = context {
- prompt += "\n\nVISUAL CONTEXT: \(context)"
- }
-
- prompt += """
-
- Provide brief, actionable guidance for this cooking step.
- If the context indicates the step is complete, confirm it.
- If there are issues, suggest corrections.
- Keep response under 50 words.
- """
-
- return prompt
- }
-
- // MARK: - REST API Helpers
-
- private func generateRecipesViaREST(prompt: String) async throws -> [Recipe] {
- let url = URL(string: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=\(apiKey)")!
-
- var request = URLRequest(url: url)
- request.httpMethod = "POST"
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
-
- let requestBody: [String: Any] = [
- "contents": [
- [
- "parts": [
- ["text": prompt]
- ]
- ]
- ],
- "generationConfig": [
- "temperature": 0.7,
- "topK": 40,
- "topP": 0.95,
- "maxOutputTokens": 8192
- ]
- ]
-
- request.httpBody = try JSONSerialization.data(withJSONObject: requestBody)
-
- let (data, response) = try await URLSession.shared.data(for: request)
-
- guard let httpResponse = response as? HTTPURLResponse,
- (200...299).contains(httpResponse.statusCode) else {
- throw RecipeServiceError.generationFailed("HTTP error")
- }
-
- return try parseGeminiResponse(data)
- }
-
- private func scaleRecipeViaREST(prompt: String, originalRecipe: Recipe) async throws -> Recipe {
- let url = URL(string: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=\(apiKey)")!
-
- var request = URLRequest(url: url)
- request.httpMethod = "POST"
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
-
- let requestBody: [String: Any] = [
- "contents": [
- [
- "parts": [
- ["text": prompt]
- ]
- ]
- ]
- ]
-
- request.httpBody = try JSONSerialization.data(withJSONObject: requestBody)
-
- let (data, _) = try await URLSession.shared.data(for: request)
- let recipes = try parseGeminiResponse(data)
-
- return recipes.first ?? originalRecipe
- }
-
- private func generateGuidanceViaREST(prompt: String) async throws -> String {
- let url = URL(string: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=\(apiKey)")!
-
- var request = URLRequest(url: url)
- request.httpMethod = "POST"
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
-
- let requestBody: [String: Any] = [
- "contents": [
- [
- "parts": [
- ["text": prompt]
- ]
- ]
- ]
- ]
-
- request.httpBody = try JSONSerialization.data(withJSONObject: requestBody)
-
- let (data, _) = try await URLSession.shared.data(for: request)
-
- guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
- let candidates = json["candidates"] as? [[String: Any]],
- let firstCandidate = candidates.first,
- let content = firstCandidate["content"] as? [String: Any],
- let parts = content["parts"] as? [[String: Any]],
- let firstPart = parts.first,
- let text = firstPart["text"] as? String else {
- throw RecipeServiceError.decodingError
- }
-
- return text
- }
-
- private func parseGeminiResponse(_ data: Data) throws -> [Recipe] {
- // Parse Gemini API response structure
- guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
- let candidates = json["candidates"] as? [[String: Any]],
- let firstCandidate = candidates.first,
- let content = firstCandidate["content"] as? [String: Any],
- let parts = content["parts"] as? [[String: Any]],
- let firstPart = parts.first,
- let text = firstPart["text"] as? String else {
- throw RecipeServiceError.decodingError
- }
-
- // Extract JSON from markdown code blocks if present
- let cleanedText = text
- .replacingOccurrences(of: "```json", with: "")
- .replacingOccurrences(of: "```", with: "")
- .trimmingCharacters(in: .whitespacesAndNewlines)
-
- guard let jsonData = cleanedText.data(using: .utf8),
- let recipeResponse = try? JSONDecoder().decode(GeminiRecipeResponse.self, from: jsonData) else {
- throw RecipeServiceError.decodingError
- }
-
- return recipeResponse.recipes.map { geminiRecipe in
- Recipe(
- title: geminiRecipe.title,
- description: geminiRecipe.description,
- missingIngredients: geminiRecipe.missingIngredients,
- steps: geminiRecipe.steps,
- matchScore: geminiRecipe.matchScore,
- estimatedTime: geminiRecipe.estimatedTime,
- servings: geminiRecipe.servings
- )
- }
- }
-}
-
-// MARK: - Response Models
-
-private struct GeminiRecipeResponse: Codable {
- let recipes: [GeminiRecipe]
-}
-
-private struct GeminiRecipe: Codable {
- let title: String
- let description: String
- let missingIngredients: [Ingredient]
- let steps: [String]
- let matchScore: Double
- let estimatedTime: String?
- let servings: Int?
-}
diff --git a/SousChefAI/Services/GeminiVisionService.swift b/SousChefAI/Services/GeminiVisionService.swift
deleted file mode 100644
index d35190a..0000000
--- a/SousChefAI/Services/GeminiVisionService.swift
+++ /dev/null
@@ -1,503 +0,0 @@
-//
-// GeminiVisionService.swift
-// SousChefAI
-//
-// Vision service using Google Gemini 3.0 Flash for ingredient detection
-// Sends least blurry frame per second to Gemini API for analysis
-//
-
-import Foundation
-import CoreVideo
-import CoreImage
-import Accelerate
-import UIKit
-
-/// Gemini-based implementation for vision ingredient detection
-final class GeminiVisionService: VisionService, @unchecked Sendable {
-
- private let apiKey: String
- private let modelName = "gemini-2.0-flash-exp" // Will update to 3.0 when available
-
- nonisolated init(apiKey: String = AppConfig.geminiAPIKey) {
- self.apiKey = apiKey
- }
-
- // MARK: - VisionService Protocol Implementation
-
- nonisolated func detectIngredients(from stream: AsyncStream) async throws -> [Ingredient] {
- // This method is used for continuous scanning
- // Collect frames, pick least blurry per second, send to Gemini
- var allDetectedIngredients: [Ingredient] = []
- var currentSecondFrames: [(buffer: CVPixelBuffer, blurScore: Double, timestamp: Date)] = []
- var lastProcessTime = Date()
-
- for await pixelBuffer in stream {
- let now = Date()
- let blurScore = calculateBlurScore(pixelBuffer)
-
- currentSecondFrames.append((buffer: pixelBuffer, blurScore: blurScore, timestamp: now))
-
- // Process every second
- if now.timeIntervalSince(lastProcessTime) >= 1.0 {
- // Find least blurry frame (highest Laplacian variance = sharpest)
- if let bestFrame = currentSecondFrames.max(by: { $0.blurScore < $1.blurScore }) {
- do {
- let ingredients = try await analyzeFrameWithGemini(
- bestFrame.buffer,
- existingIngredients: allDetectedIngredients
- )
-
- // Debug output
- print("🔍 GeminiVisionService: Detected \(ingredients.count) items in frame")
- if !ingredients.isEmpty {
- let jsonData = try? JSONEncoder().encode(ingredients)
- if let jsonString = jsonData.flatMap({ String(data: $0, encoding: .utf8) }) {
- print("📋 JSON Response: \(jsonString)")
- }
- }
-
- // Merge ingredients
- allDetectedIngredients = mergeIngredients(existing: allDetectedIngredients, new: ingredients)
- } catch {
- print("⚠️ GeminiVisionService: Frame analysis failed: \(error)")
- // Continue scanning on errors
- }
- }
-
- currentSecondFrames.removeAll()
- lastProcessTime = now
- }
-
- // Stop after reasonable scan time or max ingredients
- if allDetectedIngredients.count >= AppConfig.maxIngredientsPerScan {
- break
- }
- }
-
- return allDetectedIngredients
- }
-
- nonisolated func detectIngredients(from pixelBuffer: CVPixelBuffer) async throws -> [Ingredient] {
- return try await analyzeFrameWithGemini(pixelBuffer, existingIngredients: [])
- }
-
- nonisolated func analyzeCookingProgress(from stream: AsyncStream, for step: String) async throws -> CookingProgress {
- // For cooking progress, we'll use Gemini to analyze the current state
- var latestFrame: CVPixelBuffer?
-
- for await frame in stream {
- latestFrame = frame
- break // Just get one frame for now
- }
-
- guard let frame = latestFrame else {
- return CookingProgress(isComplete: false, confidence: 0.0, feedback: "No frame available")
- }
-
- return try await analyzeCookingStepWithGemini(frame, step: step)
- }
-
- // MARK: - Blur Detection (Laplacian Variance)
-
- /// Calculates blur score using Laplacian variance
- /// Higher value = sharper image, Lower value = more blurry
- nonisolated private func calculateBlurScore(_ pixelBuffer: CVPixelBuffer) -> Double {
- CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
- defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }
-
- let width = CVPixelBufferGetWidth(pixelBuffer)
- let height = CVPixelBufferGetHeight(pixelBuffer)
- let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
-
- guard let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) else {
- return 0
- }
-
- // Convert to grayscale for Laplacian calculation
- // For BGRA format, we'll use simple luminance approximation
- var grayscale = [Float](repeating: 0, count: width * height)
- let pixels = baseAddress.assumingMemoryBound(to: UInt8.self)
-
- for y in 0.. [Ingredient] {
- guard apiKey != "INSERT_KEY_HERE" else {
- throw VisionServiceError.apiKeyMissing
- }
-
- // Convert pixel buffer to base64 JPEG
- let imageData = try convertToJPEG(pixelBuffer)
- let base64Image = imageData.base64EncodedString()
-
- // Build prompt with existing ingredients for deduplication
- let existingList = existingIngredients.isEmpty
- ? "None yet"
- : existingIngredients.map { $0.name }.joined(separator: ", ")
-
- let prompt = """
- Analyze this image and identify all food items and ingredients visible.
-
- ALREADY DETECTED ITEMS (avoid duplicates, merge similar items):
- \(existingList)
-
- For each NEW item not already listed above, provide:
- 1. The item name (normalized - e.g., "milk" not "milk 2%", "whole milk", etc.)
- 2. Estimated quantity (numeric with unit, e.g., "2", "500ml", "1 dozen")
- 3. Top 3 guesses for what the item might be, with confidence (0.0-1.0)
-
- IMPORTANT:
- - If you see "milk 2%" and "milk" is already detected, DO NOT include it
- - Use simple, normalized names (e.g., "apple" not "red delicious apple")
- - Quantity should be numeric estimates
- - Only include food items and ingredients, not containers or non-food items
-
- RESPOND ONLY WITH VALID JSON in this exact format (no markdown):
- {
- "items": [
- {
- "name": "normalized item name",
- "quantity": "2",
- "guesses": [
- {"name": "primary guess", "confidence": 0.95},
- {"name": "second guess", "confidence": 0.7},
- {"name": "third guess", "confidence": 0.3}
- ]
- }
- ]
- }
-
- If no new food items are visible, return: {"items": []}
- """
-
- let url = URL(string: "https://generativelanguage.googleapis.com/v1beta/models/\(modelName):generateContent?key=\(apiKey)")!
-
- var request = URLRequest(url: url)
- request.httpMethod = "POST"
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
- request.timeoutInterval = 30
-
- let requestBody: [String: Any] = [
- "contents": [
- [
- "parts": [
- ["text": prompt],
- [
- "inline_data": [
- "mime_type": "image/jpeg",
- "data": base64Image
- ]
- ]
- ]
- ]
- ],
- "generationConfig": [
- "temperature": 0.2,
- "topK": 32,
- "topP": 0.95,
- "maxOutputTokens": 2048
- ]
- ]
-
- request.httpBody = try JSONSerialization.data(withJSONObject: requestBody)
-
- let (data, response) = try await URLSession.shared.data(for: request)
-
- guard let httpResponse = response as? HTTPURLResponse else {
- throw VisionServiceError.invalidResponse
- }
-
- guard (200...299).contains(httpResponse.statusCode) else {
- print("❌ Gemini API Error: HTTP \(httpResponse.statusCode)")
- if let errorText = String(data: data, encoding: .utf8) {
- print("❌ Error body: \(errorText)")
- }
- throw VisionServiceError.networkError(NSError(domain: "GeminiAPI", code: httpResponse.statusCode))
- }
-
- return try parseGeminiVisionResponse(data)
- }
-
- nonisolated private func analyzeCookingStepWithGemini(_ pixelBuffer: CVPixelBuffer, step: String) async throws -> CookingProgress {
- guard apiKey != "INSERT_KEY_HERE" else {
- throw VisionServiceError.apiKeyMissing
- }
-
- let imageData = try convertToJPEG(pixelBuffer)
- let base64Image = imageData.base64EncodedString()
-
- let prompt = """
- Analyze this cooking image for the following step:
- "\(step)"
-
- Determine:
- 1. Is this step complete? (true/false)
- 2. Confidence level (0.0-1.0)
- 3. Brief feedback on the current state
-
- RESPOND WITH JSON:
- {
- "isComplete": false,
- "confidence": 0.7,
- "feedback": "Brief description of current state"
- }
- """
-
- let url = URL(string: "https://generativelanguage.googleapis.com/v1beta/models/\(modelName):generateContent?key=\(apiKey)")!
-
- var request = URLRequest(url: url)
- request.httpMethod = "POST"
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
-
- let requestBody: [String: Any] = [
- "contents": [
- [
- "parts": [
- ["text": prompt],
- [
- "inline_data": [
- "mime_type": "image/jpeg",
- "data": base64Image
- ]
- ]
- ]
- ]
- ]
- ]
-
- request.httpBody = try JSONSerialization.data(withJSONObject: requestBody)
-
- let (data, _) = try await URLSession.shared.data(for: request)
-
- return try parseCookingProgressResponse(data)
- }
-
- // MARK: - Image Conversion
-
- nonisolated private func convertToJPEG(_ pixelBuffer: CVPixelBuffer) throws -> Data {
- let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
- let context = CIContext()
-
- guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else {
- throw VisionServiceError.invalidResponse
- }
-
- let uiImage = UIImage(cgImage: cgImage)
-
- // Compress to reasonable size for API
- guard let jpegData = uiImage.jpegData(compressionQuality: 0.7) else {
- throw VisionServiceError.invalidResponse
- }
-
- return jpegData
- }
-
- // MARK: - Response Parsing
-
- nonisolated private func parseGeminiVisionResponse(_ data: Data) throws -> [Ingredient] {
- guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
- let candidates = json["candidates"] as? [[String: Any]],
- let firstCandidate = candidates.first,
- let content = firstCandidate["content"] as? [String: Any],
- let parts = content["parts"] as? [[String: Any]],
- let firstPart = parts.first,
- let text = firstPart["text"] as? String else {
- throw VisionServiceError.decodingError(NSError(domain: "Parsing", code: 0))
- }
-
- // Clean up response (remove markdown if present)
- let cleanedText = text
- .replacingOccurrences(of: "```json", with: "")
- .replacingOccurrences(of: "```", with: "")
- .trimmingCharacters(in: .whitespacesAndNewlines)
-
- print("📝 Cleaned Gemini response: \(cleanedText)")
-
- guard let jsonData = cleanedText.data(using: .utf8) else {
- throw VisionServiceError.decodingError(NSError(domain: "Parsing", code: 1))
- }
-
- let response = try JSONDecoder().decode(GeminiVisionResponse.self, from: jsonData)
-
- return response.items.map { item in
- // Use the highest confidence guess as the primary name
- let bestGuess = item.guesses.first
- let confidence = bestGuess?.confidence ?? 0.5
-
- return Ingredient(
- name: item.name,
- estimatedQuantity: item.quantity,
- confidence: confidence,
- guesses: item.guesses.map { IngredientGuess(name: $0.name, confidence: $0.confidence) }
- )
- }
- }
-
- nonisolated private func parseCookingProgressResponse(_ data: Data) throws -> CookingProgress {
- guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
- let candidates = json["candidates"] as? [[String: Any]],
- let firstCandidate = candidates.first,
- let content = firstCandidate["content"] as? [String: Any],
- let parts = content["parts"] as? [[String: Any]],
- let firstPart = parts.first,
- let text = firstPart["text"] as? String else {
- throw VisionServiceError.decodingError(NSError(domain: "Parsing", code: 0))
- }
-
- let cleanedText = text
- .replacingOccurrences(of: "```json", with: "")
- .replacingOccurrences(of: "```", with: "")
- .trimmingCharacters(in: .whitespacesAndNewlines)
-
- guard let jsonData = cleanedText.data(using: .utf8),
- let progressJson = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] else {
- throw VisionServiceError.decodingError(NSError(domain: "Parsing", code: 1))
- }
-
- return CookingProgress(
- isComplete: progressJson["isComplete"] as? Bool ?? false,
- confidence: progressJson["confidence"] as? Double ?? 0.5,
- feedback: progressJson["feedback"] as? String ?? "Processing..."
- )
- }
-
- // MARK: - Ingredient Merging
-
- /// Merges new ingredients with existing ones, handling similar names and taking max quantity
- nonisolated private func mergeIngredients(existing: [Ingredient], new: [Ingredient]) -> [Ingredient] {
- var merged = existing.reduce(into: [String: Ingredient]()) { dict, ingredient in
- dict[ingredient.name.lowercased()] = ingredient
- }
-
- for newIngredient in new {
- let normalizedName = newIngredient.name.lowercased()
-
- // Check for similar existing items
- let similarKey = merged.keys.first { existingKey in
- isSimilarIngredient(existingKey, normalizedName)
- }
-
- if let key = similarKey, let existing = merged[key] {
- // Merge: take max quantity, higher confidence
- let mergedQuantity = mergeQuantities(existing.estimatedQuantity, newIngredient.estimatedQuantity)
- let mergedConfidence = max(existing.confidence, newIngredient.confidence)
-
- merged[key] = Ingredient(
- id: existing.id,
- name: existing.name, // Keep original name
- estimatedQuantity: mergedQuantity,
- confidence: mergedConfidence,
- guesses: existing.guesses // Keep original guesses
- )
- } else {
- // Add as new
- merged[normalizedName] = newIngredient
- }
- }
-
- return Array(merged.values).sorted { $0.confidence > $1.confidence }
- }
-
- /// Checks if two ingredient names are similar (e.g., "milk" and "milk 2%")
- nonisolated private func isSimilarIngredient(_ name1: String, _ name2: String) -> Bool {
- // Exact match
- if name1 == name2 { return true }
-
- // One contains the other
- if name1.contains(name2) || name2.contains(name1) { return true }
-
- // Common ingredient variations
- let variations: [[String]] = [
- ["milk", "whole milk", "2% milk", "skim milk", "milk 2%"],
- ["egg", "eggs", "large eggs"],
- ["butter", "unsalted butter", "salted butter"],
- ["cheese", "cheddar", "cheddar cheese"],
- ["chicken", "chicken breast", "chicken thigh"],
- ["onion", "onions", "yellow onion", "white onion"],
- ["tomato", "tomatoes", "cherry tomatoes"],
- ["potato", "potatoes", "russet potato"]
- ]
-
- for group in variations {
- let lowercaseGroup = group.map { $0.lowercased() }
- if lowercaseGroup.contains(name1) && lowercaseGroup.contains(name2) {
- return true
- }
- }
-
- return false
- }
-
- /// Merges two quantity strings, taking the maximum
- nonisolated private func mergeQuantities(_ q1: String, _ q2: String) -> String {
- // Extract numeric values
- let num1 = extractNumber(from: q1) ?? 0
- let num2 = extractNumber(from: q2) ?? 0
-
- // Return the quantity with larger number
- return num1 >= num2 ? q1 : q2
- }
-
- nonisolated private func extractNumber(from string: String) -> Double? {
- let pattern = #"[\d.]+"#
- guard let regex = try? NSRegularExpression(pattern: pattern),
- let match = regex.firstMatch(in: string, range: NSRange(string.startIndex..., in: string)),
- let range = Range(match.range, in: string) else {
- return nil
- }
- return Double(string[range])
- }
-}
-
-// MARK: - Response Models
-
-private struct GeminiVisionResponse: Codable {
- let items: [GeminiVisionItem]
-}
-
-private struct GeminiVisionItem: Codable {
- let name: String
- let quantity: String
- let guesses: [GeminiGuess]
-}
-
-private struct GeminiGuess: Codable {
- let name: String
- let confidence: Double
-}
diff --git a/SousChefAI/Services/RecipeService.swift b/SousChefAI/Services/RecipeService.swift
deleted file mode 100644
index 3d5db44..0000000
--- a/SousChefAI/Services/RecipeService.swift
+++ /dev/null
@@ -1,56 +0,0 @@
-//
-// RecipeService.swift
-// SousChefAI
-//
-// Protocol for recipe generation and AI reasoning services
-//
-
-import Foundation
-
-/// Protocol for AI-powered recipe generation
-protocol RecipeService: Sendable {
- /// Generates recipes based on available ingredients and user preferences
- /// - Parameters:
- /// - inventory: Available ingredients
- /// - profile: User dietary preferences and restrictions
- /// - Returns: Array of recipe suggestions with match scores
- func generateRecipes(inventory: [Ingredient], profile: UserProfile) async throws -> [Recipe]
-
- /// Scales a recipe based on a limiting ingredient quantity
- /// - Parameters:
- /// - recipe: The recipe to scale
- /// - ingredient: The limiting ingredient
- /// - quantity: Available quantity of the limiting ingredient
- /// - Returns: Scaled recipe with adjusted portions
- func scaleRecipe(_ recipe: Recipe, for ingredient: Ingredient, quantity: String) async throws -> Recipe
-
- /// Provides real-time cooking guidance
- /// - Parameters:
- /// - step: Current cooking step
- /// - context: Additional context (e.g., visual feedback)
- /// - Returns: Guidance text
- func provideCookingGuidance(for step: String, context: String?) async throws -> String
-}
-
-enum RecipeServiceError: Error, LocalizedError {
- case apiKeyMissing
- case invalidRequest
- case generationFailed(String)
- case networkError(Error)
- case decodingError
-
- var errorDescription: String? {
- switch self {
- case .apiKeyMissing:
- return "Recipe service API key not configured"
- case .invalidRequest:
- return "Invalid recipe generation request"
- case .generationFailed(let reason):
- return "Recipe generation failed: \(reason)"
- case .networkError(let error):
- return "Network error: \(error.localizedDescription)"
- case .decodingError:
- return "Failed to parse recipe response"
- }
- }
-}
diff --git a/SousChefAI/Services/VisionService.swift b/SousChefAI/Services/VisionService.swift
deleted file mode 100644
index d3bbad1..0000000
--- a/SousChefAI/Services/VisionService.swift
+++ /dev/null
@@ -1,60 +0,0 @@
-//
-// VisionService.swift
-// SousChefAI
-//
-// Protocol-based vision service for ingredient detection
-// Allows swapping between different AI providers
-//
-
-import Foundation
-@preconcurrency import CoreVideo
-
-/// Protocol for vision-based ingredient detection services
-protocol VisionService: Sendable {
- /// Detects ingredients from a stream of video frames
- /// - Parameter stream: Async stream of pixel buffers from camera
- /// - Returns: Array of detected ingredients with confidence scores
- func detectIngredients(from stream: AsyncStream) async throws -> [Ingredient]
-
- /// Detects ingredients from a single image
- /// - Parameter pixelBuffer: Single frame to analyze
- /// - Returns: Array of detected ingredients with confidence scores
- func detectIngredients(from pixelBuffer: CVPixelBuffer) async throws -> [Ingredient]
-
- /// Analyzes cooking progress for a given step
- /// - Parameters:
- /// - stream: Video stream of current cooking
- /// - step: The cooking step to monitor
- /// - Returns: Progress update and completion detection
- func analyzeCookingProgress(from stream: AsyncStream, for step: String) async throws -> CookingProgress
-}
-
-/// Represents cooking progress analysis
-struct CookingProgress: Sendable {
- let isComplete: Bool
- let confidence: Double
- let feedback: String
-}
-
-enum VisionServiceError: Error, LocalizedError {
- case connectionFailed
- case invalidResponse
- case apiKeyMissing
- case networkError(Error)
- case decodingError(Error)
-
- var errorDescription: String? {
- switch self {
- case .connectionFailed:
- return "Failed to connect to vision service"
- case .invalidResponse:
- return "Received invalid response from vision service"
- case .apiKeyMissing:
- return "Vision service API key not configured"
- case .networkError(let error):
- return "Network error: \(error.localizedDescription)"
- case .decodingError(let error):
- return "Failed to decode response: \(error.localizedDescription)"
- }
- }
-}
diff --git a/SousChefAI/SousChefAIApp.swift b/SousChefAI/SousChefAIApp.swift
deleted file mode 100644
index 496ff27..0000000
--- a/SousChefAI/SousChefAIApp.swift
+++ /dev/null
@@ -1,39 +0,0 @@
-//
-// SousChefAIApp.swift
-// SousChefAI
-//
-// Created by Aditya Pulipaka on 2/11/26.
-//
-
-import SwiftUI
-// Uncomment when Firebase package is added
-// import FirebaseCore
-
-@main
-struct SousChefAIApp: App {
-
- // Uncomment when Firebase package is added
- // init() {
- // FirebaseApp.configure()
- // }
-
- // [INSERT_FIREBASE_GOOGLESERVICE-INFO.PLIST_SETUP_HERE]
- // Firebase Setup Instructions:
- // 1. Add Firebase to your project via Swift Package Manager
- // - File > Add Package Dependencies
- // - URL: https://github.com/firebase/firebase-ios-sdk
- // - Add: FirebaseAuth, FirebaseFirestore
- // 2. Download GoogleService-Info.plist from Firebase Console
- // 3. Add it to the Xcode project (drag into project navigator)
- // 4. Ensure it's added to the SousChefAI target
- // 5. Uncomment the FirebaseCore import and init() above
-
- @StateObject private var repository = FirestoreRepository()
-
- var body: some Scene {
- WindowGroup {
- ContentView()
- .environmentObject(repository)
- }
- }
-}
diff --git a/SousChefAI/ViewModels/CookingModeViewModel.swift b/SousChefAI/ViewModels/CookingModeViewModel.swift
deleted file mode 100644
index cc25e76..0000000
--- a/SousChefAI/ViewModels/CookingModeViewModel.swift
+++ /dev/null
@@ -1,191 +0,0 @@
-//
-// CookingModeViewModel.swift
-// SousChefAI
-//
-// ViewModel for live cooking guidance with AI monitoring
-//
-
-import Foundation
-import AVFoundation
-import CoreVideo
-import Combine
-import UIKit
-
-@MainActor
-final class CookingModeViewModel: ObservableObject {
-
- @Published var currentStepIndex = 0
- @Published var isMonitoring = false
- @Published var feedback: String = "Ready to start"
- @Published var stepComplete = false
- @Published var confidence: Double = 0.0
- @Published var error: Error?
-
- let recipe: Recipe
- private let visionService: VisionService
- private let recipeService: RecipeService
- private let cameraManager: CameraManager
- private var monitoringTask: Task?
-
- var currentStep: String {
- guard currentStepIndex < recipe.steps.count else {
- return "Recipe complete!"
- }
- return recipe.steps[currentStepIndex]
- }
-
- var progress: Double {
- guard !recipe.steps.isEmpty else { return 0 }
- return Double(currentStepIndex) / Double(recipe.steps.count)
- }
-
- var isComplete: Bool {
- currentStepIndex >= recipe.steps.count
- }
-
- nonisolated init(recipe: Recipe,
- visionService: VisionService = ARVisionService(),
- recipeService: RecipeService = GeminiRecipeService(),
- cameraManager: CameraManager = CameraManager()) {
- self.recipe = recipe
- self.visionService = visionService
- self.recipeService = recipeService
- self.cameraManager = cameraManager
- }
-
- // MARK: - Camera Setup
-
- func setupCamera() async {
- do {
- try await cameraManager.setupSession()
- } catch {
- self.error = error
- }
- }
-
- func startCamera() {
- cameraManager.startSession()
- }
-
- func stopCamera() {
- cameraManager.stopSession()
- }
-
- func getPreviewLayer() -> AVCaptureVideoPreviewLayer? {
- cameraManager.previewLayer()
- }
-
- // MARK: - Step Navigation
-
- func nextStep() {
- guard currentStepIndex < recipe.steps.count else { return }
-
- currentStepIndex += 1
- stepComplete = false
- confidence = 0.0
- feedback = currentStepIndex < recipe.steps.count ? "Starting next step..." : "Recipe complete!"
-
- if !isComplete && isMonitoring {
- // Restart monitoring for new step
- stopMonitoring()
- startMonitoring()
- }
- }
-
- func previousStep() {
- guard currentStepIndex > 0 else { return }
-
- currentStepIndex -= 1
- stepComplete = false
- confidence = 0.0
- feedback = "Returned to previous step"
-
- if isMonitoring {
- stopMonitoring()
- startMonitoring()
- }
- }
-
- // MARK: - AI Monitoring
-
- func startMonitoring() {
- guard !isComplete, !isMonitoring else { return }
-
- isMonitoring = true
- feedback = "Monitoring your cooking..."
-
- monitoringTask = Task {
- do {
- let stream = cameraManager.frameStream()
- let progress = try await visionService.analyzeCookingProgress(
- from: stream,
- for: currentStep
- )
-
- handleProgress(progress)
- } catch {
- self.error = error
- feedback = "Monitoring paused"
- isMonitoring = false
- }
- }
- }
-
- func stopMonitoring() {
- monitoringTask?.cancel()
- monitoringTask = nil
- isMonitoring = false
- feedback = "Monitoring stopped"
- }
-
- private func handleProgress(_ progress: CookingProgress) {
- confidence = progress.confidence
- feedback = progress.feedback
- stepComplete = progress.isComplete
-
- if progress.isComplete && progress.confidence > 0.8 {
- // Play haptic feedback
- let generator = UINotificationFeedbackGenerator()
- generator.notificationOccurred(.success)
-
- // Speak the feedback using text-to-speech
- speakFeedback("Step complete! \(progress.feedback)")
- }
- }
-
- // MARK: - Text Guidance
-
- func getTextGuidance() async {
- do {
- let guidance = try await recipeService.provideCookingGuidance(
- for: currentStep,
- context: feedback
- )
- feedback = guidance
- } catch {
- self.error = error
- }
- }
-
- // MARK: - Text-to-Speech
-
- private func speakFeedback(_ text: String) {
- let utterance = AVSpeechUtterance(string: text)
- utterance.voice = AVSpeechSynthesisVoice(language: "en-US")
- utterance.rate = 0.5
-
- let synthesizer = AVSpeechSynthesizer()
- synthesizer.speak(utterance)
- }
-
- func speakCurrentStep() {
- speakFeedback(currentStep)
- }
-
- // MARK: - Cleanup
-
- func cleanup() {
- stopMonitoring()
- stopCamera()
- }
-}
diff --git a/SousChefAI/ViewModels/RecipeGeneratorViewModel.swift b/SousChefAI/ViewModels/RecipeGeneratorViewModel.swift
deleted file mode 100644
index dc2a378..0000000
--- a/SousChefAI/ViewModels/RecipeGeneratorViewModel.swift
+++ /dev/null
@@ -1,135 +0,0 @@
-//
-// RecipeGeneratorViewModel.swift
-// SousChefAI
-//
-// ViewModel for recipe generation and filtering
-//
-
-import Foundation
-import Combine
-
-@MainActor
-final class RecipeGeneratorViewModel: ObservableObject {
-
- @Published var recipes: [Recipe] = []
- @Published var filteredRecipes: [Recipe] = []
- @Published var isGenerating = false
- @Published var error: Error?
- @Published var selectedFilter: RecipeFilter = .all
-
- private let recipeService: RecipeService
- private let repository: FirestoreRepository
-
- nonisolated init(recipeService: RecipeService = GeminiRecipeService(),
- repository: FirestoreRepository = FirestoreRepository()) {
- self.recipeService = recipeService
- self.repository = repository
- }
-
- // MARK: - Recipe Generation
-
- func generateRecipes(inventory: [Ingredient], profile: UserProfile) async {
- isGenerating = true
- error = nil
-
- do {
- let generatedRecipes = try await recipeService.generateRecipes(
- inventory: inventory,
- profile: profile
- )
-
- recipes = generatedRecipes.sorted { $0.matchScore > $1.matchScore }
- applyFilter()
- } catch {
- self.error = error
- }
-
- isGenerating = false
- }
-
- // MARK: - Filtering
-
- func applyFilter() {
- switch selectedFilter {
- case .all:
- filteredRecipes = recipes
-
- case .scavenger:
- filteredRecipes = recipes.filter { $0.category == .scavenger }
-
- case .upgrader:
- filteredRecipes = recipes.filter { $0.category == .upgrader }
-
- case .highMatch:
- filteredRecipes = recipes.filter { $0.matchScore >= 0.8 }
- }
- }
-
- func setFilter(_ filter: RecipeFilter) {
- selectedFilter = filter
- applyFilter()
- }
-
- // MARK: - Recipe Scaling
-
- func scaleRecipe(_ recipe: Recipe, for ingredient: Ingredient, quantity: String) async {
- do {
- let scaledRecipe = try await recipeService.scaleRecipe(
- recipe,
- for: ingredient,
- quantity: quantity
- )
-
- // Update the recipe in the list
- if let index = recipes.firstIndex(where: { $0.id == recipe.id }) {
- recipes[index] = scaledRecipe
- applyFilter()
- }
- } catch {
- self.error = error
- }
- }
-
- // MARK: - Favorites
-
- func saveRecipe(_ recipe: Recipe) async {
- do {
- try await repository.saveRecipe(recipe)
- } catch {
- self.error = error
- }
- }
-}
-
-// MARK: - Recipe Filter
-
-enum RecipeFilter: String, CaseIterable, Identifiable {
- case all = "All Recipes"
- case scavenger = "The Scavenger"
- case upgrader = "The Upgrader"
- case highMatch = "High Match"
-
- var id: String { rawValue }
-
- var icon: String {
- switch self {
- case .all: return "square.grid.2x2"
- case .scavenger: return "checkmark.circle.fill"
- case .upgrader: return "cart.badge.plus"
- case .highMatch: return "star.fill"
- }
- }
-
- var description: String {
- switch self {
- case .all:
- return "Show all recipes"
- case .scavenger:
- return "Uses only your ingredients"
- case .upgrader:
- return "Needs 1-2 additional items"
- case .highMatch:
- return "80%+ ingredient match"
- }
- }
-}
diff --git a/SousChefAI/ViewModels/ScannerViewModel.swift b/SousChefAI/ViewModels/ScannerViewModel.swift
deleted file mode 100644
index 831402b..0000000
--- a/SousChefAI/ViewModels/ScannerViewModel.swift
+++ /dev/null
@@ -1,364 +0,0 @@
-//
-// ScannerViewModel.swift
-// SousChefAI
-//
-// ViewModel for the scanner view with real-time ingredient detection
-//
-
-import Foundation
-import SwiftUI
-import CoreVideo
-import AVFoundation
-import Combine
-
-@MainActor
-final class ScannerViewModel: ObservableObject {
-
- @Published var detectedIngredients: [Ingredient] = []
- @Published var isScanning = false
- @Published var error: Error?
- @Published var scanProgress: String = "Ready to scan"
-
- /// The most recently detected new ingredient (for banner display)
- @Published var latestNewIngredient: Ingredient?
-
- private let visionService: VisionService
- private let cameraManager: CameraManager
- private var scanTask: Task?
-
- /// Callback when a new ingredient is detected (not a duplicate)
- var onNewIngredientDetected: ((Ingredient) -> Void)?
-
- nonisolated init(cameraManager: CameraManager = CameraManager()) {
- print("📱 ScannerViewModel.init() - Creating ViewModel at \(Date())")
-
- // Select vision service based on configuration
- let visionService: VisionService = switch AppConfig.scanningMode {
- case .geminiVision:
- GeminiVisionService()
- case .arKit:
- ARVisionService()
- }
-
- print("📱 ScannerViewModel.init() - Using \(AppConfig.scanningMode.rawValue) scanning mode")
-
- self.visionService = visionService
- self.cameraManager = cameraManager
- }
-
- // MARK: - Camera Management
-
- func setupCamera() async {
- print("📱 ScannerViewModel.setupCamera() - STARTED at \(Date())")
- do {
- try await cameraManager.setupSession()
- print("📱 ScannerViewModel.setupCamera() - ✅ SUCCESS at \(Date())")
- } catch {
- print("📱 ScannerViewModel.setupCamera() - ❌ ERROR: \(error)")
- self.error = error
- }
- }
-
- func startCamera() {
- cameraManager.startSession()
- }
-
- func stopCamera() {
- cameraManager.stopSession()
- }
-
- func getPreviewLayer() -> AVCaptureVideoPreviewLayer? {
- print("📱 ScannerViewModel.getPreviewLayer() - ⚠️ REQUESTING preview layer at \(Date())")
- return cameraManager.previewLayer()
- }
-
- // MARK: - Scanning
-
- func startScanning() {
- guard !isScanning else { return }
-
- isScanning = true
- scanProgress = "Scanning ingredients..."
- print("📱 ScannerViewModel.startScanning() - Started with \(AppConfig.scanningMode.rawValue) mode")
-
- scanTask = Task {
- let startTime = Date()
-
- do {
- let stream = cameraManager.frameStream()
-
- // For Gemini mode, we use real-time detection with callbacks
- if AppConfig.scanningMode == .geminiVision {
- // Process frames continuously until stopped or timeout
- var lastProcessTime = Date()
- var currentSecondFrames: [(buffer: CVPixelBuffer, timestamp: Date)] = []
-
- for await frame in stream {
- guard !Task.isCancelled else { break }
-
- // Check timeout
- if Date().timeIntervalSince(startTime) >= AppConfig.maxScanDuration {
- print("📱 ScannerViewModel: Max scan duration reached")
- break
- }
-
- currentSecondFrames.append((buffer: frame, timestamp: Date()))
-
- // Process every second
- let now = Date()
- if now.timeIntervalSince(lastProcessTime) >= AppConfig.geminiFrameInterval {
- // Pick the frame from the middle of the batch (reasonable approximation)
- if let bestFrame = currentSecondFrames[safe: currentSecondFrames.count / 2]?.buffer {
- do {
- let previousCount = detectedIngredients.count
- let ingredients = try await visionService.detectIngredients(from: bestFrame)
-
- // Find new ingredients before merging
- let newIngredients = findNewIngredients(ingredients)
-
- // Merge with existing
- updateDetectedIngredients(ingredients, mergeMode: true)
-
- // Notify about new ingredients
- for newIngredient in newIngredients {
- print("🆕 New ingredient detected: \(newIngredient.name)")
- latestNewIngredient = newIngredient
- onNewIngredientDetected?(newIngredient)
- }
-
- scanProgress = "Found \(detectedIngredients.count) items..."
- } catch {
- print("⚠️ Frame analysis error: \(error)")
- // Continue scanning on errors
- }
- }
-
- currentSecondFrames.removeAll()
- lastProcessTime = now
- }
-
- // Stop if we have enough ingredients
- if detectedIngredients.count >= AppConfig.maxIngredientsPerScan {
- break
- }
- }
- } else {
- // AR mode: use batch detection
- let ingredients = try await visionService.detectIngredients(from: stream)
- updateDetectedIngredients(ingredients)
- }
-
- scanProgress = "Scan complete! Found \(detectedIngredients.count) ingredients"
- } catch {
- self.error = error
- scanProgress = "Scan failed: \(error.localizedDescription)"
- }
-
- isScanning = false
- }
- }
-
- func stopScanning() {
- scanTask?.cancel()
- scanTask = nil
- isScanning = false
- scanProgress = detectedIngredients.isEmpty ? "Ready to scan" : "Scan captured"
- }
-
- // MARK: - Real-time Detection Mode
-
- func startRealTimeDetection() {
- guard !isScanning else { return }
-
- isScanning = true
- scanProgress = "Detecting in real-time..."
-
- scanTask = Task {
- let stream = cameraManager.frameStream()
-
- for await frame in stream {
- guard !Task.isCancelled else { break }
-
- do {
- // Process individual frames
- let ingredients = try await visionService.detectIngredients(from: frame)
- updateDetectedIngredients(ingredients, mergeMode: true)
-
- scanProgress = "Detected \(detectedIngredients.count) items"
- } catch {
- // Continue on errors in real-time mode
- continue
- }
-
- // Throttle to avoid overwhelming the API
- try? await Task.sleep(for: .seconds(1))
- }
-
- isScanning = false
- }
- }
-
- // MARK: - Ingredient Management
-
- /// Finds ingredients that are truly new (not already in our list)
- private func findNewIngredients(_ newIngredients: [Ingredient]) -> [Ingredient] {
- return newIngredients.filter { newIngredient in
- !detectedIngredients.contains { existing in
- isSimilarIngredient(existing.name, newIngredient.name)
- }
- }
- }
-
- /// Checks if two ingredient names refer to the same item
- private func isSimilarIngredient(_ name1: String, _ name2: String) -> Bool {
- let n1 = name1.lowercased()
- let n2 = name2.lowercased()
-
- // Exact match
- if n1 == n2 { return true }
-
- // One contains the other
- if n1.contains(n2) || n2.contains(n1) { return true }
-
- return false
- }
-
- private func updateDetectedIngredients(_ newIngredients: [Ingredient], mergeMode: Bool = false) {
- if mergeMode {
- // Merge with existing ingredients, keeping higher confidence and max quantity
- var merged = detectedIngredients.reduce(into: [String: Ingredient]()) { dict, ingredient in
- dict[ingredient.name.lowercased()] = ingredient
- }
-
- for ingredient in newIngredients {
- let normalizedName = ingredient.name.lowercased()
-
- // Check for similar existing items
- let similarKey = merged.keys.first { existingKey in
- isSimilarIngredient(existingKey, normalizedName)
- }
-
- if let key = similarKey, let existing = merged[key] {
- // Merge: take max quantity, higher confidence
- let mergedQuantity = mergeQuantities(existing.estimatedQuantity, ingredient.estimatedQuantity)
- let mergedConfidence = max(existing.confidence, ingredient.confidence)
-
- merged[key] = Ingredient(
- id: existing.id,
- name: existing.name,
- estimatedQuantity: mergedQuantity,
- confidence: mergedConfidence,
- guesses: existing.guesses.isEmpty ? ingredient.guesses : existing.guesses
- )
- } else {
- merged[normalizedName] = ingredient
- }
- }
-
- detectedIngredients = Array(merged.values).sorted { $0.confidence > $1.confidence }
- } else {
- detectedIngredients = newIngredients
- }
- }
-
- /// Merges two quantity strings, taking the maximum numeric value
- private func mergeQuantities(_ q1: String, _ q2: String) -> String {
- let num1 = extractNumber(from: q1) ?? 0
- let num2 = extractNumber(from: q2) ?? 0
- return num1 >= num2 ? q1 : q2
- }
-
- private func extractNumber(from string: String) -> Double? {
- let pattern = #"[\d.]+"#
- guard let regex = try? NSRegularExpression(pattern: pattern),
- let match = regex.firstMatch(in: string, range: NSRange(string.startIndex..., in: string)),
- let range = Range(match.range, in: string) else {
- return nil
- }
- return Double(string[range])
- }
-
- func addIngredient(_ ingredient: Ingredient) {
- if !detectedIngredients.contains(where: { $0.id == ingredient.id }) {
- detectedIngredients.append(ingredient)
- }
- }
-
- func removeIngredient(_ ingredient: Ingredient) {
- detectedIngredients.removeAll { $0.id == ingredient.id }
- }
-
- func updateIngredient(_ ingredient: Ingredient) {
- if let index = detectedIngredients.firstIndex(where: { $0.id == ingredient.id }) {
- detectedIngredients[index] = ingredient
- }
- }
-
- // MARK: - Manual Entry
-
- func addManualIngredient(name: String, quantity: String) {
- let ingredient = Ingredient(
- name: name,
- estimatedQuantity: quantity,
- confidence: 1.0
- )
- detectedIngredients.append(ingredient)
- }
-
- // MARK: - Cleanup
-
- func cleanup() async {
- print("📱 ScannerViewModel.cleanup() - Starting cleanup")
- stopScanning()
- await cameraManager.cleanup()
- print("📱 ScannerViewModel.cleanup() - ✅ Cleanup complete")
- }
-
- // MARK: - Local Persistence
-
- /// Saves ingredients locally using UserDefaults
- /// TODO: Migrate to FirestoreRepository when Firebase is configured
- /// To migrate: Replace this method with a call to FirestoreRepository.saveIngredients()
- func saveIngredientsLocally() {
- do {
- let data = try JSONEncoder().encode(detectedIngredients)
- UserDefaults.standard.set(data, forKey: "savedIngredients")
- print("💾 Saved \(detectedIngredients.count) ingredients locally")
- } catch {
- print("❌ Failed to save ingredients: \(error)")
- }
- }
-
- /// Loads ingredients from local storage
- /// TODO: Migrate to FirestoreRepository when Firebase is configured
- /// To migrate: Replace this method with a call to FirestoreRepository.loadIngredients()
- func loadIngredientsLocally() {
- guard let data = UserDefaults.standard.data(forKey: "savedIngredients") else {
- print("📂 No saved ingredients found")
- return
- }
-
- do {
- detectedIngredients = try JSONDecoder().decode([Ingredient].self, from: data)
- print("📂 Loaded \(detectedIngredients.count) ingredients from local storage")
- } catch {
- print("❌ Failed to load ingredients: \(error)")
- }
- }
-
- /// Clears all saved ingredients
- func clearSavedIngredients() {
- detectedIngredients.removeAll()
- UserDefaults.standard.removeObject(forKey: "savedIngredients")
- print("🗑️ Cleared all saved ingredients")
- }
-}
-
-// MARK: - Array Safe Subscript Extension
-
-extension Collection {
- /// Returns the element at the specified index if it exists, otherwise nil.
- subscript(safe index: Index) -> Element? {
- indices.contains(index) ? self[index] : nil
- }
-}
diff --git a/SousChefAI/Views/CookingModeView.swift b/SousChefAI/Views/CookingModeView.swift
deleted file mode 100644
index 70c4b47..0000000
--- a/SousChefAI/Views/CookingModeView.swift
+++ /dev/null
@@ -1,353 +0,0 @@
-//
-// CookingModeView.swift
-// SousChefAI
-//
-// Live cooking mode with AI-powered visual monitoring and guidance
-//
-
-import SwiftUI
-import AVFoundation
-
-struct CookingModeView: View {
- @Environment(\.dismiss) private var dismiss
- @StateObject private var viewModel: CookingModeViewModel
- @State private var showingAllSteps = false
- @State private var previewLayer: AVCaptureVideoPreviewLayer?
-
- init(recipe: Recipe) {
- _viewModel = StateObject(wrappedValue: CookingModeViewModel(recipe: recipe))
- }
-
- var body: some View {
- NavigationStack {
- ZStack {
- // Camera preview background
- if viewModel.isMonitoring, let previewLayer = previewLayer {
- CameraPreviewView(previewLayer: previewLayer)
- .ignoresSafeArea()
- .opacity(0.3)
- }
-
- // Main content
- VStack(spacing: 0) {
- // Progress bar
- progressBar
-
- ScrollView {
- VStack(spacing: 20) {
- // Current step card
- currentStepCard
-
- // AI feedback card
- if viewModel.isMonitoring {
- aiFeedbackCard
- }
-
- // Controls
- controlButtons
- }
- .padding()
- }
- }
- }
- .navigationTitle("Cooking Mode")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- ToolbarItem(placement: .cancellationAction) {
- Button("Exit") {
- viewModel.cleanup()
- dismiss()
- }
- }
-
- ToolbarItem(placement: .primaryAction) {
- Button {
- showingAllSteps = true
- } label: {
- Label("All Steps", systemImage: "list.bullet")
- }
- }
- }
- .task {
- await viewModel.setupCamera()
- previewLayer = viewModel.getPreviewLayer()
- viewModel.startCamera()
- }
- .onDisappear {
- viewModel.cleanup()
- }
- .sheet(isPresented: $showingAllSteps) {
- AllStepsSheet(
- steps: viewModel.recipe.steps,
- currentStep: viewModel.currentStepIndex
- )
- }
- }
- }
-
- // MARK: - UI Components
-
- private var progressBar: some View {
- VStack(spacing: 8) {
- HStack {
- Text("Step \(viewModel.currentStepIndex + 1) of \(viewModel.recipe.steps.count)")
- .font(.caption)
- .foregroundStyle(.secondary)
-
- Spacer()
-
- Text("\(Int(viewModel.progress * 100))%")
- .font(.caption)
- .fontWeight(.semibold)
- .foregroundStyle(.blue)
- }
-
- ProgressView(value: viewModel.progress)
- .tint(.blue)
- }
- .padding()
- .background(Color(.systemBackground))
- }
-
- private var currentStepCard: some View {
- VStack(alignment: .leading, spacing: 16) {
- HStack {
- Text("Current Step")
- .font(.caption)
- .foregroundStyle(.secondary)
- .textCase(.uppercase)
-
- Spacer()
-
- if viewModel.stepComplete {
- Label("Complete", systemImage: "checkmark.circle.fill")
- .font(.caption)
- .foregroundStyle(.green)
- }
- }
-
- Text(viewModel.currentStep)
- .font(.title3)
- .fontWeight(.semibold)
- .fixedSize(horizontal: false, vertical: true)
-
- // Speak button
- Button {
- viewModel.speakCurrentStep()
- } label: {
- Label("Read Aloud", systemImage: "speaker.wave.2.fill")
- .font(.subheadline)
- .foregroundStyle(.blue)
- }
- }
- .padding()
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(Color(.secondarySystemGroupedBackground))
- .clipShape(RoundedRectangle(cornerRadius: 16))
- }
-
- private var aiFeedbackCard: some View {
- VStack(alignment: .leading, spacing: 12) {
- HStack {
- Image(systemName: "sparkles")
- .foregroundStyle(.purple)
-
- Text("AI Assistant")
- .font(.caption)
- .foregroundStyle(.secondary)
- .textCase(.uppercase)
-
- Spacer()
-
- if viewModel.confidence > 0 {
- Text("\(Int(viewModel.confidence * 100))%")
- .font(.caption2)
- .fontWeight(.semibold)
- .foregroundStyle(.white)
- .padding(.horizontal, 8)
- .padding(.vertical, 4)
- .background(confidenceColor)
- .clipShape(Capsule())
- }
- }
-
- Text(viewModel.feedback)
- .font(.body)
- .fixedSize(horizontal: false, vertical: true)
-
- if viewModel.isMonitoring {
- HStack {
- ProgressView()
- .scaleEffect(0.8)
- Text("Monitoring...")
- .font(.caption)
- .foregroundStyle(.secondary)
- }
- }
- }
- .padding()
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(
- LinearGradient(
- colors: [Color.purple.opacity(0.1), Color.blue.opacity(0.1)],
- startPoint: .topLeading,
- endPoint: .bottomTrailing
- )
- )
- .clipShape(RoundedRectangle(cornerRadius: 16))
- }
-
- private var controlButtons: some View {
- VStack(spacing: 12) {
- // AI monitoring toggle
- if !viewModel.isComplete {
- if viewModel.isMonitoring {
- Button {
- viewModel.stopMonitoring()
- } label: {
- Label("Stop AI Monitoring", systemImage: "eye.slash.fill")
- .font(.headline)
- .foregroundStyle(.white)
- .frame(maxWidth: .infinity)
- .padding()
- .background(Color.red)
- .clipShape(RoundedRectangle(cornerRadius: 12))
- }
- } else {
- Button {
- viewModel.startMonitoring()
- } label: {
- Label("Start AI Monitoring", systemImage: "eye.fill")
- .font(.headline)
- .foregroundStyle(.white)
- .frame(maxWidth: .infinity)
- .padding()
- .background(Color.purple)
- .clipShape(RoundedRectangle(cornerRadius: 12))
- }
- }
- }
-
- // Navigation buttons
- HStack(spacing: 12) {
- Button {
- viewModel.previousStep()
- } label: {
- Label("Previous", systemImage: "arrow.left")
- .frame(maxWidth: .infinity)
- .padding()
- .background(Color(.secondarySystemGroupedBackground))
- .clipShape(RoundedRectangle(cornerRadius: 12))
- }
- .disabled(viewModel.currentStepIndex == 0)
-
- if viewModel.isComplete {
- Button {
- viewModel.cleanup()
- dismiss()
- } label: {
- Label("Finish", systemImage: "checkmark.circle.fill")
- .font(.headline)
- .foregroundStyle(.white)
- .frame(maxWidth: .infinity)
- .padding()
- .background(Color.green)
- .clipShape(RoundedRectangle(cornerRadius: 12))
- }
- } else {
- Button {
- viewModel.nextStep()
- } label: {
- Label("Next Step", systemImage: "arrow.right")
- .frame(maxWidth: .infinity)
- .padding()
- .background(viewModel.stepComplete ? Color.green : Color.blue)
- .foregroundStyle(.white)
- .clipShape(RoundedRectangle(cornerRadius: 12))
- }
- }
- }
- }
- }
-
- private var confidenceColor: Color {
- if viewModel.confidence >= 0.8 {
- return .green
- } else if viewModel.confidence >= 0.5 {
- return .orange
- } else {
- return .red
- }
- }
-}
-
-// MARK: - All Steps Sheet
-
-struct AllStepsSheet: View {
- @Environment(\.dismiss) private var dismiss
-
- let steps: [String]
- let currentStep: Int
-
- var body: some View {
- NavigationStack {
- List {
- ForEach(Array(steps.enumerated()), id: \.offset) { index, step in
- HStack(alignment: .top, spacing: 12) {
- // Step number
- Text("\(index + 1)")
- .font(.headline)
- .foregroundStyle(.white)
- .frame(width: 32, height: 32)
- .background(index == currentStep ? Color.blue : Color.gray)
- .clipShape(Circle())
-
- // Step text
- VStack(alignment: .leading, spacing: 4) {
- Text(step)
- .font(.body)
- .fixedSize(horizontal: false, vertical: true)
-
- if index == currentStep {
- Text("Current Step")
- .font(.caption)
- .foregroundStyle(.blue)
- } else if index < currentStep {
- Text("Completed")
- .font(.caption)
- .foregroundStyle(.green)
- }
- }
- }
- .padding(.vertical, 4)
- }
- }
- .navigationTitle("All Steps")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- ToolbarItem(placement: .confirmationAction) {
- Button("Done") {
- dismiss()
- }
- }
- }
- }
- }
-}
-
-#Preview {
- CookingModeView(recipe: Recipe(
- title: "Scrambled Eggs",
- description: "Simple and delicious scrambled eggs",
- steps: [
- "Crack 3 eggs into a bowl",
- "Add a splash of milk and whisk until combined",
- "Heat butter in a non-stick pan over medium heat",
- "Pour eggs into the pan",
- "Gently stir with a spatula until soft curds form",
- "Season with salt and pepper",
- "Serve immediately while hot"
- ],
- matchScore: 0.95
- ))
-}
diff --git a/SousChefAI/Views/InventoryView.swift b/SousChefAI/Views/InventoryView.swift
deleted file mode 100644
index 1743e53..0000000
--- a/SousChefAI/Views/InventoryView.swift
+++ /dev/null
@@ -1,314 +0,0 @@
-//
-// InventoryView.swift
-// SousChefAI
-//
-// View for reviewing and editing detected ingredients before recipe generation
-//
-
-import SwiftUI
-
-struct InventoryView: View {
- @StateObject private var repository = FirestoreRepository()
- @State private var ingredients: [Ingredient]
- @State private var dietaryRestrictions: Set = []
- @State private var nutritionGoals = ""
- @State private var showingRecipeGenerator = false
- @State private var showingPreferences = false
- @State private var editingIngredient: Ingredient?
-
- init(ingredients: [Ingredient]) {
- _ingredients = State(initialValue: ingredients)
- }
-
- var body: some View {
- List {
- // Preferences Section
- Section {
- Button {
- showingPreferences = true
- } label: {
- HStack {
- Image(systemName: "slider.horizontal.3")
- .foregroundStyle(.blue)
-
- VStack(alignment: .leading, spacing: 4) {
- Text("Dietary Preferences")
- .font(.headline)
-
- if dietaryRestrictions.isEmpty {
- Text("Not set")
- .font(.caption)
- .foregroundStyle(.secondary)
- } else {
- Text(dietaryRestrictions.sorted().joined(separator: ", "))
- .font(.caption)
- .foregroundStyle(.secondary)
- }
- }
-
- Spacer()
-
- Image(systemName: "chevron.right")
- .font(.caption)
- .foregroundStyle(.tertiary)
- }
- }
- }
-
- // Ingredients Section
- Section {
- ForEach(ingredients) { ingredient in
- IngredientRow(ingredient: ingredient) {
- editingIngredient = ingredient
- } onDelete: {
- deleteIngredient(ingredient)
- }
- }
- } header: {
- HStack {
- Text("Detected Ingredients")
- Spacer()
- Text("\(ingredients.count) items")
- .font(.caption)
- .foregroundStyle(.secondary)
- }
- } footer: {
- Text("Tap an ingredient to edit quantity or remove it. Items with yellow indicators have low confidence and should be verified.")
- .font(.caption)
- }
- }
- .navigationTitle("Your Inventory")
- .navigationBarTitleDisplayMode(.large)
- .toolbar {
- ToolbarItem(placement: .primaryAction) {
- Button {
- showingRecipeGenerator = true
- } label: {
- Label("Generate Recipes", systemImage: "sparkles")
- .fontWeight(.semibold)
- }
- .disabled(ingredients.isEmpty)
- }
- }
- .sheet(isPresented: $showingPreferences) {
- PreferencesSheet(
- dietaryRestrictions: $dietaryRestrictions,
- nutritionGoals: $nutritionGoals
- )
- }
- .sheet(item: $editingIngredient) { ingredient in
- EditIngredientSheet(ingredient: ingredient) { updated in
- updateIngredient(updated)
- }
- }
- .navigationDestination(isPresented: $showingRecipeGenerator) {
- RecipeGeneratorView(
- inventory: ingredients,
- userProfile: createUserProfile()
- )
- }
- .task {
- await loadUserPreferences()
- }
- }
-
- // MARK: - Actions
-
- private func deleteIngredient(_ ingredient: Ingredient) {
- withAnimation {
- ingredients.removeAll { $0.id == ingredient.id }
- }
- }
-
- private func updateIngredient(_ updated: Ingredient) {
- if let index = ingredients.firstIndex(where: { $0.id == updated.id }) {
- ingredients[index] = updated
- }
- }
-
- private func createUserProfile() -> UserProfile {
- UserProfile(
- dietaryRestrictions: Array(dietaryRestrictions),
- nutritionGoals: nutritionGoals,
- pantryStaples: []
- )
- }
-
- private func loadUserPreferences() async {
- if let profile = repository.currentUser {
- dietaryRestrictions = Set(profile.dietaryRestrictions)
- nutritionGoals = profile.nutritionGoals
- }
- }
-}
-
-// MARK: - Ingredient Row
-
-struct IngredientRow: View {
- let ingredient: Ingredient
- let onEdit: () -> Void
- let onDelete: () -> Void
-
- var body: some View {
- HStack(spacing: 12) {
- // Status indicator
- Circle()
- .fill(ingredient.needsVerification ? Color.orange : Color.green)
- .frame(width: 8, height: 8)
-
- VStack(alignment: .leading, spacing: 4) {
- Text(ingredient.name)
- .font(.body)
- .fontWeight(.medium)
-
- HStack(spacing: 8) {
- Text(ingredient.estimatedQuantity)
- .font(.caption)
- .foregroundStyle(.secondary)
-
- if ingredient.needsVerification {
- Text("• Low confidence")
- .font(.caption2)
- .foregroundStyle(.orange)
- }
- }
- }
-
- Spacer()
-
- // Confidence badge
- Text("\(Int(ingredient.confidence * 100))%")
- .font(.caption2)
- .fontWeight(.semibold)
- .foregroundStyle(.white)
- .padding(.horizontal, 8)
- .padding(.vertical, 4)
- .background(ingredient.needsVerification ? Color.orange : Color.green)
- .clipShape(Capsule())
- }
- .contentShape(Rectangle())
- .onTapGesture {
- onEdit()
- }
- .swipeActions(edge: .trailing, allowsFullSwipe: true) {
- Button(role: .destructive) {
- onDelete()
- } label: {
- Label("Delete", systemImage: "trash")
- }
- }
- }
-}
-
-// MARK: - Preferences Sheet
-
-struct PreferencesSheet: View {
- @Environment(\.dismiss) private var dismiss
- @Binding var dietaryRestrictions: Set
- @Binding var nutritionGoals: String
-
- var body: some View {
- NavigationStack {
- Form {
- Section("Dietary Restrictions") {
- ForEach(UserProfile.commonRestrictions, id: \.self) { restriction in
- Toggle(restriction, isOn: Binding(
- get: { dietaryRestrictions.contains(restriction) },
- set: { isOn in
- if isOn {
- dietaryRestrictions.insert(restriction)
- } else {
- dietaryRestrictions.remove(restriction)
- }
- }
- ))
- }
- }
-
- Section("Nutrition Goals") {
- TextField("E.g., High protein, Low carb", text: $nutritionGoals, axis: .vertical)
- .lineLimit(3...5)
- }
- }
- .navigationTitle("Preferences")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- ToolbarItem(placement: .confirmationAction) {
- Button("Done") {
- dismiss()
- }
- }
- }
- }
- }
-}
-
-// MARK: - Edit Ingredient Sheet
-
-struct EditIngredientSheet: View {
- @Environment(\.dismiss) private var dismiss
- @State private var name: String
- @State private var quantity: String
-
- let ingredient: Ingredient
- let onSave: (Ingredient) -> Void
-
- init(ingredient: Ingredient, onSave: @escaping (Ingredient) -> Void) {
- self.ingredient = ingredient
- self.onSave = onSave
- _name = State(initialValue: ingredient.name)
- _quantity = State(initialValue: ingredient.estimatedQuantity)
- }
-
- var body: some View {
- NavigationStack {
- Form {
- Section("Ingredient Details") {
- TextField("Name", text: $name)
- .textInputAutocapitalization(.words)
-
- TextField("Quantity", text: $quantity)
- }
-
- Section {
- HStack {
- Text("Detection Confidence")
- Spacer()
- Text("\(Int(ingredient.confidence * 100))%")
- .foregroundStyle(.secondary)
- }
- }
- }
- .navigationTitle("Edit Ingredient")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- ToolbarItem(placement: .cancellationAction) {
- Button("Cancel") {
- dismiss()
- }
- }
-
- ToolbarItem(placement: .confirmationAction) {
- Button("Save") {
- var updated = ingredient
- updated.name = name
- updated.estimatedQuantity = quantity
- onSave(updated)
- dismiss()
- }
- .disabled(name.isEmpty || quantity.isEmpty)
- }
- }
- }
- }
-}
-
-#Preview {
- NavigationStack {
- InventoryView(ingredients: [
- Ingredient(name: "Tomatoes", estimatedQuantity: "3 medium", confidence: 0.95),
- Ingredient(name: "Cheese", estimatedQuantity: "200g", confidence: 0.65),
- Ingredient(name: "Eggs", estimatedQuantity: "6 large", confidence: 0.88)
- ])
- }
-}
diff --git a/SousChefAI/Views/RecipeGeneratorView.swift b/SousChefAI/Views/RecipeGeneratorView.swift
deleted file mode 100644
index 7ca514e..0000000
--- a/SousChefAI/Views/RecipeGeneratorView.swift
+++ /dev/null
@@ -1,391 +0,0 @@
-//
-// RecipeGeneratorView.swift
-// SousChefAI
-//
-// View for generating and displaying recipe suggestions
-//
-
-import SwiftUI
-
-struct RecipeGeneratorView: View {
- @StateObject private var viewModel = RecipeGeneratorViewModel()
- @State private var selectedRecipe: Recipe?
- @State private var showingScaleSheet = false
-
- let inventory: [Ingredient]
- let userProfile: UserProfile
-
- var body: some View {
- Group {
- if viewModel.isGenerating {
- loadingView
- } else if viewModel.filteredRecipes.isEmpty && !viewModel.recipes.isEmpty {
- emptyFilterView
- } else if viewModel.filteredRecipes.isEmpty {
- emptyStateView
- } else {
- recipeListView
- }
- }
- .navigationTitle("Recipe Ideas")
- .navigationBarTitleDisplayMode(.large)
- .toolbar {
- ToolbarItem(placement: .primaryAction) {
- Menu {
- ForEach(RecipeFilter.allCases) { filter in
- Button {
- viewModel.setFilter(filter)
- } label: {
- Label(filter.rawValue, systemImage: filter.icon)
- }
- }
- } label: {
- Label("Filter", systemImage: viewModel.selectedFilter.icon)
- }
- }
- }
- .task {
- await viewModel.generateRecipes(inventory: inventory, profile: userProfile)
- }
- .sheet(item: $selectedRecipe) { recipe in
- RecipeDetailView(recipe: recipe) {
- Task {
- await viewModel.saveRecipe(recipe)
- }
- }
- }
- }
-
- // MARK: - Views
-
- private var loadingView: some View {
- VStack(spacing: 20) {
- ProgressView()
- .scaleEffect(1.5)
-
- Text("Generating recipes...")
- .font(.headline)
-
- Text("Analyzing your ingredients and preferences")
- .font(.subheadline)
- .foregroundStyle(.secondary)
- .multilineTextAlignment(.center)
- }
- .padding()
- }
-
- private var emptyFilterView: some View {
- ContentUnavailableView(
- "No recipes match this filter",
- systemImage: "line.3.horizontal.decrease.circle",
- description: Text("Try selecting a different filter to see more recipes")
- )
- }
-
- private var emptyStateView: some View {
- ContentUnavailableView(
- "No recipes generated",
- systemImage: "fork.knife.circle",
- description: Text("We couldn't generate recipes with your current ingredients. Try adding more items.")
- )
- }
-
- private var recipeListView: some View {
- ScrollView {
- LazyVStack(spacing: 16) {
- // Filter description
- filterDescriptionBanner
-
- // Recipe cards
- ForEach(viewModel.filteredRecipes) { recipe in
- RecipeCard(recipe: recipe)
- .onTapGesture {
- selectedRecipe = recipe
- }
- }
- }
- .padding()
- }
- }
-
- private var filterDescriptionBanner: some View {
- HStack {
- Image(systemName: viewModel.selectedFilter.icon)
- .foregroundStyle(.blue)
-
- VStack(alignment: .leading, spacing: 2) {
- Text(viewModel.selectedFilter.rawValue)
- .font(.subheadline)
- .fontWeight(.semibold)
-
- Text(viewModel.selectedFilter.description)
- .font(.caption)
- .foregroundStyle(.secondary)
- }
-
- Spacer()
-
- Text("\(viewModel.filteredRecipes.count)")
- .font(.title3)
- .fontWeight(.bold)
- .foregroundStyle(.blue)
- }
- .padding()
- .background(Color.blue.opacity(0.1))
- .clipShape(RoundedRectangle(cornerRadius: 12))
- }
-}
-
-// MARK: - Recipe Card
-
-struct RecipeCard: View {
- let recipe: Recipe
-
- var body: some View {
- VStack(alignment: .leading, spacing: 12) {
- // Header
- HStack(alignment: .top) {
- VStack(alignment: .leading, spacing: 4) {
- Text(recipe.title)
- .font(.headline)
- .lineLimit(2)
-
- if let time = recipe.estimatedTime {
- Label(time, systemImage: "clock")
- .font(.caption)
- .foregroundStyle(.secondary)
- }
- }
-
- Spacer()
-
- // Match score badge
- VStack(spacing: 4) {
- Text("\(Int(recipe.matchScore * 100))%")
- .font(.title3)
- .fontWeight(.bold)
- .foregroundStyle(matchScoreColor)
-
- Text("Match")
- .font(.caption2)
- .foregroundStyle(.secondary)
- }
- }
-
- // Description
- Text(recipe.description)
- .font(.subheadline)
- .foregroundStyle(.secondary)
- .lineLimit(3)
-
- Divider()
-
- // Footer
- HStack {
- // Category badge
- Label(recipe.category.rawValue, systemImage: categoryIcon)
- .font(.caption)
- .foregroundStyle(.white)
- .padding(.horizontal, 10)
- .padding(.vertical, 5)
- .background(categoryColor)
- .clipShape(Capsule())
-
- Spacer()
-
- // Missing ingredients indicator
- if !recipe.missingIngredients.isEmpty {
- Label("\(recipe.missingIngredients.count) missing", systemImage: "cart")
- .font(.caption)
- .foregroundStyle(.orange)
- }
-
- if let servings = recipe.servings {
- Label("\(servings) servings", systemImage: "person.2")
- .font(.caption)
- .foregroundStyle(.secondary)
- }
- }
- }
- .padding()
- .background(Color(.secondarySystemGroupedBackground))
- .clipShape(RoundedRectangle(cornerRadius: 16))
- .shadow(color: .black.opacity(0.05), radius: 5, x: 0, y: 2)
- }
-
- private var matchScoreColor: Color {
- if recipe.matchScore >= 0.8 {
- return .green
- } else if recipe.matchScore >= 0.6 {
- return .orange
- } else {
- return .red
- }
- }
-
- private var categoryColor: Color {
- switch recipe.category {
- case .scavenger:
- return .green
- case .upgrader:
- return .blue
- case .shopping:
- return .orange
- }
- }
-
- private var categoryIcon: String {
- switch recipe.category {
- case .scavenger:
- return "checkmark.circle.fill"
- case .upgrader:
- return "cart.badge.plus"
- case .shopping:
- return "cart.fill"
- }
- }
-}
-
-// MARK: - Recipe Detail View
-
-struct RecipeDetailView: View {
- @Environment(\.dismiss) private var dismiss
- @State private var currentStep = 0
- @State private var showingCookingMode = false
-
- let recipe: Recipe
- let onSave: () -> Void
-
- var body: some View {
- NavigationStack {
- ScrollView {
- VStack(alignment: .leading, spacing: 20) {
- // Header
- VStack(alignment: .leading, spacing: 8) {
- Text(recipe.title)
- .font(.title)
- .fontWeight(.bold)
-
- Text(recipe.description)
- .font(.body)
- .foregroundStyle(.secondary)
-
- HStack {
- if let time = recipe.estimatedTime {
- Label(time, systemImage: "clock")
- }
-
- if let servings = recipe.servings {
- Label("\(servings) servings", systemImage: "person.2")
- }
-
- Spacer()
-
- Text("\(Int(recipe.matchScore * 100))% match")
- .font(.subheadline)
- .fontWeight(.semibold)
- .foregroundStyle(.green)
- }
- .font(.caption)
- .foregroundStyle(.secondary)
- }
- .padding()
- .background(Color(.secondarySystemGroupedBackground))
- .clipShape(RoundedRectangle(cornerRadius: 12))
-
- // Missing ingredients
- if !recipe.missingIngredients.isEmpty {
- VStack(alignment: .leading, spacing: 12) {
- Text("Missing Ingredients")
- .font(.headline)
-
- ForEach(recipe.missingIngredients) { ingredient in
- HStack {
- Image(systemName: "cart")
- .foregroundStyle(.orange)
- Text(ingredient.name)
- Spacer()
- Text(ingredient.estimatedQuantity)
- .foregroundStyle(.secondary)
- }
- .font(.subheadline)
- }
- }
- .padding()
- .background(Color.orange.opacity(0.1))
- .clipShape(RoundedRectangle(cornerRadius: 12))
- }
-
- // Cooking steps
- VStack(alignment: .leading, spacing: 12) {
- Text("Instructions")
- .font(.headline)
-
- ForEach(Array(recipe.steps.enumerated()), id: \.offset) { index, step in
- HStack(alignment: .top, spacing: 12) {
- Text("\(index + 1)")
- .font(.headline)
- .foregroundStyle(.white)
- .frame(width: 32, height: 32)
- .background(Color.blue)
- .clipShape(Circle())
-
- Text(step)
- .font(.body)
- .fixedSize(horizontal: false, vertical: true)
- }
- }
- }
- .padding()
- .background(Color(.secondarySystemGroupedBackground))
- .clipShape(RoundedRectangle(cornerRadius: 12))
-
- // Start cooking button
- Button {
- showingCookingMode = true
- } label: {
- Label("Start Cooking", systemImage: "play.circle.fill")
- .font(.headline)
- .foregroundStyle(.white)
- .frame(maxWidth: .infinity)
- .padding()
- .background(Color.green)
- .clipShape(RoundedRectangle(cornerRadius: 12))
- }
- }
- .padding()
- }
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- ToolbarItem(placement: .cancellationAction) {
- Button("Close") {
- dismiss()
- }
- }
-
- ToolbarItem(placement: .primaryAction) {
- Button {
- onSave()
- } label: {
- Label("Save", systemImage: "heart")
- }
- }
- }
- .sheet(isPresented: $showingCookingMode) {
- CookingModeView(recipe: recipe)
- }
- }
- }
-}
-
-#Preview {
- NavigationStack {
- RecipeGeneratorView(
- inventory: [
- Ingredient(name: "Tomatoes", estimatedQuantity: "3 medium", confidence: 0.95),
- Ingredient(name: "Eggs", estimatedQuantity: "6 large", confidence: 0.88)
- ],
- userProfile: UserProfile()
- )
- }
-}
diff --git a/SousChefAI/Views/ScannerView.swift b/SousChefAI/Views/ScannerView.swift
deleted file mode 100644
index 1fd454b..0000000
--- a/SousChefAI/Views/ScannerView.swift
+++ /dev/null
@@ -1,462 +0,0 @@
-//
-// ScannerView.swift
-// SousChefAI
-//
-// AR camera view for scanning and detecting ingredients in real-time
-//
-
-import SwiftUI
-import ARKit
-import RealityKit
-import AVFoundation
-
-struct ScannerView: View {
- @StateObject private var viewModel = ScannerViewModel()
- @State private var showingInventory = false
- @State private var showingManualEntry = false
- @State private var detectedPlanes = 0
- @State private var lastRaycastResult = ""
- @State private var showARView = false
- @State private var previewLayer: AVCaptureVideoPreviewLayer?
-
- // Banner notification state
- @State private var showBanner = false
- @State private var bannerIngredient: Ingredient?
- @State private var bannerTask: Task?
-
- init() {
- print("🔵 ScannerView.init() - View initialized at \(Date())")
- }
-
- var body: some View {
- print("🔵 ScannerView.body - Body evaluated at \(Date()), showARView: \(showARView)")
- return NavigationStack {
- ZStack {
- // AR camera preview or regular camera
- if showARView {
- ARViewContainer(
- detectedPlanes: $detectedPlanes,
- lastRaycastResult: $lastRaycastResult
- )
- .ignoresSafeArea()
- } else {
- if let previewLayer = previewLayer {
- CameraPreviewView(previewLayer: previewLayer)
- .ignoresSafeArea()
- } else {
- Color.black
- .ignoresSafeArea()
- .overlay {
- ProgressView("Initializing camera...")
- .foregroundStyle(.white)
- }
- }
- }
-
- // Overlay UI
- VStack {
- // New ingredient banner (top of screen)
- if showBanner, let ingredient = bannerIngredient {
- NewIngredientBanner(ingredient: ingredient)
- .transition(.move(edge: .top).combined(with: .opacity))
- .padding(.horizontal)
- .padding(.top, 8)
- }
-
- // Top status bar
- statusBar
- .padding()
-
- // AR Debug info (only when AR is active)
- if showARView {
- arDebugInfo
- .padding(.horizontal)
- }
-
- Spacer()
-
- // Detected ingredients list
- if !viewModel.detectedIngredients.isEmpty {
- detectedIngredientsOverlay
- }
-
- // Bottom controls
- controlsBar
- .padding()
- }
- }
- .navigationTitle(showARView ? "AR Scanner" : "Ingredient Scanner")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- ToolbarItem(placement: .navigationBarTrailing) {
- Button {
- showingManualEntry = true
- } label: {
- Image(systemName: "plus.circle")
- }
- }
- }
- .task {
- print("🔵 ScannerView.task - Task started at \(Date())")
-
- // Load any previously saved ingredients
- viewModel.loadIngredientsLocally()
-
- // Setup new ingredient notification handler
- viewModel.onNewIngredientDetected = { [self] ingredient in
- showNewIngredientBanner(ingredient)
- }
-
- if !showARView {
- print("🔵 ScannerView.task - Calling setupCamera()")
- await viewModel.setupCamera()
- print("🔵 ScannerView.task - Getting preview layer after setup")
- previewLayer = viewModel.getPreviewLayer()
- print("🔵 ScannerView.task - Preview layer set: \(previewLayer != nil)")
- print("🔵 ScannerView.task - Calling startCamera()")
- viewModel.startCamera()
- }
- }
- .onDisappear {
- print("🔵 ScannerView.onDisappear - Cleaning up at \(Date())")
- bannerTask?.cancel()
- Task {
- await viewModel.cleanup()
- }
- }
- .onChange(of: viewModel.isScanning) { wasScanning, isScanning in
- // When scanning stops, save ingredients and optionally navigate
- if wasScanning && !isScanning && !viewModel.detectedIngredients.isEmpty {
- viewModel.saveIngredientsLocally()
- }
- }
- .alert("Camera Error", isPresented: .constant(viewModel.error != nil)) {
- Button("OK") {
- viewModel.error = nil
- }
- } message: {
- if let error = viewModel.error {
- Text(error.localizedDescription)
- }
- }
- .sheet(isPresented: $showingManualEntry) {
- ManualIngredientEntry { name, quantity in
- viewModel.addManualIngredient(name: name, quantity: quantity)
- }
- }
- .navigationDestination(isPresented: $showingInventory) {
- InventoryView(ingredients: viewModel.detectedIngredients)
- }
- }
- }
-
- // MARK: - Banner Management
-
- private func showNewIngredientBanner(_ ingredient: Ingredient) {
- // Cancel any existing banner dismissal
- bannerTask?.cancel()
-
- // Show new banner
- withAnimation(.spring(response: 0.3)) {
- bannerIngredient = ingredient
- showBanner = true
- }
-
- // Auto-dismiss after 1 second
- bannerTask = Task { @MainActor in
- try? await Task.sleep(for: .seconds(1))
- guard !Task.isCancelled else { return }
- withAnimation(.easeOut(duration: 0.3)) {
- showBanner = false
- }
- }
- }
-
- // MARK: - UI Components
-
- private var statusBar: some View {
- HStack {
- VStack(alignment: .leading, spacing: 4) {
- Text(showARView ? "AR Mode Active" : viewModel.scanProgress)
- .font(.headline)
- .foregroundStyle(.white)
-
- if viewModel.isScanning {
- ProgressView()
- .tint(.white)
- }
- }
-
- Spacer()
-
- Text("\(viewModel.detectedIngredients.count)")
- .font(.title2)
- .fontWeight(.bold)
- .foregroundStyle(.white)
- .padding(.horizontal, 12)
- .padding(.vertical, 6)
- .background(.ultraThinMaterial)
- .clipShape(Capsule())
- }
- .padding()
- .background(.ultraThinMaterial)
- .clipShape(RoundedRectangle(cornerRadius: 12))
- }
-
- private var arDebugInfo: some View {
- VStack(alignment: .leading, spacing: 4) {
- Text("Detected Planes: \(detectedPlanes)")
- .font(.caption)
- .foregroundStyle(.white)
-
- if !lastRaycastResult.isEmpty {
- Text(lastRaycastResult)
- .font(.caption2)
- .foregroundStyle(.white)
- }
- }
- .padding(8)
- .background(.ultraThinMaterial)
- .clipShape(RoundedRectangle(cornerRadius: 8))
- }
-
- private var detectedIngredientsOverlay: some View {
- ScrollView(.horizontal, showsIndicators: false) {
- HStack(spacing: 12) {
- ForEach(viewModel.detectedIngredients.prefix(5)) { ingredient in
- IngredientChip(ingredient: ingredient)
- }
-
- if viewModel.detectedIngredients.count > 5 {
- Text("+\(viewModel.detectedIngredients.count - 5) more")
- .font(.caption)
- .foregroundStyle(.white)
- .padding(.horizontal, 12)
- .padding(.vertical, 8)
- .background(.ultraThinMaterial)
- .clipShape(Capsule())
- }
- }
- .padding(.horizontal)
- }
- .padding(.bottom, 8)
- }
-
- private var controlsBar: some View {
- VStack(spacing: 16) {
- // AR Toggle button
- if !viewModel.isScanning {
- Button {
- withAnimation {
- showARView.toggle()
- if !showARView {
- Task {
- await viewModel.setupCamera()
- previewLayer = viewModel.getPreviewLayer()
- viewModel.startCamera()
- }
- } else {
- viewModel.stopCamera()
- }
- }
- } label: {
- Label(showARView ? "Exit AR Mode" : "Start AR Scan", systemImage: showARView ? "camera.fill" : "arkit")
- .font(.headline)
- .foregroundStyle(.white)
- .frame(maxWidth: .infinity)
- .padding()
- .background(showARView ? Color.orange : Color.blue)
- .clipShape(RoundedRectangle(cornerRadius: 16))
- }
- }
-
- // Main scanning action button (only in non-AR mode)
- if !showARView {
- if viewModel.isScanning {
- Button {
- viewModel.stopScanning()
- } label: {
- Label("Stop Scanning", systemImage: "stop.circle.fill")
- .font(.headline)
- .foregroundStyle(.white)
- .frame(maxWidth: .infinity)
- .padding()
- .background(Color.red)
- .clipShape(RoundedRectangle(cornerRadius: 16))
- }
- } else {
- Button {
- viewModel.startScanning()
- } label: {
- Label("Detect Ingredients", systemImage: "camera.viewfinder")
- .font(.headline)
- .foregroundStyle(.white)
- .frame(maxWidth: .infinity)
- .padding()
- .background(Color.green)
- .clipShape(RoundedRectangle(cornerRadius: 16))
- }
- }
- }
-
- // Secondary actions
- if !viewModel.detectedIngredients.isEmpty {
- Button {
- showingInventory = true
- } label: {
- Label("Continue to Inventory", systemImage: "arrow.right.circle.fill")
- .font(.headline)
- .foregroundStyle(.white)
- .frame(maxWidth: .infinity)
- .padding()
- .background(Color.purple)
- .clipShape(RoundedRectangle(cornerRadius: 16))
- }
- }
- }
- .padding()
- .background(.ultraThinMaterial)
- .clipShape(RoundedRectangle(cornerRadius: 20))
- }
-}
-
-// MARK: - Camera Preview
-
-struct CameraPreviewView: UIViewRepresentable {
- let previewLayer: AVCaptureVideoPreviewLayer
-
- func makeUIView(context: Context) -> UIView {
- print("🟢 CameraPreviewView.makeUIView() - Creating preview view at \(Date())")
- let view = UIView(frame: .zero)
- view.backgroundColor = .black
- previewLayer.frame = view.bounds
- view.layer.addSublayer(previewLayer)
- print("🟢 CameraPreviewView.makeUIView() - Preview layer added to view")
- return view
- }
-
- func updateUIView(_ uiView: UIView, context: Context) {
- DispatchQueue.main.async {
- previewLayer.frame = uiView.bounds
- }
- }
-}
-
-// MARK: - Ingredient Chip
-
-struct IngredientChip: View {
- let ingredient: Ingredient
-
- var body: some View {
- VStack(alignment: .leading, spacing: 4) {
- Text(ingredient.name)
- .font(.caption)
- .fontWeight(.semibold)
-
- Text(ingredient.estimatedQuantity)
- .font(.caption2)
- .foregroundStyle(.secondary)
- }
- .foregroundStyle(.white)
- .padding(.horizontal, 12)
- .padding(.vertical, 8)
- .background(ingredient.needsVerification ? Color.orange.opacity(0.9) : Color.green.opacity(0.9))
- .clipShape(RoundedRectangle(cornerRadius: 8))
- }
-}
-
-// MARK: - Manual Entry Sheet
-
-struct ManualIngredientEntry: View {
- @Environment(\.dismiss) private var dismiss
- @State private var name = ""
- @State private var quantity = ""
-
- let onAdd: (String, String) -> Void
-
- var body: some View {
- NavigationStack {
- Form {
- Section("Ingredient Details") {
- TextField("Name", text: $name)
- .textInputAutocapitalization(.words)
-
- TextField("Quantity (e.g., 2 cups, 500g)", text: $quantity)
- }
- }
- .navigationTitle("Add Ingredient")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- ToolbarItem(placement: .cancellationAction) {
- Button("Cancel") {
- dismiss()
- }
- }
-
- ToolbarItem(placement: .confirmationAction) {
- Button("Add") {
- onAdd(name, quantity)
- dismiss()
- }
- .disabled(name.isEmpty)
- }
- }
- }
- }
-}
-
-// MARK: - New Ingredient Banner
-
-struct NewIngredientBanner: View {
- let ingredient: Ingredient
-
- var body: some View {
- HStack(spacing: 12) {
- Image(systemName: "plus.circle.fill")
- .foregroundStyle(.white)
- .font(.title2)
-
- VStack(alignment: .leading, spacing: 2) {
- Text("New Item Detected")
- .font(.caption)
- .foregroundStyle(.white.opacity(0.8))
-
- Text(ingredient.name.capitalized)
- .font(.headline)
- .foregroundStyle(.white)
-
- if !ingredient.estimatedQuantity.isEmpty {
- Text("Qty: \(ingredient.estimatedQuantity)")
- .font(.caption)
- .foregroundStyle(.white.opacity(0.8))
- }
- }
-
- Spacer()
-
- // Confidence indicator
- Text("\(Int(ingredient.confidence * 100))%")
- .font(.caption)
- .fontWeight(.semibold)
- .foregroundStyle(.white)
- .padding(.horizontal, 8)
- .padding(.vertical, 4)
- .background(.white.opacity(0.2))
- .clipShape(Capsule())
- }
- .padding()
- .background(
- LinearGradient(
- colors: [Color.green, Color.green.opacity(0.8)],
- startPoint: .leading,
- endPoint: .trailing
- )
- )
- .clipShape(RoundedRectangle(cornerRadius: 12))
- .shadow(color: .black.opacity(0.2), radius: 8, y: 4)
- }
-}
-
-#Preview {
- ScannerView()
-}
diff --git a/SousChefAITests/SousChefAITests.swift b/SousChefAITests/SousChefAITests.swift
deleted file mode 100644
index fbc86d6..0000000
--- a/SousChefAITests/SousChefAITests.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-//
-// SousChefAITests.swift
-// SousChefAITests
-//
-// Created by Aditya Pulipaka on 2/11/26.
-//
-
-import Testing
-@testable import SousChefAI
-
-struct SousChefAITests {
-
- @Test func example() async throws {
- // Write your test here and use APIs like `#expect(...)` to check expected conditions.
- }
-
-}
diff --git a/SousChefAIUITests/SousChefAIUITests.swift b/SousChefAIUITests/SousChefAIUITests.swift
deleted file mode 100644
index 597cb83..0000000
--- a/SousChefAIUITests/SousChefAIUITests.swift
+++ /dev/null
@@ -1,41 +0,0 @@
-//
-// SousChefAIUITests.swift
-// SousChefAIUITests
-//
-// Created by Aditya Pulipaka on 2/11/26.
-//
-
-import XCTest
-
-final class SousChefAIUITests: XCTestCase {
-
- override func setUpWithError() throws {
- // Put setup code here. This method is called before the invocation of each test method in the class.
-
- // In UI tests it is usually best to stop immediately when a failure occurs.
- continueAfterFailure = false
-
- // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
- }
-
- override func tearDownWithError() throws {
- // Put teardown code here. This method is called after the invocation of each test method in the class.
- }
-
- @MainActor
- func testExample() throws {
- // UI tests must launch the application that they test.
- let app = XCUIApplication()
- app.launch()
-
- // Use XCTAssert and related functions to verify your tests produce the correct results.
- }
-
- @MainActor
- func testLaunchPerformance() throws {
- // This measures how long it takes to launch your application.
- measure(metrics: [XCTApplicationLaunchMetric()]) {
- XCUIApplication().launch()
- }
- }
-}
diff --git a/SousChefAIUITests/SousChefAIUITestsLaunchTests.swift b/SousChefAIUITests/SousChefAIUITestsLaunchTests.swift
deleted file mode 100644
index c5f4cee..0000000
--- a/SousChefAIUITests/SousChefAIUITestsLaunchTests.swift
+++ /dev/null
@@ -1,33 +0,0 @@
-//
-// SousChefAIUITestsLaunchTests.swift
-// SousChefAIUITests
-//
-// Created by Aditya Pulipaka on 2/11/26.
-//
-
-import XCTest
-
-final class SousChefAIUITestsLaunchTests: XCTestCase {
-
- override class var runsForEachTargetApplicationUIConfiguration: Bool {
- true
- }
-
- override func setUpWithError() throws {
- continueAfterFailure = false
- }
-
- @MainActor
- func testLaunch() throws {
- let app = XCUIApplication()
- app.launch()
-
- // Insert steps here to perform after app launch but before taking a screenshot,
- // such as logging into a test account or navigating somewhere in the app
-
- let attachment = XCTAttachment(screenshot: app.screenshot())
- attachment.name = "Launch Screen"
- attachment.lifetime = .keepAlways
- add(attachment)
- }
-}