From 31a7a2d5df837492330b87aa4663146f7c539869 Mon Sep 17 00:00:00 2001 From: Sven Date: Thu, 23 Apr 2026 13:03:18 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20#20:=20Aktivit=C3=A4tsvorschl=C3=A4ge-Fea?= =?UTF-8?q?ture=20entfernt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Funktion war zu unspezifisch und wenig nützlich. Komplett entfernt: - PersonalityEngine: suggestedActivities, ActivityStyle, ActivitySuggestion, preferredActivityStyle, highlightNovelty - PersonDetailView: activityHint, personalityStore, intentionSuggestionButton(), refreshActivityHint() - NahbarPersonalityTests: highlightNovelty-Tests + SuggestedActivitiesTests-Suite 500 Tests, 0 Fehler. Co-Authored-By: Claude Sonnet 4.6 --- nahbar/nahbar/Localizable.xcstrings | 119 +++++++++++++----- nahbar/nahbar/PersonDetailView.swift | 54 -------- nahbar/nahbar/PersonalityEngine.swift | 108 ---------------- .../nahbarTests/NahbarPersonalityTests.swift | 73 ----------- 4 files changed, 90 insertions(+), 264 deletions(-) diff --git a/nahbar/nahbar/Localizable.xcstrings b/nahbar/nahbar/Localizable.xcstrings index 6e813de..1d95a55 100644 --- a/nahbar/nahbar/Localizable.xcstrings +++ b/nahbar/nahbar/Localizable.xcstrings @@ -618,6 +618,7 @@ } }, "Alle %lld Tage – basierend auf deinem Profil" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -918,6 +919,7 @@ }, "App-Schutz" : { "comment" : "SettingsView – section header for app lock settings", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -971,8 +973,12 @@ } } } + }, + "Auf Max upgraden" : { + }, "Auf Max upgraden – KI-Analyse freischalten" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1275,6 +1281,9 @@ } } } + }, + "Darstellung & Profil" : { + }, "Das kann bis zu einer Minute dauern." : { "comment" : "LogbuchView – AI analysis loading subtitle", @@ -1300,6 +1309,7 @@ }, "Daten werden geräteübergreifend synchronisiert" : { "comment" : "SettingsView – iCloud sync enabled subtitle", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1323,6 +1333,7 @@ }, "Daten werden nur lokal gespeichert" : { "comment" : "SettingsView – iCloud sync disabled subtitle", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1567,6 +1578,7 @@ }, "Diagnose" : { "comment" : "SettingsView – section header for developer diagnostics", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2101,6 +2113,7 @@ } }, "Empfohlenes Nudge-Intervall" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2132,6 +2145,9 @@ } } } + }, + "Entwickler" : { + }, "Entwickler-Log" : { "comment" : "SettingsView / LogExportView – developer log nav title", @@ -2512,6 +2528,9 @@ } } } + }, + "Funktionen" : { + }, "Für wen?" : { "comment" : "TodayPersonPickerSheet – navigation title", @@ -2614,6 +2633,9 @@ } } } + }, + "Geräteübergreifend synchronisiert" : { + }, "Geschenkidee anzeigen" : { "comment" : "TodayView GiftSuggestionRow – collapsed state button", @@ -2970,6 +2992,7 @@ }, "iCloud" : { "comment" : "SettingsView – iCloud section header", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2991,6 +3014,7 @@ } }, "Idee: %@" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3168,9 +3192,6 @@ } } } - }, - "Kalender-Einstellungen" : { - }, "Kauf wiederherstellen" : { "comment" : "PaywallView – restore purchases button", @@ -3249,9 +3270,30 @@ } } } + }, + "KI Insights freischalten" : { + + }, + "KI Insights zu %@" : { + "comment" : "AIAnalysisSheet – navigation title mit Personenname", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "AI Insights on %@" + } + } + } + }, + "KI Insights, Themes & mehr" : { + + }, + "KI Modell" : { + }, "KI-Analyse" : { "comment" : "SettingsView – section header for AI settings", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3263,6 +3305,7 @@ }, "KI-Analyse, Themes & mehr" : { "comment" : "SettingsView – Pro upsell button subtitle", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -4158,6 +4201,7 @@ }, "nahbar Max freischalten für KI-Analyse" : { "comment" : "LogbuchView – upsell button for AI analysis", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -4190,6 +4234,9 @@ } } } + }, + "nahbar Pro oder Max" : { + }, "nahbar-log.txt" : { "comment" : "The file name of the log export.", @@ -4462,6 +4509,12 @@ } } } + }, + "Nudge alle %lld Tage · Quiz abgeschlossen" : { + + }, + "Nur lokal gespeichert" : { + }, "Nur Moment löschen" : { "localizations" : { @@ -4674,6 +4727,9 @@ } } } + }, + "Persönlichkeitsquiz" : { + }, "Persönlichkeitsquiz starten" : { "localizations" : { @@ -4731,6 +4787,7 @@ } }, "Pro oder Max-Abo" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -4773,9 +4830,13 @@ } } } + }, + "Push nach dem Treffen" : { + }, "Push-Benachrichtigung nach dem Besuch" : { "comment" : "SettingsView – aftermath notification toggle subtitle", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -4838,6 +4899,7 @@ } }, "Quiz zurücksetzen" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -5124,6 +5186,9 @@ } } } + }, + "System" : { + }, "Tag" : { "comment" : "PaywallView – subscription period label (day)", @@ -5146,6 +5211,9 @@ } } } + }, + "Tägliche Erinnerung für Anrufe" : { + }, "Teile WhatsApp-Nachrichten direkt in nahbar – sie werden als Momente gespeichert." : { "comment" : "FeatureTourStep description – WhatsApp share feature", @@ -5227,6 +5295,9 @@ } } } + }, + "Termine & Geburtstage" : { + }, "Themenvorschläge" : { "comment" : "AddMomentView – conversation suggestions section title", @@ -5547,6 +5618,7 @@ }, "Über nahbar" : { "comment" : "SettingsView – about section header", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -5670,7 +5742,6 @@ } }, "Verlauf" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -5680,6 +5751,18 @@ } } }, + "Verlauf & KI Insights zu %@" : { + "comment" : "PersonDetailView – logbuch section header mit Personenname", + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "History & AI Insights on %@" + } + } + } + }, "Verlauf & KI-Analyse" : { "comment" : "PersonDetailView – logbuch section header (legacy, nicht mehr in Verwendung)", "extractionState" : "stale", @@ -5692,28 +5775,6 @@ } } }, - "Verlauf & KI Insights zu %@" : { - "comment" : "PersonDetailView – logbuch section header mit Personenname", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "History & AI Insights on %@" - } - } - } - }, - "KI Insights zu %@" : { - "comment" : "AIAnalysisSheet – navigation title mit Personenname", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "AI Insights on %@" - } - } - } - }, "Version" : { "comment" : "SettingsView – version info row label", "extractionState" : "stale", @@ -5888,9 +5949,6 @@ } } } - }, - "Vorschau Geburtstage & Termine" : { - }, "Vorschläge fehlgeschlagen" : { "comment" : "AddMomentView – conversation suggestions error title", @@ -6516,6 +6574,9 @@ } } } + }, + "Zurücksetzen" : { + }, "Zusammen essen" : { "comment" : "PersonDetailView – activity suggestion: have a meal together (group)", diff --git a/nahbar/nahbar/PersonDetailView.swift b/nahbar/nahbar/PersonDetailView.swift index dc9dadf..4963444 100644 --- a/nahbar/nahbar/PersonDetailView.swift +++ b/nahbar/nahbar/PersonDetailView.swift @@ -55,9 +55,7 @@ struct PersonDetailView: View { // Fallback wenn keine Mail-App installiert @State private var showingEmailFallback = false - @StateObject private var personalityStore = PersonalityStore.shared @StateObject private var storeManager = StoreManager.shared - @State private var activityHint: String = "" // KI-Analyse @State private var showingAIAnalysis = false @@ -422,12 +420,6 @@ struct PersonDetailView: View { .tourTarget(.addMomentButton) } - // Persönlichkeitsbasierte Vorhaben-Vorschläge (ersetzt nextStepSection) - if person.openIntentions.isEmpty, - let profile = personalityStore.profile, profile.isComplete { - intentionSuggestionButton(profile: profile) - } - if person.sortedMoments.isEmpty { Text("Noch nichts festgehalten. Dein nächstes Gespräch kann hier beginnen.") .font(.system(size: 14)) @@ -458,52 +450,6 @@ struct PersonDetailView: View { } } - // MARK: - Vorhaben-Vorschlag - - private func intentionSuggestionButton(profile: PersonalityProfile) -> some View { - let hint = activityHint.isEmpty ? refreshActivityHint(profile: profile) : activityHint - - return HStack(spacing: 0) { - Button { - showingAddMoment = true - } label: { - HStack(spacing: 6) { - Image(systemName: "brain") - .font(.system(size: 11)) - .foregroundStyle(NahbarInsightStyle.accentPetrol) - Text("Idee: \(hint)") - .font(.system(size: 13)) - .foregroundStyle(theme.contentSecondary) - .lineLimit(1) - } - .padding(.leading, 14) - .padding(.vertical, 7) - .frame(maxWidth: .infinity, alignment: .leading) - } - - // Neue Idee würfeln - Button { - activityHint = refreshActivityHint(profile: profile) - } label: { - Image(systemName: "arrow.clockwise") - .font(.system(size: 12)) - .foregroundStyle(theme.contentTertiary) - .padding(.horizontal, 12) - .padding(.vertical, 7) - } - } - } - - @discardableResult - private func refreshActivityHint(profile: PersonalityProfile) -> String { - let suggestions = PersonalityEngine.suggestedActivities( - for: profile, tag: person.tag, count: 2 - ) - let hint = suggestions.joined(separator: " oder ") - activityHint = hint - return hint - } - // MARK: - Logbuch Vorschau private let logbuchPreviewLimit = 5 diff --git a/nahbar/nahbar/PersonalityEngine.swift b/nahbar/nahbar/PersonalityEngine.swift index e1f5983..cb758ad 100644 --- a/nahbar/nahbar/PersonalityEngine.swift +++ b/nahbar/nahbar/PersonalityEngine.swift @@ -185,95 +185,6 @@ enum PersonalityEngine { } } - // MARK: - Vorhaben-Priorisierung - - /// Gibt an, welche Art von Aktivität zuerst angezeigt werden soll. - static func preferredActivityStyle(for profile: PersonalityProfile?) -> ActivityStyle { - guard let profile else { return .oneOnOne } - switch profile.level(for: .extraversion) { - case .high: return .group - case .medium: return .oneOnOne - case .low: return .oneOnOne - } - } - - /// Gibt an, ob Erlebnis-Aktivitäten hervorgehoben werden sollen. - static func highlightNovelty(for profile: PersonalityProfile?) -> Bool { - profile?.level(for: .openness) == .high - } - - /// Gibt `count` Aktivitätsvorschläge zurück, gewichtet nach Persönlichkeit und Kontakt-Tag. - /// Innerhalb gleicher Scores wird zufällig variiert – jeder Aufruf kann andere Ergebnisse liefern. - static func suggestedActivities( - for profile: PersonalityProfile?, - tag: PersonTag?, - count: Int = 2 - ) -> [String] { - let preferred = preferredActivityStyle(for: profile) - let highlightNew = highlightNovelty(for: profile) - - func score(_ s: ActivitySuggestion) -> Int { - var p = 0 - if s.style == preferred { p += 2 } - if s.isNovelty && highlightNew { p += 1 } - if let t = s.preferredTag, t == tag { p += 1 } - return p - } - - // Nach Score gruppieren, innerhalb jeder Gruppe mischen → Abwechslung - let grouped = Dictionary(grouping: activityPool) { score($0) } - var result: [String] = [] - for key in grouped.keys.sorted(by: >) { - guard result.count < count else { break } - let bucket = (grouped[key] ?? []).shuffled() - for item in bucket { - guard result.count < count else { break } - result.append(item.text) - } - } - return result - } - - // MARK: - Aktivitäts-Pool (intern, für Tests zugänglich via suggestedActivities) - - static let activityPool: [ActivitySuggestion] = [ - // ── 1:1 ────────────────────────────────────────────────────────────── - ActivitySuggestion("Kaffee trinken", style: .oneOnOne, isNovelty: false, preferredTag: nil), - ActivitySuggestion("Spazieren gehen", style: .oneOnOne, isNovelty: false, preferredTag: nil), - ActivitySuggestion("Zusammen frühstücken", style: .oneOnOne, isNovelty: false, preferredTag: nil), - ActivitySuggestion("Mittagessen", style: .oneOnOne, isNovelty: false, preferredTag: .work), - ActivitySuggestion("Auf ein Getränk treffen", style: .oneOnOne, isNovelty: false, preferredTag: nil), - ActivitySuggestion("Zusammen kochen", style: .oneOnOne, isNovelty: false, preferredTag: .family), - ActivitySuggestion("Bummeln gehen", style: .oneOnOne, isNovelty: false, preferredTag: nil), - ActivitySuggestion("Rad fahren", style: .oneOnOne, isNovelty: false, preferredTag: nil), - ActivitySuggestion("Joggen gehen", style: .oneOnOne, isNovelty: false, preferredTag: nil), - ActivitySuggestion("Picknick", style: .oneOnOne, isNovelty: false, preferredTag: nil), - ActivitySuggestion("Besuch machen", style: .oneOnOne, isNovelty: false, preferredTag: .family), - ActivitySuggestion("Gemeinsam lesen", style: .oneOnOne, isNovelty: false, preferredTag: nil), - // ── Gruppe ─────────────────────────────────────────────────────────── - ActivitySuggestion("Abendessen", style: .group, isNovelty: false, preferredTag: nil), - ActivitySuggestion("Spieleabend", style: .group, isNovelty: false, preferredTag: .friends), - ActivitySuggestion("Kino", style: .group, isNovelty: false, preferredTag: .friends), - ActivitySuggestion("Konzert oder Show", style: .group, isNovelty: false, preferredTag: nil), - ActivitySuggestion("Museum besuchen", style: .group, isNovelty: false, preferredTag: nil), - ActivitySuggestion("Wandern", style: .group, isNovelty: false, preferredTag: nil), - ActivitySuggestion("Grillabend", style: .group, isNovelty: false, preferredTag: .friends), - ActivitySuggestion("Sportevent", style: .group, isNovelty: false, preferredTag: nil), - ActivitySuggestion("Veranstaltung besuchen", style: .group, isNovelty: false, preferredTag: .community), - // ── Erlebnis ───────────────────────────────────────────────────────── - ActivitySuggestion("Etwas Neues ausprobieren", style: nil, isNovelty: true, preferredTag: nil), - ActivitySuggestion("Escape Room", style: nil, isNovelty: true, preferredTag: .friends), - ActivitySuggestion("Kochkurs", style: nil, isNovelty: true, preferredTag: nil), - ActivitySuggestion("Weinprobe oder Tasting", style: nil, isNovelty: true, preferredTag: nil), - ActivitySuggestion("Kletterpark", style: nil, isNovelty: true, preferredTag: .friends), - ActivitySuggestion("Workshop besuchen", style: nil, isNovelty: true, preferredTag: .community), - ActivitySuggestion("Karaoke", style: nil, isNovelty: true, preferredTag: .friends), - // ── Einfach / Remote ───────────────────────────────────────────────── - ActivitySuggestion("Anrufen", style: nil, isNovelty: false, preferredTag: nil), - ActivitySuggestion("Nachricht schicken", style: nil, isNovelty: false, preferredTag: nil), - ActivitySuggestion("Artikel oder Tipp teilen", style: nil, isNovelty: false, preferredTag: nil), - ] - // MARK: - Intervall-Empfehlung für Einstellungen /// Gibt den empfohlenen Benachrichtigungs-Intervall für das Einstellungsmenü zurück. @@ -301,23 +212,4 @@ enum RatingPromptTiming { case delayed(seconds: Int, copy: String?) } -/// Präferierter Aktivitätsstil für Vorhaben-Vorschläge. -enum ActivityStyle { - case group - case oneOnOne -} -/// Ein einzelner Aktivitätsvorschlag aus dem Pool. -struct ActivitySuggestion { - let text: String - let style: ActivityStyle? - let isNovelty: Bool - let preferredTag: PersonTag? - - init(_ text: String, style: ActivityStyle?, isNovelty: Bool, preferredTag: PersonTag?) { - self.text = text - self.style = style - self.isNovelty = isNovelty - self.preferredTag = preferredTag - } -} diff --git a/nahbar/nahbarTests/NahbarPersonalityTests.swift b/nahbar/nahbarTests/NahbarPersonalityTests.swift index 262da42..930454f 100644 --- a/nahbar/nahbarTests/NahbarPersonalityTests.swift +++ b/nahbar/nahbarTests/NahbarPersonalityTests.swift @@ -416,79 +416,6 @@ struct PersonalityEngineBehaviorTests { } } - @Test("Hohe Offenheit → highlightNovelty true") - func highOpennessHighlightsNovelty() { - let p = profile(o: .high) - #expect(PersonalityEngine.highlightNovelty(for: p)) - } - - @Test("Niedrige Offenheit → highlightNovelty false") - func lowOpennessDoesNotHighlightNovelty() { - let p = profile(o: .low) - #expect(!PersonalityEngine.highlightNovelty(for: p)) - } -} - -// MARK: - suggestedActivities Tests - -@Suite("PersonalityEngine – suggestedActivities") -struct SuggestedActivitiesTests { - - @Test("Gibt genau count Elemente zurück") - func returnsRequestedCount() { - let result = PersonalityEngine.suggestedActivities(for: nil, tag: nil, count: 2) - #expect(result.count == 2) - } - - @Test("count: 1 → genau ein Vorschlag") - func countOne() { - let result = PersonalityEngine.suggestedActivities(for: nil, tag: nil, count: 1) - #expect(result.count == 1) - } - - @Test("Alle zurückgegebenen Texte stammen aus dem Pool") - func resultsAreFromPool() { - let poolTexts = Set(PersonalityEngine.activityPool.map(\.text)) - let result = PersonalityEngine.suggestedActivities(for: nil, tag: nil, count: 5) - for text in result { - #expect(poolTexts.contains(text), "'\(text)' nicht im Pool") - } - } - - @Test("Pool hat mindestens 20 Einträge") - func poolIsSufficient() { - #expect(PersonalityEngine.activityPool.count >= 20) - } - - @Test("Keine Duplikate in einem Ergebnis") - func noDuplicates() { - let result = PersonalityEngine.suggestedActivities(for: nil, tag: nil, count: 5) - #expect(result.count == Set(result).count) - } - - @Test("Ergebnis ist nicht leer wenn Pool vorhanden") - func notEmptyWhenPoolExists() { - let result = PersonalityEngine.suggestedActivities(for: nil, tag: nil, count: 2) - #expect(!result.isEmpty) - } - - @Test("Pool enthält Erlebnis-Aktivitäten (isNovelty)") - func poolContainsNoveltyActivities() { - #expect(PersonalityEngine.activityPool.contains { $0.isNovelty }) - } - - @Test("Pool enthält 1:1 und Gruppen-Aktivitäten") - func poolContainsBothStyles() { - #expect(PersonalityEngine.activityPool.contains { $0.style == .oneOnOne }) - #expect(PersonalityEngine.activityPool.contains { $0.style == .group }) - } - - @Test("Pool enthält Tag-spezifische Aktivitäten") - func poolContainsTagSpecificActivities() { - #expect(PersonalityEngine.activityPool.contains { $0.preferredTag == .friends }) - #expect(PersonalityEngine.activityPool.contains { $0.preferredTag == .family }) - #expect(PersonalityEngine.activityPool.contains { $0.preferredTag == .work }) - } } // MARK: - GenderSelectionScreen Skip-Logik