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:
@@ -54,7 +54,7 @@ class ExportService {
|
||||
// MARK: - CSV Export
|
||||
|
||||
func generateCSV(entries: [MoodEntryModel]) -> String {
|
||||
var csv = "Date,Mood,Mood Value,Notes,Weekday,Entry Type,Timestamp\n"
|
||||
var csv = "Date,Mood,Mood Value,Notes,Reflection,Weekday,Entry Type,Timestamp\n"
|
||||
|
||||
let sortedEntries = entries.sorted { $0.forDate > $1.forDate }
|
||||
|
||||
@@ -63,11 +63,14 @@ class ExportService {
|
||||
let mood = entry.mood.widgetDisplayName
|
||||
let moodValue = entry.moodValue + 1 // 1-5 scale
|
||||
let notes = escapeCSV(entry.notes ?? "")
|
||||
let reflectionText = entry.reflectionJSON
|
||||
.flatMap { GuidedReflection.decode(from: $0) }
|
||||
.map { $0.responses.filter { !$0.answer.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }.map { "Q: \($0.question) A: \($0.answer)" }.joined(separator: " | ") } ?? ""
|
||||
let weekday = weekdayName(from: entry.weekDay)
|
||||
let entryType = EntryType(rawValue: entry.entryType)?.description ?? "Unknown"
|
||||
let timestamp = isoFormatter.string(from: entry.timestamp)
|
||||
|
||||
csv += "\(date),\(mood),\(moodValue),\(notes),\(weekday),\(entryType),\(timestamp)\n"
|
||||
csv += "\(date),\(mood),\(moodValue),\(notes),\(escapeCSV(reflectionText)),\(weekday),\(entryType),\(timestamp)\n"
|
||||
}
|
||||
|
||||
return csv
|
||||
|
||||
@@ -213,6 +213,18 @@ final class ReportPDFGenerator {
|
||||
<td>\(escapeHTML(weatherStr))</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
if let reflection = entry.reflection, reflection.answeredCount > 0 {
|
||||
rows += """
|
||||
<tr class="reflection-row">
|
||||
<td colspan="4">
|
||||
<div class="reflection-block">
|
||||
\(formatReflectionHTML(reflection))
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
return """
|
||||
@@ -261,6 +273,13 @@ final class ReportPDFGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private func formatReflectionHTML(_ reflection: GuidedReflection) -> String {
|
||||
reflection.responses
|
||||
.filter { !$0.answer.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
||||
.map { "<p class=\"reflection-q\">\(escapeHTML($0.question))</p><p class=\"reflection-a\">\(escapeHTML($0.answer))</p>" }
|
||||
.joined()
|
||||
}
|
||||
|
||||
private func escapeHTML(_ string: String) -> String {
|
||||
string
|
||||
.replacingOccurrences(of: "&", with: "&")
|
||||
@@ -396,6 +415,10 @@ final class ReportPDFGenerator {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.stats { font-size: 10pt; color: #666; margin-bottom: 6px; }
|
||||
.reflection-row td { border-bottom: none; padding-top: 0; }
|
||||
.reflection-block { background: #f9f7ff; padding: 8px 12px; margin: 4px 0 8px 0; border-radius: 4px; }
|
||||
.reflection-q { font-style: italic; font-size: 9pt; color: #666; margin-bottom: 2px; }
|
||||
.reflection-a { font-size: 10pt; color: #333; margin-bottom: 6px; }
|
||||
.page-break { page-break-before: always; }
|
||||
.no-page-break { page-break-inside: avoid; }
|
||||
.footer {
|
||||
|
||||
Reference in New Issue
Block a user