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, is_v2: bool, 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(PlanDefinitions.pro_plan(is_v2, true))
140                        .child(
141                            Button::new("pro", "Get Started")
142                                .full_width()
143                                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
144                                .on_click(move |_, _window, cx| {
145                                    telemetry::event!(
146                                        "Upgrade To Pro Clicked",
147                                        state = "young-account"
148                                    );
149                                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
150                                }),
151                        ),
152                )
153                .into_any_element()
154        } else {
155            v_flex()
156                .relative()
157                .gap_1()
158                .child(Headline::new("Welcome to Zed AI"))
159                .child(
160                    v_flex()
161                        .mt_2()
162                        .gap_1()
163                        .child(
164                            h_flex()
165                                .gap_2()
166                                .child(
167                                    Label::new("Free")
168                                        .size(LabelSize::Small)
169                                        .color(Color::Muted)
170                                        .buffer_font(cx),
171                                )
172                                .child(
173                                    Label::new("(Current Plan)")
174                                        .size(LabelSize::Small)
175                                        .color(Color::Custom(
176                                            cx.theme().colors().text_muted.opacity(0.6),
177                                        ))
178                                        .buffer_font(cx),
179                                )
180                                .child(Divider::horizontal()),
181                        )
182                        .child(PlanDefinitions.free_plan(is_v2)),
183                )
184                .when_some(
185                    self.dismiss_onboarding.as_ref(),
186                    |this, dismiss_callback| {
187                        let callback = dismiss_callback.clone();
188
189                        this.child(
190                            h_flex().absolute().top_0().right_0().child(
191                                IconButton::new("dismiss_onboarding", IconName::Close)
192                                    .icon_size(IconSize::Small)
193                                    .tooltip(Tooltip::text("Dismiss"))
194                                    .on_click(move |_, window, cx| {
195                                        telemetry::event!(
196                                            "Banner Dismissed",
197                                            source = "AI Onboarding",
198                                        );
199                                        callback(window, cx)
200                                    }),
201                            ),
202                        )
203                    },
204                )
205                .child(
206                    v_flex()
207                        .mt_2()
208                        .gap_1()
209                        .child(
210                            h_flex()
211                                .gap_2()
212                                .child(
213                                    Label::new("Pro Trial")
214                                        .size(LabelSize::Small)
215                                        .color(Color::Accent)
216                                        .buffer_font(cx),
217                                )
218                                .child(Divider::horizontal()),
219                        )
220                        .child(PlanDefinitions.pro_trial(is_v2, true))
221                        .child(
222                            Button::new("pro", "Start Free Trial")
223                                .full_width()
224                                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
225                                .on_click(move |_, _window, cx| {
226                                    telemetry::event!(
227                                        "Start Trial Clicked",
228                                        state = "post-sign-in"
229                                    );
230                                    cx.open_url(&zed_urls::start_trial_url(cx))
231                                }),
232                        ),
233                )
234                .into_any_element()
235        }
236    }
237
238    fn render_trial_state(&self, is_v2: bool, _cx: &mut App) -> AnyElement {
239        v_flex()
240            .relative()
241            .gap_1()
242            .child(Headline::new("Welcome to the Zed Pro Trial"))
243            .child(
244                Label::new("Here's what you get for the next 14 days:")
245                    .color(Color::Muted)
246                    .mb_2(),
247            )
248            .child(PlanDefinitions.pro_trial(is_v2, false))
249            .when_some(
250                self.dismiss_onboarding.as_ref(),
251                |this, dismiss_callback| {
252                    let callback = dismiss_callback.clone();
253                    this.child(
254                        h_flex().absolute().top_0().right_0().child(
255                            IconButton::new("dismiss_onboarding", IconName::Close)
256                                .icon_size(IconSize::Small)
257                                .tooltip(Tooltip::text("Dismiss"))
258                                .on_click(move |_, window, cx| {
259                                    telemetry::event!(
260                                        "Banner Dismissed",
261                                        source = "AI Onboarding",
262                                    );
263                                    callback(window, cx)
264                                }),
265                        ),
266                    )
267                },
268            )
269            .into_any_element()
270    }
271
272    fn render_pro_plan_state(&self, is_v2: bool, _cx: &mut App) -> AnyElement {
273        v_flex()
274            .gap_1()
275            .child(Headline::new("Welcome to Zed Pro"))
276            .child(
277                Label::new("Here's what you get:")
278                    .color(Color::Muted)
279                    .mb_2(),
280            )
281            .child(PlanDefinitions.pro_plan(is_v2, false))
282            .when_some(
283                self.dismiss_onboarding.as_ref(),
284                |this, dismiss_callback| {
285                    let callback = dismiss_callback.clone();
286                    this.child(
287                        h_flex().absolute().top_0().right_0().child(
288                            IconButton::new("dismiss_onboarding", IconName::Close)
289                                .icon_size(IconSize::Small)
290                                .tooltip(Tooltip::text("Dismiss"))
291                                .on_click(move |_, window, cx| {
292                                    telemetry::event!(
293                                        "Banner Dismissed",
294                                        source = "AI Onboarding",
295                                    );
296                                    callback(window, cx)
297                                }),
298                        ),
299                    )
300                },
301            )
302            .into_any_element()
303    }
304}
305
306impl RenderOnce for ZedAiOnboarding {
307    fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
308        if matches!(self.sign_in_status, SignInStatus::SignedIn) {
309            match self.plan {
310                None => self.render_free_plan_state(cx.has_flag::<BillingV2FeatureFlag>(), cx),
311                Some(plan @ (Plan::ZedFree | Plan::ZedFreeV2)) => {
312                    self.render_free_plan_state(plan.is_v2(), cx)
313                }
314                Some(plan @ (Plan::ZedProTrial | Plan::ZedProTrialV2)) => {
315                    self.render_trial_state(plan.is_v2(), cx)
316                }
317                Some(plan @ (Plan::ZedPro | Plan::ZedProV2)) => {
318                    self.render_pro_plan_state(plan.is_v2(), cx)
319                }
320            }
321        } else {
322            self.render_sign_in_disclaimer(cx)
323        }
324    }
325}
326
327impl Component for ZedAiOnboarding {
328    fn scope() -> ComponentScope {
329        ComponentScope::Onboarding
330    }
331
332    fn name() -> &'static str {
333        "Agent Panel Banners"
334    }
335
336    fn sort_name() -> &'static str {
337        "Agent Panel Banners"
338    }
339
340    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
341        fn onboarding(
342            sign_in_status: SignInStatus,
343            plan: Option<Plan>,
344            account_too_young: bool,
345        ) -> AnyElement {
346            ZedAiOnboarding {
347                sign_in_status,
348                plan,
349                account_too_young,
350                continue_with_zed_ai: Arc::new(|_, _| {}),
351                sign_in: Arc::new(|_, _| {}),
352                dismiss_onboarding: None,
353            }
354            .into_any_element()
355        }
356
357        Some(
358            v_flex()
359                .gap_4()
360                .items_center()
361                .max_w_4_5()
362                .children(vec![
363                    single_example(
364                        "Not Signed-in",
365                        onboarding(SignInStatus::SignedOut, None, false),
366                    ),
367                    single_example(
368                        "Young Account",
369                        onboarding(SignInStatus::SignedIn, None, true),
370                    ),
371                    single_example(
372                        "Free Plan",
373                        onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false),
374                    ),
375                    single_example(
376                        "Pro Trial",
377                        onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false),
378                    ),
379                    single_example(
380                        "Pro Plan",
381                        onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false),
382                    ),
383                ])
384                .into_any_element(),
385        )
386    }
387}