1use crate::database::{ChatEntry, Database};
2use anyhow::Result;
3use chrono::{DateTime, Datelike, Duration, Utc};
4use colored::Colorize;
5use std::collections::HashMap;
6
7#[derive(Debug, Clone)]
8pub struct UsageStats {
9 pub total_tokens: u64,
10 pub total_requests: u64,
11 pub input_tokens: u64,
12 pub output_tokens: u64,
13 pub model_usage: Vec<(String, u64, u64)>, pub daily_usage: Vec<(String, u64, u64)>, pub weekly_usage: Vec<(String, u64, u64)>, pub monthly_usage: Vec<(String, u64, u64)>, pub yearly_usage: Vec<(String, u64, u64)>, pub date_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
19}
20
21#[derive(Debug, Clone)]
22#[allow(dead_code)]
23pub enum TimeFrame {
24 Daily,
25 Weekly,
26 Monthly,
27 Yearly,
28}
29
30pub struct UsageAnalyzer {
31 db: Database,
32}
33
34impl UsageAnalyzer {
35 pub fn new() -> Result<Self> {
36 Ok(Self {
37 db: Database::new()?,
38 })
39 }
40
41 pub fn get_usage_stats(&self, days_back: Option<u32>) -> Result<UsageStats> {
42 let entries = if let Some(days) = days_back {
43 let cutoff_date = Utc::now() - Duration::days(days as i64);
44 self.get_entries_since(cutoff_date)?
45 } else {
46 self.db.get_all_logs()?
47 };
48
49 if entries.is_empty() {
50 return Ok(UsageStats {
51 total_tokens: 0,
52 total_requests: 0,
53 input_tokens: 0,
54 output_tokens: 0,
55 model_usage: Vec::new(),
56 daily_usage: Vec::new(),
57 weekly_usage: Vec::new(),
58 monthly_usage: Vec::new(),
59 yearly_usage: Vec::new(),
60 date_range: None,
61 });
62 }
63
64 let mut total_input_tokens = 0u64;
65 let mut total_output_tokens = 0u64;
66 let mut model_stats: HashMap<String, (u64, u64)> = HashMap::new(); let mut daily_stats: HashMap<String, (u64, u64)> = HashMap::new();
68 let mut weekly_stats: HashMap<String, (u64, u64)> = HashMap::new();
69 let mut monthly_stats: HashMap<String, (u64, u64)> = HashMap::new();
70 let mut yearly_stats: HashMap<String, (u64, u64)> = HashMap::new();
71
72 let mut earliest_date = entries[0].timestamp;
73 let mut latest_date = entries[0].timestamp;
74
75 for entry in &entries {
76 if entry.timestamp < earliest_date {
78 earliest_date = entry.timestamp;
79 }
80 if entry.timestamp > latest_date {
81 latest_date = entry.timestamp;
82 }
83
84 let input_tokens = entry.input_tokens.unwrap_or(0) as u64;
86 let output_tokens = entry.output_tokens.unwrap_or(0) as u64;
87 let total_entry_tokens = input_tokens + output_tokens;
88
89 total_input_tokens += input_tokens;
90 total_output_tokens += output_tokens;
91
92 let model_entry = model_stats.entry(entry.model.clone()).or_insert((0, 0));
94 model_entry.0 += 1; model_entry.1 += total_entry_tokens; let date = entry.timestamp.date_naive();
99 let daily_key = date.format("%Y-%m-%d").to_string();
100 let daily_entry = daily_stats.entry(daily_key).or_insert((0, 0));
101 daily_entry.0 += 1;
102 daily_entry.1 += total_entry_tokens;
103
104 let year = entry.timestamp.year();
106 let week = entry.timestamp.iso_week().week();
107 let weekly_key = format!("{}-W{:02}", year, week);
108 let weekly_entry = weekly_stats.entry(weekly_key).or_insert((0, 0));
109 weekly_entry.0 += 1;
110 weekly_entry.1 += total_entry_tokens;
111
112 let monthly_key = date.format("%Y-%m").to_string();
114 let monthly_entry = monthly_stats.entry(monthly_key).or_insert((0, 0));
115 monthly_entry.0 += 1;
116 monthly_entry.1 += total_entry_tokens;
117
118 let yearly_key = year.to_string();
120 let yearly_entry = yearly_stats.entry(yearly_key).or_insert((0, 0));
121 yearly_entry.0 += 1;
122 yearly_entry.1 += total_entry_tokens;
123 }
124
125 let mut model_usage: Vec<(String, u64, u64)> = model_stats
127 .into_iter()
128 .map(|(model, (requests, tokens))| (model, requests, tokens))
129 .collect();
130 model_usage.sort_by(|a, b| b.2.cmp(&a.2)); let mut daily_usage: Vec<(String, u64, u64)> = daily_stats
133 .into_iter()
134 .map(|(date, (requests, tokens))| (date, requests, tokens))
135 .collect();
136 daily_usage.sort_by(|a, b| a.0.cmp(&b.0)); let mut weekly_usage: Vec<(String, u64, u64)> = weekly_stats
139 .into_iter()
140 .map(|(week, (requests, tokens))| (week, requests, tokens))
141 .collect();
142 weekly_usage.sort_by(|a, b| a.0.cmp(&b.0));
143
144 let mut monthly_usage: Vec<(String, u64, u64)> = monthly_stats
145 .into_iter()
146 .map(|(month, (requests, tokens))| (month, requests, tokens))
147 .collect();
148 monthly_usage.sort_by(|a, b| a.0.cmp(&b.0));
149
150 let mut yearly_usage: Vec<(String, u64, u64)> = yearly_stats
151 .into_iter()
152 .map(|(year, (requests, tokens))| (year, requests, tokens))
153 .collect();
154 yearly_usage.sort_by(|a, b| a.0.cmp(&b.0));
155
156 Ok(UsageStats {
157 total_tokens: total_input_tokens + total_output_tokens,
158 total_requests: entries.len() as u64,
159 input_tokens: total_input_tokens,
160 output_tokens: total_output_tokens,
161 model_usage,
162 daily_usage,
163 weekly_usage,
164 monthly_usage,
165 yearly_usage,
166 date_range: Some((earliest_date, latest_date)),
167 })
168 }
169
170 fn get_entries_since(&self, cutoff_date: DateTime<Utc>) -> Result<Vec<ChatEntry>> {
171 let all_entries = self.db.get_all_logs()?;
174 Ok(all_entries
175 .into_iter()
176 .filter(|entry| entry.timestamp >= cutoff_date)
177 .collect())
178 }
179}
180
181pub struct BarChart;
182
183impl BarChart {
184 pub fn render_horizontal(
185 title: &str,
186 data: &[(String, u64, u64)],
187 value_type: &str, max_width: usize,
189 max_items: usize,
190 ) {
191 if data.is_empty() {
192 println!("{} No data available", "ℹ️".blue());
193 return;
194 }
195
196 println!("\n{}", title.bold().blue());
197
198 let display_data: Vec<_> = data.iter().take(max_items).collect();
199 let max_value = display_data
200 .iter()
201 .map(|(_, requests, tokens)| {
202 if value_type == "tokens" {
203 *tokens
204 } else {
205 *requests
206 }
207 })
208 .max()
209 .unwrap_or(1);
210
211 let max_label_width = display_data
212 .iter()
213 .map(|(label, _, _)| label.len())
214 .max()
215 .unwrap_or(10);
216
217 for (label, requests, tokens) in display_data {
218 let value = if value_type == "tokens" {
219 *tokens
220 } else {
221 *requests
222 };
223 let bar_width = if max_value > 0 {
224 ((value as f64 / max_value as f64) * max_width as f64) as usize
225 } else {
226 0
227 };
228
229 let bar = "█".repeat(bar_width);
230 let formatted_value = if value_type == "tokens" {
231 Self::format_tokens(*tokens)
232 } else {
233 format!("{}", requests)
234 };
235
236 println!(
237 " {:width$} │{:bar_width$} {} ({})",
238 label.bold(),
239 bar.green(),
240 formatted_value.yellow(),
241 if value_type == "tokens" {
242 format!("{} req", requests)
243 } else {
244 Self::format_tokens(*tokens)
245 },
246 width = max_label_width,
247 bar_width = max_width
248 );
249 }
250 }
251
252 pub fn render_time_series(
253 title: &str,
254 data: &[(String, u64, u64)],
255 value_type: &str,
256 max_width: usize,
257 max_items: usize,
258 ) {
259 if data.is_empty() {
260 println!("{} No data available", "ℹ️".blue());
261 return;
262 }
263
264 println!("\n{}", title.bold().blue());
265
266 let display_data: Vec<_> = data.iter().rev().take(max_items).rev().collect();
267 let max_value = display_data
268 .iter()
269 .map(|(_, requests, tokens)| {
270 if value_type == "tokens" {
271 *tokens
272 } else {
273 *requests
274 }
275 })
276 .max()
277 .unwrap_or(1);
278
279 let max_label_width = display_data
280 .iter()
281 .map(|(label, _, _)| label.len())
282 .max()
283 .unwrap_or(10);
284
285 for (label, requests, tokens) in display_data {
286 let value = if value_type == "tokens" {
287 *tokens
288 } else {
289 *requests
290 };
291 let bar_width = if max_value > 0 {
292 ((value as f64 / max_value as f64) * max_width as f64) as usize
293 } else {
294 0
295 };
296
297 let bar = "▓".repeat(bar_width);
298 let formatted_value = if value_type == "tokens" {
299 Self::format_tokens(*tokens)
300 } else {
301 format!("{}", requests)
302 };
303
304 println!(
305 " {:width$} │{:bar_width$} {} ({})",
306 label.bold(),
307 bar.cyan(),
308 formatted_value.yellow(),
309 if value_type == "tokens" {
310 format!("{} req", requests)
311 } else {
312 Self::format_tokens(*tokens)
313 },
314 width = max_label_width,
315 bar_width = max_width
316 );
317 }
318 }
319
320 fn format_tokens(tokens: u64) -> String {
321 if tokens >= 1_000_000 {
322 format!("{:.1}M", tokens as f64 / 1_000_000.0)
323 } else if tokens >= 1_000 {
324 format!("{:.1}k", tokens as f64 / 1_000.0)
325 } else {
326 format!("{}", tokens)
327 }
328 }
329}
330
331pub fn display_usage_overview(stats: &UsageStats) {
332 println!("\n{}", "📊 Usage Overview".bold().blue());
333 println!();
334
335 println!(
337 "{} {}",
338 "Total Requests:".bold(),
339 stats.total_requests.to_string().green()
340 );
341 println!(
342 "{} {}",
343 "Total Tokens:".bold(),
344 BarChart::format_tokens(stats.total_tokens).green()
345 );
346 println!(
347 "{} {}",
348 "Input Tokens:".bold(),
349 BarChart::format_tokens(stats.input_tokens).cyan()
350 );
351 println!(
352 "{} {}",
353 "Output Tokens:".bold(),
354 BarChart::format_tokens(stats.output_tokens).yellow()
355 );
356
357 if let Some((earliest, latest)) = stats.date_range {
358 let duration = latest.signed_duration_since(earliest);
359 println!(
360 "{} {} to {} ({} days)",
361 "Date Range:".bold(),
362 earliest.format("%Y-%m-%d").to_string().dimmed(),
363 latest.format("%Y-%m-%d").to_string().dimmed(),
364 duration.num_days().max(1)
365 );
366 }
367
368 if stats.total_requests > 0 {
370 let avg_tokens = stats.total_tokens / stats.total_requests;
371 let avg_input = stats.input_tokens / stats.total_requests;
372 let avg_output = stats.output_tokens / stats.total_requests;
373 println!();
374 println!("{}", "📈 Averages per Request".bold().blue());
375 println!(
376 "{} {}",
377 "Total Tokens:".bold(),
378 BarChart::format_tokens(avg_tokens).green()
379 );
380 println!(
381 "{} {}",
382 "Input Tokens:".bold(),
383 BarChart::format_tokens(avg_input).cyan()
384 );
385 println!(
386 "{} {}",
387 "Output Tokens:".bold(),
388 BarChart::format_tokens(avg_output).yellow()
389 );
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn test_usage_analyzer_creation() {
399 let _result = UsageAnalyzer::new();
402 }
405
406 #[test]
407 fn test_usage_stats_struct_creation() {
408 let stats = UsageStats {
410 total_tokens: 0,
411 total_requests: 0,
412 input_tokens: 0,
413 output_tokens: 0,
414 model_usage: Vec::new(),
415 daily_usage: Vec::new(),
416 weekly_usage: Vec::new(),
417 monthly_usage: Vec::new(),
418 yearly_usage: Vec::new(),
419 date_range: None,
420 };
421
422 assert_eq!(stats.total_tokens, 0);
423 assert_eq!(stats.total_requests, 0);
424 assert!(stats.model_usage.is_empty());
425 }
426
427 #[test]
428 fn test_bar_chart_format_tokens() {
429 assert_eq!(BarChart::format_tokens(500), "500");
431 assert_eq!(BarChart::format_tokens(1500), "1.5k");
432 assert_eq!(BarChart::format_tokens(1_500_000), "1.5M");
433 }
434}