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::{
 21    Divider, List, ListBulletItem, RegisterComponent, Tooltip, Vector, VectorName, prelude::*,
 22};
 23
 24#[derive(PartialEq)]
 25pub enum SignInStatus {
 26    SignedIn,
 27    SigningIn,
 28    SignedOut,
 29}
 30
 31impl From<client::Status> for SignInStatus {
 32    fn from(status: client::Status) -> Self {
 33        if status.is_signing_in() {
 34            Self::SigningIn
 35        } else if status.is_signed_out() {
 36            Self::SignedOut
 37        } else {
 38            Self::SignedIn
 39        }
 40    }
 41}
 42
 43#[derive(RegisterComponent, IntoElement)]
 44pub struct ZedAiOnboarding {
 45    pub sign_in_status: SignInStatus,
 46    pub plan: Option<Plan>,
 47    pub account_too_young: bool,
 48    pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
 49    pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
 50    pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
 51}
 52
 53impl ZedAiOnboarding {
 54    pub fn new(
 55        client: Arc<Client>,
 56        user_store: &Entity<UserStore>,
 57        continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
 58        cx: &mut App,
 59    ) -> Self {
 60        let store = user_store.read(cx);
 61        let status = *client.status().borrow();
 62
 63        Self {
 64            sign_in_status: status.into(),
 65            plan: store.plan(),
 66            account_too_young: store.account_too_young(),
 67            continue_with_zed_ai,
 68            sign_in: Arc::new(move |_window, cx| {
 69                cx.spawn({
 70                    let client = client.clone();
 71                    async move |cx| client.sign_in_with_optional_connect(true, cx).await
 72                })
 73                .detach_and_log_err(cx);
 74            }),
 75            dismiss_onboarding: None,
 76        }
 77    }
 78
 79    pub fn with_dismiss(
 80        mut self,
 81        dismiss_callback: impl Fn(&mut Window, &mut App) + 'static,
 82    ) -> Self {
 83        self.dismiss_onboarding = Some(Arc::new(dismiss_callback));
 84        self
 85    }
 86
 87    fn certified_user_stamp(cx: &App) -> impl IntoElement {
 88        div().absolute().bottom_1().right_1().child(
 89            Vector::new(
 90                VectorName::ProUserStamp,
 91                rems_from_px(156.),
 92                rems_from_px(60.),
 93            )
 94            .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.8))),
 95        )
 96    }
 97
 98    fn pro_trial_stamp(cx: &App) -> impl IntoElement {
 99        div().absolute().bottom_1().right_1().child(
100            Vector::new(
101                VectorName::ProTrialStamp,
102                rems_from_px(156.),
103                rems_from_px(60.),
104            )
105            .color(Color::Custom(cx.theme().colors().text.alpha(0.8))),
106        )
107    }
108
109    fn business_stamp(cx: &App) -> impl IntoElement {
110        div().absolute().bottom_1().right_1().child(
111            Vector::new(
112                VectorName::BusinessStamp,
113                rems_from_px(156.),
114                rems_from_px(60.),
115            )
116            .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.8))),
117        )
118    }
119
120    fn student_stamp(cx: &App) -> impl IntoElement {
121        div().absolute().bottom_1().right_1().child(
122            Vector::new(
123                VectorName::StudentStamp,
124                rems_from_px(156.),
125                rems_from_px(60.),
126            )
127            .color(Color::Custom(cx.theme().colors().text.alpha(0.8))),
128        )
129    }
130
131    fn render_dismiss_button(&self) -> Option<AnyElement> {
132        self.dismiss_onboarding.as_ref().map(|dismiss_callback| {
133            let callback = dismiss_callback.clone();
134
135            h_flex()
136                .absolute()
137                .top_0()
138                .right_0()
139                .child(
140                    IconButton::new("dismiss_onboarding", IconName::Close)
141                        .icon_size(IconSize::Small)
142                        .tooltip(Tooltip::text("Dismiss"))
143                        .on_click(move |_, window, cx| {
144                            telemetry::event!("Banner Dismissed", source = "AI Onboarding",);
145                            callback(window, cx)
146                        }),
147                )
148                .into_any_element()
149        })
150    }
151
152    fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
153        let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
154
155        v_flex()
156            .w_full()
157            .relative()
158            .gap_1()
159            .child(Headline::new("Welcome to Zed AI"))
160            .child(
161                Label::new("Sign in to try Zed Pro for 14 days, no credit card required.")
162                    .color(Color::Muted)
163                    .mb_2(),
164            )
165            .child(PlanDefinitions.pro_plan())
166            .child(
167                Button::new("sign_in", "Try Zed Pro for Free")
168                    .disabled(signing_in)
169                    .full_width()
170                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
171                    .on_click({
172                        let callback = self.sign_in.clone();
173                        move |_, window, cx| {
174                            telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
175                            callback(window, cx)
176                        }
177                    }),
178            )
179            .children(self.render_dismiss_button())
180            .into_any_element()
181    }
182
183    fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
184        if self.account_too_young {
185            v_flex()
186                .relative()
187                .min_w_0()
188                .gap_1()
189                .child(Headline::new("Welcome to Zed AI"))
190                .child(YoungAccountBanner)
191                .child(
192                    v_flex()
193                        .mt_2()
194                        .gap_1()
195                        .child(
196                            h_flex()
197                                .gap_2()
198                                .child(
199                                    Label::new("Pro")
200                                        .size(LabelSize::Small)
201                                        .color(Color::Accent)
202                                        .buffer_font(cx),
203                                )
204                                .child(Divider::horizontal()),
205                        )
206                        .child(PlanDefinitions.pro_plan())
207                        .child(
208                            Button::new("pro", "Get Started")
209                                .full_width()
210                                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
211                                .on_click(move |_, _window, cx| {
212                                    telemetry::event!(
213                                        "Upgrade To Pro Clicked",
214                                        state = "young-account"
215                                    );
216                                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
217                                }),
218                        ),
219                )
220                .into_any_element()
221        } else {
222            v_flex()
223                .w_full()
224                .relative()
225                .gap_1()
226                .child(Headline::new("Welcome to Zed AI"))
227                .child(
228                    v_flex()
229                        .mt_2()
230                        .gap_1()
231                        .child(
232                            h_flex()
233                                .gap_2()
234                                .child(
235                                    Label::new("Free")
236                                        .size(LabelSize::Small)
237                                        .color(Color::Muted)
238                                        .buffer_font(cx),
239                                )
240                                .child(
241                                    Label::new("(Current Plan)")
242                                        .size(LabelSize::Small)
243                                        .color(Color::Custom(
244                                            cx.theme().colors().text_muted.opacity(0.6),
245                                        ))
246                                        .buffer_font(cx),
247                                )
248                                .child(Divider::horizontal()),
249                        )
250                        .child(PlanDefinitions.free_plan()),
251                )
252                .children(self.render_dismiss_button())
253                .child(
254                    v_flex()
255                        .mt_2()
256                        .gap_1()
257                        .child(
258                            h_flex()
259                                .gap_2()
260                                .child(
261                                    Label::new("Pro Trial")
262                                        .size(LabelSize::Small)
263                                        .color(Color::Accent)
264                                        .buffer_font(cx),
265                                )
266                                .child(Divider::horizontal()),
267                        )
268                        .child(PlanDefinitions.pro_trial(true))
269                        .child(
270                            Button::new("pro", "Start Free Trial")
271                                .full_width()
272                                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
273                                .on_click(move |_, _window, cx| {
274                                    telemetry::event!(
275                                        "Start Trial Clicked",
276                                        state = "post-sign-in"
277                                    );
278                                    cx.open_url(&zed_urls::start_trial_url(cx))
279                                }),
280                        ),
281                )
282                .into_any_element()
283        }
284    }
285
286    fn render_trial_state(&self, cx: &mut App) -> AnyElement {
287        v_flex()
288            .w_full()
289            .relative()
290            .gap_1()
291            .child(Self::pro_trial_stamp(cx))
292            .child(Headline::new("Welcome to the Zed Pro Trial"))
293            .child(
294                Label::new("Here's what you get for the next 14 days:")
295                    .color(Color::Muted)
296                    .mb_2(),
297            )
298            .child(PlanDefinitions.pro_trial(false))
299            .children(self.render_dismiss_button())
300            .into_any_element()
301    }
302
303    fn render_pro_plan_state(&self, cx: &mut App) -> AnyElement {
304        v_flex()
305            .w_full()
306            .relative()
307            .gap_1()
308            .child(Self::certified_user_stamp(cx))
309            .child(Headline::new("Welcome to Zed Pro"))
310            .child(
311                Label::new("Here's what you get:")
312                    .color(Color::Muted)
313                    .mb_2(),
314            )
315            .child(PlanDefinitions.pro_plan())
316            .children(self.render_dismiss_button())
317            .into_any_element()
318    }
319
320    fn render_business_plan_state(&self, cx: &mut App) -> AnyElement {
321        v_flex()
322            .w_full()
323            .relative()
324            .gap_1()
325            .child(Self::business_stamp(cx))
326            .child(Headline::new("Welcome to Zed Business"))
327            .child(
328                Label::new("Here's what you get:")
329                    .color(Color::Muted)
330                    .mb_2(),
331            )
332            .child(PlanDefinitions.business_plan())
333            .children(self.render_dismiss_button())
334            .into_any_element()
335    }
336
337    fn render_student_plan_state(&self, cx: &mut App) -> AnyElement {
338        v_flex()
339            .w_full()
340            .relative()
341            .gap_1()
342            .child(Self::student_stamp(cx))
343            .child(Headline::new("Welcome to Zed Student"))
344            .child(
345                Label::new("Here's what you get:")
346                    .color(Color::Muted)
347                    .mb_2(),
348            )
349            .child(PlanDefinitions.student_plan())
350            .children(self.render_dismiss_button())
351            .into_any_element()
352    }
353}
354
355impl RenderOnce for ZedAiOnboarding {
356    fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
357        if matches!(self.sign_in_status, SignInStatus::SignedIn) {
358            match self.plan {
359                None => self.render_free_plan_state(cx),
360                Some(Plan::ZedFree) => self.render_free_plan_state(cx),
361                Some(Plan::ZedProTrial) => self.render_trial_state(cx),
362                Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
363                Some(Plan::ZedBusiness) => self.render_business_plan_state(cx),
364                Some(Plan::ZedStudent) => self.render_student_plan_state(cx),
365            }
366        } else {
367            self.render_sign_in_disclaimer(cx)
368        }
369    }
370}
371
372impl Component for ZedAiOnboarding {
373    fn scope() -> ComponentScope {
374        ComponentScope::Onboarding
375    }
376
377    fn name() -> &'static str {
378        "Agent New User Onboarding"
379    }
380
381    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
382        fn onboarding(
383            sign_in_status: SignInStatus,
384            plan: Option<Plan>,
385            account_too_young: bool,
386        ) -> AnyElement {
387            div()
388                .w_full()
389                .min_w_40()
390                .max_w(px(1100.))
391                .child(
392                    AgentPanelOnboardingCard::new().child(
393                        ZedAiOnboarding {
394                            sign_in_status,
395                            plan,
396                            account_too_young,
397                            continue_with_zed_ai: Arc::new(|_, _| {}),
398                            sign_in: Arc::new(|_, _| {}),
399                            dismiss_onboarding: None,
400                        }
401                        .into_any_element(),
402                    ),
403                )
404                .into_any_element()
405        }
406
407        Some(
408            v_flex()
409                .min_w_0()
410                .gap_4()
411                .children(vec![
412                    single_example(
413                        "Not Signed-in",
414                        onboarding(SignInStatus::SignedOut, None, false),
415                    ),
416                    single_example(
417                        "Young Account",
418                        onboarding(SignInStatus::SignedIn, None, true),
419                    ),
420                    single_example(
421                        "Free Plan",
422                        onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false),
423                    ),
424                    single_example(
425                        "Pro Trial",
426                        onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false),
427                    ),
428                    single_example(
429                        "Pro Plan",
430                        onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false),
431                    ),
432                    single_example(
433                        "Business Plan",
434                        onboarding(SignInStatus::SignedIn, Some(Plan::ZedBusiness), false),
435                    ),
436                    single_example(
437                        "Student Plan",
438                        onboarding(SignInStatus::SignedIn, Some(Plan::ZedStudent), false),
439                    ),
440                ])
441                .into_any_element(),
442        )
443    }
444}
445
446#[derive(RegisterComponent)]
447pub struct AgentLayoutOnboarding {
448    pub use_agent_layout: Arc<dyn Fn(&mut Window, &mut App)>,
449    pub revert_to_editor_layout: Arc<dyn Fn(&mut Window, &mut App)>,
450    pub dismissed: Arc<dyn Fn(&mut Window, &mut App)>,
451    pub is_agent_layout: bool,
452}
453
454impl Render for AgentLayoutOnboarding {
455    fn render(&mut self, _window: &mut ui::Window, _cx: &mut Context<Self>) -> impl IntoElement {
456        let description = "The new threads sidebar, positioned in the far left of your workspace, allows you to manage agents across many projects. Your agent thread lives alongside it, and all other panels live on the right.";
457
458        let dismiss_button = div().absolute().top_1().right_1().child(
459            IconButton::new("dismiss", IconName::Close)
460                .icon_size(IconSize::Small)
461                .on_click({
462                    let dismiss = self.dismissed.clone();
463                    move |_, window, cx| {
464                        telemetry::event!("Agentic Layout Onboarding Dismissed");
465                        dismiss(window, cx)
466                    }
467                }),
468        );
469
470        let primary_button = if self.is_agent_layout {
471            Button::new("revert", "Use Previous Layout")
472                .label_size(LabelSize::Small)
473                .style(ButtonStyle::Outlined)
474                .on_click({
475                    let revert = self.revert_to_editor_layout.clone();
476                    let dismiss = self.dismissed.clone();
477                    move |_, window, cx| {
478                        telemetry::event!("Clicked to Use Previous Layout");
479                        revert(window, cx);
480                        dismiss(window, cx);
481                    }
482                })
483        } else {
484            Button::new("start", "Use New Layout")
485                .label_size(LabelSize::Small)
486                .style(ButtonStyle::Outlined)
487                .on_click({
488                    let use_layout = self.use_agent_layout.clone();
489                    let dismiss = self.dismissed.clone();
490                    move |_, window, cx| {
491                        telemetry::event!("Clicked to Use New Layout");
492                        use_layout(window, cx);
493                        dismiss(window, cx);
494                    }
495                })
496        };
497
498        let content = v_flex()
499            .min_w_0()
500            .w_full()
501            .relative()
502            .gap_1()
503            .child(Label::new("A new workspace layout for agentic work"))
504            .child(Label::new(description).color(Color::Muted).mb_2())
505            .child(
506                List::new()
507                    .child(ListBulletItem::new("Use your favorite agents in parallel"))
508                    .child(ListBulletItem::new("Isolate agents using worktrees"))
509                    .child(ListBulletItem::new(
510                        "Combine multiple projects in one window",
511                    )),
512            )
513            .child(
514                h_flex()
515                    .w_full()
516                    .gap_1()
517                    .flex_wrap()
518                    .justify_end()
519                    .child(primary_button),
520            )
521            .child(dismiss_button);
522
523        AgentPanelOnboardingCard::new().child(content)
524    }
525}
526
527impl Component for AgentLayoutOnboarding {
528    fn scope() -> ComponentScope {
529        ComponentScope::Onboarding
530    }
531
532    fn name() -> &'static str {
533        "Agent Layout Onboarding"
534    }
535
536    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
537        let onboarding = cx.new(|_cx| AgentLayoutOnboarding {
538            use_agent_layout: Arc::new(|_, _| {}),
539            revert_to_editor_layout: Arc::new(|_, _| {}),
540            dismissed: Arc::new(|_, _| {}),
541            is_agent_layout: false,
542        });
543
544        Some(
545            v_flex()
546                .min_w_0()
547                .gap_4()
548                .child(single_example(
549                    "Agent Layout Onboarding",
550                    div()
551                        .w_full()
552                        .min_w_40()
553                        .max_w(px(1100.))
554                        .child(onboarding)
555                        .into_any_element(),
556                ))
557                .into_any_element(),
558        )
559    }
560}