moly_kit/widgets/
chat.rs

1use makepad_widgets::defer_with_redraw::DeferWithRedraw;
2use makepad_widgets::*;
3use std::cell::{Ref, RefMut};
4use std::sync::{Arc, Mutex};
5
6use crate::aitk::utils::tool::display_name_from_namespaced;
7use crate::prelude::*;
8use crate::utils::makepad::events::EventExt;
9use crate::widgets::stt_input::*;
10
11// Re-export type needed to configure STT.
12pub use crate::widgets::stt_input::SttUtility;
13
14script_mod!(
15    use mod.prelude.widgets.*
16    use mod.widgets.*
17
18    mod.widgets.ChatBase = #(Chat::register_widget(vm))
19    mod.widgets.Chat = set_type_default() do mod.widgets.ChatBase {
20        flow: Down
21        messages := Messages {}
22        prompt := PromptInput {}
23        stt_input := SttInput { visible: false }
24
25        View {
26            width: Fill, height: Fit
27            flow: Overlay
28
29            audio_modal := MolyModal {
30                dismiss_on_focus_lost: false
31                content: RealtimeContent {}
32            }
33        }
34    }
35);
36
37/// A batteries-included chat to implement chatbots.
38#[derive(Script, ScriptHook, Widget)]
39pub struct Chat {
40    #[deref]
41    deref: View,
42
43    #[rust]
44    chat_controller: Option<Arc<Mutex<ChatController>>>,
45
46    /// Toggles response streaming on or off. Default is on.
47    // TODO: Implement this.
48    #[live(true)]
49    pub stream: bool,
50
51    #[rust]
52    plugin_id: Option<ChatControllerPluginRegistrationId>,
53}
54
55impl Widget for Chat {
56    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
57        // Handle audio devices setup
58        if let Event::AudioDevices(devices) = event {
59            let input = devices.default_input();
60            if !input.is_empty() {
61                cx.use_audio_inputs(&input);
62            }
63        }
64
65        self.ui_runner().handle(cx, event, scope, self);
66        self.deref.handle_event(cx, event, scope);
67
68        self.handle_messages(cx, event);
69        self.handle_prompt_input(cx, event);
70        self.handle_stt_input_actions(cx, event);
71        self.handle_realtime(cx);
72        self.handle_modal_dismissal(cx, event);
73    }
74
75    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
76        let has_stt = self.stt_input_ref(cx).read().stt_utility().is_some();
77        self.prompt_input_ref(cx)
78            .write()
79            .set_stt_visible(cx, has_stt);
80
81        self.deref.draw_walk(cx, scope, walk)
82    }
83}
84
85impl Chat {
86    /// Getter to the underlying [PromptInputRef] independent of its id.
87    pub fn prompt_input_ref(&self, cx: &Cx) -> PromptInputRef {
88        self.prompt_input(cx, ids!(prompt))
89    }
90
91    /// Getter to the underlying [MessagesRef] independent of its id.
92    pub fn messages_ref(&self, cx: &Cx) -> MessagesRef {
93        self.messages(cx, ids!(messages))
94    }
95
96    /// Getter to the underlying [SttInputRef] independent of its id.
97    pub fn stt_input_ref(&self, cx: &Cx) -> SttInputRef {
98        self.stt_input(cx, ids!(stt_input))
99    }
100
101    /// Configures the STT utility to be used for speech-to-text.
102    pub fn set_stt_utility(&mut self, cx: &Cx, utility: Option<SttUtility>) {
103        self.stt_input_ref(cx).write().set_stt_utility(utility);
104    }
105
106    /// Returns the current STT utility, if any, as a clone.
107    pub fn stt_utility(&self, cx: &Cx) -> Option<SttUtility> {
108        self.stt_input_ref(cx).read().stt_utility().cloned()
109    }
110
111    fn handle_prompt_input(&mut self, cx: &mut Cx, event: &Event) {
112        let submitted = self
113            .prompt_input_ref(cx)
114            .read()
115            .submitted(cx, event.actions());
116        if submitted {
117            self.handle_submit(cx);
118        }
119
120        let call_pressed = self
121            .prompt_input_ref(cx)
122            .read()
123            .call_pressed(cx, event.actions());
124        if call_pressed {
125            self.handle_call(cx);
126        }
127
128        let stt_pressed = self
129            .prompt_input_ref(cx)
130            .read()
131            .stt_pressed(cx, event.actions());
132        if stt_pressed {
133            self.prompt_input_ref(cx).set_visible(cx, false);
134            self.stt_input_ref(cx).set_visible(cx, true);
135            self.stt_input_ref(cx).write().start_recording(cx);
136            self.redraw(cx);
137        }
138    }
139
140    fn handle_stt_input_actions(&mut self, cx: &mut Cx, event: &Event) {
141        let transcription = self.stt_input_ref(cx).read().transcribed(event.actions());
142
143        if let Some(transcription) = transcription {
144            self.stt_input_ref(cx).set_visible(cx, false);
145            self.prompt_input_ref(cx).set_visible(cx, true);
146
147            let mut text = self.prompt_input_ref(cx).text();
148            if let Some(last) = text.as_bytes().last()
149                && *last != b' '
150            {
151                text.push(' ');
152            }
153            text.push_str(&transcription);
154            self.prompt_input_ref(cx).set_text(cx, &text);
155
156            self.prompt_input_ref(cx).redraw(cx);
157        }
158
159        let cancelled = self.stt_input_ref(cx).read().cancelled(event.actions());
160        if cancelled {
161            self.stt_input_ref(cx).set_visible(cx, false);
162            self.prompt_input_ref(cx).set_visible(cx, true);
163            self.prompt_input_ref(cx).redraw(cx);
164        }
165    }
166
167    fn handle_realtime(&mut self, cx: &mut Cx) {
168        if self.realtime(cx, ids!(realtime)).connection_requested()
169            && self
170                .chat_controller
171                .as_ref()
172                .map(|c| c.lock().unwrap().state().bot_id.is_some())
173                .unwrap_or(false)
174        {
175            self.chat_controller
176                .as_mut()
177                .unwrap()
178                .lock()
179                .unwrap()
180                .dispatch_task(ChatTask::Send);
181        }
182    }
183
184    fn handle_modal_dismissal(&mut self, cx: &mut Cx, event: &Event) {
185        for action in event.actions() {
186            if let RealtimeModalAction::DismissModal = action.cast() {
187                self.moly_modal(cx, ids!(audio_modal)).close(cx);
188            }
189        }
190
191        if self
192            .moly_modal(cx, ids!(audio_modal))
193            .dismissed(event.actions())
194        {
195            let mut conversation_messages = self
196                .realtime(cx, ids!(realtime))
197                .take_conversation_messages();
198
199            self.realtime(cx, ids!(realtime)).reset_state(cx);
200
201            if !conversation_messages.is_empty() {
202                let chat_controller = self.chat_controller.clone().unwrap();
203
204                let mut all_messages = chat_controller.lock().unwrap().state().messages.clone();
205
206                let system_message = Message {
207                    from: EntityId::App,
208                    content: MessageContent {
209                        text: "Voice call started.".to_string(),
210                        ..Default::default()
211                    },
212                    ..Default::default()
213                };
214                conversation_messages.insert(0, system_message);
215
216                let system_message = Message {
217                    from: EntityId::App,
218                    content: MessageContent {
219                        text: "Voice call ended.".to_string(),
220                        ..Default::default()
221                    },
222                    ..Default::default()
223                };
224                conversation_messages.push(system_message);
225
226                all_messages.extend(conversation_messages);
227                chat_controller
228                    .lock()
229                    .unwrap()
230                    .dispatch_mutation(VecMutation::Set(all_messages));
231
232                self.messages_ref(cx).write().instant_scroll_to_bottom(cx);
233            }
234        }
235    }
236
237    fn handle_capabilities(&mut self, cx: &mut Cx) {
238        let capabilities = self.chat_controller.as_ref().and_then(|controller| {
239            let lock = controller.lock().unwrap();
240            let bot_id = lock.state().bot_id.as_ref()?;
241            lock.state()
242                .get_bot(bot_id)
243                .map(|bot| bot.capabilities.clone())
244        });
245
246        self.prompt_input_ref(cx)
247            .write()
248            .set_bot_capabilities(cx, capabilities);
249    }
250
251    fn handle_messages(&mut self, cx: &mut Cx, event: &Event) {
252        for action in event.actions() {
253            let Some(action) = action.as_widget_action() else {
254                continue;
255            };
256
257            if action.widget_uid != self.messages_ref(cx).widget_uid() {
258                continue;
259            }
260
261            let chat_controller = self.chat_controller.clone().unwrap();
262
263            match action.cast::<MessagesAction>() {
264                MessagesAction::Delete(index) => chat_controller
265                    .lock()
266                    .unwrap()
267                    .dispatch_mutation(VecMutation::<Message>::RemoveOne(index)),
268                MessagesAction::Copy(index) => {
269                    let lock = chat_controller.lock().unwrap();
270                    let text = &lock.state().messages[index].content.text;
271                    cx.copy_to_clipboard(text);
272                }
273                MessagesAction::EditSave(index) => {
274                    let text = self
275                        .messages_ref(cx)
276                        .read()
277                        .current_editor_text(cx)
278                        .expect("no editor text");
279
280                    self.messages_ref(cx)
281                        .write()
282                        .set_message_editor_visibility(index, false);
283
284                    let mut lock = chat_controller.lock().unwrap();
285
286                    let mutation =
287                        VecMutation::update_with(&lock.state().messages, index, |message| {
288                            message.update_content(move |content| {
289                                content.text = text;
290                            });
291                        });
292
293                    lock.dispatch_mutation(mutation);
294                }
295                MessagesAction::EditRegenerate(index) => {
296                    let mut messages =
297                        chat_controller.lock().unwrap().state().messages[0..=index].to_vec();
298
299                    let text = self
300                        .messages_ref(cx)
301                        .read()
302                        .current_editor_text(cx)
303                        .expect("no editor text");
304
305                    self.messages_ref(cx)
306                        .write()
307                        .set_message_editor_visibility(index, false);
308
309                    messages[index].update_content(|content| {
310                        content.text = text;
311                    });
312
313                    chat_controller
314                        .lock()
315                        .unwrap()
316                        .dispatch_mutation(VecMutation::Set(messages));
317
318                    if self
319                        .chat_controller
320                        .as_ref()
321                        .map(|c| c.lock().unwrap().state().bot_id.is_some())
322                        .unwrap_or(false)
323                    {
324                        chat_controller
325                            .lock()
326                            .unwrap()
327                            .dispatch_task(ChatTask::Send);
328                    }
329                }
330                MessagesAction::ToolApprove(index) => {
331                    let mut lock = chat_controller.lock().unwrap();
332
333                    let mut updated_message = lock.state().messages[index].clone();
334
335                    for tool_call in &mut updated_message.content.tool_calls {
336                        tool_call.permission_status = ToolCallPermissionStatus::Approved;
337                    }
338
339                    lock.dispatch_mutation(VecMutation::Update(index, updated_message));
340
341                    let tools = lock.state().messages[index].content.tool_calls.clone();
342                    let bot_id = lock.state().bot_id.clone();
343                    lock.dispatch_task(ChatTask::Execute(tools, bot_id));
344                }
345                MessagesAction::ToolDeny(index) => {
346                    let mut lock = chat_controller.lock().unwrap();
347
348                    let mut updated_message = lock.state().messages[index].clone();
349
350                    updated_message.update_content(|content| {
351                        for tool_call in &mut content.tool_calls {
352                            tool_call.permission_status = ToolCallPermissionStatus::Denied;
353                        }
354                    });
355
356                    lock.dispatch_mutation(VecMutation::Update(index, updated_message));
357
358                    let tool_results: Vec<ToolResult> = lock.state().messages[index]
359                        .content
360                        .tool_calls
361                        .iter()
362                        .map(|tc| {
363                            let display_name = display_name_from_namespaced(&tc.name);
364                            ToolResult {
365                                tool_call_id: tc.id.clone(),
366                                content: format!(
367                                    "Tool execution was denied by the user. \
368                                     Tool '{}' was not executed.",
369                                    display_name
370                                ),
371                                is_error: true,
372                            }
373                        })
374                        .collect();
375
376                    lock.dispatch_mutation(VecMutation::Push(Message {
377                        from: EntityId::Tool,
378                        content: MessageContent {
379                            text: "\u{1f6ab} Tool execution was denied \
380                                   by the user."
381                                .to_string(),
382                            tool_results,
383                            ..Default::default()
384                        },
385                        ..Default::default()
386                    }));
387                }
388                MessagesAction::None => {}
389            }
390        }
391    }
392
393    fn handle_submit(&mut self, cx: &mut Cx) {
394        let mut prompt = self.prompt_input_ref(cx);
395        let chat_controller = self.chat_controller.clone().unwrap();
396
397        if prompt.read().has_send_task()
398            && self
399                .chat_controller
400                .as_ref()
401                .map(|c| c.lock().unwrap().state().bot_id.is_some())
402                .unwrap_or(false)
403        {
404            let text = prompt.text();
405            let attachments = prompt
406                .read()
407                .attachment_list_ref(cx)
408                .read()
409                .attachments
410                .clone();
411
412            if !text.is_empty() || !attachments.is_empty() {
413                chat_controller
414                    .lock()
415                    .unwrap()
416                    .dispatch_mutation(VecMutation::Push(Message {
417                        from: EntityId::User,
418                        content: MessageContent {
419                            text,
420                            attachments,
421                            ..Default::default()
422                        },
423                        ..Default::default()
424                    }));
425            }
426
427            prompt.write().reset(cx);
428            chat_controller
429                .lock()
430                .unwrap()
431                .dispatch_task(ChatTask::Send);
432        } else if prompt.read().has_stop_task() {
433            chat_controller
434                .lock()
435                .unwrap()
436                .dispatch_task(ChatTask::Stop);
437        }
438    }
439
440    fn handle_call(&mut self, _cx: &mut Cx) {
441        if self
442            .chat_controller
443            .as_ref()
444            .map(|c| c.lock().unwrap().state().bot_id.is_some())
445            .unwrap_or(false)
446        {
447            self.chat_controller
448                .as_mut()
449                .unwrap()
450                .lock()
451                .unwrap()
452                .dispatch_task(ChatTask::Send);
453        }
454    }
455
456    /// Returns true if the chat is currently streaming.
457    pub fn is_streaming(&self) -> bool {
458        self.chat_controller
459            .as_ref()
460            .unwrap()
461            .lock()
462            .unwrap()
463            .state()
464            .is_streaming
465    }
466
467    /// Sets the chat controller for this chat widget.
468    pub fn set_chat_controller(
469        &mut self,
470        cx: &mut Cx,
471        chat_controller: Option<Arc<Mutex<ChatController>>>,
472    ) {
473        if self.chat_controller.as_ref().map(Arc::as_ptr)
474            == chat_controller.as_ref().map(Arc::as_ptr)
475        {
476            return;
477        }
478
479        self.unlink_current_controller();
480        self.chat_controller = chat_controller;
481
482        self.messages_ref(cx).write().chat_controller = self.chat_controller.clone();
483        self.realtime(cx, ids!(realtime))
484            .set_chat_controller(self.chat_controller.clone());
485        self.prompt_input_ref(cx)
486            .write()
487            .set_chat_controller(cx, self.chat_controller.clone());
488
489        if let Some(controller) = self.chat_controller.as_ref() {
490            let mut guard = controller.lock().unwrap();
491
492            let plugin = Plugin::new(self.ui_runner());
493            self.plugin_id = Some(guard.append_plugin(plugin));
494        }
495    }
496
497    /// Returns a reference to the chat controller, if set.
498    pub fn chat_controller(&self) -> Option<&Arc<Mutex<ChatController>>> {
499        self.chat_controller.as_ref()
500    }
501
502    fn unlink_current_controller(&mut self) {
503        if let Some(plugin_id) = self.plugin_id {
504            if let Some(controller) = self.chat_controller.as_ref() {
505                controller.lock().unwrap().remove_plugin(plugin_id);
506            }
507        }
508
509        self.chat_controller = None;
510        self.plugin_id = None;
511    }
512
513    fn handle_streaming_start(&mut self, cx: &mut Cx) {
514        self.prompt_input_ref(cx).write().set_stop();
515        self.messages_ref(cx).write().animated_scroll_to_bottom(cx);
516        self.redraw(cx);
517    }
518
519    fn handle_streaming_end(&mut self, cx: &mut Cx) {
520        self.prompt_input_ref(cx).write().set_send();
521        self.redraw(cx);
522    }
523}
524
525// TODO: Since `ChatRef` is generated by a macro, I can't document this
526// to give these functions better visibility from the module view.
527impl ChatRef {
528    /// Immutable access to the underlying [Chat].
529    ///
530    /// Panics if the widget reference is empty or if it's already borrowed.
531    pub fn read(&self) -> Ref<'_, Chat> {
532        self.borrow().unwrap()
533    }
534
535    /// Mutable access to the underlying [Chat].
536    ///
537    /// Panics if the widget reference is empty or if it's already borrowed.
538    pub fn write(&mut self) -> RefMut<'_, Chat> {
539        self.borrow_mut().unwrap()
540    }
541
542    /// Immutable reader to the underlying [Chat].
543    ///
544    /// Panics if the widget reference is empty or if it's already borrowed.
545    pub fn read_with<R>(&self, f: impl FnOnce(&Chat) -> R) -> R {
546        f(&*self.read())
547    }
548
549    /// Mutable writer to the underlying [Chat].
550    ///
551    /// Panics if the widget reference is empty or if it's already borrowed.
552    pub fn write_with<R>(&mut self, f: impl FnOnce(&mut Chat) -> R) -> R {
553        f(&mut *self.write())
554    }
555}
556
557impl Drop for Chat {
558    fn drop(&mut self) {
559        self.unlink_current_controller();
560    }
561}
562
563struct Plugin {
564    ui: UiRunner<Chat>,
565}
566
567impl Plugin {
568    fn new(ui: UiRunner<Chat>) -> Self {
569        Self { ui }
570    }
571}
572
573impl ChatControllerPlugin for Plugin {
574    fn on_state_ready(&mut self, _state: &ChatState, mutations: &[ChatStateMutation]) {
575        for mutation in mutations {
576            match mutation {
577                ChatStateMutation::SetIsStreaming(true) => {
578                    self.ui.defer(|chat, cx, _| {
579                        chat.handle_streaming_start(cx);
580                    });
581                }
582                ChatStateMutation::SetIsStreaming(false) => {
583                    self.ui.defer(|chat, cx, _| {
584                        chat.handle_streaming_end(cx);
585                    });
586                }
587                ChatStateMutation::MutateBots(_) => {
588                    self.ui.defer(|chat, cx, _| {
589                        if let Some(controller) = &chat.chat_controller {
590                            let mut lock = controller.lock().unwrap();
591                            if let Some(bot_id) = lock.state().bot_id.clone() {
592                                let bot_still_available =
593                                    lock.state().bots.iter().any(|b| &b.id == &bot_id);
594                                if !bot_still_available {
595                                    lock.dispatch_mutation(ChatStateMutation::SetBotId(None));
596                                }
597                            }
598                        }
599
600                        chat.handle_capabilities(cx);
601                    });
602                }
603                ChatStateMutation::SetBotId(_bot_id) => {
604                    self.ui.defer(move |chat, cx, _| {
605                        chat.handle_capabilities(cx);
606                    });
607                }
608                _ => {}
609            }
610        }
611
612        // Always redraw on state change.
613        self.ui.defer_with_redraw(move |_, _, _| {});
614    }
615
616    fn on_upgrade(&mut self, upgrade: Upgrade, bot_id: &BotId) -> Option<Upgrade> {
617        match upgrade {
618            Upgrade::Realtime(channel) => {
619                let entity_id = EntityId::Bot(bot_id.clone());
620                self.ui.defer(move |me, cx, _| {
621                    me.handle_streaming_end(cx);
622
623                    let mut realtime = me.realtime(cx, ids!(realtime));
624                    realtime.set_bot_entity_id(cx, entity_id);
625                    realtime.set_realtime_channel(channel.clone());
626
627                    let modal = me.moly_modal(cx, ids!(audio_modal));
628                    modal.open_as_dialog(cx);
629                });
630                None
631            }
632            #[allow(unreachable_patterns)]
633            upgrade => Some(upgrade),
634        }
635    }
636}