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_api_types::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_dismiss_button(&self) -> Option<AnyElement> {
 88        self.dismiss_onboarding.as_ref().map(|dismiss_callback| {
 89            let callback = dismiss_callback.clone();
 90
 91            h_flex()
 92                .absolute()
 93                .top_0()
 94                .right_0()
 95                .child(
 96                    IconButton::new("dismiss_onboarding", IconName::Close)
 97                        .icon_size(IconSize::Small)
 98                        .tooltip(Tooltip::text("Dismiss"))
 99                        .on_click(move |_, window, cx| {
100                            telemetry::event!("Banner Dismissed", source = "AI Onboarding",);
101                            callback(window, cx)
102                        }),
103                )
104                .into_any_element()
105        })
106    }
107
108    fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
109        let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
110
111        v_flex()
112            .relative()
113            .gap_1()
114            .child(Headline::new("Welcome to Zed AI"))
115            .child(
116                Label::new("Sign in to try Zed Pro for 14 days, no credit card required.")
117                    .color(Color::Muted)
118                    .mb_2(),
119            )
120            .child(PlanDefinitions.pro_plan())
121            .child(
122                Button::new("sign_in", "Try Zed Pro for Free")
123                    .disabled(signing_in)
124                    .full_width()
125                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
126                    .on_click({
127                        let callback = self.sign_in.clone();
128                        move |_, window, cx| {
129                            telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
130                            callback(window, cx)
131                        }
132                    }),
133            )
134            .children(self.render_dismiss_button())
135            .into_any_element()
136    }
137
138    fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
139        if self.account_too_young {
140            v_flex()
141                .relative()
142                .max_w_full()
143                .gap_1()
144                .child(Headline::new("Welcome to Zed AI"))
145                .child(YoungAccountBanner)
146                .child(
147                    v_flex()
148                        .mt_2()
149                        .gap_1()
150                        .child(
151                            h_flex()
152                                .gap_2()
153                                .child(
154                                    Label::new("Pro")
155                                        .size(LabelSize::Small)
156                                        .color(Color::Accent)
157                                        .buffer_font(cx),
158                                )
159                                .child(Divider::horizontal()),
160                        )
161                        .child(PlanDefinitions.pro_plan())
162                        .child(
163                            Button::new("pro", "Get Started")
164                                .full_width()
165                                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
166                                .on_click(move |_, _window, cx| {
167                                    telemetry::event!(
168                                        "Upgrade To Pro Clicked",
169                                        state = "young-account"
170                                    );
171                                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
172                                }),
173                        ),
174                )
175                .into_any_element()
176        } else {
177            v_flex()
178                .relative()
179                .gap_1()
180                .child(Headline::new("Welcome to Zed AI"))
181                .child(
182                    v_flex()
183                        .mt_2()
184                        .gap_1()
185                        .child(
186                            h_flex()
187                                .gap_2()
188                                .child(
189                                    Label::new("Free")
190                                        .size(LabelSize::Small)
191                                        .color(Color::Muted)
192                                        .buffer_font(cx),
193                                )
194                                .child(
195                                    Label::new("(Current Plan)")
196                                        .size(LabelSize::Small)
197                                        .color(Color::Custom(
198                                            cx.theme().colors().text_muted.opacity(0.6),
199                                        ))
200                                        .buffer_font(cx),
201                                )
202                                .child(Divider::horizontal()),
203                        )
204                        .child(PlanDefinitions.free_plan()),
205                )
206                .children(self.render_dismiss_button())
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(PlanDefinitions.pro_trial(true))
223                        .child(
224                            Button::new("pro", "Start Free Trial")
225                                .full_width()
226                                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
227                                .on_click(move |_, _window, cx| {
228                                    telemetry::event!(
229                                        "Start Trial Clicked",
230                                        state = "post-sign-in"
231                                    );
232                                    cx.open_url(&zed_urls::start_trial_url(cx))
233                                }),
234                        ),
235                )
236                .into_any_element()
237        }
238    }
239
240    fn render_trial_state(&self, _cx: &mut App) -> AnyElement {
241        v_flex()
242            .relative()
243            .gap_1()
244            .child(Headline::new("Welcome to the Zed Pro Trial"))
245            .child(
246                Label::new("Here's what you get for the next 14 days:")
247                    .color(Color::Muted)
248                    .mb_2(),
249            )
250            .child(PlanDefinitions.pro_trial(false))
251            .children(self.render_dismiss_button())
252            .into_any_element()
253    }
254
255    fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement {
256        v_flex()
257            .gap_1()
258            .child(Headline::new("Welcome to Zed Pro"))
259            .child(
260                Label::new("Here's what you get:")
261                    .color(Color::Muted)
262                    .mb_2(),
263            )
264            .child(PlanDefinitions.pro_plan())
265            .children(self.render_dismiss_button())
266            .into_any_element()
267    }
268
269    fn render_student_plan_state(&self, _cx: &mut App) -> AnyElement {
270        v_flex()
271            .gap_1()
272            .child(Headline::new("Welcome to Zed Student"))
273            .child(
274                Label::new("Here's what you get:")
275                    .color(Color::Muted)
276                    .mb_2(),
277            )
278            .child(PlanDefinitions.student_plan())
279            .children(self.render_dismiss_button())
280            .into_any_element()
281    }
282}
283
284impl RenderOnce for ZedAiOnboarding {
285    fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
286        if matches!(self.sign_in_status, SignInStatus::SignedIn) {
287            match self.plan {
288                None => self.render_free_plan_state(cx),
289                Some(Plan::ZedFree) => self.render_free_plan_state(cx),
290                Some(Plan::ZedProTrial) => self.render_trial_state(cx),
291                Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
292                Some(Plan::ZedStudent) => self.render_student_plan_state(cx),
293            }
294        } else {
295            self.render_sign_in_disclaimer(cx)
296        }
297    }
298}
299
300impl Component for ZedAiOnboarding {
301    fn scope() -> ComponentScope {
302        ComponentScope::Onboarding
303    }
304
305    fn name() -> &'static str {
306        "Agent Panel Banners"
307    }
308
309    fn sort_name() -> &'static str {
310        "Agent Panel Banners"
311    }
312
313    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
314        fn onboarding(
315            sign_in_status: SignInStatus,
316            plan: Option<Plan>,
317            account_too_young: bool,
318        ) -> AnyElement {
319            ZedAiOnboarding {
320                sign_in_status,
321                plan,
322                account_too_young,
323                continue_with_zed_ai: Arc::new(|_, _| {}),
324                sign_in: Arc::new(|_, _| {}),
325                dismiss_onboarding: None,
326            }
327            .into_any_element()
328        }
329
330        Some(
331            v_flex()
332                .gap_4()
333                .items_center()
334                .max_w_4_5()
335                .children(vec![
336                    single_example(
337                        "Not Signed-in",
338                        onboarding(SignInStatus::SignedOut, None, false),
339                    ),
340                    single_example(
341                        "Young Account",
342                        onboarding(SignInStatus::SignedIn, None, true),
343                    ),
344                    single_example(
345                        "Free Plan",
346                        onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false),
347                    ),
348                    single_example(
349                        "Pro Trial",
350                        onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false),
351                    ),
352                    single_example(
353                        "Pro Plan",
354                        onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false),
355                    ),
356                ])
357                .into_any_element(),
358        )
359    }
360}