ai_onboarding.rs

  1mod agent_api_keys_onboarding;
  2mod agent_panel_onboarding_card;
  3mod agent_panel_onboarding_content;
  4mod edit_prediction_onboarding_content;
  5mod young_account_banner;
  6
  7pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders};
  8pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
  9pub use agent_panel_onboarding_content::AgentPanelOnboarding;
 10pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
 11pub use young_account_banner::YoungAccountBanner;
 12
 13use std::sync::Arc;
 14
 15use client::{Client, UserStore, zed_urls};
 16use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
 17use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*};
 18
 19#[derive(IntoElement)]
 20pub struct BulletItem {
 21    label: SharedString,
 22}
 23
 24impl BulletItem {
 25    pub fn new(label: impl Into<SharedString>) -> Self {
 26        Self {
 27            label: label.into(),
 28        }
 29    }
 30}
 31
 32impl RenderOnce for BulletItem {
 33    fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
 34        let line_height = 0.85 * window.line_height();
 35
 36        ListItem::new("list-item")
 37            .selectable(false)
 38            .child(
 39                h_flex()
 40                    .w_full()
 41                    .min_w_0()
 42                    .gap_1()
 43                    .items_start()
 44                    .child(
 45                        h_flex().h(line_height).justify_center().child(
 46                            Icon::new(IconName::Dash)
 47                                .size(IconSize::XSmall)
 48                                .color(Color::Hidden),
 49                        ),
 50                    )
 51                    .child(div().w_full().min_w_0().child(Label::new(self.label))),
 52            )
 53            .into_any_element()
 54    }
 55}
 56
 57pub enum SignInStatus {
 58    SignedIn,
 59    SigningIn,
 60    SignedOut,
 61}
 62
 63impl From<client::Status> for SignInStatus {
 64    fn from(status: client::Status) -> Self {
 65        if status.is_signing_in() {
 66            Self::SigningIn
 67        } else if status.is_signed_out() {
 68            Self::SignedOut
 69        } else {
 70            Self::SignedIn
 71        }
 72    }
 73}
 74
 75#[derive(RegisterComponent, IntoElement)]
 76pub struct ZedAiOnboarding {
 77    pub sign_in_status: SignInStatus,
 78    pub has_accepted_terms_of_service: bool,
 79    pub plan: Option<proto::Plan>,
 80    pub account_too_young: bool,
 81    pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
 82    pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
 83    pub accept_terms_of_service: Arc<dyn Fn(&mut Window, &mut App)>,
 84    pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
 85}
 86
 87impl ZedAiOnboarding {
 88    pub fn new(
 89        client: Arc<Client>,
 90        user_store: &Entity<UserStore>,
 91        continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
 92        cx: &mut App,
 93    ) -> Self {
 94        let store = user_store.read(cx);
 95        let status = *client.status().borrow();
 96
 97        Self {
 98            sign_in_status: status.into(),
 99            has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false),
100            plan: store.current_plan(),
101            account_too_young: store.account_too_young(),
102            continue_with_zed_ai,
103            accept_terms_of_service: Arc::new({
104                let store = user_store.clone();
105                move |_window, cx| {
106                    let task = store.update(cx, |store, cx| store.accept_terms_of_service(cx));
107                    task.detach_and_log_err(cx);
108                }
109            }),
110            sign_in: Arc::new(move |_window, cx| {
111                cx.spawn({
112                    let client = client.clone();
113                    async move |cx| {
114                        client.authenticate_and_connect(true, cx).await;
115                    }
116                })
117                .detach();
118            }),
119            dismiss_onboarding: None,
120        }
121    }
122
123    pub fn with_dismiss(
124        mut self,
125        dismiss_callback: impl Fn(&mut Window, &mut App) + 'static,
126    ) -> Self {
127        self.dismiss_onboarding = Some(Arc::new(dismiss_callback));
128        self
129    }
130
131    fn free_plan_definition(&self, cx: &mut App) -> impl IntoElement {
132        v_flex()
133            .mt_2()
134            .gap_1()
135            .child(
136                h_flex()
137                    .gap_2()
138                    .child(
139                        Label::new("Free")
140                            .size(LabelSize::Small)
141                            .color(Color::Muted)
142                            .buffer_font(cx),
143                    )
144                    .child(
145                        Label::new("(Current Plan)")
146                            .size(LabelSize::Small)
147                            .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6)))
148                            .buffer_font(cx),
149                    )
150                    .child(Divider::horizontal()),
151            )
152            .child(
153                List::new()
154                    .child(BulletItem::new("50 prompts per month with Claude models"))
155                    .child(BulletItem::new(
156                        "2,000 accepted edit predictions with Zeta, our open-source model",
157                    )),
158            )
159    }
160
161    fn pro_trial_definition(&self) -> impl IntoElement {
162        List::new()
163            .child(BulletItem::new("150 prompts with Claude models"))
164            .child(BulletItem::new(
165                "Unlimited accepted edit predictions with Zeta, our open-source model",
166            ))
167    }
168
169    fn pro_plan_definition(&self, cx: &mut App) -> impl IntoElement {
170        v_flex().mt_2().gap_1().map(|this| {
171            if self.account_too_young {
172                this.child(
173                    h_flex()
174                        .gap_2()
175                        .child(
176                            Label::new("Pro")
177                                .size(LabelSize::Small)
178                                .color(Color::Accent)
179                                .buffer_font(cx),
180                        )
181                        .child(Divider::horizontal()),
182                )
183                .child(
184                    List::new()
185                        .child(BulletItem::new("500 prompts per month with Claude models"))
186                        .child(BulletItem::new(
187                            "Unlimited accepted edit predictions with Zeta, our open-source model",
188                        ))
189                        .child(BulletItem::new("$20 USD per month")),
190                )
191                .child(
192                    Button::new("pro", "Get Started")
193                        .full_width()
194                        .style(ButtonStyle::Tinted(ui::TintColor::Accent))
195                        .on_click(move |_, _window, cx| {
196                            telemetry::event!("Upgrade To Pro Clicked", state = "young-account");
197                            cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
198                        }),
199                )
200            } else {
201                this.child(
202                    h_flex()
203                        .gap_2()
204                        .child(
205                            Label::new("Pro Trial")
206                                .size(LabelSize::Small)
207                                .color(Color::Accent)
208                                .buffer_font(cx),
209                        )
210                        .child(Divider::horizontal()),
211                )
212                .child(
213                    List::new()
214                        .child(self.pro_trial_definition())
215                        .child(BulletItem::new(
216                            "Try it out for 14 days for free, no credit card required",
217                        )),
218                )
219                .child(
220                    Button::new("pro", "Start Free Trial")
221                        .full_width()
222                        .style(ButtonStyle::Tinted(ui::TintColor::Accent))
223                        .on_click(move |_, _window, cx| {
224                            telemetry::event!("Start Trial Clicked", state = "post-sign-in");
225                            cx.open_url(&zed_urls::start_trial_url(cx))
226                        }),
227                )
228            }
229        })
230    }
231
232    fn render_accept_terms_of_service(&self) -> AnyElement {
233        v_flex()
234            .gap_1()
235            .w_full()
236            .child(Headline::new("Accept Terms of Service"))
237            .child(
238                Label::new("We don’t sell your data, track you across the web, or compromise your privacy.")
239                    .color(Color::Muted)
240                    .mb_2(),
241            )
242            .child(
243                Button::new("terms_of_service", "Review Terms of Service")
244                    .full_width()
245                    .style(ButtonStyle::Outlined)
246                    .icon(IconName::ArrowUpRight)
247                    .icon_color(Color::Muted)
248                    .icon_size(IconSize::XSmall)
249                    .on_click(move |_, _window, cx| {
250                        telemetry::event!("Review Terms of Service Clicked");
251                        cx.open_url(&zed_urls::terms_of_service(cx))
252                    }),
253            )
254            .child(
255                Button::new("accept_terms", "Accept")
256                    .full_width()
257                    .style(ButtonStyle::Tinted(TintColor::Accent))
258                    .on_click({
259                        let callback = self.accept_terms_of_service.clone();
260                        move |_, window, cx| {
261                            telemetry::event!("Terms of Service Accepted");
262                            (callback)(window, cx)}
263                    }),
264            )
265            .into_any_element()
266    }
267
268    fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
269        let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
270
271        v_flex()
272            .gap_1()
273            .child(Headline::new("Welcome to Zed AI"))
274            .child(
275                Label::new("Sign in to try Zed Pro for 14 days, no credit card required.")
276                    .color(Color::Muted)
277                    .mb_2(),
278            )
279            .child(self.pro_trial_definition())
280            .child(
281                Button::new("sign_in", "Try Zed Pro for Free")
282                    .disabled(signing_in)
283                    .full_width()
284                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
285                    .on_click({
286                        let callback = self.sign_in.clone();
287                        move |_, window, cx| {
288                            telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
289                            callback(window, cx)
290                        }
291                    }),
292            )
293            .into_any_element()
294    }
295
296    fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
297        let young_account_banner = YoungAccountBanner;
298
299        v_flex()
300            .relative()
301            .gap_1()
302            .child(Headline::new("Welcome to Zed AI"))
303            .map(|this| {
304                if self.account_too_young {
305                    this.child(young_account_banner)
306                } else {
307                    this.child(self.free_plan_definition(cx)).when_some(
308                        self.dismiss_onboarding.as_ref(),
309                        |this, dismiss_callback| {
310                            let callback = dismiss_callback.clone();
311
312                            this.child(
313                                h_flex().absolute().top_0().right_0().child(
314                                    IconButton::new("dismiss_onboarding", IconName::Close)
315                                        .icon_size(IconSize::Small)
316                                        .tooltip(Tooltip::text("Dismiss"))
317                                        .on_click(move |_, window, cx| {
318                                            telemetry::event!(
319                                                "Banner Dismissed",
320                                                source = "AI Onboarding",
321                                            );
322                                            callback(window, cx)
323                                        }),
324                                ),
325                            )
326                        },
327                    )
328                }
329            })
330            .child(self.pro_plan_definition(cx))
331            .into_any_element()
332    }
333
334    fn render_trial_state(&self, _cx: &mut App) -> AnyElement {
335        v_flex()
336            .relative()
337            .gap_1()
338            .child(Headline::new("Welcome to the Zed Pro Trial"))
339            .child(
340                Label::new("Here's what you get for the next 14 days:")
341                    .color(Color::Muted)
342                    .mb_2(),
343            )
344            .child(
345                List::new()
346                    .child(BulletItem::new("150 prompts with Claude models"))
347                    .child(BulletItem::new(
348                        "Unlimited edit predictions with Zeta, our open-source model",
349                    )),
350            )
351            .when_some(
352                self.dismiss_onboarding.as_ref(),
353                |this, dismiss_callback| {
354                    let callback = dismiss_callback.clone();
355                    this.child(
356                        h_flex().absolute().top_0().right_0().child(
357                            IconButton::new("dismiss_onboarding", IconName::Close)
358                                .icon_size(IconSize::Small)
359                                .tooltip(Tooltip::text("Dismiss"))
360                                .on_click(move |_, window, cx| {
361                                    telemetry::event!(
362                                        "Banner Dismissed",
363                                        source = "AI Onboarding",
364                                    );
365                                    callback(window, cx)
366                                }),
367                        ),
368                    )
369                },
370            )
371            .into_any_element()
372    }
373
374    fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement {
375        v_flex()
376            .gap_1()
377            .child(Headline::new("Welcome to Zed Pro"))
378            .child(
379                Label::new("Here's what you get:")
380                    .color(Color::Muted)
381                    .mb_2(),
382            )
383            .child(
384                List::new()
385                    .child(BulletItem::new("500 prompts with Claude models"))
386                    .child(BulletItem::new(
387                        "Unlimited edit predictions with Zeta, our open-source model",
388                    )),
389            )
390            .child(
391                Button::new("pro", "Continue with Zed Pro")
392                    .full_width()
393                    .style(ButtonStyle::Outlined)
394                    .on_click({
395                        let callback = self.continue_with_zed_ai.clone();
396                        move |_, window, cx| {
397                            telemetry::event!("Banner Dismissed", source = "AI Onboarding");
398                            callback(window, cx)
399                        }
400                    }),
401            )
402            .into_any_element()
403    }
404}
405
406impl RenderOnce for ZedAiOnboarding {
407    fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
408        if matches!(self.sign_in_status, SignInStatus::SignedIn) {
409            if self.has_accepted_terms_of_service {
410                match self.plan {
411                    None | Some(proto::Plan::Free) => self.render_free_plan_state(cx),
412                    Some(proto::Plan::ZedProTrial) => self.render_trial_state(cx),
413                    Some(proto::Plan::ZedPro) => self.render_pro_plan_state(cx),
414                }
415            } else {
416                self.render_accept_terms_of_service()
417            }
418        } else {
419            self.render_sign_in_disclaimer(cx)
420        }
421    }
422}
423
424impl Component for ZedAiOnboarding {
425    fn scope() -> ComponentScope {
426        ComponentScope::Agent
427    }
428
429    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
430        fn onboarding(
431            sign_in_status: SignInStatus,
432            has_accepted_terms_of_service: bool,
433            plan: Option<proto::Plan>,
434            account_too_young: bool,
435        ) -> AnyElement {
436            ZedAiOnboarding {
437                sign_in_status,
438                has_accepted_terms_of_service,
439                plan,
440                account_too_young,
441                continue_with_zed_ai: Arc::new(|_, _| {}),
442                sign_in: Arc::new(|_, _| {}),
443                accept_terms_of_service: Arc::new(|_, _| {}),
444                dismiss_onboarding: None,
445            }
446            .into_any_element()
447        }
448
449        Some(
450            v_flex()
451                .p_4()
452                .gap_4()
453                .children(vec![
454                    single_example(
455                        "Not Signed-in",
456                        onboarding(SignInStatus::SignedOut, false, None, false),
457                    ),
458                    single_example(
459                        "Not Accepted ToS",
460                        onboarding(SignInStatus::SignedIn, false, None, false),
461                    ),
462                    single_example(
463                        "Account too young",
464                        onboarding(SignInStatus::SignedIn, false, None, true),
465                    ),
466                    single_example(
467                        "Free Plan",
468                        onboarding(SignInStatus::SignedIn, true, Some(proto::Plan::Free), false),
469                    ),
470                    single_example(
471                        "Pro Trial",
472                        onboarding(
473                            SignInStatus::SignedIn,
474                            true,
475                            Some(proto::Plan::ZedProTrial),
476                            false,
477                        ),
478                    ),
479                    single_example(
480                        "Pro Plan",
481                        onboarding(
482                            SignInStatus::SignedIn,
483                            true,
484                            Some(proto::Plan::ZedPro),
485                            false,
486                        ),
487                    ),
488                ])
489                .into_any_element(),
490        )
491    }
492}