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