1use makepad_widgets::defer_with_redraw::DeferWithRedraw;
2use makepad_widgets::*;
3use std::cell::{Ref, RefMut};
4
5#[allow(unused)]
6use crate::{
7 aitk::protocol::*,
8 utils::makepad::events::EventExt,
9 widgets::attachment_list::{AttachmentListRef, AttachmentListWidgetExt},
10};
11
12script_mod! {
13 use mod.prelude.widgets.*
14
15 let SubmitButton = Button {
16 width: 28,
17 height: 28,
18 padding: 0
19 margin: Inset { bottom: 2 },
20 align: Align { x: 0.5, y: 0.5 }
21
22 draw_text +: {
23 text_style: theme.font_icons { font_size: 11 }
24 color: #fff
25 color_hover: #fff
26 color_focus: #fff
27 color_down: #fff
28 }
29
30 draw_bg +: {
31 get_color: fn() {
32 if self.disabled == 1.0 {
33 return #xD0D5DD;
34 }
35 return #000;
36 }
37
38 pixel: fn() {
39 let sdf = Sdf2d.viewport(self.pos * self.rect_size)
40 let center = self.rect_size * 0.5
41 let radius = min(self.rect_size.x self.rect_size.y) * 0.5
42
43 sdf.circle(center.x center.y radius)
44 sdf.fill_keep(self.get_color())
45
46 return sdf.result
47 }
48 }
49 }
50
51 let SendButton = SubmitButton {
52 text: "\u{f062}" }
54
55 let StopButton = SubmitButton {
56 visible: false
57 text: "\u{f04d}" draw_text +: {
59 text_style +: { font_size: 8.5 }
60 }
61 }
62
63 let AttachButton = Button {
64 visible: false
65 text: "\u{f0c6}" width: Fit,
67 height: Fit,
68 padding: Inset { left: 8, right: 8, top: 6, bottom: 6 }
69 draw_text +: {
70 text_style: theme.font_icons { font_size: 13. }
71 color: #333,
72 color_hover: #111,
73 color_focus: #111
74 color_down: #000
75 }
76 draw_bg +: {
77 color_down: #0000
78 border_radius: 7.
79 border_size: 0.
80 color_hover: #xf2
81 }
82 }
83
84 let AudioButton = Button {
85 visible: false
86 width: 28, height: 28
87 text: "\u{f590}" draw_text +: {
89 text_style: theme.font_icons { font_size: 13. }
90 color: #333,
91 color_hover: #111,
92 color_focus: #111
93 color_down: #000
94 }
95 draw_bg +: {
96 color_down: #0000
97 border_radius: 7.
98 border_size: 0.
99 }
100 }
101
102 let SttButton = Button {
103 visible: false
104 width: 28, height: 28
105 text: "\u{f130}" draw_text +: {
107 text_style: theme.font_icons { font_size: 13. }
108 color: #333,
109 color_hover: #111,
110 color_focus: #111
111 color_down: #000
112 }
113 draw_bg +: {
114 color_down: #0000
115 border_radius: 7.
116 border_size: 0.
117 }
118 }
119
120 let SendControls = View {
121 width: Fit, height: Fit
122 align: Align { x: 0.5, y: 0.5 }
123 spacing: 10
124 stt := SttButton {}
125 audio := AudioButton {}
126 send := SendButton {}
127 stop := StopButton {}
128 }
129
130 mod.widgets.PromptInputBase = #(PromptInput::register_widget(vm))
131 mod.widgets.PromptInput = set_type_default() do mod.widgets.PromptInputBase {
132 height: Fit { max: FitBound.Abs(350) }
133 flow: Down
134
135 persistent := RoundedView {
136 height: Fit
137 flow: Down
138 padding: Inset { top: 10, bottom: 10, left: 10, right: 10 }
139 draw_bg +: {
140 color: #fff,
141 border_radius: 10.0,
142 border_color: #xD0D5DD,
143 border_size: 1.0,
144 }
145 top := View {
146 height: Fit
147 attachments := mod.widgets.DenseAttachmentList {}
148 }
149 center := View {
150 height: Fit
151 text_input := TextInput {
152 height: Fit {
153 min: FitBound.Abs(35)
154 max: FitBound.Abs(180)
155 }
156 width: Fill
157 empty_text: "Start typing...",
158 draw_bg +: {
159 pixel: fn() {
160 return vec4(0.)
161 }
162 }
163 draw_text +: {
164 color: #000
165 color_hover: #000
166 color_focus: #000
167 color_empty: #x98A2B3
168 color_empty_focus: #x98A2B3
169 color_empty_hover: #x98A2B3
170 text_style +: { font_size: 11 }
171 }
172 draw_selection +: {
173 color: #xd9e7e999
174 color_hover: #xd9e7e999
175 color_focus: #xd9e7e999
176 }
177 draw_cursor +: {
178 color: #000
179 }
180 }
181 right := View {
182 width: Fit, height: Fit
183 }
184 }
185 bottom := View {
186 height: Fit
187 left := View {
188 width: Fit, height: Fit
189 align: Align { x: 0.0, y: 0.5 }
190 attach := AttachButton {}
191 model_selector := mod.widgets.ModelSelector {}
192 }
193 width: Fill, height: Fit
194 separator := View { width: Fill, height: 1 }
195 SendControls {}
196 }
197 }
198 }
199}
200
201#[derive(Default, Copy, Clone, PartialEq)]
202pub enum Task {
203 #[default]
204 Send,
205 Stop,
206}
207
208#[derive(Default, Copy, Clone, PartialEq)]
209pub enum Interactivity {
210 #[default]
211 Enabled,
212 Disabled,
213}
214
215#[derive(Script, Widget)]
219pub struct PromptInput {
220 #[deref]
221 pub deref: View,
222
223 #[live(String::from("Start typing..."))]
225 pub empty_text: String,
226
227 #[live(String::from("For realtime models, use the audio feature ->"))]
229 pub realtime_empty_text: String,
230
231 #[rust]
233 pub task: Task,
234
235 #[rust]
237 pub interactivity: Interactivity,
238
239 #[rust]
241 pub bot_capabilities: Option<BotCapabilities>,
242}
243
244impl ScriptHook for PromptInput {
245 fn on_after_new(&mut self, _vm: &mut ScriptVm) {
246 }
249}
250
251impl Widget for PromptInput {
252 fn set_text(&mut self, cx: &mut Cx, v: &str) {
253 self.text_input(cx, ids!(text_input)).set_text(cx, v);
254 }
255
256 fn text(&self) -> String {
257 self.child_by_path(&[id!(text_input)])
258 .borrow::<TextInput>()
259 .map(|ti| ti.text())
260 .unwrap_or_else(|| {
261 error!("PromptInput::text(): text_input child not found or wrong type");
262 String::new()
263 })
264 }
265
266 fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
267 self.deref.handle_event(cx, event, scope);
268 self.ui_runner().handle(cx, event, scope, self);
269
270 if self.button(cx, ids!(attach)).clicked(event.actions()) {
271 let ui = self.ui_runner();
272 Attachment::pick_multiple(move |result| match result {
273 Ok(attachments) => {
274 ui.defer_with_redraw(move |me: &mut PromptInput, cx, _| {
275 let mut list = me.attachment_list_ref(cx);
276 list.write().attachments.extend(attachments);
277 list.write().on_tap(move |list, index| {
278 list.attachments.remove(index);
279 });
280 });
281 }
282 Err(_) => {}
283 });
284 }
285 }
286
287 fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
288 let supports_realtime = self
289 .bot_capabilities
290 .as_ref()
291 .map(|caps| caps.has_capability(&BotCapability::AudioCall))
292 .unwrap_or(false);
293
294 let supports_attachments = self
295 .bot_capabilities
296 .as_ref()
297 .map(|caps| caps.has_capability(&BotCapability::AttachmentInput))
298 .unwrap_or(false);
299
300 #[cfg(any(
302 target_os = "windows",
303 target_os = "macos",
304 target_os = "linux",
305 target_arch = "wasm32"
306 ))]
307 self.button(cx, ids!(attach))
308 .set_visible(cx, supports_attachments);
309
310 #[cfg(not(any(
311 target_os = "windows",
312 target_os = "macos",
313 target_os = "linux",
314 target_arch = "wasm32"
315 )))]
316 self.button(cx, ids!(attach)).set_visible(cx, false);
317
318 #[cfg(not(target_arch = "wasm32"))]
320 self.button(cx, ids!(audio))
321 .set_visible(cx, supports_realtime);
322
323 let mut send = self.button(cx, ids!(send));
325 let mut stop = self.button(cx, ids!(stop));
326
327 if supports_realtime {
328 send.set_visible(cx, false);
329 stop.set_visible(cx, false);
330 } else {
331 match self.task {
332 Task::Send => {
333 send.set_visible(cx, true);
334 stop.set_visible(cx, false);
335 }
336 Task::Stop => {
337 send.set_visible(cx, false);
338 stop.set_visible(cx, true);
339 }
340 }
341 }
342
343 let input = self.text_input(cx, ids!(text_input));
345 if supports_realtime {
346 self.interactivity = Interactivity::Disabled;
347 input.set_is_read_only(cx, true);
348 input.set_empty_text(cx, self.realtime_empty_text.clone());
349 } else {
350 input.set_is_read_only(cx, false);
351 input.set_empty_text(cx, self.empty_text.clone());
352 }
353
354 let enabled = self.interactivity == Interactivity::Enabled;
355 send.set_enabled(cx, enabled);
356 stop.set_enabled(cx, enabled);
357
358 self.deref.draw_walk(cx, scope, walk)
359 }
360}
361
362impl PromptInput {
363 pub fn reset(&mut self, cx: &mut Cx) {
365 self.text_input_ref(cx).set_text(cx, "");
366 self.attachment_list_ref(cx).write().attachments.clear();
367 }
368
369 pub fn text_input_ref(&self, cx: &Cx) -> TextInputRef {
371 self.text_input(cx, ids!(text_input))
372 }
373
374 pub fn submitted(&self, cx: &Cx, actions: &Actions) -> bool {
379 let send = self.button(cx, ids!(send));
380 let stop = self.button(cx, ids!(stop));
381 let input = self.text_input_ref(cx);
382 (send.clicked(actions) || stop.clicked(actions) || input.returned(actions).is_some())
383 && self.interactivity == Interactivity::Enabled
384 }
385
386 pub fn call_pressed(&self, cx: &Cx, actions: &Actions) -> bool {
387 self.button(cx, ids!(audio)).clicked(actions)
388 }
389
390 pub fn stt_pressed(&self, cx: &Cx, actions: &Actions) -> bool {
391 self.button(cx, ids!(stt)).clicked(actions)
392 }
393
394 pub fn has_send_task(&self) -> bool {
396 self.task == Task::Send
397 }
398
399 pub fn has_stop_task(&self) -> bool {
401 self.task == Task::Stop
402 }
403
404 pub fn enable(&mut self) {
406 self.interactivity = Interactivity::Enabled;
407 }
408
409 pub fn disable(&mut self) {
411 self.interactivity = Interactivity::Disabled;
412 }
413
414 pub fn set_send(&mut self) {
416 self.task = Task::Send;
417 }
418
419 pub fn set_stop(&mut self) {
421 self.task = Task::Stop;
422 }
423
424 pub(crate) fn attachment_list_ref(&self, cx: &Cx) -> AttachmentListRef {
425 self.attachment_list(cx, ids!(attachments))
426 }
427
428 pub fn set_chat_controller(
430 &mut self,
431 cx: &Cx,
432 controller: Option<
433 std::sync::Arc<std::sync::Mutex<crate::aitk::controllers::chat::ChatController>>,
434 >,
435 ) {
436 if let Some(mut inner) = self
437 .widget(cx, ids!(model_selector))
438 .borrow_mut::<crate::widgets::model_selector::ModelSelector>()
439 {
440 inner.chat_controller = controller;
441 }
442 }
443
444 pub fn set_bot_capabilities(&mut self, cx: &mut Cx, capabilities: Option<BotCapabilities>) {
446 let was_realtime = self
447 .bot_capabilities
448 .as_ref()
449 .map(|caps| caps.has_capability(&BotCapability::AudioCall))
450 .unwrap_or(false);
451
452 let is_realtime = capabilities
453 .as_ref()
454 .map(|caps| caps.has_capability(&BotCapability::AudioCall))
455 .unwrap_or(false);
456
457 self.bot_capabilities = capabilities;
458
459 if was_realtime && !is_realtime {
461 self.interactivity = Interactivity::Enabled;
462 self.text_input_ref(cx).set_is_read_only(cx, false);
463 self.text_input_ref(cx).set_text(cx, "");
464 }
465
466 self.redraw(cx);
467 }
468
469 pub fn set_stt_visible(&mut self, cx: &mut Cx, visible: bool) {
471 self.button(cx, ids!(stt)).set_visible(cx, visible);
472 }
473}
474
475impl PromptInputRef {
476 pub fn read(&self) -> Ref<'_, PromptInput> {
480 self.borrow().unwrap()
481 }
482
483 pub fn write(&mut self) -> RefMut<'_, PromptInput> {
487 self.borrow_mut().unwrap()
488 }
489
490 pub fn read_with<R>(&self, f: impl FnOnce(&PromptInput) -> R) -> R {
494 f(&*self.read())
495 }
496
497 pub fn write_with<R>(&mut self, f: impl FnOnce(&mut PromptInput) -> R) -> R {
501 f(&mut *self.write())
502 }
503}