moly_kit/widgets/
messages.rs

1use std::{
2    cell::{Ref, RefMut},
3    collections::HashSet,
4    sync::{Arc, Mutex},
5};
6
7use crate::{
8    aitk::{controllers::chat::ChatController, protocol::*},
9    utils::makepad::{events::EventExt, portal_list::ItemsRangeIter, ui_runner::DeferRedraw},
10    widgets::{
11        avatar::AvatarWidgetRefExt, chat_line::ChatLineAction,
12        message_loading::MessageLoadingWidgetRefExt,
13    },
14};
15use makepad_code_editor::code_view::CodeViewWidgetRefExt;
16use makepad_widgets::*;
17
18use super::{
19    citation::CitationAction,
20    slot::{Slot, SlotWidgetRefExt},
21    standard_message_content::StandardMessageContentWidgetRefExt,
22};
23
24script_mod! {
25    use mod.prelude.widgets.*
26    use mod.widgets.*
27
28    mod.widgets.MessagesBase = #(Messages::register_widget(vm))
29    mod.widgets.Messages = set_type_default() do mod.widgets.MessagesBase {
30        flow: Overlay,
31
32        list := PortalList {
33            grab_key_focus: true
34            scroll_bar +: {
35                bar_size: 0.0,
36            }
37            UserLine := UserLine {}
38            BotLine := BotLine {}
39            LoadingLine := LoadingLine {}
40            AppLine := AppLine {}
41            ErrorLine := ErrorLine {}
42            SystemLine := SystemLine {}
43            ToolRequestLine := ToolRequestLine {}
44            ToolResultLine := ToolResultLine {}
45            Empty := View { height: 0 }
46        }
47        View {
48            align: Align { x: 1.0, y: 1.0 },
49            jump_to_bottom := Button {
50                width: 36,
51                height: 36,
52                margin: Inset { left: 2, right: 2, top: 2, bottom: 10 },
53                icon_walk +: {
54                    width: 16, height: 16
55                    margin: Inset { left: 4.5, top: 6.5 },
56                }
57                draw_icon +: {
58                    svg: crate_resource("self://resources/jump_to_bottom.svg")
59                    color: #x1C1B1F,
60                    color_hover: #x0
61                }
62                draw_bg +: {
63                    pixel: fn() -> vec4 {
64                        let sdf = Sdf2d.viewport(self.pos * self.rect_size);
65                        let center = self.rect_size * 0.5;
66                        let radius = min(self.rect_size.x self.rect_size.y) * 0.5;
67
68                        sdf.circle(center.x center.y radius - 1.0);
69                        sdf.fill_keep(#fff);
70                        sdf.stroke(#xEAECF0 1.5);
71
72                        return sdf.result
73                    }
74                }
75            }
76        }
77    }
78}
79
80/// Relevant actions that should be handled by a parent.
81///
82/// If includes an index, it refers to the index of the message in the list.
83#[derive(Debug, PartialEq, Copy, Clone, Default)]
84pub enum MessagesAction {
85    /// The message at the given index should be copied.
86    Copy(usize),
87
88    /// The message at the given index should be deleted.
89    Delete(usize),
90
91    /// The message at the given index should be edited and saved.
92    EditSave(usize),
93
94    /// The message at the given index should be edited, saved and the messages
95    /// history should be regenerated from here.
96    EditRegenerate(usize),
97
98    /// The tool request at the given index should be approved and executed.
99    ToolApprove(usize),
100
101    /// The tool request at the given index should be denied.
102    ToolDeny(usize),
103
104    #[default]
105    None,
106}
107
108/// Represents the current open editor for a message.
109#[derive(Debug)]
110struct Editor {
111    index: usize,
112    buffer: String,
113}
114
115pub trait CustomContent {
116    fn content_widget(
117        &mut self,
118        cx: &mut Cx,
119        previous_widget: WidgetRef,
120        content: &MessageContent,
121    ) -> Option<WidgetRef>;
122}
123
124/// View over a conversation with messages.
125///
126/// This is mostly a dummy widget. Prefer using and adapting
127/// [crate::widgets::chat::Chat] instead.
128#[derive(Script, Widget, ScriptHook)]
129pub struct Messages {
130    #[deref]
131    deref: View,
132    #[source]
133    source: ScriptObjectRef,
134
135    #[rust]
136    // Note: This should be `pub(crate)` but Makepad macros don't work with it.
137    pub chat_controller: Option<Arc<Mutex<ChatController>>>,
138
139    #[rust]
140    current_editor: Option<Editor>,
141
142    #[rust]
143    is_list_end_drawn: bool,
144
145    /// Keep track of the drawn items in the [[PortalList]] to be able to
146    /// retrieve the visible items anytime.
147    ///
148    /// The method [[PortalList::visible_items]] just returns a count/length.
149    #[rust]
150    visible_range: Option<(usize, usize)>,
151
152    #[rust]
153    list_height: f64,
154
155    #[rust]
156    needs_extra_draw_pass: bool,
157
158    #[rust]
159    custom_contents: Vec<Box<dyn CustomContent>>,
160
161    /// Tracks which error message indices have their details expanded.
162    #[rust]
163    expanded_error_details: HashSet<usize>,
164}
165
166impl Widget for Messages {
167    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
168        self.ui_runner().handle(cx, event, scope, self);
169        self.deref.handle_event(cx, event, scope);
170        self.handle_list(cx, event, scope);
171
172        let jump_to_bottom = self.button(cx, ids!(jump_to_bottom));
173
174        if jump_to_bottom.clicked(event.actions()) {
175            self.animated_scroll_to_bottom(cx);
176            self.redraw(cx);
177        }
178
179        for action in event.widget_actions() {
180            if let CitationAction::Open(url) = action.cast() {
181                let _ = robius_open::Uri::new(url.as_str()).open();
182            }
183        }
184    }
185
186    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
187        let list = self.portal_list(cx, ids!(list));
188        let list_uid = list.widget_uid();
189
190        while let Some(widget) = self.deref.draw_walk(cx, scope, walk).step() {
191            if widget.widget_uid() == list_uid {
192                self.draw_list(cx, widget.as_portal_list());
193            }
194        }
195
196        self.button(cx, ids!(jump_to_bottom))
197            .set_visible(cx, !self.is_at_bottom());
198
199        let previous_list_height = self.list_height;
200        self.list_height = list.area().rect(cx).size.y;
201
202        self.needs_extra_draw_pass =
203            self.needs_extra_draw_pass || (self.list_height != previous_list_height);
204
205        if self.needs_extra_draw_pass {
206            self.needs_extra_draw_pass = false;
207            self.ui_runner().defer_redraw();
208        }
209
210        DrawStep::done()
211    }
212}
213
214impl Messages {
215    fn draw_list(&mut self, cx: &mut Cx2d, list_ref: PortalListRef) {
216        self.is_list_end_drawn = false;
217        self.visible_range = None;
218
219        let chat_controller = self
220            .chat_controller
221            .clone()
222            .expect("no chat controller set");
223
224        let mut chat_controller = chat_controller.lock().unwrap();
225
226        let last_message_index = chat_controller.state().messages.len().checked_sub(1);
227        let second_last_message_index = last_message_index.and_then(|i| i.checked_sub(1));
228
229        let mut did_filler_draw = false;
230        let mut second_last_message_height = 0.0;
231        let mut last_message_height = 0.0;
232
233        chat_controller
234            .dangerous_state_mut()
235            .messages
236            .push(Message {
237                from: EntityId::App,
238                content: MessageContent {
239                    text: "FIL".into(),
240                    ..Default::default()
241                },
242                ..Default::default()
243            });
244
245        chat_controller
246            .dangerous_state_mut()
247            .messages
248            .push(Message {
249                from: EntityId::App,
250                content: MessageContent {
251                    text: "EOC".into(),
252                    ..Default::default()
253                },
254                ..Default::default()
255            });
256
257        let mut list = list_ref.borrow_mut().unwrap();
258        list.set_item_range(cx, 0, chat_controller.state().messages.len());
259
260        while let Some(index) = list.next_visible_item(cx) {
261            let total = chat_controller.state().messages.len();
262            if index >= total {
263                continue;
264            }
265
266            if let Some((_start, end)) = &mut self.visible_range {
267                *end = (*end).max(index);
268            } else {
269                self.visible_range = Some((index, index));
270            }
271
272            let message = &chat_controller.state().messages[index];
273
274            let item = match &message.from {
275                EntityId::System => {
276                    let item = if message.metadata.is_writing() {
277                        let item = list.item(cx, index, id!(LoadingLine));
278                        item.message_loading(cx, ids!(content_section.loading))
279                            .animate(cx);
280                        item
281                    } else {
282                        list.item(cx, index, id!(SystemLine))
283                    };
284
285                    item.avatar(cx, ids!(avatar)).borrow_mut().unwrap().avatar =
286                        Some(EntityAvatar::Text("S".into()));
287                    item.label(cx, ids!(name)).set_text(cx, "System");
288
289                    if !message.metadata.is_writing() {
290                        item.slot(cx, ids!(content))
291                            .current()
292                            .as_standard_message_content()
293                            .set_content(cx, &message.content);
294                    }
295
296                    self.apply_editor_visibility(cx, &item, index);
297                    item
298                }
299                EntityId::Tool => {
300                    let item = if message.metadata.is_writing() {
301                        let item = list.item(cx, index, id!(LoadingLine));
302                        item.message_loading(cx, ids!(content_section.loading))
303                            .animate(cx);
304                        item
305                    } else {
306                        list.item(cx, index, id!(ToolResultLine))
307                    };
308
309                    item.avatar(cx, ids!(avatar)).borrow_mut().unwrap().avatar =
310                        Some(EntityAvatar::Text("T".into()));
311                    item.label(cx, ids!(name)).set_text(cx, "Tool");
312
313                    if !message.metadata.is_writing() {
314                        item.slot(cx, ids!(content))
315                            .current()
316                            .as_standard_message_content()
317                            .set_content(cx, &message.content);
318                    }
319
320                    self.apply_editor_visibility(cx, &item, index);
321                    item
322                }
323                EntityId::App => {
324                    if message.content.text == "EOC" {
325                        let mut item = list.item(cx, index, id!(Empty));
326                        script_apply_eval!(cx, item, {
327                            height: 0.1
328                        });
329                        item.draw_all(cx, &mut Scope::empty());
330                        self.is_list_end_drawn = true;
331                        item
332                    } else if message.content.text == "FIL" {
333                        did_filler_draw = true;
334
335                        let mut item = list.item(cx, index, id!(Empty));
336
337                        const MAX_SECOND_LAST_MESSAGE_VISIBILITY: f64 = 100.0;
338                        const SECOND_LAST_MESSAGE_DRAW_GUARANTEE: f64 = 1.0;
339
340                        let height = (self.list_height
341                            - last_message_height
342                            - second_last_message_height
343                                .clamp(0.0, MAX_SECOND_LAST_MESSAGE_VISIBILITY)
344                            - SECOND_LAST_MESSAGE_DRAW_GUARANTEE)
345                            .clamp(0.0, f64::INFINITY);
346
347                        script_apply_eval!(cx, item, {
348                            height: #(height)
349                        });
350                        item
351                    } else if let Some((left, right)) = message.content.text.split_once(':')
352                        && let Some("error") = left
353                            .split_whitespace()
354                            .last()
355                            .map(|s| s.to_lowercase())
356                            .as_deref()
357                    {
358                        let item = list.item(cx, index, id!(ErrorLine));
359                        item.avatar(cx, ids!(avatar)).borrow_mut().unwrap().avatar =
360                            Some(EntityAvatar::Text("X".into()));
361                        item.label(cx, ids!(name)).set_text(cx, left);
362
363                        let error_content = MessageContent {
364                            text: right.to_string(),
365                            ..Default::default()
366                        };
367                        item.slot(cx, ids!(content))
368                            .current()
369                            .as_standard_message_content()
370                            .set_content(cx, &error_content);
371
372                        let error_text = &message.content.text;
373                        let note = error_note_for(error_text);
374                        let has_note = !note.is_empty();
375
376                        if has_note {
377                            item.label(cx, ids!(error_note)).set_text(cx, note);
378                        }
379
380                        let has_details = message
381                            .content
382                            .data
383                            .as_ref()
384                            .is_some_and(|d| !d.trim().is_empty());
385                        let show_section = has_note || has_details;
386                        item.view(cx, ids!(error_details_section))
387                            .set_visible(cx, show_section);
388                        item.view(cx, ids!(error_note)).set_visible(cx, has_note);
389                        item.view(cx, ids!(error_details_toggle))
390                            .set_visible(cx, has_details);
391
392                        let is_expanded = self.expanded_error_details.contains(&index);
393                        item.view(cx, ids!(error_details))
394                            .set_visible(cx, is_expanded);
395                        let toggle_text = if is_expanded {
396                            "Hide details"
397                        } else {
398                            "Show details"
399                        };
400                        item.label(cx, ids!(toggle_label)).set_text(cx, toggle_text);
401
402                        if has_details && is_expanded {
403                            item.label(cx, ids!(details_text))
404                                .set_text(cx, message.content.data.as_deref().unwrap_or(""));
405                        }
406
407                        self.apply_editor_visibility(cx, &item, index);
408                        item
409                    } else {
410                        let item = list.item(cx, index, id!(AppLine));
411                        item.avatar(cx, ids!(avatar)).borrow_mut().unwrap().avatar =
412                            Some(EntityAvatar::Text("A".into()));
413
414                        item.slot(cx, ids!(content))
415                            .current()
416                            .as_standard_message_content()
417                            .set_content(cx, &message.content);
418
419                        self.apply_editor_visibility(cx, &item, index);
420                        item
421                    }
422                }
423                EntityId::User => {
424                    let item = list.item(cx, index, id!(UserLine));
425
426                    item.avatar(cx, ids!(avatar)).borrow_mut().unwrap().avatar =
427                        Some(EntityAvatar::Text("Y".into()));
428                    item.label(cx, ids!(name)).set_text(cx, "You");
429
430                    let slot_ref = item.slot(cx, ids!(content));
431                    let current = slot_ref.current();
432                    let mut smc = current.as_standard_message_content();
433                    smc.set_content(cx, &message.content);
434
435                    self.apply_editor_visibility(cx, &item, index);
436                    item
437                }
438                EntityId::Bot(id) => {
439                    let bot = chat_controller.state().get_bot(id);
440
441                    let (name, avatar) = bot
442                        .as_ref()
443                        .map(|b| (b.name.clone(), b.avatar.clone()))
444                        .unwrap_or_else(|| {
445                            let model_name = format!("{} (unavailable)", id.id());
446                            let first_char = model_name.chars().next().unwrap_or('B');
447                            let avatar = EntityAvatar::Text(first_char.to_uppercase().to_string());
448                            (model_name, avatar)
449                        });
450
451                    let is_loading = message.metadata.is_writing() && message.content.is_empty();
452
453                    let item =
454                        if is_loading {
455                            let item = list.item(cx, index, id!(LoadingLine));
456                            item.message_loading(cx, ids!(content_section.loading))
457                                .animate(cx);
458                            item
459                        } else if !message.content.tool_calls.is_empty() {
460                            let item = list.item(cx, index, id!(ToolRequestLine));
461
462                            let has_pending = message.content.tool_calls.iter().any(|tc| {
463                                tc.permission_status == ToolCallPermissionStatus::Pending
464                            });
465                            let has_denied =
466                                message.content.tool_calls.iter().any(|tc| {
467                                    tc.permission_status == ToolCallPermissionStatus::Denied
468                                });
469
470                            item.view(cx, ids!(tool_actions))
471                                .set_visible(cx, has_pending);
472
473                            if has_denied {
474                                item.view(cx, ids!(status_view)).set_visible(cx, true);
475                                item.label(cx, ids!(approved_status)).set_text(cx, "Denied");
476                            } else {
477                                item.view(cx, ids!(status_view)).set_visible(cx, false);
478                            }
479
480                            item
481                        } else {
482                            list.item(cx, index, id!(BotLine))
483                        };
484
485                    item.avatar(cx, ids!(avatar)).borrow_mut().unwrap().avatar = Some(avatar);
486                    item.label(cx, ids!(name)).set_text(cx, name.as_str());
487
488                    if !is_loading {
489                        let mut slot = item.slot(cx, ids!(content));
490                        if let Some(custom_content) = self
491                            .custom_contents
492                            .iter_mut()
493                            .find_map(|cw| cw.content_widget(cx, slot.current(), &message.content))
494                        {
495                            slot.replace(custom_content);
496                        } else {
497                            slot.restore();
498                            slot.default()
499                                .as_standard_message_content()
500                                .set_content_with_metadata(cx, &message.content, &message.metadata);
501                        }
502
503                        let has_any_tool_calls = !message.content.tool_calls.is_empty();
504                        if !has_any_tool_calls {
505                            self.apply_editor_visibility(cx, &item, index);
506                        }
507                    }
508
509                    item
510                }
511            };
512
513            item.draw_all(cx, &mut Scope::empty());
514
515            if let Some(second_last_message_index) = second_last_message_index
516                && index == second_last_message_index
517            {
518                if did_filler_draw {
519                    self.needs_extra_draw_pass = true;
520                } else {
521                    second_last_message_height = item.area().rect(cx).size.y;
522                }
523            } else if let Some(last_message_index) = last_message_index
524                && index == last_message_index
525            {
526                last_message_height = item.area().rect(cx).size.y;
527            }
528        }
529
530        if let Some(message) = chat_controller.dangerous_state_mut().messages.pop() {
531            assert!(message.from == EntityId::App);
532            assert!(message.content.text == "EOC");
533        }
534
535        if let Some(message) = chat_controller.dangerous_state_mut().messages.pop() {
536            assert!(message.from == EntityId::App);
537            assert!(message.content.text.starts_with("FIL"));
538        }
539    }
540
541    /// Check if we're at the end of the messages list.
542    pub fn is_at_bottom(&self) -> bool {
543        self.is_list_end_drawn
544    }
545
546    /// Jump to the end of the list instantly.
547    pub fn instant_scroll_to_bottom(&mut self, cx: &mut Cx) {
548        let chat_controller = self
549            .chat_controller
550            .as_ref()
551            .expect("no chat controller set");
552
553        if chat_controller.lock().unwrap().state().messages.len() > 0 {
554            let list = self.portal_list(cx, ids!(list));
555
556            list.set_first_id_and_scroll(
557                chat_controller
558                    .lock()
559                    .unwrap()
560                    .state()
561                    .messages
562                    .len()
563                    .saturating_sub(1),
564                0.0,
565            );
566
567            self.redraw(cx);
568        }
569    }
570
571    /// Smoothly scroll to the end of the list.
572    ///
573    /// Warning: Do not continuously fire this method. Use
574    /// [`Self::instant_scroll_to_bottom`] instead.
575    pub fn animated_scroll_to_bottom(&mut self, cx: &mut Cx) {
576        if self.is_at_bottom() {
577            self.instant_scroll_to_bottom(cx);
578            return;
579        }
580
581        let chat_controller = self
582            .chat_controller
583            .as_ref()
584            .expect("no chat controller set");
585
586        if chat_controller.lock().unwrap().state().messages.len() > 0 {
587            let list = self.portal_list(cx, ids!(list));
588            list.smooth_scroll_to_end(cx, 100.0, None);
589        }
590    }
591
592    /// Show or hide the editor for a message.
593    ///
594    /// Limitation: Only one editor can be shown at a time. If you
595    /// try to show another editor, the previous one will be hidden.
596    pub fn set_message_editor_visibility(&mut self, index: usize, visible: bool) {
597        let chat_controller = self
598            .chat_controller
599            .as_ref()
600            .expect("no chat controller set")
601            .clone();
602
603        if index >= chat_controller.lock().unwrap().state().messages.len() {
604            return;
605        }
606
607        if visible {
608            let buffer = chat_controller.lock().unwrap().state().messages[index]
609                .content
610                .text
611                .clone();
612            self.current_editor = Some(Editor { index, buffer });
613        } else if self.current_editor.as_ref().map(|e| e.index) == Some(index) {
614            self.current_editor = None;
615        }
616    }
617
618    /// If currently editing a message, returns the text in its
619    /// editor.
620    pub fn current_editor_text(&self, cx: &Cx) -> Option<String> {
621        self.current_editor
622            .as_ref()
623            .and_then(|editor| self.portal_list(cx, ids!(list)).get_item(editor.index))
624            .map(|(_id, widget)| widget.text_input(cx, ids!(input)).text())
625    }
626
627    /// If currently editing a message, returns the index of the
628    /// message.
629    pub fn current_editor_index(&self) -> Option<usize> {
630        self.current_editor.as_ref().map(|e| e.index)
631    }
632
633    fn handle_list(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
634        let Some(range) = self.visible_range else {
635            return;
636        };
637
638        let list = self.portal_list(cx, ids!(list));
639        let range = range.0..=range.1;
640
641        for (index, item) in ItemsRangeIter::new(list, range) {
642            for action in item.filter_actions(event.actions()) {
643                match action.cast() {
644                    ChatLineAction::Copy => {
645                        cx.widget_action(self.widget_uid(), MessagesAction::Copy(index));
646                    }
647                    ChatLineAction::Delete => {
648                        cx.widget_action(self.widget_uid(), MessagesAction::Delete(index));
649                    }
650                    ChatLineAction::Edit => {
651                        self.set_message_editor_visibility(index, true);
652                        self.redraw(cx);
653                    }
654                    ChatLineAction::EditCancel => {
655                        self.set_message_editor_visibility(index, false);
656                        self.redraw(cx);
657                    }
658                    ChatLineAction::Save => {
659                        cx.widget_action(self.widget_uid(), MessagesAction::EditSave(index));
660                    }
661                    ChatLineAction::SaveAndRegenerate => {
662                        cx.widget_action(self.widget_uid(), MessagesAction::EditRegenerate(index));
663                    }
664                    ChatLineAction::ToolApprove => {
665                        cx.widget_action(self.widget_uid(), MessagesAction::ToolApprove(index));
666                    }
667                    ChatLineAction::ToolDeny => {
668                        cx.widget_action(self.widget_uid(), MessagesAction::ToolDeny(index));
669                    }
670                    ChatLineAction::EditorChanged => {
671                        let text = item.text_input(cx, ids!(input)).text();
672                        self.current_editor.as_mut().unwrap().buffer = text;
673                    }
674                    ChatLineAction::ErrorDetailsToggle => {
675                        if !self.expanded_error_details.remove(&index) {
676                            self.expanded_error_details.insert(index);
677                        }
678                        self.redraw(cx);
679                    }
680                    ChatLineAction::None => {}
681                }
682            }
683        }
684
685        // Handle code copy
686        for action in event.actions() {
687            let Some(wa) = action.downcast_ref::<WidgetAction>() else {
688                continue;
689            };
690            if !matches!(
691                wa.action.downcast_ref::<ButtonAction>(),
692                Some(ButtonAction::Clicked(_))
693            ) {
694                continue;
695            }
696            let tree = cx.widget_tree();
697            let path = tree.path_to(wa.widget_uid);
698            if !path.contains(&id!(copy_code_button)) {
699                continue;
700            }
701            let cv = tree.find_flood(wa.widget_uid, ids!(code_view));
702            let text = cv.as_code_view().text();
703            cx.copy_to_clipboard(&text);
704        }
705    }
706
707    fn apply_editor_visibility(&mut self, cx: &mut Cx, widget: &WidgetRef, index: usize) {
708        let editor = widget.view(cx, ids!(editor));
709        let edit_actions = widget.view(cx, ids!(edit_actions));
710        let content_section = widget.view(cx, ids!(content_section));
711
712        let is_current_editor = self.current_editor.as_ref().map(|e| e.index) == Some(index);
713
714        edit_actions.set_visible(cx, is_current_editor);
715        editor.set_visible(cx, is_current_editor);
716        content_section.set_visible(cx, !is_current_editor);
717
718        if is_current_editor {
719            editor
720                .text_input(cx, ids!(input))
721                .set_text(cx, &self.current_editor.as_ref().unwrap().buffer);
722        }
723    }
724
725    /// Registers a custom content provider for bot messages.
726    pub fn register_custom_content<T: CustomContent + 'static>(&mut self, widget: T) {
727        self.custom_contents.push(Box::new(widget));
728    }
729}
730
731/// Extracts an HTTP status code from error text matching the
732/// pattern "status {code}".
733fn extract_status_code(error_text: &str) -> Option<u16> {
734    let mut tokens = error_text.split_whitespace();
735    while let Some(token) = tokens.next() {
736        if token.eq_ignore_ascii_case("status") {
737            if let Some(code) = tokens.next().and_then(|t| t.parse::<u16>().ok()) {
738                return Some(code);
739            }
740        }
741    }
742    None
743}
744
745/// Returns a user-friendly note for common HTTP error status
746/// codes found in the error text, or an empty string if no match.
747fn error_note_for(error_text: &str) -> &'static str {
748    match extract_status_code(error_text) {
749        Some(429) => {
750            "This usually means you've hit a rate limit, \
751             run out of quota/credits, or do not have \
752             access to this resource/model in your \
753             current plan."
754        }
755        Some(401) => {
756            "This usually means your API key is invalid \
757             or expired."
758        }
759        Some(403) => {
760            "This usually means you do not have \
761             permission to access this resource."
762        }
763        Some(400) => {
764            "This might be an error on our side. If the \
765             problem persists, please file an issue on \
766             GitHub."
767        }
768        Some(500 | 502 | 503 | 504) => {
769            "A server error occurred. This is likely a \
770             temporary issue with the provider."
771        }
772        _ => "",
773    }
774}
775
776impl MessagesRef {
777    /// Immutable access to the underlying [`Messages`].
778    ///
779    /// Panics if the widget reference is empty or if it's already
780    /// borrowed.
781    pub fn read(&self) -> Ref<'_, Messages> {
782        self.borrow().unwrap()
783    }
784
785    /// Mutable access to the underlying [`Messages`].
786    ///
787    /// Panics if the widget reference is empty or if it's already
788    /// borrowed.
789    pub fn write(&mut self) -> RefMut<'_, Messages> {
790        self.borrow_mut().unwrap()
791    }
792
793    /// Immutable reader to the underlying [`Messages`].
794    ///
795    /// Panics if the widget reference is empty or if it's already
796    /// borrowed.
797    pub fn read_with<R>(&self, f: impl FnOnce(&Messages) -> R) -> R {
798        f(&*self.read())
799    }
800
801    /// Mutable writer to the underlying [`Messages`].
802    ///
803    /// Panics if the widget reference is empty or if it's already
804    /// borrowed.
805    pub fn write_with<R>(&mut self, f: impl FnOnce(&mut Messages) -> R) -> R {
806        f(&mut *self.write())
807    }
808}