use gpui::{Action, ClickEvent, FocusHandle, prelude::*}; use ui::{Chip, ElevationIndex, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*}; use zed_actions::agent::ToggleModelSelector; use crate::CycleFavoriteModels; enum ModelIcon { Name(IconName), Path(SharedString), } #[derive(IntoElement)] pub struct ModelSelectorHeader { title: SharedString, has_border: bool, } impl ModelSelectorHeader { pub fn new(title: impl Into, has_border: bool) -> Self { Self { title: title.into(), has_border, } } } impl RenderOnce for ModelSelectorHeader { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { div() .px_2() .pb_1() .when(self.has_border, |this| { this.mt_1() .pt_2() .border_t_1() .border_color(cx.theme().colors().border_variant) }) .child( Label::new(self.title) .size(LabelSize::XSmall) .color(Color::Muted), ) } } #[derive(IntoElement)] pub struct ModelSelectorListItem { index: usize, title: SharedString, icon: Option, is_selected: bool, is_focused: bool, is_latest: bool, is_favorite: bool, on_toggle_favorite: Option>, cost_info: Option, } impl ModelSelectorListItem { pub fn new(index: usize, title: impl Into) -> Self { Self { index, title: title.into(), icon: None, is_selected: false, is_focused: false, is_latest: false, is_favorite: false, on_toggle_favorite: None, cost_info: None, } } pub fn icon(mut self, icon: IconName) -> Self { self.icon = Some(ModelIcon::Name(icon)); self } pub fn icon_path(mut self, path: SharedString) -> Self { self.icon = Some(ModelIcon::Path(path)); self } pub fn is_selected(mut self, is_selected: bool) -> Self { self.is_selected = is_selected; self } pub fn is_focused(mut self, is_focused: bool) -> Self { self.is_focused = is_focused; self } pub fn is_latest(mut self, is_latest: bool) -> Self { self.is_latest = is_latest; self } pub fn is_favorite(mut self, is_favorite: bool) -> Self { self.is_favorite = is_favorite; self } pub fn on_toggle_favorite( mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, ) -> Self { self.on_toggle_favorite = Some(Box::new(handler)); self } pub fn cost_info(mut self, cost_info: Option) -> Self { self.cost_info = cost_info; self } } impl RenderOnce for ModelSelectorListItem { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { let model_icon_color = if self.is_selected { Color::Accent } else { Color::Muted }; let is_favorite = self.is_favorite; ListItem::new(self.index) .inset(true) .spacing(ListItemSpacing::Sparse) .toggle_state(self.is_focused) .child( h_flex() .w_full() .gap_1p5() .when_some(self.icon, |this, icon| { this.child( match icon { ModelIcon::Name(icon_name) => Icon::new(icon_name), ModelIcon::Path(icon_path) => Icon::from_external_svg(icon_path), } .color(model_icon_color) .size(IconSize::Small), ) }) .child(Label::new(self.title).truncate()) .when(self.is_latest, |parent| parent.child(Chip::new("Latest"))) .when_some(self.cost_info, |this, cost_info| { let tooltip_text = if cost_info.ends_with('×') { format!("Cost Multiplier: {}", cost_info) } else if cost_info.contains('$') { format!("Cost per Million Tokens: {}", cost_info) } else { format!("Cost: {}", cost_info) }; this.child(Chip::new(cost_info).tooltip(Tooltip::text(tooltip_text))) }), ) .end_slot(div().pr_2().when(self.is_selected, |this| { this.child(Icon::new(IconName::Check).color(Color::Accent)) })) .end_slot_on_hover(div().pr_1p5().when_some(self.on_toggle_favorite, { |this, handle_click| { let (icon, color, tooltip) = if is_favorite { (IconName::StarFilled, Color::Accent, "Unfavorite Model") } else { (IconName::Star, Color::Default, "Favorite Model") }; this.child( IconButton::new(("toggle-favorite", self.index), icon) .layer(ElevationIndex::ElevatedSurface) .icon_color(color) .icon_size(IconSize::Small) .tooltip(Tooltip::text(tooltip)) .on_click(move |event, window, cx| (handle_click)(event, window, cx)), ) } })) } } #[derive(IntoElement)] pub struct ModelSelectorFooter { action: Box, focus_handle: FocusHandle, } impl ModelSelectorFooter { pub fn new(action: Box, focus_handle: FocusHandle) -> Self { Self { action, focus_handle, } } } impl RenderOnce for ModelSelectorFooter { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let action = self.action; let focus_handle = self.focus_handle; h_flex() .w_full() .p_1p5() .border_t_1() .border_color(cx.theme().colors().border_variant) .child( Button::new("configure", "Configure") .full_width() .style(ButtonStyle::Outlined) .key_binding( KeyBinding::for_action_in(action.as_ref(), &focus_handle, cx) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(move |_, window, cx| { window.dispatch_action(action.boxed_clone(), cx); }), ) } } #[derive(IntoElement)] pub struct ModelSelectorTooltip { show_cycle_row: bool, } impl ModelSelectorTooltip { pub fn new() -> Self { Self { show_cycle_row: true, } } pub fn show_cycle_row(mut self, show: bool) -> Self { self.show_cycle_row = show; self } } impl RenderOnce for ModelSelectorTooltip { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { v_flex() .gap_1() .child( h_flex() .gap_2() .justify_between() .child(Label::new("Change Model")) .child(KeyBinding::for_action(&ToggleModelSelector, cx)), ) .when(self.show_cycle_row, |this| { this.child( h_flex() .pt_1() .gap_2() .border_t_1() .border_color(cx.theme().colors().border_variant) .justify_between() .child(Label::new("Cycle Favorited Models")) .child(KeyBinding::for_action(&CycleFavoriteModels, cx)), ) }) } }