Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 87 additions & 10 deletions Wave/Services/HistoryManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import Foundation
@Observable
final class HistoryManager {
private(set) var records: [TranscriptionRecord] = []
private var usageStats = UsageStats()

init() { load() }
init() {
load()
backfillUsageFromRecords()
}

func add(_ text: String) {
let record = TranscriptionRecord(id: UUID(), text: text, date: Date())
records.insert(record, at: 0)
if records.count > 50 { records = Array(records.prefix(50)) }
usageStats.record(words: record.wordCount, at: record.date)
save()
saveUsage()
}

func remove(_ id: UUID) {
Expand All @@ -24,14 +30,75 @@ final class HistoryManager {
}

var wordsToday: Int {
records.filter { Calendar.current.isDateInToday($0.date) }
let key = UsageStats.dayKey(for: Date())
return usageStats.dailyWords[key] ?? records.filter { Calendar.current.isDateInToday($0.date) }
.reduce(0) { $0 + $1.wordCount }
}

var wordsThisWeek: Int {
let start = Calendar.current.date(byAdding: .day, value: -7, to: Date())!
return records.filter { $0.date >= start }
.reduce(0) { $0 + $1.wordCount }
let calendar = Calendar.current
let start = calendar.date(byAdding: .day, value: -6, to: calendar.startOfDay(for: Date()))!
return usageStats.dailyWords.reduce(0) { total, entry in
guard let date = Self.date(fromDayKey: entry.key), date >= start else { return total }
return total + entry.value
}
}

var totalWords: Int { usageStats.totalWords }

var estimatedTimeSavedSeconds: Int { usageStats.estimatedTimeSavedSeconds }

var activityHeatmap: [[Int]] {
var grid = Array(repeating: Array(repeating: 0, count: 24), count: 7)
for weekday in 1...7 {
for hour in 0..<24 {
let key = UsageStats.gridKey(weekday: weekday, hour: hour)
grid[weekday - 1][hour] = usageStats.hourGrid[key] ?? 0
}
}
return grid
}

var activityHeatmapMax: Int {
activityHeatmap.flatMap { $0 }.max() ?? 0
}

var wordsLast7Days: [Int] {
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
return (0..<7).reversed().map { offset in
guard let date = calendar.date(byAdding: .day, value: -offset, to: today) else { return 0 }
return usageStats.dailyWords[UsageStats.dayKey(for: date)] ?? 0
}
}

var wordsLast7DayLabels: [String] {
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
let formatter = DateFormatter()
formatter.dateFormat = "EEE"
return (0..<7).reversed().map { offset in
guard let date = calendar.date(byAdding: .day, value: -offset, to: today) else { return "" }
return formatter.string(from: date)
}
}

var wordsLast7DaysMax: Int {
wordsLast7Days.max() ?? 0
}

private func backfillUsageFromRecords() {
guard usageStats.totalWords == 0, !records.isEmpty else { return }
for record in records {
usageStats.record(words: record.wordCount, at: record.date)
}
saveUsage()
}

private static func date(fromDayKey key: String) -> Date? {
let parts = key.split(separator: "-").compactMap { Int($0) }
guard parts.count == 3 else { return nil }
return Calendar.current.date(from: DateComponents(year: parts[0], month: parts[1], day: parts[2]))
}

private func save() {
Expand All @@ -41,9 +108,19 @@ final class HistoryManager {
}

private func load() {
guard let data = UserDefaults.standard.data(forKey: "transcriptionHistory"),
let decoded = try? JSONDecoder().decode([TranscriptionRecord].self, from: data)
else { return }
records = decoded
if let data = UserDefaults.standard.data(forKey: "transcriptionHistory"),
let decoded = try? JSONDecoder().decode([TranscriptionRecord].self, from: data) {
records = decoded
}
if let data = UserDefaults.standard.data(forKey: "usageStats"),
let decoded = try? JSONDecoder().decode(UsageStats.self, from: data) {
usageStats = decoded
}
}

private func saveUsage() {
if let data = try? JSONEncoder().encode(usageStats) {
UserDefaults.standard.set(data, forKey: "usageStats")
}
}
}
}
37 changes: 37 additions & 0 deletions Wave/Services/UsageStats.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation

struct UsageStats: Codable {
var hourGrid: [String: Int] = [:]
var dailyWords: [String: Int] = [:]
var totalWords: Int = 0

static let typingWPM = 40.0
static let dictationWPM = 140.0

static func gridKey(weekday: Int, hour: Int) -> String {
"\(weekday)-\(hour)"
}

static func dayKey(for date: Date, calendar: Calendar = .current) -> String {
let components = calendar.dateComponents([.year, .month, .day], from: date)
return String(format: "%04d-%02d-%02d", components.year!, components.month!, components.day!)
}

var estimatedTimeSavedSeconds: Int {
let typingSecondsPerWord = 60.0 / Self.typingWPM
let dictationSecondsPerWord = 60.0 / Self.dictationWPM
let saved = Double(totalWords) * (typingSecondsPerWord - dictationSecondsPerWord)
return max(0, Int(saved.rounded()))
}

mutating func record(words: Int, at date: Date, calendar: Calendar = .current) {
guard words > 0 else { return }
let weekday = calendar.component(.weekday, from: date)
let hour = calendar.component(.hour, from: date)
let gridKey = Self.gridKey(weekday: weekday, hour: hour)
hourGrid[gridKey, default: 0] += words
let dayKey = Self.dayKey(for: date, calendar: calendar)
dailyWords[dayKey, default: 0] += words
totalWords += words
}
}
Loading