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