@@ -0,0 +1,135 @@
+use gpui::{Action, IntoElement, ParentElement, RenderOnce, point};
+use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
+use ui::{Divider, List, prelude::*};
+
+use crate::BulletItem;
+
+pub struct ApiKeysWithProviders {
+ configured_providers: Vec<(IconName, SharedString)>,
+}
+
+impl ApiKeysWithProviders {
+ pub fn new(cx: &mut Context<Self>) -> Self {
+ cx.subscribe(
+ &LanguageModelRegistry::global(cx),
+ |this: &mut Self, _registry, event: &language_model::Event, cx| match event {
+ language_model::Event::ProviderStateChanged
+ | language_model::Event::AddedProvider(_)
+ | language_model::Event::RemovedProvider(_) => {
+ this.configured_providers = Self::compute_configured_providers(cx)
+ }
+ _ => {}
+ },
+ )
+ .detach();
+
+ Self {
+ configured_providers: Self::compute_configured_providers(cx),
+ }
+ }
+
+ fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> {
+ LanguageModelRegistry::read_global(cx)
+ .providers()
+ .iter()
+ .filter(|provider| {
+ provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
+ })
+ .map(|provider| (provider.icon(), provider.name().0.clone()))
+ .collect()
+ }
+
+ pub fn has_providers(&self) -> bool {
+ !self.configured_providers.is_empty()
+ }
+}
+
+impl Render for ApiKeysWithProviders {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let configured_providers_list =
+ self.configured_providers
+ .iter()
+ .cloned()
+ .map(|(icon, name)| {
+ h_flex()
+ .gap_1p5()
+ .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
+ .child(Label::new(name))
+ });
+
+ h_flex()
+ .mx_2p5()
+ .p_1()
+ .pb_0()
+ .gap_2()
+ .rounded_t_lg()
+ .border_t_1()
+ .border_x_1()
+ .border_color(cx.theme().colors().border.opacity(0.5))
+ .bg(cx.theme().colors().background.alpha(0.5))
+ .shadow(vec![gpui::BoxShadow {
+ color: gpui::black().opacity(0.15),
+ offset: point(px(1.), px(-1.)),
+ blur_radius: px(3.),
+ spread_radius: px(0.),
+ }])
+ .child(
+ h_flex()
+ .px_2p5()
+ .py_1p5()
+ .gap_2()
+ .flex_wrap()
+ .rounded_t(px(5.))
+ .overflow_hidden()
+ .border_t_1()
+ .border_x_1()
+ .border_color(cx.theme().colors().border)
+ .bg(cx.theme().colors().panel_background)
+ .child(Icon::new(IconName::Info).size(IconSize::XSmall).color(Color::Muted))
+ .child(Label::new("Or start now using API keys from your environment for the following providers:").color(Color::Muted))
+ .children(configured_providers_list)
+ )
+ }
+}
+
+#[derive(IntoElement)]
+pub struct ApiKeysWithoutProviders;
+
+impl ApiKeysWithoutProviders {
+ pub fn new() -> Self {
+ Self
+ }
+}
+
+impl RenderOnce for ApiKeysWithoutProviders {
+ fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+ v_flex()
+ .mt_2()
+ .gap_1()
+ .child(
+ h_flex()
+ .gap_2()
+ .child(
+ Label::new("API Keys")
+ .size(LabelSize::Small)
+ .color(Color::Muted)
+ .buffer_font(cx),
+ )
+ .child(Divider::horizontal()),
+ )
+ .child(List::new().child(BulletItem::new(
+ "You can also use AI in Zed by bringing your own API keys",
+ )))
+ .child(
+ Button::new("configure-providers", "Configure Providers")
+ .full_width()
+ .style(ButtonStyle::Outlined)
+ .on_click(move |_, window, cx| {
+ window.dispatch_action(
+ zed_actions::agent::OpenConfiguration.boxed_clone(),
+ cx,
+ );
+ }),
+ )
+ }
+}
@@ -1,12 +1,11 @@
use std::sync::Arc;
use client::{Client, UserStore};
-use gpui::{Action, ClickEvent, Entity, IntoElement, ParentElement};
+use gpui::{Entity, IntoElement, ParentElement};
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
-use ui::{Divider, List, prelude::*};
-use zed_actions::agent::{OpenConfiguration, ToggleModelSelector};
+use ui::prelude::*;
-use crate::{AgentPanelOnboardingCard, BulletItem, ZedAiOnboarding};
+use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding};
pub struct AgentPanelOnboarding {
user_store: Entity<UserStore>,
@@ -53,93 +52,34 @@ impl AgentPanelOnboarding {
.map(|provider| (provider.icon(), provider.name().0.clone()))
.collect()
}
-
- fn configure_providers(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
- window.dispatch_action(OpenConfiguration.boxed_clone(), cx);
- cx.notify();
- }
-
- fn render_api_keys_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
- let has_existing_providers = self.configured_providers.len() > 0;
- let configure_provider_label = if has_existing_providers {
- "Configure Other Provider"
- } else {
- "Configure Providers"
- };
-
- let content = if has_existing_providers {
- List::new()
- .child(BulletItem::new(
- "Or start now using API keys from your environment for the following providers:"
- ))
- .child(
- h_flex()
- .px_5()
- .gap_2()
- .flex_wrap()
- .children(self.configured_providers.iter().cloned().map(|(icon, name)|
- h_flex()
- .gap_1p5()
- .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
- .child(Label::new(name))
- ))
- )
- .child(BulletItem::new(
- "No need for any of the plans or even to sign in",
- ))
- } else {
- List::new()
- .child(BulletItem::new(
- "You can also use AI in Zed by bringing your own API keys",
- ))
- .child(BulletItem::new(
- "No need for any of the plans or even to sign in",
- ))
- };
-
- v_flex()
- .mt_2()
- .gap_1()
- .child(
- h_flex()
- .gap_2()
- .child(
- Label::new("API Keys")
- .size(LabelSize::Small)
- .color(Color::Muted)
- .buffer_font(cx),
- )
- .child(Divider::horizontal()),
- )
- .child(content)
- .when(has_existing_providers, |this| {
- this.child(
- Button::new("pick-model", "Choose Model")
- .full_width()
- .style(ButtonStyle::Outlined)
- .on_click(|_event, window, cx| {
- window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
- }),
- )
- })
- .child(
- Button::new("configure-providers", configure_provider_label)
- .full_width()
- .style(ButtonStyle::Outlined)
- .on_click(cx.listener(Self::configure_providers)),
- )
- }
}
impl Render for AgentPanelOnboarding {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let enrolled_in_trial = matches!(
+ self.user_store.read(cx).current_plan(),
+ Some(proto::Plan::ZedProTrial)
+ );
+
AgentPanelOnboardingCard::new()
- .child(ZedAiOnboarding::new(
- self.client.clone(),
- &self.user_store,
- self.continue_with_zed_ai.clone(),
- cx,
- ))
- .child(self.render_api_keys_section(cx))
+ .child(
+ ZedAiOnboarding::new(
+ self.client.clone(),
+ &self.user_store,
+ self.continue_with_zed_ai.clone(),
+ cx,
+ )
+ .with_dismiss({
+ let callback = self.continue_with_zed_ai.clone();
+ move |window, cx| callback(window, cx)
+ }),
+ )
+ .map(|this| {
+ if enrolled_in_trial || self.configured_providers.len() >= 1 {
+ this
+ } else {
+ this.child(ApiKeysWithoutProviders::new())
+ }
+ })
}
}
@@ -1,8 +1,10 @@
+mod agent_api_keys_onboarding;
mod agent_panel_onboarding_card;
mod agent_panel_onboarding_content;
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 edit_prediction_onboarding_content::EditPredictionOnboarding;
@@ -12,7 +14,7 @@ use std::sync::Arc;
use client::{Client, UserStore, zed_urls};
use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
-use ui::{Divider, List, ListItem, RegisterComponent, TintColor, prelude::*};
+use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*};
pub struct BulletItem {
label: SharedString,
@@ -69,6 +71,7 @@ pub struct ZedAiOnboarding {
pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
pub accept_terms_of_service: Arc<dyn Fn(&mut Window, &mut App)>,
+ pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
}
impl ZedAiOnboarding {
@@ -80,6 +83,7 @@ impl ZedAiOnboarding {
) -> Self {
let store = user_store.read(cx);
let status = *client.status().borrow();
+
Self {
sign_in_status: status.into(),
has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false),
@@ -102,14 +106,22 @@ impl ZedAiOnboarding {
})
.detach();
}),
+ dismiss_onboarding: None,
}
}
- fn render_free_plan_section(&self, cx: &mut App) -> impl IntoElement {
+ pub fn with_dismiss(
+ mut self,
+ dismiss_callback: impl Fn(&mut Window, &mut App) + 'static,
+ ) -> Self {
+ self.dismiss_onboarding = Some(Arc::new(dismiss_callback));
+ self
+ }
+
+ fn free_plan_definition(&self, cx: &mut App) -> impl IntoElement {
v_flex()
.mt_2()
.gap_1()
- .when(self.account_too_young, |this| this.opacity(0.4))
.child(
h_flex()
.gap_2()
@@ -119,6 +131,12 @@ impl ZedAiOnboarding {
.color(Color::Muted)
.buffer_font(cx),
)
+ .child(
+ Label::new("(Current Plan)")
+ .size(LabelSize::Small)
+ .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6)))
+ .buffer_font(cx),
+ )
.child(Divider::horizontal()),
)
.child(
@@ -130,65 +148,89 @@ impl ZedAiOnboarding {
"2000 accepted edit predictions using our open-source Zeta model",
)),
)
- .child(
- Button::new("continue", "Continue Free")
- .disabled(self.account_too_young)
- .full_width()
- .style(ButtonStyle::Outlined)
- .on_click({
- let callback = self.continue_with_zed_ai.clone();
- move |_, window, cx| callback(window, cx)
- }),
- )
}
- fn render_pro_plan_section(&self, cx: &mut App) -> impl IntoElement {
- let (button_label, button_url) = if self.account_too_young {
- ("Start with Pro", zed_urls::upgrade_to_zed_pro_url(cx))
- } else {
- ("Start Pro Trial", zed_urls::start_trial_url(cx))
- };
+ fn pro_trial_definition(&self) -> impl IntoElement {
+ List::new()
+ .child(BulletItem::new(
+ "150 prompts per month with the Claude models",
+ ))
+ .child(BulletItem::new(
+ "Unlimited accepted edit predictions using our open-source Zeta model",
+ ))
+ }
- v_flex()
- .mt_2()
- .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 per month with Claude models"))
- .child(BulletItem::new("Unlimited edit predictions"))
- .when(!self.account_too_young, |this| {
- this.child(BulletItem::new(
- "Try it out for 14 days with no charge, no credit card required",
+ fn pro_plan_definition(&self, cx: &mut App) -> impl IntoElement {
+ v_flex().mt_2().gap_1().map(|this| {
+ if self.account_too_young {
+ this.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 per month with Claude models"))
+ .child(BulletItem::new(
+ "Unlimited accepted edit predictions using our open-source Zeta model",
))
- }),
- )
- .child(
- Button::new("pro", button_label)
- .full_width()
- .style(ButtonStyle::Tinted(ui::TintColor::Accent))
- .on_click(move |_, _window, cx| cx.open_url(&button_url)),
- )
+ .child(BulletItem::new("USD $20 per month")),
+ )
+ .child(
+ Button::new("pro", "Start with Pro")
+ .full_width()
+ .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+ .on_click(move |_, _window, cx| {
+ cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
+ }),
+ )
+ } else {
+ this.child(
+ h_flex()
+ .gap_2()
+ .child(
+ Label::new("Pro Trial")
+ .size(LabelSize::Small)
+ .color(Color::Accent)
+ .buffer_font(cx),
+ )
+ .child(Divider::horizontal()),
+ )
+ .child(
+ List::new()
+ .child(self.pro_trial_definition())
+ .child(BulletItem::new(
+ "Try it out for 14 days with no charge and no credit card required",
+ )),
+ )
+ .child(
+ Button::new("pro", "Start Pro Trial")
+ .full_width()
+ .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+ .on_click(move |_, _window, cx| {
+ cx.open_url(&zed_urls::start_trial_url(cx))
+ }),
+ )
+ }
+ })
}
- fn render_accept_terms_of_service(&self) -> Div {
+ fn render_accept_terms_of_service(&self) -> AnyElement {
v_flex()
- .w_full()
.gap_1()
+ .w_full()
.child(Headline::new("Before startingβ¦"))
- .child(Label::new(
- "Make sure you have read and accepted Zed AI's terms of service.",
- ))
+ .child(
+ Label::new("Make sure you have read and accepted Zed AI's terms of service.")
+ .color(Color::Muted)
+ .mb_2(),
+ )
.child(
Button::new("terms_of_service", "View and Read the Terms of Service")
.full_width()
@@ -196,9 +238,7 @@ impl ZedAiOnboarding {
.icon(IconName::ArrowUpRight)
.icon_color(Color::Muted)
.icon_size(IconSize::XSmall)
- .on_click(move |_, _window, cx| {
- cx.open_url("https://zed.dev/terms-of-service")
- }),
+ .on_click(move |_, _window, cx| cx.open_url(&zed_urls::terms_of_service(cx))),
)
.child(
Button::new("accept_terms", "I've read it and accept it")
@@ -209,23 +249,23 @@ impl ZedAiOnboarding {
move |_, window, cx| (callback)(window, cx)
}),
)
+ .into_any_element()
}
- fn render_sign_in_disclaimer(&self, _cx: &mut App) -> Div {
- const SIGN_IN_DISCLAIMER: &str =
- "To start using AI in Zed with our hosted models, sign in and subscribe to a plan.";
+ fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
v_flex()
- .gap_2()
+ .gap_1()
.child(Headline::new("Welcome to Zed AI"))
- .child(div().w_full().child(Label::new(SIGN_IN_DISCLAIMER)))
.child(
- Button::new("sign_in", "Sign In with GitHub")
- .icon(IconName::Github)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::Small)
- .icon_color(Color::Muted)
+ Label::new("Sign in to start using AI in Zed with a free trial of the Pro plan, which includes:")
+ .color(Color::Muted)
+ .mb_2(),
+ )
+ .child(self.pro_trial_definition())
+ .child(
+ Button::new("sign_in", "Sign in to Start Trial")
.disabled(signing_in)
.full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
@@ -234,36 +274,55 @@ impl ZedAiOnboarding {
move |_, window, cx| callback(window, cx)
}),
)
+ .into_any_element()
}
- fn render_free_plan_onboarding(&self, cx: &mut App) -> Div {
- const PLANS_DESCRIPTION: &str = "Choose how you want to start.";
+ fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
let young_account_banner = YoungAccountBanner;
v_flex()
+ .relative()
+ .gap_1()
.child(Headline::new("Welcome to Zed AI"))
.child(
- Label::new(PLANS_DESCRIPTION)
- .size(LabelSize::Small)
+ Label::new("Choose how you want to start.")
.color(Color::Muted)
- .mt_1()
- .mb_3(),
+ .mb_2(),
)
- .when(self.account_too_young, |this| {
- this.child(young_account_banner)
+ .map(|this| {
+ if self.account_too_young {
+ this.child(young_account_banner)
+ } else {
+ this.child(self.free_plan_definition(cx)).when_some(
+ self.dismiss_onboarding.as_ref(),
+ |this, dismiss_callback| {
+ let callback = dismiss_callback.clone();
+
+ this.child(
+ h_flex().absolute().top_0().right_0().child(
+ IconButton::new("dismiss_onboarding", IconName::Close)
+ .icon_size(IconSize::Small)
+ .tooltip(Tooltip::text("Dismiss"))
+ .on_click(move |_, window, cx| callback(window, cx)),
+ ),
+ )
+ },
+ )
+ }
})
- .child(self.render_free_plan_section(cx))
- .child(self.render_pro_plan_section(cx))
+ .child(self.pro_plan_definition(cx))
+ .into_any_element()
}
- fn render_trial_onboarding(&self, _cx: &mut App) -> Div {
+ fn render_trial_state(&self, _cx: &mut App) -> AnyElement {
v_flex()
- .child(Headline::new("Welcome to the trial of Zed Pro"))
+ .relative()
+ .gap_1()
+ .child(Headline::new("Welcome to the Zed Pro free trial"))
.child(
Label::new("Here's what you get for the next 14 days:")
- .size(LabelSize::Small)
.color(Color::Muted)
- .mt_1(),
+ .mb_2(),
)
.child(
List::new()
@@ -272,25 +331,31 @@ impl ZedAiOnboarding {
"Unlimited edit predictions with Zeta, our open-source model",
)),
)
- .child(
- Button::new("trial", "Start Trial")
- .full_width()
- .style(ButtonStyle::Outlined)
- .on_click({
- let callback = self.continue_with_zed_ai.clone();
- move |_, window, cx| callback(window, cx)
- }),
+ .when_some(
+ self.dismiss_onboarding.as_ref(),
+ |this, dismiss_callback| {
+ let callback = dismiss_callback.clone();
+ this.child(
+ h_flex().absolute().top_0().right_0().child(
+ IconButton::new("dismiss_onboarding", IconName::Close)
+ .icon_size(IconSize::Small)
+ .tooltip(Tooltip::text("Dismiss"))
+ .on_click(move |_, window, cx| callback(window, cx)),
+ ),
+ )
+ },
)
+ .into_any_element()
}
- fn render_pro_plan_onboarding(&self, _cx: &mut App) -> Div {
+ fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement {
v_flex()
+ .gap_1()
.child(Headline::new("Welcome to Zed Pro"))
.child(
Label::new("Here's what you get:")
- .size(LabelSize::Small)
.color(Color::Muted)
- .mt_1(),
+ .mb_2(),
)
.child(
List::new()
@@ -306,6 +371,7 @@ impl ZedAiOnboarding {
move |_, window, cx| callback(window, cx)
}),
)
+ .into_any_element()
}
}
@@ -314,9 +380,9 @@ impl RenderOnce for ZedAiOnboarding {
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
if self.has_accepted_terms_of_service {
match self.plan {
- None | Some(proto::Plan::Free) => self.render_free_plan_onboarding(cx),
- Some(proto::Plan::ZedProTrial) => self.render_trial_onboarding(cx),
- Some(proto::Plan::ZedPro) => self.render_pro_plan_onboarding(cx),
+ None | Some(proto::Plan::Free) => self.render_free_plan_state(cx),
+ Some(proto::Plan::ZedProTrial) => self.render_trial_state(cx),
+ Some(proto::Plan::ZedPro) => self.render_pro_plan_state(cx),
}
} else {
self.render_accept_terms_of_service()
@@ -339,18 +405,17 @@ impl Component for ZedAiOnboarding {
plan: Option<proto::Plan>,
account_too_young: bool,
) -> AnyElement {
- div()
- .w(px(800.))
- .child(ZedAiOnboarding {
- sign_in_status,
- has_accepted_terms_of_service,
- plan,
- account_too_young,
- continue_with_zed_ai: Arc::new(|_, _| {}),
- sign_in: Arc::new(|_, _| {}),
- accept_terms_of_service: Arc::new(|_, _| {}),
- })
- .into_any_element()
+ ZedAiOnboarding {
+ sign_in_status,
+ has_accepted_terms_of_service,
+ plan,
+ account_too_young,
+ continue_with_zed_ai: Arc::new(|_, _| {}),
+ sign_in: Arc::new(|_, _| {}),
+ accept_terms_of_service: Arc::new(|_, _| {}),
+ dismiss_onboarding: None,
+ }
+ .into_any_element()
}
Some(
@@ -368,7 +433,7 @@ impl Component for ZedAiOnboarding {
),
single_example(
"Account too young",
- onboarding(SignInStatus::SignedIn, true, None, true),
+ onboarding(SignInStatus::SignedIn, false, None, true),
),
single_example(
"Free Plan",