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_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(true, false))
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, is_v2: bool, 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(is_v2, true))
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(is_v2)),
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(is_v2, 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, is_v2: bool, _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(is_v2, false))
251            .children(self.render_dismiss_button())
252            .into_any_element()
253    }
254
255    fn render_pro_plan_state(&self, is_v2: bool, _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(is_v2, false))
265            .children(self.render_dismiss_button())
266            .into_any_element()
267    }
268}
269
270impl RenderOnce for ZedAiOnboarding {
271    fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
272        if matches!(self.sign_in_status, SignInStatus::SignedIn) {
273            match self.plan {
274                None => self.render_free_plan_state(true, cx),
275                Some(plan @ (Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree))) => {
276                    self.render_free_plan_state(plan.is_v2(), cx)
277                }
278                Some(plan @ (Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial))) => {
279                    self.render_trial_state(plan.is_v2(), cx)
280                }
281                Some(plan @ (Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro))) => {
282                    self.render_pro_plan_state(plan.is_v2(), cx)
283                }
284            }
285        } else {
286            self.render_sign_in_disclaimer(cx)
287        }
288    }
289}
290
291impl Component for ZedAiOnboarding {
292    fn scope() -> ComponentScope {
293        ComponentScope::Onboarding
294    }
295
296    fn name() -> &'static str {
297        "Agent Panel Banners"
298    }
299
300    fn sort_name() -> &'static str {
301        "Agent Panel Banners"
302    }
303
304    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
305        fn onboarding(
306            sign_in_status: SignInStatus,
307            plan: Option<Plan>,
308            account_too_young: bool,
309        ) -> AnyElement {
310            ZedAiOnboarding {
311                sign_in_status,
312                plan,
313                account_too_young,
314                continue_with_zed_ai: Arc::new(|_, _| {}),
315                sign_in: Arc::new(|_, _| {}),
316                dismiss_onboarding: None,
317            }
318            .into_any_element()
319        }
320
321        Some(
322            v_flex()
323                .gap_4()
324                .items_center()
325                .max_w_4_5()
326                .children(vec![
327                    single_example(
328                        "Not Signed-in",
329                        onboarding(SignInStatus::SignedOut, None, false),
330                    ),
331                    single_example(
332                        "Young Account",
333                        onboarding(SignInStatus::SignedIn, None, true),
334                    ),
335                    single_example(
336                        "Free Plan",
337                        onboarding(
338                            SignInStatus::SignedIn,
339                            Some(Plan::V2(PlanV2::ZedFree)),
340                            false,
341                        ),
342                    ),
343                    single_example(
344                        "Pro Trial",
345                        onboarding(
346                            SignInStatus::SignedIn,
347                            Some(Plan::V2(PlanV2::ZedProTrial)),
348                            false,
349                        ),
350                    ),
351                    single_example(
352                        "Pro Plan",
353                        onboarding(
354                            SignInStatus::SignedIn,
355                            Some(Plan::V2(PlanV2::ZedPro)),
356                            false,
357                        ),
358                    ),
359                ])
360                .into_any_element(),
361        )
362    }
363}