systemprompt_cli/commands/analytics/shared/
output.rs1use 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(cents: i64) -> String {
127 let dollars = cents as f64 / 100.0;
128 match dollars {
129 d if d < 0.01 && cents > 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}