ai_upsell_card.rs

  1use std::{sync::Arc, time::Duration};
  2
  3use client::{Client, zed_urls};
  4use cloud_llm_client::Plan;
  5use gpui::{
  6    Animation, AnimationExt, AnyElement, App, IntoElement, RenderOnce, Transformation, Window,
  7    percentage,
  8};
  9use ui::{Divider, Vector, VectorName, prelude::*};
 10
 11use crate::{SignInStatus, plan_definitions::PlanDefinitions};
 12
 13#[derive(IntoElement, RegisterComponent)]
 14pub struct AiUpsellCard {
 15    pub sign_in_status: SignInStatus,
 16    pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
 17    pub user_plan: Option<Plan>,
 18    pub tab_index: Option<isize>,
 19}
 20
 21impl AiUpsellCard {
 22    pub fn new(client: Arc<Client>, user_plan: Option<Plan>) -> Self {
 23        let status = *client.status().borrow();
 24
 25        Self {
 26            user_plan,
 27            sign_in_status: status.into(),
 28            sign_in: Arc::new(move |_window, cx| {
 29                cx.spawn({
 30                    let client = client.clone();
 31                    async move |cx| client.sign_in_with_optional_connect(true, cx).await
 32                })
 33                .detach_and_log_err(cx);
 34            }),
 35            tab_index: None,
 36        }
 37    }
 38}
 39
 40impl RenderOnce for AiUpsellCard {
 41    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
 42        let plan_definitions = PlanDefinitions;
 43
 44        let pro_section = v_flex()
 45            .flex_grow()
 46            .w_full()
 47            .gap_1()
 48            .child(
 49                h_flex()
 50                    .gap_2()
 51                    .child(
 52                        Label::new("Pro")
 53                            .size(LabelSize::Small)
 54                            .color(Color::Accent)
 55                            .buffer_font(cx),
 56                    )
 57                    .child(Divider::horizontal()),
 58            )
 59            .child(plan_definitions.pro_plan(false));
 60
 61        let free_section = v_flex()
 62            .flex_grow()
 63            .w_full()
 64            .gap_1()
 65            .child(
 66                h_flex()
 67                    .gap_2()
 68                    .child(
 69                        Label::new("Free")
 70                            .size(LabelSize::Small)
 71                            .color(Color::Muted)
 72                            .buffer_font(cx),
 73                    )
 74                    .child(Divider::horizontal()),
 75            )
 76            .child(plan_definitions.free_plan());
 77
 78        let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child(
 79            Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.))
 80                .color(Color::Custom(cx.theme().colors().border.opacity(0.05))),
 81        );
 82
 83        let gradient_bg = div()
 84            .absolute()
 85            .inset_0()
 86            .size_full()
 87            .bg(gpui::linear_gradient(
 88                180.,
 89                gpui::linear_color_stop(
 90                    cx.theme().colors().elevated_surface_background.opacity(0.8),
 91                    0.,
 92                ),
 93                gpui::linear_color_stop(
 94                    cx.theme().colors().elevated_surface_background.opacity(0.),
 95                    0.8,
 96                ),
 97            ));
 98
 99        let description = PlanDefinitions::AI_DESCRIPTION;
100
101        let card = v_flex()
102            .relative()
103            .flex_grow()
104            .p_4()
105            .pt_3()
106            .border_1()
107            .border_color(cx.theme().colors().border)
108            .rounded_lg()
109            .overflow_hidden()
110            .child(grid_bg)
111            .child(gradient_bg);
112
113        let plans_section = h_flex()
114            .w_full()
115            .mt_1p5()
116            .mb_2p5()
117            .items_start()
118            .gap_6()
119            .child(free_section)
120            .child(pro_section);
121
122        let footer_container = v_flex().items_center().gap_1();
123
124        let certified_user_stamp = div()
125            .absolute()
126            .top_2()
127            .right_2()
128            .size(rems_from_px(72.))
129            .child(
130                Vector::new(
131                    VectorName::CertifiedUserStamp,
132                    rems_from_px(72.),
133                    rems_from_px(72.),
134                )
135                .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.3)))
136                .with_animation(
137                    "loading_stamp",
138                    Animation::new(Duration::from_secs(10)).repeat(),
139                    |this, delta| this.transform(Transformation::rotate(percentage(delta))),
140                ),
141            );
142
143        let pro_trial_stamp = div()
144            .absolute()
145            .top_2()
146            .right_2()
147            .size(rems_from_px(72.))
148            .child(
149                Vector::new(
150                    VectorName::ProTrialStamp,
151                    rems_from_px(72.),
152                    rems_from_px(72.),
153                )
154                .color(Color::Custom(cx.theme().colors().text.alpha(0.2))),
155            );
156
157        match self.sign_in_status {
158            SignInStatus::SignedIn => match self.user_plan {
159                None | Some(Plan::ZedFree) => card
160                    .child(Label::new("Try Zed AI").size(LabelSize::Large))
161                    .child(
162                        div()
163                            .max_w_3_4()
164                            .mb_2()
165                            .child(Label::new(description).color(Color::Muted)),
166                    )
167                    .child(plans_section)
168                    .child(
169                        footer_container
170                            .child(
171                                Button::new("start_trial", "Start 14-day Free Pro Trial")
172                                    .full_width()
173                                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
174                                    .when_some(self.tab_index, |this, tab_index| {
175                                        this.tab_index(tab_index)
176                                    })
177                                    .on_click(move |_, _window, cx| {
178                                        telemetry::event!(
179                                            "Start Trial Clicked",
180                                            state = "post-sign-in"
181                                        );
182                                        cx.open_url(&zed_urls::start_trial_url(cx))
183                                    }),
184                            )
185                            .child(
186                                Label::new("No credit card required")
187                                    .size(LabelSize::Small)
188                                    .color(Color::Muted),
189                            ),
190                    ),
191                Some(Plan::ZedProTrial) => card
192                    .child(pro_trial_stamp)
193                    .child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large))
194                    .child(
195                        Label::new("Here's what you get for the next 14 days:")
196                            .color(Color::Muted)
197                            .mb_2(),
198                    )
199                    .child(plan_definitions.pro_trial(false)),
200                Some(Plan::ZedPro) => card
201                    .child(certified_user_stamp)
202                    .child(Label::new("You're in the Zed Pro plan").size(LabelSize::Large))
203                    .child(
204                        Label::new("Here's what you get:")
205                            .color(Color::Muted)
206                            .mb_2(),
207                    )
208                    .child(plan_definitions.pro_plan(false)),
209            },
210            // Signed Out State
211            _ => card
212                .child(Label::new("Try Zed AI").size(LabelSize::Large))
213                .child(
214                    div()
215                        .max_w_3_4()
216                        .mb_2()
217                        .child(Label::new(description).color(Color::Muted)),
218                )
219                .child(plans_section)
220                .child(
221                    Button::new("sign_in", "Sign In")
222                        .full_width()
223                        .style(ButtonStyle::Tinted(ui::TintColor::Accent))
224                        .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index))
225                        .on_click({
226                            let callback = self.sign_in.clone();
227                            move |_, window, cx| {
228                                telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
229                                callback(window, cx)
230                            }
231                        }),
232                ),
233        }
234    }
235}
236
237impl Component for AiUpsellCard {
238    fn scope() -> ComponentScope {
239        ComponentScope::Agent
240    }
241
242    fn name() -> &'static str {
243        "AI Upsell Card"
244    }
245
246    fn sort_name() -> &'static str {
247        "AI Upsell Card"
248    }
249
250    fn description() -> Option<&'static str> {
251        Some("A card presenting the Zed AI product during user's first-open onboarding flow.")
252    }
253
254    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
255        Some(
256            v_flex()
257                .gap_4()
258                .children(vec![example_group(vec![
259                    single_example(
260                        "Signed Out State",
261                        AiUpsellCard {
262                            sign_in_status: SignInStatus::SignedOut,
263                            sign_in: Arc::new(|_, _| {}),
264                            user_plan: None,
265                            tab_index: Some(0),
266                        }
267                        .into_any_element(),
268                    ),
269                    single_example(
270                        "Free Plan",
271                        AiUpsellCard {
272                            sign_in_status: SignInStatus::SignedIn,
273                            sign_in: Arc::new(|_, _| {}),
274                            user_plan: Some(Plan::ZedFree),
275                            tab_index: Some(1),
276                        }
277                        .into_any_element(),
278                    ),
279                    single_example(
280                        "Pro Trial",
281                        AiUpsellCard {
282                            sign_in_status: SignInStatus::SignedIn,
283                            sign_in: Arc::new(|_, _| {}),
284                            user_plan: Some(Plan::ZedProTrial),
285                            tab_index: Some(1),
286                        }
287                        .into_any_element(),
288                    ),
289                    single_example(
290                        "Pro Plan",
291                        AiUpsellCard {
292                            sign_in_status: SignInStatus::SignedIn,
293                            sign_in: Arc::new(|_, _| {}),
294                            user_plan: Some(Plan::ZedPro),
295                            tab_index: Some(1),
296                        }
297                        .into_any_element(),
298                    ),
299                ])])
300                .into_any_element(),
301        )
302    }
303}