From 508b581215a8f306540da22b5c7f89e236139e15 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 28 Feb 2025 12:06:47 -0300 Subject: [PATCH] assistant: Refine settings view's instruction visuals (#25812) I've been bothered by using simple hyphens for bullet lists here for a while; it kinda looked cheap and not well-formatted. So, in this PR, I'm adding a new, custom UI component in the `language_models` crate, called `InstructionListItem`, based off the `ListItem` that's somewhat mimic'ing what a `
  • ` would be on the web. It does have a "rigid" structure as in it's always a label followed by a button (which is optional), but that seems okay given it has been the overall shape of the copy we've been using here. Also, never really loved that we were pasting URLs directly, that kinda felt cheap, too. I could see an argument where it's just clearer, but it looks too cluttered, as URLs aren't super pretty, necessarily. | Before | After | |--------|--------| | | | Release Notes: - N/A --- crates/language_models/src/language_models.rs | 1 + .../language_models/src/provider/anthropic.rs | 35 +++++---- .../language_models/src/provider/bedrock.rs | 75 ++++++++----------- .../language_models/src/provider/deepseek.rs | 34 ++++----- crates/language_models/src/provider/google.rs | 33 ++++---- .../language_models/src/provider/mistral.rs | 40 +++++----- .../language_models/src/provider/open_ai.rs | 42 +++++------ crates/language_models/src/ui.rs | 2 + .../src/ui/instruction_list_item.rs | 66 ++++++++++++++++ 9 files changed, 180 insertions(+), 148 deletions(-) create mode 100644 crates/language_models/src/ui.rs create mode 100644 crates/language_models/src/ui/instruction_list_item.rs diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index 4e0aee55b06a4ea529ca62318bd26f82b23c9af3..880bad6201baf8e788765a7a12733c7eaf7a024f 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 828ed020754fb71b98600baac586511e1825cc18..011224cf1a4f86cbfe1b87795bfff1fbb56951d4 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 cbbf8678755b84c37afac946b53a1b5a8600fea2..99453386e34766aec957be39cd682e3470a55b1c 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 84d34307cb86945ec28a618f31f8a00f4e01fdc8..85a44ea2a4c1a6952df42323c79f620a4611810d 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 934a06af55271dbefe4d2fe0ad22370a35369f46..dfaf46ed68f204a6475fec95ec350ab29903546f 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 55a6413ef623fe4632657aace9e63016f0303dac..9076105312f5a11fa452530f8df88a76e686aaaa 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 c249af0bb7038a59e6e501c43c9993398839d661..1ece503e3782e904fac04dbdf20dd22d88f847f4 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 0000000000000000000000000000000000000000..80321656007ab3dfa19a5171d5bead18c9a5cc99 --- /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 0000000000000000000000000000000000000000..daf5646d65ce75a9d76e8f8f2b7e74e138568f5f --- /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() + } +}