1use std::sync::Arc;
2
3use client::{Client, zed_urls};
4use cloud_llm_client::Plan;
5use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
6use ui::{Divider, List, Vector, VectorName, prelude::*};
7
8use crate::{BulletItem, SignInStatus};
9
10#[derive(IntoElement, RegisterComponent)]
11pub struct AiUpsellCard {
12 pub sign_in_status: SignInStatus,
13 pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
14 pub user_plan: Option<Plan>,
15 pub tab_index: Option<isize>,
16}
17
18impl AiUpsellCard {
19 pub fn new(client: Arc<Client>, user_plan: Option<Plan>) -> Self {
20 let status = *client.status().borrow();
21
22 Self {
23 user_plan,
24 sign_in_status: status.into(),
25 sign_in: Arc::new(move |_window, cx| {
26 cx.spawn({
27 let client = client.clone();
28 async move |cx| client.sign_in_with_optional_connect(true, cx).await
29 })
30 .detach_and_log_err(cx);
31 }),
32 tab_index: None,
33 }
34 }
35}
36
37impl RenderOnce for AiUpsellCard {
38 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
39 let pro_section = v_flex()
40 .flex_grow()
41 .w_full()
42 .gap_1()
43 .child(
44 h_flex()
45 .gap_2()
46 .child(
47 Label::new("Pro")
48 .size(LabelSize::Small)
49 .color(Color::Accent)
50 .buffer_font(cx),
51 )
52 .child(Divider::horizontal()),
53 )
54 .child(
55 List::new()
56 .child(BulletItem::new("500 prompts with Claude models"))
57 .child(BulletItem::new(
58 "Unlimited edit predictions with Zeta, our open-source model",
59 )),
60 );
61
62 let free_section = v_flex()
63 .flex_grow()
64 .w_full()
65 .gap_1()
66 .child(
67 h_flex()
68 .gap_2()
69 .child(
70 Label::new("Free")
71 .size(LabelSize::Small)
72 .color(Color::Muted)
73 .buffer_font(cx),
74 )
75 .child(Divider::horizontal()),
76 )
77 .child(
78 List::new()
79 .child(BulletItem::new("50 prompts with Claude models"))
80 .child(BulletItem::new("2,000 accepted edit predictions")),
81 );
82
83 let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child(
84 Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.))
85 .color(Color::Custom(cx.theme().colors().border.opacity(0.05))),
86 );
87
88 let gradient_bg = div()
89 .absolute()
90 .inset_0()
91 .size_full()
92 .bg(gpui::linear_gradient(
93 180.,
94 gpui::linear_color_stop(
95 cx.theme().colors().elevated_surface_background.opacity(0.8),
96 0.,
97 ),
98 gpui::linear_color_stop(
99 cx.theme().colors().elevated_surface_background.opacity(0.),
100 0.8,
101 ),
102 ));
103
104 const DESCRIPTION: &str = "Zed offers a complete agentic experience, with robust editing and reviewing features to collaborate with AI.";
105
106 let footer_buttons = match self.sign_in_status {
107 SignInStatus::SignedIn => v_flex()
108 .items_center()
109 .gap_1()
110 .child(
111 Button::new("sign_in", "Start 14-day Free Pro Trial")
112 .full_width()
113 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
114 .on_click(move |_, _window, cx| {
115 telemetry::event!("Start Trial Clicked", state = "post-sign-in");
116 cx.open_url(&zed_urls::start_trial_url(cx))
117 })
118 .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
119 )
120 .child(
121 Label::new("No credit card required")
122 .size(LabelSize::Small)
123 .color(Color::Muted),
124 )
125 .into_any_element(),
126 _ => Button::new("sign_in", "Sign In")
127 .full_width()
128 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
129 .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index))
130 .on_click({
131 let callback = self.sign_in.clone();
132 move |_, window, cx| {
133 telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
134 callback(window, cx)
135 }
136 })
137 .into_any_element(),
138 };
139
140 v_flex()
141 .relative()
142 .p_4()
143 .pt_3()
144 .border_1()
145 .border_color(cx.theme().colors().border)
146 .rounded_lg()
147 .overflow_hidden()
148 .child(grid_bg)
149 .child(gradient_bg)
150 .child(Label::new("Try Zed AI").size(LabelSize::Large))
151 .child(
152 div()
153 .max_w_3_4()
154 .mb_2()
155 .child(Label::new(DESCRIPTION).color(Color::Muted)),
156 )
157 .child(
158 h_flex()
159 .w_full()
160 .mt_1p5()
161 .mb_2p5()
162 .items_start()
163 .gap_6()
164 .child(free_section)
165 .child(pro_section),
166 )
167 .child(footer_buttons)
168 }
169}
170
171impl Component for AiUpsellCard {
172 fn scope() -> ComponentScope {
173 ComponentScope::Agent
174 }
175
176 fn name() -> &'static str {
177 "AI Upsell Card"
178 }
179
180 fn sort_name() -> &'static str {
181 "AI Upsell Card"
182 }
183
184 fn description() -> Option<&'static str> {
185 Some("A card presenting the Zed AI product during user's first-open onboarding flow.")
186 }
187
188 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
189 Some(
190 v_flex()
191 .p_4()
192 .gap_4()
193 .children(vec![example_group(vec![
194 single_example(
195 "Signed Out State",
196 AiUpsellCard {
197 sign_in_status: SignInStatus::SignedOut,
198 sign_in: Arc::new(|_, _| {}),
199 user_plan: None,
200 tab_index: Some(0),
201 }
202 .into_any_element(),
203 ),
204 single_example(
205 "Signed In State",
206 AiUpsellCard {
207 sign_in_status: SignInStatus::SignedIn,
208 sign_in: Arc::new(|_, _| {}),
209 user_plan: None,
210 tab_index: Some(1),
211 }
212 .into_any_element(),
213 ),
214 ])])
215 .into_any_element(),
216 )
217 }
218}