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:
28
Shared/Models/AIEntryTags.swift
Normal file
28
Shared/Models/AIEntryTags.swift
Normal 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]
|
||||
}
|
||||
26
Shared/Models/AIReflectionFeedback.swift
Normal file
26
Shared/Models/AIReflectionFeedback.swift
Normal 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
|
||||
}
|
||||
64
Shared/Models/AIWeeklyDigest.swift
Normal file
64
Shared/Models/AIWeeklyDigest.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user