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, is_v2: bool, 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(PlanDefinitions.pro_plan(is_v2, true))
140 .child(
141 Button::new("pro", "Get Started")
142 .full_width()
143 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
144 .on_click(move |_, _window, cx| {
145 telemetry::event!(
146 "Upgrade To Pro Clicked",
147 state = "young-account"
148 );
149 cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
150 }),
151 ),
152 )
153 .into_any_element()
154 } else {
155 v_flex()
156 .relative()
157 .gap_1()
158 .child(Headline::new("Welcome to Zed AI"))
159 .child(
160 v_flex()
161 .mt_2()
162 .gap_1()
163 .child(
164 h_flex()
165 .gap_2()
166 .child(
167 Label::new("Free")
168 .size(LabelSize::Small)
169 .color(Color::Muted)
170 .buffer_font(cx),
171 )
172 .child(
173 Label::new("(Current Plan)")
174 .size(LabelSize::Small)
175 .color(Color::Custom(
176 cx.theme().colors().text_muted.opacity(0.6),
177 ))
178 .buffer_font(cx),
179 )
180 .child(Divider::horizontal()),
181 )
182 .child(PlanDefinitions.free_plan(is_v2)),
183 )
184 .when_some(
185 self.dismiss_onboarding.as_ref(),
186 |this, dismiss_callback| {
187 let callback = dismiss_callback.clone();
188
189 this.child(
190 h_flex().absolute().top_0().right_0().child(
191 IconButton::new("dismiss_onboarding", IconName::Close)
192 .icon_size(IconSize::Small)
193 .tooltip(Tooltip::text("Dismiss"))
194 .on_click(move |_, window, cx| {
195 telemetry::event!(
196 "Banner Dismissed",
197 source = "AI Onboarding",
198 );
199 callback(window, cx)
200 }),
201 ),
202 )
203 },
204 )
205 .child(
206 v_flex()
207 .mt_2()
208 .gap_1()
209 .child(
210 h_flex()
211 .gap_2()
212 .child(
213 Label::new("Pro Trial")
214 .size(LabelSize::Small)
215 .color(Color::Accent)
216 .buffer_font(cx),
217 )
218 .child(Divider::horizontal()),
219 )
220 .child(PlanDefinitions.pro_trial(is_v2, true))
221 .child(
222 Button::new("pro", "Start Free Trial")
223 .full_width()
224 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
225 .on_click(move |_, _window, cx| {
226 telemetry::event!(
227 "Start Trial Clicked",
228 state = "post-sign-in"
229 );
230 cx.open_url(&zed_urls::start_trial_url(cx))
231 }),
232 ),
233 )
234 .into_any_element()
235 }
236 }
237
238 fn render_trial_state(&self, is_v2: bool, _cx: &mut App) -> AnyElement {
239 v_flex()
240 .relative()
241 .gap_1()
242 .child(Headline::new("Welcome to the Zed Pro Trial"))
243 .child(
244 Label::new("Here's what you get for the next 14 days:")
245 .color(Color::Muted)
246 .mb_2(),
247 )
248 .child(PlanDefinitions.pro_trial(is_v2, false))
249 .when_some(
250 self.dismiss_onboarding.as_ref(),
251 |this, dismiss_callback| {
252 let callback = dismiss_callback.clone();
253 this.child(
254 h_flex().absolute().top_0().right_0().child(
255 IconButton::new("dismiss_onboarding", IconName::Close)
256 .icon_size(IconSize::Small)
257 .tooltip(Tooltip::text("Dismiss"))
258 .on_click(move |_, window, cx| {
259 telemetry::event!(
260 "Banner Dismissed",
261 source = "AI Onboarding",
262 );
263 callback(window, cx)
264 }),
265 ),
266 )
267 },
268 )
269 .into_any_element()
270 }
271
272 fn render_pro_plan_state(&self, is_v2: bool, _cx: &mut App) -> AnyElement {
273 v_flex()
274 .gap_1()
275 .child(Headline::new("Welcome to Zed Pro"))
276 .child(
277 Label::new("Here's what you get:")
278 .color(Color::Muted)
279 .mb_2(),
280 )
281 .child(PlanDefinitions.pro_plan(is_v2, false))
282 .when_some(
283 self.dismiss_onboarding.as_ref(),
284 |this, dismiss_callback| {
285 let callback = dismiss_callback.clone();
286 this.child(
287 h_flex().absolute().top_0().right_0().child(
288 IconButton::new("dismiss_onboarding", IconName::Close)
289 .icon_size(IconSize::Small)
290 .tooltip(Tooltip::text("Dismiss"))
291 .on_click(move |_, window, cx| {
292 telemetry::event!(
293 "Banner Dismissed",
294 source = "AI Onboarding",
295 );
296 callback(window, cx)
297 }),
298 ),
299 )
300 },
301 )
302 .into_any_element()
303 }
304}
305
306impl RenderOnce for ZedAiOnboarding {
307 fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
308 if matches!(self.sign_in_status, SignInStatus::SignedIn) {
309 match self.plan {
310 None => self.render_free_plan_state(cx.has_flag::<BillingV2FeatureFlag>(), cx),
311 Some(plan @ (Plan::ZedFree | Plan::ZedFreeV2)) => {
312 self.render_free_plan_state(plan.is_v2(), cx)
313 }
314 Some(plan @ (Plan::ZedProTrial | Plan::ZedProTrialV2)) => {
315 self.render_trial_state(plan.is_v2(), cx)
316 }
317 Some(plan @ (Plan::ZedPro | Plan::ZedProV2)) => {
318 self.render_pro_plan_state(plan.is_v2(), cx)
319 }
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}