usage_callouts.rs

  1use client::{ModelRequestUsage, RequestUsage, zed_urls};
  2use cloud_llm_client::{Plan, UsageLimit};
  3use component::{empty_example, example_group_with_title, single_example};
  4use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
  5use ui::{Callout, prelude::*};
  6
  7#[derive(IntoElement, RegisterComponent)]
  8pub struct UsageCallout {
  9    plan: Plan,
 10    usage: ModelRequestUsage,
 11}
 12
 13impl UsageCallout {
 14    pub fn new(plan: Plan, usage: ModelRequestUsage) -> Self {
 15        Self { plan, usage }
 16    }
 17}
 18
 19impl RenderOnce for UsageCallout {
 20    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
 21        let (is_limit_reached, is_approaching_limit, remaining) = match self.usage.limit {
 22            UsageLimit::Limited(limit) => {
 23                let percentage = self.usage.amount as f32 / limit as f32;
 24                let is_limit_reached = percentage >= 1.0;
 25                let is_near_limit = percentage >= 0.9 && percentage < 1.0;
 26                (
 27                    is_limit_reached,
 28                    is_near_limit,
 29                    limit.saturating_sub(self.usage.amount),
 30                )
 31            }
 32            UsageLimit::Unlimited => (false, false, 0),
 33        };
 34
 35        if !is_limit_reached && !is_approaching_limit {
 36            return div().into_any_element();
 37        }
 38
 39        let (title, message, button_text, url) = if is_limit_reached {
 40            match self.plan {
 41                Plan::ZedFree => (
 42                    "Out of free prompts",
 43                    "Upgrade to continue, wait for the next reset, or switch to API key."
 44                        .to_string(),
 45                    "Upgrade",
 46                    zed_urls::account_url(cx),
 47                ),
 48                Plan::ZedProTrial => (
 49                    "Out of trial prompts",
 50                    "Upgrade to Zed Pro to continue, or switch to API key.".to_string(),
 51                    "Upgrade",
 52                    zed_urls::account_url(cx),
 53                ),
 54                Plan::ZedPro => (
 55                    "Out of included prompts",
 56                    "Enable usage-based billing to continue.".to_string(),
 57                    "Manage",
 58                    zed_urls::account_url(cx),
 59                ),
 60            }
 61        } else {
 62            match self.plan {
 63                Plan::ZedFree => (
 64                    "Reaching free plan limit soon",
 65                    format!(
 66                        "{remaining} remaining - Upgrade to increase limit, or switch providers",
 67                    ),
 68                    "Upgrade",
 69                    zed_urls::account_url(cx),
 70                ),
 71                Plan::ZedProTrial => (
 72                    "Reaching trial limit soon",
 73                    format!(
 74                        "{remaining} remaining - Upgrade to increase limit, or switch providers",
 75                    ),
 76                    "Upgrade",
 77                    zed_urls::account_url(cx),
 78                ),
 79                _ => return div().into_any_element(),
 80            }
 81        };
 82
 83        let icon = if is_limit_reached {
 84            Icon::new(IconName::Close)
 85                .color(Color::Error)
 86                .size(IconSize::XSmall)
 87        } else {
 88            Icon::new(IconName::Warning)
 89                .color(Color::Warning)
 90                .size(IconSize::XSmall)
 91        };
 92
 93        div()
 94            .border_t_1()
 95            .border_color(cx.theme().colors().border)
 96            .child(
 97                Callout::new()
 98                    .icon(icon)
 99                    .title(title)
100                    .description(message)
101                    .primary_action(
102                        Button::new("upgrade", button_text)
103                            .label_size(LabelSize::Small)
104                            .on_click(move |_, _, cx| {
105                                cx.open_url(&url);
106                            }),
107                    ),
108            )
109            .into_any_element()
110    }
111}
112
113impl Component for UsageCallout {
114    fn scope() -> ComponentScope {
115        ComponentScope::Agent
116    }
117
118    fn sort_name() -> &'static str {
119        "AgentUsageCallout"
120    }
121
122    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
123        let free_examples = example_group_with_title(
124            "Free Plan",
125            vec![
126                single_example(
127                    "Approaching limit (90%)",
128                    UsageCallout::new(
129                        Plan::ZedFree,
130                        ModelRequestUsage(RequestUsage {
131                            limit: UsageLimit::Limited(50),
132                            amount: 45, // 90% of limit
133                        }),
134                    )
135                    .into_any_element(),
136                ),
137                single_example(
138                    "Limit reached (100%)",
139                    UsageCallout::new(
140                        Plan::ZedFree,
141                        ModelRequestUsage(RequestUsage {
142                            limit: UsageLimit::Limited(50),
143                            amount: 50, // 100% of limit
144                        }),
145                    )
146                    .into_any_element(),
147                ),
148            ],
149        );
150
151        let trial_examples = example_group_with_title(
152            "Zed Pro Trial",
153            vec![
154                single_example(
155                    "Approaching limit (90%)",
156                    UsageCallout::new(
157                        Plan::ZedProTrial,
158                        ModelRequestUsage(RequestUsage {
159                            limit: UsageLimit::Limited(150),
160                            amount: 135, // 90% of limit
161                        }),
162                    )
163                    .into_any_element(),
164                ),
165                single_example(
166                    "Limit reached (100%)",
167                    UsageCallout::new(
168                        Plan::ZedProTrial,
169                        ModelRequestUsage(RequestUsage {
170                            limit: UsageLimit::Limited(150),
171                            amount: 150, // 100% of limit
172                        }),
173                    )
174                    .into_any_element(),
175                ),
176            ],
177        );
178
179        let pro_examples = example_group_with_title(
180            "Zed Pro",
181            vec![
182                single_example(
183                    "Limit reached (100%)",
184                    UsageCallout::new(
185                        Plan::ZedPro,
186                        ModelRequestUsage(RequestUsage {
187                            limit: UsageLimit::Limited(500),
188                            amount: 500, // 100% of limit
189                        }),
190                    )
191                    .into_any_element(),
192                ),
193                empty_example("Unlimited plan (no callout shown)"),
194            ],
195        );
196
197        Some(
198            v_flex()
199                .p_4()
200                .gap_4()
201                .child(free_examples)
202                .child(trial_examples)
203                .child(pro_examples)
204                .into_any_element(),
205        )
206    }
207}