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 SwiftData
|
||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
// MARK: - AI Analysis State
|
|
||||||
|
|
||||||
private enum AnalysisState {
|
|
||||||
case idle
|
|
||||||
case loading
|
|
||||||
case result(AIAnalysisResult, Date)
|
|
||||||
case error(String)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Timeline Item
|
// MARK: - Timeline Item
|
||||||
|
|
||||||
private enum LogbuchItem: Identifiable {
|
private enum LogbuchItem: Identifiable {
|
||||||
@@ -64,15 +55,8 @@ struct LogbuchView: View {
|
|||||||
@Environment(\.nahbarTheme) var theme
|
@Environment(\.nahbarTheme) var theme
|
||||||
@Environment(\.modelContext) var modelContext
|
@Environment(\.modelContext) var modelContext
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
@StateObject private var store = StoreManager.shared
|
|
||||||
let person: Person
|
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
|
// Kalender-Lösch-Bestätigung
|
||||||
@State private var momentPendingDelete: Moment? = nil
|
@State private var momentPendingDelete: Moment? = nil
|
||||||
@State private var showCalendarDeleteDialog = false
|
@State private var showCalendarDeleteDialog = false
|
||||||
@@ -96,8 +80,6 @@ struct LogbuchView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRO: KI-Analyse
|
|
||||||
aiAnalysisCard
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
.padding(.top, 16)
|
.padding(.top, 16)
|
||||||
@@ -107,13 +89,6 @@ struct LogbuchView: View {
|
|||||||
.navigationTitle("Logbuch")
|
.navigationTitle("Logbuch")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.themedNavBar()
|
.themedNavBar()
|
||||||
.sheet(isPresented: $showPaywall) { PaywallView(targeting: .max) }
|
|
||||||
.sheet(isPresented: $showAIConsent) {
|
|
||||||
AIConsentSheet {
|
|
||||||
aiConsentGiven = true
|
|
||||||
Task { await runAnalysis() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sheet(item: $momentForTextEdit) { moment in
|
.sheet(item: $momentForTextEdit) { moment in
|
||||||
EditMomentView(moment: moment)
|
EditMomentView(moment: moment)
|
||||||
}
|
}
|
||||||
@@ -147,12 +122,6 @@ struct LogbuchView: View {
|
|||||||
guard notification.userInfo?[NSInvalidatedAllObjectsKey] != nil else { return }
|
guard notification.userInfo?[NSInvalidatedAllObjectsKey] != nil else { return }
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
.onAppear {
|
|
||||||
if let cached = AIAnalysisService.shared.loadCached(for: person) {
|
|
||||||
analysisState = .result(cached.asResult, cached.analyzedAt)
|
|
||||||
}
|
|
||||||
remainingRequests = AIAnalysisService.shared.remainingRequests
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Month Section
|
// MARK: - Month Section
|
||||||
@@ -320,197 +289,6 @@ struct LogbuchView: View {
|
|||||||
.padding(.vertical, 48)
|
.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
|
// MARK: - Data
|
||||||
|
|
||||||
private var mergedItems: [LogbuchItem] {
|
private var mergedItems: [LogbuchItem] {
|
||||||
|
|||||||
@@ -542,7 +542,7 @@ struct PersonDetailView: View {
|
|||||||
|
|
||||||
return VStack(alignment: .leading, spacing: 10) {
|
return VStack(alignment: .leading, spacing: 10) {
|
||||||
HStack {
|
HStack {
|
||||||
SectionHeader(title: "Verlauf & KI Insights zu \(person.firstName)", icon: "sparkles")
|
SectionHeader(title: "Verlauf", icon: "clock.arrow.circlepath")
|
||||||
Spacer()
|
Spacer()
|
||||||
NavigationLink(destination: LogbuchView(person: person)) {
|
NavigationLink(destination: LogbuchView(person: person)) {
|
||||||
Text("Alle")
|
Text("Alle")
|
||||||
|
|||||||
Reference in New Issue
Block a user