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