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