Files
hope-voice-api/ios/Sources/LiveKit/TranscriptHistory.swift
T
2026-10-04 07:23:49 -04:00

54 lines
2.1 KiB
Swift

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_isolated_tests"
private let storageKey: String
static let maxEntries = 50
init(origin: String? = nil, userID: String? = nil) {
// Length-prefixed parts prevent ambiguous origin/account combinations.
storageKey = origin.flatMap { origin in userID.map { "voice_history_v2:\(origin.utf8.count):\(origin):\($0)" } } ?? Self.key
if let raw = AppPreferences.store.string(forKey: storageKey),
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: storageKey)
}
}