moly_kit/widgets/
prompt_input.rs

1use makepad_widgets::*;
2use std::cell::{Ref, RefMut};
3
4#[allow(unused)]
5use crate::{
6    Attachment,
7    protocol::{BotCapabilities, BotCapability},
8    utils::makepad::EventExt,
9    widgets::attachment_list::{AttachmentListRef, AttachmentListWidgetExt},
10};
11
12live_design! {
13    use link::theme::*;
14    use link::widgets::*;
15    use link::moly_kit_theme::*;
16    use link::shaders::*;
17
18    use crate::widgets::attachment_list::*;
19
20    pub PromptInput = {{PromptInput}} <CommandTextInput> {
21        send_icon: dep("crate://self/resources/send.svg"),
22        stop_icon: dep("crate://self/resources/stop.svg"),
23
24        height: 80
25        persistent = {
26            height: Fill
27            padding: {top: 8, bottom: 6, left: 4, right: 10}
28            draw_bg: {
29                color: #fff,
30                border_radius: 10.0,
31                border_color: #D0D5DD,
32                border_size: 1.0,
33            }
34            center = {
35                height: Fill
36                left = {
37                    attach = <Button> {
38                        visible: false
39                        text: "",
40                        draw_text: {
41                            text_style: <THEME_FONT_ICONS> {
42                                font_size: 16.
43                            }
44                            color: #000,
45                            color_hover: #000,
46                            color_focus: #000
47                            color_down: #000
48                        }
49                        draw_bg: {
50                            color_down: #0000
51                            border_radius: 7.
52                            border_size: 0.
53                        }
54                    }
55                }
56                text_input = {
57                    height: Fill
58                    empty_text: "Start typing...",
59                    draw_bg: {
60                        fn pixel(self) -> vec4 {
61                            return vec4(0.);
62                        }
63                    }
64                    draw_text: {
65                        color: #000
66                        color_hover: #000
67                        color_focus: #000
68                        color_empty: #98A2B3
69                        color_empty_focus: #98A2B3
70                        text_style: {font_size: 11}
71                    }
72                    draw_selection: {
73                        color: #d9e7e9
74                        color_hover: #d9e7e9
75                        color_focus: #d9e7e9
76                    }
77                    draw_cursor: {
78                        fn pixel(self) -> vec4 {
79                            return #bbb;
80                        }
81                    }
82                }
83                right = {
84                    align: {x: 0.5, y: 0.5}
85                    spacing: 5
86                    audio = <Button> {
87                        visible: false
88                        text: ""
89                        draw_text: {
90                            text_style: <THEME_FONT_ICONS> {
91                                font_size: 16.
92                            }
93                            color: #000,
94                            color_hover: #000,
95                            color_focus: #000
96                            color_down: #000
97                        }
98                        draw_bg: {
99                            color_down: #0000
100                            border_radius: 7.
101                            border_size: 0.
102                        }
103                    }
104                    submit = <Button> {
105                        width: 28,
106                        height: 28,
107                        padding: {right: 2},
108                        margin: {bottom: 2},
109
110                        draw_icon: {
111                            color: #fff
112                        }
113
114                        draw_bg: {
115                            fn get_color(self) -> vec4 {
116                                if self.enabled == 0.0 {
117                                    return #D0D5DD;
118                                }
119
120                                return #000;
121                            }
122
123                            fn pixel(self) -> vec4 {
124                                let sdf = Sdf2d::viewport(self.pos * self.rect_size);
125                                let center = self.rect_size * 0.5;
126                                let radius = min(self.rect_size.x, self.rect_size.y) * 0.5;
127
128                                sdf.circle(center.x, center.y, radius);
129                                sdf.fill_keep(self.get_color());
130
131                                return sdf.result
132                            }
133                        }
134                        icon_walk: {
135                            width: 12,
136                            height: 12
137                            margin: {top: 0, left: 2},
138                        }
139                    }
140                }
141            }
142            bottom = {
143                attachments = <DenseAttachmentList> {
144                    wrapper = {
145                        margin: { top: 6 }
146                    }
147                }
148            }
149        }
150    }
151}
152
153#[derive(Default, Copy, Clone, PartialEq)]
154pub enum Task {
155    #[default]
156    Send,
157    Stop,
158}
159
160#[derive(Default, Copy, Clone, PartialEq)]
161pub enum Interactivity {
162    #[default]
163    Enabled,
164    Disabled,
165}
166
167/// A prepared text input for conversation with bots.
168///
169/// This is mostly a dummy widget. Prefer using and adapting [crate::widgets::chat::Chat] instead.
170#[derive(Live, Widget)]
171pub struct PromptInput {
172    #[deref]
173    deref: CommandTextInput,
174
175    /// Icon used by this widget when the task is set to [Task::Send].
176    #[live]
177    pub send_icon: LiveValue,
178
179    /// Icon used by this widget when the task is set to [Task::Stop].
180    #[live]
181    pub stop_icon: LiveValue,
182
183    /// If this widget should provoke sending a message or stopping the current response.
184    #[rust]
185    pub task: Task,
186
187    /// If this widget should be interactive or not.
188    #[rust]
189    pub interactivity: Interactivity,
190
191    /// Capabilities of the currently selected bot
192    #[rust]
193    pub bot_capabilities: Option<BotCapabilities>,
194}
195
196impl LiveHook for PromptInput {
197    #[allow(unused)]
198    fn after_new_from_doc(&mut self, cx: &mut Cx) {
199        self.update_button_visibility(cx);
200    }
201}
202
203impl Widget for PromptInput {
204    fn set_text(&mut self, cx: &mut Cx, v: &str) {
205        self.deref.set_text(cx, v);
206    }
207
208    fn text(&self) -> String {
209        self.deref.text()
210    }
211
212    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
213        self.deref.handle_event(cx, event, scope);
214        self.ui_runner().handle(cx, event, scope, self);
215
216        if self.button(id!(attach)).clicked(event.actions()) {
217            let ui = self.ui_runner();
218            Attachment::pick_multiple(move |result| match result {
219                Ok(attachments) => {
220                    ui.defer_with_redraw(move |me, _, _| {
221                        let mut list = me.attachment_list_ref();
222                        list.write().attachments.extend(attachments);
223                        list.write().on_tap(move |list, index| {
224                            list.attachments.remove(index);
225                        });
226                    });
227                }
228                Err(_) => {}
229            });
230        }
231    }
232
233    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
234        let button = self.button(id!(submit));
235
236        match self.task {
237            Task::Send => {
238                button.apply_over(
239                    cx,
240                    live! {
241                        draw_icon: {
242                            svg_file: (self.send_icon),
243                        }
244                    },
245                );
246            }
247            Task::Stop => {
248                button.apply_over(
249                    cx,
250                    live! {
251                        draw_icon: {
252                            svg_file: (self.stop_icon),
253                        }
254                    },
255                );
256            }
257        }
258
259        match self.interactivity {
260            Interactivity::Enabled => {
261                button.apply_over(
262                    cx,
263                    live! {
264                        draw_bg: {
265                            enabled: 1.0
266                        }
267                    },
268                );
269                button.set_enabled(cx, true);
270            }
271            Interactivity::Disabled => {
272                button.apply_over(
273                    cx,
274                    live! {
275                        draw_bg: {
276                            enabled: 0.0
277                        }
278                    },
279                );
280                button.set_enabled(cx, false);
281            }
282        }
283
284        self.deref.draw_walk(cx, scope, walk)
285    }
286}
287
288impl PromptInput {
289    /// Reset this prompt input erasing text, removing attachments, etc.
290    ///
291    /// Shadows the [`CommandTextInput::reset`] method.
292    pub fn reset(&mut self, cx: &mut Cx) {
293        self.deref.reset(cx);
294        self.attachment_list_ref().write().attachments.clear();
295    }
296
297    /// Check if the submit button or the return key was pressed.
298    ///
299    /// Note: To know what the button submission means, check [Self::task] or
300    /// the utility methods.
301    pub fn submitted(&self, actions: &Actions) -> bool {
302        let submit = self.button(id!(submit));
303        let input = self.text_input_ref();
304        (submit.clicked(actions) || input.returned(actions).is_some())
305            && self.interactivity == Interactivity::Enabled
306    }
307
308    pub fn call_pressed(&self, actions: &Actions) -> bool {
309        self.button(id!(audio)).clicked(actions)
310    }
311
312    /// Shorthand to check if [Self::task] is set to [Task::Send].
313    pub fn has_send_task(&self) -> bool {
314        self.task == Task::Send
315    }
316
317    /// Shorthand to check if [Self::task] is set to [Task::Stop].
318    pub fn has_stop_task(&self) -> bool {
319        self.task == Task::Stop
320    }
321
322    /// Allows submission.
323    pub fn enable(&mut self) {
324        self.interactivity = Interactivity::Enabled;
325    }
326
327    /// Disallows submission.
328    pub fn disable(&mut self) {
329        self.interactivity = Interactivity::Disabled;
330    }
331
332    /// Shorthand to set [Self::task] to [Task::Send].
333    pub fn set_send(&mut self) {
334        self.task = Task::Send;
335    }
336
337    /// Shorthand to set [Self::task] to [Task::Stop].
338    pub fn set_stop(&mut self) {
339        self.task = Task::Stop;
340    }
341
342    pub(crate) fn attachment_list_ref(&self) -> AttachmentListRef {
343        self.attachment_list(id!(attachments))
344    }
345
346    /// Set the capabilities of the currently selected bot
347    pub fn set_bot_capabilities(&mut self, cx: &mut Cx, capabilities: Option<BotCapabilities>) {
348        self.bot_capabilities = capabilities;
349        self.update_button_visibility(cx);
350    }
351
352    /// Update button visibility based on bot capabilities
353    fn update_button_visibility(&mut self, cx: &mut Cx) {
354        let supports_attachments = self
355            .bot_capabilities
356            .as_ref()
357            .map(|caps| caps.supports_attachments())
358            .unwrap_or(false);
359
360        let supports_realtime = self
361            .bot_capabilities
362            .as_ref()
363            .map(|caps| caps.supports_realtime())
364            .unwrap_or(false);
365
366        // Show attach button only if bot supports attachments AND we're on a supported platform
367        #[cfg(any(
368            target_os = "windows",
369            target_os = "macos",
370            target_os = "linux",
371            target_arch = "wasm32"
372        ))]
373        self.button(id!(attach))
374            .set_visible(cx, supports_attachments);
375
376        #[cfg(not(any(
377            target_os = "windows",
378            target_os = "macos",
379            target_os = "linux",
380            target_arch = "wasm32"
381        )))]
382        self.button(id!(attach)).set_visible(cx, false);
383
384        // Show audio/call button only if bot supports realtime, we're on a supported platform
385        // and realtime feature is enabled
386        #[cfg(not(target_arch = "wasm32"))]
387        #[cfg(feature = "realtime")]
388        self.button(id!(audio)).set_visible(cx, supports_realtime);
389
390        if supports_realtime {
391            self.interactivity = Interactivity::Disabled;
392            self.text_input_ref().set_is_read_only(cx, true);
393            self.text_input_ref()
394                .set_text(cx, "For realtime models, use the audio feature ->");
395            self.redraw(cx);
396        } else {
397            self.interactivity = Interactivity::Enabled;
398            self.text_input_ref().set_is_read_only(cx, false);
399            self.text_input_ref().set_text(cx, "");
400            self.redraw(cx);
401        }
402    }
403}
404
405impl PromptInputRef {
406    /// Immutable access to the underlying [[PromptInput]].
407    ///
408    /// Panics if the widget reference is empty or if it's already borrowed.
409    pub fn read(&self) -> Ref<'_, PromptInput> {
410        self.borrow().unwrap()
411    }
412
413    /// Mutable access to the underlying [[PromptInput]].
414    ///
415    /// Panics if the widget reference is empty or if it's already borrowed.
416    pub fn write(&mut self) -> RefMut<'_, PromptInput> {
417        self.borrow_mut().unwrap()
418    }
419
420    /// Immutable reader to the underlying [[PromptInput]].
421    ///
422    /// Panics if the widget reference is empty or if it's already borrowed.
423    pub fn read_with<R>(&self, f: impl FnOnce(&PromptInput) -> R) -> R {
424        f(&*self.read())
425    }
426
427    /// Mutable writer to the underlying [[PromptInput]].
428    ///
429    /// Panics if the widget reference is empty or if it's already borrowed.
430    pub fn write_with<R>(&mut self, f: impl FnOnce(&mut PromptInput) -> R) -> R {
431        f(&mut *self.write())
432    }
433}