Add guided reflection flow with mood-adaptive CBT/ACT questions
Walks users through 3-4 guided questions based on mood category: positive (great/good) gets gratitude-oriented questions, neutral (average) gets exploratory questions, and negative (bad/horrible) gets empathetic questions. Stored as JSON in MoodEntryModel, integrated into PDF reports, AI summaries, and CSV export. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,12 +36,14 @@ struct ReportEntry {
|
||||
let mood: Mood
|
||||
let notes: String?
|
||||
let weather: WeatherData?
|
||||
let reflection: GuidedReflection?
|
||||
|
||||
init(from model: MoodEntryModel) {
|
||||
self.date = model.forDate
|
||||
self.mood = model.mood
|
||||
self.notes = model.notes
|
||||
self.weather = model.weatherJSON.flatMap { WeatherData.decode(from: $0) }
|
||||
self.reflection = model.reflectionJSON.flatMap { GuidedReflection.decode(from: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
110
Shared/Models/GuidedReflection.swift
Normal file
110
Shared/Models/GuidedReflection.swift
Normal file
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// GuidedReflection.swift
|
||||
// Reflect
|
||||
//
|
||||
// Codable model for guided reflection responses, stored as JSON in MoodEntryModel.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Mood Category
|
||||
|
||||
enum MoodCategory: String, Codable {
|
||||
case positive // great, good → 3 questions
|
||||
case neutral // average → 4 questions
|
||||
case negative // bad, horrible → 4 questions
|
||||
|
||||
init(from mood: Mood) {
|
||||
switch mood {
|
||||
case .great, .good: self = .positive
|
||||
case .average: self = .neutral
|
||||
case .horrible, .bad: self = .negative
|
||||
default: self = .neutral
|
||||
}
|
||||
}
|
||||
|
||||
var questionCount: Int {
|
||||
switch self {
|
||||
case .positive: return 3
|
||||
case .neutral, .negative: return 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Guided Reflection
|
||||
|
||||
struct GuidedReflection: Codable, Equatable {
|
||||
|
||||
struct Response: Codable, Equatable, Identifiable {
|
||||
var id: Int // question index (0-based)
|
||||
let question: String
|
||||
var answer: String
|
||||
}
|
||||
|
||||
let moodCategory: MoodCategory
|
||||
var responses: [Response]
|
||||
var completedAt: Date?
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
var isComplete: Bool {
|
||||
responses.count == moodCategory.questionCount &&
|
||||
responses.allSatisfy { !$0.answer.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
||||
}
|
||||
|
||||
var answeredCount: Int {
|
||||
responses.filter { !$0.answer.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }.count
|
||||
}
|
||||
|
||||
var totalQuestions: Int {
|
||||
moodCategory.questionCount
|
||||
}
|
||||
|
||||
// MARK: - Factory
|
||||
|
||||
static func createNew(for mood: Mood) -> GuidedReflection {
|
||||
let category = MoodCategory(from: mood)
|
||||
let questionTexts = questions(for: category)
|
||||
let responses = questionTexts.enumerated().map { index, question in
|
||||
Response(id: index, question: question, answer: "")
|
||||
}
|
||||
return GuidedReflection(moodCategory: category, responses: responses, completedAt: nil)
|
||||
}
|
||||
|
||||
static func questions(for category: MoodCategory) -> [String] {
|
||||
switch category {
|
||||
case .positive:
|
||||
return [
|
||||
String(localized: "guided_reflection_positive_q1"),
|
||||
String(localized: "guided_reflection_positive_q2"),
|
||||
String(localized: "guided_reflection_positive_q3"),
|
||||
]
|
||||
case .neutral:
|
||||
return [
|
||||
String(localized: "guided_reflection_neutral_q1"),
|
||||
String(localized: "guided_reflection_neutral_q2"),
|
||||
String(localized: "guided_reflection_neutral_q3"),
|
||||
String(localized: "guided_reflection_neutral_q4"),
|
||||
]
|
||||
case .negative:
|
||||
return [
|
||||
String(localized: "guided_reflection_negative_q1"),
|
||||
String(localized: "guided_reflection_negative_q2"),
|
||||
String(localized: "guided_reflection_negative_q3"),
|
||||
String(localized: "guided_reflection_negative_q4"),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - JSON Helpers
|
||||
|
||||
func encode() -> String? {
|
||||
guard let data = try? JSONEncoder().encode(self) else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
static func decode(from json: String) -> GuidedReflection? {
|
||||
guard let data = json.data(using: .utf8) else { return nil }
|
||||
return try? JSONDecoder().decode(GuidedReflection.self, from: data)
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,9 @@ final class MoodEntryModel {
|
||||
// Weather
|
||||
var weatherJSON: String?
|
||||
|
||||
// Guided Reflection
|
||||
var reflectionJSON: String?
|
||||
|
||||
// Computed properties
|
||||
var mood: Mood {
|
||||
Mood(rawValue: moodValue) ?? .missing
|
||||
@@ -62,7 +65,8 @@ final class MoodEntryModel {
|
||||
canDelete: Bool = true,
|
||||
notes: String? = nil,
|
||||
photoID: UUID? = nil,
|
||||
weatherJSON: String? = nil
|
||||
weatherJSON: String? = nil,
|
||||
reflectionJSON: String? = nil
|
||||
) {
|
||||
self.forDate = forDate
|
||||
self.moodValue = mood.rawValue
|
||||
@@ -74,6 +78,7 @@ final class MoodEntryModel {
|
||||
self.notes = notes
|
||||
self.photoID = photoID
|
||||
self.weatherJSON = weatherJSON
|
||||
self.reflectionJSON = reflectionJSON
|
||||
}
|
||||
|
||||
// Convenience initializer for raw values
|
||||
@@ -87,7 +92,8 @@ final class MoodEntryModel {
|
||||
canDelete: Bool = true,
|
||||
notes: String? = nil,
|
||||
photoID: UUID? = nil,
|
||||
weatherJSON: String? = nil
|
||||
weatherJSON: String? = nil,
|
||||
reflectionJSON: String? = nil
|
||||
) {
|
||||
self.forDate = forDate
|
||||
self.moodValue = moodValue
|
||||
@@ -99,5 +105,6 @@ final class MoodEntryModel {
|
||||
self.notes = notes
|
||||
self.photoID = photoID
|
||||
self.weatherJSON = weatherJSON
|
||||
self.reflectionJSON = reflectionJSON
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user