1mod agent_panel_onboarding_card;
2mod agent_panel_onboarding_content;
3mod edit_prediction_onboarding_content;
4mod young_account_banner;
5
6pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
7pub use agent_panel_onboarding_content::AgentPanelOnboarding;
8pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
9pub use young_account_banner::YoungAccountBanner;
10
11use std::sync::Arc;
12
13use client::{Client, UserStore, zed_urls};
14use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
15use ui::{Divider, List, ListItem, RegisterComponent, TintColor, prelude::*};
16
17pub struct BulletItem {
18 label: SharedString,
19}
20
21impl BulletItem {
22 pub fn new(label: impl Into<SharedString>) -> Self {
23 Self {
24 label: label.into(),
25 }
26 }
27}
28
29impl IntoElement for BulletItem {
30 type Element = AnyElement;
31
32 fn into_element(self) -> Self::Element {
33 ListItem::new("list-item")
34 .selectable(false)
35 .start_slot(
36 Icon::new(IconName::Dash)
37 .size(IconSize::XSmall)
38 .color(Color::Hidden),
39 )
40 .child(div().w_full().child(Label::new(self.label)))
41 .into_any_element()
42 }
43}
44
45pub enum SignInStatus {
46 SignedIn,
47 SigningIn,
48 SignedOut,
49}
50
51impl From<client::Status> for SignInStatus {
52 fn from(status: client::Status) -> Self {
53 if status.is_signing_in() {
54 Self::SigningIn
55 } else if status.is_signed_out() {
56 Self::SignedOut
57 } else {
58 Self::SignedIn
59 }
60 }
61}
62
63#[derive(RegisterComponent, IntoElement)]
64pub struct ZedAiOnboarding {
65 pub sign_in_status: SignInStatus,
66 pub has_accepted_terms_of_service: bool,
67 pub plan: Option<proto::Plan>,
68 pub account_too_young: bool,
69 pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
70 pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
71 pub accept_terms_of_service: Arc<dyn Fn(&mut Window, &mut App)>,
72}
73
74impl ZedAiOnboarding {
75 pub fn new(
76 client: Arc<Client>,
77 user_store: &Entity<UserStore>,
78 continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
79 cx: &mut App,
80 ) -> Self {
81 let store = user_store.read(cx);
82 let status = *client.status().borrow();
83 Self {
84 sign_in_status: status.into(),
85 has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false),
86 plan: store.current_plan(),
87 account_too_young: store.account_too_young(),
88 continue_with_zed_ai,
89 accept_terms_of_service: Arc::new({
90 let store = user_store.clone();
91 move |_window, cx| {
92 let task = store.update(cx, |store, cx| store.accept_terms_of_service(cx));
93 task.detach_and_log_err(cx);
94 }
95 }),
96 sign_in: Arc::new(move |_window, cx| {
97 cx.spawn({
98 let client = client.clone();
99 async move |cx| {
100 client.authenticate_and_connect(true, cx).await;
101 }
102 })
103 .detach();
104 }),
105 }
106 }
107
108 fn render_free_plan_section(&self, cx: &mut App) -> impl IntoElement {
109 v_flex()
110 .mt_2()
111 .gap_1()
112 .when(self.account_too_young, |this| this.opacity(0.4))
113 .child(
114 h_flex()
115 .gap_2()
116 .child(
117 Label::new("Free")
118 .size(LabelSize::Small)
119 .color(Color::Muted)
120 .buffer_font(cx),
121 )
122 .child(Divider::horizontal()),
123 )
124 .child(
125 List::new()
126 .child(BulletItem::new(
127 "50 prompts per month with the Claude models",
128 ))
129 .child(BulletItem::new(
130 "2000 accepted edit predictions using our open-source Zeta model",
131 )),
132 )
133 .child(
134 Button::new("continue", "Continue Free")
135 .disabled(self.account_too_young)
136 .full_width()
137 .style(ButtonStyle::Outlined)
138 .on_click({
139 let callback = self.continue_with_zed_ai.clone();
140 move |_, window, cx| callback(window, cx)
141 }),
142 )
143 }
144
145 fn render_pro_plan_section(&self, cx: &mut App) -> impl IntoElement {
146 let (button_label, button_url) = if self.account_too_young {
147 ("Start with Pro", zed_urls::upgrade_to_zed_pro_url(cx))
148 } else {
149 ("Start Pro Trial", zed_urls::account_url(cx))
150 };
151
152 v_flex()
153 .mt_2()
154 .gap_1()
155 .child(
156 h_flex()
157 .gap_2()
158 .child(
159 Label::new("Pro")
160 .size(LabelSize::Small)
161 .color(Color::Accent)
162 .buffer_font(cx),
163 )
164 .child(Divider::horizontal()),
165 )
166 .child(
167 List::new()
168 .child(BulletItem::new("500 prompts per month with Claude models"))
169 .child(BulletItem::new("Unlimited edit predictions"))
170 .when(!self.account_too_young, |this| {
171 this.child(BulletItem::new(
172 "Try it out for 14 days with no charge, no credit card required",
173 ))
174 }),
175 )
176 .child(
177 Button::new("pro", button_label)
178 .full_width()
179 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
180 .on_click(move |_, _window, cx| cx.open_url(&button_url)),
181 )
182 }
183
184 fn render_accept_terms_of_service(&self) -> Div {
185 v_flex()
186 .w_full()
187 .gap_1()
188 .child(Headline::new("Before starting…"))
189 .child(Label::new(
190 "Make sure you have read and accepted Zed AI's terms of service.",
191 ))
192 .child(
193 Button::new("terms_of_service", "View and Read the Terms of Service")
194 .full_width()
195 .style(ButtonStyle::Outlined)
196 .icon(IconName::ArrowUpRight)
197 .icon_color(Color::Muted)
198 .icon_size(IconSize::XSmall)
199 .on_click(move |_, _window, cx| {
200 cx.open_url("https://zed.dev/terms-of-service")
201 }),
202 )
203 .child(
204 Button::new("accept_terms", "I've read it and accept it")
205 .full_width()
206 .style(ButtonStyle::Tinted(TintColor::Accent))
207 .on_click({
208 let callback = self.accept_terms_of_service.clone();
209 move |_, window, cx| (callback)(window, cx)
210 }),
211 )
212 }
213
214 fn render_sign_in_disclaimer(&self, _cx: &mut App) -> Div {
215 const SIGN_IN_DISCLAIMER: &str =
216 "To start using AI in Zed with our hosted models, sign in and subscribe to a plan.";
217 let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
218
219 v_flex()
220 .gap_2()
221 .child(Headline::new("Welcome to Zed AI"))
222 .child(div().w_full().child(Label::new(SIGN_IN_DISCLAIMER)))
223 .child(
224 Button::new("sign_in", "Sign In with GitHub")
225 .icon(IconName::Github)
226 .icon_position(IconPosition::Start)
227 .icon_size(IconSize::Small)
228 .icon_color(Color::Muted)
229 .disabled(signing_in)
230 .full_width()
231 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
232 .on_click({
233 let callback = self.sign_in.clone();
234 move |_, window, cx| callback(window, cx)
235 }),
236 )
237 }
238
239 fn render_free_plan_onboarding(&self, cx: &mut App) -> Div {
240 const PLANS_DESCRIPTION: &str = "Choose how you want to start.";
241 let young_account_banner = YoungAccountBanner;
242
243 v_flex()
244 .child(Headline::new("Welcome to Zed AI"))
245 .child(
246 Label::new(PLANS_DESCRIPTION)
247 .size(LabelSize::Small)
248 .color(Color::Muted)
249 .mt_1()
250 .mb_3(),
251 )
252 .when(self.account_too_young, |this| {
253 this.child(young_account_banner)
254 })
255 .child(self.render_free_plan_section(cx))
256 .child(self.render_pro_plan_section(cx))
257 }
258
259 fn render_trial_onboarding(&self, _cx: &mut App) -> Div {
260 v_flex()
261 .child(Headline::new("Welcome to the trial of Zed Pro"))
262 .child(
263 Label::new("Here's what you get for the next 14 days:")
264 .size(LabelSize::Small)
265 .color(Color::Muted)
266 .mt_1(),
267 )
268 .child(
269 List::new()
270 .child(BulletItem::new("150 prompts with Claude models"))
271 .child(BulletItem::new(
272 "Unlimited edit predictions with Zeta, our open-source model",
273 )),
274 )
275 .child(
276 Button::new("trial", "Start Trial")
277 .full_width()
278 .style(ButtonStyle::Outlined)
279 .on_click({
280 let callback = self.continue_with_zed_ai.clone();
281 move |_, window, cx| callback(window, cx)
282 }),
283 )
284 }
285
286 fn render_pro_plan_onboarding(&self, _cx: &mut App) -> Div {
287 v_flex()
288 .child(Headline::new("Welcome to Zed Pro"))
289 .child(
290 Label::new("Here's what you get:")
291 .size(LabelSize::Small)
292 .color(Color::Muted)
293 .mt_1(),
294 )
295 .child(
296 List::new()
297 .child(BulletItem::new("500 prompts with Claude models"))
298 .child(BulletItem::new("Unlimited edit predictions")),
299 )
300 .child(
301 Button::new("pro", "Continue with Zed Pro")
302 .full_width()
303 .style(ButtonStyle::Outlined)
304 .on_click({
305 let callback = self.continue_with_zed_ai.clone();
306 move |_, window, cx| callback(window, cx)
307 }),
308 )
309 }
310}
311
312impl RenderOnce for ZedAiOnboarding {
313 fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
314 if matches!(self.sign_in_status, SignInStatus::SignedIn) {
315 if self.has_accepted_terms_of_service {
316 match self.plan {
317 None | Some(proto::Plan::Free) => self.render_free_plan_onboarding(cx),
318 Some(proto::Plan::ZedProTrial) => self.render_trial_onboarding(cx),
319 Some(proto::Plan::ZedPro) => self.render_pro_plan_onboarding(cx),
320 }
321 } else {
322 self.render_accept_terms_of_service()
323 }
324 } else {
325 self.render_sign_in_disclaimer(cx)
326 }
327 }
328}
329
330impl Component for ZedAiOnboarding {
331 fn scope() -> ComponentScope {
332 ComponentScope::Agent
333 }
334
335 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
336 fn onboarding(
337 sign_in_status: SignInStatus,
338 has_accepted_terms_of_service: bool,
339 plan: Option<proto::Plan>,
340 account_too_young: bool,
341 ) -> AnyElement {
342 div()
343 .w(px(800.))
344 .child(ZedAiOnboarding {
345 sign_in_status,
346 has_accepted_terms_of_service,
347 plan,
348 account_too_young,
349 continue_with_zed_ai: Arc::new(|_, _| {}),
350 sign_in: Arc::new(|_, _| {}),
351 accept_terms_of_service: Arc::new(|_, _| {}),
352 })
353 .into_any_element()
354 }
355
356 Some(
357 v_flex()
358 .p_4()
359 .gap_4()
360 .children(vec![
361 single_example(
362 "Not Signed-in",
363 onboarding(SignInStatus::SignedOut, false, None, false),
364 ),
365 single_example(
366 "Not Accepted ToS",
367 onboarding(SignInStatus::SignedIn, false, None, false),
368 ),
369 single_example(
370 "Account too young",
371 onboarding(SignInStatus::SignedIn, true, None, true),
372 ),
373 single_example(
374 "Free Plan",
375 onboarding(SignInStatus::SignedIn, true, Some(proto::Plan::Free), false),
376 ),
377 single_example(
378 "Pro Trial",
379 onboarding(
380 SignInStatus::SignedIn,
381 true,
382 Some(proto::Plan::ZedProTrial),
383 false,
384 ),
385 ),
386 single_example(
387 "Pro Plan",
388 onboarding(
389 SignInStatus::SignedIn,
390 true,
391 Some(proto::Plan::ZedPro),
392 false,
393 ),
394 ),
395 ])
396 .into_any_element(),
397 )
398 }
399}