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, 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        let plan_definitions = PlanDefinitions;
 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(plan_definitions.pro_plan(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        let young_account_banner = YoungAccountBanner;
118        let plan_definitions = PlanDefinitions;
119
120        if self.account_too_young {
121            v_flex()
122                .relative()
123                .max_w_full()
124                .gap_1()
125                .child(Headline::new("Welcome to Zed AI"))
126                .child(young_account_banner)
127                .child(
128                    v_flex()
129                        .mt_2()
130                        .gap_1()
131                        .child(
132                            h_flex()
133                                .gap_2()
134                                .child(
135                                    Label::new("Pro")
136                                        .size(LabelSize::Small)
137                                        .color(Color::Accent)
138                                        .buffer_font(cx),
139                                )
140                                .child(Divider::horizontal()),
141                        )
142                        .child(plan_definitions.pro_plan(true))
143                        .child(
144                            Button::new("pro", "Get Started")
145                                .full_width()
146                                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
147                                .on_click(move |_, _window, cx| {
148                                    telemetry::event!(
149                                        "Upgrade To Pro Clicked",
150                                        state = "young-account"
151                                    );
152                                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
153                                }),
154                        ),
155                )
156                .into_any_element()
157        } else {
158            v_flex()
159                .relative()
160                .gap_1()
161                .child(Headline::new("Welcome to Zed AI"))
162                .child(
163                    v_flex()
164                        .mt_2()
165                        .gap_1()
166                        .child(
167                            h_flex()
168                                .gap_2()
169                                .child(
170                                    Label::new("Free")
171                                        .size(LabelSize::Small)
172                                        .color(Color::Muted)
173                                        .buffer_font(cx),
174                                )
175                                .child(
176                                    Label::new("(Current Plan)")
177                                        .size(LabelSize::Small)
178                                        .color(Color::Custom(
179                                            cx.theme().colors().text_muted.opacity(0.6),
180                                        ))
181                                        .buffer_font(cx),
182                                )
183                                .child(Divider::horizontal()),
184                        )
185                        .child(plan_definitions.free_plan()),
186                )
187                .when_some(
188                    self.dismiss_onboarding.as_ref(),
189                    |this, dismiss_callback| {
190                        let callback = dismiss_callback.clone();
191
192                        this.child(
193                            h_flex().absolute().top_0().right_0().child(
194                                IconButton::new("dismiss_onboarding", IconName::Close)
195                                    .icon_size(IconSize::Small)
196                                    .tooltip(Tooltip::text("Dismiss"))
197                                    .on_click(move |_, window, cx| {
198                                        telemetry::event!(
199                                            "Banner Dismissed",
200                                            source = "AI Onboarding",
201                                        );
202                                        callback(window, cx)
203                                    }),
204                            ),
205                        )
206                    },
207                )
208                .child(
209                    v_flex()
210                        .mt_2()
211                        .gap_1()
212                        .child(
213                            h_flex()
214                                .gap_2()
215                                .child(
216                                    Label::new("Pro Trial")
217                                        .size(LabelSize::Small)
218                                        .color(Color::Accent)
219                                        .buffer_font(cx),
220                                )
221                                .child(Divider::horizontal()),
222                        )
223                        .child(plan_definitions.pro_trial(true))
224                        .child(
225                            Button::new("pro", "Start Free Trial")
226                                .full_width()
227                                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
228                                .on_click(move |_, _window, cx| {
229                                    telemetry::event!(
230                                        "Start Trial Clicked",
231                                        state = "post-sign-in"
232                                    );
233                                    cx.open_url(&zed_urls::start_trial_url(cx))
234                                }),
235                        ),
236                )
237                .into_any_element()
238        }
239    }
240
241    fn render_trial_state(&self, _cx: &mut App) -> AnyElement {
242        let plan_definitions = PlanDefinitions;
243
244        v_flex()
245            .relative()
246            .gap_1()
247            .child(Headline::new("Welcome to the Zed Pro Trial"))
248            .child(
249                Label::new("Here's what you get for the next 14 days:")
250                    .color(Color::Muted)
251                    .mb_2(),
252            )
253            .child(plan_definitions.pro_trial(false))
254            .when_some(
255                self.dismiss_onboarding.as_ref(),
256                |this, dismiss_callback| {
257                    let callback = dismiss_callback.clone();
258                    this.child(
259                        h_flex().absolute().top_0().right_0().child(
260                            IconButton::new("dismiss_onboarding", IconName::Close)
261                                .icon_size(IconSize::Small)
262                                .tooltip(Tooltip::text("Dismiss"))
263                                .on_click(move |_, window, cx| {
264                                    telemetry::event!(
265                                        "Banner Dismissed",
266                                        source = "AI Onboarding",
267                                    );
268                                    callback(window, cx)
269                                }),
270                        ),
271                    )
272                },
273            )
274            .into_any_element()
275    }
276
277    fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement {
278        let plan_definitions = PlanDefinitions;
279
280        v_flex()
281            .gap_1()
282            .child(Headline::new("Welcome to Zed Pro"))
283            .child(
284                Label::new("Here's what you get:")
285                    .color(Color::Muted)
286                    .mb_2(),
287            )
288            .child(plan_definitions.pro_plan(false))
289            .when_some(
290                self.dismiss_onboarding.as_ref(),
291                |this, dismiss_callback| {
292                    let callback = dismiss_callback.clone();
293                    this.child(
294                        h_flex().absolute().top_0().right_0().child(
295                            IconButton::new("dismiss_onboarding", IconName::Close)
296                                .icon_size(IconSize::Small)
297                                .tooltip(Tooltip::text("Dismiss"))
298                                .on_click(move |_, window, cx| {
299                                    telemetry::event!(
300                                        "Banner Dismissed",
301                                        source = "AI Onboarding",
302                                    );
303                                    callback(window, cx)
304                                }),
305                        ),
306                    )
307                },
308            )
309            .into_any_element()
310    }
311}
312
313impl RenderOnce for ZedAiOnboarding {
314    fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
315        if matches!(self.sign_in_status, SignInStatus::SignedIn) {
316            match self.plan {
317                None | Some(Plan::ZedFree) => self.render_free_plan_state(cx),
318                Some(Plan::ZedProTrial) => self.render_trial_state(cx),
319                Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
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}