// // ReflectionFeedbackView.swift // Reflect // // Displays AI-generated personalized feedback after completing a guided reflection. // import SwiftUI struct ReflectionFeedbackView: View { let mood: Mood let reflection: GuidedReflection let onDismiss: () -> Void @AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @State private var feedback: ReflectionFeedbackState = .loading @State private var appeared = false private var accentColor: Color { moodTint.color(forMood: mood) } private var textColor: Color { theme.currentTheme.labelColor } var body: some View { VStack(spacing: 24) { headerIcon switch feedback { case .loading: loadingContent case .loaded(let affirmation, let observation, let takeaway, let iconName): feedbackContent(affirmation: affirmation, observation: observation, takeaway: takeaway, iconName: iconName) case .error: fallbackContent case .unavailable: fallbackContent } dismissButton } .padding(24) .background( RoundedRectangle(cornerRadius: 24) .fill(Color(.secondarySystemBackground)) ) .padding(.horizontal, 20) .opacity(appeared ? 1 : 0) .scaleEffect(appeared ? 1 : 0.95) .task { await generateFeedback() } .onAppear { withAnimation(.easeOut(duration: 0.3)) { appeared = true } } .accessibilityIdentifier(AccessibilityID.ReflectionFeedback.container) } // MARK: - Header private var headerIcon: some View { VStack(spacing: 12) { Image(systemName: "sparkles") .font(.system(size: 32)) .foregroundStyle(accentColor) .symbolEffect(.pulse, options: .repeating, isActive: feedback.isLoading) Text(String(localized: "Your Reflection")) .font(.headline) .foregroundColor(textColor) } } // MARK: - Loading private var loadingContent: some View { VStack(spacing: 16) { ForEach(0..<3, id: \.self) { _ in RoundedRectangle(cornerRadius: 8) .fill(Color(.systemGray5)) .frame(height: 16) .shimmering() } } .padding(.vertical, 8) .accessibilityIdentifier(AccessibilityID.ReflectionFeedback.loading) } // MARK: - Feedback Content private func feedbackContent(affirmation: String, observation: String, takeaway: String, iconName: String) -> some View { VStack(alignment: .leading, spacing: 16) { feedbackRow(icon: iconName, text: affirmation) feedbackRow(icon: "eye.fill", text: observation) feedbackRow(icon: "arrow.right.circle.fill", text: takeaway) } .transition(.opacity.combined(with: .move(edge: .bottom))) .accessibilityIdentifier(AccessibilityID.ReflectionFeedback.content) } private func feedbackRow(icon: String, text: String) -> some View { HStack(alignment: .top, spacing: 12) { Image(systemName: icon) .font(.body) .foregroundStyle(accentColor) .frame(width: 24, height: 24) Text(text) .font(.subheadline) .foregroundColor(textColor) .fixedSize(horizontal: false, vertical: true) } } // MARK: - Fallback (no AI available) private var fallbackContent: some View { VStack(spacing: 8) { Text(String(localized: "Great job completing your reflection. Taking time to check in with yourself is a powerful habit.")) .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) } .accessibilityIdentifier(AccessibilityID.ReflectionFeedback.fallback) } // MARK: - Dismiss private var dismissButton: some View { Button { onDismiss() } label: { Text(String(localized: "Done")) .font(.headline) .frame(maxWidth: .infinity) .padding(.vertical, 14) } .buttonStyle(.borderedProminent) .tint(accentColor) .accessibilityIdentifier(AccessibilityID.ReflectionFeedback.doneButton) } // MARK: - Generation private func generateFeedback() async { // Check premium access guard !IAPManager.shared.shouldShowPaywall else { feedback = .unavailable return } if #available(iOS 26, *) { let service = FoundationModelsReflectionService() do { let result = try await service.generateFeedback(for: reflection, mood: mood) withAnimation(.easeInOut(duration: 0.3)) { feedback = .loaded( affirmation: result.affirmation, observation: result.observation, takeaway: result.takeaway, iconName: result.iconName ) } } catch { withAnimation { feedback = .error } } } else { feedback = .unavailable } } } // MARK: - State private enum ReflectionFeedbackState { case loading case loaded(affirmation: String, observation: String, takeaway: String, iconName: String) case error case unavailable var isLoading: Bool { if case .loading = self { return true } return false } } // MARK: - Shimmer Effect private struct ShimmerModifier: ViewModifier { @State private var phase: CGFloat = 0 func body(content: Content) -> some View { content .overlay( LinearGradient( colors: [.clear, Color.white.opacity(0.3), .clear], startPoint: .leading, endPoint: .trailing ) .offset(x: phase) .onAppear { withAnimation(.linear(duration: 1.5).repeatForever(autoreverses: false)) { phase = 300 } } ) .mask(content) } } private extension View { func shimmering() -> some View { modifier(ShimmerModifier()) } }