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