Fix #20: Aktivitätsvorschläge-Feature entfernt

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 <noreply@anthropic.com>
This commit is contained in:
2026-04-23 13:03:18 +02:00
parent 9e932f0f2c
commit 31a7a2d5df
4 changed files with 90 additions and 264 deletions
+90 -29
View File
@@ -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)",
-54
View File
@@ -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
-108
View File
@@ -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
}
}
@@ -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