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(
145 "50 prompts per month with the Claude models",
146 ))
147 .child(BulletItem::new(
148 "2000 accepted edit predictions using our open-source Zeta model",
149 )),
150 )
151 }
152
153 fn pro_trial_definition(&self) -> impl IntoElement {
154 List::new()
155 .child(BulletItem::new(
156 "150 prompts per month with the Claude models",
157 ))
158 .child(BulletItem::new(
159 "Unlimited accepted edit predictions using our open-source Zeta model",
160 ))
161 }
162
163 fn pro_plan_definition(&self, cx: &mut App) -> impl IntoElement {
164 v_flex().mt_2().gap_1().map(|this| {
165 if self.account_too_young {
166 this.child(
167 h_flex()
168 .gap_2()
169 .child(
170 Label::new("Pro")
171 .size(LabelSize::Small)
172 .color(Color::Accent)
173 .buffer_font(cx),
174 )
175 .child(Divider::horizontal()),
176 )
177 .child(
178 List::new()
179 .child(BulletItem::new("500 prompts per month with Claude models"))
180 .child(BulletItem::new(
181 "Unlimited accepted edit predictions using our open-source Zeta model",
182 ))
183 .child(BulletItem::new("USD $20 per month")),
184 )
185 .child(
186 Button::new("pro", "Start with Pro")
187 .full_width()
188 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
189 .on_click(move |_, _window, cx| {
190 cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
191 }),
192 )
193 } else {
194 this.child(
195 h_flex()
196 .gap_2()
197 .child(
198 Label::new("Pro Trial")
199 .size(LabelSize::Small)
200 .color(Color::Accent)
201 .buffer_font(cx),
202 )
203 .child(Divider::horizontal()),
204 )
205 .child(
206 List::new()
207 .child(self.pro_trial_definition())
208 .child(BulletItem::new(
209 "Try it out for 14 days with no charge and no credit card required",
210 )),
211 )
212 .child(
213 Button::new("pro", "Start Pro Trial")
214 .full_width()
215 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
216 .on_click(move |_, _window, cx| {
217 cx.open_url(&zed_urls::start_trial_url(cx))
218 }),
219 )
220 }
221 })
222 }
223
224 fn render_accept_terms_of_service(&self) -> AnyElement {
225 v_flex()
226 .gap_1()
227 .w_full()
228 .child(Headline::new("Before starting…"))
229 .child(
230 Label::new("Make sure you have read and accepted Zed AI's terms of service.")
231 .color(Color::Muted)
232 .mb_2(),
233 )
234 .child(
235 Button::new("terms_of_service", "View and Read the Terms of Service")
236 .full_width()
237 .style(ButtonStyle::Outlined)
238 .icon(IconName::ArrowUpRight)
239 .icon_color(Color::Muted)
240 .icon_size(IconSize::XSmall)
241 .on_click(move |_, _window, cx| cx.open_url(&zed_urls::terms_of_service(cx))),
242 )
243 .child(
244 Button::new("accept_terms", "I've read it and accept it")
245 .full_width()
246 .style(ButtonStyle::Tinted(TintColor::Accent))
247 .on_click({
248 let callback = self.accept_terms_of_service.clone();
249 move |_, window, cx| (callback)(window, cx)
250 }),
251 )
252 .into_any_element()
253 }
254
255 fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
256 let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
257
258 v_flex()
259 .gap_1()
260 .child(Headline::new("Welcome to Zed AI"))
261 .child(
262 Label::new("Sign in to start using AI in Zed with a free trial of the Pro plan, which includes:")
263 .color(Color::Muted)
264 .mb_2(),
265 )
266 .child(self.pro_trial_definition())
267 .child(
268 Button::new("sign_in", "Sign in to Start Trial")
269 .disabled(signing_in)
270 .full_width()
271 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
272 .on_click({
273 let callback = self.sign_in.clone();
274 move |_, window, cx| callback(window, cx)
275 }),
276 )
277 .into_any_element()
278 }
279
280 fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
281 let young_account_banner = YoungAccountBanner;
282
283 v_flex()
284 .relative()
285 .gap_1()
286 .child(Headline::new("Welcome to Zed AI"))
287 .child(
288 Label::new("Choose how you want to start.")
289 .color(Color::Muted)
290 .mb_2(),
291 )
292 .map(|this| {
293 if self.account_too_young {
294 this.child(young_account_banner)
295 } else {
296 this.child(self.free_plan_definition(cx)).when_some(
297 self.dismiss_onboarding.as_ref(),
298 |this, dismiss_callback| {
299 let callback = dismiss_callback.clone();
300
301 this.child(
302 h_flex().absolute().top_0().right_0().child(
303 IconButton::new("dismiss_onboarding", IconName::Close)
304 .icon_size(IconSize::Small)
305 .tooltip(Tooltip::text("Dismiss"))
306 .on_click(move |_, window, cx| callback(window, cx)),
307 ),
308 )
309 },
310 )
311 }
312 })
313 .child(self.pro_plan_definition(cx))
314 .into_any_element()
315 }
316
317 fn render_trial_state(&self, _cx: &mut App) -> AnyElement {
318 v_flex()
319 .relative()
320 .gap_1()
321 .child(Headline::new("Welcome to the Zed Pro free trial"))
322 .child(
323 Label::new("Here's what you get for the next 14 days:")
324 .color(Color::Muted)
325 .mb_2(),
326 )
327 .child(
328 List::new()
329 .child(BulletItem::new("150 prompts with Claude models"))
330 .child(BulletItem::new(
331 "Unlimited edit predictions with Zeta, our open-source model",
332 )),
333 )
334 .when_some(
335 self.dismiss_onboarding.as_ref(),
336 |this, dismiss_callback| {
337 let callback = dismiss_callback.clone();
338 this.child(
339 h_flex().absolute().top_0().right_0().child(
340 IconButton::new("dismiss_onboarding", IconName::Close)
341 .icon_size(IconSize::Small)
342 .tooltip(Tooltip::text("Dismiss"))
343 .on_click(move |_, window, cx| callback(window, cx)),
344 ),
345 )
346 },
347 )
348 .into_any_element()
349 }
350
351 fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement {
352 v_flex()
353 .gap_1()
354 .child(Headline::new("Welcome to Zed Pro"))
355 .child(
356 Label::new("Here's what you get:")
357 .color(Color::Muted)
358 .mb_2(),
359 )
360 .child(
361 List::new()
362 .child(BulletItem::new("500 prompts with Claude models"))
363 .child(BulletItem::new("Unlimited edit predictions")),
364 )
365 .child(
366 Button::new("pro", "Continue with Zed Pro")
367 .full_width()
368 .style(ButtonStyle::Outlined)
369 .on_click({
370 let callback = self.continue_with_zed_ai.clone();
371 move |_, window, cx| callback(window, cx)
372 }),
373 )
374 .into_any_element()
375 }
376}
377
378impl RenderOnce for ZedAiOnboarding {
379 fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
380 if matches!(self.sign_in_status, SignInStatus::SignedIn) {
381 if self.has_accepted_terms_of_service {
382 match self.plan {
383 None | Some(proto::Plan::Free) => self.render_free_plan_state(cx),
384 Some(proto::Plan::ZedProTrial) => self.render_trial_state(cx),
385 Some(proto::Plan::ZedPro) => self.render_pro_plan_state(cx),
386 }
387 } else {
388 self.render_accept_terms_of_service()
389 }
390 } else {
391 self.render_sign_in_disclaimer(cx)
392 }
393 }
394}
395
396impl Component for ZedAiOnboarding {
397 fn scope() -> ComponentScope {
398 ComponentScope::Agent
399 }
400
401 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
402 fn onboarding(
403 sign_in_status: SignInStatus,
404 has_accepted_terms_of_service: bool,
405 plan: Option<proto::Plan>,
406 account_too_young: bool,
407 ) -> AnyElement {
408 ZedAiOnboarding {
409 sign_in_status,
410 has_accepted_terms_of_service,
411 plan,
412 account_too_young,
413 continue_with_zed_ai: Arc::new(|_, _| {}),
414 sign_in: Arc::new(|_, _| {}),
415 accept_terms_of_service: Arc::new(|_, _| {}),
416 dismiss_onboarding: None,
417 }
418 .into_any_element()
419 }
420
421 Some(
422 v_flex()
423 .p_4()
424 .gap_4()
425 .children(vec![
426 single_example(
427 "Not Signed-in",
428 onboarding(SignInStatus::SignedOut, false, None, false),
429 ),
430 single_example(
431 "Not Accepted ToS",
432 onboarding(SignInStatus::SignedIn, false, None, false),
433 ),
434 single_example(
435 "Account too young",
436 onboarding(SignInStatus::SignedIn, false, None, true),
437 ),
438 single_example(
439 "Free Plan",
440 onboarding(SignInStatus::SignedIn, true, Some(proto::Plan::Free), false),
441 ),
442 single_example(
443 "Pro Trial",
444 onboarding(
445 SignInStatus::SignedIn,
446 true,
447 Some(proto::Plan::ZedProTrial),
448 false,
449 ),
450 ),
451 single_example(
452 "Pro Plan",
453 onboarding(
454 SignInStatus::SignedIn,
455 true,
456 Some(proto::Plan::ZedPro),
457 false,
458 ),
459 ),
460 ])
461 .into_any_element(),
462 )
463 }
464}