diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index 7fffb60ecc..3aec9c62cd 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -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, diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs new file mode 100644 index 0000000000..041e0d87ec --- /dev/null +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -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, +} + +impl AiUpsellCard { + pub fn new(client: Arc) -> 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 { + 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(), + ) + } +}