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