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:
@@ -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] {
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user