Add custom icon for Anthropic hosted models (#16436)

Nathan Sobo created

This commit adds a custom icon for Anthropic hosted models.


![CleanShot 2024-08-18 at 15 40
38@2x](https://github.com/user-attachments/assets/d467ccab-9628-4258-89fc-782e0d4a48d4)
![CleanShot 2024-08-18 at 15 40
34@2x](https://github.com/user-attachments/assets/7efaff9c-6a58-47ba-87ea-e0fe0586fedc)


- Adding a new SVG icon for Anthropic hosted models.
  - The new icon is located at: `assets/icons/ai_anthropic_hosted.svg`
- Updating the LanguageModel trait to include an optional icon method
- Implementing the icon method for CloudModel to return the custom icon
for Anthropic hosted models
- Updating the UI components to use the model-specific icon when
available
- Adding a new IconName variant for the Anthropic hosted icon

We should change the non-hosted icon in some small way to distinguish it
from the hosted version. I duplicated the path for now so we can
hopefully add it for the next release.

Release Notes:

- N/A

Change summary

assets/icons/ai_anthropic_hosted.svg           | 11 +++++++++++
crates/assistant/src/assistant_panel.rs        |  2 +-
crates/assistant/src/model_selector.rs         |  9 +++++----
crates/language_model/src/language_model.rs    |  4 ++++
crates/language_model/src/model/cloud_model.rs |  8 ++++++++
crates/language_model/src/provider/cloud.rs    |  4 ++++
crates/ui/src/components/icon.rs               |  2 ++
7 files changed, 35 insertions(+), 5 deletions(-)

Detailed changes

assets/icons/ai_anthropic_hosted.svg 🔗

@@ -0,0 +1,11 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_1896_18)">
+<path d="M11.094 3.09999H8.952L12.858 12.9H15L11.094 3.09999Z" fill="#1F1F1E"/>
+<path d="M4.906 3.09999L1 12.9H3.184L3.98284 10.842H8.06915L8.868 12.9H11.052L7.146 3.09999H4.906ZM4.68928 9.02199L6.026 5.57799L7.3627 9.02199H4.68928Z" fill="#1F1F1E"/>
+</g>
+<defs>
+<clipPath id="clip0_1896_18">
+<rect width="14" height="9.8" fill="white" transform="translate(1 3.09999)"/>
+</clipPath>
+</defs>
+</svg>

crates/assistant/src/assistant_panel.rs 🔗

@@ -4135,7 +4135,7 @@ impl Render for ContextEditorToolbarItem {
                                             (Some(provider), Some(model)) => h_flex()
                                                 .gap_1()
                                                 .child(
-                                                    Icon::new(provider.icon())
+                                                    Icon::new(model.icon().unwrap_or_else(|| provider.icon()))
                                                         .color(Color::Muted)
                                                         .size(IconSize::XSmall),
                                                 )

crates/assistant/src/model_selector.rs 🔗

@@ -36,7 +36,7 @@ pub struct ModelPickerDelegate {
 #[derive(Clone)]
 struct ModelInfo {
     model: Arc<dyn LanguageModel>,
-    provider_icon: IconName,
+    icon: IconName,
     availability: LanguageModelAvailability,
     is_selected: bool,
 }
@@ -156,7 +156,7 @@ impl PickerDelegate for ModelPickerDelegate {
                 .selected(selected)
                 .start_slot(
                     div().pr_1().child(
-                        Icon::new(model_info.provider_icon)
+                        Icon::new(model_info.icon)
                             .color(Color::Muted)
                             .size(IconSize::Medium),
                     ),
@@ -261,16 +261,17 @@ impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
             .iter()
             .flat_map(|provider| {
                 let provider_id = provider.id();
-                let provider_icon = provider.icon();
+                let icon = provider.icon();
                 let selected_model = selected_model.clone();
                 let selected_provider = selected_provider.clone();
 
                 provider.provided_models(cx).into_iter().map(move |model| {
                     let model = model.clone();
+                    let icon = model.icon().unwrap_or(icon);
 
                     ModelInfo {
                         model: model.clone(),
-                        provider_icon,
+                        icon,
                         availability: model.availability(),
                         is_selected: selected_model.as_ref() == Some(&model.id())
                             && selected_provider.as_ref() == Some(&provider_id),

crates/language_model/src/language_model.rs 🔗

@@ -54,6 +54,10 @@ pub struct LanguageModelCacheConfiguration {
 pub trait LanguageModel: Send + Sync {
     fn id(&self) -> LanguageModelId;
     fn name(&self) -> LanguageModelName;
+    /// If None, falls back to [LanguageModelProvider::icon]
+    fn icon(&self) -> Option<IconName> {
+        None
+    }
     fn provider_id(&self) -> LanguageModelProviderId;
     fn provider_name(&self) -> LanguageModelProviderName;
     fn telemetry_id(&self) -> String;

crates/language_model/src/model/cloud_model.rs 🔗

@@ -2,6 +2,7 @@ use proto::Plan;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use strum::EnumIter;
+use ui::IconName;
 
 use crate::LanguageModelAvailability;
 
@@ -65,6 +66,13 @@ impl CloudModel {
         }
     }
 
+    pub fn icon(&self) -> Option<IconName> {
+        match self {
+            Self::Anthropic(_) => Some(IconName::AiAnthropicHosted),
+            _ => None,
+        }
+    }
+
     pub fn max_token_count(&self) -> usize {
         match self {
             Self::Anthropic(model) => model.max_token_count(),

crates/language_model/src/provider/cloud.rs 🔗

@@ -398,6 +398,10 @@ impl LanguageModel for CloudLanguageModel {
         LanguageModelName::from(self.model.display_name().to_string())
     }
 
+    fn icon(&self) -> Option<IconName> {
+        self.model.icon()
+    }
+
     fn provider_id(&self) -> LanguageModelProviderId {
         LanguageModelProviderId(PROVIDER_ID.into())
     }

crates/ui/src/components/icon.rs 🔗

@@ -107,6 +107,7 @@ impl IconSize {
 pub enum IconName {
     Ai,
     AiAnthropic,
+    AiAnthropicHosted,
     AiOpenAi,
     AiGoogle,
     AiOllama,
@@ -275,6 +276,7 @@ impl IconName {
         match self {
             IconName::Ai => "icons/ai.svg",
             IconName::AiAnthropic => "icons/ai_anthropic.svg",
+            IconName::AiAnthropicHosted => "icons/ai_anthropic_hosted.svg",
             IconName::AiOpenAi => "icons/ai_open_ai.svg",
             IconName::AiGoogle => "icons/ai_google.svg",
             IconName::AiOllama => "icons/ai_ollama.svg",