1use makepad_widgets::*;
2use std::sync::{Arc, Mutex};
3
4use crate::{
5 aitk::{
6 controllers::chat::{ChatController, ChatStateMutation},
7 protocol::*,
8 },
9 utils::makepad::events::EventExt,
10 widgets::{
11 model_selector_item::ModelSelectorItemAction, model_selector_list::ModelSelectorList,
12 moly_modal::MolyModalWidgetExt,
13 },
14};
15
16script_mod! {
17 use mod.prelude.widgets.*
18 use mod.widgets.*
19
20 let ModelSelectorButton = Button {
21 width: Fit
22 height: Fit
23 padding: Inset { left: 8, right: 8, top: 6, bottom: 6 }
24
25 draw_bg +: {
26 color_down: #0000
27 border_radius: 7.
28 border_size: 0.
29 color_hover: #xf2
30 }
31
32 draw_text +: {
33 text_style: theme.font_regular {
34 font_size: 11.
35 }
36 color: #222
37 color_hover: #111
38 color_focus: #111
39 color_down: #000
40 }
41 }
42
43 let ModelSelectorOptions = RoundedShadowView {
44 width: Fill, height: Fit
45 padding: 8
46 flow: Down
47 spacing: 8
48
49 show_bg: true
50 draw_bg +: {
51 color: #xf9
52 border_radius: 6.0
53 shadow_color: instance(#0002)
54 shadow_radius: 9.0
55 shadow_offset: vec2(0.0, -2.0)
56 }
57
58 search_container := RoundedView {
59 width: Fill, height: Fit
60 show_bg: true
61 padding: Inset { top: 4, bottom: 4, left: 8, right: 8 }
62 spacing: 8
63 align: Align { x: 0.0, y: 0.5 }
64 draw_bg +: {
65 border_radius: 6.0
66 border_color: #xD0D5DD
67 border_size: 1.0
68 color: #fff
69 }
70
71 search_input := TextInput {
72 width: Fill, height: Fit
73 draw_bg +: {
74 pixel: fn() -> vec4 {
75 return vec4(0.);
76 }
77 }
78 draw_text +: {
79 text_style: theme.font_regular { font_size: 11 }
80 color: #000
81 color_hover: #x98A2B3
82 color_focus: #000
83 color_empty: #x98A2B3
84 color_empty_focus: #x98A2B3
85 color_empty_hover: #x98A2B3
86 }
87 draw_cursor +: {
88 color: #000
89 }
90 empty_text: "Search models"
91 }
92 }
93
94 list_container := ScrollYView {
95 width: Fill
96 height: 200
97 scroll_bars +: {
98 scroll_bar_y +: {
99 drag_scrolling: true
100 draw_bg +: {
101 color: #xD9
102 color_hover: #888
103 color_drag: #777
104 }
105 }
106 }
107
108 list := ModelSelectorList {}
109 }
110 }
111
112 mod.widgets.ModelSelector = #(ModelSelector::register_widget(vm)) {
113 width: Fit, height: Fit
114 flow: Overlay
115
116 button := ModelSelectorButton {
117 text: "Loading model..."
118 }
119
120 modal := MolyModal {
121 dismiss_on_focus_lost: true
122 bg_view +: {
123 visible: false
124 }
125 align: Align { x: 0.0, y: 0.0 }
126
127 content +: {
128 width: 400
129 height: Fit
130 padding: Inset { top: 20, left: 10, right: 10, bottom: 20 }
131 options := ModelSelectorOptions {}
132 }
133 }
134 }
135}
136
137#[derive(Script, ScriptHook, Widget)]
138pub struct ModelSelector {
139 #[deref]
140 view: View,
141
142 #[rust]
143 pub chat_controller: Option<Arc<Mutex<ChatController>>>,
144
145 #[rust]
146 pub open: bool,
147}
148
149impl Widget for ModelSelector {
150 fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
151 self.view.handle_event(cx, event, scope);
152 self.widget_match_event(cx, event, scope);
153
154 if self.button(cx, ids!(button)).clicked(event.actions()) {
156 if !self.open {
157 self.open_modal(cx);
158 } else {
159 self.close_modal(cx);
160 }
161 }
162
163 if self.moly_modal(cx, ids!(modal)).dismissed(event.actions()) {
165 self.close_modal(cx);
166 self.clear_search(cx);
167 self.button(cx, ids!(button)).reset_hover(cx);
168 }
169
170 if self.open && !cx.display_context.is_desktop() {
172 if let Hit::FingerUp(fe) = event.hits(cx, self.view(cx, ids!(modal.bg_view)).area()) {
173 if fe.was_tap() {
174 self.close_modal(cx);
175 self.clear_search(cx);
176 self.button(cx, ids!(button)).reset_hover(cx);
177 }
178 }
179 }
180 }
181
182 fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
183 let (bots, selected_bot_id) = if let Some(chat_controller) = &self.chat_controller {
185 let state = chat_controller.lock().unwrap().state().clone();
186 (state.bots, state.bot_id)
187 } else {
188 (Vec::new(), None)
189 };
190
191 if bots.is_empty() {
193 self.button(cx, ids!(button))
194 .set_text(cx, "No models available");
195 self.button(cx, ids!(button)).set_enabled(cx, false);
196 } else {
197 self.button(cx, ids!(button)).set_enabled(cx, true);
198
199 if let Some(bot_id) = &selected_bot_id {
201 if let Some(bot) = bots.iter().find(|b| &b.id == bot_id) {
202 self.button(cx, ids!(button)).set_text(cx, &bot.name);
203 } else {
204 self.button(cx, ids!(button))
205 .set_text(cx, "Choose an AI assistant");
206 }
207 } else {
208 self.button(cx, ids!(button))
209 .set_text(cx, "Choose an AI assistant");
210 }
211 }
212
213 if let Some(controller) = &self.chat_controller
215 && let Some(mut list) = self
216 .widget(cx, ids!(options.list_container.list))
217 .borrow_mut::<ModelSelectorList>()
218 && Arc::as_ptr(controller)
219 != list
220 .chat_controller
221 .as_ref()
222 .map(Arc::as_ptr)
223 .unwrap_or(std::ptr::null())
224 {
225 {
226 list.chat_controller = Some(controller.clone());
227 }
228 }
229
230 self.view.draw_walk(cx, scope, walk)
231 }
232}
233
234impl WidgetMatchEvent for ModelSelector {
235 fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) {
236 if let Some(text) = self
238 .text_input(cx, ids!(options.search_container.search_input))
239 .changed(actions)
240 {
241 if let Some(mut list) = self
242 .widget(cx, ids!(options.list_container.list))
243 .borrow_mut::<ModelSelectorList>()
244 {
245 list.search_filter = text;
246 list.items.clear();
247 list.total_height = None;
248 }
249 }
250
251 let list_widget = self.widget(cx, ids!(options.list_container.list));
255 for action in actions {
256 let Some(action) = action.as_widget_action() else {
257 continue;
258 };
259
260 if action.widget_uid != list_widget.widget_uid() {
261 continue;
262 }
263
264 match action.cast() {
265 ModelSelectorItemAction::BotSelected(bot_id) => {
266 if let Some(controller) = &self.chat_controller {
267 controller
268 .lock()
269 .unwrap()
270 .dispatch_mutation(ChatStateMutation::SetBotId(Some(bot_id)));
271 }
272
273 self.button(cx, ids!(button)).reset_hover(cx);
274 self.close_modal(cx);
275 self.clear_search(cx);
276 self.redraw(cx);
277 }
278 _ => {}
279 }
280 }
281 }
282}
283
284impl ModelSelector {
285 fn open_modal(&mut self, cx: &mut Cx) {
286 self.open = true;
287
288 let button_rect = self.button(cx, ids!(button)).area().rect(cx);
289 let is_desktop = cx.display_context.is_desktop();
290
291 if is_desktop {
292 let padding = Inset {
293 top: 20.0,
294 left: 10.0,
295 right: 10.0,
296 bottom: 10.0,
297 };
298 let mut content = self.view(cx, ids!(modal.content));
299 script_apply_eval!(cx, content, {
300 width: 400
301 padding: #(padding)
302 });
303
304 let anchor = DVec2 {
305 x: button_rect.pos.x,
306 y: button_rect.pos.y,
307 };
308 self.moly_modal(cx, ids!(modal))
309 .open_as_popup_above(cx, anchor, 5.0);
310 } else {
311 let fill = Size::fill();
312 let mut content = self.view(cx, ids!(modal.content));
313 script_apply_eval!(cx, content, {
314 width: #(fill)
315 padding: 0
316 });
317
318 self.moly_modal(cx, ids!(modal)).open_as_bottom_sheet(cx);
319 }
320 }
321
322 fn close_modal(&mut self, cx: &mut Cx) {
323 self.open = false;
324 self.moly_modal(cx, ids!(modal)).close(cx);
325 }
326
327 fn clear_search(&mut self, cx: &mut Cx) {
328 if let Some(mut list) = self
329 .widget(cx, ids!(options.list_container.list))
330 .borrow_mut::<ModelSelectorList>()
331 {
332 list.search_filter.clear();
333 list.items.clear();
334 list.total_height = None;
335 }
336 self.text_input(cx, ids!(options.search_container.search_input))
337 .set_text(cx, "");
338 self.redraw(cx);
339 }
340}
341
342impl ModelSelectorRef {
343 pub fn set_chat_controller(&mut self, controller: Option<Arc<Mutex<ChatController>>>) {
345 if let Some(mut inner) = self.borrow_mut() {
346 inner.chat_controller = controller;
347 }
348 }
349
350 pub fn set_grouping<F>(&mut self, cx: &Cx, grouping: F)
357 where
358 F: Fn(&Bot) -> BotGroup + 'static,
359 {
360 if let Some(inner) = self.borrow_mut() {
361 if let Some(mut list) = inner
362 .widget(cx, ids!(options.list_container.list))
363 .borrow_mut::<ModelSelectorList>()
364 {
365 list.grouping = Box::new(grouping);
366 }
367 }
368 }
369}
370
371pub fn default_grouping(bot: &Bot) -> BotGroup {
373 BotGroup {
374 id: "all".to_string(),
375 label: "All".to_string(),
376 icon: Some(bot.avatar.clone()),
377 }
378}
379
380#[derive(Clone, Debug)]
387pub struct BotGroup {
388 pub id: String,
390 pub label: String,
392 pub icon: Option<EntityAvatar>,
394}