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:
Trey t
2026-03-11 10:51:36 -05:00
parent 19b4c8b05b
commit 5bd8f8076a
13 changed files with 15340 additions and 13617 deletions

View File

@@ -158,6 +158,7 @@ struct EntryDetailView: View {
@State private var showDeleteConfirmation = false
@State private var showFullScreenPhoto = false
@State private var selectedPhotoItem: PhotosPickerItem?
@State private var showReflectionFlow = false
@State private var selectedMood: Mood?
private var currentMood: Mood {
@@ -184,6 +185,11 @@ struct EntryDetailView: View {
// Mood section
moodSection
// Guided reflection section
if currentMood != .missing && currentMood != .placeholder {
reflectionSection
}
// Notes section
notesSection
@@ -218,6 +224,9 @@ struct EntryDetailView: View {
.sheet(isPresented: $showNoteEditor) {
NoteEditorView(entry: entry)
}
.sheet(isPresented: $showReflectionFlow) {
GuidedReflectionView(entry: entry)
}
.alert("Delete Entry", isPresented: $showDeleteConfirmation) {
Button("Delete", role: .destructive) {
onDelete()
@@ -417,6 +426,74 @@ struct EntryDetailView: View {
}
}
private var reflectionSection: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text(String(localized: "guided_reflection_title"))
.font(.headline)
.foregroundColor(textColor)
Spacer()
Button {
AnalyticsManager.shared.track(.reflectionStarted)
showReflectionFlow = true
} label: {
Text(entry.reflectionJSON != nil
? String(localized: "guided_reflection_edit")
: String(localized: "guided_reflection_begin"))
.font(.subheadline)
.fontWeight(.medium)
}
}
Button {
AnalyticsManager.shared.track(.reflectionStarted)
showReflectionFlow = true
} label: {
HStack {
if let json = entry.reflectionJSON,
let reflection = GuidedReflection.decode(from: json),
reflection.answeredCount > 0 {
VStack(alignment: .leading, spacing: 6) {
Text(reflection.responses.first(where: {
!$0.answer.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
})?.answer ?? "")
.font(.body)
.foregroundColor(textColor)
.multilineTextAlignment(.leading)
.lineLimit(3)
Text(String(localized: "guided_reflection_answered_count \(reflection.answeredCount) \(reflection.totalQuestions)"))
.font(.caption)
.foregroundStyle(.secondary)
}
} else {
HStack(spacing: 8) {
Image(systemName: "sparkles")
.foregroundStyle(.secondary)
Text(String(localized: "guided_reflection_empty_prompt"))
.foregroundStyle(.secondary)
}
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(.tertiary)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.systemBackground))
)
}
.buttonStyle(.plain)
}
}
private func weatherSection(_ weatherData: WeatherData) -> some View {
VStack(alignment: .leading, spacing: 12) {
Text("Weather")