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
|
@ -17,11 +17,13 @@ doctest = false
|
|||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
ai_onboarding.workspace = true
|
||||
anyhow.workspace = true
|
||||
arrayvec.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
copilot.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
|
@ -35,8 +37,6 @@ language.workspace = true
|
|||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
migrator.workspace = true
|
||||
paths.workspace = true
|
||||
postage.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
|
|
|
@ -34,7 +34,6 @@ pub fn init(cx: &mut App) {
|
|||
workspace,
|
||||
workspace.user_store().clone(),
|
||||
workspace.client().clone(),
|
||||
workspace.app_state().fs.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
|
|
@ -1,40 +1,33 @@
|
|||
use std::{sync::Arc, time::Duration};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{ZED_PREDICT_DATA_COLLECTION_CHOICE, onboarding_event};
|
||||
use anyhow::Context as _;
|
||||
use crate::{ZedPredictUpsell, onboarding_event};
|
||||
use ai_onboarding::EditPredictionOnboarding;
|
||||
use client::{Client, UserStore};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use db::kvp::Dismissable;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, MouseDownEvent, Render, ease_in_out, svg,
|
||||
ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
|
||||
linear_color_stop, linear_gradient,
|
||||
};
|
||||
use language::language_settings::{AllLanguageSettings, EditPredictionProvider};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use ui::{Checkbox, TintColor, prelude::*};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Workspace, notifications::NotifyTaskExt};
|
||||
use settings::update_settings_file;
|
||||
use ui::{Vector, VectorName, prelude::*};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
/// Introduces user to Zed's Edit Prediction feature and terms of service
|
||||
pub struct ZedPredictModal {
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
onboarding: Entity<EditPredictionOnboarding>,
|
||||
focus_handle: FocusHandle,
|
||||
sign_in_status: SignInStatus,
|
||||
terms_of_service: bool,
|
||||
data_collection_expanded: bool,
|
||||
data_collection_opted_in: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum SignInStatus {
|
||||
/// Signed out or signed in but not from this modal
|
||||
Idle,
|
||||
/// Authentication triggered from this modal
|
||||
Waiting,
|
||||
/// Signed in after authentication from this modal
|
||||
SignedIn,
|
||||
pub(crate) fn set_edit_prediction_provider(provider: EditPredictionProvider, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |settings, _| {
|
||||
settings
|
||||
.features
|
||||
.get_or_insert(Default::default())
|
||||
.edit_prediction_provider = Some(provider);
|
||||
});
|
||||
}
|
||||
|
||||
impl ZedPredictModal {
|
||||
|
@ -42,127 +35,45 @@ impl ZedPredictModal {
|
|||
workspace: &mut Workspace,
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.toggle_modal(window, cx, |_window, cx| Self {
|
||||
user_store,
|
||||
client,
|
||||
fs,
|
||||
focus_handle: cx.focus_handle(),
|
||||
sign_in_status: SignInStatus::Idle,
|
||||
terms_of_service: false,
|
||||
data_collection_expanded: false,
|
||||
data_collection_opted_in: false,
|
||||
workspace.toggle_modal(window, cx, |_window, cx| {
|
||||
let weak_entity = cx.weak_entity();
|
||||
Self {
|
||||
onboarding: cx.new(|cx| {
|
||||
EditPredictionOnboarding::new(
|
||||
user_store.clone(),
|
||||
client.clone(),
|
||||
copilot::Copilot::global(cx)
|
||||
.map_or(false, |copilot| copilot.read(cx).status().is_configured()),
|
||||
Arc::new({
|
||||
let this = weak_entity.clone();
|
||||
move |_window, cx| {
|
||||
ZedPredictUpsell::set_dismissed(true, cx);
|
||||
set_edit_prediction_provider(EditPredictionProvider::Zed, cx);
|
||||
this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
|
||||
}
|
||||
}),
|
||||
Arc::new({
|
||||
let this = weak_entity.clone();
|
||||
move |window, cx| {
|
||||
ZedPredictUpsell::set_dismissed(true, cx);
|
||||
set_edit_prediction_provider(EditPredictionProvider::Copilot, cx);
|
||||
this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
|
||||
copilot::initiate_sign_in(window, cx);
|
||||
}
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn view_terms(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.open_url("https://zed.dev/terms-of-service");
|
||||
cx.notify();
|
||||
|
||||
onboarding_event!("ToS Link Clicked");
|
||||
}
|
||||
|
||||
fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.open_url("https://zed.dev/blog/edit-prediction");
|
||||
cx.notify();
|
||||
|
||||
onboarding_event!("Blog Link clicked");
|
||||
}
|
||||
|
||||
fn inline_completions_doc(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.open_url("https://zed.dev/docs/configuring-zed#disabled-globs");
|
||||
cx.notify();
|
||||
|
||||
onboarding_event!("Docs Link Clicked");
|
||||
}
|
||||
|
||||
fn accept_and_enable(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task = self
|
||||
.user_store
|
||||
.update(cx, |this, cx| this.accept_terms_of_service(cx));
|
||||
let fs = self.fs.clone();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
task.await?;
|
||||
|
||||
let mut data_collection_opted_in = false;
|
||||
this.update(cx, |this, _cx| {
|
||||
data_collection_opted_in = this.data_collection_opted_in;
|
||||
})
|
||||
.ok();
|
||||
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(
|
||||
ZED_PREDICT_DATA_COLLECTION_CHOICE.into(),
|
||||
data_collection_opted_in.to_string(),
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
// Make sure edit prediction provider setting is using the new key
|
||||
let settings_path = paths::settings_file().as_path();
|
||||
let settings_path = fs.canonicalize(settings_path).await.with_context(|| {
|
||||
format!("Failed to canonicalize settings path {:?}", settings_path)
|
||||
})?;
|
||||
|
||||
if let Some(settings) = fs.load(&settings_path).await.log_err() {
|
||||
if let Some(new_settings) =
|
||||
migrator::migrate_edit_prediction_provider_settings(&settings)?
|
||||
{
|
||||
fs.atomic_write(settings_path, new_settings).await?;
|
||||
}
|
||||
}
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
update_settings_file::<AllLanguageSettings>(this.fs.clone(), cx, move |file, _| {
|
||||
file.features
|
||||
.get_or_insert(Default::default())
|
||||
.edit_prediction_provider = Some(EditPredictionProvider::Zed);
|
||||
});
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
})
|
||||
.detach_and_notify_err(window, cx);
|
||||
|
||||
onboarding_event!(
|
||||
"Enable Clicked",
|
||||
data_collection_opted_in = self.data_collection_opted_in,
|
||||
);
|
||||
}
|
||||
|
||||
fn sign_in(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let client = self.client.clone();
|
||||
self.sign_in_status = SignInStatus::Waiting;
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let result = client
|
||||
.authenticate_and_connect(true, &cx)
|
||||
.await
|
||||
.into_response();
|
||||
|
||||
let status = match result {
|
||||
Ok(_) => SignInStatus::SignedIn,
|
||||
Err(_) => SignInStatus::Idle,
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.sign_in_status = status;
|
||||
onboarding_event!("Signed In");
|
||||
cx.notify()
|
||||
})?;
|
||||
|
||||
result
|
||||
})
|
||||
.detach_and_notify_err(window, cx);
|
||||
|
||||
onboarding_event!("Sign In Clicked");
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
ZedPredictUpsell::set_dismissed(true, cx);
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
@ -177,85 +88,12 @@ impl Focusable for ZedPredictModal {
|
|||
|
||||
impl ModalView for ZedPredictModal {}
|
||||
|
||||
impl ZedPredictModal {
|
||||
fn render_data_collection_explanation(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
fn label_item(label_text: impl Into<SharedString>) -> impl Element {
|
||||
Label::new(label_text).color(Color::Muted).into_element()
|
||||
}
|
||||
|
||||
fn info_item(label_text: impl Into<SharedString>) -> impl Element {
|
||||
h_flex()
|
||||
.items_start()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.mt_1p5()
|
||||
.child(Icon::new(IconName::Check).size(IconSize::XSmall)),
|
||||
)
|
||||
.child(div().w_full().child(label_item(label_text)))
|
||||
}
|
||||
|
||||
fn multiline_info_item<E1: Into<SharedString>, E2: IntoElement>(
|
||||
first_line: E1,
|
||||
second_line: E2,
|
||||
) -> impl Element {
|
||||
v_flex()
|
||||
.child(info_item(first_line))
|
||||
.child(div().pl_5().child(second_line))
|
||||
}
|
||||
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.p_2()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().editor_background.opacity(0.5))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
div().child(
|
||||
Label::new("To improve edit predictions, please consider contributing to our open dataset based on your interactions within open source repositories.")
|
||||
.mb_1()
|
||||
)
|
||||
)
|
||||
.child(info_item(
|
||||
"We collect data exclusively from open source projects.",
|
||||
))
|
||||
.child(info_item(
|
||||
"Zed automatically detects if your project is open source.",
|
||||
))
|
||||
.child(info_item("Toggle participation at any time via the status bar menu."))
|
||||
.child(multiline_info_item(
|
||||
"If turned on, this setting applies for all open source repositories",
|
||||
label_item("you open in Zed.")
|
||||
))
|
||||
.child(multiline_info_item(
|
||||
"Files with sensitive data, like `.env`, are excluded by default",
|
||||
h_flex()
|
||||
.w_full()
|
||||
.flex_wrap()
|
||||
.child(label_item("via the"))
|
||||
.child(
|
||||
Button::new("doc-link", "disabled_globs").on_click(
|
||||
cx.listener(Self::inline_completions_doc),
|
||||
),
|
||||
)
|
||||
.child(label_item("setting.")),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ZedPredictModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let window_height = window.viewport_size().height;
|
||||
let max_height = window_height - px(200.);
|
||||
|
||||
let has_subscription_period = self.user_store.read(cx).subscription_period().is_some();
|
||||
let plan = self.user_store.read(cx).current_plan().filter(|_| {
|
||||
// Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
|
||||
has_subscription_period
|
||||
});
|
||||
|
||||
let base = v_flex()
|
||||
v_flex()
|
||||
.id("edit-prediction-onboarding")
|
||||
.key_context("ZedPredictModal")
|
||||
.relative()
|
||||
|
@ -264,14 +102,9 @@ impl Render for ZedPredictModal {
|
|||
.max_h(max_height)
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.when(self.data_collection_expanded, |element| {
|
||||
element.overflow_y_scroll()
|
||||
})
|
||||
.when(!self.data_collection_expanded, |element| {
|
||||
element.overflow_hidden()
|
||||
})
|
||||
.elevation_3(cx)
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.overflow_hidden()
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
|
||||
onboarding_event!("Cancelled", trigger = "Action");
|
||||
|
@ -282,77 +115,30 @@ impl Render for ZedPredictModal {
|
|||
}))
|
||||
.child(
|
||||
div()
|
||||
.p_1p5()
|
||||
.opacity(0.5)
|
||||
.absolute()
|
||||
.top_1()
|
||||
.left_1()
|
||||
.top(px(-8.0))
|
||||
.right_0()
|
||||
.h(px(200.))
|
||||
.w(px(400.))
|
||||
.h(px(92.))
|
||||
.child(
|
||||
svg()
|
||||
.path("icons/zed_predict_bg.svg")
|
||||
.text_color(cx.theme().colors().icon_disabled)
|
||||
.w(px(530.))
|
||||
.h(px(128.))
|
||||
.overflow_hidden(),
|
||||
Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.))
|
||||
.color(Color::Custom(cx.theme().colors().text.alpha(0.32))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.mb_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Introducing Zed AI's")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Headline::new("Edit Prediction").size(HeadlineSize::Large)),
|
||||
)
|
||||
.child({
|
||||
let tab = |n: usize| {
|
||||
let text_color = cx.theme().colors().text;
|
||||
let border_color = cx.theme().colors().text_accent.opacity(0.4);
|
||||
|
||||
h_flex().child(
|
||||
h_flex()
|
||||
.px_4()
|
||||
.py_0p5()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.rounded_sm()
|
||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.text_size(TextSize::XSmall.rems(cx))
|
||||
.text_color(text_color)
|
||||
.child("tab")
|
||||
.with_animation(
|
||||
n,
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
move |tab, delta| {
|
||||
let delta = (delta - 0.15 * n as f32) / 0.7;
|
||||
let delta = 1.0 - (0.5 - delta).abs() * 2.;
|
||||
let delta = ease_in_out(delta.clamp(0., 1.));
|
||||
let delta = 0.1 + 0.9 * delta;
|
||||
|
||||
tab.border_color(border_color.opacity(delta))
|
||||
.text_color(text_color.opacity(delta))
|
||||
},
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.pr_2p5()
|
||||
.child(tab(0).ml_neg_20())
|
||||
.child(tab(1))
|
||||
.child(tab(2).ml_20())
|
||||
}),
|
||||
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),
|
||||
)),
|
||||
)
|
||||
.child(h_flex().absolute().top_2().right_2().child(
|
||||
IconButton::new("cancel", IconName::X).on_click(cx.listener(
|
||||
|
@ -361,148 +147,7 @@ impl Render for ZedPredictModal {
|
|||
cx.emit(DismissEvent);
|
||||
},
|
||||
)),
|
||||
));
|
||||
|
||||
let blog_post_button = Button::new("view-blog", "Read the Blog Post")
|
||||
.full_width()
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener(Self::view_blog));
|
||||
|
||||
if self.user_store.read(cx).current_user().is_some() {
|
||||
let copy = match self.sign_in_status {
|
||||
SignInStatus::Idle => {
|
||||
"Zed can now predict your next edit on every keystroke. Powered by Zeta, our open-source, open-dataset language model."
|
||||
}
|
||||
SignInStatus::SignedIn => "Almost there! Ensure you:",
|
||||
SignInStatus::Waiting => unreachable!(),
|
||||
};
|
||||
|
||||
let accordion_icons = if self.data_collection_expanded {
|
||||
(IconName::ChevronUp, IconName::ChevronDown)
|
||||
} else {
|
||||
(IconName::ChevronDown, IconName::ChevronUp)
|
||||
};
|
||||
let plan = plan.unwrap_or(proto::Plan::Free);
|
||||
|
||||
base.child(Label::new(copy).color(Color::Muted))
|
||||
.child(
|
||||
h_flex().child(
|
||||
Checkbox::new("plan", ToggleState::Selected)
|
||||
.fill()
|
||||
.disabled(true)
|
||||
.label(format!(
|
||||
"You get {} edit predictions through your {}.",
|
||||
if plan == proto::Plan::Free {
|
||||
"2,000"
|
||||
} else {
|
||||
"unlimited"
|
||||
},
|
||||
match plan {
|
||||
proto::Plan::Free => "Zed Free plan",
|
||||
proto::Plan::ZedPro => "Zed Pro plan",
|
||||
proto::Plan::ZedProTrial => "Zed Pro trial",
|
||||
}
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.child(
|
||||
Checkbox::new("tos-checkbox", self.terms_of_service.into())
|
||||
.fill()
|
||||
.label("I have read and accept the")
|
||||
.on_click(cx.listener(move |this, state, _window, cx| {
|
||||
this.terms_of_service = *state == ToggleState::Selected;
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("view-tos", "Terms of Service")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener(Self::view_terms)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
.child(
|
||||
Checkbox::new(
|
||||
"training-data-checkbox",
|
||||
self.data_collection_opted_in.into(),
|
||||
)
|
||||
.label(
|
||||
"Contribute to the open dataset when editing open source.",
|
||||
)
|
||||
.fill()
|
||||
.on_click(cx.listener(
|
||||
move |this, state, _window, cx| {
|
||||
this.data_collection_opted_in =
|
||||
*state == ToggleState::Selected;
|
||||
cx.notify()
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Button::new("learn-more", "Learn More")
|
||||
.icon(accordion_icons.0)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.data_collection_expanded =
|
||||
!this.data_collection_expanded;
|
||||
cx.notify();
|
||||
|
||||
if this.data_collection_expanded {
|
||||
onboarding_event!(
|
||||
"Data Collection Learn More Clicked"
|
||||
);
|
||||
}
|
||||
})),
|
||||
),
|
||||
)
|
||||
.when(self.data_collection_expanded, |element| {
|
||||
element.child(self.render_data_collection_explanation(cx))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.child(
|
||||
Button::new("accept-tos", "Enable Edit Prediction")
|
||||
.disabled(!self.terms_of_service)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::accept_and_enable)),
|
||||
)
|
||||
.child(blog_post_button),
|
||||
)
|
||||
} else {
|
||||
base.child(
|
||||
Label::new("To set Zed as your edit prediction provider, please sign in.")
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.child(
|
||||
Button::new("accept-tos", "Sign in with GitHub")
|
||||
.disabled(self.sign_in_status == SignInStatus::Waiting)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::sign_in)),
|
||||
)
|
||||
.child(blog_post_button),
|
||||
)
|
||||
}
|
||||
))
|
||||
.child(self.onboarding.clone())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ mod onboarding_telemetry;
|
|||
mod rate_completion_modal;
|
||||
|
||||
pub(crate) use completion_diff_element::*;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
pub use init::*;
|
||||
use inline_completion::DataCollectionState;
|
||||
use license_detection::LICENSE_FILES_TO_CHECK;
|
||||
|
@ -95,6 +95,38 @@ impl std::fmt::Display for InlineCompletionId {
|
|||
}
|
||||
}
|
||||
|
||||
struct ZedPredictUpsell;
|
||||
|
||||
impl Dismissable for ZedPredictUpsell {
|
||||
const KEY: &'static str = "dismissed-edit-predict-upsell";
|
||||
|
||||
fn dismissed() -> bool {
|
||||
// To make this backwards compatible with older versions of Zed, we
|
||||
// check if the user has seen the previous Edit Prediction Onboarding
|
||||
// before, by checking the data collection choice which was written to
|
||||
// the database once the user clicked on "Accept and Enable"
|
||||
if KEY_VALUE_STORE
|
||||
.read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE)
|
||||
.log_err()
|
||||
.map_or(false, |s| s.is_some())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
KEY_VALUE_STORE
|
||||
.read_kvp(Self::KEY)
|
||||
.log_err()
|
||||
.map_or(false, |s| s.is_some())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_show_upsell_modal(user_store: &Entity<UserStore>, cx: &App) -> bool {
|
||||
match user_store.read(cx).current_user_has_accepted_terms() {
|
||||
Some(true) => !ZedPredictUpsell::dismissed(),
|
||||
Some(false) | None => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ZetaGlobal(Entity<Zeta>);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue