Add AI enablement guidance with reason-specific UI and localized translations
Show specific guidance when Apple Intelligence is unavailable: - Device not eligible: "iPhone 15 Pro or later required" - Not enabled: step-by-step path + "Open Settings" button - Model downloading: "Please wait" + "Try Again" button - Pre-iOS 26: "Update required" Auto re-checks availability when app returns to foreground so enabling Apple Intelligence in Settings immediately triggers insight generation. Adds translations for all new AI strings across de, es, fr, ja, ko, pt-BR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ struct InsightsView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@@ -107,6 +108,11 @@ struct InsightsView: View {
|
||||
viewModel.generateInsights()
|
||||
loadWeeklyDigest()
|
||||
}
|
||||
.onChange(of: scenePhase) { _, newPhase in
|
||||
if newPhase == .active {
|
||||
viewModel.recheckAvailability()
|
||||
}
|
||||
}
|
||||
.padding(.top)
|
||||
}
|
||||
|
||||
@@ -136,6 +142,11 @@ struct InsightsView: View {
|
||||
private var insightsContent: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
// AI enablement guidance when not available
|
||||
if !viewModel.isAIAvailable && !iapManager.shouldShowPaywall {
|
||||
aiEnablementCard
|
||||
}
|
||||
|
||||
// Weekly Digest Card
|
||||
if let digest = weeklyDigest {
|
||||
WeeklyDigestCardView(digest: digest)
|
||||
@@ -193,6 +204,91 @@ struct InsightsView: View {
|
||||
.disabled(iapManager.shouldShowPaywall)
|
||||
}
|
||||
|
||||
// MARK: - AI Enablement Card
|
||||
|
||||
private var aiEnablementCard: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: aiEnablementIcon)
|
||||
.font(.system(size: 36))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text(aiEnablementTitle)
|
||||
.font(.headline)
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(aiEnablementDescription)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
if viewModel.aiUnavailableReason == .notEnabled {
|
||||
Button {
|
||||
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
} label: {
|
||||
Label(String(localized: "Open Settings"), systemImage: "gear")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.purple)
|
||||
}
|
||||
|
||||
if viewModel.aiUnavailableReason == .modelDownloading {
|
||||
Button {
|
||||
viewModel.recheckAvailability()
|
||||
} label: {
|
||||
Label(String(localized: "Try Again"), systemImage: "arrow.clockwise")
|
||||
.font(.subheadline.weight(.medium))
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(Color(.secondarySystemBackground))
|
||||
)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
private var aiEnablementIcon: String {
|
||||
switch viewModel.aiUnavailableReason {
|
||||
case .deviceNotEligible: return "iphone.slash"
|
||||
case .notEnabled: return "gearshape.fill"
|
||||
case .modelDownloading: return "arrow.down.circle"
|
||||
case .preiOS26: return "arrow.up.circle"
|
||||
case .unknown: return "brain.head.profile"
|
||||
}
|
||||
}
|
||||
|
||||
private var aiEnablementTitle: String {
|
||||
switch viewModel.aiUnavailableReason {
|
||||
case .deviceNotEligible: return String(localized: "Device Not Supported")
|
||||
case .notEnabled: return String(localized: "Enable Apple Intelligence")
|
||||
case .modelDownloading: return String(localized: "AI Model Downloading")
|
||||
case .preiOS26: return String(localized: "Update Required")
|
||||
case .unknown: return String(localized: "AI Unavailable")
|
||||
}
|
||||
}
|
||||
|
||||
private var aiEnablementDescription: String {
|
||||
switch viewModel.aiUnavailableReason {
|
||||
case .deviceNotEligible:
|
||||
return String(localized: "AI insights require iPhone 15 Pro or later with Apple Intelligence.")
|
||||
case .notEnabled:
|
||||
return String(localized: "Turn on Apple Intelligence to unlock personalized mood insights.\n\nSettings → Apple Intelligence & Siri → Apple Intelligence")
|
||||
case .modelDownloading:
|
||||
return String(localized: "The AI model is still downloading. This may take a few minutes.")
|
||||
case .preiOS26:
|
||||
return String(localized: "AI insights require iOS 26 or later with Apple Intelligence.")
|
||||
case .unknown:
|
||||
return String(localized: "Apple Intelligence is required for personalized insights.")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Generating State
|
||||
|
||||
private var isGeneratingInsights: Bool {
|
||||
|
||||
@@ -40,6 +40,7 @@ class InsightsViewModel: ObservableObject {
|
||||
@Published var allTimeLoadingState: InsightLoadingState = .idle
|
||||
|
||||
@Published var isAIAvailable: Bool = false
|
||||
@Published var aiUnavailableReason: AIUnavailableReason = .preiOS26
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
@@ -57,10 +58,12 @@ class InsightsViewModel: ObservableObject {
|
||||
let service = FoundationModelsInsightService()
|
||||
insightService = service
|
||||
isAIAvailable = service.isAvailable
|
||||
aiUnavailableReason = service.unavailableReason
|
||||
service.prewarm()
|
||||
} else {
|
||||
insightService = nil
|
||||
isAIAvailable = false
|
||||
aiUnavailableReason = .preiOS26
|
||||
}
|
||||
|
||||
dataListenerToken = DataController.shared.addNewDataListener { [weak self] in
|
||||
@@ -185,14 +188,10 @@ class InsightsViewModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if AI is available
|
||||
// Check if AI is available — show reason-specific guidance
|
||||
guard isAIAvailable else {
|
||||
updateInsights([Insight(
|
||||
icon: "brain.head.profile",
|
||||
title: "AI Unavailable",
|
||||
description: "Apple Intelligence is required for personalized insights. Please enable it in Settings.",
|
||||
mood: nil
|
||||
)])
|
||||
let (icon, title, description) = unavailableMessage()
|
||||
updateInsights([Insight(icon: icon, title: title, description: description, mood: nil)])
|
||||
updateState(.error("AI not available"))
|
||||
return
|
||||
}
|
||||
@@ -220,13 +219,47 @@ class InsightsViewModel: ObservableObject {
|
||||
updateState(.error(error.localizedDescription))
|
||||
}
|
||||
} else {
|
||||
updateInsights([Insight(
|
||||
icon: "brain.head.profile",
|
||||
title: "AI Unavailable",
|
||||
description: "Apple Intelligence is required for personalized insights. Please enable it in Settings.",
|
||||
mood: nil
|
||||
)])
|
||||
let (icon, title, description) = unavailableMessage()
|
||||
updateInsights([Insight(icon: icon, title: title, description: description, mood: nil)])
|
||||
updateState(.error("AI not available"))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Unavailable Messages
|
||||
|
||||
private func unavailableMessage() -> (icon: String, title: String, description: String) {
|
||||
switch aiUnavailableReason {
|
||||
case .deviceNotEligible:
|
||||
return ("iphone.slash", "Device Not Supported",
|
||||
String(localized: "AI insights require iPhone 15 Pro or later with Apple Intelligence."))
|
||||
case .notEnabled:
|
||||
return ("gearshape.fill", "Apple Intelligence Disabled",
|
||||
String(localized: "Turn on Apple Intelligence in Settings → Apple Intelligence & Siri to unlock AI insights."))
|
||||
case .modelDownloading:
|
||||
return ("arrow.down.circle", "AI Model Downloading",
|
||||
String(localized: "The AI model is still downloading. Please wait a few minutes and try again."))
|
||||
case .preiOS26:
|
||||
return ("arrow.up.circle", "Update Required",
|
||||
String(localized: "AI insights require iOS 26 or later with Apple Intelligence."))
|
||||
case .unknown:
|
||||
return ("brain.head.profile", "AI Unavailable",
|
||||
String(localized: "Apple Intelligence is required for personalized insights."))
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-check AI availability (e.g., after returning from Settings)
|
||||
func recheckAvailability() {
|
||||
if #available(iOS 26, *), let service = insightService as? FoundationModelsInsightService {
|
||||
service.checkAvailability()
|
||||
let wasAvailable = isAIAvailable
|
||||
isAIAvailable = service.isAvailable
|
||||
aiUnavailableReason = service.unavailableReason
|
||||
|
||||
// If just became available, generate insights
|
||||
if !wasAvailable && isAIAvailable {
|
||||
service.prewarm()
|
||||
generateInsights()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user