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 .child(
336 Button::new("pro", "Continue with Zed Pro")
337 .full_width()
338 .style(ButtonStyle::Outlined)
339 .on_click({
340 let callback = self.continue_with_zed_ai.clone();
341 move |_, window, cx| {
342 telemetry::event!("Banner Dismissed", source = "AI Onboarding");
343 callback(window, cx)
344 }
345 }),
346 )
347 .into_any_element()
348 }
349}
350
351impl RenderOnce for ZedAiOnboarding {
352 fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
353 if matches!(self.sign_in_status, SignInStatus::SignedIn) {
354 if self.has_accepted_terms_of_service {
355 match self.plan {
356 None | Some(Plan::ZedFree) => self.render_free_plan_state(cx),
357 Some(Plan::ZedProTrial) => self.render_trial_state(cx),
358 Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
359 }
360 } else {
361 self.render_accept_terms_of_service()
362 }
363 } else {
364 self.render_sign_in_disclaimer(cx)
365 }
366 }
367}
368
369impl Component for ZedAiOnboarding {
370 fn scope() -> ComponentScope {
371 ComponentScope::Onboarding
372 }
373
374 fn name() -> &'static str {
375 "Agent Panel Banners"
376 }
377
378 fn sort_name() -> &'static str {
379 "Agent Panel Banners"
380 }
381
382 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
383 fn onboarding(
384 sign_in_status: SignInStatus,
385 has_accepted_terms_of_service: bool,
386 plan: Option<Plan>,
387 account_too_young: bool,
388 ) -> AnyElement {
389 ZedAiOnboarding {
390 sign_in_status,
391 has_accepted_terms_of_service,
392 plan,
393 account_too_young,
394 continue_with_zed_ai: Arc::new(|_, _| {}),
395 sign_in: Arc::new(|_, _| {}),
396 accept_terms_of_service: Arc::new(|_, _| {}),
397 dismiss_onboarding: None,
398 }
399 .into_any_element()
400 }
401
402 Some(
403 v_flex()
404 .gap_4()
405 .items_center()
406 .max_w_4_5()
407 .children(vec![
408 single_example(
409 "Not Signed-in",
410 onboarding(SignInStatus::SignedOut, false, None, false),
411 ),
412 single_example(
413 "Not Accepted ToS",
414 onboarding(SignInStatus::SignedIn, false, None, false),
415 ),
416 single_example(
417 "Young Account",
418 onboarding(SignInStatus::SignedIn, true, None, true),
419 ),
420 single_example(
421 "Free Plan",
422 onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedFree), false),
423 ),
424 single_example(
425 "Pro Trial",
426 onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedProTrial), false),
427 ),
428 single_example(
429 "Pro Plan",
430 onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedPro), false),
431 ),
432 ])
433 .into_any_element(),
434 )
435 }
436}