1use std::{sync::Arc, time::Duration};
2
3use client::{Client, zed_urls};
4use cloud_llm_client::Plan;
5use gpui::{
6 Animation, AnimationExt, AnyElement, App, IntoElement, RenderOnce, Transformation, Window,
7 percentage,
8};
9use ui::{Divider, Vector, VectorName, prelude::*};
10
11use crate::{SignInStatus, plan_definitions::PlanDefinitions};
12
13#[derive(IntoElement, RegisterComponent)]
14pub struct AiUpsellCard {
15 pub sign_in_status: SignInStatus,
16 pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
17 pub user_plan: Option<Plan>,
18 pub tab_index: Option<isize>,
19}
20
21impl AiUpsellCard {
22 pub fn new(client: Arc<Client>, user_plan: Option<Plan>) -> Self {
23 let status = *client.status().borrow();
24
25 Self {
26 user_plan,
27 sign_in_status: status.into(),
28 sign_in: Arc::new(move |_window, cx| {
29 cx.spawn({
30 let client = client.clone();
31 async move |cx| client.sign_in_with_optional_connect(true, cx).await
32 })
33 .detach_and_log_err(cx);
34 }),
35 tab_index: None,
36 }
37 }
38}
39
40impl RenderOnce for AiUpsellCard {
41 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
42 let plan_definitions = PlanDefinitions;
43
44 let pro_section = v_flex()
45 .flex_grow()
46 .w_full()
47 .gap_1()
48 .child(
49 h_flex()
50 .gap_2()
51 .child(
52 Label::new("Pro")
53 .size(LabelSize::Small)
54 .color(Color::Accent)
55 .buffer_font(cx),
56 )
57 .child(Divider::horizontal()),
58 )
59 .child(plan_definitions.pro_plan(false));
60
61 let free_section = v_flex()
62 .flex_grow()
63 .w_full()
64 .gap_1()
65 .child(
66 h_flex()
67 .gap_2()
68 .child(
69 Label::new("Free")
70 .size(LabelSize::Small)
71 .color(Color::Muted)
72 .buffer_font(cx),
73 )
74 .child(Divider::horizontal()),
75 )
76 .child(plan_definitions.free_plan());
77
78 let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child(
79 Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.))
80 .color(Color::Custom(cx.theme().colors().border.opacity(0.05))),
81 );
82
83 let gradient_bg = div()
84 .absolute()
85 .inset_0()
86 .size_full()
87 .bg(gpui::linear_gradient(
88 180.,
89 gpui::linear_color_stop(
90 cx.theme().colors().elevated_surface_background.opacity(0.8),
91 0.,
92 ),
93 gpui::linear_color_stop(
94 cx.theme().colors().elevated_surface_background.opacity(0.),
95 0.8,
96 ),
97 ));
98
99 let description = PlanDefinitions::AI_DESCRIPTION;
100
101 let card = v_flex()
102 .relative()
103 .flex_grow()
104 .p_4()
105 .pt_3()
106 .border_1()
107 .border_color(cx.theme().colors().border)
108 .rounded_lg()
109 .overflow_hidden()
110 .child(grid_bg)
111 .child(gradient_bg);
112
113 let plans_section = h_flex()
114 .w_full()
115 .mt_1p5()
116 .mb_2p5()
117 .items_start()
118 .gap_6()
119 .child(free_section)
120 .child(pro_section);
121
122 let footer_container = v_flex().items_center().gap_1();
123
124 let certified_user_stamp = div()
125 .absolute()
126 .top_2()
127 .right_2()
128 .size(rems_from_px(72.))
129 .child(
130 Vector::new(
131 VectorName::CertifiedUserStamp,
132 rems_from_px(72.),
133 rems_from_px(72.),
134 )
135 .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.3)))
136 .with_animation(
137 "loading_stamp",
138 Animation::new(Duration::from_secs(10)).repeat(),
139 |this, delta| this.transform(Transformation::rotate(percentage(delta))),
140 ),
141 );
142
143 let pro_trial_stamp = div()
144 .absolute()
145 .top_2()
146 .right_2()
147 .size(rems_from_px(72.))
148 .child(
149 Vector::new(
150 VectorName::ProTrialStamp,
151 rems_from_px(72.),
152 rems_from_px(72.),
153 )
154 .color(Color::Custom(cx.theme().colors().text.alpha(0.2))),
155 );
156
157 match self.sign_in_status {
158 SignInStatus::SignedIn => match self.user_plan {
159 None | Some(Plan::ZedFree) => card
160 .child(Label::new("Try Zed AI").size(LabelSize::Large))
161 .child(
162 div()
163 .max_w_3_4()
164 .mb_2()
165 .child(Label::new(description).color(Color::Muted)),
166 )
167 .child(plans_section)
168 .child(
169 footer_container
170 .child(
171 Button::new("start_trial", "Start 14-day Free Pro Trial")
172 .full_width()
173 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
174 .when_some(self.tab_index, |this, tab_index| {
175 this.tab_index(tab_index)
176 })
177 .on_click(move |_, _window, cx| {
178 telemetry::event!(
179 "Start Trial Clicked",
180 state = "post-sign-in"
181 );
182 cx.open_url(&zed_urls::start_trial_url(cx))
183 }),
184 )
185 .child(
186 Label::new("No credit card required")
187 .size(LabelSize::Small)
188 .color(Color::Muted),
189 ),
190 ),
191 Some(Plan::ZedProTrial) => card
192 .child(pro_trial_stamp)
193 .child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large))
194 .child(
195 Label::new("Here's what you get for the next 14 days:")
196 .color(Color::Muted)
197 .mb_2(),
198 )
199 .child(plan_definitions.pro_trial(false)),
200 Some(Plan::ZedPro) => card
201 .child(certified_user_stamp)
202 .child(Label::new("You're in the Zed Pro plan").size(LabelSize::Large))
203 .child(
204 Label::new("Here's what you get:")
205 .color(Color::Muted)
206 .mb_2(),
207 )
208 .child(plan_definitions.pro_plan(false)),
209 },
210 // Signed Out State
211 _ => card
212 .child(Label::new("Try Zed AI").size(LabelSize::Large))
213 .child(
214 div()
215 .max_w_3_4()
216 .mb_2()
217 .child(Label::new(description).color(Color::Muted)),
218 )
219 .child(plans_section)
220 .child(
221 Button::new("sign_in", "Sign In")
222 .full_width()
223 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
224 .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index))
225 .on_click({
226 let callback = self.sign_in.clone();
227 move |_, window, cx| {
228 telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
229 callback(window, cx)
230 }
231 }),
232 ),
233 }
234 }
235}
236
237impl Component for AiUpsellCard {
238 fn scope() -> ComponentScope {
239 ComponentScope::Agent
240 }
241
242 fn name() -> &'static str {
243 "AI Upsell Card"
244 }
245
246 fn sort_name() -> &'static str {
247 "AI Upsell Card"
248 }
249
250 fn description() -> Option<&'static str> {
251 Some("A card presenting the Zed AI product during user's first-open onboarding flow.")
252 }
253
254 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
255 Some(
256 v_flex()
257 .gap_4()
258 .children(vec![example_group(vec![
259 single_example(
260 "Signed Out State",
261 AiUpsellCard {
262 sign_in_status: SignInStatus::SignedOut,
263 sign_in: Arc::new(|_, _| {}),
264 user_plan: None,
265 tab_index: Some(0),
266 }
267 .into_any_element(),
268 ),
269 single_example(
270 "Free Plan",
271 AiUpsellCard {
272 sign_in_status: SignInStatus::SignedIn,
273 sign_in: Arc::new(|_, _| {}),
274 user_plan: Some(Plan::ZedFree),
275 tab_index: Some(1),
276 }
277 .into_any_element(),
278 ),
279 single_example(
280 "Pro Trial",
281 AiUpsellCard {
282 sign_in_status: SignInStatus::SignedIn,
283 sign_in: Arc::new(|_, _| {}),
284 user_plan: Some(Plan::ZedProTrial),
285 tab_index: Some(1),
286 }
287 .into_any_element(),
288 ),
289 single_example(
290 "Pro Plan",
291 AiUpsellCard {
292 sign_in_status: SignInStatus::SignedIn,
293 sign_in: Arc::new(|_, _| {}),
294 user_plan: Some(Plan::ZedPro),
295 tab_index: Some(1),
296 }
297 .into_any_element(),
298 ),
299 ])])
300 .into_any_element(),
301 )
302 }
303}