// // InsightsViewModel.swift // Feels // // Created by Claude Code on 12/9/24. // import Foundation import SwiftUI /// Represents a single insight to display in the UI struct Insight: Identifiable { let id = UUID() let icon: String let title: String let description: String let mood: Mood? } /// Loading state for each insight section enum InsightLoadingState: Equatable { case idle case loading case loaded case error(String) } /// ViewModel for the Insights tab - uses Apple Foundation Models for AI-powered insights @MainActor class InsightsViewModel: ObservableObject { // MARK: - Published Properties @Published var monthInsights: [Insight] = [] @Published var yearInsights: [Insight] = [] @Published var allTimeInsights: [Insight] = [] @Published var monthLoadingState: InsightLoadingState = .idle @Published var yearLoadingState: InsightLoadingState = .idle @Published var allTimeLoadingState: InsightLoadingState = .idle @Published var isAIAvailable: Bool = false // MARK: - Dependencies private let insightService = FoundationModelsInsightService() private let healthService = HealthService.shared private let calendar = Calendar.current // MARK: - Initialization init() { isAIAvailable = insightService.isAvailable DataController.shared.addNewDataListener { [weak self] in self?.onDataChanged() } } /// Called when mood data changes in another tab. Invalidates cached insights /// so they are regenerated with fresh data on next view appearance. private func onDataChanged() { insightService.invalidateCache() monthLoadingState = .idle yearLoadingState = .idle allTimeLoadingState = .idle } // MARK: - Public Methods /// Generate insights for all time periods func generateInsights() { Task { await generateAllInsights() } } /// Force refresh all insights (invalidates cache) func refreshInsights() { insightService.invalidateCache() generateInsights() } // MARK: - Private Methods private func generateAllInsights() async { let now = Date() // Get date ranges let monthStart = calendar.date(from: calendar.dateComponents([.year, .month], from: now))! let yearStart = calendar.date(from: calendar.dateComponents([.year], from: now))! let allTimeStart = Date(timeIntervalSince1970: 0) // Fetch entries for each period let monthEntries = DataController.shared.getData(startDate: monthStart, endDate: now, includedDays: [1, 2, 3, 4, 5, 6, 7]) let yearEntries = DataController.shared.getData(startDate: yearStart, endDate: now, includedDays: [1, 2, 3, 4, 5, 6, 7]) let allTimeEntries = DataController.shared.getData(startDate: allTimeStart, endDate: now, includedDays: [1, 2, 3, 4, 5, 6, 7]) // Generate insights concurrently for all three periods await withTaskGroup(of: Void.self) { group in group.addTask { @MainActor in await self.generatePeriodInsights( entries: monthEntries, periodName: "this month", updateState: { self.monthLoadingState = $0 }, updateInsights: { self.monthInsights = $0 } ) } group.addTask { @MainActor in await self.generatePeriodInsights( entries: yearEntries, periodName: "this year", updateState: { self.yearLoadingState = $0 }, updateInsights: { self.yearInsights = $0 } ) } group.addTask { @MainActor in await self.generatePeriodInsights( entries: allTimeEntries, periodName: "all time", updateState: { self.allTimeLoadingState = $0 }, updateInsights: { self.allTimeInsights = $0 } ) } } } private func generatePeriodInsights( entries: [MoodEntryModel], periodName: String, updateState: @escaping (InsightLoadingState) -> Void, updateInsights: @escaping ([Insight]) -> Void ) async { // Filter valid entries let validEntries = entries.filter { ![.missing, .placeholder].contains($0.mood) } // Handle empty data case guard !validEntries.isEmpty else { updateInsights([Insight( icon: "questionmark.circle", title: "No Data Yet", description: "Start logging your moods to see insights for \(periodName).", mood: nil )]) updateState(.loaded) return } // Check if AI is available guard isAIAvailable else { updateInsights([Insight( icon: "brain.head.profile", title: "AI Unavailable", description: "Apple Intelligence is required for personalized insights. Please enable it in Settings.", mood: nil )]) updateState(.error("AI not available")) return } updateState(.loading) // Fetch health data if enabled - pass raw averages to AI for correlation analysis var healthAverages: HealthService.HealthAverages? if healthService.isEnabled && healthService.isAuthorized { let healthData = await healthService.fetchHealthData(for: validEntries) healthAverages = healthService.computeHealthAverages(entries: validEntries, healthData: healthData) } do { let insights = try await insightService.generateInsights( for: validEntries, periodName: periodName, count: 5, healthAverages: healthAverages ) updateInsights(insights) updateState(.loaded) } catch { // On error, provide a helpful message updateInsights([Insight( icon: "exclamationmark.triangle", title: "Insights Unavailable", description: "Unable to generate AI insights right now. Please try again later.", mood: nil )]) updateState(.error(error.localizedDescription)) } } }