Track usage independent of conversation save state; fix Analytics toolbar button

Conversation text is still only persisted when explicitly saved (⌘S),
but tokens/cost/model/provider are now logged to a new usage_events
table for every completed AI response regardless — the Analytics
view now reads from this table instead of messages, so it reflects
real usage even for conversations that were never saved. Adds a
By Provider chart mode alongside Over Time/By Model.

Also fixes the Analytics entry point: ToolbarItem(placement: .navigation)
silently doesn't render in a plain .sheet-presented NavigationStack on
macOS. Moved the button inline next to the segmented picker, matching
the codebase's existing convention (e.g. the model-favorites star filter).
This commit is contained in:
2026-08-12 13:49:12 +02:00
parent d460230158
commit 30e18f92a5
7 changed files with 514 additions and 17 deletions
+41
View File
@@ -108,6 +108,47 @@ struct ModelUsageStat: Identifiable, Sendable {
}
}
/// Usage grouped by provider (openrouter/anthropic/openai/ollama/appleOnDevice), from `usage_events`.
struct ProviderUsageStat: Identifiable, Sendable {
var id: String { provider }
let provider: String
var questionCount: Int
var totalTokens: Int
var totalCost: Double
var hasCostData: Bool
var lastUsed: Date
nonisolated init(
provider: String,
questionCount: Int,
totalTokens: Int,
totalCost: Double,
hasCostData: Bool,
lastUsed: Date
) {
self.provider = provider
self.questionCount = questionCount
self.totalTokens = totalTokens
self.totalCost = totalCost
self.hasCostData = hasCostData
self.lastUsed = lastUsed
}
var totalTokensDisplay: String {
if totalTokens >= 1_000_000 {
return String(format: "%.1fM", Double(totalTokens) / 1_000_000)
} else if totalTokens >= 1000 {
return String(format: "%.1fK", Double(totalTokens) / 1000)
} else {
return "\(totalTokens)"
}
}
var totalCostDisplay: String {
hasCostData ? String(format: "$%.4f", totalCost) : "N/A"
}
}
/// One day's usage totals, for time-series charting in the Analytics view.
struct DailyUsageStat: Identifiable, Sendable {
var id: Date { day }