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(
145                        "50 prompts per month with the Claude models",
146                    ))
147                    .child(BulletItem::new(
148                        "2000 accepted edit predictions using our open-source Zeta model",
149                    )),
150            )
151    }
152
153    fn pro_trial_definition(&self) -> impl IntoElement {
154        List::new()
155            .child(BulletItem::new(
156                "150 prompts per month with the Claude models",
157            ))
158            .child(BulletItem::new(
159                "Unlimited accepted edit predictions using our open-source Zeta model",
160            ))
161    }
162
163    fn pro_plan_definition(&self, cx: &mut App) -> impl IntoElement {
164        v_flex().mt_2().gap_1().map(|this| {
165            if self.account_too_young {
166                this.child(
167                    h_flex()
168                        .gap_2()
169                        .child(
170                            Label::new("Pro")
171                                .size(LabelSize::Small)
172                                .color(Color::Accent)
173                                .buffer_font(cx),
174                        )
175                        .child(Divider::horizontal()),
176                )
177                .child(
178                    List::new()
179                        .child(BulletItem::new("500 prompts per month with Claude models"))
180                        .child(BulletItem::new(
181                            "Unlimited accepted edit predictions using our open-source Zeta model",
182                        ))
183                        .child(BulletItem::new("USD $20 per month")),
184                )
185                .child(
186                    Button::new("pro", "Start with Pro")
187                        .full_width()
188                        .style(ButtonStyle::Tinted(ui::TintColor::Accent))
189                        .on_click(move |_, _window, cx| {
190                            cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
191                        }),
192                )
193            } else {
194                this.child(
195                    h_flex()
196                        .gap_2()
197                        .child(
198                            Label::new("Pro Trial")
199                                .size(LabelSize::Small)
200                                .color(Color::Accent)
201                                .buffer_font(cx),
202                        )
203                        .child(Divider::horizontal()),
204                )
205                .child(
206                    List::new()
207                        .child(self.pro_trial_definition())
208                        .child(BulletItem::new(
209                            "Try it out for 14 days with no charge and no credit card required",
210                        )),
211                )
212                .child(
213                    Button::new("pro", "Start Pro Trial")
214                        .full_width()
215                        .style(ButtonStyle::Tinted(ui::TintColor::Accent))
216                        .on_click(move |_, _window, cx| {
217                            cx.open_url(&zed_urls::start_trial_url(cx))
218                        }),
219                )
220            }
221        })
222    }
223
224    fn render_accept_terms_of_service(&self) -> AnyElement {
225        v_flex()
226            .gap_1()
227            .w_full()
228            .child(Headline::new("Before starting…"))
229            .child(
230                Label::new("Make sure you have read and accepted Zed AI's terms of service.")
231                    .color(Color::Muted)
232                    .mb_2(),
233            )
234            .child(
235                Button::new("terms_of_service", "View and Read the Terms of Service")
236                    .full_width()
237                    .style(ButtonStyle::Outlined)
238                    .icon(IconName::ArrowUpRight)
239                    .icon_color(Color::Muted)
240                    .icon_size(IconSize::XSmall)
241                    .on_click(move |_, _window, cx| cx.open_url(&zed_urls::terms_of_service(cx))),
242            )
243            .child(
244                Button::new("accept_terms", "I've read it and accept it")
245                    .full_width()
246                    .style(ButtonStyle::Tinted(TintColor::Accent))
247                    .on_click({
248                        let callback = self.accept_terms_of_service.clone();
249                        move |_, window, cx| (callback)(window, cx)
250                    }),
251            )
252            .into_any_element()
253    }
254
255    fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
256        let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
257
258        v_flex()
259            .gap_1()
260            .child(Headline::new("Welcome to Zed AI"))
261            .child(
262                Label::new("Sign in to start using AI in Zed with a free trial of the Pro plan, which includes:")
263                    .color(Color::Muted)
264                    .mb_2(),
265            )
266            .child(self.pro_trial_definition())
267            .child(
268                Button::new("sign_in", "Sign in to Start Trial")
269                    .disabled(signing_in)
270                    .full_width()
271                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
272                    .on_click({
273                        let callback = self.sign_in.clone();
274                        move |_, window, cx| callback(window, cx)
275                    }),
276            )
277            .into_any_element()
278    }
279
280    fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
281        let young_account_banner = YoungAccountBanner;
282
283        v_flex()
284            .relative()
285            .gap_1()
286            .child(Headline::new("Welcome to Zed AI"))
287            .child(
288                Label::new("Choose how you want to start.")
289                    .color(Color::Muted)
290                    .mb_2(),
291            )
292            .map(|this| {
293                if self.account_too_young {
294                    this.child(young_account_banner)
295                } else {
296                    this.child(self.free_plan_definition(cx)).when_some(
297                        self.dismiss_onboarding.as_ref(),
298                        |this, dismiss_callback| {
299                            let callback = dismiss_callback.clone();
300
301                            this.child(
302                                h_flex().absolute().top_0().right_0().child(
303                                    IconButton::new("dismiss_onboarding", IconName::Close)
304                                        .icon_size(IconSize::Small)
305                                        .tooltip(Tooltip::text("Dismiss"))
306                                        .on_click(move |_, window, cx| callback(window, cx)),
307                                ),
308                            )
309                        },
310                    )
311                }
312            })
313            .child(self.pro_plan_definition(cx))
314            .into_any_element()
315    }
316
317    fn render_trial_state(&self, _cx: &mut App) -> AnyElement {
318        v_flex()
319            .relative()
320            .gap_1()
321            .child(Headline::new("Welcome to the Zed Pro free trial"))
322            .child(
323                Label::new("Here's what you get for the next 14 days:")
324                    .color(Color::Muted)
325                    .mb_2(),
326            )
327            .child(
328                List::new()
329                    .child(BulletItem::new("150 prompts with Claude models"))
330                    .child(BulletItem::new(
331                        "Unlimited edit predictions with Zeta, our open-source model",
332                    )),
333            )
334            .when_some(
335                self.dismiss_onboarding.as_ref(),
336                |this, dismiss_callback| {
337                    let callback = dismiss_callback.clone();
338                    this.child(
339                        h_flex().absolute().top_0().right_0().child(
340                            IconButton::new("dismiss_onboarding", IconName::Close)
341                                .icon_size(IconSize::Small)
342                                .tooltip(Tooltip::text("Dismiss"))
343                                .on_click(move |_, window, cx| callback(window, cx)),
344                        ),
345                    )
346                },
347            )
348            .into_any_element()
349    }
350
351    fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement {
352        v_flex()
353            .gap_1()
354            .child(Headline::new("Welcome to Zed Pro"))
355            .child(
356                Label::new("Here's what you get:")
357                    .color(Color::Muted)
358                    .mb_2(),
359            )
360            .child(
361                List::new()
362                    .child(BulletItem::new("500 prompts with Claude models"))
363                    .child(BulletItem::new("Unlimited edit predictions")),
364            )
365            .child(
366                Button::new("pro", "Continue with Zed Pro")
367                    .full_width()
368                    .style(ButtonStyle::Outlined)
369                    .on_click({
370                        let callback = self.continue_with_zed_ai.clone();
371                        move |_, window, cx| callback(window, cx)
372                    }),
373            )
374            .into_any_element()
375    }
376}
377
378impl RenderOnce for ZedAiOnboarding {
379    fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
380        if matches!(self.sign_in_status, SignInStatus::SignedIn) {
381            if self.has_accepted_terms_of_service {
382                match self.plan {
383                    None | Some(proto::Plan::Free) => self.render_free_plan_state(cx),
384                    Some(proto::Plan::ZedProTrial) => self.render_trial_state(cx),
385                    Some(proto::Plan::ZedPro) => self.render_pro_plan_state(cx),
386                }
387            } else {
388                self.render_accept_terms_of_service()
389            }
390        } else {
391            self.render_sign_in_disclaimer(cx)
392        }
393    }
394}
395
396impl Component for ZedAiOnboarding {
397    fn scope() -> ComponentScope {
398        ComponentScope::Agent
399    }
400
401    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
402        fn onboarding(
403            sign_in_status: SignInStatus,
404            has_accepted_terms_of_service: bool,
405            plan: Option<proto::Plan>,
406            account_too_young: bool,
407        ) -> AnyElement {
408            ZedAiOnboarding {
409                sign_in_status,
410                has_accepted_terms_of_service,
411                plan,
412                account_too_young,
413                continue_with_zed_ai: Arc::new(|_, _| {}),
414                sign_in: Arc::new(|_, _| {}),
415                accept_terms_of_service: Arc::new(|_, _| {}),
416                dismiss_onboarding: None,
417            }
418            .into_any_element()
419        }
420
421        Some(
422            v_flex()
423                .p_4()
424                .gap_4()
425                .children(vec![
426                    single_example(
427                        "Not Signed-in",
428                        onboarding(SignInStatus::SignedOut, false, None, false),
429                    ),
430                    single_example(
431                        "Not Accepted ToS",
432                        onboarding(SignInStatus::SignedIn, false, None, false),
433                    ),
434                    single_example(
435                        "Account too young",
436                        onboarding(SignInStatus::SignedIn, false, None, true),
437                    ),
438                    single_example(
439                        "Free Plan",
440                        onboarding(SignInStatus::SignedIn, true, Some(proto::Plan::Free), false),
441                    ),
442                    single_example(
443                        "Pro Trial",
444                        onboarding(
445                            SignInStatus::SignedIn,
446                            true,
447                            Some(proto::Plan::ZedProTrial),
448                            false,
449                        ),
450                    ),
451                    single_example(
452                        "Pro Plan",
453                        onboarding(
454                            SignInStatus::SignedIn,
455                            true,
456                            Some(proto::Plan::ZedPro),
457                            false,
458                        ),
459                    ),
460                ])
461                .into_any_element(),
462        )
463    }
464}