1use super::model_selector_item::{ModelSelectorItemAction, ModelSelectorItemWidgetRefExt};
2use crate::{
3 aitk::{controllers::chat::ChatController, protocol::*},
4 utils::makepad::load_image_from_resource,
5 widgets::model_selector::{BotGroup, default_grouping},
6};
7use makepad_widgets::*;
8use std::collections::HashMap;
9use std::sync::{Arc, Mutex};
10
11type ErasedGroupingClosure = Box<dyn Fn(&Bot) -> BotGroup>;
13
14pub trait BotFilter {
16 fn should_show(&self, bot: &Bot) -> bool;
18}
19
20script_mod! {
21 use mod.prelude.widgets.*
22
23 mod.widgets.ModelSelectorList = #(ModelSelectorList::register_widget(vm)) {
24 width: Fill, height: Fit
25 flow: Down
26
27 item_template := mod.widgets.ModelSelectorItem {}
28
29 section_label_template := View {
30 width: Fill, height: Fit
31 padding: Inset { left: 14, top: 6, bottom: 4 }
32 align: Align { x: 0.0, y: 0.5 }
33 spacing: 4
34
35 icon_view := View {
36 width: Fit, height: Fit
37 visible: false
38 icon_image := Image {
39 width: 25.0, height: 25.0
40 }
41 }
42
43 icon_fallback_view := RoundedView {
44 width: 25.0, height: 25.0
45 visible: false
46 show_bg: true
47 draw_bg +: {
48 color: #344054
49 border_radius: 6.0
50 }
51 align: Align { x: 0.5, y: 0.5 }
52
53 icon_fallback_label := Label {
54 draw_text +: {
55 text_style: theme.font_bold { font_size: 13.0 }
56 color: #fff
57 }
58 }
59 }
60
61 label := Label {
62 draw_text +: {
63 text_style: theme.font_bold { font_size: 10.0 }
64 color: #989898
65 }
66 }
67 }
68 }
69}
70
71#[derive(Script, Widget)]
72pub struct ModelSelectorList {
73 #[uid]
74 uid: WidgetUid,
75
76 #[redraw]
77 #[rust]
78 area: Area,
79
80 #[walk]
81 walk: Walk,
82
83 #[layout]
84 layout: Layout,
85
86 #[rust]
88 templates: HashMap<LiveId, ScriptObjectRef>,
89
90 #[rust]
91 pub items: ComponentMap<LiveId, WidgetRef>,
92
93 #[rust]
94 pub search_filter: String,
95
96 #[rust]
97 pub total_height: Option<f64>,
98
99 #[rust]
100 pub chat_controller: Option<Arc<Mutex<ChatController>>>,
101
102 #[rust(Box::new(default_grouping) as ErasedGroupingClosure)]
103 pub grouping: ErasedGroupingClosure,
104
105 #[rust]
106 pub filter: Option<Box<dyn BotFilter>>,
107}
108
109impl ScriptHook for ModelSelectorList {
110 fn on_before_apply(
111 &mut self,
112 _vm: &mut ScriptVm,
113 apply: &Apply,
114 _scope: &mut Scope,
115 _value: ScriptValue,
116 ) {
117 if apply.is_reload() {
118 self.templates.clear();
119 }
120 }
121
122 fn on_after_apply(
123 &mut self,
124 vm: &mut ScriptVm,
125 apply: &Apply,
126 _scope: &mut Scope,
127 value: ScriptValue,
128 ) {
129 if !apply.is_eval() {
130 if let Some(obj) = value.as_object() {
131 vm.vec_with(obj, |vm, vec| {
132 for kv in vec {
133 if let Some(id) = kv.key.as_id() {
134 if let Some(template_obj) = kv.value.as_object() {
135 self.templates
136 .insert(id, vm.bx.heap.new_object_ref(template_obj));
137 }
138 }
139 }
140 });
141 }
142 }
143 }
144}
145
146impl Widget for ModelSelectorList {
147 fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
148 for (_, item) in self.items.iter_mut() {
150 item.handle_event(cx, event, scope);
151 }
152
153 self.widget_match_event(cx, event, scope);
154 }
155
156 fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep {
157 cx.begin_turtle(walk, self.layout);
158
159 let (bots, selected_bot_id) = if let Some(chat_controller) = &self.chat_controller {
161 {
162 let chat_controller = chat_controller.lock().unwrap();
163 let state = chat_controller.state();
164 (state.bots.clone(), state.bot_id.clone())
165 }
166 } else {
167 (Vec::new(), None)
168 };
169
170 self.draw_items(cx, &bots, selected_bot_id.as_ref());
171
172 cx.end_turtle_with_area(&mut self.area);
173 DrawStep::done()
174 }
175}
176
177impl WidgetMatchEvent for ModelSelectorList {
178 fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) {
179 for action in actions {
182 let Some(widget_action) = action.as_widget_action() else {
183 continue;
184 };
185
186 if let ModelSelectorItemAction::BotSelected(bot_id) = widget_action.cast() {
187 cx.widget_action(
188 self.widget_uid(),
189 ModelSelectorItemAction::BotSelected(bot_id),
190 );
191 }
192 }
193 }
194}
195
196impl ModelSelectorList {
197 fn create_widget_from_template(
198 templates: &HashMap<LiveId, ScriptObjectRef>,
199 cx: &mut Cx,
200 template_name: LiveId,
201 ) -> WidgetRef {
202 let template_ref = templates.get(&template_name).expect("template not found");
203 let template_value: ScriptValue = template_ref.as_object().into();
204 cx.with_vm(|vm| WidgetRef::script_from_value(vm, template_value))
205 }
206
207 fn draw_items(&mut self, cx: &mut Cx2d, bots: &[Bot], selected_bot_id: Option<&BotId>) {
208 let mut total_height = 0.0;
209
210 let terms = self
212 .search_filter
213 .split_whitespace()
214 .map(|s| s.to_ascii_lowercase())
215 .collect::<Vec<_>>();
216
217 let filtered_bots: Vec<&Bot> = bots
218 .iter()
219 .filter(|bot| {
220 let matches_search = if terms.is_empty() {
222 true
223 } else {
224 let name = bot.name.to_ascii_lowercase();
225 let id = bot.id.as_str().to_ascii_lowercase();
226 terms.iter().all(|t| name.contains(t) || id.contains(t))
227 };
228
229 let passes_filter = self.filter.as_ref().map_or(true, |f| f.should_show(bot));
231
232 matches_search && passes_filter
233 })
234 .collect();
235
236 let mut groups: HashMap<String, ((String, Option<EntityAvatar>), Vec<&Bot>)> =
238 HashMap::new();
239 for bot in filtered_bots {
240 let group = (self.grouping)(bot);
241 groups
242 .entry(group.id)
243 .or_insert_with(|| ((group.label, group.icon), Vec::new()))
244 .1
245 .push(bot);
246 }
247
248 let mut group_list: Vec<_> = groups.into_iter().collect();
250 group_list.sort_by(|(a_id, _), (b_id, _)| a_id.cmp(b_id));
251
252 for (group_id, ((group_label, group_icon), mut group_bots)) in group_list {
253 let section_id = LiveId::from_str(&format!("section_{}", group_id));
255 let templates = &self.templates;
256 let section_label = self.items.get_or_insert(cx, section_id, |cx| {
257 Self::create_widget_from_template(templates, cx, id!(section_label_template))
258 });
259
260 section_label
261 .label(cx, ids!(label))
262 .set_text(cx, &group_label);
263
264 match group_icon
265 .or_else(|| EntityAvatar::from_first_grapheme(&group_label.to_uppercase()))
266 .unwrap_or_else(|| EntityAvatar::Text("?".into()))
267 {
268 EntityAvatar::Image(image) => {
269 section_label
270 .view(cx, ids!(icon_fallback_view))
271 .set_visible(cx, false);
272 section_label
273 .view(cx, ids!(icon_view))
274 .set_visible(cx, true);
275 let img = section_label.image(cx, ids!(icon_image));
276 let _ = load_image_from_resource(&img, cx, &image)
277 .or_else(|_| img.load_image_file_by_path(cx, image.as_ref()));
278 }
279 EntityAvatar::Text(text) => {
280 section_label
281 .view(cx, ids!(icon_view))
282 .set_visible(cx, false);
283 section_label
284 .view(cx, ids!(icon_fallback_view))
285 .set_visible(cx, true);
286 section_label
287 .label(cx, ids!(icon_fallback_label))
288 .set_text(cx, &text);
289 }
290 }
291
292 let _ = section_label.draw_all(cx, &mut Scope::empty());
293 total_height += section_label.area().rect(cx).size.y;
294
295 group_bots.sort_by(|a, b| a.name.cmp(&b.name));
297
298 for bot in group_bots {
300 let item_id = LiveId::from_str(bot.id.as_str());
301
302 let templates = &self.templates;
303 let item_widget = self.items.get_or_insert(cx, item_id, |cx| {
304 Self::create_widget_from_template(templates, cx, id!(item_template))
305 });
306
307 let mut item = item_widget.as_model_selector_item();
308 item.set_bot(bot.clone());
309
310 let is_selected = selected_bot_id == Some(&bot.id);
311 item.set_selected(is_selected);
312
313 let _ = item_widget.draw_all(cx, &mut Scope::empty());
314 total_height += item_widget.area().rect(cx).size.y;
315 }
316 }
317
318 self.total_height = Some(total_height);
319 }
320}
321
322impl ModelSelectorListRef {
323 pub fn get_height(&self) -> f64 {
325 if let Some(inner) = self.borrow() {
326 inner.total_height.unwrap_or(0.0)
327 } else {
328 0.0
329 }
330 }
331
332 pub fn set_search_filter(&mut self, cx: &mut Cx, filter: &str) {
334 if let Some(mut inner) = self.borrow_mut() {
335 inner.search_filter = filter.to_string();
336 inner.items.clear();
337 inner.total_height = None;
338 inner.redraw(cx);
339 }
340 }
341
342 pub fn clear_search_filter(&mut self, cx: &mut Cx) {
344 self.set_search_filter(cx, "");
345 }
346
347 pub fn set_chat_controller(&mut self, controller: Option<Arc<Mutex<ChatController>>>) {
349 if let Some(mut inner) = self.borrow_mut() {
350 inner.chat_controller = controller;
351 }
352 }
353
354 pub fn set_grouping<F>(&mut self, grouping: F)
356 where
357 F: Fn(&Bot) -> BotGroup + 'static,
358 {
359 if let Some(mut inner) = self.borrow_mut() {
360 inner.grouping = Box::new(grouping);
361 }
362 }
363}