Skip to main content

systemprompt_cli/commands/analytics/shared/
output.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
5pub struct TrendPoint {
6    pub timestamp: String,
7    pub value: f64,
8    #[serde(skip_serializing_if = "Option::is_none")]
9    pub label: Option<String>,
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
13pub struct TrendData {
14    pub points: Vec<TrendPoint>,
15    pub period: String,
16    pub metric: String,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
20pub struct StatsSummary {
21    pub total: i64,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub change_percent: Option<f64>,
24    pub period: String,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
28pub struct BreakdownItem {
29    pub name: String,
30    pub count: i64,
31    pub percentage: f64,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
35pub struct BreakdownData {
36    pub items: Vec<BreakdownItem>,
37    pub total: i64,
38    pub label: String,
39}
40
41impl BreakdownData {
42    pub fn new(label: impl Into<String>) -> Self {
43        Self {
44            items: Vec::new(),
45            total: 0,
46            label: label.into(),
47        }
48    }
49
50    pub fn add(&mut self, name: impl Into<String>, count: i64) {
51        self.total += count;
52        self.items.push(BreakdownItem {
53            name: name.into(),
54            count,
55            percentage: 0.0,
56        });
57    }
58
59    pub fn finalize(&mut self) {
60        if self.total > 0 {
61            self.items.iter_mut().for_each(|item| {
62                item.percentage = (item.count as f64 / self.total as f64) * 100.0;
63            });
64        }
65        self.items.sort_by(|a, b| b.count.cmp(&a.count));
66    }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
70pub struct MetricCard {
71    pub label: String,
72    pub value: String,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub change: Option<String>,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub secondary: Option<String>,
77}
78
79impl MetricCard {
80    pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
81        Self {
82            label: label.into(),
83            value: value.into(),
84            change: None,
85            secondary: None,
86        }
87    }
88
89    pub fn with_change(mut self, change: impl Into<String>) -> Self {
90        self.change = Some(change.into());
91        self
92    }
93
94    pub fn with_secondary(mut self, secondary: impl Into<String>) -> Self {
95        self.secondary = Some(secondary.into());
96        self
97    }
98}
99
100pub fn format_number(n: i64) -> String {
101    let s = n.abs().to_string();
102    let chars: Vec<char> = s.chars().collect();
103    let formatted: String = chars
104        .iter()
105        .rev()
106        .enumerate()
107        .flat_map(|(i, c)| {
108            if i > 0 && i % 3 == 0 {
109                vec![',', *c]
110            } else {
111                vec![*c]
112            }
113        })
114        .collect::<Vec<_>>()
115        .into_iter()
116        .rev()
117        .collect();
118
119    if n < 0 {
120        format!("-{}", formatted)
121    } else {
122        formatted
123    }
124}
125
126pub fn format_cost(microdollars: i64) -> String {
127    let dollars = microdollars as f64 / 1_000_000.0;
128    match dollars {
129        d if d < 0.01 && microdollars > 0 => format!("${:.4}", d),
130        d if d < 100.0 => format!("${:.2}", d),
131        _ => format!("${:.0}", dollars),
132    }
133}
134
135pub fn format_percent(value: f64) -> String {
136    match value.abs() {
137        v if v < 0.1 => format!("{:.2}%", value),
138        v if v < 10.0 => format!("{:.1}%", value),
139        _ => format!("{:.0}%", value),
140    }
141}
142
143pub fn format_change(current: i64, previous: i64) -> Option<String> {
144    (previous != 0).then(|| {
145        let change = ((current - previous) as f64 / previous as f64) * 100.0;
146        let sign = if change >= 0.0 { "+" } else { "" };
147        format!("{}{:.1}%", sign, change)
148    })
149}
150
151pub fn format_tokens(tokens: i64) -> String {
152    match tokens {
153        t if t < 1000 => format!("{}", t),
154        t if t < 1_000_000 => format!("{:.1}K", t as f64 / 1000.0),
155        _ => format!("{:.1}M", tokens as f64 / 1_000_000.0),
156    }
157}