diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 86cc81cb7bc7b89967e69949ced891a7cb845cea..287c76fc6dfea530ce53b48178024ef185b98134 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -20,13 +20,13 @@ use std::pin::Pin; use std::str::FromStr; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; -use ui::{Icon, IconName, List, Tooltip, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; use zed_env_vars::{EnvVar, env_var}; use crate::api_key::ApiKeyState; -use crate::ui::InstructionListItem; +use crate::ui::{ConfiguredApiCard, InstructionListItem}; pub use settings::AnthropicAvailableModel as AvailableModel; @@ -909,9 +909,21 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") + } else { + let api_url = AnthropicLanguageModelProvider::api_url(cx); + if api_url == ANTHROPIC_API_URL { + "API key configured".to_string() + } else { + format!("API key configured for {}", api_url) + } + }; if self.load_credentials_task.is_some() { - div().child(Label::new("Loading credentials...")).into_any() + div() + .child(Label::new("Loading credentials...")) + .into_any_element() } else if self.should_render_editor(cx) { v_flex() .size_full() @@ -941,56 +953,17 @@ impl Render for ConfigurationView { .size(LabelSize::Small) .color(Color::Muted), ) - .into_any() + .into_any_element() } else { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .flex_1() - .min_w_0() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .child(Label::new(if env_var_set { - format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") - } else { - let api_url = AnthropicLanguageModelProvider::api_url(cx); - if api_url == ANTHROPIC_API_URL { - "API key configured".to_string() - } else { - format!("API key configured for {}", api_url) - } - })) - ), - ) - .child( - h_flex() - .flex_shrink_0() - .child( - Button::new("reset-key", "Reset Key") - .label_size(LabelSize::Small) - .icon(Some(IconName::Trash)) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .disabled(env_var_set) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))) - }) - .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), - ), - ) - .into_any() + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .when(env_var_set, |this| { + this.tooltip_label(format!( + "To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable." + )) + }) + .into_any_element() } } } diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index 5699dd8e6693c26bd62f65fb160e0e30a62dda63..14dd575f23952ee732c5d9714d2e091cf50d606f 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -2,7 +2,7 @@ use std::pin::Pin; use std::str::FromStr; use std::sync::Arc; -use crate::ui::InstructionListItem; +use crate::ui::{ConfiguredApiCard, InstructionListItem}; use anyhow::{Context as _, Result, anyhow}; use aws_config::stalled_stream_protection::StalledStreamProtectionConfig; use aws_config::{BehaviorVersion, Region}; @@ -41,7 +41,7 @@ use serde_json::Value; use settings::{BedrockAvailableModel as AvailableModel, Settings, SettingsStore}; use smol::lock::OnceCell; use strum::{EnumIter, IntoEnumIterator, IntoStaticStr}; -use ui::{Icon, IconName, List, Tooltip, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; @@ -1155,47 +1155,37 @@ impl Render for ConfigurationView { return div().child(Label::new("Loading credentials...")).into_any(); } + let configured_label = if env_var_set { + format!( + "Access Key ID is set in {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, Secret Key is set in {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR}, Region is set in {ZED_BEDROCK_REGION_VAR} environment variables." + ) + } else { + match bedrock_method { + Some(BedrockAuthMethod::Automatic) => "You are using automatic credentials.".into(), + Some(BedrockAuthMethod::NamedProfile) => "You are using named profile.".into(), + Some(BedrockAuthMethod::SingleSignOn) => { + "You are using a single sign on profile.".into() + } + None => "You are using static credentials.".into(), + } + }; + + let tooltip_label = if env_var_set { + Some(format!( + "To reset your credentials, unset the {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR}, and {ZED_BEDROCK_REGION_VAR} environment variables." + )) + } else if bedrock_method.is_some() { + Some("You cannot reset credentials as they're being derived, check Zed settings to understand how.".to_string()) + } else { + None + }; + if self.should_render_editor(cx) { - return h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child(Label::new(if env_var_set { - format!("Access Key ID is set in {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, Secret Key is set in {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR}, Region is set in {ZED_BEDROCK_REGION_VAR} environment variables.") - } else { - match bedrock_method { - Some(BedrockAuthMethod::Automatic) => "You are using automatic credentials".into(), - Some(BedrockAuthMethod::NamedProfile) => { - "You are using named profile".into() - }, - Some(BedrockAuthMethod::SingleSignOn) => "You are using a single sign on profile".into(), - None => "You are using static credentials".into(), - } - })), - ) - .child( - Button::new("reset-key", "Reset Key") - .icon(Some(IconName::Trash)) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .disabled(env_var_set || bedrock_method.is_some()) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your credentials, unset the {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR}, and {ZED_BEDROCK_REGION_VAR} environment variables."))) - }) - .when(bedrock_method.is_some(), |this| { - this.tooltip(Tooltip::text("You cannot reset credentials as they're being derived, check Zed settings to understand how")) - }) - .on_click(cx.listener(|this, _, window, cx| this.reset_credentials(window, cx))), - ) - .into_any(); + return ConfiguredApiCard::new(configured_label) + .disabled(env_var_set || bedrock_method.is_some()) + .on_click(cx.listener(|this, _, window, cx| this.reset_credentials(window, cx))) + .when_some(tooltip_label, |this, label| this.tooltip_label(label)) + .into_any_element(); } v_flex() @@ -1241,7 +1231,7 @@ impl Render for ConfigurationView { } impl ConfigurationView { - fn render_static_credentials_ui(&self) -> AnyElement { + fn render_static_credentials_ui(&self) -> impl IntoElement { v_flex() .my_2() .gap_1p5() @@ -1278,6 +1268,5 @@ impl ConfigurationView { .child(self.secret_access_key_editor.clone()) .child(self.session_token_editor.clone()) .child(self.region_editor.clone()) - .into_any_element() } } diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index 6c665a0c1f06aa44e2b86f96517f7998fc02f4d3..0d95120322a592f1732aa53b3470108ccde76473 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -29,6 +29,8 @@ use settings::SettingsStore; use ui::{CommonAnimationExt, prelude::*}; use util::debug_panic; +use crate::ui::ConfiguredApiCard; + const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("copilot_chat"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("GitHub Copilot Chat"); @@ -1326,27 +1328,12 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { if self.state.read(cx).is_authenticated(cx) { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child(Label::new("Authorized")), - ) - .child( - Button::new("sign_out", "Sign Out") - .label_size(LabelSize::Small) - .on_click(|_, window, cx| { - window.dispatch_action(copilot::SignOut.boxed_clone(), cx); - }), - ) + ConfiguredApiCard::new("Authorized") + .button_label("Sign Out") + .on_click(|_, window, cx| { + window.dispatch_action(copilot::SignOut.boxed_clone(), cx); + }) + .into_any_element() } else { let loading_icon = Icon::new(IconName::ArrowCircle).with_rotate_animation(4); @@ -1357,37 +1344,49 @@ impl Render for ConfigurationView { Status::Starting { task: _ } => h_flex() .gap_2() .child(loading_icon) - .child(Label::new("Starting Copilot…")), + .child(Label::new("Starting Copilot…")) + .into_any_element(), Status::SigningIn { prompt: _ } | Status::SignedOut { awaiting_signing_in: true, } => h_flex() .gap_2() .child(loading_icon) - .child(Label::new("Signing into Copilot…")), + .child(Label::new("Signing into Copilot…")) + .into_any_element(), Status::Error(_) => { const LABEL: &str = "Copilot had issues starting. Please try restarting it. If the issue persists, try reinstalling Copilot."; v_flex() .gap_6() .child(Label::new(LABEL)) .child(svg().size_8().path(IconName::CopilotError.path())) + .into_any_element() } _ => { const LABEL: &str = "To use Zed's agent with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription."; - v_flex().gap_2().child(Label::new(LABEL)).child( - Button::new("sign_in", "Sign in to use GitHub Copilot") - .full_width() - .style(ButtonStyle::Outlined) - .icon_color(Color::Muted) - .icon(IconName::Github) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .on_click(|_, window, cx| copilot::initiate_sign_in(window, cx)), - ) + v_flex() + .gap_2() + .child(Label::new(LABEL)) + .child( + Button::new("sign_in", "Sign in to use GitHub Copilot") + .full_width() + .style(ButtonStyle::Outlined) + .icon_color(Color::Muted) + .icon(IconName::Github) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .on_click(|_, window, cx| { + copilot::initiate_sign_in(window, cx) + }), + ) + .into_any_element() } }, - None => v_flex().gap_6().child(Label::new(ERROR_LABEL)), + None => v_flex() + .gap_6() + .child(Label::new(ERROR_LABEL)) + .into_any_element(), } } } diff --git a/crates/language_models/src/provider/deepseek.rs b/crates/language_models/src/provider/deepseek.rs index 103d068d671acf331fbba73072e3edf2fcc10411..1d573fd964d0f183393bb766c492566f622a4901 100644 --- a/crates/language_models/src/provider/deepseek.rs +++ b/crates/language_models/src/provider/deepseek.rs @@ -19,11 +19,12 @@ use std::pin::Pin; use std::str::FromStr; use std::sync::{Arc, LazyLock}; -use ui::{Icon, IconName, List, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; use zed_env_vars::{EnvVar, env_var}; +use crate::ui::ConfiguredApiCard; use crate::{api_key::ApiKeyState, ui::InstructionListItem}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("deepseek"); @@ -601,9 +602,21 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") + } else { + let api_url = DeepSeekLanguageModelProvider::api_url(cx); + if api_url == DEEPSEEK_API_URL { + "API key configured".to_string() + } else { + format!("API key configured for {}", api_url) + } + }; if self.load_credentials_task.is_some() { - div().child(Label::new("Loading credentials...")).into_any() + div() + .child(Label::new("Loading credentials...")) + .into_any_element() } else if self.should_render_editor(cx) { v_flex() .size_full() @@ -628,51 +641,12 @@ impl Render for ConfigurationView { .size(LabelSize::Small) .color(Color::Muted), ) - .into_any() + .into_any_element() } else { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .flex_1() - .min_w_0() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child(div().w_full().overflow_x_hidden().text_ellipsis().child( - Label::new(if env_var_set { - format!( - "API key set in {API_KEY_ENV_VAR_NAME} environment variable" - ) - } else { - let api_url = DeepSeekLanguageModelProvider::api_url(cx); - if api_url == DEEPSEEK_API_URL { - "API key configured".to_string() - } else { - format!("API key configured for {}", api_url) - } - }), - )), - ) - .child( - h_flex().flex_shrink_0().child( - Button::new("reset-key", "Reset Key") - .label_size(LabelSize::Small) - .icon(Some(IconName::Trash)) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .disabled(env_var_set) - .on_click( - cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)), - ), - ), - ) - .into_any() + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .into_any_element() } } } diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index a9c97ca939ac55c621bf38b62736af91ab2211ee..e33b118e30fca60e147bd2f311e844626da9b368 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -28,14 +28,14 @@ use std::sync::{ atomic::{self, AtomicU64}, }; use strum::IntoEnumIterator; -use ui::{Icon, IconName, List, Tooltip, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; use zed_env_vars::EnvVar; use crate::api_key::ApiKey; use crate::api_key::ApiKeyState; -use crate::ui::InstructionListItem; +use crate::ui::{ConfiguredApiCard, InstructionListItem}; const PROVIDER_ID: LanguageModelProviderId = language_model::GOOGLE_PROVIDER_ID; const PROVIDER_NAME: LanguageModelProviderName = language_model::GOOGLE_PROVIDER_NAME; @@ -835,9 +835,24 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!( + "API key set in {} environment variable", + API_KEY_ENV_VAR.name + ) + } else { + let api_url = GoogleLanguageModelProvider::api_url(cx); + if api_url == google_ai::API_URL { + "API key configured".to_string() + } else { + format!("API key configured for {}", api_url) + } + }; if self.load_credentials_task.is_some() { - div().child(Label::new("Loading credentials...")).into_any() + div() + .child(Label::new("Loading credentials...")) + .into_any_element() } else if self.should_render_editor(cx) { v_flex() .size_full() @@ -864,58 +879,15 @@ impl Render for ConfigurationView { ) .size(LabelSize::Small).color(Color::Muted), ) - .into_any() + .into_any_element() } else { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .flex_1() - .min_w_0() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .child(Label::new( - if env_var_set { - format!("API key set in {} environment variable", API_KEY_ENV_VAR.name) - } else { - let api_url = GoogleLanguageModelProvider::api_url(cx); - if api_url == google_ai::API_URL { - "API key configured".to_string() - } else { - format!("API key configured for {}", api_url) - } - } - )) - ), - ) - .child( - h_flex() - .flex_shrink_0() - .child( - Button::new("reset-key", "Reset Key") - .label_size(LabelSize::Small) - .icon(Some(IconName::Trash)) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .disabled(env_var_set) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your API key, make sure {GEMINI_API_KEY_VAR_NAME} and {GOOGLE_AI_API_KEY_VAR_NAME} environment variables are unset."))) - }) - .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), - ), - ) - .into_any() + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .when(env_var_set, |this| { + this.tooltip_label(format!("To reset your API key, make sure {GEMINI_API_KEY_VAR_NAME} and {GOOGLE_AI_API_KEY_VAR_NAME} environment variables are unset.")) + }) + .into_any_element() } } } diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index e0bfa5d8eb66ec4ec390bc534ceebe0663610197..2d30dfca21d8cbc4fd1be3575801919148f705b3 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -19,11 +19,12 @@ use std::pin::Pin; use std::str::FromStr; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; -use ui::{Icon, IconName, List, Tooltip, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; use zed_env_vars::{EnvVar, env_var}; +use crate::ui::ConfiguredApiCard; use crate::{api_key::ApiKeyState, ui::InstructionListItem}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("mistral"); @@ -883,6 +884,12 @@ impl ConfigurationView { let key_state = &self.state.read(cx).codestral_api_key_state; let should_show_editor = !key_state.has_key(); let env_var_set = key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable") + } else { + "Codestral API key configured".to_string() + }; + if should_show_editor { v_flex() .id("codestral") @@ -910,42 +917,19 @@ impl ConfigurationView { .size(LabelSize::Small).color(Color::Muted), ).into_any() } else { - h_flex() - .id("codestral") - .mt_2() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child(Label::new(if env_var_set { - format!("API key set in {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable") - } else { - "Codestral API key configured".to_string() - })), + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .when(env_var_set, |this| { + this.tooltip_label(format!( + "To reset your API key, \ + unset the {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable." + )) + }) + .on_click( + cx.listener(|this, _, window, cx| this.reset_codestral_api_key(window, cx)), ) - .child( - Button::new("reset-key", "Reset Key") - .label_size(LabelSize::Small) - .icon(Some(IconName::Trash)) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .disabled(env_var_set) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!( - "To reset your API key, \ - unset the {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable." - ))) - }) - .on_click( - cx.listener(|this, _, window, cx| this.reset_codestral_api_key(window, cx)), - ), - ).into_any() + .into_any_element() } } } @@ -953,6 +937,16 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") + } else { + let api_url = MistralLanguageModelProvider::api_url(cx); + if api_url == MISTRAL_API_URL { + "API key configured".to_string() + } else { + format!("API key configured for {}", api_url) + } + }; if self.load_credentials_task.is_some() { div().child(Label::new("Loading credentials...")).into_any() @@ -987,68 +981,17 @@ impl Render for ConfigurationView { } else { v_flex() .size_full() + .gap_1() .child( - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .flex_1() - .min_w_0() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .child( - Label::new( - if env_var_set { - format!( - "API key set in {API_KEY_ENV_VAR_NAME} environment variable" - ) - } else { - let api_url = MistralLanguageModelProvider::api_url(cx); - if api_url == MISTRAL_API_URL { - "API key configured".to_string() - } else { - format!( - "API key configured for {}", - api_url - ) - } - } - ) - ), - ), - ) - .child( - h_flex() - .flex_shrink_0() - .child( - Button::new("reset-key", "Reset Key") - .label_size(LabelSize::Small) - .icon(Some(IconName::Trash)) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .disabled(env_var_set) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!( - "To reset your API key, \ - unset the {API_KEY_ENV_VAR_NAME} environment variable." - ))) - }) - .on_click(cx.listener(|this, _, window, cx| { - this.reset_api_key(window, cx) - })), - ), - ), + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .when(env_var_set, |this| { + this.tooltip_label(format!( + "To reset your API key, \ + unset the {API_KEY_ENV_VAR_NAME} environment variable." + )) + }), ) .child(self.render_codestral_api_key_editor(cx)) .into_any() diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index 6341baa6f36db36a180d14c957b49dadd901e9a0..a0aada7d1a7b557e1e5aa07f19dd3e38492fc972 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -28,7 +28,7 @@ use zed_env_vars::{EnvVar, env_var}; use crate::AllLanguageModelSettings; use crate::api_key::ApiKeyState; -use crate::ui::InstructionListItem; +use crate::ui::{ConfiguredApiCard, InstructionListItem}; const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download"; const OLLAMA_LIBRARY_URL: &str = "https://ollama.com/library"; @@ -749,9 +749,14 @@ impl ConfigurationView { )) } - fn render_api_key_editor(&self, cx: &Context) -> Div { + fn render_api_key_editor(&self, cx: &Context) -> impl IntoElement { let state = self.state.read(cx); let env_var_set = state.api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable.") + } else { + "API key configured".to_string() + }; if !state.api_key_state.has_key() { v_flex() @@ -764,40 +769,15 @@ impl ConfigurationView { .size(LabelSize::Small) .color(Color::Muted), ) + .into_any_element() } else { - h_flex() - .p_3() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().elevated_surface_background) - .child( - h_flex() - .gap_2() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child( - Label::new( - if env_var_set { - format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable.") - } else { - "API key configured".to_string() - } - ) - ) - ) - .child( - Button::new("reset-api-key", "Reset API Key") - .label_size(LabelSize::Small) - .icon(IconName::Undo) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .layer(ElevationIndex::ModalSurface) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))) - }) - .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), - ) + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .when(env_var_set, |this| { + this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")) + }) + .into_any_element() } } @@ -909,7 +889,7 @@ impl Render for ConfigurationView { ) .child( IconButton::new("refresh-models", IconName::RotateCcw) - .tooltip(Tooltip::text("Refresh models")) + .tooltip(Tooltip::text("Refresh Models")) .on_click(cx.listener(|this, _, _, cx| { this.state.update(cx, |state, _| { state.fetched_models.clear(); diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index aa925a9b582adae5c1ade0b9cdce6765e8614cb6..cabd78c35be58667fd799fe34de07e1d1bfa5808 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -20,11 +20,12 @@ use std::pin::Pin; use std::str::FromStr as _; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; -use ui::{ElevationIndex, List, Tooltip, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; use zed_env_vars::{EnvVar, env_var}; +use crate::ui::ConfiguredApiCard; use crate::{api_key::ApiKeyState, ui::InstructionListItem}; const PROVIDER_ID: LanguageModelProviderId = language_model::OPEN_AI_PROVIDER_ID; @@ -762,6 +763,16 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") + } else { + let api_url = OpenAiLanguageModelProvider::api_url(cx); + if api_url == OPEN_AI_API_URL { + "API key configured".to_string() + } else { + format!("API key configured for {}", api_url) + } + }; let api_key_section = if self.should_render_editor(cx) { v_flex() @@ -795,58 +806,15 @@ impl Render for ConfigurationView { ) .size(LabelSize::Small).color(Color::Muted), ) - .into_any() + .into_any_element() } else { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .flex_1() - .min_w_0() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .child(Label::new( - if env_var_set { - format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") - } else { - let api_url = OpenAiLanguageModelProvider::api_url(cx); - if api_url == OPEN_AI_API_URL { - "API key configured".to_string() - } else { - format!("API key configured for {}", api_url) - } - } - )) - ), - ) - .child( - h_flex() - .flex_shrink_0() - .child( - Button::new("reset-api-key", "Reset API Key") - .label_size(LabelSize::Small) - .icon(IconName::Undo) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .layer(ElevationIndex::ModalSurface) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))) - }) - .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), - ), - ) - .into_any() + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .when(env_var_set, |this| { + this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")) + }) + .into_any_element() }; let compatible_api_section = h_flex() diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 0cc3711489ab25b80c9e995558f18917fbfad343..6326968a916a7d6a21811ee22c328564e1ec4682 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -17,11 +17,12 @@ use settings::{OpenRouterAvailableModel as AvailableModel, Settings, SettingsSto use std::pin::Pin; use std::str::FromStr as _; use std::sync::{Arc, LazyLock}; -use ui::{Icon, IconName, List, Tooltip, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; use zed_env_vars::{EnvVar, env_var}; +use crate::ui::ConfiguredApiCard; use crate::{api_key::ApiKeyState, ui::InstructionListItem}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("openrouter"); @@ -777,9 +778,21 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") + } else { + let api_url = OpenRouterLanguageModelProvider::api_url(cx); + if api_url == OPEN_ROUTER_API_URL { + "API key configured".to_string() + } else { + format!("API key configured for {}", api_url) + } + }; if self.load_credentials_task.is_some() { - div().child(Label::new("Loading credentials...")).into_any() + div() + .child(Label::new("Loading credentials...")) + .into_any_element() } else if self.should_render_editor(cx) { v_flex() .size_full() @@ -806,56 +819,15 @@ impl Render for ConfigurationView { ) .size(LabelSize::Small).color(Color::Muted), ) - .into_any() + .into_any_element() } else { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .flex_1() - .min_w_0() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .child(Label::new(if env_var_set { - format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") - } else { - let api_url = OpenRouterLanguageModelProvider::api_url(cx); - if api_url == OPEN_ROUTER_API_URL { - "API key configured".to_string() - } else { - format!("API key configured for {}", api_url) - } - })) - ), - ) - .child( - h_flex() - .flex_shrink_0() - .child( - Button::new("reset-key", "Reset Key") - .label_size(LabelSize::Small) - .icon(Some(IconName::Trash)) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .disabled(env_var_set) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))) - }) - .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), - ), - ) - .into_any() + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .when(env_var_set, |this| { + this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")) + }) + .into_any_element() } } } diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index 9adc794ceaf255756736b401fff45f1131d4b310..20db24274aae0249efcfc897cb1bdfdcce8f1220 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -14,13 +14,16 @@ pub use settings::VercelAvailableModel as AvailableModel; use settings::{Settings, SettingsStore}; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; -use ui::{ElevationIndex, List, Tooltip, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; use vercel::{Model, VERCEL_API_URL}; use zed_env_vars::{EnvVar, env_var}; -use crate::{api_key::ApiKeyState, ui::InstructionListItem}; +use crate::{ + api_key::ApiKeyState, + ui::{ConfiguredApiCard, InstructionListItem}, +}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("vercel"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Vercel"); @@ -448,6 +451,16 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") + } else { + let api_url = VercelLanguageModelProvider::api_url(cx); + if api_url == VERCEL_API_URL { + "API key configured".to_string() + } else { + format!("API key configured for {}", api_url) + } + }; let api_key_section = if self.should_render_editor(cx) { v_flex() @@ -477,56 +490,15 @@ impl Render for ConfigurationView { .size(LabelSize::Small) .color(Color::Muted), ) - .into_any() + .into_any_element() } else { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .flex_1() - .min_w_0() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .child(Label::new(if env_var_set { - format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") - } else { - let api_url = VercelLanguageModelProvider::api_url(cx); - if api_url == VERCEL_API_URL { - "API key configured".to_string() - } else { - format!("API key configured for {}", api_url) - } - })) - ), - ) - .child( - h_flex() - .flex_shrink_0() - .child( - Button::new("reset-api-key", "Reset API Key") - .label_size(LabelSize::Small) - .icon(IconName::Undo) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .layer(ElevationIndex::ModalSurface) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))) - }) - .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), - ), - ) - .into_any() + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .when(env_var_set, |this| { + this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")) + }) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .into_any_element() }; if self.load_credentials_task.is_some() { diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index 979824442c6d03a8f735448003425e94a83e46ea..e7ee71ba86e202fe17d567923f4b04d3c886ae08 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -14,13 +14,16 @@ pub use settings::XaiAvailableModel as AvailableModel; use settings::{Settings, SettingsStore}; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; -use ui::{ElevationIndex, List, Tooltip, prelude::*}; +use ui::{List, prelude::*}; use ui_input::InputField; use util::ResultExt; use x_ai::{Model, XAI_API_URL}; use zed_env_vars::{EnvVar, env_var}; -use crate::{api_key::ApiKeyState, ui::InstructionListItem}; +use crate::{ + api_key::ApiKeyState, + ui::{ConfiguredApiCard, InstructionListItem}, +}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("x_ai"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("xAI"); @@ -445,6 +448,16 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); + let configured_card_label = if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") + } else { + let api_url = XAiLanguageModelProvider::api_url(cx); + if api_url == XAI_API_URL { + "API key configured".to_string() + } else { + format!("API key configured for {}", api_url) + } + }; let api_key_section = if self.should_render_editor(cx) { v_flex() @@ -474,56 +487,15 @@ impl Render for ConfigurationView { .size(LabelSize::Small) .color(Color::Muted), ) - .into_any() + .into_any_element() } else { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .flex_1() - .min_w_0() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .child(Label::new(if env_var_set { - format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable") - } else { - let api_url = XAiLanguageModelProvider::api_url(cx); - if api_url == XAI_API_URL { - "API key configured".to_string() - } else { - format!("API key configured for {}", api_url) - } - })) - ), - ) - .child( - h_flex() - .flex_shrink_0() - .child( - Button::new("reset-api-key", "Reset API Key") - .label_size(LabelSize::Small) - .icon(IconName::Undo) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .layer(ElevationIndex::ModalSurface) - .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))) - }) - .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), - ), - ) - .into_any() + ConfiguredApiCard::new(configured_card_label) + .disabled(env_var_set) + .when(env_var_set, |this| { + this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")) + }) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) + .into_any_element() }; if self.load_credentials_task.is_some() { diff --git a/crates/language_models/src/ui.rs b/crates/language_models/src/ui.rs index 80321656007ab3dfa19a5171d5bead18c9a5cc99..1d7796ecc2b6c2a78b3ebc02dc9cd29bd8cfa2c6 100644 --- a/crates/language_models/src/ui.rs +++ b/crates/language_models/src/ui.rs @@ -1,2 +1,4 @@ +pub mod configured_api_card; pub mod instruction_list_item; +pub use configured_api_card::ConfiguredApiCard; pub use instruction_list_item::InstructionListItem; diff --git a/crates/language_models/src/ui/configured_api_card.rs b/crates/language_models/src/ui/configured_api_card.rs new file mode 100644 index 0000000000000000000000000000000000000000..063ac1717f3aa5de1a448e26c94df7530fec588f --- /dev/null +++ b/crates/language_models/src/ui/configured_api_card.rs @@ -0,0 +1,86 @@ +use gpui::{ClickEvent, IntoElement, ParentElement, SharedString}; +use ui::{Tooltip, prelude::*}; + +#[derive(IntoElement)] +pub struct ConfiguredApiCard { + label: SharedString, + button_label: Option, + tooltip_label: Option, + disabled: bool, + on_click: Option>, +} + +impl ConfiguredApiCard { + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + button_label: None, + tooltip_label: None, + disabled: false, + on_click: None, + } + } + + pub fn on_click( + mut self, + handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_click = Some(Box::new(handler)); + self + } + + pub fn button_label(mut self, button_label: impl Into) -> Self { + self.button_label = Some(button_label.into()); + self + } + + pub fn tooltip_label(mut self, tooltip_label: impl Into) -> Self { + self.tooltip_label = Some(tooltip_label.into()); + self + } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} + +impl RenderOnce for ConfiguredApiCard { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let button_label = self.button_label.unwrap_or("Reset Key".into()); + let button_id = SharedString::new(format!("id-{}", button_label)); + + h_flex() + .mt_0p5() + .p_1() + .justify_between() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().background) + .child( + h_flex() + .flex_1() + .min_w_0() + .gap_1() + .child(Icon::new(IconName::Check).color(Color::Success)) + .child(Label::new(self.label).truncate()), + ) + .child( + Button::new(button_id, button_label) + .label_size(LabelSize::Small) + .icon(IconName::Undo) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) + .disabled(self.disabled) + .when_some(self.tooltip_label, |this, label| { + this.tooltip(Tooltip::text(label)) + }) + .when_some( + self.on_click.filter(|_| !self.disabled), + |this, on_click| this.on_click(on_click), + ), + ) + } +}