ai_upsell_card.rs

  1use std::sync::Arc;
  2
  3use client::{Client, zed_urls};
  4use cloud_llm_client::Plan;
  5use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
  6use ui::{Divider, List, Vector, VectorName, prelude::*};
  7
  8use crate::{BulletItem, SignInStatus};
  9
 10#[derive(IntoElement, RegisterComponent)]
 11pub struct AiUpsellCard {
 12    pub sign_in_status: SignInStatus,
 13    pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
 14    pub user_plan: Option<Plan>,
 15    pub tab_index: Option<isize>,
 16}
 17
 18impl AiUpsellCard {
 19    pub fn new(client: Arc<Client>, user_plan: Option<Plan>) -> Self {
 20        let status = *client.status().borrow();
 21
 22        Self {
 23            user_plan,
 24            sign_in_status: status.into(),
 25            sign_in: Arc::new(move |_window, cx| {
 26                cx.spawn({
 27                    let client = client.clone();
 28                    async move |cx| client.sign_in_with_optional_connect(true, cx).await
 29                })
 30                .detach_and_log_err(cx);
 31            }),
 32            tab_index: None,
 33        }
 34    }
 35}
 36
 37impl RenderOnce for AiUpsellCard {
 38    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
 39        let pro_section = v_flex()
 40            .flex_grow()
 41            .w_full()
 42            .gap_1()
 43            .child(
 44                h_flex()
 45                    .gap_2()
 46                    .child(
 47                        Label::new("Pro")
 48                            .size(LabelSize::Small)
 49                            .color(Color::Accent)
 50                            .buffer_font(cx),
 51                    )
 52                    .child(Divider::horizontal()),
 53            )
 54            .child(
 55                List::new()
 56                    .child(BulletItem::new("500 prompts with Claude models"))
 57                    .child(BulletItem::new(
 58                        "Unlimited edit predictions with Zeta, our open-source model",
 59                    )),
 60            );
 61
 62        let free_section = v_flex()
 63            .flex_grow()
 64            .w_full()
 65            .gap_1()
 66            .child(
 67                h_flex()
 68                    .gap_2()
 69                    .child(
 70                        Label::new("Free")
 71                            .size(LabelSize::Small)
 72                            .color(Color::Muted)
 73                            .buffer_font(cx),
 74                    )
 75                    .child(Divider::horizontal()),
 76            )
 77            .child(
 78                List::new()
 79                    .child(BulletItem::new("50 prompts with Claude models"))
 80                    .child(BulletItem::new("2,000 accepted edit predictions")),
 81            );
 82
 83        let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child(
 84            Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.))
 85                .color(Color::Custom(cx.theme().colors().border.opacity(0.05))),
 86        );
 87
 88        let gradient_bg = div()
 89            .absolute()
 90            .inset_0()
 91            .size_full()
 92            .bg(gpui::linear_gradient(
 93                180.,
 94                gpui::linear_color_stop(
 95                    cx.theme().colors().elevated_surface_background.opacity(0.8),
 96                    0.,
 97                ),
 98                gpui::linear_color_stop(
 99                    cx.theme().colors().elevated_surface_background.opacity(0.),
100                    0.8,
101                ),
102            ));
103
104        const DESCRIPTION: &str = "Zed offers a complete agentic experience, with robust editing and reviewing features to collaborate with AI.";
105
106        let footer_buttons = match self.sign_in_status {
107            SignInStatus::SignedIn => v_flex()
108                .items_center()
109                .gap_1()
110                .child(
111                    Button::new("sign_in", "Start 14-day Free Pro Trial")
112                        .full_width()
113                        .style(ButtonStyle::Tinted(ui::TintColor::Accent))
114                        .on_click(move |_, _window, cx| {
115                            telemetry::event!("Start Trial Clicked", state = "post-sign-in");
116                            cx.open_url(&zed_urls::start_trial_url(cx))
117                        })
118                        .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
119                )
120                .child(
121                    Label::new("No credit card required")
122                        .size(LabelSize::Small)
123                        .color(Color::Muted),
124                )
125                .into_any_element(),
126            _ => Button::new("sign_in", "Sign In")
127                .full_width()
128                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
129                .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index))
130                .on_click({
131                    let callback = self.sign_in.clone();
132                    move |_, window, cx| {
133                        telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
134                        callback(window, cx)
135                    }
136                })
137                .into_any_element(),
138        };
139
140        v_flex()
141            .relative()
142            .p_4()
143            .pt_3()
144            .border_1()
145            .border_color(cx.theme().colors().border)
146            .rounded_lg()
147            .overflow_hidden()
148            .child(grid_bg)
149            .child(gradient_bg)
150            .child(Label::new("Try Zed AI").size(LabelSize::Large))
151            .child(
152                div()
153                    .max_w_3_4()
154                    .mb_2()
155                    .child(Label::new(DESCRIPTION).color(Color::Muted)),
156            )
157            .child(
158                h_flex()
159                    .w_full()
160                    .mt_1p5()
161                    .mb_2p5()
162                    .items_start()
163                    .gap_6()
164                    .child(free_section)
165                    .child(pro_section),
166            )
167            .child(footer_buttons)
168    }
169}
170
171impl Component for AiUpsellCard {
172    fn scope() -> ComponentScope {
173        ComponentScope::Agent
174    }
175
176    fn name() -> &'static str {
177        "AI Upsell Card"
178    }
179
180    fn sort_name() -> &'static str {
181        "AI Upsell Card"
182    }
183
184    fn description() -> Option<&'static str> {
185        Some("A card presenting the Zed AI product during user's first-open onboarding flow.")
186    }
187
188    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
189        Some(
190            v_flex()
191                .p_4()
192                .gap_4()
193                .children(vec![example_group(vec![
194                    single_example(
195                        "Signed Out State",
196                        AiUpsellCard {
197                            sign_in_status: SignInStatus::SignedOut,
198                            sign_in: Arc::new(|_, _| {}),
199                            user_plan: None,
200                            tab_index: Some(0),
201                        }
202                        .into_any_element(),
203                    ),
204                    single_example(
205                        "Signed In State",
206                        AiUpsellCard {
207                            sign_in_status: SignInStatus::SignedIn,
208                            sign_in: Arc::new(|_, _| {}),
209                            user_plan: None,
210                            tab_index: Some(1),
211                        }
212                        .into_any_element(),
213                    ),
214                ])])
215                .into_any_element(),
216        )
217    }
218}