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