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