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