Add AI-powered mental wellness features: Reflection Companion, Pattern Tags, Weekly Digest

Three new Foundation Models features to deepen user engagement with mental wellness:

1. AI Reflection Companion — personalized feedback after completing guided reflections,
   referencing the user's actual words with personality-pack-adapted tone
2. Mood Pattern Tags — auto-extracts theme tags (work, family, stress, etc.) from notes
   and reflections, displayed as colored pills on entries
3. Weekly Emotional Digest — BGTask-scheduled Sunday digest with headline, summary,
   highlight, and intention; shown as card in Insights tab with notification

All features: on-device (zero cost), premium-gated, iOS 26+ with graceful degradation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-04 00:47:28 -05:00
parent 43ff239781
commit ab8d8fbdc0
18 changed files with 1076 additions and 3 deletions

View File

@@ -0,0 +1,28 @@
//
// AIEntryTags.swift
// Reflect
//
// @Generable model for AI-extracted theme tags from mood entry notes and reflections.
//
import Foundation
import FoundationModels
/// A single AI-extracted theme tag
@available(iOS 26, *)
@Generable
struct AITag: Equatable {
@Guide(description: "Theme label — one of: work, family, social, health, sleep, exercise, stress, gratitude, growth, creative, nature, self-care, finances, relationships, loneliness, motivation")
var label: String
@Guide(description: "Confidence level: high or medium")
var confidence: String
}
/// Container for extracted tags from a single entry
@available(iOS 26, *)
@Generable
struct AIEntryTags: Equatable {
@Guide(description: "Array of 1-4 theme tags extracted from the text", .maximumCount(4))
var tags: [AITag]
}

View File

@@ -0,0 +1,26 @@
//
// AIReflectionFeedback.swift
// Reflect
//
// @Generable model for AI-powered reflection feedback after guided reflection completion.
//
import Foundation
import FoundationModels
/// AI-generated personalized feedback after completing a guided reflection
@available(iOS 26, *)
@Generable
struct AIReflectionFeedback: Equatable {
@Guide(description: "A warm, specific affirmation of what the user did well in their reflection (1 sentence)")
var affirmation: String
@Guide(description: "An observation connecting something the user wrote to a meaningful pattern or insight (1 sentence)")
var observation: String
@Guide(description: "A brief, actionable takeaway the user can carry forward (1 sentence)")
var takeaway: String
@Guide(description: "SF Symbol name for the feedback icon (e.g., sparkles, heart.fill, leaf.fill, star.fill)")
var iconName: String
}

View File

@@ -0,0 +1,64 @@
//
// AIWeeklyDigest.swift
// Reflect
//
// @Generable model and storage for AI-generated weekly emotional digest.
//
import Foundation
import FoundationModels
/// AI-generated weekly mood digest
@available(iOS 26, *)
@Generable
struct AIWeeklyDigestResponse: Equatable {
@Guide(description: "An engaging headline summarizing the week's emotional arc (3-7 words)")
var headline: String
@Guide(description: "A warm 2-3 sentence summary of the week's mood patterns and notable moments")
var summary: String
@Guide(description: "The best moment or strongest positive pattern from the week (1 sentence)")
var highlight: String
@Guide(description: "A gentle, actionable intention or suggestion for the coming week (1 sentence)")
var intention: String
@Guide(description: "SF Symbol name for the digest icon (e.g., sun.max.fill, leaf.fill, heart.fill)")
var iconName: String
}
/// Storable weekly digest (Codable for UserDefaults persistence)
struct WeeklyDigest: Codable, Equatable {
let headline: String
let summary: String
let highlight: String
let intention: String
let iconName: String
let generatedAt: Date
let weekStartDate: Date
let weekEndDate: Date
var isFromCurrentWeek: Bool {
let calendar = Calendar.current
let now = Date()
let currentWeekStart = calendar.dateInterval(of: .weekOfYear, for: now)?.start ?? now
let digestWeekStart = calendar.dateInterval(of: .weekOfYear, for: weekStartDate)?.start ?? weekStartDate
return calendar.isDate(currentWeekStart, inSameDayAs: digestWeekStart) ||
calendar.isDate(digestWeekStart, inSameDayAs: calendar.date(byAdding: .weekOfYear, value: -1, to: currentWeekStart)!)
}
/// Whether the digest was dismissed by the user
static var isDismissedKey: String { "weeklyDigestDismissedDate" }
static func markDismissed() {
GroupUserDefaults.groupDefaults.set(Date(), forKey: isDismissedKey)
}
static func isDismissed(for digest: WeeklyDigest) -> Bool {
guard let dismissedDate = GroupUserDefaults.groupDefaults.object(forKey: isDismissedKey) as? Date else {
return false
}
return dismissedDate >= digest.generatedAt
}
}

View File

@@ -48,11 +48,28 @@ final class MoodEntryModel {
// Guided Reflection
var reflectionJSON: String?
// AI-extracted theme tags (JSON array of strings)
var tagsJSON: String?
// Computed properties
var mood: Mood {
Mood(rawValue: moodValue) ?? .missing
}
/// Decoded tags from tagsJSON, or empty array if none
var tags: [String] {
guard let json = tagsJSON, let data = json.data(using: .utf8),
let decoded = try? JSONDecoder().decode([String].self, from: data) else {
return []
}
return decoded
}
/// Whether this entry has AI-extracted tags
var hasTags: Bool {
!tags.isEmpty
}
var moodString: String {
mood.strValue
}