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 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;
11pub use ai_upsell_card::AiUpsellCard;
12pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
13pub use young_account_banner::YoungAccountBanner;
14
15use std::sync::Arc;
16
17use client::{Client, UserStore, zed_urls};
18use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
19use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*};
20
21#[derive(IntoElement)]
22pub struct BulletItem {
23 label: SharedString,
24}
25
26impl BulletItem {
27 pub fn new(label: impl Into<SharedString>) -> Self {
28 Self {
29 label: label.into(),
30 }
31 }
32}
33
34impl RenderOnce for BulletItem {
35 fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
36 let line_height = 0.85 * window.line_height();
37
38 ListItem::new("list-item")
39 .selectable(false)
40 .child(
41 h_flex()
42 .w_full()
43 .min_w_0()
44 .gap_1()
45 .items_start()
46 .child(
47 h_flex().h(line_height).justify_center().child(
48 Icon::new(IconName::Dash)
49 .size(IconSize::XSmall)
50 .color(Color::Hidden),
51 ),
52 )
53 .child(div().w_full().min_w_0().child(Label::new(self.label))),
54 )
55 .into_any_element()
56 }
57}
58
59#[derive(PartialEq)]
60pub enum SignInStatus {
61 SignedIn,
62 SigningIn,
63 SignedOut,
64}
65
66impl From<client::Status> for SignInStatus {
67 fn from(status: client::Status) -> Self {
68 if status.is_signing_in() {
69 Self::SigningIn
70 } else if status.is_signed_out() {
71 Self::SignedOut
72 } else {
73 Self::SignedIn
74 }
75 }
76}
77
78#[derive(RegisterComponent, IntoElement)]
79pub struct ZedAiOnboarding {
80 pub sign_in_status: SignInStatus,
81 pub has_accepted_terms_of_service: bool,
82 pub plan: Option<proto::Plan>,
83 pub account_too_young: bool,
84 pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
85 pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
86 pub accept_terms_of_service: Arc<dyn Fn(&mut Window, &mut App)>,
87 pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
88}
89
90impl ZedAiOnboarding {
91 pub fn new(
92 client: Arc<Client>,
93 user_store: &Entity<UserStore>,
94 continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
95 cx: &mut App,
96 ) -> Self {
97 let store = user_store.read(cx);
98 let status = *client.status().borrow();
99
100 Self {
101 sign_in_status: status.into(),
102 has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false),
103 plan: store.current_plan(),
104 account_too_young: store.account_too_young(),
105 continue_with_zed_ai,
106 accept_terms_of_service: Arc::new({
107 let store = user_store.clone();
108 move |_window, cx| {
109 let task = store.update(cx, |store, cx| store.accept_terms_of_service(cx));
110 task.detach_and_log_err(cx);
111 }
112 }),
113 sign_in: Arc::new(move |_window, cx| {
114 cx.spawn({
115 let client = client.clone();
116 async move |cx| {
117 client.authenticate_and_connect(true, cx).await;
118 }
119 })
120 .detach();
121 }),
122 dismiss_onboarding: None,
123 }
124 }
125
126 pub fn with_dismiss(
127 mut self,
128 dismiss_callback: impl Fn(&mut Window, &mut App) + 'static,
129 ) -> Self {
130 self.dismiss_onboarding = Some(Arc::new(dismiss_callback));
131 self
132 }
133
134 fn free_plan_definition(&self, cx: &mut App) -> impl IntoElement {
135 v_flex()
136 .mt_2()
137 .gap_1()
138 .child(
139 h_flex()
140 .gap_2()
141 .child(
142 Label::new("Free")
143 .size(LabelSize::Small)
144 .color(Color::Muted)
145 .buffer_font(cx),
146 )
147 .child(
148 Label::new("(Current Plan)")
149 .size(LabelSize::Small)
150 .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6)))
151 .buffer_font(cx),
152 )
153 .child(Divider::horizontal()),
154 )
155 .child(
156 List::new()
157 .child(BulletItem::new("50 prompts per month with Claude models"))
158 .child(BulletItem::new(
159 "2,000 accepted edit predictions with Zeta, our open-source model",
160 )),
161 )
162 }
163
164 fn pro_trial_definition(&self) -> impl IntoElement {
165 List::new()
166 .child(BulletItem::new("150 prompts with Claude models"))
167 .child(BulletItem::new(
168 "Unlimited accepted edit predictions with Zeta, our open-source model",
169 ))
170 }
171
172 fn pro_plan_definition(&self, cx: &mut App) -> impl IntoElement {
173 v_flex().mt_2().gap_1().map(|this| {
174 if self.account_too_young {
175 this.child(
176 h_flex()
177 .gap_2()
178 .child(
179 Label::new("Pro")
180 .size(LabelSize::Small)
181 .color(Color::Accent)
182 .buffer_font(cx),
183 )
184 .child(Divider::horizontal()),
185 )
186 .child(
187 List::new()
188 .child(BulletItem::new("500 prompts per month with Claude models"))
189 .child(BulletItem::new(
190 "Unlimited accepted edit predictions with Zeta, our open-source model",
191 ))
192 .child(BulletItem::new("$20 USD per month")),
193 )
194 .child(
195 Button::new("pro", "Get Started")
196 .full_width()
197 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
198 .on_click(move |_, _window, cx| {
199 telemetry::event!("Upgrade To Pro Clicked", state = "young-account");
200 cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
201 }),
202 )
203 } else {
204 this.child(
205 h_flex()
206 .gap_2()
207 .child(
208 Label::new("Pro Trial")
209 .size(LabelSize::Small)
210 .color(Color::Accent)
211 .buffer_font(cx),
212 )
213 .child(Divider::horizontal()),
214 )
215 .child(
216 List::new()
217 .child(self.pro_trial_definition())
218 .child(BulletItem::new(
219 "Try it out for 14 days for free, no credit card required",
220 )),
221 )
222 .child(
223 Button::new("pro", "Start Free Trial")
224 .full_width()
225 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
226 .on_click(move |_, _window, cx| {
227 telemetry::event!("Start Trial Clicked", state = "post-sign-in");
228 cx.open_url(&zed_urls::start_trial_url(cx))
229 }),
230 )
231 }
232 })
233 }
234
235 fn render_accept_terms_of_service(&self) -> AnyElement {
236 v_flex()
237 .gap_1()
238 .w_full()
239 .child(Headline::new("Accept Terms of Service"))
240 .child(
241 Label::new("We don’t sell your data, track you across the web, or compromise your privacy.")
242 .color(Color::Muted)
243 .mb_2(),
244 )
245 .child(
246 Button::new("terms_of_service", "Review Terms of Service")
247 .full_width()
248 .style(ButtonStyle::Outlined)
249 .icon(IconName::ArrowUpRight)
250 .icon_color(Color::Muted)
251 .icon_size(IconSize::XSmall)
252 .on_click(move |_, _window, cx| {
253 telemetry::event!("Review Terms of Service Clicked");
254 cx.open_url(&zed_urls::terms_of_service(cx))
255 }),
256 )
257 .child(
258 Button::new("accept_terms", "Accept")
259 .full_width()
260 .style(ButtonStyle::Tinted(TintColor::Accent))
261 .on_click({
262 let callback = self.accept_terms_of_service.clone();
263 move |_, window, cx| {
264 telemetry::event!("Terms of Service Accepted");
265 (callback)(window, cx)}
266 }),
267 )
268 .into_any_element()
269 }
270
271 fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
272 let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
273
274 v_flex()
275 .gap_1()
276 .child(Headline::new("Welcome to Zed AI"))
277 .child(
278 Label::new("Sign in to try Zed Pro for 14 days, no credit card required.")
279 .color(Color::Muted)
280 .mb_2(),
281 )
282 .child(self.pro_trial_definition())
283 .child(
284 Button::new("sign_in", "Try Zed Pro for Free")
285 .disabled(signing_in)
286 .full_width()
287 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
288 .on_click({
289 let callback = self.sign_in.clone();
290 move |_, window, cx| {
291 telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
292 callback(window, cx)
293 }
294 }),
295 )
296 .into_any_element()
297 }
298
299 fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
300 let young_account_banner = YoungAccountBanner;
301
302 v_flex()
303 .relative()
304 .gap_1()
305 .child(Headline::new("Welcome to Zed AI"))
306 .map(|this| {
307 if self.account_too_young {
308 this.child(young_account_banner)
309 } else {
310 this.child(self.free_plan_definition(cx)).when_some(
311 self.dismiss_onboarding.as_ref(),
312 |this, dismiss_callback| {
313 let callback = dismiss_callback.clone();
314
315 this.child(
316 h_flex().absolute().top_0().right_0().child(
317 IconButton::new("dismiss_onboarding", IconName::Close)
318 .icon_size(IconSize::Small)
319 .tooltip(Tooltip::text("Dismiss"))
320 .on_click(move |_, window, cx| {
321 telemetry::event!(
322 "Banner Dismissed",
323 source = "AI Onboarding",
324 );
325 callback(window, cx)
326 }),
327 ),
328 )
329 },
330 )
331 }
332 })
333 .child(self.pro_plan_definition(cx))
334 .into_any_element()
335 }
336
337 fn render_trial_state(&self, _cx: &mut App) -> AnyElement {
338 v_flex()
339 .relative()
340 .gap_1()
341 .child(Headline::new("Welcome to the Zed Pro Trial"))
342 .child(
343 Label::new("Here's what you get for the next 14 days:")
344 .color(Color::Muted)
345 .mb_2(),
346 )
347 .child(
348 List::new()
349 .child(BulletItem::new("150 prompts with Claude models"))
350 .child(BulletItem::new(
351 "Unlimited edit predictions with Zeta, our open-source model",
352 )),
353 )
354 .when_some(
355 self.dismiss_onboarding.as_ref(),
356 |this, dismiss_callback| {
357 let callback = dismiss_callback.clone();
358 this.child(
359 h_flex().absolute().top_0().right_0().child(
360 IconButton::new("dismiss_onboarding", IconName::Close)
361 .icon_size(IconSize::Small)
362 .tooltip(Tooltip::text("Dismiss"))
363 .on_click(move |_, window, cx| {
364 telemetry::event!(
365 "Banner Dismissed",
366 source = "AI Onboarding",
367 );
368 callback(window, cx)
369 }),
370 ),
371 )
372 },
373 )
374 .into_any_element()
375 }
376
377 fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement {
378 v_flex()
379 .gap_1()
380 .child(Headline::new("Welcome to Zed Pro"))
381 .child(
382 Label::new("Here's what you get:")
383 .color(Color::Muted)
384 .mb_2(),
385 )
386 .child(
387 List::new()
388 .child(BulletItem::new("500 prompts with Claude models"))
389 .child(BulletItem::new(
390 "Unlimited edit predictions with Zeta, our open-source model",
391 )),
392 )
393 .child(
394 Button::new("pro", "Continue with Zed Pro")
395 .full_width()
396 .style(ButtonStyle::Outlined)
397 .on_click({
398 let callback = self.continue_with_zed_ai.clone();
399 move |_, window, cx| {
400 telemetry::event!("Banner Dismissed", source = "AI Onboarding");
401 callback(window, cx)
402 }
403 }),
404 )
405 .into_any_element()
406 }
407}
408
409impl RenderOnce for ZedAiOnboarding {
410 fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
411 if matches!(self.sign_in_status, SignInStatus::SignedIn) {
412 if self.has_accepted_terms_of_service {
413 match self.plan {
414 None | Some(proto::Plan::Free) => self.render_free_plan_state(cx),
415 Some(proto::Plan::ZedProTrial) => self.render_trial_state(cx),
416 Some(proto::Plan::ZedPro) => self.render_pro_plan_state(cx),
417 }
418 } else {
419 self.render_accept_terms_of_service()
420 }
421 } else {
422 self.render_sign_in_disclaimer(cx)
423 }
424 }
425}
426
427impl Component for ZedAiOnboarding {
428 fn scope() -> ComponentScope {
429 ComponentScope::Agent
430 }
431
432 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
433 fn onboarding(
434 sign_in_status: SignInStatus,
435 has_accepted_terms_of_service: bool,
436 plan: Option<proto::Plan>,
437 account_too_young: bool,
438 ) -> AnyElement {
439 ZedAiOnboarding {
440 sign_in_status,
441 has_accepted_terms_of_service,
442 plan,
443 account_too_young,
444 continue_with_zed_ai: Arc::new(|_, _| {}),
445 sign_in: Arc::new(|_, _| {}),
446 accept_terms_of_service: Arc::new(|_, _| {}),
447 dismiss_onboarding: None,
448 }
449 .into_any_element()
450 }
451
452 Some(
453 v_flex()
454 .p_4()
455 .gap_4()
456 .children(vec![
457 single_example(
458 "Not Signed-in",
459 onboarding(SignInStatus::SignedOut, false, None, false),
460 ),
461 single_example(
462 "Not Accepted ToS",
463 onboarding(SignInStatus::SignedIn, false, None, false),
464 ),
465 single_example(
466 "Account too young",
467 onboarding(SignInStatus::SignedIn, false, None, true),
468 ),
469 single_example(
470 "Free Plan",
471 onboarding(SignInStatus::SignedIn, true, Some(proto::Plan::Free), false),
472 ),
473 single_example(
474 "Pro Trial",
475 onboarding(
476 SignInStatus::SignedIn,
477 true,
478 Some(proto::Plan::ZedProTrial),
479 false,
480 ),
481 ),
482 single_example(
483 "Pro Plan",
484 onboarding(
485 SignInStatus::SignedIn,
486 true,
487 Some(proto::Plan::ZedPro),
488 false,
489 ),
490 ),
491 ])
492 .into_any_element(),
493 )
494 }
495}