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 = "With the new Threads Sidebar, you can manage multiple agents across several projects, all in one window.";
457
458        let dismiss_button = div().absolute().top_0().right_0().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 workflows"))
504            .child(Label::new(description).color(Color::Muted).mb_2())
505            .child(
506                List::new()
507                    .child(ListBulletItem::new(
508                        "The Sidebar and Agent Panel are on the left by default",
509                    ))
510                    .child(ListBulletItem::new(
511                        "The Project Panel and all other panels shift to the right",
512                    ))
513                    .child(ListBulletItem::new(
514                        "You can always customize your workspace layout in your Settings",
515                    )),
516            )
517            .child(
518                h_flex()
519                    .w_full()
520                    .gap_1()
521                    .flex_wrap()
522                    .justify_end()
523                    .child(
524                        Button::new("learn", "Learn More")
525                            .label_size(LabelSize::Small)
526                            .style(ButtonStyle::OutlinedGhost)
527                            .on_click(move |_, _, cx| {
528                                cx.open_url(&zed_urls::parallel_agents_blog(cx))
529                            }),
530                    )
531                    .child(primary_button),
532            )
533            .child(dismiss_button);
534
535        AgentPanelOnboardingCard::new().child(content)
536    }
537}
538
539impl Component for AgentLayoutOnboarding {
540    fn scope() -> ComponentScope {
541        ComponentScope::Onboarding
542    }
543
544    fn name() -> &'static str {
545        "Agent Layout Onboarding"
546    }
547
548    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
549        let onboarding = cx.new(|_cx| AgentLayoutOnboarding {
550            use_agent_layout: Arc::new(|_, _| {}),
551            revert_to_editor_layout: Arc::new(|_, _| {}),
552            dismissed: Arc::new(|_, _| {}),
553            is_agent_layout: false,
554        });
555
556        Some(
557            v_flex()
558                .min_w_0()
559                .gap_4()
560                .child(single_example(
561                    "Agent Layout Onboarding",
562                    div()
563                        .w_full()
564                        .min_w_40()
565                        .max_w(px(1100.))
566                        .child(onboarding)
567                        .into_any_element(),
568                ))
569                .into_any_element(),
570        )
571    }
572}