makepad_studio/ai_chat/
ai_chat_view.rs

1use {
2    crate::{
3        app::{AppData, AppAction},
4        ai_chat::ai_chat_manager::*,
5        file_system::file_system::{EditSession,OpenDocument},
6        makepad_widgets::*,
7    },
8    std::{
9        env,
10    },
11};
12
13live_design!{
14    use link::shaders::*;
15    use link::widgets::*;
16    use link::theme::*;
17    
18    use makepad_code_editor::code_view::CodeView;
19
20    User = <RoundedView> {
21        height: Fit,
22        flow: Down,
23        margin: <THEME_MSPACE_3> {}
24        padding: <THEME_MSPACE_2> { top: (THEME_SPACE_1+4), bottom: (THEME_SPACE_2) } 
25        draw_bg: { color: (THEME_COLOR_U_1) }
26
27        <View> {
28            height: Fit, width: Fill,
29            flow: Right,
30            align: { x: 0., y: 0. },
31            spacing: (THEME_SPACE_3),
32            padding: { left: (THEME_SPACE_1), right: (THEME_SPACE_1), top: (THEME_SPACE_1-1) }
33            margin: { bottom: -5.}
34
35            <View> { width: Fill }
36
37        }
38
39        <View>{
40            height:Fit, width: Fill,
41
42            message_input = <TextInput> {
43                width: Fill,
44                height: Fit,
45
46                text: ""
47                empty_text: "Enter prompt"
48            }
49
50            /*send_button = <ButtonFlatter> {
51                width: Fit,
52                padding: <THEME_MSPACE_V_1> {}
53                margin: { left: -35.}
54                draw_icon: {
55                    color: (THEME_COLOR_U_4),
56                    svg_file: dep("crate://self/resources/icons/icon_run.svg"),
57                }
58                icon_walk: { width: 6. }
59            }
60            
61            // <ButtonFlatter> {
62            //     icon_walk: { width: 16, height: Fit}
63            //     text: ">"
64            // }
65                    
66            clear_button = <ButtonFlatter> {
67                width: Fit,
68                padding: <THEME_MSPACE_V_1> {}
69                draw_icon: {
70                    color: (THEME_COLOR_U_4),
71                    svg_file: dep("crate://self/resources/icons/icon_times.svg"),
72                }
73                icon_walk: { width: 7. }
74            }*/
75        }
76        
77        
78    }
79    
80    Assistant = <RoundedView> {
81        flow: Down
82        margin: <THEME_MSPACE_H_3> {}
83        padding: <THEME_MSPACE_H_2> { bottom: (THEME_SPACE_2) }
84
85        draw_bg: {
86            color: (THEME_COLOR_D_2)
87        }
88        flow: Down
89        busy = <View>{
90            width: 70, height: 10,
91            margin: {top:10.,bottom:0}
92            padding: 0.,
93            show_bg: true,
94            draw_bg:{
95                fn pixel(self)->vec4{
96                    let sdf = Sdf2d::viewport(self.pos * self.rect_size);
97                    let x = 0.;
98                    for i in 0..5{
99                        x = x + 8.;
100                        sdf.circle(x,5.,2.5);
101                        sdf.fill((THEME_COLOR_MAKEPAD));
102                    }
103                    return sdf.result
104                }
105            }
106        }
107        md = <Markdown>{
108            code_block = <View>{
109                
110                width:Fill,
111                height:Fit,
112                flow: Overlay
113                code_view = <CodeView>{
114                    keep_cursor_at_end: true,
115                    editor:{
116                        height: 200,
117                        draw_bg: { color: ((THEME_COLOR_D_HIDDEN)) }
118                    }
119                }
120                <View>{
121                    width:Fill,
122                    height:Fit,
123                    align:{ x: 1.0 }
124                    
125                    run_button = <ButtonFlat> {
126                        width: Fit,
127                        height: Fit,
128                        padding: <THEME_MSPACE_2> {}
129                        margin: 0.
130                        icon_walk: {
131                            width: 12, height: Fit,
132                            margin: { left: 10 }
133                        }
134                                                
135                        draw_icon: {
136                            color: (THEME_COLOR_U_4),
137                            svg_file: dep("crate://self/resources/icons/icon_run.svg"),
138                        }
139                        icon_walk: { width: 9. }
140                    }
141                    copy_button = <ButtonFlat> {
142                        margin:{right:20}
143                        icon_walk: {
144                            width: 12, height: Fit,
145                            margin: { left: 10 }
146                        }
147                        draw_icon: {
148                            color: (THEME_COLOR_U_4)
149                            svg_file: dep("crate://self/resources/icons/icon_copy.svg"),
150                        }
151                    }
152                    
153                }
154                
155            }
156            use_code_block_widget: true,
157            body:""
158        }
159        
160    }
161    
162    pub AiChatView = {{AiChatView}}{
163        flow: Down,
164        height: Fill, width: Fill,
165        spacing: (THEME_SPACE_1),
166        show_bg: true,
167        draw_bg: { color: (THEME_COLOR_D_1) },
168        
169        tb = <DockToolbar> {
170            content = {
171                height: Fill, width: Fill,
172                flow: Right,
173                padding:{top:1}
174                align: { x: 0.0, y: 0.5},
175                margin: <THEME_MSPACE_H_2> {}
176                spacing: (THEME_SPACE_2),
177
178                auto_run = <CheckBoxCustom> {
179                    text: "Auto",
180                    align: { y: 0.5 }
181                    draw_bg: { check_type: None }
182                    spacing: (THEME_SPACE_1),
183                    padding: <THEME_MSPACE_V_2> {}
184                    icon_walk: { width: 10. }
185                    draw_icon: {
186                        color: (THEME_COLOR_D_4),
187                        //color_active: (STUDIO_PALETTE_6),
188                        svg_file: dep("crate://self/resources/icons/icon_auto.svg"),
189                    }
190                }
191                                
192                <View> {
193                    flow: Right,
194                    width: Fit,
195                    height: Fit,
196                    spacing: (THEME_SPACE_1)
197                    
198                    <Pbold> { width: Fit, text: "Model", margin: 0., padding: <THEME_MSPACE_V_1> {} }
199                    model_dropdown = <DropDownFlat> { width: Fit, popup_menu_position: BelowInput }
200                }
201                
202                <View> {
203                    flow: Right,
204                    width: Fit,
205                    height: Fit,
206                    spacing: (THEME_SPACE_1)
207                    
208                    <Pbold> { width: Fit, text: "Context", margin: 0., padding: <THEME_MSPACE_V_1> {} }
209                    context_dropdown = <DropDownFlat>{ width: Fit, popup_menu_position: BelowInput }
210                }
211                
212                <View> {
213                    flow: Right,
214                    width: Fit,
215                    height: Fit,
216                    spacing: (THEME_SPACE_1)
217                    
218                    <Pbold> { width: Fit, text: "Project", margin: 0., padding: <THEME_MSPACE_V_1> {} }
219                    project_dropdown = <DropDownFlat> { width: Fit, popup_menu_position: BelowInput }
220                }
221                
222/*
223                <P> {
224                    width: Fit,
225                    height: Fit,
226                    draw_text: {
227                        color: (THEME_COLOR_U_4)
228                    }
229                    text: "First Prompt / "
230                }
231                <Pbold> { width: Fit, text: "Last Prompt" }
232*/
233                <View> { width: Fill }
234
235                history_left = <ButtonFlatter> {
236                    width: Fit,
237                    draw_bg: { color_focus: #0000 }
238                    padding: <THEME_MSPACE_1> {}
239                    draw_icon: {
240                        svg_file: dep("crate://self/resources/icons/icon_history_rew.svg"),
241                    }
242                    icon_walk: { width: 5. }
243                }
244
245                slot = <Label> {
246                    draw_text: {
247                        color: (THEME_COLOR_U_4)
248                    }
249                    width: Fit,
250                    text: "0"
251                }
252
253                history_right = <ButtonFlatter> {
254                    width: Fit,
255                    padding: <THEME_MSPACE_1> {}
256                    draw_bg: { color_focus: #0000 }
257                    draw_icon: {
258                        svg_file: dep("crate://self/resources/icons/icon_history_ff.svg"),
259                    }
260                    icon_walk: { width: 5. }
261                }
262
263                history_delete = <ButtonFlatter> {
264                    width: Fit,
265                    text: ""
266                    draw_bg: { color_focus: #0000 }
267                    draw_icon: {
268                        svg_file: dep("crate://self/resources/icons/icon_del.svg"),
269                    }
270                    icon_walk: { width: 10. }
271                }
272/*
273                <Vr> {}
274
275                <ButtonFlatter> {
276                    width: Fit,
277                    text: ""
278                    draw_icon: {
279                        svg_file: dep("crate://self/resources/icons/icon_add.svg"),
280                    }
281                    icon_walk: { width: 13. }
282                }*/
283            }
284        }
285
286        // lets make portal list with User and Assistant components
287        // and lets fix the portal lists scroll
288        list = <PortalList>{
289            drag_scrolling: false
290            max_pull_down: 0.0
291            //auto_tail: true
292            User = <User>{}
293            Assistant = <Assistant>{}
294        }
295    }
296} 
297 
298#[derive(Live, LiveHook, Widget)] 
299pub struct AiChatView{
300    #[deref] view:View,
301    #[rust] initialised: bool,
302    #[rust] history_slot: usize,
303}
304
305impl AiChatView{
306    fn handle_own_actions(&mut self, cx: &mut Cx, actions:&Actions, scope: &mut Scope){
307        let data = scope.data.get_mut::<AppData>().unwrap();
308        let session_id = scope.path.from_end(0);
309        
310        if let Some(EditSession::AiChat(chat_id)) = data.file_system.get_session_mut(session_id){
311            let chat_id = *chat_id;
312            if let Some(OpenDocument::AiChat(doc)) = data.file_system.open_documents.get_mut(&chat_id){
313                                
314                if let Some(value) = self.check_box(id!(auto_run)).changed(actions){
315                    doc.auto_run = value;
316                }
317                
318                // items with actions
319                let chat_list = self.view.portal_list(id!(list));
320                let items_len = doc.file.history[self.history_slot].messages.len();
321                for (item_id, _item) in chat_list.items_with_actions(&actions) {
322                    let item_id = items_len - item_id - 1;
323                    if let Some(wa) = actions.widget_action(id!(copy_button)){
324                        if wa.widget().as_button().pressed(actions){
325                            //let code_view = wa.widget_nth(2).widget(id!(code_view));
326                        }
327                    }
328                    if let Some(wa) = actions.widget_action(id!(run_button)){
329                        if wa.widget().as_button().pressed(actions){
330                            cx.action(AppAction::RunAiChat{chat_id, history_slot:self.history_slot, item_id});
331                        }
332                    }
333                }
334                    
335                if self.button(id!(history_left)).pressed(actions){
336                    // first we check if our messages are the same as 'slot'.
337                    // if not, we should create an undo item first
338                    self.history_slot = self.history_slot.saturating_sub(1);
339                    cx.action(AppAction::RedrawAiChat{chat_id});
340                }
341                if self.button(id!(history_right)).pressed(actions){
342                    self.history_slot = (self.history_slot + 1).min(doc.file.history.len().saturating_sub(1));
343                    cx.action(AppAction::RedrawAiChat{chat_id});
344                }
345                if self.button(id!(history_delete)).pressed(actions){
346                    doc.file.remove_slot(cx, &mut self.history_slot);
347                    cx.action(AppAction::RedrawAiChat{chat_id});
348                    cx.action(AppAction::SaveAiChat{chat_id});
349                }
350                
351                                
352                if let Some(ctx_id) = self.drop_down(id!(context_dropdown)).selected(actions){
353                    let ctx_name = &data.ai_chat_manager.contexts[ctx_id].name;
354                    doc.file.set_base_context(self.history_slot, ctx_name);
355                }
356                                    
357                if let Some(model_id) = self.drop_down(id!(model_dropdown)).selected(actions){
358                    let model = &data.ai_chat_manager.models[model_id].name;
359                    doc.file.set_model(self.history_slot, model);
360                }
361                                    
362                if let Some(project_id) = self.drop_down(id!(project_dropdown)).selected(actions){
363                    let model = &data.ai_chat_manager.projects[project_id].name;
364                    doc.file.set_project(self.history_slot, model);
365                }
366                                
367                let list = self.view.portal_list(id!(list));
368                let items_len = doc.file.history[self.history_slot].messages.len();
369                for (item_id,item) in list.items_with_actions(actions){
370                    let item_id = items_len - item_id - 1;
371                    let message_input = item.text_input(id!(message_input));
372                    if let Some(text) = message_input.changed(actions){
373                        doc.file.fork_chat_at(cx, &mut self.history_slot, item_id, text);
374                        cx.action(AppAction::RedrawAiChat{chat_id});
375                        cx.action(AppAction::SaveAiChat{chat_id});
376                    }
377                    if message_input.escaped(actions){
378                        cx.action(AppAction::CancelAiGeneration{chat_id});
379                    }
380                    
381                    
382                    if let Some(ke) = item.text_input(id!(message_input)).key_down_unhandled(actions){
383                        if ke.key_code == KeyCode::ReturnKey && ke.modifiers.logo{
384                            // run it
385                            cx.action(AppAction::RunAiChat{chat_id, history_slot: self.history_slot, item_id});
386                        }
387                        if ke.key_code == KeyCode::ArrowLeft && ke.modifiers.logo{
388                            self.history_slot = self.history_slot.saturating_sub(1);
389                            cx.action(AppAction::RedrawAiChat{chat_id});
390                            if ke.modifiers.control{
391                                cx.action(AppAction::RunAiChat{chat_id, history_slot: self.history_slot, item_id});
392                            }
393                        }
394                        if ke.key_code == KeyCode::ArrowRight && ke.modifiers.logo{
395                            self.history_slot = (self.history_slot + 1).min(doc.file.history.len().saturating_sub(1));
396                            cx.action(AppAction::RedrawAiChat{chat_id});                 
397                            if ke.modifiers.control{
398                                cx.action(AppAction::RunAiChat{chat_id, history_slot: self.history_slot, item_id});
399                            }
400                        }
401                    }
402                    
403                    if item.button(id!(run_button)).pressed(actions){
404                        cx.action(AppAction::RunAiChat{chat_id, history_slot: self.history_slot, item_id});
405                    }
406                    
407                    if item.button(id!(send_button)).pressed(actions) || 
408                    item.text_input(id!(message_input)).returned(actions).is_some(){
409                        // we'd already be forked
410                        let text = message_input.text();
411                        
412                        doc.file.fork_chat_at(cx, &mut self.history_slot, item_id, text);
413                        // alright so we press send/enter now what
414                        // we now call 'setaichatlen' this will 'fork' our current index
415                        // what if our chat is empty? then we dont fork
416                        doc.file.clamp_slot(&mut self.history_slot);
417                        // lets fetch the context
418                        // println!("{}", dd.selected_item());
419                        // alright lets collect the context
420                        cx.action(AppAction::SendAiChatToBackend{chat_id, history_slot: self.history_slot});
421                        cx.action(AppAction::SaveAiChat{chat_id});
422                        cx.action(AppAction::RedrawAiChat{chat_id});
423                    }
424                    // lets clear the messages
425                    if item.button(id!(clear_button)).pressed(actions){
426                        doc.file.fork_chat_at(cx, &mut self.history_slot, item_id, "".to_string());
427                        cx.action(AppAction::SaveAiChat{chat_id});
428                        cx.action(AppAction::RedrawAiChat{chat_id});
429                    }
430                }
431            }
432        }
433       
434    }
435}
436impl Widget for AiChatView {
437    fn draw_walk(&mut self, cx: &mut Cx2d, scope:&mut Scope, walk:Walk)->DrawStep{
438        let data = scope.data.get_mut::<AppData>().unwrap();
439        let session_id = scope.path.from_end(0); 
440        if let Some(EditSession::AiChat(chat_id)) = data.file_system.get_session_mut(session_id){
441            let chat_id = *chat_id;
442            if let Some(OpenDocument::AiChat(doc)) = data.file_system.open_documents.get(&chat_id){
443                if !self.initialised{
444                    self.initialised = true;
445                    self.history_slot = doc.file.history.iter()
446                    .enumerate()
447                    .max_by(|(_, a), (_, b)| a.last_time.total_cmp(&b.last_time))
448                    .map(|(index, _)| index).unwrap_or(0);
449                }
450                
451                self.check_box(id!(auto_run)).set_active(cx, doc.auto_run);
452                
453                let history_len = doc.file.history.len(); 
454                self.label(id!(slot)).set_text_with(|v| fmt_over!(v, "{}/{}", self.history_slot+1, history_len));
455                                
456                let messages = &doc.file.history[self.history_slot];
457                // model dropdown
458                let dd = self.drop_down(id!(model_dropdown));
459                // ok how do we set these dropdown labels without causing memory changes
460                let mut i = data.ai_chat_manager.models.iter();
461                dd.set_labels_with(cx, |label|{i.next().map(|m| label.push_str(&m.name));});
462                if let Some(pos) = data.ai_chat_manager.models.iter().position(|b| b.name == messages.model){
463                    dd.set_selected_item(cx, pos);
464                }
465                                                                            
466                let dd = self.drop_down(id!(context_dropdown));
467                let mut i = data.ai_chat_manager.contexts.iter();
468                dd.set_labels_with(cx, |label|{i.next().map(|m| label.push_str(&m.name));});
469                                                                            
470                if let Some(pos) = data.ai_chat_manager.contexts.iter().position(|ctx| ctx.name == messages.base_context){
471                    dd.set_selected_item(cx, pos);
472                }
473                                                                            
474                let dd = self.drop_down(id!(project_dropdown));
475                let mut i = data.ai_chat_manager.projects.iter();
476                dd.set_labels_with(cx, |label|{i.next().map(|m| label.push_str(&m.name));});
477                                                                                                                
478                if let Some(pos) = data.ai_chat_manager.projects.iter().position(|ctx| ctx.name == messages.project){
479                    dd.set_selected_item(cx, pos);
480                }
481                                        
482                while let Some(item) =  self.view.draw_walk(cx, &mut Scope::empty(), walk).step(){
483                    
484                    if let Some(mut list) = item.as_portal_list().borrow_mut() {
485                        doc.file.clamp_slot(&mut self.history_slot);
486                        let items_len = doc.file.history[self.history_slot].messages.len();
487                        list.set_item_range(cx, 0, items_len);
488                        
489                        while let Some(item_id) = list.next_visible_item(cx) {
490                            match doc.file.history[self.history_slot].messages.get(items_len-item_id-1){
491                                Some(AiChatMessage::Assistant(val))=>{
492                                    let busy = item_id == 0 && 
493                                    doc.in_flight.is_some();
494                                    let item = list.item(cx, item_id, live_id!(Assistant));
495                                    // alright we got the assistant. lets set the markdown stuff
496                                    item.widget(id!(md)).set_text(cx, &val);
497                                    item.view(id!(busy)).set_visible(cx, busy);
498                                    item.draw_all_unscoped(cx);
499                                }
500                                Some(AiChatMessage::User(val))=>{
501                                    // lets set the value to the text input
502                                    let item = list.item(cx, item_id, live_id!(User));
503                                    
504                                    item.widget(id!(message_input)).set_text(cx, &val.message);
505                                    item.draw_all_unscoped(cx);
506                                }
507                                _=>()
508                            }
509                        }
510                    }
511                }
512            }
513        }
514        DrawStep::done()
515    }
516    
517    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope){
518        let ac = cx.capture_actions(|cx|{
519            self.view.handle_event(cx, event, scope);
520        });
521        if ac.len()>0{
522            self.handle_own_actions(cx, &ac, scope)
523        }
524    }
525}