
1. Welcome Page Open 2. Welcome Nav clicked 3. Skip clicked 4. Font changed 5. Import settings clicked 6. Inlay Hints 7. Git Blame 8. Format on Save 9. Font Ligature 10. Ai Enabled 11. Ai Provider Modal open Release Notes: - N/A --------- Co-authored-by: Marshall Bowers <git@maxdeviant.com>
431 lines
16 KiB
Rust
431 lines
16 KiB
Rust
use std::sync::Arc;
|
|
|
|
use ai_onboarding::AiUpsellCard;
|
|
use client::{Client, UserStore, zed_urls};
|
|
use fs::Fs;
|
|
use gpui::{
|
|
Action, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity,
|
|
Window, prelude::*,
|
|
};
|
|
use itertools;
|
|
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
|
use project::DisableAiSettings;
|
|
use settings::{Settings, update_settings_file};
|
|
use ui::{
|
|
Badge, ButtonLike, Divider, KeyBinding, Modal, ModalFooter, ModalHeader, Section, SwitchField,
|
|
ToggleState, prelude::*, tooltip_container,
|
|
};
|
|
use util::ResultExt;
|
|
use workspace::{ModalView, Workspace};
|
|
use zed_actions::agent::OpenSettings;
|
|
|
|
const FEATURED_PROVIDERS: [&'static str; 4] = ["anthropic", "google", "openai", "ollama"];
|
|
|
|
fn render_llm_provider_section(
|
|
tab_index: &mut isize,
|
|
workspace: WeakEntity<Workspace>,
|
|
disabled: bool,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> impl IntoElement {
|
|
v_flex()
|
|
.gap_4()
|
|
.child(
|
|
v_flex()
|
|
.child(Label::new("Or use other LLM providers").size(LabelSize::Large))
|
|
.child(
|
|
Label::new("Bring your API keys to use the available providers with Zed's UI for free.")
|
|
.color(Color::Muted),
|
|
),
|
|
)
|
|
.child(render_llm_provider_card(tab_index, workspace, disabled, window, cx))
|
|
}
|
|
|
|
fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> impl IntoElement {
|
|
let (title, description) = if disabled {
|
|
(
|
|
"AI is disabled across Zed",
|
|
"Re-enable it any time in Settings.",
|
|
)
|
|
} else {
|
|
(
|
|
"Privacy is the default for Zed",
|
|
"Any use or storage of your data is with your explicit, single-use, opt-in consent.",
|
|
)
|
|
};
|
|
|
|
v_flex()
|
|
.relative()
|
|
.pt_2()
|
|
.pb_2p5()
|
|
.pl_3()
|
|
.pr_2()
|
|
.border_1()
|
|
.border_dashed()
|
|
.border_color(cx.theme().colors().border.opacity(0.5))
|
|
.bg(cx.theme().colors().surface_background.opacity(0.3))
|
|
.rounded_lg()
|
|
.overflow_hidden()
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.justify_between()
|
|
.child(Label::new(title))
|
|
.child(
|
|
h_flex()
|
|
.gap_1()
|
|
.child(
|
|
Badge::new("Privacy")
|
|
.icon(IconName::ShieldCheck)
|
|
.tooltip(move |_, cx| cx.new(|_| AiPrivacyTooltip::new()).into()),
|
|
)
|
|
.child(
|
|
Button::new("learn_more", "Learn More")
|
|
.style(ButtonStyle::Outlined)
|
|
.label_size(LabelSize::Small)
|
|
.icon(IconName::ArrowUpRight)
|
|
.icon_size(IconSize::XSmall)
|
|
.icon_color(Color::Muted)
|
|
.on_click(|_, _, cx| {
|
|
cx.open_url(&zed_urls::ai_privacy_and_security(cx))
|
|
})
|
|
.tab_index({
|
|
*tab_index += 1;
|
|
*tab_index - 1
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
.child(
|
|
Label::new(description)
|
|
.size(LabelSize::Small)
|
|
.color(Color::Muted),
|
|
)
|
|
}
|
|
|
|
fn render_llm_provider_card(
|
|
tab_index: &mut isize,
|
|
workspace: WeakEntity<Workspace>,
|
|
disabled: bool,
|
|
_: &mut Window,
|
|
cx: &mut App,
|
|
) -> impl IntoElement {
|
|
let registry = LanguageModelRegistry::read_global(cx);
|
|
|
|
v_flex()
|
|
.border_1()
|
|
.border_color(cx.theme().colors().border)
|
|
.bg(cx.theme().colors().surface_background.opacity(0.5))
|
|
.rounded_lg()
|
|
.overflow_hidden()
|
|
.children(itertools::intersperse_with(
|
|
FEATURED_PROVIDERS
|
|
.into_iter()
|
|
.flat_map(|provider_name| {
|
|
registry.provider(&LanguageModelProviderId::new(provider_name))
|
|
})
|
|
.enumerate()
|
|
.map(|(index, provider)| {
|
|
let group_name = SharedString::new(format!("onboarding-hover-group-{}", index));
|
|
let is_authenticated = provider.is_authenticated(cx);
|
|
|
|
ButtonLike::new(("onboarding-ai-setup-buttons", index))
|
|
.size(ButtonSize::Large)
|
|
.tab_index({
|
|
*tab_index += 1;
|
|
*tab_index - 1
|
|
})
|
|
.child(
|
|
h_flex()
|
|
.group(&group_name)
|
|
.px_0p5()
|
|
.w_full()
|
|
.gap_2()
|
|
.justify_between()
|
|
.child(
|
|
h_flex()
|
|
.gap_1()
|
|
.child(
|
|
Icon::new(provider.icon())
|
|
.color(Color::Muted)
|
|
.size(IconSize::XSmall),
|
|
)
|
|
.child(Label::new(provider.name().0)),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.gap_1()
|
|
.when(!is_authenticated, |el| {
|
|
el.visible_on_hover(group_name.clone())
|
|
.child(
|
|
Icon::new(IconName::Settings)
|
|
.color(Color::Muted)
|
|
.size(IconSize::XSmall),
|
|
)
|
|
.child(
|
|
Label::new("Configure")
|
|
.color(Color::Muted)
|
|
.size(LabelSize::Small),
|
|
)
|
|
})
|
|
.when(is_authenticated && !disabled, |el| {
|
|
el.child(
|
|
Icon::new(IconName::Check)
|
|
.color(Color::Success)
|
|
.size(IconSize::XSmall),
|
|
)
|
|
.child(
|
|
Label::new("Configured")
|
|
.color(Color::Muted)
|
|
.size(LabelSize::Small),
|
|
)
|
|
}),
|
|
),
|
|
)
|
|
.on_click({
|
|
let workspace = workspace.clone();
|
|
move |_, window, cx| {
|
|
workspace
|
|
.update(cx, |workspace, cx| {
|
|
workspace.toggle_modal(window, cx, |window, cx| {
|
|
telemetry::event!(
|
|
"Welcome AI Modal Opened",
|
|
provider = provider.name().0,
|
|
);
|
|
|
|
let modal = AiConfigurationModal::new(
|
|
provider.clone(),
|
|
window,
|
|
cx,
|
|
);
|
|
window.focus(&modal.focus_handle(cx));
|
|
modal
|
|
});
|
|
})
|
|
.log_err();
|
|
}
|
|
})
|
|
.into_any_element()
|
|
}),
|
|
|| Divider::horizontal().into_any_element(),
|
|
))
|
|
.child(Divider::horizontal())
|
|
.child(
|
|
Button::new("agent_settings", "Add Many Others")
|
|
.size(ButtonSize::Large)
|
|
.icon(IconName::Plus)
|
|
.icon_position(IconPosition::Start)
|
|
.icon_color(Color::Muted)
|
|
.icon_size(IconSize::XSmall)
|
|
.on_click(|_event, window, cx| {
|
|
window.dispatch_action(OpenSettings.boxed_clone(), cx)
|
|
})
|
|
.tab_index({
|
|
*tab_index += 1;
|
|
*tab_index - 1
|
|
}),
|
|
)
|
|
}
|
|
|
|
pub(crate) fn render_ai_setup_page(
|
|
workspace: WeakEntity<Workspace>,
|
|
user_store: Entity<UserStore>,
|
|
client: Arc<Client>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> impl IntoElement {
|
|
let mut tab_index = 0;
|
|
let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
|
|
|
|
v_flex()
|
|
.gap_2()
|
|
.child(
|
|
SwitchField::new(
|
|
"enable_ai",
|
|
"Enable AI features",
|
|
None,
|
|
if is_ai_disabled {
|
|
ToggleState::Unselected
|
|
} else {
|
|
ToggleState::Selected
|
|
},
|
|
|&toggle_state, _, cx| {
|
|
let enabled = match toggle_state {
|
|
ToggleState::Indeterminate => {
|
|
return;
|
|
}
|
|
ToggleState::Unselected => true,
|
|
ToggleState::Selected => false,
|
|
};
|
|
|
|
telemetry::event!(
|
|
"Welcome AI Enabled",
|
|
toggle = if enabled { "on" } else { "off" },
|
|
);
|
|
|
|
let fs = <dyn Fs>::global(cx);
|
|
update_settings_file::<DisableAiSettings>(
|
|
fs,
|
|
cx,
|
|
move |ai_settings: &mut Option<bool>, _| {
|
|
*ai_settings = Some(enabled);
|
|
},
|
|
);
|
|
},
|
|
)
|
|
.tab_index({
|
|
tab_index += 1;
|
|
tab_index - 1
|
|
}),
|
|
)
|
|
.child(render_privacy_card(&mut tab_index, is_ai_disabled, cx))
|
|
.child(
|
|
v_flex()
|
|
.mt_2()
|
|
.gap_6()
|
|
.child({
|
|
let mut ai_upsell_card =
|
|
AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx);
|
|
|
|
ai_upsell_card.tab_index = Some({
|
|
tab_index += 1;
|
|
tab_index - 1
|
|
});
|
|
|
|
ai_upsell_card
|
|
})
|
|
.child(render_llm_provider_section(
|
|
&mut tab_index,
|
|
workspace,
|
|
is_ai_disabled,
|
|
window,
|
|
cx,
|
|
))
|
|
.when(is_ai_disabled, |this| {
|
|
this.child(
|
|
div()
|
|
.id("backdrop")
|
|
.size_full()
|
|
.absolute()
|
|
.inset_0()
|
|
.bg(cx.theme().colors().editor_background)
|
|
.opacity(0.8)
|
|
.block_mouse_except_scroll(),
|
|
)
|
|
}),
|
|
)
|
|
}
|
|
|
|
struct AiConfigurationModal {
|
|
focus_handle: FocusHandle,
|
|
selected_provider: Arc<dyn LanguageModelProvider>,
|
|
configuration_view: AnyView,
|
|
}
|
|
|
|
impl AiConfigurationModal {
|
|
fn new(
|
|
selected_provider: Arc<dyn LanguageModelProvider>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let focus_handle = cx.focus_handle();
|
|
let configuration_view = selected_provider.configuration_view(window, cx);
|
|
|
|
Self {
|
|
focus_handle,
|
|
configuration_view,
|
|
selected_provider,
|
|
}
|
|
}
|
|
|
|
fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
|
|
cx.emit(DismissEvent);
|
|
}
|
|
}
|
|
|
|
impl ModalView for AiConfigurationModal {}
|
|
|
|
impl EventEmitter<DismissEvent> for AiConfigurationModal {}
|
|
|
|
impl Focusable for AiConfigurationModal {
|
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
|
self.focus_handle.clone()
|
|
}
|
|
}
|
|
|
|
impl Render for AiConfigurationModal {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
v_flex()
|
|
.key_context("OnboardingAiConfigurationModal")
|
|
.w(rems(34.))
|
|
.elevation_3(cx)
|
|
.track_focus(&self.focus_handle)
|
|
.on_action(
|
|
cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
|
|
)
|
|
.child(
|
|
Modal::new("onboarding-ai-setup-modal", None)
|
|
.header(
|
|
ModalHeader::new()
|
|
.icon(
|
|
Icon::new(self.selected_provider.icon())
|
|
.color(Color::Muted)
|
|
.size(IconSize::Small),
|
|
)
|
|
.headline(self.selected_provider.name().0),
|
|
)
|
|
.section(Section::new().child(self.configuration_view.clone()))
|
|
.footer(
|
|
ModalFooter::new().end_slot(
|
|
Button::new("ai-onb-modal-Done", "Done")
|
|
.key_binding(
|
|
KeyBinding::for_action_in(
|
|
&menu::Cancel,
|
|
&self.focus_handle.clone(),
|
|
window,
|
|
cx,
|
|
)
|
|
.map(|kb| kb.size(rems_from_px(12.))),
|
|
)
|
|
.on_click(cx.listener(|this, _event, _window, cx| {
|
|
this.cancel(&menu::Cancel, cx)
|
|
})),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
pub struct AiPrivacyTooltip {}
|
|
|
|
impl AiPrivacyTooltip {
|
|
pub fn new() -> Self {
|
|
Self {}
|
|
}
|
|
}
|
|
|
|
impl Render for AiPrivacyTooltip {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
const DESCRIPTION: &'static str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. ";
|
|
|
|
tooltip_container(window, cx, move |this, _, _| {
|
|
this.child(
|
|
h_flex()
|
|
.gap_1()
|
|
.child(
|
|
Icon::new(IconName::ShieldCheck)
|
|
.size(IconSize::Small)
|
|
.color(Color::Muted),
|
|
)
|
|
.child(Label::new("Privacy First")),
|
|
)
|
|
.child(
|
|
div().max_w_64().child(
|
|
Label::new(DESCRIPTION)
|
|
.size(LabelSize::Small)
|
|
.color(Color::Muted),
|
|
),
|
|
)
|
|
})
|
|
}
|
|
}
|