ai_onboarding.rs

  1mod agent_api_keys_onboarding;
  2mod agent_panel_onboarding_card;
  3mod agent_panel_onboarding_content;
  4mod edit_prediction_onboarding_content;
  5mod plan_definitions;
  6mod young_account_banner;
  7
  8pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders};
  9pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
 10pub use agent_panel_onboarding_content::AgentPanelOnboarding;
 11use cloud_api_types::Plan;
 12pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
 13pub use plan_definitions::PlanDefinitions;
 14pub use young_account_banner::YoungAccountBanner;
 15
 16use std::sync::Arc;
 17
 18use client::{Client, UserStore, zed_urls};
 19use gpui::{AnyElement, Entity, IntoElement, ParentElement};
 20use ui::{Divider, RegisterComponent, Tooltip, Vector, VectorName, prelude::*};
 21
 22#[derive(PartialEq)]
 23pub enum SignInStatus {
 24    SignedIn,
 25    SigningIn,
 26    SignedOut,
 27}
 28
 29impl From<client::Status> for SignInStatus {
 30    fn from(status: client::Status) -> Self {
 31        if status.is_signing_in() {
 32            Self::SigningIn
 33        } else if status.is_signed_out() {
 34            Self::SignedOut
 35        } else {
 36            Self::SignedIn
 37        }
 38    }
 39}
 40
 41#[derive(RegisterComponent, IntoElement)]
 42pub struct ZedAiOnboarding {
 43    pub sign_in_status: SignInStatus,
 44    pub plan: Option<Plan>,
 45    pub account_too_young: bool,
 46    pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
 47    pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
 48    pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
 49}
 50
 51impl ZedAiOnboarding {
 52    pub fn new(
 53        client: Arc<Client>,
 54        user_store: &Entity<UserStore>,
 55        continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
 56        cx: &mut App,
 57    ) -> Self {
 58        let store = user_store.read(cx);
 59        let status = *client.status().borrow();
 60
 61        Self {
 62            sign_in_status: status.into(),
 63            plan: store.plan(),
 64            account_too_young: store.account_too_young(),
 65            continue_with_zed_ai,
 66            sign_in: Arc::new(move |_window, cx| {
 67                cx.spawn({
 68                    let client = client.clone();
 69                    async move |cx| client.sign_in_with_optional_connect(true, cx).await
 70                })
 71                .detach_and_log_err(cx);
 72            }),
 73            dismiss_onboarding: None,
 74        }
 75    }
 76
 77    pub fn with_dismiss(
 78        mut self,
 79        dismiss_callback: impl Fn(&mut Window, &mut App) + 'static,
 80    ) -> Self {
 81        self.dismiss_onboarding = Some(Arc::new(dismiss_callback));
 82        self
 83    }
 84
 85    fn certified_user_stamp(cx: &App) -> impl IntoElement {
 86        div().absolute().bottom_1().right_1().child(
 87            Vector::new(
 88                VectorName::ProUserStamp,
 89                rems_from_px(156.),
 90                rems_from_px(60.),
 91            )
 92            .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.8))),
 93        )
 94    }
 95
 96    fn pro_trial_stamp(cx: &App) -> impl IntoElement {
 97        div().absolute().bottom_1().right_1().child(
 98            Vector::new(
 99                VectorName::ProTrialStamp,
100                rems_from_px(156.),
101                rems_from_px(60.),
102            )
103            .color(Color::Custom(cx.theme().colors().text.alpha(0.8))),
104        )
105    }
106
107    fn business_stamp(cx: &App) -> impl IntoElement {
108        div().absolute().bottom_1().right_1().child(
109            Vector::new(
110                VectorName::BusinessStamp,
111                rems_from_px(156.),
112                rems_from_px(60.),
113            )
114            .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.8))),
115        )
116    }
117
118    fn student_stamp(cx: &App) -> impl IntoElement {
119        div().absolute().bottom_1().right_1().child(
120            Vector::new(
121                VectorName::StudentStamp,
122                rems_from_px(156.),
123                rems_from_px(60.),
124            )
125            .color(Color::Custom(cx.theme().colors().text.alpha(0.8))),
126        )
127    }
128
129    fn render_dismiss_button(&self) -> Option<AnyElement> {
130        self.dismiss_onboarding.as_ref().map(|dismiss_callback| {
131            let callback = dismiss_callback.clone();
132
133            h_flex()
134                .absolute()
135                .top_0()
136                .right_0()
137                .child(
138                    IconButton::new("dismiss_onboarding", IconName::Close)
139                        .icon_size(IconSize::Small)
140                        .tooltip(Tooltip::text("Dismiss"))
141                        .on_click(move |_, window, cx| {
142                            telemetry::event!("Banner Dismissed", source = "AI Onboarding",);
143                            callback(window, cx)
144                        }),
145                )
146                .into_any_element()
147        })
148    }
149
150    fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
151        let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
152
153        v_flex()
154            .w_full()
155            .relative()
156            .gap_1()
157            .child(Headline::new("Welcome to Zed AI"))
158            .child(
159                Label::new("Sign in to try Zed Pro for 14 days, no credit card required.")
160                    .color(Color::Muted)
161                    .mb_2(),
162            )
163            .child(PlanDefinitions.pro_plan())
164            .child(
165                Button::new("sign_in", "Try Zed Pro for Free")
166                    .disabled(signing_in)
167                    .full_width()
168                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
169                    .on_click({
170                        let callback = self.sign_in.clone();
171                        move |_, window, cx| {
172                            telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
173                            callback(window, cx)
174                        }
175                    }),
176            )
177            .children(self.render_dismiss_button())
178            .into_any_element()
179    }
180
181    fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
182        if self.account_too_young {
183            v_flex()
184                .relative()
185                .min_w_0()
186                .gap_1()
187                .child(Headline::new("Welcome to Zed AI"))
188                .child(YoungAccountBanner)
189                .child(
190                    v_flex()
191                        .mt_2()
192                        .gap_1()
193                        .child(
194                            h_flex()
195                                .gap_2()
196                                .child(
197                                    Label::new("Pro")
198                                        .size(LabelSize::Small)
199                                        .color(Color::Accent)
200                                        .buffer_font(cx),
201                                )
202                                .child(Divider::horizontal()),
203                        )
204                        .child(PlanDefinitions.pro_plan())
205                        .child(
206                            Button::new("pro", "Get Started")
207                                .full_width()
208                                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
209                                .on_click(move |_, _window, cx| {
210                                    telemetry::event!(
211                                        "Upgrade To Pro Clicked",
212                                        state = "young-account"
213                                    );
214                                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
215                                }),
216                        ),
217                )
218                .into_any_element()
219        } else {
220            v_flex()
221                .w_full()
222                .relative()
223                .gap_1()
224                .child(Headline::new("Welcome to Zed AI"))
225                .child(
226                    v_flex()
227                        .mt_2()
228                        .gap_1()
229                        .child(
230                            h_flex()
231                                .gap_2()
232                                .child(
233                                    Label::new("Free")
234                                        .size(LabelSize::Small)
235                                        .color(Color::Muted)
236                                        .buffer_font(cx),
237                                )
238                                .child(
239                                    Label::new("(Current Plan)")
240                                        .size(LabelSize::Small)
241                                        .color(Color::Custom(
242                                            cx.theme().colors().text_muted.opacity(0.6),
243                                        ))
244                                        .buffer_font(cx),
245                                )
246                                .child(Divider::horizontal()),
247                        )
248                        .child(PlanDefinitions.free_plan()),
249                )
250                .children(self.render_dismiss_button())
251                .child(
252                    v_flex()
253                        .mt_2()
254                        .gap_1()
255                        .child(
256                            h_flex()
257                                .gap_2()
258                                .child(
259                                    Label::new("Pro Trial")
260                                        .size(LabelSize::Small)
261                                        .color(Color::Accent)
262                                        .buffer_font(cx),
263                                )
264                                .child(Divider::horizontal()),
265                        )
266                        .child(PlanDefinitions.pro_trial(true))
267                        .child(
268                            Button::new("pro", "Start Free Trial")
269                                .full_width()
270                                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
271                                .on_click(move |_, _window, cx| {
272                                    telemetry::event!(
273                                        "Start Trial Clicked",
274                                        state = "post-sign-in"
275                                    );
276                                    cx.open_url(&zed_urls::start_trial_url(cx))
277                                }),
278                        ),
279                )
280                .into_any_element()
281        }
282    }
283
284    fn render_trial_state(&self, cx: &mut App) -> AnyElement {
285        v_flex()
286            .w_full()
287            .relative()
288            .gap_1()
289            .child(Self::pro_trial_stamp(cx))
290            .child(Headline::new("Welcome to the Zed Pro Trial"))
291            .child(
292                Label::new("Here's what you get for the next 14 days:")
293                    .color(Color::Muted)
294                    .mb_2(),
295            )
296            .child(PlanDefinitions.pro_trial(false))
297            .children(self.render_dismiss_button())
298            .into_any_element()
299    }
300
301    fn render_pro_plan_state(&self, cx: &mut App) -> AnyElement {
302        v_flex()
303            .w_full()
304            .relative()
305            .gap_1()
306            .child(Self::certified_user_stamp(cx))
307            .child(Headline::new("Welcome to Zed Pro"))
308            .child(
309                Label::new("Here's what you get:")
310                    .color(Color::Muted)
311                    .mb_2(),
312            )
313            .child(PlanDefinitions.pro_plan())
314            .children(self.render_dismiss_button())
315            .into_any_element()
316    }
317
318    fn render_business_plan_state(&self, cx: &mut App) -> AnyElement {
319        v_flex()
320            .w_full()
321            .relative()
322            .gap_1()
323            .child(Self::business_stamp(cx))
324            .child(Headline::new("Welcome to Zed Business"))
325            .child(
326                Label::new("Here's what you get:")
327                    .color(Color::Muted)
328                    .mb_2(),
329            )
330            .child(PlanDefinitions.business_plan())
331            .children(self.render_dismiss_button())
332            .into_any_element()
333    }
334
335    fn render_student_plan_state(&self, cx: &mut App) -> AnyElement {
336        v_flex()
337            .w_full()
338            .relative()
339            .gap_1()
340            .child(Self::student_stamp(cx))
341            .child(Headline::new("Welcome to Zed Student"))
342            .child(
343                Label::new("Here's what you get:")
344                    .color(Color::Muted)
345                    .mb_2(),
346            )
347            .child(PlanDefinitions.student_plan())
348            .children(self.render_dismiss_button())
349            .into_any_element()
350    }
351}
352
353impl RenderOnce for ZedAiOnboarding {
354    fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
355        if matches!(self.sign_in_status, SignInStatus::SignedIn) {
356            match self.plan {
357                None => self.render_free_plan_state(cx),
358                Some(Plan::ZedFree) => self.render_free_plan_state(cx),
359                Some(Plan::ZedProTrial) => self.render_trial_state(cx),
360                Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
361                Some(Plan::ZedBusiness) => self.render_business_plan_state(cx),
362                Some(Plan::ZedStudent) => self.render_student_plan_state(cx),
363            }
364        } else {
365            self.render_sign_in_disclaimer(cx)
366        }
367    }
368}
369
370impl Component for ZedAiOnboarding {
371    fn scope() -> ComponentScope {
372        ComponentScope::Onboarding
373    }
374
375    fn name() -> &'static str {
376        "Agent New User Onboarding"
377    }
378
379    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
380        fn onboarding(
381            sign_in_status: SignInStatus,
382            plan: Option<Plan>,
383            account_too_young: bool,
384        ) -> AnyElement {
385            div()
386                .w_full()
387                .min_w_40()
388                .max_w(px(1100.))
389                .child(
390                    AgentPanelOnboardingCard::new().child(
391                        ZedAiOnboarding {
392                            sign_in_status,
393                            plan,
394                            account_too_young,
395                            continue_with_zed_ai: Arc::new(|_, _| {}),
396                            sign_in: Arc::new(|_, _| {}),
397                            dismiss_onboarding: None,
398                        }
399                        .into_any_element(),
400                    ),
401                )
402                .into_any_element()
403        }
404
405        Some(
406            v_flex()
407                .min_w_0()
408                .gap_4()
409                .children(vec![
410                    single_example(
411                        "Not Signed-in",
412                        onboarding(SignInStatus::SignedOut, None, false),
413                    ),
414                    single_example(
415                        "Young Account",
416                        onboarding(SignInStatus::SignedIn, None, true),
417                    ),
418                    single_example(
419                        "Free Plan",
420                        onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false),
421                    ),
422                    single_example(
423                        "Pro Trial",
424                        onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false),
425                    ),
426                    single_example(
427                        "Pro Plan",
428                        onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false),
429                    ),
430                    single_example(
431                        "Business Plan",
432                        onboarding(SignInStatus::SignedIn, Some(Plan::ZedBusiness), false),
433                    ),
434                    single_example(
435                        "Student Plan",
436                        onboarding(SignInStatus::SignedIn, Some(Plan::ZedStudent), false),
437                    ),
438                ])
439                .into_any_element(),
440        )
441    }
442}