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| {
 28                        client.authenticate_and_connect(true, cx).await;
 29                    }
 30                })
 31                .detach();
 32            }),
 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                )
119                .child(
120                    Label::new("No credit card required")
121                        .size(LabelSize::Small)
122                        .color(Color::Muted),
123                )
124                .into_any_element(),
125            _ => Button::new("sign_in", "Sign In")
126                .full_width()
127                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
128                .on_click({
129                    let callback = self.sign_in.clone();
130                    move |_, window, cx| {
131                        telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
132                        callback(window, cx)
133                    }
134                })
135                .into_any_element(),
136        };
137
138        v_flex()
139            .relative()
140            .p_4()
141            .pt_3()
142            .border_1()
143            .border_color(cx.theme().colors().border)
144            .rounded_lg()
145            .overflow_hidden()
146            .child(grid_bg)
147            .child(gradient_bg)
148            .child(Label::new("Try Zed AI").size(LabelSize::Large))
149            .child(
150                div()
151                    .max_w_3_4()
152                    .mb_2()
153                    .child(Label::new(DESCRIPTION).color(Color::Muted)),
154            )
155            .child(
156                h_flex()
157                    .w_full()
158                    .mt_1p5()
159                    .mb_2p5()
160                    .items_start()
161                    .gap_6()
162                    .child(free_section)
163                    .child(pro_section),
164            )
165            .child(footer_buttons)
166    }
167}
168
169impl Component for AiUpsellCard {
170    fn scope() -> ComponentScope {
171        ComponentScope::Agent
172    }
173
174    fn name() -> &'static str {
175        "AI Upsell Card"
176    }
177
178    fn sort_name() -> &'static str {
179        "AI Upsell Card"
180    }
181
182    fn description() -> Option<&'static str> {
183        Some("A card presenting the Zed AI product during user's first-open onboarding flow.")
184    }
185
186    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
187        Some(
188            v_flex()
189                .p_4()
190                .gap_4()
191                .children(vec![example_group(vec![
192                    single_example(
193                        "Signed Out State",
194                        AiUpsellCard {
195                            sign_in_status: SignInStatus::SignedOut,
196                            sign_in: Arc::new(|_, _| {}),
197                            user_plan: None,
198                        }
199                        .into_any_element(),
200                    ),
201                    single_example(
202                        "Signed In State",
203                        AiUpsellCard {
204                            sign_in_status: SignInStatus::SignedIn,
205                            sign_in: Arc::new(|_, _| {}),
206                            user_plan: None,
207                        }
208                        .into_any_element(),
209                    ),
210                ])])
211                .into_any_element(),
212        )
213    }
214}