@@ -1,12 +1,14 @@
mod agent_api_keys_onboarding;
mod agent_panel_onboarding_card;
mod agent_panel_onboarding_content;
+mod ai_upsell_card;
mod edit_prediction_onboarding_content;
mod young_account_banner;
pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders};
pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
pub use agent_panel_onboarding_content::AgentPanelOnboarding;
+pub use ai_upsell_card::AiUpsellCard;
pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
pub use young_account_banner::YoungAccountBanner;
@@ -54,6 +56,7 @@ impl RenderOnce for BulletItem {
}
}
+#[derive(PartialEq)]
pub enum SignInStatus {
SignedIn,
SigningIn,
@@ -0,0 +1,201 @@
+use std::sync::Arc;
+
+use client::{Client, zed_urls};
+use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
+use ui::{Divider, List, Vector, VectorName, prelude::*};
+
+use crate::{BulletItem, SignInStatus};
+
+#[derive(IntoElement, RegisterComponent)]
+pub struct AiUpsellCard {
+ pub sign_in_status: SignInStatus,
+ pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
+}
+
+impl AiUpsellCard {
+ pub fn new(client: Arc<Client>) -> Self {
+ let status = *client.status().borrow();
+
+ Self {
+ sign_in_status: status.into(),
+ sign_in: Arc::new(move |_window, cx| {
+ cx.spawn({
+ let client = client.clone();
+ async move |cx| {
+ client.authenticate_and_connect(true, cx).await;
+ }
+ })
+ .detach();
+ }),
+ }
+ }
+}
+
+impl RenderOnce for AiUpsellCard {
+ fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+ let pro_section = v_flex()
+ .w_full()
+ .gap_1()
+ .child(
+ h_flex()
+ .gap_2()
+ .child(
+ Label::new("Pro")
+ .size(LabelSize::Small)
+ .color(Color::Accent)
+ .buffer_font(cx),
+ )
+ .child(Divider::horizontal()),
+ )
+ .child(
+ List::new()
+ .child(BulletItem::new("500 prompts with Claude models"))
+ .child(BulletItem::new(
+ "Unlimited edit predictions with Zeta, our open-source model",
+ )),
+ );
+
+ let free_section = v_flex()
+ .w_full()
+ .gap_1()
+ .child(
+ h_flex()
+ .gap_2()
+ .child(
+ Label::new("Free")
+ .size(LabelSize::Small)
+ .color(Color::Muted)
+ .buffer_font(cx),
+ )
+ .child(Divider::horizontal()),
+ )
+ .child(
+ List::new()
+ .child(BulletItem::new("50 prompts with the Claude models"))
+ .child(BulletItem::new("2,000 accepted edit predictions")),
+ );
+
+ let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child(
+ Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.))
+ .color(Color::Custom(cx.theme().colors().border.opacity(0.05))),
+ );
+
+ let gradient_bg = div()
+ .absolute()
+ .inset_0()
+ .size_full()
+ .bg(gpui::linear_gradient(
+ 180.,
+ gpui::linear_color_stop(
+ cx.theme().colors().elevated_surface_background.opacity(0.8),
+ 0.,
+ ),
+ gpui::linear_color_stop(
+ cx.theme().colors().elevated_surface_background.opacity(0.),
+ 0.8,
+ ),
+ ));
+
+ const DESCRIPTION: &str = "Zed offers a complete agentic experience, with robust editing and reviewing features to collaborate with AI.";
+
+ let footer_buttons = match self.sign_in_status {
+ SignInStatus::SignedIn => v_flex()
+ .items_center()
+ .gap_1()
+ .child(
+ Button::new("sign_in", "Start 14-day Free Pro Trial")
+ .full_width()
+ .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+ .on_click(move |_, _window, cx| {
+ telemetry::event!("Start Trial Clicked", state = "post-sign-in");
+ cx.open_url(&zed_urls::start_trial_url(cx))
+ }),
+ )
+ .child(
+ Label::new("No credit card required")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .into_any_element(),
+ _ => Button::new("sign_in", "Sign In")
+ .full_width()
+ .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+ .on_click({
+ let callback = self.sign_in.clone();
+ move |_, window, cx| {
+ telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
+ callback(window, cx)
+ }
+ })
+ .into_any_element(),
+ };
+
+ v_flex()
+ .relative()
+ .p_6()
+ .pt_4()
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .rounded_lg()
+ .overflow_hidden()
+ .child(grid_bg)
+ .child(gradient_bg)
+ .child(Headline::new("Try Zed AI"))
+ .child(Label::new(DESCRIPTION).color(Color::Muted).mb_2())
+ .child(
+ h_flex()
+ .mt_1p5()
+ .mb_2p5()
+ .items_start()
+ .gap_12()
+ .child(free_section)
+ .child(pro_section),
+ )
+ .child(footer_buttons)
+ }
+}
+
+impl Component for AiUpsellCard {
+ fn scope() -> ComponentScope {
+ ComponentScope::Agent
+ }
+
+ fn name() -> &'static str {
+ "AI Upsell Card"
+ }
+
+ fn sort_name() -> &'static str {
+ "AI Upsell Card"
+ }
+
+ fn description() -> Option<&'static str> {
+ Some("A card presenting the Zed AI product during user's first-open onboarding flow.")
+ }
+
+ fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+ Some(
+ v_flex()
+ .p_4()
+ .gap_4()
+ .children(vec![example_group(vec![
+ single_example(
+ "Signed Out State",
+ AiUpsellCard {
+ sign_in_status: SignInStatus::SignedOut,
+ sign_in: Arc::new(|_, _| {}),
+ }
+ .into_any_element(),
+ ),
+ single_example(
+ "Signed In State",
+ AiUpsellCard {
+ sign_in_status: SignInStatus::SignedIn,
+ sign_in: Arc::new(|_, _| {}),
+ }
+ .into_any_element(),
+ ),
+ ])])
+ .into_any_element(),
+ )
+ }
+}