From 044c4389a84f9b4b6d95e304bc27050c14cda880 Mon Sep 17 00:00:00 2001 From: "qwen.ai[bot]" Date: Wed, 15 Jul 2026 22:07:54 +0000 Subject: [PATCH] update branch --- .gitignore | 42 ++++++++------ Lumen/Features/Home/HomeDashboardView.swift | 7 ++- .../Intelligence/SuggestionEngine.swift | 13 +++-- Lumen/Services/NotificationService.swift | 29 ++++++++++ Lumen/Views/AppState.swift | 56 +++++++++++++++++++ Lumen/Views/LumenApp.swift | 1 + 6 files changed, 125 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index ffc1ded..bae016b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,38 @@ -```swift +``` +# Build artifacts +*.o +*.obj +*.a +*.lib +*.dylib +*.so +*.dll +*.exe +*.out + # Xcode *.xcodeproj/ *.xcworkspace/ -*.xcuserstate -project.xcworkspace/xcuserdata/ -xcuserdata/ - -# Build +*.xcarchive/ +*.app/ build/ DerivedData/ -# Swift Package Manager -.swiftpm/ -Package.resolved +# Swift +*.swiftmodule +*.swiftdoc +*.sib + +# Dependencies +Packages/ +.build/ +.swift-version -# Temporary files -*.tmp +# Logs *.log -*.swp # Environment .env .env.local -*.env.* - -# Dependencies -Pods/ -Carthage/ +.env.* ``` \ No newline at end of file diff --git a/Lumen/Features/Home/HomeDashboardView.swift b/Lumen/Features/Home/HomeDashboardView.swift index cd59b4f..8c144b1 100644 --- a/Lumen/Features/Home/HomeDashboardView.swift +++ b/Lumen/Features/Home/HomeDashboardView.swift @@ -80,6 +80,7 @@ struct HomeDashboardView: View { .sheet(item: $lumenSheet) { sheet in lumenSheetContent(sheet) } + .animation(appState.sensoryProfile.shouldReduceMotion ? .default : .spring(response: 0.42, dampingFraction: 0.86), value: isStatusOverlayVisible) .onAppear { viewModel.load() locationService.requestLocationPermission() @@ -458,6 +459,9 @@ struct HomeDashboardView: View { private func handleLumenSuggestion() { lumenSheet = nil + + // Record that a suggestion was shown today (for sensory profile limits) + appState.recordSuggestionShown() // Execute the same scene the Action sheet displayed — single source of truth. if let sceneName = suggestedSceneName, let scene = findScene(named: sceneName) { @@ -516,7 +520,8 @@ struct HomeDashboardView: View { hourOfDay: Calendar.current.component(.hour, from: Date()), candidates: suggestionCandidates, dailySuggestionLimit: appState.sensoryProfile.dailySuggestionLimit, - pausedSuggestions: appState.suggestionsPaused + pausedSuggestions: appState.suggestionsPaused, + hasShownSuggestionToday: appState.hasReachedDailySuggestionLimit ).topSuggestion() } diff --git a/Lumen/Services/Intelligence/SuggestionEngine.swift b/Lumen/Services/Intelligence/SuggestionEngine.swift index aa80c66..7f44186 100644 --- a/Lumen/Services/Intelligence/SuggestionEngine.swift +++ b/Lumen/Services/Intelligence/SuggestionEngine.swift @@ -90,6 +90,9 @@ struct SuggestionEngine: Equatable { /// Minimum confidence before Lumen will surface a suggestion at all. Below /// this, the calm default is to stay quiet rather than nudge on a hunch. static let surfaceThreshold: Double = 0.2 + + /// Tracks if a suggestion has already been shown today (for limit enforcement) + let hasShownSuggestionToday: Bool // MARK: Public API @@ -112,14 +115,14 @@ struct SuggestionEngine: Equatable { func topSuggestion() -> SceneSuggestion? { guard !pausedSuggestions else { return nil } - let best = rankedSuggestions().first - guard let best = best, best.confidence >= Self.surfaceThreshold else { return nil } - - // Enforce daily suggestion limit from sensory profile - if let limit = dailySuggestionLimit, limit <= 0 { + // Enforce daily suggestion limit from sensory profile BEFORE scoring + if hasShownSuggestionToday { return nil } + let best = rankedSuggestions().first + guard let best = best, best.confidence >= Self.surfaceThreshold else { return nil } + return best } diff --git a/Lumen/Services/NotificationService.swift b/Lumen/Services/NotificationService.swift index 5cbfe2b..f23540e 100644 --- a/Lumen/Services/NotificationService.swift +++ b/Lumen/Services/NotificationService.swift @@ -80,6 +80,35 @@ final class NotificationService { } } + /// Notify user before an automated transition happens (for sensory profile transition warnings) + func notifyUpcomingTransition(sceneName: String, minutesUntil: Int) { + let content = UNMutableNotificationContent() + content.title = "Lumen — Upcoming Change" + content.subtitle = "In \(minutesUntil) minutes" + content.body = "'\(sceneName)' will activate soon. You can adjust or postpone in the app." + content.sound = .default + + // Add custom data for deep linking + content.userInfo = [ + "sceneName": sceneName, + "eventType": "upcoming_transition", + "minutesUntil": minutesUntil, + "timestamp": Date().timeIntervalSince1970 + ] + + // Schedule notification to fire at the warning time + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(minutesUntil * 60), repeats: false) + let request = UNNotificationRequest(identifier: "transition_\(UUID().uuidString)", content: content, trigger: trigger) + + UNUserNotificationCenter.current().add(request) { error in + if let error = error { + print("Failed to schedule transition warning: \(error)") + } else { + print("Transition warning scheduled: \(sceneName) in \(minutesUntil) min") + } + } + } + // MARK: - Clear Notifications func clearBadge() { diff --git a/Lumen/Views/AppState.swift b/Lumen/Views/AppState.swift index e69adc9..c24ae42 100644 --- a/Lumen/Views/AppState.swift +++ b/Lumen/Views/AppState.swift @@ -9,6 +9,8 @@ import Observation @Observable final class AppState { @ObservationIgnored private static let sensoryProfileDefaultsKey = "lumen.sensoryProfile.v1" + @ObservationIgnored private static let suggestionCountKey = "lumen.suggestionCount.v1" + @ObservationIgnored private static let lastSuggestionDateKey = "lumen.lastSuggestionDate.v1" @ObservationIgnored private let userDefaults: UserDefaults var selectedTab: Tab = .home @@ -20,10 +22,64 @@ final class AppState { didSet { saveSensoryProfile() } } var suggestionsPaused: Bool = false + + /// Tracks how many suggestions have been shown today (respects sensory profile limits) + var todaysSuggestionCount: Int { + didSet { saveSuggestionTracking() } + } + + /// Last date a suggestion was shown (for daily reset) + var lastSuggestionDate: Date? { + didSet { saveSuggestionTracking() } + } init(userDefaults: UserDefaults = .standard) { self.userDefaults = userDefaults self.sensoryProfile = Self.loadSensoryProfile(from: userDefaults) + self.todaysSuggestionCount = Self.loadTodaysSuggestionCount(from: userDefaults) + self.lastSuggestionDate = Self.loadLastSuggestionDate(from: userDefaults) + Self.resetSuggestionCountIfNeeded(userDefaults: userDefaults) + } + + /// Check if daily limit has been reached based on sensory profile + var hasReachedDailySuggestionLimit: Bool { + guard let limit = sensoryProfile.dailySuggestionLimit else { return false } + return todaysSuggestionCount >= limit + } + + /// Increment suggestion count and reset if new day + func recordSuggestionShown() { + Self.resetSuggestionCountIfNeeded(userDefaults: userDefaults) + todaysSuggestionCount += 1 + lastSuggestionDate = Date() + } + + /// Reset suggestion count for new day + private static func resetSuggestionCountIfNeeded(userDefaults: UserDefaults) { + guard let lastDate = loadLastSuggestionDate(from: userDefaults) else { return } + + let calendar = Calendar.current + if !calendar.isDateInToday(lastDate) { + userDefaults.set(0, forKey: suggestionCountKey) + userDefaults.set(Date(), forKey: lastSuggestionDateKey) + } + } + + private static func loadTodaysSuggestionCount(from userDefaults: UserDefaults) -> Int { + return userDefaults.integer(forKey: suggestionCountKey) + } + + private static func loadLastSuggestionDate(from userDefaults: UserDefaults) -> Date? { + guard let data = userDefaults.data(forKey: lastSuggestionDateKey) else { return nil } + return try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? Date + } + + private func saveSuggestionTracking() { + userDefaults.set(todaysSuggestionCount, forKey: Self.suggestionCountKey) + if let date = lastSuggestionDate { + let data = try? NSKeyedArchiver.archivedData(withRootObject: date, requiringSecureCoding: false) + userDefaults.set(data, forKey: Self.lastSuggestionDateKey) + } } enum Tab: String, CaseIterable, Hashable { diff --git a/Lumen/Views/LumenApp.swift b/Lumen/Views/LumenApp.swift index 2a3798d..c8b709e 100644 --- a/Lumen/Views/LumenApp.swift +++ b/Lumen/Views/LumenApp.swift @@ -48,6 +48,7 @@ struct LumenApp: App { .environment(locationService) .environment(remoteService) .environment(localDeviceService) + .environment(\.accessibilityReduceMotion, appState.sensoryProfile.shouldReduceMotion) } .modelContainer(container) }