Add all-time usage statistics tab
Extends the existing Stats sheet (⌘⇧S) with a Session/All-Time segmented picker. All-Time aggregates tokens, cost, and message counts across every saved conversation, broken down by model and by conversation, using the modelId already stored per message — no schema change needed.
This commit is contained in:
@@ -576,6 +576,118 @@ final class DatabaseService: Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Usage Statistics
|
||||
|
||||
nonisolated func getOverallUsageStats() throws -> UsageStats {
|
||||
try dbQueue.read { db in
|
||||
guard let row = try Row.fetchOne(db, sql: """
|
||||
SELECT COUNT(*) AS cnt,
|
||||
COALESCE(SUM(tokens), 0) AS tokens,
|
||||
COALESCE(SUM(cost), 0) AS cost,
|
||||
COUNT(cost) AS costCount,
|
||||
MIN(timestamp) AS minTs,
|
||||
MAX(timestamp) AS maxTs
|
||||
FROM messages
|
||||
""")
|
||||
else {
|
||||
return UsageStats()
|
||||
}
|
||||
|
||||
let costCount: Int = row["costCount"]
|
||||
let minTs: String? = row["minTs"]
|
||||
let maxTs: String? = row["maxTs"]
|
||||
|
||||
return UsageStats(
|
||||
totalMessages: row["cnt"],
|
||||
totalTokens: row["tokens"],
|
||||
totalCost: row["cost"],
|
||||
hasCostData: costCount > 0,
|
||||
firstMessageDate: minTs.flatMap { Self.isoDate(from: $0) },
|
||||
lastMessageDate: maxTs.flatMap { Self.isoDate(from: $0) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func getUsageByModel() throws -> [ModelUsageStat] {
|
||||
try dbQueue.read { db in
|
||||
let rows = try Row.fetchAll(db, sql: """
|
||||
SELECT modelId,
|
||||
COUNT(*) AS cnt,
|
||||
COALESCE(SUM(tokens), 0) AS tokens,
|
||||
COALESCE(SUM(cost), 0) AS cost,
|
||||
COUNT(cost) AS costCount,
|
||||
MAX(timestamp) AS lastUsed
|
||||
FROM messages
|
||||
WHERE modelId IS NOT NULL
|
||||
GROUP BY modelId
|
||||
""")
|
||||
|
||||
let stats: [ModelUsageStat] = rows.compactMap { row in
|
||||
guard let modelId: String = row["modelId"],
|
||||
let lastUsedString: String = row["lastUsed"],
|
||||
let lastUsed = Self.isoDate(from: lastUsedString)
|
||||
else { return nil }
|
||||
|
||||
let costCount: Int = row["costCount"]
|
||||
return ModelUsageStat(
|
||||
modelId: modelId,
|
||||
messageCount: row["cnt"],
|
||||
totalTokens: row["tokens"],
|
||||
totalCost: row["cost"],
|
||||
hasCostData: costCount > 0,
|
||||
lastUsed: lastUsed
|
||||
)
|
||||
}
|
||||
|
||||
return stats.sorted { lhs, rhs in
|
||||
if lhs.hasCostData || rhs.hasCostData {
|
||||
return lhs.totalCost > rhs.totalCost
|
||||
}
|
||||
return lhs.totalTokens > rhs.totalTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func getUsageByConversation(limit: Int = 20) throws -> [ConversationUsageStat] {
|
||||
try dbQueue.read { db in
|
||||
let rows = try Row.fetchAll(db, sql: """
|
||||
SELECT m.conversationId AS conversationId,
|
||||
c.name AS name,
|
||||
COUNT(*) AS cnt,
|
||||
COALESCE(SUM(m.tokens), 0) AS tokens,
|
||||
COALESCE(SUM(m.cost), 0) AS cost,
|
||||
COUNT(m.cost) AS costCount
|
||||
FROM messages m
|
||||
JOIN conversations c ON m.conversationId = c.id
|
||||
GROUP BY m.conversationId
|
||||
""")
|
||||
|
||||
let stats: [ConversationUsageStat] = rows.compactMap { row in
|
||||
guard let conversationIdString: String = row["conversationId"],
|
||||
let conversationId = UUID(uuidString: conversationIdString)
|
||||
else { return nil }
|
||||
|
||||
let costCount: Int = row["costCount"]
|
||||
return ConversationUsageStat(
|
||||
conversationId: conversationId,
|
||||
name: row["name"],
|
||||
messageCount: row["cnt"],
|
||||
totalTokens: row["tokens"],
|
||||
totalCost: row["cost"],
|
||||
hasCostData: costCount > 0
|
||||
)
|
||||
}
|
||||
|
||||
let sorted = stats.sorted { lhs, rhs in
|
||||
if lhs.hasCostData || rhs.hasCostData {
|
||||
return lhs.totalCost > rhs.totalCost
|
||||
}
|
||||
return lhs.totalTokens > rhs.totalTokens
|
||||
}
|
||||
return Array(sorted.prefix(limit))
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func deleteConversation(id: UUID) throws -> Bool {
|
||||
Log.db.info("Deleting conversation \(id.uuidString)")
|
||||
return try dbQueue.write { db in
|
||||
|
||||
Reference in New Issue
Block a user