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