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_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(true, false))
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, is_v2: bool, 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(is_v2, true))
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(is_v2)),
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(is_v2, 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, is_v2: bool, _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(is_v2, false))
251 .children(self.render_dismiss_button())
252 .into_any_element()
253 }
254
255 fn render_pro_plan_state(&self, is_v2: bool, _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(is_v2, false))
265 .children(self.render_dismiss_button())
266 .into_any_element()
267 }
268}
269
270impl RenderOnce for ZedAiOnboarding {
271 fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
272 if matches!(self.sign_in_status, SignInStatus::SignedIn) {
273 match self.plan {
274 None => self.render_free_plan_state(true, cx),
275 Some(plan @ (Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree))) => {
276 self.render_free_plan_state(plan.is_v2(), cx)
277 }
278 Some(plan @ (Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial))) => {
279 self.render_trial_state(plan.is_v2(), cx)
280 }
281 Some(plan @ (Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro))) => {
282 self.render_pro_plan_state(plan.is_v2(), cx)
283 }
284 }
285 } else {
286 self.render_sign_in_disclaimer(cx)
287 }
288 }
289}
290
291impl Component for ZedAiOnboarding {
292 fn scope() -> ComponentScope {
293 ComponentScope::Onboarding
294 }
295
296 fn name() -> &'static str {
297 "Agent Panel Banners"
298 }
299
300 fn sort_name() -> &'static str {
301 "Agent Panel Banners"
302 }
303
304 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
305 fn onboarding(
306 sign_in_status: SignInStatus,
307 plan: Option<Plan>,
308 account_too_young: bool,
309 ) -> AnyElement {
310 ZedAiOnboarding {
311 sign_in_status,
312 plan,
313 account_too_young,
314 continue_with_zed_ai: Arc::new(|_, _| {}),
315 sign_in: Arc::new(|_, _| {}),
316 dismiss_onboarding: None,
317 }
318 .into_any_element()
319 }
320
321 Some(
322 v_flex()
323 .gap_4()
324 .items_center()
325 .max_w_4_5()
326 .children(vec![
327 single_example(
328 "Not Signed-in",
329 onboarding(SignInStatus::SignedOut, None, false),
330 ),
331 single_example(
332 "Young Account",
333 onboarding(SignInStatus::SignedIn, None, true),
334 ),
335 single_example(
336 "Free Plan",
337 onboarding(
338 SignInStatus::SignedIn,
339 Some(Plan::V2(PlanV2::ZedFree)),
340 false,
341 ),
342 ),
343 single_example(
344 "Pro Trial",
345 onboarding(
346 SignInStatus::SignedIn,
347 Some(Plan::V2(PlanV2::ZedProTrial)),
348 false,
349 ),
350 ),
351 single_example(
352 "Pro Plan",
353 onboarding(
354 SignInStatus::SignedIn,
355 Some(Plan::V2(PlanV2::ZedPro)),
356 false,
357 ),
358 ),
359 ])
360 .into_any_element(),
361 )
362 }
363}