model_selector_components.rs

  1use gpui::{Action, ClickEvent, FocusHandle, prelude::*};
  2use ui::{Chip, ElevationIndex, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
  3use zed_actions::agent::ToggleModelSelector;
  4
  5use crate::CycleFavoriteModels;
  6
  7enum ModelIcon {
  8    Name(IconName),
  9    Path(SharedString),
 10}
 11
 12#[derive(IntoElement)]
 13pub struct ModelSelectorHeader {
 14    title: SharedString,
 15    has_border: bool,
 16}
 17
 18impl ModelSelectorHeader {
 19    pub fn new(title: impl Into<SharedString>, has_border: bool) -> Self {
 20        Self {
 21            title: title.into(),
 22            has_border,
 23        }
 24    }
 25}
 26
 27impl RenderOnce for ModelSelectorHeader {
 28    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
 29        div()
 30            .px_2()
 31            .pb_1()
 32            .when(self.has_border, |this| {
 33                this.mt_1()
 34                    .pt_2()
 35                    .border_t_1()
 36                    .border_color(cx.theme().colors().border_variant)
 37            })
 38            .child(
 39                Label::new(self.title)
 40                    .size(LabelSize::XSmall)
 41                    .color(Color::Muted),
 42            )
 43    }
 44}
 45
 46#[derive(IntoElement)]
 47pub struct ModelSelectorListItem {
 48    index: usize,
 49    title: SharedString,
 50    icon: Option<ModelIcon>,
 51    is_selected: bool,
 52    is_focused: bool,
 53    is_latest: bool,
 54    is_favorite: bool,
 55    on_toggle_favorite: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
 56    cost_info: Option<SharedString>,
 57}
 58
 59impl ModelSelectorListItem {
 60    pub fn new(index: usize, title: impl Into<SharedString>) -> Self {
 61        Self {
 62            index,
 63            title: title.into(),
 64            icon: None,
 65            is_selected: false,
 66            is_focused: false,
 67            is_latest: false,
 68            is_favorite: false,
 69            on_toggle_favorite: None,
 70            cost_info: None,
 71        }
 72    }
 73
 74    pub fn icon(mut self, icon: IconName) -> Self {
 75        self.icon = Some(ModelIcon::Name(icon));
 76        self
 77    }
 78
 79    pub fn icon_path(mut self, path: SharedString) -> Self {
 80        self.icon = Some(ModelIcon::Path(path));
 81        self
 82    }
 83
 84    pub fn is_selected(mut self, is_selected: bool) -> Self {
 85        self.is_selected = is_selected;
 86        self
 87    }
 88
 89    pub fn is_focused(mut self, is_focused: bool) -> Self {
 90        self.is_focused = is_focused;
 91        self
 92    }
 93
 94    pub fn is_latest(mut self, is_latest: bool) -> Self {
 95        self.is_latest = is_latest;
 96        self
 97    }
 98
 99    pub fn is_favorite(mut self, is_favorite: bool) -> Self {
100        self.is_favorite = is_favorite;
101        self
102    }
103
104    pub fn on_toggle_favorite(
105        mut self,
106        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
107    ) -> Self {
108        self.on_toggle_favorite = Some(Box::new(handler));
109        self
110    }
111
112    pub fn cost_info(mut self, cost_info: Option<SharedString>) -> Self {
113        self.cost_info = cost_info;
114        self
115    }
116}
117
118impl RenderOnce for ModelSelectorListItem {
119    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
120        let model_icon_color = if self.is_selected {
121            Color::Accent
122        } else {
123            Color::Muted
124        };
125
126        let is_favorite = self.is_favorite;
127
128        ListItem::new(self.index)
129            .inset(true)
130            .spacing(ListItemSpacing::Sparse)
131            .toggle_state(self.is_focused)
132            .child(
133                h_flex()
134                    .w_full()
135                    .gap_1p5()
136                    .when_some(self.icon, |this, icon| {
137                        this.child(
138                            match icon {
139                                ModelIcon::Name(icon_name) => Icon::new(icon_name),
140                                ModelIcon::Path(icon_path) => Icon::from_external_svg(icon_path),
141                            }
142                            .color(model_icon_color)
143                            .size(IconSize::Small),
144                        )
145                    })
146                    .child(Label::new(self.title).truncate())
147                    .when(self.is_latest, |parent| parent.child(Chip::new("Latest")))
148                    .when_some(self.cost_info, |this, cost_info| {
149                        let tooltip_text = if cost_info.ends_with('×') {
150                            format!("Cost Multiplier: {}", cost_info)
151                        } else if cost_info.contains('$') {
152                            format!("Cost per Million Tokens: {}", cost_info)
153                        } else {
154                            format!("Cost: {}", cost_info)
155                        };
156
157                        this.child(Chip::new(cost_info).tooltip(Tooltip::text(tooltip_text)))
158                    }),
159            )
160            .end_slot(div().pr_2().when(self.is_selected, |this| {
161                this.child(Icon::new(IconName::Check).color(Color::Accent))
162            }))
163            .end_hover_slot(div().pr_1p5().when_some(self.on_toggle_favorite, {
164                |this, handle_click| {
165                    let (icon, color, tooltip) = if is_favorite {
166                        (IconName::StarFilled, Color::Accent, "Unfavorite Model")
167                    } else {
168                        (IconName::Star, Color::Default, "Favorite Model")
169                    };
170                    this.child(
171                        IconButton::new(("toggle-favorite", self.index), icon)
172                            .layer(ElevationIndex::ElevatedSurface)
173                            .icon_color(color)
174                            .icon_size(IconSize::Small)
175                            .tooltip(Tooltip::text(tooltip))
176                            .on_click(move |event, window, cx| (handle_click)(event, window, cx)),
177                    )
178                }
179            }))
180    }
181}
182
183#[derive(IntoElement)]
184pub struct ModelSelectorFooter {
185    action: Box<dyn Action>,
186    focus_handle: FocusHandle,
187}
188
189impl ModelSelectorFooter {
190    pub fn new(action: Box<dyn Action>, focus_handle: FocusHandle) -> Self {
191        Self {
192            action,
193            focus_handle,
194        }
195    }
196}
197
198impl RenderOnce for ModelSelectorFooter {
199    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
200        let action = self.action;
201        let focus_handle = self.focus_handle;
202
203        h_flex()
204            .w_full()
205            .p_1p5()
206            .border_t_1()
207            .border_color(cx.theme().colors().border_variant)
208            .child(
209                Button::new("configure", "Configure")
210                    .full_width()
211                    .style(ButtonStyle::Outlined)
212                    .key_binding(
213                        KeyBinding::for_action_in(action.as_ref(), &focus_handle, cx)
214                            .map(|kb| kb.size(rems_from_px(12.))),
215                    )
216                    .on_click(move |_, window, cx| {
217                        window.dispatch_action(action.boxed_clone(), cx);
218                    }),
219            )
220    }
221}
222
223#[derive(IntoElement)]
224pub struct ModelSelectorTooltip {
225    show_cycle_row: bool,
226}
227
228impl ModelSelectorTooltip {
229    pub fn new() -> Self {
230        Self {
231            show_cycle_row: true,
232        }
233    }
234
235    pub fn show_cycle_row(mut self, show: bool) -> Self {
236        self.show_cycle_row = show;
237        self
238    }
239}
240
241impl RenderOnce for ModelSelectorTooltip {
242    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
243        v_flex()
244            .gap_1()
245            .child(
246                h_flex()
247                    .gap_2()
248                    .justify_between()
249                    .child(Label::new("Change Model"))
250                    .child(KeyBinding::for_action(&ToggleModelSelector, cx)),
251            )
252            .when(self.show_cycle_row, |this| {
253                this.child(
254                    h_flex()
255                        .pt_1()
256                        .gap_2()
257                        .border_t_1()
258                        .border_color(cx.theme().colors().border_variant)
259                        .justify_between()
260                        .child(Label::new("Cycle Favorited Models"))
261                        .child(KeyBinding::for_action(&CycleFavoriteModels, cx)),
262                )
263            })
264    }
265}