1mod agent_api_keys_onboarding;
2mod agent_panel_onboarding_card;
3mod agent_panel_onboarding_content;
4mod edit_prediction_onboarding_content;
5mod plan_definitions;
6mod young_account_banner;
7
8pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders};
9pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
10pub use agent_panel_onboarding_content::AgentPanelOnboarding;
11use cloud_api_types::Plan;
12pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
13pub use plan_definitions::PlanDefinitions;
14pub use young_account_banner::YoungAccountBanner;
15
16use std::sync::Arc;
17
18use client::{Client, UserStore, zed_urls};
19use gpui::{AnyElement, Entity, IntoElement, ParentElement};
20use ui::{Divider, RegisterComponent, Tooltip, Vector, VectorName, prelude::*};
21
22#[derive(PartialEq)]
23pub enum SignInStatus {
24 SignedIn,
25 SigningIn,
26 SignedOut,
27}
28
29impl From<client::Status> for SignInStatus {
30 fn from(status: client::Status) -> Self {
31 if status.is_signing_in() {
32 Self::SigningIn
33 } else if status.is_signed_out() {
34 Self::SignedOut
35 } else {
36 Self::SignedIn
37 }
38 }
39}
40
41#[derive(RegisterComponent, IntoElement)]
42pub struct ZedAiOnboarding {
43 pub sign_in_status: SignInStatus,
44 pub plan: Option<Plan>,
45 pub account_too_young: bool,
46 pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
47 pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
48 pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
49}
50
51impl ZedAiOnboarding {
52 pub fn new(
53 client: Arc<Client>,
54 user_store: &Entity<UserStore>,
55 continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
56 cx: &mut App,
57 ) -> Self {
58 let store = user_store.read(cx);
59 let status = *client.status().borrow();
60
61 Self {
62 sign_in_status: status.into(),
63 plan: store.plan(),
64 account_too_young: store.account_too_young(),
65 continue_with_zed_ai,
66 sign_in: Arc::new(move |_window, cx| {
67 cx.spawn({
68 let client = client.clone();
69 async move |cx| client.sign_in_with_optional_connect(true, cx).await
70 })
71 .detach_and_log_err(cx);
72 }),
73 dismiss_onboarding: None,
74 }
75 }
76
77 pub fn with_dismiss(
78 mut self,
79 dismiss_callback: impl Fn(&mut Window, &mut App) + 'static,
80 ) -> Self {
81 self.dismiss_onboarding = Some(Arc::new(dismiss_callback));
82 self
83 }
84
85 fn certified_user_stamp(cx: &App) -> impl IntoElement {
86 div().absolute().bottom_1().right_1().child(
87 Vector::new(
88 VectorName::ProUserStamp,
89 rems_from_px(156.),
90 rems_from_px(60.),
91 )
92 .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.8))),
93 )
94 }
95
96 fn pro_trial_stamp(cx: &App) -> impl IntoElement {
97 div().absolute().bottom_1().right_1().child(
98 Vector::new(
99 VectorName::ProTrialStamp,
100 rems_from_px(156.),
101 rems_from_px(60.),
102 )
103 .color(Color::Custom(cx.theme().colors().text.alpha(0.8))),
104 )
105 }
106
107 fn business_stamp(cx: &App) -> impl IntoElement {
108 div().absolute().bottom_1().right_1().child(
109 Vector::new(
110 VectorName::BusinessStamp,
111 rems_from_px(156.),
112 rems_from_px(60.),
113 )
114 .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.8))),
115 )
116 }
117
118 fn student_stamp(cx: &App) -> impl IntoElement {
119 div().absolute().bottom_1().right_1().child(
120 Vector::new(
121 VectorName::StudentStamp,
122 rems_from_px(156.),
123 rems_from_px(60.),
124 )
125 .color(Color::Custom(cx.theme().colors().text.alpha(0.8))),
126 )
127 }
128
129 fn render_dismiss_button(&self) -> Option<AnyElement> {
130 self.dismiss_onboarding.as_ref().map(|dismiss_callback| {
131 let callback = dismiss_callback.clone();
132
133 h_flex()
134 .absolute()
135 .top_0()
136 .right_0()
137 .child(
138 IconButton::new("dismiss_onboarding", IconName::Close)
139 .icon_size(IconSize::Small)
140 .tooltip(Tooltip::text("Dismiss"))
141 .on_click(move |_, window, cx| {
142 telemetry::event!("Banner Dismissed", source = "AI Onboarding",);
143 callback(window, cx)
144 }),
145 )
146 .into_any_element()
147 })
148 }
149
150 fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
151 let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
152
153 v_flex()
154 .w_full()
155 .relative()
156 .gap_1()
157 .child(Headline::new("Welcome to Zed AI"))
158 .child(
159 Label::new("Sign in to try Zed Pro for 14 days, no credit card required.")
160 .color(Color::Muted)
161 .mb_2(),
162 )
163 .child(PlanDefinitions.pro_plan())
164 .child(
165 Button::new("sign_in", "Try Zed Pro for Free")
166 .disabled(signing_in)
167 .full_width()
168 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
169 .on_click({
170 let callback = self.sign_in.clone();
171 move |_, window, cx| {
172 telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
173 callback(window, cx)
174 }
175 }),
176 )
177 .children(self.render_dismiss_button())
178 .into_any_element()
179 }
180
181 fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
182 if self.account_too_young {
183 v_flex()
184 .relative()
185 .min_w_0()
186 .gap_1()
187 .child(Headline::new("Welcome to Zed AI"))
188 .child(YoungAccountBanner)
189 .child(
190 v_flex()
191 .mt_2()
192 .gap_1()
193 .child(
194 h_flex()
195 .gap_2()
196 .child(
197 Label::new("Pro")
198 .size(LabelSize::Small)
199 .color(Color::Accent)
200 .buffer_font(cx),
201 )
202 .child(Divider::horizontal()),
203 )
204 .child(PlanDefinitions.pro_plan())
205 .child(
206 Button::new("pro", "Get Started")
207 .full_width()
208 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
209 .on_click(move |_, _window, cx| {
210 telemetry::event!(
211 "Upgrade To Pro Clicked",
212 state = "young-account"
213 );
214 cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
215 }),
216 ),
217 )
218 .into_any_element()
219 } else {
220 v_flex()
221 .w_full()
222 .relative()
223 .gap_1()
224 .child(Headline::new("Welcome to Zed AI"))
225 .child(
226 v_flex()
227 .mt_2()
228 .gap_1()
229 .child(
230 h_flex()
231 .gap_2()
232 .child(
233 Label::new("Free")
234 .size(LabelSize::Small)
235 .color(Color::Muted)
236 .buffer_font(cx),
237 )
238 .child(
239 Label::new("(Current Plan)")
240 .size(LabelSize::Small)
241 .color(Color::Custom(
242 cx.theme().colors().text_muted.opacity(0.6),
243 ))
244 .buffer_font(cx),
245 )
246 .child(Divider::horizontal()),
247 )
248 .child(PlanDefinitions.free_plan()),
249 )
250 .children(self.render_dismiss_button())
251 .child(
252 v_flex()
253 .mt_2()
254 .gap_1()
255 .child(
256 h_flex()
257 .gap_2()
258 .child(
259 Label::new("Pro Trial")
260 .size(LabelSize::Small)
261 .color(Color::Accent)
262 .buffer_font(cx),
263 )
264 .child(Divider::horizontal()),
265 )
266 .child(PlanDefinitions.pro_trial(true))
267 .child(
268 Button::new("pro", "Start Free Trial")
269 .full_width()
270 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
271 .on_click(move |_, _window, cx| {
272 telemetry::event!(
273 "Start Trial Clicked",
274 state = "post-sign-in"
275 );
276 cx.open_url(&zed_urls::start_trial_url(cx))
277 }),
278 ),
279 )
280 .into_any_element()
281 }
282 }
283
284 fn render_trial_state(&self, cx: &mut App) -> AnyElement {
285 v_flex()
286 .w_full()
287 .relative()
288 .gap_1()
289 .child(Self::pro_trial_stamp(cx))
290 .child(Headline::new("Welcome to the Zed Pro Trial"))
291 .child(
292 Label::new("Here's what you get for the next 14 days:")
293 .color(Color::Muted)
294 .mb_2(),
295 )
296 .child(PlanDefinitions.pro_trial(false))
297 .children(self.render_dismiss_button())
298 .into_any_element()
299 }
300
301 fn render_pro_plan_state(&self, cx: &mut App) -> AnyElement {
302 v_flex()
303 .w_full()
304 .relative()
305 .gap_1()
306 .child(Self::certified_user_stamp(cx))
307 .child(Headline::new("Welcome to Zed Pro"))
308 .child(
309 Label::new("Here's what you get:")
310 .color(Color::Muted)
311 .mb_2(),
312 )
313 .child(PlanDefinitions.pro_plan())
314 .children(self.render_dismiss_button())
315 .into_any_element()
316 }
317
318 fn render_business_plan_state(&self, cx: &mut App) -> AnyElement {
319 v_flex()
320 .w_full()
321 .relative()
322 .gap_1()
323 .child(Self::business_stamp(cx))
324 .child(Headline::new("Welcome to Zed Business"))
325 .child(
326 Label::new("Here's what you get:")
327 .color(Color::Muted)
328 .mb_2(),
329 )
330 .child(PlanDefinitions.business_plan())
331 .children(self.render_dismiss_button())
332 .into_any_element()
333 }
334
335 fn render_student_plan_state(&self, cx: &mut App) -> AnyElement {
336 v_flex()
337 .w_full()
338 .relative()
339 .gap_1()
340 .child(Self::student_stamp(cx))
341 .child(Headline::new("Welcome to Zed Student"))
342 .child(
343 Label::new("Here's what you get:")
344 .color(Color::Muted)
345 .mb_2(),
346 )
347 .child(PlanDefinitions.student_plan())
348 .children(self.render_dismiss_button())
349 .into_any_element()
350 }
351}
352
353impl RenderOnce for ZedAiOnboarding {
354 fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
355 if matches!(self.sign_in_status, SignInStatus::SignedIn) {
356 match self.plan {
357 None => self.render_free_plan_state(cx),
358 Some(Plan::ZedFree) => self.render_free_plan_state(cx),
359 Some(Plan::ZedProTrial) => self.render_trial_state(cx),
360 Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
361 Some(Plan::ZedBusiness) => self.render_business_plan_state(cx),
362 Some(Plan::ZedStudent) => self.render_student_plan_state(cx),
363 }
364 } else {
365 self.render_sign_in_disclaimer(cx)
366 }
367 }
368}
369
370impl Component for ZedAiOnboarding {
371 fn scope() -> ComponentScope {
372 ComponentScope::Onboarding
373 }
374
375 fn name() -> &'static str {
376 "Agent New User Onboarding"
377 }
378
379 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
380 fn onboarding(
381 sign_in_status: SignInStatus,
382 plan: Option<Plan>,
383 account_too_young: bool,
384 ) -> AnyElement {
385 div()
386 .w_full()
387 .min_w_40()
388 .max_w(px(1100.))
389 .child(
390 AgentPanelOnboardingCard::new().child(
391 ZedAiOnboarding {
392 sign_in_status,
393 plan,
394 account_too_young,
395 continue_with_zed_ai: Arc::new(|_, _| {}),
396 sign_in: Arc::new(|_, _| {}),
397 dismiss_onboarding: None,
398 }
399 .into_any_element(),
400 ),
401 )
402 .into_any_element()
403 }
404
405 Some(
406 v_flex()
407 .min_w_0()
408 .gap_4()
409 .children(vec![
410 single_example(
411 "Not Signed-in",
412 onboarding(SignInStatus::SignedOut, None, false),
413 ),
414 single_example(
415 "Young Account",
416 onboarding(SignInStatus::SignedIn, None, true),
417 ),
418 single_example(
419 "Free Plan",
420 onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false),
421 ),
422 single_example(
423 "Pro Trial",
424 onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false),
425 ),
426 single_example(
427 "Pro Plan",
428 onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false),
429 ),
430 single_example(
431 "Business Plan",
432 onboarding(SignInStatus::SignedIn, Some(Plan::ZedBusiness), false),
433 ),
434 single_example(
435 "Student Plan",
436 onboarding(SignInStatus::SignedIn, Some(Plan::ZedStudent), false),
437 ),
438 ])
439 .into_any_element(),
440 )
441 }
442}