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