ai_onboarding.rs

  1mod agent_panel_onboarding_card;
  2mod agent_panel_onboarding_content;
  3mod edit_prediction_onboarding_content;
  4mod young_account_banner;
  5
  6pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
  7pub use agent_panel_onboarding_content::AgentPanelOnboarding;
  8pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
  9pub use young_account_banner::YoungAccountBanner;
 10
 11use std::sync::Arc;
 12
 13use client::{Client, UserStore, zed_urls};
 14use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
 15use ui::{Divider, List, ListItem, RegisterComponent, TintColor, prelude::*};
 16
 17pub struct BulletItem {
 18    label: SharedString,
 19}
 20
 21impl BulletItem {
 22    pub fn new(label: impl Into<SharedString>) -> Self {
 23        Self {
 24            label: label.into(),
 25        }
 26    }
 27}
 28
 29impl IntoElement for BulletItem {
 30    type Element = AnyElement;
 31
 32    fn into_element(self) -> Self::Element {
 33        ListItem::new("list-item")
 34            .selectable(false)
 35            .start_slot(
 36                Icon::new(IconName::Dash)
 37                    .size(IconSize::XSmall)
 38                    .color(Color::Hidden),
 39            )
 40            .child(div().w_full().child(Label::new(self.label)))
 41            .into_any_element()
 42    }
 43}
 44
 45pub enum SignInStatus {
 46    SignedIn,
 47    SigningIn,
 48    SignedOut,
 49}
 50
 51impl From<client::Status> for SignInStatus {
 52    fn from(status: client::Status) -> Self {
 53        if status.is_signing_in() {
 54            Self::SigningIn
 55        } else if status.is_signed_out() {
 56            Self::SignedOut
 57        } else {
 58            Self::SignedIn
 59        }
 60    }
 61}
 62
 63#[derive(RegisterComponent, IntoElement)]
 64pub struct ZedAiOnboarding {
 65    pub sign_in_status: SignInStatus,
 66    pub has_accepted_terms_of_service: bool,
 67    pub plan: Option<proto::Plan>,
 68    pub account_too_young: bool,
 69    pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
 70    pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
 71    pub accept_terms_of_service: Arc<dyn Fn(&mut Window, &mut App)>,
 72}
 73
 74impl ZedAiOnboarding {
 75    pub fn new(
 76        client: Arc<Client>,
 77        user_store: &Entity<UserStore>,
 78        continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
 79        cx: &mut App,
 80    ) -> Self {
 81        let store = user_store.read(cx);
 82        let status = *client.status().borrow();
 83        Self {
 84            sign_in_status: status.into(),
 85            has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false),
 86            plan: store.current_plan(),
 87            account_too_young: store.account_too_young(),
 88            continue_with_zed_ai,
 89            accept_terms_of_service: Arc::new({
 90                let store = user_store.clone();
 91                move |_window, cx| {
 92                    let task = store.update(cx, |store, cx| store.accept_terms_of_service(cx));
 93                    task.detach_and_log_err(cx);
 94                }
 95            }),
 96            sign_in: Arc::new(move |_window, cx| {
 97                cx.spawn({
 98                    let client = client.clone();
 99                    async move |cx| {
100                        client.authenticate_and_connect(true, cx).await;
101                    }
102                })
103                .detach();
104            }),
105        }
106    }
107
108    fn render_free_plan_section(&self, cx: &mut App) -> impl IntoElement {
109        v_flex()
110            .mt_2()
111            .gap_1()
112            .when(self.account_too_young, |this| this.opacity(0.4))
113            .child(
114                h_flex()
115                    .gap_2()
116                    .child(
117                        Label::new("Free")
118                            .size(LabelSize::Small)
119                            .color(Color::Muted)
120                            .buffer_font(cx),
121                    )
122                    .child(Divider::horizontal()),
123            )
124            .child(
125                List::new()
126                    .child(BulletItem::new(
127                        "50 prompts per month with the Claude models",
128                    ))
129                    .child(BulletItem::new(
130                        "2000 accepted edit predictions using our open-source Zeta model",
131                    )),
132            )
133            .child(
134                Button::new("continue", "Continue Free")
135                    .disabled(self.account_too_young)
136                    .full_width()
137                    .style(ButtonStyle::Outlined)
138                    .on_click({
139                        let callback = self.continue_with_zed_ai.clone();
140                        move |_, window, cx| callback(window, cx)
141                    }),
142            )
143    }
144
145    fn render_pro_plan_section(&self, cx: &mut App) -> impl IntoElement {
146        let (button_label, button_url) = if self.account_too_young {
147            ("Start with Pro", zed_urls::upgrade_to_zed_pro_url(cx))
148        } else {
149            ("Start Pro Trial", zed_urls::account_url(cx))
150        };
151
152        v_flex()
153            .mt_2()
154            .gap_1()
155            .child(
156                h_flex()
157                    .gap_2()
158                    .child(
159                        Label::new("Pro")
160                            .size(LabelSize::Small)
161                            .color(Color::Accent)
162                            .buffer_font(cx),
163                    )
164                    .child(Divider::horizontal()),
165            )
166            .child(
167                List::new()
168                    .child(BulletItem::new("500 prompts per month with Claude models"))
169                    .child(BulletItem::new("Unlimited edit predictions"))
170                    .when(!self.account_too_young, |this| {
171                        this.child(BulletItem::new(
172                            "Try it out for 14 days with no charge, no credit card required",
173                        ))
174                    }),
175            )
176            .child(
177                Button::new("pro", button_label)
178                    .full_width()
179                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
180                    .on_click(move |_, _window, cx| cx.open_url(&button_url)),
181            )
182    }
183
184    fn render_accept_terms_of_service(&self) -> Div {
185        v_flex()
186            .w_full()
187            .gap_1()
188            .child(Headline::new("Before starting…"))
189            .child(Label::new(
190                "Make sure you have read and accepted Zed AI's terms of service.",
191            ))
192            .child(
193                Button::new("terms_of_service", "View and Read the Terms of Service")
194                    .full_width()
195                    .style(ButtonStyle::Outlined)
196                    .icon(IconName::ArrowUpRight)
197                    .icon_color(Color::Muted)
198                    .icon_size(IconSize::XSmall)
199                    .on_click(move |_, _window, cx| {
200                        cx.open_url("https://zed.dev/terms-of-service")
201                    }),
202            )
203            .child(
204                Button::new("accept_terms", "I've read it and accept it")
205                    .full_width()
206                    .style(ButtonStyle::Tinted(TintColor::Accent))
207                    .on_click({
208                        let callback = self.accept_terms_of_service.clone();
209                        move |_, window, cx| (callback)(window, cx)
210                    }),
211            )
212    }
213
214    fn render_sign_in_disclaimer(&self, _cx: &mut App) -> Div {
215        const SIGN_IN_DISCLAIMER: &str =
216            "To start using AI in Zed with our hosted models, sign in and subscribe to a plan.";
217        let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
218
219        v_flex()
220            .gap_2()
221            .child(Headline::new("Welcome to Zed AI"))
222            .child(div().w_full().child(Label::new(SIGN_IN_DISCLAIMER)))
223            .child(
224                Button::new("sign_in", "Sign In with GitHub")
225                    .icon(IconName::Github)
226                    .icon_position(IconPosition::Start)
227                    .icon_size(IconSize::Small)
228                    .icon_color(Color::Muted)
229                    .disabled(signing_in)
230                    .full_width()
231                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
232                    .on_click({
233                        let callback = self.sign_in.clone();
234                        move |_, window, cx| callback(window, cx)
235                    }),
236            )
237    }
238
239    fn render_free_plan_onboarding(&self, cx: &mut App) -> Div {
240        const PLANS_DESCRIPTION: &str = "Choose how you want to start.";
241        let young_account_banner = YoungAccountBanner;
242
243        v_flex()
244            .child(Headline::new("Welcome to Zed AI"))
245            .child(
246                Label::new(PLANS_DESCRIPTION)
247                    .size(LabelSize::Small)
248                    .color(Color::Muted)
249                    .mt_1()
250                    .mb_3(),
251            )
252            .when(self.account_too_young, |this| {
253                this.child(young_account_banner)
254            })
255            .child(self.render_free_plan_section(cx))
256            .child(self.render_pro_plan_section(cx))
257    }
258
259    fn render_trial_onboarding(&self, _cx: &mut App) -> Div {
260        v_flex()
261            .child(Headline::new("Welcome to the trial of Zed Pro"))
262            .child(
263                Label::new("Here's what you get for the next 14 days:")
264                    .size(LabelSize::Small)
265                    .color(Color::Muted)
266                    .mt_1(),
267            )
268            .child(
269                List::new()
270                    .child(BulletItem::new("150 prompts with Claude models"))
271                    .child(BulletItem::new(
272                        "Unlimited edit predictions with Zeta, our open-source model",
273                    )),
274            )
275            .child(
276                Button::new("trial", "Start Trial")
277                    .full_width()
278                    .style(ButtonStyle::Outlined)
279                    .on_click({
280                        let callback = self.continue_with_zed_ai.clone();
281                        move |_, window, cx| callback(window, cx)
282                    }),
283            )
284    }
285
286    fn render_pro_plan_onboarding(&self, _cx: &mut App) -> Div {
287        v_flex()
288            .child(Headline::new("Welcome to Zed Pro"))
289            .child(
290                Label::new("Here's what you get:")
291                    .size(LabelSize::Small)
292                    .color(Color::Muted)
293                    .mt_1(),
294            )
295            .child(
296                List::new()
297                    .child(BulletItem::new("500 prompts with Claude models"))
298                    .child(BulletItem::new("Unlimited edit predictions")),
299            )
300            .child(
301                Button::new("pro", "Continue with Zed Pro")
302                    .full_width()
303                    .style(ButtonStyle::Outlined)
304                    .on_click({
305                        let callback = self.continue_with_zed_ai.clone();
306                        move |_, window, cx| callback(window, cx)
307                    }),
308            )
309    }
310}
311
312impl RenderOnce for ZedAiOnboarding {
313    fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
314        if matches!(self.sign_in_status, SignInStatus::SignedIn) {
315            if self.has_accepted_terms_of_service {
316                match self.plan {
317                    None | Some(proto::Plan::Free) => self.render_free_plan_onboarding(cx),
318                    Some(proto::Plan::ZedProTrial) => self.render_trial_onboarding(cx),
319                    Some(proto::Plan::ZedPro) => self.render_pro_plan_onboarding(cx),
320                }
321            } else {
322                self.render_accept_terms_of_service()
323            }
324        } else {
325            self.render_sign_in_disclaimer(cx)
326        }
327    }
328}
329
330impl Component for ZedAiOnboarding {
331    fn scope() -> ComponentScope {
332        ComponentScope::Agent
333    }
334
335    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
336        fn onboarding(
337            sign_in_status: SignInStatus,
338            has_accepted_terms_of_service: bool,
339            plan: Option<proto::Plan>,
340            account_too_young: bool,
341        ) -> AnyElement {
342            div()
343                .w(px(800.))
344                .child(ZedAiOnboarding {
345                    sign_in_status,
346                    has_accepted_terms_of_service,
347                    plan,
348                    account_too_young,
349                    continue_with_zed_ai: Arc::new(|_, _| {}),
350                    sign_in: Arc::new(|_, _| {}),
351                    accept_terms_of_service: Arc::new(|_, _| {}),
352                })
353                .into_any_element()
354        }
355
356        Some(
357            v_flex()
358                .p_4()
359                .gap_4()
360                .children(vec![
361                    single_example(
362                        "Not Signed-in",
363                        onboarding(SignInStatus::SignedOut, false, None, false),
364                    ),
365                    single_example(
366                        "Not Accepted ToS",
367                        onboarding(SignInStatus::SignedIn, false, None, false),
368                    ),
369                    single_example(
370                        "Account too young",
371                        onboarding(SignInStatus::SignedIn, true, None, true),
372                    ),
373                    single_example(
374                        "Free Plan",
375                        onboarding(SignInStatus::SignedIn, true, Some(proto::Plan::Free), false),
376                    ),
377                    single_example(
378                        "Pro Trial",
379                        onboarding(
380                            SignInStatus::SignedIn,
381                            true,
382                            Some(proto::Plan::ZedProTrial),
383                            false,
384                        ),
385                    ),
386                    single_example(
387                        "Pro Plan",
388                        onboarding(
389                            SignInStatus::SignedIn,
390                            true,
391                            Some(proto::Plan::ZedPro),
392                            false,
393                        ),
394                    ),
395                ])
396                .into_any_element(),
397        )
398    }
399}