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, PlanV1, PlanV2};
 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, 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 plan: Option<Plan>,
 47    pub account_too_young: bool,
 48    pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
 49    pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
 50    pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
 51}
 52
 53impl ZedAiOnboarding {
 54    pub fn new(
 55        client: Arc<Client>,
 56        user_store: &Entity<UserStore>,
 57        continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
 58        cx: &mut App,
 59    ) -> Self {
 60        let store = user_store.read(cx);
 61        let status = *client.status().borrow();
 62
 63        Self {
 64            sign_in_status: status.into(),
 65            plan: store.plan(),
 66            account_too_young: store.account_too_young(),
 67            continue_with_zed_ai,
 68            sign_in: Arc::new(move |_window, cx| {
 69                cx.spawn({
 70                    let client = client.clone();
 71                    async move |cx| client.sign_in_with_optional_connect(true, cx).await
 72                })
 73                .detach_and_log_err(cx);
 74            }),
 75            dismiss_onboarding: None,
 76        }
 77    }
 78
 79    pub fn with_dismiss(
 80        mut self,
 81        dismiss_callback: impl Fn(&mut Window, &mut App) + 'static,
 82    ) -> Self {
 83        self.dismiss_onboarding = Some(Arc::new(dismiss_callback));
 84        self
 85    }
 86
 87    fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
 88        let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
 89
 90        v_flex()
 91            .gap_1()
 92            .child(Headline::new("Welcome to Zed AI"))
 93            .child(
 94                Label::new("Sign in to try Zed Pro for 14 days, no credit card required.")
 95                    .color(Color::Muted)
 96                    .mb_2(),
 97            )
 98            .child(PlanDefinitions.pro_plan(true, false))
 99            .child(
100                Button::new("sign_in", "Try Zed Pro for Free")
101                    .disabled(signing_in)
102                    .full_width()
103                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
104                    .on_click({
105                        let callback = self.sign_in.clone();
106                        move |_, window, cx| {
107                            telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
108                            callback(window, cx)
109                        }
110                    }),
111            )
112            .into_any_element()
113    }
114
115    fn render_free_plan_state(&self, is_v2: bool, cx: &mut App) -> AnyElement {
116        if self.account_too_young {
117            v_flex()
118                .relative()
119                .max_w_full()
120                .gap_1()
121                .child(Headline::new("Welcome to Zed AI"))
122                .child(YoungAccountBanner)
123                .child(
124                    v_flex()
125                        .mt_2()
126                        .gap_1()
127                        .child(
128                            h_flex()
129                                .gap_2()
130                                .child(
131                                    Label::new("Pro")
132                                        .size(LabelSize::Small)
133                                        .color(Color::Accent)
134                                        .buffer_font(cx),
135                                )
136                                .child(Divider::horizontal()),
137                        )
138                        .child(PlanDefinitions.pro_plan(is_v2, true))
139                        .child(
140                            Button::new("pro", "Get Started")
141                                .full_width()
142                                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
143                                .on_click(move |_, _window, cx| {
144                                    telemetry::event!(
145                                        "Upgrade To Pro Clicked",
146                                        state = "young-account"
147                                    );
148                                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
149                                }),
150                        ),
151                )
152                .into_any_element()
153        } else {
154            v_flex()
155                .relative()
156                .gap_1()
157                .child(Headline::new("Welcome to Zed AI"))
158                .child(
159                    v_flex()
160                        .mt_2()
161                        .gap_1()
162                        .child(
163                            h_flex()
164                                .gap_2()
165                                .child(
166                                    Label::new("Free")
167                                        .size(LabelSize::Small)
168                                        .color(Color::Muted)
169                                        .buffer_font(cx),
170                                )
171                                .child(
172                                    Label::new("(Current Plan)")
173                                        .size(LabelSize::Small)
174                                        .color(Color::Custom(
175                                            cx.theme().colors().text_muted.opacity(0.6),
176                                        ))
177                                        .buffer_font(cx),
178                                )
179                                .child(Divider::horizontal()),
180                        )
181                        .child(PlanDefinitions.free_plan(is_v2)),
182                )
183                .when_some(
184                    self.dismiss_onboarding.as_ref(),
185                    |this, dismiss_callback| {
186                        let callback = dismiss_callback.clone();
187
188                        this.child(
189                            h_flex().absolute().top_0().right_0().child(
190                                IconButton::new("dismiss_onboarding", IconName::Close)
191                                    .icon_size(IconSize::Small)
192                                    .tooltip(Tooltip::text("Dismiss"))
193                                    .on_click(move |_, window, cx| {
194                                        telemetry::event!(
195                                            "Banner Dismissed",
196                                            source = "AI Onboarding",
197                                        );
198                                        callback(window, cx)
199                                    }),
200                            ),
201                        )
202                    },
203                )
204                .child(
205                    v_flex()
206                        .mt_2()
207                        .gap_1()
208                        .child(
209                            h_flex()
210                                .gap_2()
211                                .child(
212                                    Label::new("Pro Trial")
213                                        .size(LabelSize::Small)
214                                        .color(Color::Accent)
215                                        .buffer_font(cx),
216                                )
217                                .child(Divider::horizontal()),
218                        )
219                        .child(PlanDefinitions.pro_trial(is_v2, true))
220                        .child(
221                            Button::new("pro", "Start Free Trial")
222                                .full_width()
223                                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
224                                .on_click(move |_, _window, cx| {
225                                    telemetry::event!(
226                                        "Start Trial Clicked",
227                                        state = "post-sign-in"
228                                    );
229                                    cx.open_url(&zed_urls::start_trial_url(cx))
230                                }),
231                        ),
232                )
233                .into_any_element()
234        }
235    }
236
237    fn render_trial_state(&self, is_v2: bool, _cx: &mut App) -> AnyElement {
238        v_flex()
239            .relative()
240            .gap_1()
241            .child(Headline::new("Welcome to the Zed Pro Trial"))
242            .child(
243                Label::new("Here's what you get for the next 14 days:")
244                    .color(Color::Muted)
245                    .mb_2(),
246            )
247            .child(PlanDefinitions.pro_trial(is_v2, false))
248            .when_some(
249                self.dismiss_onboarding.as_ref(),
250                |this, dismiss_callback| {
251                    let callback = dismiss_callback.clone();
252                    this.child(
253                        h_flex().absolute().top_0().right_0().child(
254                            IconButton::new("dismiss_onboarding", IconName::Close)
255                                .icon_size(IconSize::Small)
256                                .tooltip(Tooltip::text("Dismiss"))
257                                .on_click(move |_, window, cx| {
258                                    telemetry::event!(
259                                        "Banner Dismissed",
260                                        source = "AI Onboarding",
261                                    );
262                                    callback(window, cx)
263                                }),
264                        ),
265                    )
266                },
267            )
268            .into_any_element()
269    }
270
271    fn render_pro_plan_state(&self, is_v2: bool, _cx: &mut App) -> AnyElement {
272        v_flex()
273            .gap_1()
274            .child(Headline::new("Welcome to Zed Pro"))
275            .child(
276                Label::new("Here's what you get:")
277                    .color(Color::Muted)
278                    .mb_2(),
279            )
280            .child(PlanDefinitions.pro_plan(is_v2, false))
281            .when_some(
282                self.dismiss_onboarding.as_ref(),
283                |this, dismiss_callback| {
284                    let callback = dismiss_callback.clone();
285                    this.child(
286                        h_flex().absolute().top_0().right_0().child(
287                            IconButton::new("dismiss_onboarding", IconName::Close)
288                                .icon_size(IconSize::Small)
289                                .tooltip(Tooltip::text("Dismiss"))
290                                .on_click(move |_, window, cx| {
291                                    telemetry::event!(
292                                        "Banner Dismissed",
293                                        source = "AI Onboarding",
294                                    );
295                                    callback(window, cx)
296                                }),
297                        ),
298                    )
299                },
300            )
301            .into_any_element()
302    }
303}
304
305impl RenderOnce for ZedAiOnboarding {
306    fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
307        if matches!(self.sign_in_status, SignInStatus::SignedIn) {
308            match self.plan {
309                None => self.render_free_plan_state(true, cx),
310                Some(plan @ (Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree))) => {
311                    self.render_free_plan_state(plan.is_v2(), cx)
312                }
313                Some(plan @ (Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial))) => {
314                    self.render_trial_state(plan.is_v2(), cx)
315                }
316                Some(plan @ (Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro))) => {
317                    self.render_pro_plan_state(plan.is_v2(), cx)
318                }
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(
373                            SignInStatus::SignedIn,
374                            Some(Plan::V2(PlanV2::ZedFree)),
375                            false,
376                        ),
377                    ),
378                    single_example(
379                        "Pro Trial",
380                        onboarding(
381                            SignInStatus::SignedIn,
382                            Some(Plan::V2(PlanV2::ZedProTrial)),
383                            false,
384                        ),
385                    ),
386                    single_example(
387                        "Pro Plan",
388                        onboarding(
389                            SignInStatus::SignedIn,
390                            Some(Plan::V2(PlanV2::ZedPro)),
391                            false,
392                        ),
393                    ),
394                ])
395                .into_any_element(),
396        )
397    }
398}