Files
OSGKeyboard/OSGKeyboardShared/Models/UsageStatistics.swift
Rocky 3de665d254 feat(keyboard): ship AI mode surface with streaming search answers
Add the AI keyboard tab, Agent settings, and user-owned LLM key path for 1.7.0, including streaming answers and web-search transports without the built-in DeepSeek fallback.
2026-08-11 01:06:27 +08:00

124 lines
4.6 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// UsageStatistics.swift
// OSGKeyboard · Shared
//
// Cumulative dictation metrics shown on the home / dashboard stats cards.
// Mirrored through iCloud KVS when settings sync is enabled.
import Foundation
public struct UsageStatistics: Codable, Equatable, Sendable {
public var updatedAt: Date
public var dictationDurationSeconds: TimeInterval
public var dictationCharacterCount: Int
public var translationCharacterCount: Int
public var aiCharacterCount: Int
public init(
updatedAt: Date = Date(),
dictationDurationSeconds: TimeInterval = 0,
dictationCharacterCount: Int = 0,
translationCharacterCount: Int = 0,
aiCharacterCount: Int = 0
) {
self.updatedAt = updatedAt
self.dictationDurationSeconds = dictationDurationSeconds
self.dictationCharacterCount = dictationCharacterCount
self.translationCharacterCount = translationCharacterCount
self.aiCharacterCount = aiCharacterCount
}
public static let zero = UsageStatistics(updatedAt: .distantPast)
public var totalInputCharacterCount: Int {
dictationCharacterCount + translationCharacterCount + aiCharacterCount
}
/// Combine lifetime totals from two devices. After merge, each device
/// continues accumulating locally so `max` converges to the union.
public static func merge(local: UsageStatistics, remote: UsageStatistics) -> UsageStatistics {
UsageStatistics(
updatedAt: max(local.updatedAt, remote.updatedAt),
dictationDurationSeconds: max(local.dictationDurationSeconds, remote.dictationDurationSeconds),
dictationCharacterCount: max(local.dictationCharacterCount, remote.dictationCharacterCount),
translationCharacterCount: max(local.translationCharacterCount, remote.translationCharacterCount),
aiCharacterCount: max(local.aiCharacterCount, remote.aiCharacterCount)
)
}
private enum CodingKeys: String, CodingKey {
case updatedAt
case dictationDurationSeconds
case dictationCharacterCount
case translationCharacterCount
case aiCharacterCount
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
updatedAt = try container.decode(Date.self, forKey: .updatedAt)
dictationDurationSeconds = try container.decodeIfPresent(
TimeInterval.self,
forKey: .dictationDurationSeconds
) ?? 0
dictationCharacterCount = try container.decodeIfPresent(
Int.self,
forKey: .dictationCharacterCount
) ?? 0
translationCharacterCount = try container.decodeIfPresent(
Int.self,
forKey: .translationCharacterCount
) ?? 0
aiCharacterCount = try container.decodeIfPresent(
Int.self,
forKey: .aiCharacterCount
) ?? 0
}
}
public enum UsageStatisticsStorage {
public static let storageKey = "usageStatistics.v1"
/// Legacy macOS dashboard counter (word split); migrated on first load.
public static let legacyMacTotalWordsKey = "mac.totalWords"
/// PreApp Group iOS storage in `UserDefaults.standard`.
public static let legacyStandardDefaultsKey = "usageStatistics.v1"
public static func load(from defaults: UserDefaults) -> UsageStatistics {
if let data = defaults.data(forKey: storageKey),
let stats = try? JSONDecoder().decode(UsageStatistics.self, from: data) {
return stats
}
return .zero
}
public static func save(_ stats: UsageStatistics, to defaults: UserDefaults) {
guard let data = try? JSONEncoder().encode(stats) else { return }
defaults.set(data, forKey: storageKey)
}
/// One-time imports from older per-platform keys.
public static func migrateLegacyIfNeeded(into defaults: UserDefaults) -> UsageStatistics {
var stats = load(from: defaults)
guard stats == .zero else { return stats }
let legacyWords = defaults.integer(forKey: legacyMacTotalWordsKey)
if legacyWords > 0 {
stats.dictationCharacterCount = legacyWords
stats.updatedAt = Date()
save(stats, to: defaults)
return stats
}
#if os(iOS)
if let data = UserDefaults.standard.data(forKey: legacyStandardDefaultsKey),
let legacy = try? JSONDecoder().decode(UsageStatistics.self, from: data),
legacy != .zero {
stats = legacy
stats.updatedAt = Date()
save(stats, to: defaults)
}
#endif
return stats
}
}