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