From ace6801d01de6ace8d5f1b27d5df5f0a00ceabed Mon Sep 17 00:00:00 2001 From: Sven Date: Thu, 23 Apr 2026 12:38:31 +0200 Subject: [PATCH] Refactor: KI-Auswertung aus Logbuch entfernt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die KI-Karte (aiAnalysisCard) wurde vollständig aus LogbuchView ausgebaut. KI Insights sind weiterhin über den Sparkles-Button im Kontakt-Header zugänglich. Sektionsüberschrift in PersonDetailView von "Verlauf & KI Insights zu [Name]" auf "Verlauf" vereinfacht. Co-Authored-By: Claude Sonnet 4.6 --- nahbar/nahbar/LogbuchView.swift | 222 --------------------------- nahbar/nahbar/PersonDetailView.swift | 2 +- 2 files changed, 1 insertion(+), 223 deletions(-) diff --git a/nahbar/nahbar/LogbuchView.swift b/nahbar/nahbar/LogbuchView.swift index f6f7d67..7a78dcb 100644 --- a/nahbar/nahbar/LogbuchView.swift +++ b/nahbar/nahbar/LogbuchView.swift @@ -2,15 +2,6 @@ import SwiftUI import SwiftData import CoreData -// MARK: - AI Analysis State - -private enum AnalysisState { - case idle - case loading - case result(AIAnalysisResult, Date) - case error(String) -} - // MARK: - Timeline Item private enum LogbuchItem: Identifiable { @@ -64,15 +55,8 @@ struct LogbuchView: View { @Environment(\.nahbarTheme) var theme @Environment(\.modelContext) var modelContext @Environment(\.dismiss) var dismiss - @StateObject private var store = StoreManager.shared let person: Person - @State private var analysisState: AnalysisState = .idle - @State private var showPaywall = false - @State private var showAIConsent = false - @State private var remainingRequests: Int = AIAnalysisService.shared.remainingRequests - @AppStorage("aiConsentGiven") private var aiConsentGiven = false - // Kalender-Lösch-Bestätigung @State private var momentPendingDelete: Moment? = nil @State private var showCalendarDeleteDialog = false @@ -96,8 +80,6 @@ struct LogbuchView: View { } } - // PRO: KI-Analyse - aiAnalysisCard } .padding(.horizontal, 20) .padding(.top, 16) @@ -107,13 +89,6 @@ struct LogbuchView: View { .navigationTitle("Logbuch") .navigationBarTitleDisplayMode(.inline) .themedNavBar() - .sheet(isPresented: $showPaywall) { PaywallView(targeting: .max) } - .sheet(isPresented: $showAIConsent) { - AIConsentSheet { - aiConsentGiven = true - Task { await runAnalysis() } - } - } .sheet(item: $momentForTextEdit) { moment in EditMomentView(moment: moment) } @@ -147,12 +122,6 @@ struct LogbuchView: View { guard notification.userInfo?[NSInvalidatedAllObjectsKey] != nil else { return } dismiss() } - .onAppear { - if let cached = AIAnalysisService.shared.loadCached(for: person) { - analysisState = .result(cached.asResult, cached.analyzedAt) - } - remainingRequests = AIAnalysisService.shared.remainingRequests - } } // MARK: - Month Section @@ -320,197 +289,6 @@ struct LogbuchView: View { .padding(.vertical, 48) } - // MARK: - MAX: KI-Analyse - - private var canUseAI: Bool { - store.isMax || AIAnalysisService.shared.hasFreeQueriesLeft - } - - private var aiAnalysisCard: some View { - VStack(alignment: .leading, spacing: 12) { - HStack(spacing: 6) { - SectionHeader(title: "KI-Auswertung", icon: "sparkles") - MaxBadge() - if !store.isMax && canUseAI { - Text("\(AIAnalysisService.shared.freeQueriesRemaining) gratis") - .font(.system(size: 10, weight: .bold)) - .foregroundStyle(theme.contentTertiary) - .padding(.horizontal, 7) - .padding(.vertical, 3) - .background(theme.backgroundSecondary) - .clipShape(Capsule()) - } - } - - if !canUseAI { - // Gesperrt: alle Freiabfragen verbraucht - Button { showPaywall = true } label: { - HStack(spacing: 10) { - Image(systemName: "sparkles") - .foregroundStyle(theme.accent) - Text("nahbar Max freischalten für KI-Analyse") - .font(.system(size: 14, weight: .medium)) - .foregroundStyle(theme.accent) - Spacer() - Image(systemName: "chevron.right") - .font(.system(size: 12)) - .foregroundStyle(theme.contentTertiary) - } - .padding(16) - .background(theme.surfaceCard) - .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) - } - } else { - // Active state - VStack(alignment: .leading, spacing: 0) { - switch analysisState { - case .idle: - Button { - if aiConsentGiven { - Task { await runAnalysis() } - } else { - showAIConsent = true - } - } label: { - HStack(spacing: 10) { - Image(systemName: "sparkles") - .foregroundStyle(theme.accent) - Text("\(person.firstName) analysieren") - .font(.system(size: 15, weight: .medium)) - .foregroundStyle(theme.contentPrimary) - Spacer() - Image(systemName: "chevron.right") - .font(.system(size: 12)) - .foregroundStyle(theme.contentTertiary) - } - .padding(16) - } - - case .loading: - HStack(spacing: 12) { - ProgressView().tint(theme.accent) - VStack(alignment: .leading, spacing: 2) { - Text("Analysiere Logbuch…") - .font(.system(size: 14)) - .foregroundStyle(theme.contentSecondary) - Text("Das kann bis zu einer Minute dauern.") - .font(.system(size: 12)) - .foregroundStyle(theme.contentTertiary) - } - } - .padding(16) - - case .result(let result, let date): - VStack(alignment: .leading, spacing: 0) { - analysisSection(icon: "waveform.path", title: "Muster & Themen", text: result.patterns) - RowDivider() - analysisSection(icon: "person.2", title: "Beziehungsqualität", text: result.relationship) - RowDivider() - analysisSection(icon: "arrow.right.circle", title: "Empfehlung", text: result.recommendation) - RowDivider() - HStack(spacing: 0) { - // Zeitstempel - VStack(alignment: .leading, spacing: 1) { - Text("Analysiert") - .font(.system(size: 11)) - .foregroundStyle(theme.contentTertiary) - Text(date.formatted(.dateTime.day().month(.abbreviated).hour().minute().locale(Locale(identifier: "de_DE")))) - .font(.system(size: 11)) - .foregroundStyle(theme.contentTertiary) - } - .padding(.leading, 16) - .padding(.vertical, 12) - - Spacer() - - // Aktualisieren - Button { - Task { await runAnalysis() } - } label: { - HStack(spacing: 4) { - Image(systemName: "arrow.clockwise") - .font(.system(size: 12)) - Text(remainingRequests > 0 ? "Aktualisieren (\(remainingRequests))" : "Limit erreicht") - .font(.system(size: 13)) - } - .foregroundStyle(remainingRequests > 0 ? theme.accent : theme.contentTertiary) - } - .disabled(remainingRequests == 0 || isPurchasing) - .padding(.trailing, 16) - .padding(.vertical, 12) - } - } - - case .error(let msg): - VStack(alignment: .leading, spacing: 8) { - Label("Analyse fehlgeschlagen", systemImage: "exclamationmark.triangle") - .font(.system(size: 14, weight: .medium)) - .foregroundStyle(theme.contentSecondary) - Text(msg) - .font(.system(size: 12)) - .foregroundStyle(theme.contentTertiary) - Button { - Task { await runAnalysis() } - } label: { - Text("Erneut versuchen") - .font(.system(size: 13, weight: .medium)) - .foregroundStyle(theme.accent) - } - } - .padding(16) - } - } - .background(theme.surfaceCard) - .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) - } - } - } - - private func analysisSection(icon: String, title: String, text: String) -> some View { - HStack(alignment: .top, spacing: 12) { - Image(systemName: icon) - .font(.system(size: 13)) - .foregroundStyle(theme.accent) - .frame(width: 20) - .padding(.top, 2) - VStack(alignment: .leading, spacing: 6) { - Text(title) - .font(.system(size: 13, weight: .semibold)) - .foregroundStyle(theme.contentSecondary) - Text(LocalizedStringKey(text)) - .font(.system(size: 14, design: theme.displayDesign)) - .foregroundStyle(theme.contentPrimary) - .fixedSize(horizontal: false, vertical: true) - } - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - } - - private var isPurchasing: Bool { - if case .loading = analysisState { return true } - return false - } - - private func runAnalysis() async { - guard !mergedItems.isEmpty else { return } - guard !AIAnalysisService.shared.isRateLimited else { return } - analysisState = .loading - do { - let result = try await AIAnalysisService.shared.analyze(person: person) - remainingRequests = AIAnalysisService.shared.remainingRequests - if !store.isMax { AIAnalysisService.shared.consumeFreeQuery() } - analysisState = .result(result, Date()) - } catch { - // Bei Fehler alten Cache wiederherstellen falls vorhanden - if let cached = AIAnalysisService.shared.loadCached(for: person) { - analysisState = .result(cached.asResult, cached.analyzedAt) - } else { - analysisState = .error(error.localizedDescription) - } - } - } - // MARK: - Data private var mergedItems: [LogbuchItem] { diff --git a/nahbar/nahbar/PersonDetailView.swift b/nahbar/nahbar/PersonDetailView.swift index e7d6b86..dc9dadf 100644 --- a/nahbar/nahbar/PersonDetailView.swift +++ b/nahbar/nahbar/PersonDetailView.swift @@ -542,7 +542,7 @@ struct PersonDetailView: View { return VStack(alignment: .leading, spacing: 10) { HStack { - SectionHeader(title: "Verlauf & KI Insights zu \(person.firstName)", icon: "sparkles") + SectionHeader(title: "Verlauf", icon: "clock.arrow.circlepath") Spacer() NavigationLink(destination: LogbuchView(person: person)) { Text("Alle")