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