import Foundation #if canImport(FoundationModels) import FoundationModels #endif /// On-device ingredient generation from a dish name using Apple's Foundation /// Models (iOS 26+, Apple Intelligence). Fully local — no network, no API key, /// private. On older OS versions or unsupported devices it simply reports /// `isAvailable == false` and callers hide the "Generate" affordance. enum IngredientGenerator { /// Whether on-device generation can run right now on this device. static var isAvailable: Bool { #if canImport(FoundationModels) if #available(iOS 26.0, *) { if case .available = SystemLanguageModel.default.availability { return true } } #endif return false } /// Generates ingredient lines for a dish, localized to `language`. /// Returns an empty array when unavailable or on failure (never throws). static func generate(dishName: String, language: AppLanguage) async -> [String] { let trimmed = dishName.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return [] } #if canImport(FoundationModels) if #available(iOS 26.0, *), case .available = SystemLanguageModel.default.availability { let langName = language.resolved().displayName let instructions = """ You are a helpful cooking assistant. Given a dish name, list the \ typical grocery ingredients needed to cook it for a family of four. \ Output one ingredient per line with a rough quantity (e.g. \ "500 g minced beef"). No numbering, no bullet symbols, no headings, \ no extra commentary. Write the ingredients in \(langName). """ do { let session = LanguageModelSession(instructions: instructions) let response = try await session.respond(to: "Dish: \(trimmed)") return parseLines(response.content) } catch { CrashlyticsService.record(error, context: "ingredient_generation") return [] } } #endif return [] } /// Normalizes raw model output into clean ingredient lines, stripping any /// stray bullets or numbering the model may still emit. static func parseLines(_ text: String) -> [String] { text .split(whereSeparator: \.isNewline) .map { line -> String in var s = line.trimmingCharacters(in: .whitespaces) for bullet in ["- ", "• ", "* ", "– ", "— "] where s.hasPrefix(bullet) { s.removeFirst(bullet.count) } if let range = s.range(of: #"^\d+[\.\)]\s+"#, options: .regularExpression) { s.removeSubrange(range) } return s.trimmingCharacters(in: .whitespaces) } .filter { !$0.isEmpty } } }