Refactor: KI-Auswertung aus Logbuch entfernt

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 <noreply@anthropic.com>
This commit is contained in:
2026-04-23 12:38:31 +02:00
parent 8a13962055
commit ace6801d01
2 changed files with 1 additions and 223 deletions
-222
View File
@@ -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] {
+1 -1
View File
@@ -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")