Add Usage Analytics view: tokens/questions/cost over time and by model
New large modal off the Stats sheet with a 6-way timeframe picker (Today/7 Days/Week/Month/Year/Total), three tappable summary tiles, and native Swift Charts (bar chart over time, pie chart by model). Backed by date-range-filtered DatabaseService queries plus a new daily-bucketed query — no schema changes needed since messages already carry timestamp/tokens/cost/role.
This commit is contained in:
@@ -25,6 +25,7 @@ import Foundation
|
||||
|
||||
struct UsageStats: Sendable {
|
||||
var totalMessages: Int
|
||||
var totalQuestions: Int
|
||||
var totalTokens: Int
|
||||
var totalCost: Double
|
||||
var hasCostData: Bool
|
||||
@@ -33,6 +34,7 @@ struct UsageStats: Sendable {
|
||||
|
||||
nonisolated init(
|
||||
totalMessages: Int = 0,
|
||||
totalQuestions: Int = 0,
|
||||
totalTokens: Int = 0,
|
||||
totalCost: Double = 0.0,
|
||||
hasCostData: Bool = false,
|
||||
@@ -40,6 +42,7 @@ struct UsageStats: Sendable {
|
||||
lastMessageDate: Date? = nil
|
||||
) {
|
||||
self.totalMessages = totalMessages
|
||||
self.totalQuestions = totalQuestions
|
||||
self.totalTokens = totalTokens
|
||||
self.totalCost = totalCost
|
||||
self.hasCostData = hasCostData
|
||||
@@ -66,6 +69,7 @@ struct ModelUsageStat: Identifiable, Sendable {
|
||||
var id: String { modelId }
|
||||
let modelId: String
|
||||
var messageCount: Int
|
||||
var questionCount: Int
|
||||
var totalTokens: Int
|
||||
var totalCost: Double
|
||||
var hasCostData: Bool
|
||||
@@ -74,6 +78,7 @@ struct ModelUsageStat: Identifiable, Sendable {
|
||||
nonisolated init(
|
||||
modelId: String,
|
||||
messageCount: Int,
|
||||
questionCount: Int = 0,
|
||||
totalTokens: Int,
|
||||
totalCost: Double,
|
||||
hasCostData: Bool,
|
||||
@@ -81,6 +86,7 @@ struct ModelUsageStat: Identifiable, Sendable {
|
||||
) {
|
||||
self.modelId = modelId
|
||||
self.messageCount = messageCount
|
||||
self.questionCount = questionCount
|
||||
self.totalTokens = totalTokens
|
||||
self.totalCost = totalCost
|
||||
self.hasCostData = hasCostData
|
||||
@@ -102,6 +108,33 @@ struct ModelUsageStat: Identifiable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
/// One day's usage totals, for time-series charting in the Analytics view.
|
||||
struct DailyUsageStat: Identifiable, Sendable {
|
||||
var id: Date { day }
|
||||
let day: Date
|
||||
var messageCount: Int
|
||||
var questionCount: Int
|
||||
var totalTokens: Int
|
||||
var totalCost: Double
|
||||
var hasCostData: Bool
|
||||
|
||||
nonisolated init(
|
||||
day: Date,
|
||||
messageCount: Int,
|
||||
questionCount: Int,
|
||||
totalTokens: Int,
|
||||
totalCost: Double,
|
||||
hasCostData: Bool
|
||||
) {
|
||||
self.day = day
|
||||
self.messageCount = messageCount
|
||||
self.questionCount = questionCount
|
||||
self.totalTokens = totalTokens
|
||||
self.totalCost = totalCost
|
||||
self.hasCostData = hasCostData
|
||||
}
|
||||
}
|
||||
|
||||
struct ConversationUsageStat: Identifiable, Sendable {
|
||||
let conversationId: UUID
|
||||
var id: UUID { conversationId }
|
||||
@@ -141,3 +174,33 @@ struct ConversationUsageStat: Identifiable, Sendable {
|
||||
hasCostData ? String(format: "$%.4f", totalCost) : "N/A"
|
||||
}
|
||||
}
|
||||
|
||||
/// Time window for the Analytics view. `.total` means all-time (no lower bound).
|
||||
enum AnalyticsTimeframe: String, CaseIterable, Identifiable, Sendable {
|
||||
case today
|
||||
case last7Days
|
||||
case week
|
||||
case month
|
||||
case year
|
||||
case total
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
/// Bounds for this timeframe. `start` is nil for `.total` (all time, no lower bound).
|
||||
nonisolated func dateRange(now: Date = Date(), calendar: Calendar = .current) -> (start: Date?, end: Date) {
|
||||
switch self {
|
||||
case .today:
|
||||
return (calendar.startOfDay(for: now), now)
|
||||
case .last7Days:
|
||||
return (calendar.date(byAdding: .day, value: -7, to: now) ?? now, now)
|
||||
case .week:
|
||||
return (calendar.dateInterval(of: .weekOfYear, for: now)?.start ?? now, now)
|
||||
case .month:
|
||||
return (calendar.dateInterval(of: .month, for: now)?.start ?? now, now)
|
||||
case .year:
|
||||
return (calendar.dateInterval(of: .year, for: now)?.start ?? now, now)
|
||||
case .total:
|
||||
return (nil, now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user