Add fast-follows to the AI onboarding flow (#34737)
Follow-up to https://github.com/zed-industries/zed/pull/33738. Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
This commit is contained in:
parent
5a530ecd39
commit
eaccd542fd
10 changed files with 409 additions and 215 deletions
|
@ -2300,7 +2300,20 @@ impl AgentPanel {
|
|||
return None;
|
||||
}
|
||||
|
||||
Some(div().size_full().child(self.onboarding.clone()))
|
||||
let thread_view = matches!(&self.active_view, ActiveView::Thread { .. });
|
||||
let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
|
||||
|
||||
Some(
|
||||
div()
|
||||
.size_full()
|
||||
.when(thread_view, |this| {
|
||||
this.bg(cx.theme().colors().panel_background)
|
||||
})
|
||||
.when(text_thread_view, |this| {
|
||||
this.bg(cx.theme().colors().editor_background)
|
||||
})
|
||||
.child(self.onboarding.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_trial_end_upsell(
|
||||
|
@ -3237,7 +3250,20 @@ impl Render for AgentPanel {
|
|||
.into_any(),
|
||||
)
|
||||
})
|
||||
.child(h_flex().child(message_editor.clone()))
|
||||
.child(h_flex().relative().child(message_editor.clone()).when(
|
||||
!LanguageModelRegistry::read_global(cx).has_authenticated_provider(cx),
|
||||
|this| {
|
||||
this.child(
|
||||
div()
|
||||
.size_full()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.opacity(0.8)
|
||||
.block_mouse_except_scroll(),
|
||||
)
|
||||
},
|
||||
))
|
||||
.child(self.render_drag_target(cx)),
|
||||
ActiveView::ExternalAgentThread { thread_view, .. } => parent
|
||||
.relative()
|
||||
|
|
|
@ -14,6 +14,7 @@ use agent::{
|
|||
context_store::ContextStoreEvent,
|
||||
};
|
||||
use agent_settings::{AgentSettings, CompletionMode};
|
||||
use ai_onboarding::ApiKeysWithProviders;
|
||||
use buffer_diff::BufferDiff;
|
||||
use client::UserStore;
|
||||
use collections::{HashMap, HashSet};
|
||||
|
@ -33,7 +34,8 @@ use gpui::{
|
|||
};
|
||||
use language::{Buffer, Language, Point};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModelRequestMessage, MessageContent, ZED_CLOUD_PROVIDER_ID,
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage, MessageContent,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use multi_buffer;
|
||||
use project::Project;
|
||||
|
@ -1655,9 +1657,28 @@ impl Render for MessageEditor {
|
|||
|
||||
let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
|
||||
|
||||
let enrolled_in_trial = matches!(
|
||||
self.user_store.read(cx).current_plan(),
|
||||
Some(proto::Plan::ZedProTrial)
|
||||
);
|
||||
|
||||
let configured_providers: 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();
|
||||
let has_existing_providers = configured_providers.len() > 0;
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.when(has_existing_providers && !enrolled_in_trial, |this| {
|
||||
this.child(cx.new(ApiKeysWithProviders::new))
|
||||
})
|
||||
.when(changed_buffers.len() > 0, |parent| {
|
||||
parent.child(self.render_edits_bar(&changed_buffers, window, cx))
|
||||
})
|
||||
|
|
135
crates/ai_onboarding/src/agent_api_keys_onboarding.rs
Normal file
135
crates/ai_onboarding/src/agent_api_keys_onboarding.rs
Normal file
|
@ -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,
|
||||
);
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@ impl ParentElement for AgentPanelOnboardingCard {
|
|||
impl RenderOnce for AgentPanelOnboardingCard {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
div()
|
||||
.m_4()
|
||||
.m_2p5()
|
||||
.p(px(3.))
|
||||
.elevation_2(cx)
|
||||
.rounded_lg()
|
||||
|
@ -49,6 +49,7 @@ impl RenderOnce for AgentPanelOnboardingCard {
|
|||
.right_0()
|
||||
.w(px(400.))
|
||||
.h(px(92.))
|
||||
.rounded_md()
|
||||
.child(
|
||||
Vector::new(
|
||||
VectorName::AiGrid,
|
||||
|
@ -61,11 +62,12 @@ impl RenderOnce for AgentPanelOnboardingCard {
|
|||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.top_0p5()
|
||||
.right_0p5()
|
||||
.w(px(660.))
|
||||
.h(px(401.))
|
||||
.overflow_hidden()
|
||||
.rounded_md()
|
||||
.bg(linear_gradient(
|
||||
75.,
|
||||
linear_color_stop(
|
||||
|
|
|
@ -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))
|
||||
};
|
||||
|
||||
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",
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("pro", button_label)
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(move |_, _window, cx| cx.open_url(&button_url)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_accept_terms_of_service(&self) -> Div {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(Headline::new("Before starting…"))
|
||||
.child(Label::new(
|
||||
"Make sure you have read and accepted Zed AI's terms of service.",
|
||||
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",
|
||||
))
|
||||
}
|
||||
|
||||
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(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) -> AnyElement {
|
||||
v_flex()
|
||||
.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.")
|
||||
.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",
|
||||
|
|
|
@ -6,7 +6,7 @@ pub struct YoungAccountBanner;
|
|||
|
||||
impl RenderOnce for YoungAccountBanner {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
const YOUNG_ACCOUNT_DISCLAIMER: &str = "Given your GitHub account was created less than 30 days ago, we cannot put you in the Free plan or offer you a free trial of the Pro plan. We hope you'll understand, as this is unfortunately required to prevent abuse of our service. To continue, upgrade to Pro or use your own API keys for other providers.";
|
||||
const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, we cannot offer plans to GitHub accounts created fewer than 30 days ago. To request an exception, reach out to billing@zed.dev.";
|
||||
|
||||
let label = div()
|
||||
.w_full()
|
||||
|
|
|
@ -30,3 +30,8 @@ pub fn start_trial_url(cx: &App) -> String {
|
|||
pub fn upgrade_to_zed_pro_url(cx: &App) -> String {
|
||||
format!("{server_url}/account/upgrade", server_url = server_url(cx))
|
||||
}
|
||||
|
||||
/// Returns the URL to Zed's terms of service.
|
||||
pub fn terms_of_service(cx: &App) -> String {
|
||||
format!("{server_url}/terms-of-service", server_url = server_url(cx))
|
||||
}
|
||||
|
|
|
@ -206,8 +206,8 @@ impl LanguageModelRegistry {
|
|||
None
|
||||
}
|
||||
|
||||
/// Check that we have at least one provider that is authenticated.
|
||||
fn has_authenticated_provider(&self, cx: &App) -> bool {
|
||||
/// Returns `true` if at least one provider that is authenticated.
|
||||
pub fn has_authenticated_provider(&self, cx: &App) -> bool {
|
||||
self.providers.values().any(|p| p.is_authenticated(cx))
|
||||
}
|
||||
|
||||
|
|
|
@ -1140,19 +1140,19 @@ impl RenderOnce for ZedAiConfiguration {
|
|||
let is_pro = self.plan == Some(proto::Plan::ZedPro);
|
||||
let subscription_text = match (self.plan, self.subscription_period) {
|
||||
(Some(proto::Plan::ZedPro), Some(_)) => {
|
||||
"You have access to Zed's hosted LLMs through your Pro subscription."
|
||||
"You have access to Zed's hosted models through your Pro subscription."
|
||||
}
|
||||
(Some(proto::Plan::ZedProTrial), Some(_)) => {
|
||||
"You have access to Zed's hosted LLMs through your Pro trial."
|
||||
"You have access to Zed's hosted models through your Pro trial."
|
||||
}
|
||||
(Some(proto::Plan::Free), Some(_)) => {
|
||||
"You have basic access to Zed's hosted LLMs through the Free plan."
|
||||
"You have basic access to Zed's hosted models through the Free plan."
|
||||
}
|
||||
_ => {
|
||||
if self.eligible_for_trial {
|
||||
"Subscribe for access to Zed's hosted LLMs. Start with a 14 day free trial."
|
||||
"Subscribe for access to Zed's hosted models. Start with a 14 day free trial."
|
||||
} else {
|
||||
"Subscribe for access to Zed's hosted LLMs."
|
||||
"Subscribe for access to Zed's hosted models."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1166,7 +1166,7 @@ impl RenderOnce for ZedAiConfiguration {
|
|||
Button::new("start_trial", "Start 14-day Free Pro Trial")
|
||||
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx)))
|
||||
.into_any_element()
|
||||
} else {
|
||||
Button::new("upgrade", "Upgrade to Pro")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue