import Foundation struct HistoryEntry: Codable, Equatable { let role: String let text: String let timestamp: Date } /// Last 50 final transcript entries persisted as JSON in UserDefaults. /// /// Persistence-only store. The visible transcript is the single canonical /// collection owned by `TranscriptHandler.events`; this class provides the /// durable snapshot it initializes from and syncs to. It performs no dedupe — /// all suppression is source-aware and time-bounded in the handler. @MainActor final class TranscriptHistory: ObservableObject { @Published private(set) var entries: [HistoryEntry] = [] static let key = "voice_history" static let maxEntries = 50 init() { if let raw = AppPreferences.store.string(forKey: Self.key), let data = raw.data(using: .utf8), let decoded = try? JSONDecoder().decode([HistoryEntry].self, from: data) { entries = Array(decoded.suffix(Self.maxEntries)) } } /// Replaces the persisted snapshot with exactly the finals currently in the /// canonical visible collection. Called by `TranscriptHandler` after every /// change so persistence and the UI never diverge. No dedupe is applied here: /// same-source repeats are legitimate entries. func sync(finals: [(role: String, text: String, timestamp: Date)]) { entries = Array(finals.suffix(Self.maxEntries).map { HistoryEntry(role: $0.role, text: $0.text, timestamp: $0.timestamp) }) persist() } func clear() { entries.removeAll() persist() } private func persist() { guard let data = try? JSONEncoder().encode(entries) else { return } AppPreferences.store.set(String(decoding: data, as: UTF8.self), forKey: Self.key) } }