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
22
Cargo.lock
generated
22
Cargo.lock
generated
|
@ -195,6 +195,7 @@ dependencies = [
|
|||
"agent_servers",
|
||||
"agent_settings",
|
||||
"agentic-coding-protocol",
|
||||
"ai_onboarding",
|
||||
"anyhow",
|
||||
"assistant_context",
|
||||
"assistant_slash_command",
|
||||
|
@ -329,6 +330,22 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ai_onboarding"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"client",
|
||||
"component",
|
||||
"gpui",
|
||||
"language_model",
|
||||
"proto",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"ui",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alacritty_terminal"
|
||||
version = "0.25.1-dev"
|
||||
|
@ -9066,6 +9083,7 @@ dependencies = [
|
|||
name = "language_models"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ai_onboarding",
|
||||
"anthropic",
|
||||
"anyhow",
|
||||
"aws-config",
|
||||
|
@ -20510,6 +20528,7 @@ dependencies = [
|
|||
name = "zeta"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ai_onboarding",
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"call",
|
||||
|
@ -20517,6 +20536,7 @@ dependencies = [
|
|||
"clock",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"copilot",
|
||||
"ctor",
|
||||
"db",
|
||||
"editor",
|
||||
|
@ -20531,8 +20551,6 @@ dependencies = [
|
|||
"language_model",
|
||||
"log",
|
||||
"menu",
|
||||
"migrator",
|
||||
"paths",
|
||||
"postage",
|
||||
"project",
|
||||
"proto",
|
||||
|
|
|
@ -6,6 +6,7 @@ members = [
|
|||
"crates/agent_ui",
|
||||
"crates/agent",
|
||||
"crates/agent_settings",
|
||||
"crates/ai_onboarding",
|
||||
"crates/agent_servers",
|
||||
"crates/anthropic",
|
||||
"crates/askpass",
|
||||
|
@ -227,6 +228,7 @@ agent_ui = { path = "crates/agent_ui" }
|
|||
agent_settings = { path = "crates/agent_settings" }
|
||||
agent_servers = { path = "crates/agent_servers" }
|
||||
ai = { path = "crates/ai" }
|
||||
ai_onboarding = { path = "crates/ai_onboarding" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
askpass = { path = "crates/askpass" }
|
||||
assets = { path = "crates/assets" }
|
||||
|
|
|
@ -21,6 +21,7 @@ agent.workspace = true
|
|||
agentic-coding-protocol.workspace = true
|
||||
agent_settings.workspace = true
|
||||
agent_servers.workspace = true
|
||||
ai_onboarding.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_context.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
use crate::{
|
||||
ModelUsageContext,
|
||||
language_model_selector::{
|
||||
LanguageModelSelector, ToggleModelSelector, language_model_selector,
|
||||
},
|
||||
language_model_selector::{LanguageModelSelector, language_model_selector},
|
||||
};
|
||||
use agent_settings::AgentSettings;
|
||||
use fs::Fs;
|
||||
|
@ -12,6 +10,7 @@ use picker::popover_menu::PickerPopoverMenu;
|
|||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
pub struct AgentModelSelector {
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
|
@ -96,22 +95,18 @@ impl Render for AgentModelSelector {
|
|||
let model_name = model
|
||||
.as_ref()
|
||||
.map(|model| model.model.name().0)
|
||||
.unwrap_or_else(|| SharedString::from("No model selected"));
|
||||
let provider_icon = model
|
||||
.as_ref()
|
||||
.map(|model| model.provider.icon())
|
||||
.unwrap_or_else(|| IconName::Ai);
|
||||
.unwrap_or_else(|| SharedString::from("Select a Model"));
|
||||
|
||||
let provider_icon = model.as_ref().map(|model| model.provider.icon());
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.child(
|
||||
Icon::new(provider_icon)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.when_some(provider_icon, |this, icon| {
|
||||
this.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall))
|
||||
})
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(Color::Muted)
|
||||
|
|
|
@ -11,7 +11,6 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
use crate::NewExternalAgentThread;
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
use crate::language_model_selector::ToggleModelSelector;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
||||
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
||||
|
@ -28,7 +27,7 @@ use crate::{
|
|||
render_remaining_tokens,
|
||||
},
|
||||
thread_history::{HistoryEntryElement, ThreadHistory},
|
||||
ui::AgentOnboardingModal,
|
||||
ui::{AgentOnboardingModal, EndTrialUpsell},
|
||||
};
|
||||
use agent::{
|
||||
Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio,
|
||||
|
@ -37,6 +36,7 @@ use agent::{
|
|||
thread_store::{TextThreadStore, ThreadStore},
|
||||
};
|
||||
use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView};
|
||||
use ai_onboarding::AgentPanelOnboarding;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
|
@ -48,13 +48,12 @@ use fs::Fs;
|
|||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
|
||||
Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla,
|
||||
KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
|
||||
linear_gradient, prelude::*, pulsating_between,
|
||||
KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*,
|
||||
pulsating_between,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use project::{Project, ProjectPath, Worktree};
|
||||
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
|
||||
|
@ -66,9 +65,8 @@ use theme::ThemeSettings;
|
|||
use time::UtcOffset;
|
||||
use ui::utils::WithRemSize;
|
||||
use ui::{
|
||||
Banner, Button, Callout, CheckboxWithLabel, ContextMenu, ElevationIndex, IconPosition,
|
||||
KeyBinding, PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName,
|
||||
prelude::*,
|
||||
Banner, Callout, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu, PopoverMenuHandle,
|
||||
ProgressBar, Tab, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{
|
||||
|
@ -77,7 +75,7 @@ use workspace::{
|
|||
};
|
||||
use zed_actions::{
|
||||
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
|
||||
agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding},
|
||||
agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding, ToggleModelSelector},
|
||||
assistant::{OpenRulesLibrary, ToggleFocus},
|
||||
};
|
||||
use zed_llm_client::{CompletionIntent, UsageLimit};
|
||||
|
@ -188,7 +186,7 @@ pub fn init(cx: &mut App) {
|
|||
window.refresh();
|
||||
})
|
||||
.register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
|
||||
Upsell::set_dismissed(false, cx);
|
||||
OnboardingUpsell::set_dismissed(false, cx);
|
||||
})
|
||||
.register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
|
||||
TrialEndUpsell::set_dismissed(false, cx);
|
||||
|
@ -453,7 +451,7 @@ pub struct AgentPanel {
|
|||
height: Option<Pixels>,
|
||||
zoomed: bool,
|
||||
pending_serialization: Option<Task<Result<()>>>,
|
||||
hide_upsell: bool,
|
||||
onboarding: Entity<AgentPanelOnboarding>,
|
||||
}
|
||||
|
||||
impl AgentPanel {
|
||||
|
@ -555,6 +553,7 @@ impl AgentPanel {
|
|||
let user_store = workspace.app_state().user_store.clone();
|
||||
let project = workspace.project();
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
let client = workspace.client().clone();
|
||||
let workspace = workspace.weak_handle();
|
||||
let weak_self = cx.entity().downgrade();
|
||||
|
||||
|
@ -688,6 +687,17 @@ impl AgentPanel {
|
|||
},
|
||||
);
|
||||
|
||||
let onboarding = cx.new(|cx| {
|
||||
AgentPanelOnboarding::new(
|
||||
user_store.clone(),
|
||||
client,
|
||||
|_window, cx| {
|
||||
OnboardingUpsell::set_dismissed(true, cx);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
Self {
|
||||
active_view,
|
||||
workspace,
|
||||
|
@ -719,7 +729,7 @@ impl AgentPanel {
|
|||
height: None,
|
||||
zoomed: false,
|
||||
pending_serialization: None,
|
||||
hide_upsell: false,
|
||||
onboarding,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2178,191 +2188,78 @@ impl AgentPanel {
|
|||
return false;
|
||||
}
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { thread, .. } => {
|
||||
if thread
|
||||
.read(cx)
|
||||
.thread()
|
||||
.read(cx)
|
||||
.configured_model()
|
||||
.map_or(false, |model| {
|
||||
model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
|
||||
})
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ActiveView::TextThread { .. } => {
|
||||
if LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.default_model()
|
||||
.map_or(false, |model| {
|
||||
model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
|
||||
})
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ActiveView::ExternalAgentThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => return false,
|
||||
}
|
||||
|
||||
let plan = self.user_store.read(cx).current_plan();
|
||||
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
|
||||
|
||||
matches!(plan, Some(Plan::Free)) && has_previous_trial
|
||||
}
|
||||
|
||||
fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
|
||||
fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
|
||||
if OnboardingUpsell::dismissed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { thread, .. } => {
|
||||
let is_using_zed_provider = thread
|
||||
.read(cx)
|
||||
.thread()
|
||||
.read(cx)
|
||||
.configured_model()
|
||||
.map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID);
|
||||
|
||||
if !is_using_zed_provider {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ActiveView::ExternalAgentThread { .. } => {
|
||||
return false;
|
||||
}
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if self.hide_upsell || Upsell::dismissed() {
|
||||
return false;
|
||||
ActiveView::Thread { thread, .. } => thread
|
||||
.read(cx)
|
||||
.thread()
|
||||
.read(cx)
|
||||
.configured_model()
|
||||
.map_or(true, |model| {
|
||||
model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID
|
||||
}),
|
||||
ActiveView::TextThread { .. } => LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.default_model()
|
||||
.map_or(true, |model| {
|
||||
model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID
|
||||
}),
|
||||
ActiveView::ExternalAgentThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => false,
|
||||
}
|
||||
|
||||
let plan = self.user_store.read(cx).current_plan();
|
||||
if matches!(plan, Some(Plan::ZedPro | Plan::ZedProTrial)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
|
||||
if has_previous_trial {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn render_upsell(
|
||||
fn render_onboarding(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<impl IntoElement> {
|
||||
if !self.should_render_upsell(cx) {
|
||||
if !self.should_render_onboarding(cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.user_store.read(cx).account_too_young() {
|
||||
Some(self.render_young_account_upsell(cx).into_any_element())
|
||||
} else {
|
||||
Some(self.render_trial_upsell(cx).into_any_element())
|
||||
}
|
||||
}
|
||||
|
||||
fn render_young_account_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let checkbox = CheckboxWithLabel::new(
|
||||
"dont-show-again",
|
||||
Label::new("Don't show again").color(Color::Muted),
|
||||
ToggleState::Unselected,
|
||||
move |toggle_state, _window, cx| {
|
||||
let toggle_state_bool = toggle_state.selected();
|
||||
|
||||
Upsell::set_dismissed(toggle_state_bool, cx);
|
||||
},
|
||||
);
|
||||
|
||||
let contents = div()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
|
||||
.child(
|
||||
Label::new("Your GitHub account was created less than 30 days ago, so we can't offer you a free trial.")
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
"Use your own API keys, upgrade to Zed Pro or send an email to billing-support@zed.dev.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.px_neg_1()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(h_flex().items_center().gap_1().child(checkbox))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("dismiss-button", "Not Now")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.color(Color::Muted)
|
||||
.on_click({
|
||||
let agent_panel = cx.entity();
|
||||
move |_, _, cx| {
|
||||
agent_panel.update(cx, |this, cx| {
|
||||
this.hide_upsell = true;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("cta-button", "Upgrade to Zed Pro")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
self.render_upsell_container(cx, contents)
|
||||
}
|
||||
|
||||
fn render_trial_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let checkbox = CheckboxWithLabel::new(
|
||||
"dont-show-again",
|
||||
Label::new("Don't show again").color(Color::Muted),
|
||||
ToggleState::Unselected,
|
||||
move |toggle_state, _window, cx| {
|
||||
let toggle_state_bool = toggle_state.selected();
|
||||
|
||||
Upsell::set_dismissed(toggle_state_bool, cx);
|
||||
},
|
||||
);
|
||||
|
||||
let contents = div()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
|
||||
.child(
|
||||
Label::new("Try Zed Pro for free for 14 days - no credit card required.")
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
"Use your own API keys or enable usage-based billing once you hit the cap.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.px_neg_1()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(h_flex().items_center().gap_1().child(checkbox))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("dismiss-button", "Not Now")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.color(Color::Muted)
|
||||
.on_click({
|
||||
let agent_panel = cx.entity();
|
||||
move |_, _, cx| {
|
||||
agent_panel.update(cx, |this, cx| {
|
||||
this.hide_upsell = true;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("cta-button", "Start Trial")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
self.render_upsell_container(cx, contents)
|
||||
Some(div().size_full().child(self.onboarding.clone()))
|
||||
}
|
||||
|
||||
fn render_trial_end_upsell(
|
||||
|
@ -2374,141 +2271,15 @@ impl AgentPanel {
|
|||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
self.render_upsell_container(
|
||||
cx,
|
||||
div()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(
|
||||
Headline::new("Your Zed Pro trial has expired.").size(HeadlineSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new("You've been automatically reset to the free plan.")
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.px_neg_1()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(div())
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("dismiss-button", "Stay on Free")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.color(Color::Muted)
|
||||
.on_click({
|
||||
let agent_panel = cx.entity();
|
||||
move |_, _, cx| {
|
||||
agent_panel.update(cx, |_this, cx| {
|
||||
TrialEndUpsell::set_dismissed(true, cx);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("cta-button", "Upgrade to Zed Pro")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(|_, _, cx| {
|
||||
cx.open_url(&zed_urls::account_url(cx))
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_upsell_container(&self, cx: &mut Context<Self>, content: Div) -> Div {
|
||||
div().p_2().child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.elevation_2(cx)
|
||||
.rounded(px(8.))
|
||||
.bg(cx.theme().colors().background.alpha(0.5))
|
||||
.p(px(3.))
|
||||
.child(
|
||||
div()
|
||||
.gap_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.border_1()
|
||||
.rounded(px(5.))
|
||||
.border_color(cx.theme().colors().text.alpha(0.1))
|
||||
.overflow_hidden()
|
||||
.relative()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.px_4()
|
||||
.py_3()
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right(px(-1.0))
|
||||
.w(px(441.))
|
||||
.h(px(167.))
|
||||
.child(
|
||||
Vector::new(
|
||||
VectorName::Grid,
|
||||
rems_from_px(441.),
|
||||
rems_from_px(167.),
|
||||
)
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.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(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
|
||||
),
|
||||
)
|
||||
// .child(
|
||||
// div()
|
||||
// .absolute()
|
||||
// .top_0()
|
||||
// .right(px(360.))
|
||||
// .size(px(401.))
|
||||
// .overflow_hidden()
|
||||
// .bg(cx.theme().colors().panel_background)
|
||||
// )
|
||||
.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),
|
||||
)),
|
||||
)
|
||||
.child(content),
|
||||
),
|
||||
)
|
||||
Some(EndTrialUpsell::new(Arc::new({
|
||||
let this = cx.entity();
|
||||
move |_, cx| {
|
||||
this.update(cx, |_this, cx| {
|
||||
TrialEndUpsell::set_dismissed(true, cx);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
fn render_thread_empty_state(
|
||||
|
@ -2521,8 +2292,10 @@ impl AgentPanel {
|
|||
.update(cx, |this, cx| this.recent_entries(6, cx));
|
||||
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
|
||||
let configuration_error =
|
||||
model_registry.configuration_error(model_registry.default_model(), cx);
|
||||
|
||||
let no_error = configuration_error.is_none();
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
|
@ -2530,11 +2303,9 @@ impl AgentPanel {
|
|||
.size_full()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.when(recent_history.is_empty(), |this| {
|
||||
let configuration_error_ref = &configuration_error;
|
||||
this.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.max_w_80()
|
||||
.mx_auto()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
|
@ -2542,137 +2313,91 @@ impl AgentPanel {
|
|||
.child(h_flex().child(Headline::new("Welcome to the Agent Panel")))
|
||||
.when(no_error, |parent| {
|
||||
parent
|
||||
.child(h_flex().child(
|
||||
Label::new("Ask and build anything.").color(Color::Muted),
|
||||
))
|
||||
.child(
|
||||
h_flex().child(
|
||||
Label::new("Ask and build anything.")
|
||||
.color(Color::Muted)
|
||||
.mb_2p5(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("new-thread", "Start New Thread")
|
||||
.icon(IconName::Plus)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&NewThread::default(),
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(
|
||||
NewThread::default().boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("context", "Add Context")
|
||||
.icon(IconName::FileCode)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ToggleContextPicker,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(
|
||||
ToggleContextPicker.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("mode", "Switch Model")
|
||||
.icon(IconName::DatabaseZap)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ToggleModelSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(
|
||||
ToggleModelSelector.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("settings", "View Settings")
|
||||
.icon(IconName::Settings)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&OpenConfiguration,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(
|
||||
OpenConfiguration.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.gap_1()
|
||||
.max_w_48()
|
||||
.child(
|
||||
Button::new("context", "Add Context")
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::FileCode)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ToggleContextPicker,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(
|
||||
ToggleContextPicker.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("mode", "Switch Model")
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::DatabaseZap)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ToggleModelSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(
|
||||
ToggleModelSelector.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("settings", "View Settings")
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Settings)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&OpenConfiguration,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(
|
||||
OpenConfiguration.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
.map(|parent| match configuration_error_ref {
|
||||
Some(
|
||||
err @ (ConfigurationError::ModelNotFound
|
||||
| ConfigurationError::ProviderNotAuthenticated(_)
|
||||
| ConfigurationError::NoProvider),
|
||||
) => parent
|
||||
.child(h_flex().child(
|
||||
Label::new(err.to_string()).color(Color::Muted).mb_2p5(),
|
||||
))
|
||||
.child(
|
||||
Button::new("settings", "Configure a Provider")
|
||||
.icon(IconName::Settings)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&OpenConfiguration,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(
|
||||
OpenConfiguration.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
),
|
||||
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
|
||||
parent.children(provider.render_accept_terms(
|
||||
LanguageModelProviderTosView::ThreadFreshStart,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
None => parent,
|
||||
.when_some(configuration_error.as_ref(), |this, err| {
|
||||
this.child(self.render_configuration_error(
|
||||
err,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(!recent_history.is_empty(), |parent| {
|
||||
let focus_handle = focus_handle.clone();
|
||||
let configuration_error_ref = &configuration_error;
|
||||
|
||||
parent
|
||||
.overflow_hidden()
|
||||
.p_1p5()
|
||||
|
@ -2735,49 +2460,55 @@ impl AgentPanel {
|
|||
},
|
||||
)),
|
||||
)
|
||||
.map(|parent| match configuration_error_ref {
|
||||
Some(
|
||||
err @ (ConfigurationError::ModelNotFound
|
||||
| ConfigurationError::ProviderNotAuthenticated(_)
|
||||
| ConfigurationError::NoProvider),
|
||||
) => parent.child(
|
||||
Banner::new()
|
||||
.severity(ui::Severity::Warning)
|
||||
.child(Label::new(err.to_string()).size(LabelSize::Small))
|
||||
.action_slot(
|
||||
Button::new("settings", "Configure Provider")
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&OpenConfiguration,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(
|
||||
OpenConfiguration.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
|
||||
parent.child(Banner::new().severity(ui::Severity::Warning).child(
|
||||
h_flex().w_full().children(provider.render_accept_terms(
|
||||
LanguageModelProviderTosView::ThreadEmptyState,
|
||||
cx,
|
||||
)),
|
||||
))
|
||||
}
|
||||
None => parent,
|
||||
.when_some(configuration_error.as_ref(), |this, err| {
|
||||
this.child(self.render_configuration_error(err, &focus_handle, window, cx))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn render_configuration_error(
|
||||
&self,
|
||||
configuration_error: &ConfigurationError,
|
||||
focus_handle: &FocusHandle,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
match configuration_error {
|
||||
ConfigurationError::ModelNotFound
|
||||
| ConfigurationError::ProviderNotAuthenticated(_)
|
||||
| ConfigurationError::NoProvider => Banner::new()
|
||||
.severity(ui::Severity::Warning)
|
||||
.child(Label::new(configuration_error.to_string()))
|
||||
.action_slot(
|
||||
Button::new("settings", "Configure Provider")
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&OpenConfiguration,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
|
||||
}),
|
||||
),
|
||||
ConfigurationError::ProviderPendingTermsAcceptance(provider) => {
|
||||
Banner::new().severity(ui::Severity::Warning).child(
|
||||
h_flex().w_full().children(
|
||||
provider.render_accept_terms(
|
||||
LanguageModelProviderTosView::ThreadEmptyState,
|
||||
cx,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tool_use_limit_reached(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
|
@ -2910,7 +2641,7 @@ impl AgentPanel {
|
|||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
|
@ -3300,7 +3031,7 @@ impl Render for AgentPanel {
|
|||
}))
|
||||
.on_action(cx.listener(Self::toggle_burn_mode))
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.children(self.render_upsell(window, cx))
|
||||
.children(self.render_onboarding(window, cx))
|
||||
.children(self.render_trial_end_upsell(window, cx))
|
||||
.map(|parent| match &self.active_view {
|
||||
ActiveView::Thread {
|
||||
|
@ -3309,12 +3040,14 @@ impl Render for AgentPanel {
|
|||
..
|
||||
} => parent
|
||||
.relative()
|
||||
.child(if thread.read(cx).is_empty() {
|
||||
self.render_thread_empty_state(window, cx)
|
||||
.into_any_element()
|
||||
} else {
|
||||
thread.clone().into_any_element()
|
||||
})
|
||||
.child(
|
||||
if thread.read(cx).is_empty() && !self.should_render_onboarding(cx) {
|
||||
self.render_thread_empty_state(window, cx)
|
||||
.into_any_element()
|
||||
} else {
|
||||
thread.clone().into_any_element()
|
||||
},
|
||||
)
|
||||
.children(self.render_tool_use_limit_reached(window, cx))
|
||||
.when_some(thread.read(cx).last_error(), |this, last_error| {
|
||||
this.child(
|
||||
|
@ -3352,12 +3085,36 @@ impl Render for AgentPanel {
|
|||
context_editor,
|
||||
buffer_search_bar,
|
||||
..
|
||||
} => parent.child(self.render_prompt_editor(
|
||||
context_editor,
|
||||
buffer_search_bar,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
} => {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let configuration_error =
|
||||
model_registry.configuration_error(model_registry.default_model(), cx);
|
||||
parent
|
||||
.map(|this| {
|
||||
if !self.should_render_onboarding(cx)
|
||||
&& let Some(err) = configuration_error.as_ref()
|
||||
{
|
||||
this.child(
|
||||
div().bg(cx.theme().colors().editor_background).p_2().child(
|
||||
self.render_configuration_error(
|
||||
err,
|
||||
&self.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.child(self.render_prompt_editor(
|
||||
context_editor,
|
||||
buffer_search_bar,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
ActiveView::Configuration => parent.children(self.configuration.clone()),
|
||||
});
|
||||
|
||||
|
@ -3526,9 +3283,9 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
struct Upsell;
|
||||
struct OnboardingUpsell;
|
||||
|
||||
impl Dismissable for Upsell {
|
||||
impl Dismissable for OnboardingUpsell {
|
||||
const KEY: &'static str = "dismissed-trial-upsell";
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ use crate::agent_model_selector::AgentModelSelector;
|
|||
use crate::buffer_codegen::BufferCodegen;
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
use crate::language_model_selector::ToggleModelSelector;
|
||||
use crate::message_editor::{ContextCreasesAddon, extract_message_creases, insert_message_creases};
|
||||
use crate::terminal_codegen::TerminalCodegen;
|
||||
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
|
||||
|
@ -38,6 +37,7 @@ use ui::{
|
|||
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
pub struct PromptEditor<T> {
|
||||
pub editor: Entity<Editor>,
|
||||
|
|
|
@ -3,9 +3,7 @@ use std::{cmp::Reverse, sync::Arc};
|
|||
use collections::{HashSet, IndexMap};
|
||||
use feature_flags::ZedProFeatureFlag;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task, actions,
|
||||
};
|
||||
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
||||
LanguageModelRegistry,
|
||||
|
@ -15,15 +13,6 @@ use picker::{Picker, PickerDelegate};
|
|||
use proto::Plan;
|
||||
use ui::{ListItem, ListItemSpacing, prelude::*};
|
||||
|
||||
actions!(
|
||||
agent,
|
||||
[
|
||||
/// Toggles the language model selector dropdown.
|
||||
#[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])]
|
||||
ToggleModelSelector
|
||||
]
|
||||
);
|
||||
|
||||
const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro";
|
||||
|
||||
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
|
||||
|
|
|
@ -4,7 +4,6 @@ use std::sync::Arc;
|
|||
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
use crate::agent_model_selector::AgentModelSelector;
|
||||
use crate::language_model_selector::ToggleModelSelector;
|
||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||
use crate::ui::{
|
||||
MaxModeTooltip,
|
||||
|
@ -49,6 +48,7 @@ use ui::{
|
|||
use util::ResultExt as _;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::Chat;
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
||||
|
@ -609,7 +609,11 @@ impl MessageEditor {
|
|||
)
|
||||
}
|
||||
|
||||
fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render_follow_toggle(
|
||||
&self,
|
||||
is_model_selected: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let following = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| {
|
||||
|
@ -618,6 +622,7 @@ impl MessageEditor {
|
|||
.unwrap_or(false);
|
||||
|
||||
IconButton::new("follow-agent", IconName::Crosshair)
|
||||
.disabled(is_model_selected)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.toggle_state(following)
|
||||
|
@ -786,7 +791,7 @@ impl MessageEditor {
|
|||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.child(self.render_follow_toggle(cx))
|
||||
.child(self.render_follow_toggle(is_model_selected, cx))
|
||||
.children(self.render_burn_mode_toggle(cx)),
|
||||
)
|
||||
.child(
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
use crate::{
|
||||
burn_mode_tooltip::BurnModeTooltip,
|
||||
language_model_selector::{
|
||||
LanguageModelSelector, ToggleModelSelector, language_model_selector,
|
||||
},
|
||||
language_model_selector::{LanguageModelSelector, language_model_selector},
|
||||
};
|
||||
use agent_settings::{AgentSettings, CompletionMode};
|
||||
use anyhow::Result;
|
||||
|
@ -38,8 +36,7 @@ use language::{
|
|||
language_settings::{SoftWrap, all_language_settings},
|
||||
};
|
||||
use language_model::{
|
||||
ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelProviderTosView,
|
||||
LanguageModelRegistry, Role,
|
||||
ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelRegistry, Role,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::{Picker, popover_menu::PickerPopoverMenu};
|
||||
|
@ -74,6 +71,7 @@ use workspace::{
|
|||
pane,
|
||||
searchable::{SearchEvent, SearchableItem},
|
||||
};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
|
||||
use assistant_context::{
|
||||
|
@ -1895,108 +1893,6 @@ impl TextThreadEditor {
|
|||
.update(cx, |context, cx| context.summarize(true, cx));
|
||||
}
|
||||
|
||||
fn render_notice(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
// This was previously gated behind the `zed-pro` feature flag. Since we
|
||||
// aren't planning to ship that right now, we're just hard-coding this
|
||||
// value to not show the nudge.
|
||||
let nudge = Some(false);
|
||||
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
|
||||
if nudge.map_or(false, |value| value) {
|
||||
Some(
|
||||
h_flex()
|
||||
.p_3()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_3()
|
||||
.child(Icon::new(IconName::ZedAssistant).color(Color::Accent))
|
||||
.child(Label::new("Zed AI is here! Get started by signing in →")),
|
||||
)
|
||||
.child(
|
||||
Button::new("sign-in", "Sign in")
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
let client = this
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| workspace.client().clone())
|
||||
.log_err();
|
||||
|
||||
if let Some(client) = client {
|
||||
cx.spawn(async move |context_editor, cx| {
|
||||
match client.authenticate_and_connect(true, cx).await {
|
||||
util::ConnectionResult::Timeout => {
|
||||
log::error!("Authentication timeout")
|
||||
}
|
||||
util::ConnectionResult::ConnectionReset => {
|
||||
log::error!("Connection reset")
|
||||
}
|
||||
util::ConnectionResult::Result(r) => {
|
||||
if r.log_err().is_some() {
|
||||
context_editor
|
||||
.update(cx, |_, cx| cx.notify())
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
})),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
} else if let Some(configuration_error) =
|
||||
model_registry.configuration_error(model_registry.default_model(), cx)
|
||||
{
|
||||
Some(
|
||||
h_flex()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_3()
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
.child(Label::new(configuration_error.to_string())),
|
||||
)
|
||||
.child(
|
||||
Button::new("open-configuration", "Configure Providers")
|
||||
.size(ButtonSize::Compact)
|
||||
.icon(Some(IconName::SlidersVertical))
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click({
|
||||
let focus_handle = self.focus_handle(cx).clone();
|
||||
move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(
|
||||
&zed_actions::agent::OpenConfiguration,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle(cx).clone();
|
||||
|
||||
|
@ -2128,12 +2024,13 @@ impl TextThreadEditor {
|
|||
.map(|default| default.model);
|
||||
let model_name = match active_model {
|
||||
Some(model) => model.name().0,
|
||||
None => SharedString::from("No model selected"),
|
||||
None => SharedString::from("Select Model"),
|
||||
};
|
||||
|
||||
let active_provider = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
|
||||
let provider_icon = match active_provider {
|
||||
Some(provider) => provider.icon(),
|
||||
None => IconName::Ai,
|
||||
|
@ -2581,20 +2478,7 @@ impl EventEmitter<SearchEvent> for TextThreadEditor {}
|
|||
|
||||
impl Render for TextThreadEditor {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let provider = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
|
||||
let accept_terms = if self.show_accept_terms {
|
||||
provider.as_ref().and_then(|provider| {
|
||||
provider.render_accept_terms(LanguageModelProviderTosView::PromptEditorPopup, cx)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let language_model_selector = self.language_model_selector_menu_handle.clone();
|
||||
let burn_mode_toggle = self.render_burn_mode_toggle(cx);
|
||||
|
||||
v_flex()
|
||||
.key_context("ContextEditor")
|
||||
|
@ -2611,28 +2495,12 @@ impl Render for TextThreadEditor {
|
|||
language_model_selector.toggle(window, cx);
|
||||
})
|
||||
.size_full()
|
||||
.children(self.render_notice(cx))
|
||||
.child(
|
||||
div()
|
||||
.flex_grow()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(self.editor.clone()),
|
||||
)
|
||||
.when_some(accept_terms, |this, element| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.right_3()
|
||||
.bottom_12()
|
||||
.max_w_96()
|
||||
.py_2()
|
||||
.px_3()
|
||||
.elevation_2(cx)
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.occlude()
|
||||
.child(element),
|
||||
)
|
||||
})
|
||||
.children(self.render_last_error(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
|
@ -2649,7 +2517,7 @@ impl Render for TextThreadEditor {
|
|||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(self.render_inject_context_menu(cx))
|
||||
.when_some(burn_mode_toggle, |this, element| this.child(element)),
|
||||
.children(self.render_burn_mode_toggle(cx)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
mod agent_notification;
|
||||
mod burn_mode_tooltip;
|
||||
mod context_pill;
|
||||
mod end_trial_upsell;
|
||||
mod onboarding_modal;
|
||||
pub mod preview;
|
||||
mod upsell;
|
||||
|
@ -8,4 +9,5 @@ mod upsell;
|
|||
pub use agent_notification::*;
|
||||
pub use burn_mode_tooltip::*;
|
||||
pub use context_pill::*;
|
||||
pub use end_trial_upsell::*;
|
||||
pub use onboarding_modal::*;
|
||||
|
|
112
crates/agent_ui/src/ui/end_trial_upsell.rs
Normal file
112
crates/agent_ui/src/ui/end_trial_upsell.rs
Normal file
|
@ -0,0 +1,112 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use ai_onboarding::{AgentPanelOnboardingCard, BulletItem};
|
||||
use client::zed_urls;
|
||||
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
||||
use ui::{Divider, List, prelude::*};
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct EndTrialUpsell {
|
||||
dismiss_upsell: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
}
|
||||
|
||||
impl EndTrialUpsell {
|
||||
pub fn new(dismiss_upsell: Arc<dyn Fn(&mut Window, &mut App)>) -> Self {
|
||||
Self { dismiss_upsell }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for EndTrialUpsell {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let pro_section = v_flex()
|
||||
.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")),
|
||||
)
|
||||
.child(
|
||||
Button::new("cta-button", "Upgrade to Zed Pro")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))),
|
||||
);
|
||||
|
||||
let free_section = v_flex()
|
||||
.mt_1p5()
|
||||
.gap_1()
|
||||
.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("dismiss-button", "Stay on Free")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.on_click({
|
||||
let callback = self.dismiss_upsell.clone();
|
||||
move |_, window, cx| callback(window, cx)
|
||||
}),
|
||||
);
|
||||
|
||||
AgentPanelOnboardingCard::new()
|
||||
.child(Headline::new("Your Zed Pro trial has expired."))
|
||||
.child(
|
||||
Label::new("You've been automatically reset to the Free plan.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mb_1(),
|
||||
)
|
||||
.child(pro_section)
|
||||
.child(free_section)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for EndTrialUpsell {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Agent
|
||||
}
|
||||
|
||||
fn sort_name() -> &'static str {
|
||||
"AgentEndTrialUpsell"
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
Some(
|
||||
v_flex()
|
||||
.p_4()
|
||||
.gap_4()
|
||||
.child(EndTrialUpsell {
|
||||
dismiss_upsell: Arc::new(|_, _| {}),
|
||||
})
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
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))
|
||||
}
|
||||
}
|
|
@ -301,6 +301,13 @@ impl Status {
|
|||
matches!(self, Self::Connected { .. })
|
||||
}
|
||||
|
||||
pub fn is_signing_in(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Authenticating | Self::Reauthenticating | Self::Connecting | Self::Reconnecting
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_signed_out(&self) -> bool {
|
||||
matches!(self, Self::SignedOut | Self::UpgradeRequired)
|
||||
}
|
||||
|
|
|
@ -764,6 +764,16 @@ impl UserStore {
|
|||
}
|
||||
|
||||
pub fn current_plan(&self) -> Option<proto::Plan> {
|
||||
#[cfg(debug_assertions)]
|
||||
if let Ok(plan) = std::env::var("ZED_SIMULATE_ZED_PRO_PLAN").as_ref() {
|
||||
return match plan.as_str() {
|
||||
"free" => Some(proto::Plan::Free),
|
||||
"trial" => Some(proto::Plan::ZedProTrial),
|
||||
"pro" => Some(proto::Plan::ZedPro),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
|
||||
self.current_plan
|
||||
}
|
||||
|
||||
|
|
|
@ -17,3 +17,8 @@ fn server_url(cx: &App) -> &str {
|
|||
pub fn account_url(cx: &App) -> String {
|
||||
format!("{server_url}/account", server_url = server_url(cx))
|
||||
}
|
||||
|
||||
/// Returns the URL to the upgrade page on zed.dev.
|
||||
pub fn upgrade_to_zed_pro_url(cx: &App) -> String {
|
||||
format!("{server_url}/account/upgrade", server_url = server_url(cx))
|
||||
}
|
||||
|
|
|
@ -209,8 +209,14 @@ impl Status {
|
|||
matches!(self, Status::Authorized)
|
||||
}
|
||||
|
||||
pub fn is_disabled(&self) -> bool {
|
||||
matches!(self, Status::Disabled)
|
||||
pub fn is_configured(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Status::Starting { .. }
|
||||
| Status::Error(_)
|
||||
| Status::SigningIn { .. }
|
||||
| Status::Authorized
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ actions!(
|
|||
);
|
||||
|
||||
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
|
||||
const PRIVACY_DOCS: &str = "https://zed.dev/docs/ai/privacy-and-security";
|
||||
|
||||
struct CopilotErrorToast;
|
||||
|
||||
|
@ -193,13 +194,13 @@ impl Render for InlineCompletionButton {
|
|||
cx.open_url(activate_url.as_str())
|
||||
})
|
||||
.entry(
|
||||
"Use Copilot",
|
||||
"Use Zed AI",
|
||||
None,
|
||||
move |_, cx| {
|
||||
set_completion_provider(
|
||||
fs.clone(),
|
||||
cx,
|
||||
EditPredictionProvider::Copilot,
|
||||
EditPredictionProvider::Zed,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
@ -239,22 +240,13 @@ impl Render for InlineCompletionButton {
|
|||
IconName::ZedPredictDisabled
|
||||
};
|
||||
|
||||
let current_user_terms_accepted =
|
||||
self.user_store.read(cx).current_user_has_accepted_terms();
|
||||
let has_subscription = self.user_store.read(cx).current_plan().is_some()
|
||||
&& self.user_store.read(cx).subscription_period().is_some();
|
||||
|
||||
if !has_subscription || !current_user_terms_accepted.unwrap_or(false) {
|
||||
let signed_in = current_user_terms_accepted.is_some();
|
||||
let tooltip_meta = if signed_in {
|
||||
if has_subscription {
|
||||
"Read Terms of Service"
|
||||
} else {
|
||||
"Choose a Plan"
|
||||
}
|
||||
} else {
|
||||
"Sign in to use"
|
||||
};
|
||||
if zeta::should_show_upsell_modal(&self.user_store, cx) {
|
||||
let tooltip_meta =
|
||||
match self.user_store.read(cx).current_user_has_accepted_terms() {
|
||||
Some(true) => "Choose a Plan",
|
||||
Some(false) => "Accept the Terms of Service",
|
||||
None => "Sign In",
|
||||
};
|
||||
|
||||
return div().child(
|
||||
IconButton::new("zed-predict-pending-button", zeta_icon)
|
||||
|
@ -403,15 +395,16 @@ impl InlineCompletionButton {
|
|||
) -> Entity<ContextMenu> {
|
||||
let fs = self.fs.clone();
|
||||
ContextMenu::build(window, cx, |menu, _, _| {
|
||||
menu.entry("Sign In", None, copilot::initiate_sign_in)
|
||||
menu.entry("Sign In to Copilot", None, copilot::initiate_sign_in)
|
||||
.entry("Disable Copilot", None, {
|
||||
let fs = fs.clone();
|
||||
move |_window, cx| hide_copilot(fs.clone(), cx)
|
||||
})
|
||||
.entry("Use Supermaven", None, {
|
||||
.separator()
|
||||
.entry("Use Zed AI", None, {
|
||||
let fs = fs.clone();
|
||||
move |_window, cx| {
|
||||
set_completion_provider(fs.clone(), cx, EditPredictionProvider::Supermaven)
|
||||
set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -518,7 +511,7 @@ impl InlineCompletionButton {
|
|||
);
|
||||
}
|
||||
|
||||
menu = menu.separator().header("Privacy Settings");
|
||||
menu = menu.separator().header("Privacy");
|
||||
if let Some(provider) = &self.edit_prediction_provider {
|
||||
let data_collection = provider.data_collection_state(cx);
|
||||
if data_collection.is_supported() {
|
||||
|
@ -569,13 +562,15 @@ impl InlineCompletionButton {
|
|||
.child(
|
||||
Label::new(indoc!{
|
||||
"Help us improve our open dataset model by sharing data from open source repositories. \
|
||||
Zed must detect a license file in your repo for this setting to take effect."
|
||||
Zed must detect a license file in your repo for this setting to take effect. \
|
||||
Files with sensitive data and secrets are excluded by default."
|
||||
})
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.items_start()
|
||||
.pt_2()
|
||||
.pr_1()
|
||||
.flex_1()
|
||||
.gap_1p5()
|
||||
.border_t_1()
|
||||
|
@ -635,6 +630,13 @@ impl InlineCompletionButton {
|
|||
.detach_and_log_err(cx);
|
||||
}
|
||||
}),
|
||||
).item(
|
||||
ContextMenuEntry::new("View Documentation")
|
||||
.icon(IconName::FileGeneric)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |_, cx| {
|
||||
cx.open_url(PRIVACY_DOCS);
|
||||
})
|
||||
);
|
||||
|
||||
if !self.editor_enabled.unwrap_or(true) {
|
||||
|
@ -672,6 +674,13 @@ impl InlineCompletionButton {
|
|||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, |menu, window, cx| {
|
||||
self.build_language_settings_menu(menu, window, cx)
|
||||
.separator()
|
||||
.entry("Use Zed AI instead", None, {
|
||||
let fs = self.fs.clone();
|
||||
move |_window, cx| {
|
||||
set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed)
|
||||
}
|
||||
})
|
||||
.separator()
|
||||
.link(
|
||||
"Go to Copilot Settings",
|
||||
|
@ -750,44 +759,24 @@ impl InlineCompletionButton {
|
|||
menu = menu
|
||||
.custom_entry(
|
||||
|_window, _cx| {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
.child(
|
||||
Label::new("Your GitHub account is less than 30 days old")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
Label::new("Your GitHub account is less than 30 days old.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Warning)
|
||||
.into_any_element()
|
||||
},
|
||||
|_window, cx| cx.open_url(&zed_urls::account_url(cx)),
|
||||
)
|
||||
.entry(
|
||||
"You need to upgrade to Zed Pro or contact us.",
|
||||
None,
|
||||
|_window, cx| cx.open_url(&zed_urls::account_url(cx)),
|
||||
)
|
||||
.entry("Upgrade to Zed Pro or contact us.", None, |_window, cx| {
|
||||
cx.open_url(&zed_urls::account_url(cx))
|
||||
})
|
||||
.separator();
|
||||
} else if self.user_store.read(cx).has_overdue_invoices() {
|
||||
menu = menu
|
||||
.custom_entry(
|
||||
|_window, _cx| {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
.child(
|
||||
Label::new("You have an outstanding invoice")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
Label::new("You have an outstanding invoice")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Warning)
|
||||
.into_any_element()
|
||||
},
|
||||
|_window, cx| {
|
||||
|
|
|
@ -654,7 +654,7 @@ pub enum LanguageModelProviderTosView {
|
|||
ThreadEmptyState,
|
||||
/// When there are no past interactions in the Agent Panel.
|
||||
ThreadFreshStart,
|
||||
PromptEditorPopup,
|
||||
TextThreadPopup,
|
||||
Configuration,
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ workspace = true
|
|||
path = "src/language_models.rs"
|
||||
|
||||
[dependencies]
|
||||
ai_onboarding.workspace = true
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
aws-config = { workspace = true, features = ["behavior-version-latest"] }
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use ai_onboarding::YoungAccountBanner;
|
||||
use anthropic::AnthropicModelMode;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
@ -500,7 +501,7 @@ fn render_accept_terms(
|
|||
)
|
||||
.child({
|
||||
match view_kind {
|
||||
LanguageModelProviderTosView::PromptEditorPopup => {
|
||||
LanguageModelProviderTosView::TextThreadPopup => {
|
||||
button_container.w_full().justify_end()
|
||||
}
|
||||
LanguageModelProviderTosView::Configuration => {
|
||||
|
@ -1126,6 +1127,7 @@ struct ZedAiConfiguration {
|
|||
subscription_period: Option<(DateTime<Utc>, DateTime<Utc>)>,
|
||||
eligible_for_trial: bool,
|
||||
has_accepted_terms_of_service: bool,
|
||||
account_too_young: bool,
|
||||
accept_terms_of_service_in_progress: bool,
|
||||
accept_terms_of_service_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
|
||||
sign_in_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
|
||||
|
@ -1133,18 +1135,18 @@ struct ZedAiConfiguration {
|
|||
|
||||
impl RenderOnce for ZedAiConfiguration {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
const ZED_PRICING_URL: &str = "https://zed.dev/pricing";
|
||||
let young_account_banner = YoungAccountBanner;
|
||||
|
||||
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 Zed Pro subscription."
|
||||
"You have access to Zed's hosted LLMs through your Pro subscription."
|
||||
}
|
||||
(Some(proto::Plan::ZedProTrial), Some(_)) => {
|
||||
"You have access to Zed's hosted LLMs through your Zed Pro trial."
|
||||
"You have access to Zed's hosted LLMs through your Pro trial."
|
||||
}
|
||||
(Some(proto::Plan::Free), Some(_)) => {
|
||||
"You have basic access to Zed's hosted LLMs through your Zed Free subscription."
|
||||
"You have basic access to Zed's hosted LLMs through the Free plan."
|
||||
}
|
||||
_ => {
|
||||
if self.eligible_for_trial {
|
||||
|
@ -1154,68 +1156,76 @@ impl RenderOnce for ZedAiConfiguration {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
let manage_subscription_buttons = if is_pro {
|
||||
h_flex().child(
|
||||
Button::new("manage_settings", "Manage Subscription")
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
|
||||
)
|
||||
Button::new("manage_settings", "Manage Subscription")
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))
|
||||
.into_any_element()
|
||||
} else if self.plan.is_none() || self.eligible_for_trial {
|
||||
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)))
|
||||
.into_any_element()
|
||||
} else {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("learn_more", "Learn more")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.on_click(|_, _, cx| cx.open_url(ZED_PRICING_URL)),
|
||||
)
|
||||
.child(
|
||||
Button::new(
|
||||
"upgrade",
|
||||
if self.plan.is_none() && self.eligible_for_trial {
|
||||
"Start Trial"
|
||||
} else {
|
||||
"Upgrade"
|
||||
},
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.color(Color::Accent)
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
|
||||
)
|
||||
Button::new("upgrade", "Upgrade to Pro")
|
||||
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)))
|
||||
.into_any_element()
|
||||
};
|
||||
|
||||
if self.is_connected {
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.when(!self.has_accepted_terms_of_service, |this| {
|
||||
this.child(render_accept_terms(
|
||||
LanguageModelProviderTosView::Configuration,
|
||||
self.accept_terms_of_service_in_progress,
|
||||
{
|
||||
let callback = self.accept_terms_of_service_callback.clone();
|
||||
move |window, cx| (callback)(window, cx)
|
||||
},
|
||||
))
|
||||
})
|
||||
.when(self.has_accepted_terms_of_service, |this| {
|
||||
this.child(subscription_text)
|
||||
.child(manage_subscription_buttons)
|
||||
})
|
||||
} else {
|
||||
v_flex()
|
||||
if !self.is_connected {
|
||||
return v_flex()
|
||||
.gap_2()
|
||||
.child(Label::new("Use Zed AI to access hosted language models."))
|
||||
.child(Label::new("Sign in to have access to Zed's complete agentic experience with hosted models."))
|
||||
.child(
|
||||
Button::new("sign_in", "Sign In")
|
||||
Button::new("sign_in", "Sign In to use Zed AI")
|
||||
.icon_color(Color::Muted)
|
||||
.icon(IconName::Github)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.full_width()
|
||||
.on_click({
|
||||
let callback = self.sign_in_callback.clone();
|
||||
move |_, window, cx| (callback)(window, cx)
|
||||
}),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.when(!self.has_accepted_terms_of_service, |this| {
|
||||
this.child(render_accept_terms(
|
||||
LanguageModelProviderTosView::Configuration,
|
||||
self.accept_terms_of_service_in_progress,
|
||||
{
|
||||
let callback = self.accept_terms_of_service_callback.clone();
|
||||
move |window, cx| (callback)(window, cx)
|
||||
},
|
||||
))
|
||||
})
|
||||
.map(|this| {
|
||||
if self.has_accepted_terms_of_service && self.account_too_young {
|
||||
this.child(young_account_banner).child(
|
||||
Button::new("upgrade", "Upgrade to Pro")
|
||||
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(|_, _, cx| {
|
||||
cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
|
||||
}),
|
||||
)
|
||||
} else if self.has_accepted_terms_of_service {
|
||||
this.text_sm()
|
||||
.child(subscription_text)
|
||||
.child(manage_subscription_buttons)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.when(self.has_accepted_terms_of_service, |this| this)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1264,6 +1274,7 @@ impl Render for ConfigurationView {
|
|||
subscription_period: user_store.subscription_period(),
|
||||
eligible_for_trial: user_store.trial_started_at().is_none(),
|
||||
has_accepted_terms_of_service: state.has_accepted_terms_of_service(cx),
|
||||
account_too_young: user_store.account_too_young(),
|
||||
accept_terms_of_service_in_progress: state.accept_terms_of_service_task.is_some(),
|
||||
accept_terms_of_service_callback: self.accept_terms_of_service_callback.clone(),
|
||||
sign_in_callback: self.sign_in_callback.clone(),
|
||||
|
@ -1281,6 +1292,7 @@ impl Component for ZedAiConfiguration {
|
|||
is_connected: bool,
|
||||
plan: Option<proto::Plan>,
|
||||
eligible_for_trial: bool,
|
||||
account_too_young: bool,
|
||||
has_accepted_terms_of_service: bool,
|
||||
) -> AnyElement {
|
||||
ZedAiConfiguration {
|
||||
|
@ -1291,6 +1303,7 @@ impl Component for ZedAiConfiguration {
|
|||
.then(|| (Utc::now(), Utc::now() + chrono::Duration::days(7))),
|
||||
eligible_for_trial,
|
||||
has_accepted_terms_of_service,
|
||||
account_too_young,
|
||||
accept_terms_of_service_in_progress: false,
|
||||
accept_terms_of_service_callback: Arc::new(|_, _| {}),
|
||||
sign_in_callback: Arc::new(|_, _| {}),
|
||||
|
@ -1303,30 +1316,33 @@ impl Component for ZedAiConfiguration {
|
|||
.p_4()
|
||||
.gap_4()
|
||||
.children(vec![
|
||||
single_example("Not connected", configuration(false, None, false, true)),
|
||||
single_example(
|
||||
"Not connected",
|
||||
configuration(false, None, false, false, true),
|
||||
),
|
||||
single_example(
|
||||
"Accept Terms of Service",
|
||||
configuration(true, None, true, false),
|
||||
configuration(true, None, true, false, false),
|
||||
),
|
||||
single_example(
|
||||
"No Plan - Not eligible for trial",
|
||||
configuration(true, None, false, true),
|
||||
configuration(true, None, false, false, true),
|
||||
),
|
||||
single_example(
|
||||
"No Plan - Eligible for trial",
|
||||
configuration(true, None, true, true),
|
||||
configuration(true, None, true, false, true),
|
||||
),
|
||||
single_example(
|
||||
"Free Plan",
|
||||
configuration(true, Some(proto::Plan::Free), true, true),
|
||||
configuration(true, Some(proto::Plan::Free), true, false, true),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Trial Plan",
|
||||
configuration(true, Some(proto::Plan::ZedProTrial), true, true),
|
||||
configuration(true, Some(proto::Plan::ZedProTrial), true, false, true),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Plan",
|
||||
configuration(true, Some(proto::Plan::ZedPro), true, true),
|
||||
configuration(true, Some(proto::Plan::ZedPro), true, false, true),
|
||||
),
|
||||
])
|
||||
.into_any_element(),
|
||||
|
|
|
@ -20,7 +20,7 @@ use crate::application_menu::{
|
|||
|
||||
use auto_update::AutoUpdateStatus;
|
||||
use call::ActiveCall;
|
||||
use client::{Client, UserStore};
|
||||
use client::{Client, UserStore, zed_urls};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, Context, Corner, Element, Entity, Focusable, InteractiveElement,
|
||||
IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled,
|
||||
|
@ -656,8 +656,9 @@ impl TitleBar {
|
|||
let user_login = user.github_login.clone();
|
||||
|
||||
let (plan_name, label_color, bg_color) = match plan {
|
||||
None => ("None", Color::Default, free_chip_bg),
|
||||
Some(proto::Plan::Free) => ("Free", Color::Default, free_chip_bg),
|
||||
None | Some(proto::Plan::Free) => {
|
||||
("Free", Color::Default, free_chip_bg)
|
||||
}
|
||||
Some(proto::Plan::ZedProTrial) => {
|
||||
("Pro Trial", Color::Accent, pro_chip_bg)
|
||||
}
|
||||
|
@ -680,7 +681,7 @@ impl TitleBar {
|
|||
.into_any_element()
|
||||
},
|
||||
move |_, cx| {
|
||||
cx.open_url("https://zed.dev/account");
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
},
|
||||
)
|
||||
.separator()
|
||||
|
|
|
@ -126,6 +126,10 @@ pub enum ButtonStyle {
|
|||
/// coloring like an error or success button.
|
||||
Tinted(TintColor),
|
||||
|
||||
/// Usually used as a secondary action that should have more emphasis than
|
||||
/// a fully transparent button.
|
||||
Outlined,
|
||||
|
||||
/// The default button style, used for most buttons. Has a transparent background,
|
||||
/// but has a background color to indicate states like hover and active.
|
||||
#[default]
|
||||
|
@ -180,6 +184,12 @@ impl ButtonStyle {
|
|||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
|
||||
ButtonStyle::Outlined => ButtonLikeStyles {
|
||||
background: element_bg_from_elevation(elevation, cx),
|
||||
border_color: cx.theme().colors().border_variant,
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::Subtle => ButtonLikeStyles {
|
||||
background: cx.theme().colors().ghost_element_background,
|
||||
border_color: transparent_black(),
|
||||
|
@ -219,6 +229,12 @@ impl ButtonStyle {
|
|||
styles.background = theme.darken(styles.background, 0.05, 0.2);
|
||||
styles
|
||||
}
|
||||
ButtonStyle::Outlined => ButtonLikeStyles {
|
||||
background: cx.theme().colors().ghost_element_hover,
|
||||
border_color: cx.theme().colors().border,
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::Subtle => ButtonLikeStyles {
|
||||
background: cx.theme().colors().ghost_element_hover,
|
||||
border_color: transparent_black(),
|
||||
|
@ -251,6 +267,12 @@ impl ButtonStyle {
|
|||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::Outlined => ButtonLikeStyles {
|
||||
background: cx.theme().colors().element_active,
|
||||
border_color: cx.theme().colors().border_variant,
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::Transparent => ButtonLikeStyles {
|
||||
background: transparent_black(),
|
||||
border_color: transparent_black(),
|
||||
|
@ -278,6 +300,12 @@ impl ButtonStyle {
|
|||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::Outlined => ButtonLikeStyles {
|
||||
background: cx.theme().colors().ghost_element_background,
|
||||
border_color: cx.theme().colors().border,
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::Transparent => ButtonLikeStyles {
|
||||
background: transparent_black(),
|
||||
border_color: cx.theme().colors().border_focused,
|
||||
|
@ -308,6 +336,12 @@ impl ButtonStyle {
|
|||
label_color: Color::Disabled.color(cx),
|
||||
icon_color: Color::Disabled.color(cx),
|
||||
},
|
||||
ButtonStyle::Outlined => ButtonLikeStyles {
|
||||
background: cx.theme().colors().element_disabled,
|
||||
border_color: cx.theme().colors().border_disabled,
|
||||
label_color: Color::Default.color(cx),
|
||||
icon_color: Color::Default.color(cx),
|
||||
},
|
||||
ButtonStyle::Transparent => ButtonLikeStyles {
|
||||
background: transparent_black(),
|
||||
border_color: transparent_black(),
|
||||
|
@ -525,6 +559,13 @@ impl RenderOnce for ButtonLike {
|
|||
.when_some(self.width, |this, width| {
|
||||
this.w(width).justify_center().text_center()
|
||||
})
|
||||
.when(
|
||||
match self.style {
|
||||
ButtonStyle::Outlined => true,
|
||||
_ => false,
|
||||
},
|
||||
|this| this.border_1(),
|
||||
)
|
||||
.when_some(self.rounding, |this, rounding| match rounding {
|
||||
ButtonLikeRounding::All => this.rounded_sm(),
|
||||
ButtonLikeRounding::Left => this.rounded_l_sm(),
|
||||
|
@ -538,6 +579,7 @@ impl RenderOnce for ButtonLike {
|
|||
}
|
||||
ButtonSize::None => this,
|
||||
})
|
||||
.border_color(style.enabled(self.layer, cx).border_color)
|
||||
.bg(style.enabled(self.layer, cx).background)
|
||||
.when(self.disabled, |this| {
|
||||
if self.cursor_style == CursorStyle::PointingHand {
|
||||
|
|
|
@ -277,7 +277,10 @@ pub mod agent {
|
|||
/// Displays the previous message in the history.
|
||||
PreviousHistoryMessage,
|
||||
/// Displays the next message in the history.
|
||||
NextHistoryMessage
|
||||
NextHistoryMessage,
|
||||
/// Toggles the language model selector dropdown.
|
||||
#[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])]
|
||||
ToggleModelSelector
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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