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_api_types::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, 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 plan: Option<Plan>,
47 pub account_too_young: bool,
48 pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
49 pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
50 pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
51}
52
53impl ZedAiOnboarding {
54 pub fn new(
55 client: Arc<Client>,
56 user_store: &Entity<UserStore>,
57 continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
58 cx: &mut App,
59 ) -> Self {
60 let store = user_store.read(cx);
61 let status = *client.status().borrow();
62
63 Self {
64 sign_in_status: status.into(),
65 plan: store.plan(),
66 account_too_young: store.account_too_young(),
67 continue_with_zed_ai,
68 sign_in: Arc::new(move |_window, cx| {
69 cx.spawn({
70 let client = client.clone();
71 async move |cx| client.sign_in_with_optional_connect(true, cx).await
72 })
73 .detach_and_log_err(cx);
74 }),
75 dismiss_onboarding: None,
76 }
77 }
78
79 pub fn with_dismiss(
80 mut self,
81 dismiss_callback: impl Fn(&mut Window, &mut App) + 'static,
82 ) -> Self {
83 self.dismiss_onboarding = Some(Arc::new(dismiss_callback));
84 self
85 }
86
87 fn render_dismiss_button(&self) -> Option<AnyElement> {
88 self.dismiss_onboarding.as_ref().map(|dismiss_callback| {
89 let callback = dismiss_callback.clone();
90
91 h_flex()
92 .absolute()
93 .top_0()
94 .right_0()
95 .child(
96 IconButton::new("dismiss_onboarding", IconName::Close)
97 .icon_size(IconSize::Small)
98 .tooltip(Tooltip::text("Dismiss"))
99 .on_click(move |_, window, cx| {
100 telemetry::event!("Banner Dismissed", source = "AI Onboarding",);
101 callback(window, cx)
102 }),
103 )
104 .into_any_element()
105 })
106 }
107
108 fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
109 let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
110
111 v_flex()
112 .relative()
113 .gap_1()
114 .child(Headline::new("Welcome to Zed AI"))
115 .child(
116 Label::new("Sign in to try Zed Pro for 14 days, no credit card required.")
117 .color(Color::Muted)
118 .mb_2(),
119 )
120 .child(PlanDefinitions.pro_plan())
121 .child(
122 Button::new("sign_in", "Try Zed Pro for Free")
123 .disabled(signing_in)
124 .full_width()
125 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
126 .on_click({
127 let callback = self.sign_in.clone();
128 move |_, window, cx| {
129 telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
130 callback(window, cx)
131 }
132 }),
133 )
134 .children(self.render_dismiss_button())
135 .into_any_element()
136 }
137
138 fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
139 if self.account_too_young {
140 v_flex()
141 .relative()
142 .max_w_full()
143 .gap_1()
144 .child(Headline::new("Welcome to Zed AI"))
145 .child(YoungAccountBanner)
146 .child(
147 v_flex()
148 .mt_2()
149 .gap_1()
150 .child(
151 h_flex()
152 .gap_2()
153 .child(
154 Label::new("Pro")
155 .size(LabelSize::Small)
156 .color(Color::Accent)
157 .buffer_font(cx),
158 )
159 .child(Divider::horizontal()),
160 )
161 .child(PlanDefinitions.pro_plan())
162 .child(
163 Button::new("pro", "Get Started")
164 .full_width()
165 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
166 .on_click(move |_, _window, cx| {
167 telemetry::event!(
168 "Upgrade To Pro Clicked",
169 state = "young-account"
170 );
171 cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
172 }),
173 ),
174 )
175 .into_any_element()
176 } else {
177 v_flex()
178 .relative()
179 .gap_1()
180 .child(Headline::new("Welcome to Zed AI"))
181 .child(
182 v_flex()
183 .mt_2()
184 .gap_1()
185 .child(
186 h_flex()
187 .gap_2()
188 .child(
189 Label::new("Free")
190 .size(LabelSize::Small)
191 .color(Color::Muted)
192 .buffer_font(cx),
193 )
194 .child(
195 Label::new("(Current Plan)")
196 .size(LabelSize::Small)
197 .color(Color::Custom(
198 cx.theme().colors().text_muted.opacity(0.6),
199 ))
200 .buffer_font(cx),
201 )
202 .child(Divider::horizontal()),
203 )
204 .child(PlanDefinitions.free_plan()),
205 )
206 .children(self.render_dismiss_button())
207 .child(
208 v_flex()
209 .mt_2()
210 .gap_1()
211 .child(
212 h_flex()
213 .gap_2()
214 .child(
215 Label::new("Pro Trial")
216 .size(LabelSize::Small)
217 .color(Color::Accent)
218 .buffer_font(cx),
219 )
220 .child(Divider::horizontal()),
221 )
222 .child(PlanDefinitions.pro_trial(true))
223 .child(
224 Button::new("pro", "Start Free Trial")
225 .full_width()
226 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
227 .on_click(move |_, _window, cx| {
228 telemetry::event!(
229 "Start Trial Clicked",
230 state = "post-sign-in"
231 );
232 cx.open_url(&zed_urls::start_trial_url(cx))
233 }),
234 ),
235 )
236 .into_any_element()
237 }
238 }
239
240 fn render_trial_state(&self, _cx: &mut App) -> AnyElement {
241 v_flex()
242 .relative()
243 .gap_1()
244 .child(Headline::new("Welcome to the Zed Pro Trial"))
245 .child(
246 Label::new("Here's what you get for the next 14 days:")
247 .color(Color::Muted)
248 .mb_2(),
249 )
250 .child(PlanDefinitions.pro_trial(false))
251 .children(self.render_dismiss_button())
252 .into_any_element()
253 }
254
255 fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement {
256 v_flex()
257 .gap_1()
258 .child(Headline::new("Welcome to Zed Pro"))
259 .child(
260 Label::new("Here's what you get:")
261 .color(Color::Muted)
262 .mb_2(),
263 )
264 .child(PlanDefinitions.pro_plan())
265 .children(self.render_dismiss_button())
266 .into_any_element()
267 }
268
269 fn render_business_plan_state(&self, _cx: &mut App) -> AnyElement {
270 v_flex()
271 .gap_1()
272 .child(Headline::new("Welcome to Zed Business"))
273 .child(
274 Label::new("Here's what you get:")
275 .color(Color::Muted)
276 .mb_2(),
277 )
278 .child(PlanDefinitions.business_plan())
279 .children(self.render_dismiss_button())
280 .into_any_element()
281 }
282
283 fn render_student_plan_state(&self, _cx: &mut App) -> AnyElement {
284 v_flex()
285 .gap_1()
286 .child(Headline::new("Welcome to Zed Student"))
287 .child(
288 Label::new("Here's what you get:")
289 .color(Color::Muted)
290 .mb_2(),
291 )
292 .child(PlanDefinitions.student_plan())
293 .children(self.render_dismiss_button())
294 .into_any_element()
295 }
296}
297
298impl RenderOnce for ZedAiOnboarding {
299 fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
300 if matches!(self.sign_in_status, SignInStatus::SignedIn) {
301 match self.plan {
302 None => self.render_free_plan_state(cx),
303 Some(Plan::ZedFree) => self.render_free_plan_state(cx),
304 Some(Plan::ZedProTrial) => self.render_trial_state(cx),
305 Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
306 Some(Plan::ZedBusiness) => self.render_business_plan_state(cx),
307 Some(Plan::ZedStudent) => self.render_student_plan_state(cx),
308 }
309 } else {
310 self.render_sign_in_disclaimer(cx)
311 }
312 }
313}
314
315impl Component for ZedAiOnboarding {
316 fn scope() -> ComponentScope {
317 ComponentScope::Onboarding
318 }
319
320 fn name() -> &'static str {
321 "Agent Panel Banners"
322 }
323
324 fn sort_name() -> &'static str {
325 "Agent Panel Banners"
326 }
327
328 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
329 fn onboarding(
330 sign_in_status: SignInStatus,
331 plan: Option<Plan>,
332 account_too_young: bool,
333 ) -> AnyElement {
334 ZedAiOnboarding {
335 sign_in_status,
336 plan,
337 account_too_young,
338 continue_with_zed_ai: Arc::new(|_, _| {}),
339 sign_in: Arc::new(|_, _| {}),
340 dismiss_onboarding: None,
341 }
342 .into_any_element()
343 }
344
345 Some(
346 v_flex()
347 .gap_4()
348 .items_center()
349 .max_w_4_5()
350 .children(vec![
351 single_example(
352 "Not Signed-in",
353 onboarding(SignInStatus::SignedOut, None, false),
354 ),
355 single_example(
356 "Young Account",
357 onboarding(SignInStatus::SignedIn, None, true),
358 ),
359 single_example(
360 "Free Plan",
361 onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false),
362 ),
363 single_example(
364 "Pro Trial",
365 onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false),
366 ),
367 single_example(
368 "Pro Plan",
369 onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false),
370 ),
371 single_example(
372 "Business Plan",
373 onboarding(SignInStatus::SignedIn, Some(Plan::ZedBusiness), false),
374 ),
375 single_example(
376 "Student Plan",
377 onboarding(SignInStatus::SignedIn, Some(Plan::ZedStudent), false),
378 ),
379 ])
380 .into_any_element(),
381 )
382 }
383}