Add refinements to the AI onboarding flow (#33738)
This includes making sure that both the agent panel and Zed's edit prediction have a consistent narrative when it comes to onboarding users into the AI features, considering the possible different plans and conditions (such as being signed in/out, account age, etc.) Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
This commit is contained in:
parent
9a20843ba2
commit
4476860664
33 changed files with 1465 additions and 1215 deletions
27
crates/ai_onboarding/Cargo.toml
Normal file
27
crates/ai_onboarding/Cargo.toml
Normal file
|
@ -0,0 +1,27 @@
|
|||
[package]
|
||||
name = "ai_onboarding"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/ai_onboarding.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
client.workspace = true
|
||||
component.workspace = true
|
||||
gpui.workspace = true
|
||||
language_model.workspace = true
|
||||
proto.workspace = true
|
||||
serde.workspace = true
|
||||
smallvec.workspace = true
|
||||
ui.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_actions.workspace = true
|
1
crates/ai_onboarding/LICENSE-GPL
Symbolic link
1
crates/ai_onboarding/LICENSE-GPL
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE-GPL
|
81
crates/ai_onboarding/src/agent_panel_onboarding_card.rs
Normal file
81
crates/ai_onboarding/src/agent_panel_onboarding_card.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
use gpui::{AnyElement, IntoElement, ParentElement, linear_color_stop, linear_gradient};
|
||||
use smallvec::SmallVec;
|
||||
use ui::{Vector, VectorName, prelude::*};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct AgentPanelOnboardingCard {
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl AgentPanelOnboardingCard {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for AgentPanelOnboardingCard {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AgentPanelOnboardingCard {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
div()
|
||||
.m_4()
|
||||
.p(px(3.))
|
||||
.elevation_2(cx)
|
||||
.rounded_lg()
|
||||
.bg(cx.theme().colors().background.alpha(0.5))
|
||||
.child(
|
||||
v_flex()
|
||||
.relative()
|
||||
.size_full()
|
||||
.px_4()
|
||||
.py_3()
|
||||
.gap_2()
|
||||
.border_1()
|
||||
.rounded(px(5.))
|
||||
.border_color(cx.theme().colors().text.alpha(0.1))
|
||||
.overflow_hidden()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.child(
|
||||
div()
|
||||
.opacity(0.5)
|
||||
.absolute()
|
||||
.top(px(-8.0))
|
||||
.right_0()
|
||||
.w(px(400.))
|
||||
.h(px(92.))
|
||||
.child(
|
||||
Vector::new(
|
||||
VectorName::AiGrid,
|
||||
rems_from_px(400.),
|
||||
rems_from_px(92.),
|
||||
)
|
||||
.color(Color::Custom(cx.theme().colors().text.alpha(0.32))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.w(px(660.))
|
||||
.h(px(401.))
|
||||
.overflow_hidden()
|
||||
.bg(linear_gradient(
|
||||
75.,
|
||||
linear_color_stop(
|
||||
cx.theme().colors().panel_background.alpha(0.01),
|
||||
1.0,
|
||||
),
|
||||
linear_color_stop(cx.theme().colors().panel_background, 0.45),
|
||||
)),
|
||||
)
|
||||
.children(self.children),
|
||||
)
|
||||
}
|
||||
}
|
145
crates/ai_onboarding/src/agent_panel_onboarding_content.rs
Normal file
145
crates/ai_onboarding/src/agent_panel_onboarding_content.rs
Normal file
|
@ -0,0 +1,145 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use client::{Client, UserStore};
|
||||
use gpui::{Action, ClickEvent, Entity, IntoElement, ParentElement};
|
||||
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||
use ui::{Divider, List, prelude::*};
|
||||
use zed_actions::agent::{OpenConfiguration, ToggleModelSelector};
|
||||
|
||||
use crate::{AgentPanelOnboardingCard, BulletItem, ZedAiOnboarding};
|
||||
|
||||
pub struct AgentPanelOnboarding {
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
configured_providers: Vec<(IconName, SharedString)>,
|
||||
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
}
|
||||
|
||||
impl AgentPanelOnboarding {
|
||||
pub fn new(
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
continue_with_zed_ai: impl Fn(&mut Window, &mut App) + 'static,
|
||||
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_available_providers(cx)
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
user_store,
|
||||
client,
|
||||
configured_providers: Self::compute_available_providers(cx),
|
||||
continue_with_zed_ai: Arc::new(continue_with_zed_ai),
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_available_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()
|
||||
}
|
||||
|
||||
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 {
|
||||
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))
|
||||
}
|
||||
}
|
397
crates/ai_onboarding/src/ai_onboarding.rs
Normal file
397
crates/ai_onboarding/src/ai_onboarding.rs
Normal file
|
@ -0,0 +1,397 @@
|
|||
mod agent_panel_onboarding_card;
|
||||
mod agent_panel_onboarding_content;
|
||||
mod edit_prediction_onboarding_content;
|
||||
mod young_account_banner;
|
||||
|
||||
pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
|
||||
pub use agent_panel_onboarding_content::AgentPanelOnboarding;
|
||||
pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
|
||||
pub use young_account_banner::YoungAccountBanner;
|
||||
|
||||
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::*};
|
||||
|
||||
pub struct BulletItem {
|
||||
label: SharedString,
|
||||
}
|
||||
|
||||
impl BulletItem {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for BulletItem {
|
||||
type Element = AnyElement;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
ListItem::new("list-item")
|
||||
.selectable(false)
|
||||
.start_slot(
|
||||
Icon::new(IconName::Dash)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Hidden),
|
||||
)
|
||||
.child(div().w_full().child(Label::new(self.label)))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SignInStatus {
|
||||
SignedIn,
|
||||
SigningIn,
|
||||
SignedOut,
|
||||
}
|
||||
|
||||
impl From<client::Status> for SignInStatus {
|
||||
fn from(status: client::Status) -> Self {
|
||||
if status.is_signing_in() {
|
||||
Self::SigningIn
|
||||
} else if status.is_signed_out() {
|
||||
Self::SignedOut
|
||||
} else {
|
||||
Self::SignedIn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(RegisterComponent, IntoElement)]
|
||||
pub struct ZedAiOnboarding {
|
||||
pub sign_in_status: SignInStatus,
|
||||
pub has_accepted_terms_of_service: bool,
|
||||
pub plan: Option<proto::Plan>,
|
||||
pub account_too_young: bool,
|
||||
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)>,
|
||||
}
|
||||
|
||||
impl ZedAiOnboarding {
|
||||
pub fn new(
|
||||
client: Arc<Client>,
|
||||
user_store: &Entity<UserStore>,
|
||||
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
cx: &mut App,
|
||||
) -> 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),
|
||||
plan: store.current_plan(),
|
||||
account_too_young: store.account_too_young(),
|
||||
continue_with_zed_ai,
|
||||
accept_terms_of_service: Arc::new({
|
||||
let store = user_store.clone();
|
||||
move |_window, cx| {
|
||||
let task = store.update(cx, |store, cx| store.accept_terms_of_service(cx));
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
}),
|
||||
sign_in: Arc::new(move |_window, cx| {
|
||||
cx.spawn({
|
||||
let client = client.clone();
|
||||
async move |cx| {
|
||||
client.authenticate_and_connect(true, cx).await;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_free_plan_section(&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()
|
||||
.child(
|
||||
Label::new("Free")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(Divider::horizontal()),
|
||||
)
|
||||
.child(
|
||||
List::new()
|
||||
.child(BulletItem::new(
|
||||
"50 prompts per month with the Claude models",
|
||||
))
|
||||
.child(BulletItem::new(
|
||||
"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::account_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.",
|
||||
))
|
||||
.child(
|
||||
Button::new("terms_of_service", "View and Read the Terms of Service")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.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")
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("accept_terms", "I've read it and accept it")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.on_click({
|
||||
let callback = self.accept_terms_of_service.clone();
|
||||
move |_, window, cx| (callback)(window, cx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
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.";
|
||||
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.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)
|
||||
.disabled(signing_in)
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click({
|
||||
let callback = self.sign_in.clone();
|
||||
move |_, window, cx| callback(window, cx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_free_plan_onboarding(&self, cx: &mut App) -> Div {
|
||||
const PLANS_DESCRIPTION: &str = "Choose how you want to start.";
|
||||
let young_account_banner = YoungAccountBanner;
|
||||
|
||||
v_flex()
|
||||
.child(Headline::new("Welcome to Zed AI"))
|
||||
.child(
|
||||
Label::new(PLANS_DESCRIPTION)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mt_1()
|
||||
.mb_3(),
|
||||
)
|
||||
.when(self.account_too_young, |this| {
|
||||
this.child(young_account_banner)
|
||||
})
|
||||
.child(self.render_free_plan_section(cx))
|
||||
.child(self.render_pro_plan_section(cx))
|
||||
}
|
||||
|
||||
fn render_trial_onboarding(&self, _cx: &mut App) -> Div {
|
||||
v_flex()
|
||||
.child(Headline::new("Welcome to the trial of Zed Pro"))
|
||||
.child(
|
||||
Label::new("Here's what you get for the next 14 days:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mt_1(),
|
||||
)
|
||||
.child(
|
||||
List::new()
|
||||
.child(BulletItem::new("150 prompts with Claude models"))
|
||||
.child(BulletItem::new(
|
||||
"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)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_pro_plan_onboarding(&self, _cx: &mut App) -> Div {
|
||||
v_flex()
|
||||
.child(Headline::new("Welcome to Zed Pro"))
|
||||
.child(
|
||||
Label::new("Here's what you get:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mt_1(),
|
||||
)
|
||||
.child(
|
||||
List::new()
|
||||
.child(BulletItem::new("500 prompts with Claude models"))
|
||||
.child(BulletItem::new("Unlimited edit predictions")),
|
||||
)
|
||||
.child(
|
||||
Button::new("pro", "Continue with Zed Pro")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.on_click({
|
||||
let callback = self.continue_with_zed_ai.clone();
|
||||
move |_, window, cx| callback(window, cx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ZedAiOnboarding {
|
||||
fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
|
||||
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),
|
||||
}
|
||||
} else {
|
||||
self.render_accept_terms_of_service()
|
||||
}
|
||||
} else {
|
||||
self.render_sign_in_disclaimer(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ZedAiOnboarding {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Agent
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
fn onboarding(
|
||||
sign_in_status: SignInStatus,
|
||||
has_accepted_terms_of_service: bool,
|
||||
plan: Option<proto::Plan>,
|
||||
account_too_young: bool,
|
||||
) -> AnyElement {
|
||||
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()
|
||||
}
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.p_4()
|
||||
.gap_4()
|
||||
.children(vec![
|
||||
single_example(
|
||||
"Not Signed-in",
|
||||
onboarding(SignInStatus::SignedOut, false, None, false),
|
||||
),
|
||||
single_example(
|
||||
"Not Accepted ToS",
|
||||
onboarding(SignInStatus::SignedIn, false, None, false),
|
||||
),
|
||||
single_example(
|
||||
"Account too young",
|
||||
onboarding(SignInStatus::SignedIn, false, None, true),
|
||||
),
|
||||
single_example(
|
||||
"Free Plan",
|
||||
onboarding(SignInStatus::SignedIn, true, Some(proto::Plan::Free), false),
|
||||
),
|
||||
single_example(
|
||||
"Pro Trial",
|
||||
onboarding(
|
||||
SignInStatus::SignedIn,
|
||||
true,
|
||||
Some(proto::Plan::ZedProTrial),
|
||||
false,
|
||||
),
|
||||
),
|
||||
single_example(
|
||||
"Pro Plan",
|
||||
onboarding(
|
||||
SignInStatus::SignedIn,
|
||||
true,
|
||||
Some(proto::Plan::ZedPro),
|
||||
false,
|
||||
),
|
||||
),
|
||||
])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use client::{Client, UserStore};
|
||||
use gpui::{Entity, IntoElement, ParentElement};
|
||||
use ui::prelude::*;
|
||||
|
||||
use crate::ZedAiOnboarding;
|
||||
|
||||
pub struct EditPredictionOnboarding {
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
copilot_is_configured: bool,
|
||||
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
continue_with_copilot: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
}
|
||||
|
||||
impl EditPredictionOnboarding {
|
||||
pub fn new(
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
copilot_is_configured: bool,
|
||||
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
continue_with_copilot: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
user_store,
|
||||
copilot_is_configured,
|
||||
client,
|
||||
continue_with_zed_ai,
|
||||
continue_with_copilot,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for EditPredictionOnboarding {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let github_copilot = v_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(if self.copilot_is_configured {
|
||||
"Alternatively, you can continue to use GitHub Copilot as that's already set up."
|
||||
} else {
|
||||
"Alternatively, you can use GitHub Copilot as your edit prediction provider."
|
||||
}))
|
||||
.child(
|
||||
Button::new(
|
||||
"configure-copilot",
|
||||
if self.copilot_is_configured {
|
||||
"Use Copilot"
|
||||
} else {
|
||||
"Configure Copilot"
|
||||
},
|
||||
)
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.on_click({
|
||||
let callback = self.continue_with_copilot.clone();
|
||||
move |_, window, cx| callback(window, cx)
|
||||
}),
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(ZedAiOnboarding::new(
|
||||
self.client.clone(),
|
||||
&self.user_store,
|
||||
self.continue_with_zed_ai.clone(),
|
||||
cx,
|
||||
))
|
||||
.child(ui::Divider::horizontal())
|
||||
.child(github_copilot)
|
||||
}
|
||||
}
|
21
crates/ai_onboarding/src/young_account_banner.rs
Normal file
21
crates/ai_onboarding/src/young_account_banner.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use gpui::{IntoElement, ParentElement};
|
||||
use ui::{Banner, prelude::*};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
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.";
|
||||
|
||||
let label = div()
|
||||
.w_full()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(YOUNG_ACCOUNT_DISCLAIMER);
|
||||
|
||||
div()
|
||||
.my_1()
|
||||
.child(Banner::new().severity(ui::Severity::Warning).child(label))
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue