agent: Add component preview for Zed AI configuration (#33704)
As we are in the process of improving our Onboarding UX for Zed AI, I added component previews for the Zed AI Configuration section. This should make it easier to inspect the different states we can run into. <img width="1198" alt="image" src="https://github.com/user-attachments/assets/eb774f27-9091-450d-bfae-c688d533c25e" /> Release Notes: - N/A
This commit is contained in:
parent
2caa19214b
commit
782fbfad90
5 changed files with 184 additions and 71 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -8946,8 +8946,10 @@ dependencies = [
|
|||
"aws-credential-types",
|
||||
"aws_http_client",
|
||||
"bedrock",
|
||||
"chrono",
|
||||
"client",
|
||||
"collections",
|
||||
"component",
|
||||
"copilot",
|
||||
"credentials_provider",
|
||||
"deepseek",
|
||||
|
|
|
@ -2598,7 +2598,7 @@ impl AgentPanel {
|
|||
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
|
||||
parent.child(Banner::new().severity(ui::Severity::Warning).child(
|
||||
h_flex().w_full().children(provider.render_accept_terms(
|
||||
LanguageModelProviderTosView::ThreadtEmptyState,
|
||||
LanguageModelProviderTosView::ThreadEmptyState,
|
||||
cx,
|
||||
)),
|
||||
))
|
||||
|
|
|
@ -602,7 +602,7 @@ pub trait LanguageModelProvider: 'static {
|
|||
#[derive(PartialEq, Eq)]
|
||||
pub enum LanguageModelProviderTosView {
|
||||
/// When there are some past interactions in the Agent Panel.
|
||||
ThreadtEmptyState,
|
||||
ThreadEmptyState,
|
||||
/// When there are no past interactions in the Agent Panel.
|
||||
ThreadFreshStart,
|
||||
PromptEditorPopup,
|
||||
|
|
|
@ -20,8 +20,10 @@ aws-credential-types = { workspace = true, features = [
|
|||
] }
|
||||
aws_http_client.workspace = true
|
||||
bedrock.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
credentials_provider.workspace = true
|
||||
copilot.workspace = true
|
||||
deepseek = { workspace = true, features = ["schemars"] }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use anthropic::AnthropicModelMode;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use client::{Client, ModelRequestUsage, UserStore, zed_urls};
|
||||
use futures::{
|
||||
AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream,
|
||||
|
@ -117,7 +118,7 @@ pub struct State {
|
|||
llm_api_token: LlmApiToken,
|
||||
user_store: Entity<UserStore>,
|
||||
status: client::Status,
|
||||
accept_terms: Option<Task<Result<()>>>,
|
||||
accept_terms_of_service_task: Option<Task<Result<()>>>,
|
||||
models: Vec<Arc<zed_llm_client::LanguageModel>>,
|
||||
default_model: Option<Arc<zed_llm_client::LanguageModel>>,
|
||||
default_fast_model: Option<Arc<zed_llm_client::LanguageModel>>,
|
||||
|
@ -141,7 +142,7 @@ impl State {
|
|||
llm_api_token: LlmApiToken::default(),
|
||||
user_store,
|
||||
status,
|
||||
accept_terms: None,
|
||||
accept_terms_of_service_task: None,
|
||||
models: Vec::new(),
|
||||
default_model: None,
|
||||
default_fast_model: None,
|
||||
|
@ -250,12 +251,12 @@ impl State {
|
|||
|
||||
fn accept_terms_of_service(&mut self, cx: &mut Context<Self>) {
|
||||
let user_store = self.user_store.clone();
|
||||
self.accept_terms = Some(cx.spawn(async move |this, cx| {
|
||||
self.accept_terms_of_service_task = Some(cx.spawn(async move |this, cx| {
|
||||
let _ = user_store
|
||||
.update(cx, |store, cx| store.accept_terms_of_service(cx))?
|
||||
.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.accept_terms = None;
|
||||
this.accept_terms_of_service_task = None;
|
||||
cx.notify()
|
||||
})
|
||||
}));
|
||||
|
@ -403,10 +404,8 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
|||
}
|
||||
|
||||
fn configuration_view(&self, _: &mut Window, cx: &mut App) -> AnyView {
|
||||
cx.new(|_| ConfigurationView {
|
||||
state: self.state.clone(),
|
||||
})
|
||||
.into()
|
||||
cx.new(|_| ConfigurationView::new(self.state.clone()))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn must_accept_terms(&self, cx: &App) -> bool {
|
||||
|
@ -418,7 +417,19 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
|||
view: LanguageModelProviderTosView,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
render_accept_terms(self.state.clone(), view, cx)
|
||||
let state = self.state.read(cx);
|
||||
if state.has_accepted_terms_of_service(cx) {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
render_accept_terms(view, state.accept_terms_of_service_task.is_some(), {
|
||||
let state = self.state.clone();
|
||||
move |_window, cx| {
|
||||
state.update(cx, |state, cx| state.accept_terms_of_service(cx));
|
||||
}
|
||||
})
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {
|
||||
|
@ -427,18 +438,12 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
|||
}
|
||||
|
||||
fn render_accept_terms(
|
||||
state: Entity<State>,
|
||||
view_kind: LanguageModelProviderTosView,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
if state.read(cx).has_accepted_terms_of_service(cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let accept_terms_disabled = state.read(cx).accept_terms.is_some();
|
||||
|
||||
accept_terms_of_service_in_progress: bool,
|
||||
accept_terms_callback: impl Fn(&mut Window, &mut App) + 'static,
|
||||
) -> impl IntoElement {
|
||||
let thread_fresh_start = matches!(view_kind, LanguageModelProviderTosView::ThreadFreshStart);
|
||||
let thread_empty_state = matches!(view_kind, LanguageModelProviderTosView::ThreadtEmptyState);
|
||||
let thread_empty_state = matches!(view_kind, LanguageModelProviderTosView::ThreadEmptyState);
|
||||
|
||||
let terms_button = Button::new("terms_of_service", "Terms of Service")
|
||||
.style(ButtonStyle::Subtle)
|
||||
|
@ -461,18 +466,11 @@ fn render_accept_terms(
|
|||
this.style(ButtonStyle::Tinted(TintColor::Warning))
|
||||
.label_size(LabelSize::Small)
|
||||
})
|
||||
.disabled(accept_terms_disabled)
|
||||
.on_click({
|
||||
let state = state.downgrade();
|
||||
move |_, _window, cx| {
|
||||
state
|
||||
.update(cx, |state, cx| state.accept_terms_of_service(cx))
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
.disabled(accept_terms_of_service_in_progress)
|
||||
.on_click(move |_, window, cx| (accept_terms_callback)(window, cx)),
|
||||
);
|
||||
|
||||
let form = if thread_empty_state {
|
||||
if thread_empty_state {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.flex_wrap()
|
||||
|
@ -510,12 +508,10 @@ fn render_accept_terms(
|
|||
LanguageModelProviderTosView::ThreadFreshStart => {
|
||||
button_container.w_full().justify_center()
|
||||
}
|
||||
LanguageModelProviderTosView::ThreadtEmptyState => div().w_0(),
|
||||
LanguageModelProviderTosView::ThreadEmptyState => div().w_0(),
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
Some(form.into_any())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CloudLanguageModel {
|
||||
|
@ -1060,32 +1056,24 @@ fn response_lines<T: DeserializeOwned>(
|
|||
)
|
||||
}
|
||||
|
||||
struct ConfigurationView {
|
||||
state: gpui::Entity<State>,
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
struct ZedAIConfiguration {
|
||||
is_connected: bool,
|
||||
plan: Option<proto::Plan>,
|
||||
subscription_period: Option<(DateTime<Utc>, DateTime<Utc>)>,
|
||||
eligible_for_trial: bool,
|
||||
has_accepted_terms_of_service: 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>,
|
||||
}
|
||||
|
||||
impl ConfigurationView {
|
||||
fn authenticate(&mut self, cx: &mut Context<Self>) {
|
||||
self.state.update(cx, |state, cx| {
|
||||
state.authenticate(cx).detach_and_log_err(cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigurationView {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
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 is_connected = !self.state.read(cx).is_signed_out();
|
||||
let user_store = self.state.read(cx).user_store.read(cx);
|
||||
let plan = user_store.current_plan();
|
||||
let subscription_period = user_store.subscription_period();
|
||||
let eligible_for_trial = user_store.trial_started_at().is_none();
|
||||
let has_accepted_terms = self.state.read(cx).has_accepted_terms_of_service(cx);
|
||||
|
||||
let is_pro = plan == Some(proto::Plan::ZedPro);
|
||||
let subscription_text = match (plan, subscription_period) {
|
||||
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."
|
||||
}
|
||||
|
@ -1096,7 +1084,7 @@ impl Render for ConfigurationView {
|
|||
"You have basic access to Zed's hosted LLMs through your Zed Free subscription."
|
||||
}
|
||||
_ => {
|
||||
if eligible_for_trial {
|
||||
if self.eligible_for_trial {
|
||||
"Subscribe for access to Zed's hosted LLMs. Start with a 14 day free trial."
|
||||
} else {
|
||||
"Subscribe for access to Zed's hosted LLMs."
|
||||
|
@ -1107,7 +1095,7 @@ impl Render for ConfigurationView {
|
|||
h_flex().child(
|
||||
Button::new("manage_settings", "Manage Subscription")
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.open_url(&zed_urls::account_url(cx)))),
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
|
||||
)
|
||||
} else {
|
||||
h_flex()
|
||||
|
@ -1115,28 +1103,31 @@ impl Render for ConfigurationView {
|
|||
.child(
|
||||
Button::new("learn_more", "Learn more")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.open_url(ZED_PRICING_URL))),
|
||||
.on_click(|_, _, cx| cx.open_url(ZED_PRICING_URL)),
|
||||
)
|
||||
.child(
|
||||
Button::new("upgrade", "Upgrade")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.color(Color::Accent)
|
||||
.on_click(
|
||||
cx.listener(|_, _, _, cx| cx.open_url(&zed_urls::account_url(cx))),
|
||||
),
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
|
||||
)
|
||||
};
|
||||
|
||||
if is_connected {
|
||||
if self.is_connected {
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.children(render_accept_terms(
|
||||
self.state.clone(),
|
||||
LanguageModelProviderTosView::Configuration,
|
||||
cx,
|
||||
))
|
||||
.when(has_accepted_terms, |this| {
|
||||
.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)
|
||||
})
|
||||
|
@ -1149,8 +1140,126 @@ impl Render for ConfigurationView {
|
|||
.icon_color(Color::Muted)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(cx.listener(move |this, _, _, cx| this.authenticate(cx))),
|
||||
.on_click({
|
||||
let callback = self.sign_in_callback.clone();
|
||||
move |_, window, cx| (callback)(window, cx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ConfigurationView {
|
||||
state: Entity<State>,
|
||||
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>,
|
||||
}
|
||||
|
||||
impl ConfigurationView {
|
||||
fn new(state: Entity<State>) -> Self {
|
||||
let accept_terms_of_service_callback = Arc::new({
|
||||
let state = state.clone();
|
||||
move |_window: &mut Window, cx: &mut App| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.accept_terms_of_service(cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let sign_in_callback = Arc::new({
|
||||
let state = state.clone();
|
||||
move |_window: &mut Window, cx: &mut App| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.authenticate(cx).detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
state,
|
||||
accept_terms_of_service_callback,
|
||||
sign_in_callback,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigurationView {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let state = self.state.read(cx);
|
||||
let user_store = state.user_store.read(cx);
|
||||
|
||||
ZedAIConfiguration {
|
||||
is_connected: !state.is_signed_out(),
|
||||
plan: user_store.current_plan(),
|
||||
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),
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ZedAIConfiguration {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Agent
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
fn configuration(
|
||||
is_connected: bool,
|
||||
plan: Option<proto::Plan>,
|
||||
eligible_for_trial: bool,
|
||||
has_accepted_terms_of_service: bool,
|
||||
) -> AnyElement {
|
||||
ZedAIConfiguration {
|
||||
is_connected,
|
||||
plan,
|
||||
subscription_period: plan
|
||||
.is_some()
|
||||
.then(|| (Utc::now(), Utc::now() + chrono::Duration::days(7))),
|
||||
eligible_for_trial,
|
||||
has_accepted_terms_of_service,
|
||||
accept_terms_of_service_in_progress: false,
|
||||
accept_terms_of_service_callback: Arc::new(|_, _| {}),
|
||||
sign_in_callback: Arc::new(|_, _| {}),
|
||||
}
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.p_4()
|
||||
.gap_4()
|
||||
.children(vec![
|
||||
single_example("Not connected", configuration(false, None, false, true)),
|
||||
single_example(
|
||||
"Accept Terms of Service",
|
||||
configuration(true, None, true, false),
|
||||
),
|
||||
single_example(
|
||||
"No Plan - Not eligible for trial",
|
||||
configuration(true, None, false, true),
|
||||
),
|
||||
single_example(
|
||||
"No Plan - Eligible for trial",
|
||||
configuration(true, None, true, true),
|
||||
),
|
||||
single_example(
|
||||
"Free Plan",
|
||||
configuration(true, Some(proto::Plan::Free), true, true),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Trial Plan",
|
||||
configuration(true, Some(proto::Plan::ZedProTrial), true, true),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Plan",
|
||||
configuration(true, Some(proto::Plan::ZedPro), true, true),
|
||||
),
|
||||
])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue