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