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}