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 }
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 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<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}
284 }
285
286 list = <PortalList>{
289 drag_scrolling: false
290 max_pull_down: 0.0
291 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 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 }
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 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 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 let text = message_input.text();
411
412 doc.file.fork_chat_at(cx, &mut self.history_slot, item_id, text);
413 doc.file.clamp_slot(&mut self.history_slot);
417 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 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 let dd = self.drop_down(id!(model_dropdown));
459 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 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 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}