// // FoundationModelsDigestService.swift // Reflect // // Generates weekly emotional digests using Foundation Models. // import Foundation import FoundationModels @available(iOS 26, *) @MainActor class FoundationModelsDigestService { // MARK: - Singleton static let shared = FoundationModelsDigestService() private let summarizer = MoodDataSummarizer() private init() {} // MARK: - Storage Keys private static let digestStorageKey = "latestWeeklyDigest" // MARK: - Digest Generation /// Generate a weekly digest from the past 7 days of mood data func generateWeeklyDigest() async throws -> WeeklyDigest { let calendar = Calendar.current let now = Date() let weekStart = calendar.date(byAdding: .day, value: -7, to: now)! let entries = DataController.shared.getData( startDate: weekStart, endDate: now, includedDays: [1, 2, 3, 4, 5, 6, 7] ) let validEntries = entries.filter { ![.missing, .placeholder].contains($0.mood) } guard validEntries.count >= 3 else { throw InsightGenerationError.insufficientData } let session = LanguageModelSession(instructions: systemInstructions) let prompt = buildPrompt(entries: validEntries, weekStart: weekStart, weekEnd: now) let response = try await session.respond(to: prompt, generating: AIWeeklyDigestResponse.self) let digest = WeeklyDigest( headline: response.content.headline, summary: response.content.summary, highlight: response.content.highlight, intention: response.content.intention, iconName: response.content.iconName, generatedAt: Date(), weekStartDate: weekStart, weekEndDate: now ) // Store the digest saveDigest(digest) return digest } /// Load the latest stored digest func loadLatestDigest() -> WeeklyDigest? { guard let data = GroupUserDefaults.groupDefaults.data(forKey: Self.digestStorageKey), let digest = try? JSONDecoder().decode(WeeklyDigest.self, from: data) else { return nil } return digest } // MARK: - Storage private func saveDigest(_ digest: WeeklyDigest) { if let data = try? JSONEncoder().encode(digest) { GroupUserDefaults.groupDefaults.set(data, forKey: Self.digestStorageKey) } } // MARK: - System Instructions private var systemInstructions: String { let personalityPack = UserDefaultsStore.personalityPackable() switch personalityPack { case .Default: return """ You are a warm, supportive mood companion writing a weekly emotional digest. \ Summarize the week's mood journey with encouragement and specificity. \ Be personal, brief, and uplifting. Reference specific patterns from the data. \ SF Symbols: sun.max.fill, heart.fill, star.fill, leaf.fill, sparkles """ case .MotivationalCoach: return """ You are a HIGH ENERGY motivational coach delivering a weekly performance review! \ Celebrate wins, frame challenges as growth opportunities, and fire them up for next week! \ Use exclamations and power language! \ SF Symbols: trophy.fill, flame.fill, bolt.fill, figure.run, star.fill """ case .ZenMaster: return """ You are a calm Zen master offering a weekly reflection on the emotional journey. \ Use nature metaphors, gentle wisdom, and serene observations. Find meaning in all moods. \ SF Symbols: leaf.fill, moon.fill, drop.fill, sunrise.fill, wind """ case .BestFriend: return """ You are their best friend doing a weekly check-in on how they've been. \ Be warm, casual, validating, and conversational. Celebrate with them, commiserate together. \ SF Symbols: heart.fill, hand.thumbsup.fill, sparkles, face.smiling.fill, balloon.fill """ case .DataAnalyst: return """ You are a clinical data analyst delivering a weekly mood metrics report. \ Reference exact numbers, percentages, and observed trends. Be objective but constructive. \ SF Symbols: chart.bar.fill, chart.line.uptrend.xyaxis, number, percent, doc.text.magnifyingglass """ } } // MARK: - Prompt Construction private func buildPrompt(entries: [MoodEntryModel], weekStart: Date, weekEnd: Date) -> String { let formatter = DateFormatter() formatter.dateStyle = .medium let moodList = entries.sorted { $0.forDate < $1.forDate }.map { entry in let day = entry.forDate.formatted(.dateTime.weekday(.abbreviated)) let mood = entry.mood.widgetDisplayName let hasNotes = entry.notes != nil && !entry.notes!.isEmpty let noteSnippet = hasNotes ? " (\(String(entry.notes!.prefix(50))))" : "" return "\(day): \(mood)\(noteSnippet)" }.joined(separator: "\n") let summary = summarizer.summarize(entries: entries, periodName: "this week") let avgMood = String(format: "%.1f", summary.averageMoodScore) return """ Generate a weekly emotional digest for \(formatter.string(from: weekStart)) - \(formatter.string(from: weekEnd)): \(moodList) Average mood: \(avgMood)/5, Trend: \(summary.recentTrend), Stability: \(String(format: "%.0f", summary.moodStabilityScore * 100))% Current streak: \(summary.currentLoggingStreak) days Write a warm, personalized weekly digest. """ } }