diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index 4e0aee55b0..880bad6201 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -8,6 +8,7 @@ use provider::deepseek::DeepSeekLanguageModelProvider; pub mod provider; mod settings; +pub mod ui; use crate::provider::anthropic::AnthropicLanguageModelProvider; use crate::provider::bedrock::BedrockLanguageModelProvider; diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 828ed02075..011224cf1a 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -1,3 +1,4 @@ +use crate::ui::InstructionListItem; use crate::AllLanguageModelSettings; use anthropic::{AnthropicError, ContentDelta, Event, ResponseContent}; use anyhow::{anyhow, Context as _, Result}; @@ -24,7 +25,7 @@ use std::str::FromStr; use std::sync::Arc; use strum::IntoEnumIterator; use theme::ThemeSettings; -use ui::{prelude::*, Icon, IconName, Tooltip}; +use ui::{prelude::*, Icon, IconName, List, Tooltip}; use util::{maybe, ResultExt}; const PROVIDER_ID: &str = language_model::ANTHROPIC_PROVIDER_ID; @@ -803,12 +804,6 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - const ANTHROPIC_CONSOLE_URL: &str = "https://console.anthropic.com/settings/keys"; - const INSTRUCTIONS: [&str; 3] = [ - "To use Zed's assistant with Anthropic, you need to add an API key. Follow these steps:", - "- Create one at:", - "- Paste your API key below and hit enter to use the assistant:", - ]; let env_var_set = self.state.read(cx).api_key_from_env; if self.load_credentials_task.is_some() { @@ -817,17 +812,20 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new(INSTRUCTIONS[0])) - .child(h_flex().child(Label::new(INSTRUCTIONS[1])).child( - Button::new("anthropic_console", ANTHROPIC_CONSOLE_URL) - .style(ButtonStyle::Subtle) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .on_click(move |_, _, cx| cx.open_url(ANTHROPIC_CONSOLE_URL)) - ) + .child(Label::new("To use Zed's assistant with Anthropic, you need to add an API key. Follow these steps:")) + .child( + List::new() + .child( + InstructionListItem::new( + "Create one by visiting", + Some("Anthropic's settings"), + Some("https://console.anthropic.com/settings/keys") + ) + ) + .child( + InstructionListItem::text_only("Paste your API key below and hit enter to start using the assistant") + ) ) - .child(Label::new(INSTRUCTIONS[2])) .child( h_flex() .w_full() @@ -844,7 +842,8 @@ impl Render for ConfigurationView { Label::new( format!("You can also assign the {ANTHROPIC_API_KEY_VAR} environment variable and restart Zed."), ) - .size(LabelSize::Small), + .size(LabelSize::Small) + .color(Color::Muted), ) .into_any() } else { diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index cbbf867875..99453386e3 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -2,6 +2,7 @@ use std::pin::Pin; use std::str::FromStr; use std::sync::Arc; +use crate::ui::InstructionListItem; use anyhow::{anyhow, Context as _, Result}; use aws_config::stalled_stream_protection::StalledStreamProtectionConfig; use aws_config::Region; @@ -37,7 +38,7 @@ use settings::{Settings, SettingsStore}; use strum::IntoEnumIterator; use theme::ThemeSettings; use tokio::runtime::Handle; -use ui::{prelude::*, Icon, IconName, Tooltip}; +use ui::{prelude::*, Icon, IconName, List, Tooltip}; use util::{maybe, ResultExt}; use crate::AllLanguageModelSettings; @@ -954,23 +955,7 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - const IAM_CONSOLE_URL: &str = "https://us-east-1.console.aws.amazon.com/iam/home"; - const BEDROCK_DOCS_URL: &str = - "https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html"; - const BEDROCK_MODEL_CATALOG: &str = - "https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess"; - const INSTRUCTIONS: [&str; 5] = [ - "To use Zed's assistant with Bedrock, you need to add the Access Key ID, Secret Access Key and AWS Region. Follow these steps:", - "- Create a user and security credentials here:", - "- Grant that user permissions according to this documentation:", - "- Go to the console and select the models you would like access to:", - "- Fill the fields below and hit enter to use the assistant:", - ]; - const BEDROCK_MODEL_CATALOG_LABEL: &str = "Bedrock Model Catalog"; - const BEDROCK_IAM_DOCS: &str = "Prerequisites"; - let env_var_set = self.state.read(cx).credentials_from_env; - let bg_color = cx.theme().colors().editor_background; let border_color = cx.theme().colors().border_variant; let input_base_styles = || { @@ -990,33 +975,34 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(ConfigurationView::save_credentials)) - .child(Label::new(INSTRUCTIONS[0])) - .child(h_flex().flex_wrap().child(Label::new(INSTRUCTIONS[1])).child( - Button::new("iam_console", IAM_CONSOLE_URL) - .style(ButtonStyle::Subtle) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .on_click(move |_, _window, cx| cx.open_url(IAM_CONSOLE_URL)) + .child(Label::new("To use Zed's assistant with Bedrock, you need to add the Access Key ID, Secret Access Key and AWS Region. Follow these steps:")) + .child( + List::new() + .child( + InstructionListItem::new( + "Start by", + Some("creating a user and security credentials"), + Some("https://us-east-1.console.aws.amazon.com/iam/home") + ) + ) + .child( + InstructionListItem::new( + "Grant that user permissions according to this documentation:", + Some("Prerequisites"), + Some("https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html") + ) + ) + .child( + InstructionListItem::new( + "Select the models you would like access to:", + Some("Bedrock Model Catalog"), + Some("https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess") + ) + ) + .child( + InstructionListItem::text_only("Fill the fields below and hit enter to start using the assistant") + ) ) - ) - .child(h_flex().flex_wrap().child(Label::new(INSTRUCTIONS[2])).child( - Button::new("bedrock_iam_docs", BEDROCK_IAM_DOCS) - .style(ButtonStyle::Subtle) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .on_click(move |_, _window, cx| cx.open_url(BEDROCK_DOCS_URL)) - )) - .child(h_flex().flex_wrap().child(Label::new(INSTRUCTIONS[3])).child( - Button::new("bedrock_model_catalog", BEDROCK_MODEL_CATALOG_LABEL) - .style(ButtonStyle::Subtle) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .on_click(move |_, _window, cx| cx.open_url(BEDROCK_MODEL_CATALOG)) - )) - .child(Label::new(INSTRUCTIONS[4])) .child( v_flex() .my_2() @@ -1050,7 +1036,8 @@ impl Render for ConfigurationView { Label::new( format!("You can also assign the {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR}, and {ZED_BEDROCK_REGION_VAR} environment variables and restart Zed."), ) - .size(LabelSize::Small), + .size(LabelSize::Small) + .color(Color::Muted), ) .into_any() } else { diff --git a/crates/language_models/src/provider/deepseek.rs b/crates/language_models/src/provider/deepseek.rs index 84d34307cb..85a44ea2a4 100644 --- a/crates/language_models/src/provider/deepseek.rs +++ b/crates/language_models/src/provider/deepseek.rs @@ -18,10 +18,10 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use std::sync::Arc; use theme::ThemeSettings; -use ui::{prelude::*, Icon, IconName}; +use ui::{prelude::*, Icon, IconName, List}; use util::ResultExt; -use crate::AllLanguageModelSettings; +use crate::{ui::InstructionListItem, AllLanguageModelSettings}; const PROVIDER_ID: &str = "deepseek"; const PROVIDER_NAME: &str = "DeepSeek"; @@ -607,13 +607,6 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - const DEEPSEEK_CONSOLE_URL: &str = "https://platform.deepseek.com/api_keys"; - const INSTRUCTIONS: [&str; 3] = [ - "To use DeepSeek in Zed, you need an API key:", - "- Get your API key from:", - "- Paste it below and press enter:", - ]; - let env_var_set = self.state.read(cx).api_key_from_env; if self.load_credentials_task.is_some() { @@ -622,18 +615,18 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new(INSTRUCTIONS[0])) + .child(Label::new("To use DeepSeek in Zed, you need an API key:")) .child( - h_flex().child(Label::new(INSTRUCTIONS[1])).child( - Button::new("deepseek_console", DEEPSEEK_CONSOLE_URL) - .style(ButtonStyle::Subtle) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .on_click(move |_, _window, cx| cx.open_url(DEEPSEEK_CONSOLE_URL)), - ), + List::new() + .child(InstructionListItem::new( + "Get your API key from the", + Some("DeepSeek console"), + Some("https://platform.deepseek.com/api_keys"), + )) + .child(InstructionListItem::text_only( + "Paste your API key below and hit enter to start using the assistant", + )), ) - .child(Label::new(INSTRUCTIONS[2])) .child( h_flex() .w_full() @@ -651,7 +644,8 @@ impl Render for ConfigurationView { "Or set the {} environment variable.", DEEPSEEK_API_KEY_VAR )) - .size(LabelSize::Small), + .size(LabelSize::Small) + .color(Color::Muted), ) .into_any() } else { diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index 934a06af55..dfaf46ed68 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -20,9 +20,10 @@ use settings::{Settings, SettingsStore}; use std::{future, sync::Arc}; use strum::IntoEnumIterator; use theme::ThemeSettings; -use ui::{prelude::*, Icon, IconName, Tooltip}; +use ui::{prelude::*, Icon, IconName, List, Tooltip}; use util::ResultExt; +use crate::ui::InstructionListItem; use crate::AllLanguageModelSettings; const PROVIDER_ID: &str = "google"; @@ -508,13 +509,6 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - const GOOGLE_CONSOLE_URL: &str = "https://aistudio.google.com/app/apikey"; - const INSTRUCTIONS: [&str; 3] = [ - "To use Zed's assistant with Google AI, you need to add an API key. Follow these steps:", - "- Create one by visiting:", - "- Paste your API key below and hit enter to use the assistant", - ]; - let env_var_set = self.state.read(cx).api_key_from_env; if self.load_credentials_task.is_some() { @@ -523,17 +517,18 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new(INSTRUCTIONS[0])) - .child(h_flex().child(Label::new(INSTRUCTIONS[1])).child( - Button::new("google_console", GOOGLE_CONSOLE_URL) - .style(ButtonStyle::Subtle) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .on_click(move |_, _, cx| cx.open_url(GOOGLE_CONSOLE_URL)) - ) + .child(Label::new("To use Zed's assistant with Google AI, you need to add an API key. Follow these steps:")) + .child( + List::new() + .child(InstructionListItem::new( + "Create one by visiting", + Some("Google AI's console"), + Some("https://aistudio.google.com/app/apikey"), + )) + .child(InstructionListItem::text_only( + "Paste your API key below and hit enter to start using the assistant", + )), ) - .child(Label::new(INSTRUCTIONS[2])) .child( h_flex() .w_full() @@ -550,7 +545,7 @@ impl Render for ConfigurationView { Label::new( format!("You can also assign the {GOOGLE_AI_API_KEY_VAR} environment variable and restart Zed."), ) - .size(LabelSize::Small), + .size(LabelSize::Small).color(Color::Muted), ) .into_any() } else { diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index 55a6413ef6..9076105312 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -20,10 +20,10 @@ use settings::{Settings, SettingsStore}; use std::sync::Arc; use strum::IntoEnumIterator; use theme::ThemeSettings; -use ui::{prelude::*, Icon, IconName, Tooltip}; +use ui::{prelude::*, Icon, IconName, List, Tooltip}; use util::ResultExt; -use crate::AllLanguageModelSettings; +use crate::{ui::InstructionListItem, AllLanguageModelSettings}; const PROVIDER_ID: &str = "mistral"; const PROVIDER_NAME: &str = "Mistral"; @@ -570,14 +570,6 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - const MISTRAL_CONSOLE_URL: &str = "https://console.mistral.ai/api-keys"; - const INSTRUCTIONS: [&str; 4] = [ - "To use Zed's assistant with Mistral, you need to add an API key. Follow these steps:", - " - Create one by visiting:", - " - Ensure your Mistral account has credits", - " - Paste your API key below and hit enter to start using the assistant", - ]; - let env_var_set = self.state.read(cx).api_key_from_env; if self.load_credentials_task.is_some() { @@ -586,19 +578,21 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new(INSTRUCTIONS[0])) - .child(h_flex().child(Label::new(INSTRUCTIONS[1])).child( - Button::new("mistral_console", MISTRAL_CONSOLE_URL) - .style(ButtonStyle::Subtle) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .on_click(move |_, _, cx| cx.open_url(MISTRAL_CONSOLE_URL)) - ) + .child(Label::new("To use Zed's assistant with Mistral, you need to add an API key. Follow these steps:")) + .child( + List::new() + .child(InstructionListItem::new( + "Create one by visiting", + Some("Mistral's console"), + Some("https://console.mistral.ai/api-keys"), + )) + .child(InstructionListItem::text_only( + "Ensure your Mistral account has credits", + )) + .child(InstructionListItem::text_only( + "Paste your API key below and hit enter to start using the assistant", + )), ) - .children( - (2..INSTRUCTIONS.len()).map(|n| - Label::new(INSTRUCTIONS[n])).collect::>()) .child( h_flex() .w_full() @@ -615,7 +609,7 @@ impl Render for ConfigurationView { Label::new( format!("You can also assign the {MISTRAL_API_KEY_VAR} environment variable and restart Zed."), ) - .size(LabelSize::Small), + .size(LabelSize::Small).color(Color::Muted), ) .into_any() } else { diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index c249af0bb7..1ece503e37 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -21,10 +21,10 @@ use settings::{Settings, SettingsStore}; use std::sync::Arc; use strum::IntoEnumIterator; use theme::ThemeSettings; -use ui::{prelude::*, Icon, IconName, Tooltip}; +use ui::{prelude::*, Icon, IconName, List, Tooltip}; use util::ResultExt; -use crate::AllLanguageModelSettings; +use crate::{ui::InstructionListItem, AllLanguageModelSettings}; const PROVIDER_ID: &str = "openai"; const PROVIDER_NAME: &str = "OpenAI"; @@ -540,14 +540,6 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - const OPENAI_CONSOLE_URL: &str = "https://platform.openai.com/api-keys"; - const INSTRUCTIONS: [&str; 4] = [ - "To use Zed's assistant with OpenAI, you need to add an API key. Follow these steps:", - " - Create one by visiting:", - " - Ensure your OpenAI account has credits", - " - Paste your API key below and hit enter to start using the assistant", - ]; - let env_var_set = self.state.read(cx).api_key_from_env; if self.load_credentials_task.is_some() { @@ -556,19 +548,21 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new(INSTRUCTIONS[0])) - .child(h_flex().child(Label::new(INSTRUCTIONS[1])).child( - Button::new("openai_console", OPENAI_CONSOLE_URL) - .style(ButtonStyle::Subtle) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .on_click(move |_, _, cx| cx.open_url(OPENAI_CONSOLE_URL)) - ) + .child(Label::new("To use Zed's assistant with OpenAI, you need to add an API key. Follow these steps:")) + .child( + List::new() + .child(InstructionListItem::new( + "Create one by visiting", + Some("OpenAI's console"), + Some("https://platform.openai.com/api-keys"), + )) + .child(InstructionListItem::text_only( + "Ensure your OpenAI account has credits", + )) + .child(InstructionListItem::text_only( + "Paste your API key below and hit enter to start using the assistant", + )), ) - .children( - (2..INSTRUCTIONS.len()).map(|n| - Label::new(INSTRUCTIONS[n])).collect::>()) .child( h_flex() .w_full() @@ -585,13 +579,13 @@ impl Render for ConfigurationView { Label::new( format!("You can also assign the {OPENAI_API_KEY_VAR} environment variable and restart Zed."), ) - .size(LabelSize::Small), + .size(LabelSize::Small).color(Color::Muted), ) .child( Label::new( "Note that having a subscription for another service like GitHub Copilot won't work.".to_string(), ) - .size(LabelSize::Small), + .size(LabelSize::Small).color(Color::Muted), ) .into_any() } else { diff --git a/crates/language_models/src/ui.rs b/crates/language_models/src/ui.rs new file mode 100644 index 0000000000..8032165600 --- /dev/null +++ b/crates/language_models/src/ui.rs @@ -0,0 +1,2 @@ +pub mod instruction_list_item; +pub use instruction_list_item::InstructionListItem; diff --git a/crates/language_models/src/ui/instruction_list_item.rs b/crates/language_models/src/ui/instruction_list_item.rs new file mode 100644 index 0000000000..daf5646d65 --- /dev/null +++ b/crates/language_models/src/ui/instruction_list_item.rs @@ -0,0 +1,66 @@ +use gpui::{AnyElement, IntoElement, ParentElement, SharedString}; +use ui::{prelude::*, ListItem}; + +/// A reusable list item component for adding LLM provider configuration instructions +pub struct InstructionListItem { + label: SharedString, + button_label: Option, + button_link: Option, +} + +impl InstructionListItem { + pub fn new( + label: impl Into, + button_label: Option>, + button_link: Option>, + ) -> Self { + Self { + label: label.into(), + button_label: button_label.map(|l| l.into()), + button_link: button_link.map(|l| l.into()), + } + } + + pub fn text_only(label: impl Into) -> Self { + Self { + label: label.into(), + button_label: None, + button_link: None, + } + } +} + +impl IntoElement for InstructionListItem { + type Element = AnyElement; + + fn into_element(self) -> Self::Element { + let item_content = if let (Some(button_label), Some(button_link)) = + (self.button_label, self.button_link) + { + let link = button_link.clone(); + h_flex().flex_wrap().child(Label::new(self.label)).child( + Button::new("link-button", button_label) + .style(ButtonStyle::Subtle) + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .on_click(move |_, _window, cx| cx.open_url(&link)), + ) + } else { + div().child(Label::new(self.label)) + }; + + div() + .child( + ListItem::new("list-item") + .selectable(false) + .start_slot( + Icon::new(IconName::Dash) + .size(IconSize::XSmall) + .color(Color::Hidden), + ) + .child(item_content), + ) + .into_any() + } +}