From 31d0769d3383dd4fd7c01e0cbbe710e02c774d8a Mon Sep 17 00:00:00 2001 From: pulipakaa24 Date: Tue, 28 Apr 2026 19:05:12 -0500 Subject: [PATCH] blank --- PRIVACY_SETUP.md | 88 --- PROJECT_SUMMARY.md | 256 ------- PrivacyInfo.xcprivacy | 14 - QUICKSTART.md | 139 ---- README.md | 256 ------- SETUP_GUIDE.md | 203 ------ SWIFT6_WARNINGS.md | 136 ---- SousChefAI.xcodeproj/project.pbxproj | 624 ------------------ .../contents.xcworkspacedata | 7 - .../xcschemes/xcschememanagement.plist | 14 - .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/Contents.json | 35 - SousChefAI/Assets.xcassets/Contents.json | 6 - SousChefAI/Config/AppConfig.swift | 50 -- SousChefAI/ContentView.swift | 242 ------- SousChefAI/Models/Ingredient.swift | 47 -- SousChefAI/Models/Recipe.swift | 70 -- SousChefAI/Models/UserProfile.swift | 37 -- SousChefAI/Services/ARVisionService.swift | 222 ------- SousChefAI/Services/CameraManager.swift | 290 -------- SousChefAI/Services/FirestoreRepository.swift | 248 ------- SousChefAI/Services/GeminiRecipeService.swift | 327 --------- SousChefAI/Services/GeminiVisionService.swift | 503 -------------- SousChefAI/Services/RecipeService.swift | 56 -- SousChefAI/Services/VisionService.swift | 60 -- SousChefAI/SousChefAIApp.swift | 39 -- .../ViewModels/CookingModeViewModel.swift | 191 ------ .../ViewModels/RecipeGeneratorViewModel.swift | 135 ---- SousChefAI/ViewModels/ScannerViewModel.swift | 364 ---------- SousChefAI/Views/CookingModeView.swift | 353 ---------- SousChefAI/Views/InventoryView.swift | 314 --------- SousChefAI/Views/RecipeGeneratorView.swift | 391 ----------- SousChefAI/Views/ScannerView.swift | 462 ------------- SousChefAITests/SousChefAITests.swift | 17 - SousChefAIUITests/SousChefAIUITests.swift | 41 -- .../SousChefAIUITestsLaunchTests.swift | 33 - 36 files changed, 6281 deletions(-) delete mode 100644 PRIVACY_SETUP.md delete mode 100644 PROJECT_SUMMARY.md delete mode 100644 PrivacyInfo.xcprivacy delete mode 100644 QUICKSTART.md delete mode 100644 README.md delete mode 100644 SETUP_GUIDE.md delete mode 100644 SWIFT6_WARNINGS.md delete mode 100644 SousChefAI.xcodeproj/project.pbxproj delete mode 100644 SousChefAI.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 SousChefAI.xcodeproj/xcuserdata/adipu.xcuserdatad/xcschemes/xcschememanagement.plist delete mode 100644 SousChefAI/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 SousChefAI/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 SousChefAI/Assets.xcassets/Contents.json delete mode 100644 SousChefAI/Config/AppConfig.swift delete mode 100644 SousChefAI/ContentView.swift delete mode 100644 SousChefAI/Models/Ingredient.swift delete mode 100644 SousChefAI/Models/Recipe.swift delete mode 100644 SousChefAI/Models/UserProfile.swift delete mode 100644 SousChefAI/Services/ARVisionService.swift delete mode 100644 SousChefAI/Services/CameraManager.swift delete mode 100644 SousChefAI/Services/FirestoreRepository.swift delete mode 100644 SousChefAI/Services/GeminiRecipeService.swift delete mode 100644 SousChefAI/Services/GeminiVisionService.swift delete mode 100644 SousChefAI/Services/RecipeService.swift delete mode 100644 SousChefAI/Services/VisionService.swift delete mode 100644 SousChefAI/SousChefAIApp.swift delete mode 100644 SousChefAI/ViewModels/CookingModeViewModel.swift delete mode 100644 SousChefAI/ViewModels/RecipeGeneratorViewModel.swift delete mode 100644 SousChefAI/ViewModels/ScannerViewModel.swift delete mode 100644 SousChefAI/Views/CookingModeView.swift delete mode 100644 SousChefAI/Views/InventoryView.swift delete mode 100644 SousChefAI/Views/RecipeGeneratorView.swift delete mode 100644 SousChefAI/Views/ScannerView.swift delete mode 100644 SousChefAITests/SousChefAITests.swift delete mode 100644 SousChefAIUITests/SousChefAIUITests.swift delete mode 100644 SousChefAIUITests/SousChefAIUITestsLaunchTests.swift 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) - } -}