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