ai_onboarding.rs

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