message_editor.rs

  1use editor::{Editor, EditorElement, EditorStyle};
  2use gpui::{AppContext, FocusableView, Model, TextStyle, View};
  3use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
  4use picker::Picker;
  5use settings::Settings;
  6use theme::ThemeSettings;
  7use ui::{
  8    prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
  9    PopoverMenuHandle,
 10};
 11
 12use crate::context_picker::{ContextPicker, ContextPickerDelegate};
 13use crate::thread::{RequestKind, Thread};
 14use crate::Chat;
 15
 16pub struct MessageEditor {
 17    thread: Model<Thread>,
 18    editor: View<Editor>,
 19    pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
 20    use_tools: bool,
 21}
 22
 23impl MessageEditor {
 24    pub fn new(thread: Model<Thread>, cx: &mut ViewContext<Self>) -> Self {
 25        Self {
 26            thread,
 27            editor: cx.new_view(|cx| {
 28                let mut editor = Editor::auto_height(80, cx);
 29                editor.set_placeholder_text("Ask anything…", cx);
 30
 31                editor
 32            }),
 33            context_picker_handle: PopoverMenuHandle::default(),
 34            use_tools: false,
 35        }
 36    }
 37
 38    fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
 39        self.send_to_model(RequestKind::Chat, cx);
 40    }
 41
 42    fn send_to_model(
 43        &mut self,
 44        request_kind: RequestKind,
 45        cx: &mut ViewContext<Self>,
 46    ) -> Option<()> {
 47        let provider = LanguageModelRegistry::read_global(cx).active_provider();
 48        if provider
 49            .as_ref()
 50            .map_or(false, |provider| provider.must_accept_terms(cx))
 51        {
 52            cx.notify();
 53            return None;
 54        }
 55
 56        let model_registry = LanguageModelRegistry::read_global(cx);
 57        let model = model_registry.active_model()?;
 58
 59        let user_message = self.editor.update(cx, |editor, cx| {
 60            let text = editor.text(cx);
 61            editor.clear(cx);
 62            text
 63        });
 64
 65        self.thread.update(cx, |thread, cx| {
 66            thread.insert_user_message(user_message, cx);
 67            let mut request = thread.to_completion_request(request_kind, cx);
 68
 69            if self.use_tools {
 70                request.tools = thread
 71                    .tools()
 72                    .tools(cx)
 73                    .into_iter()
 74                    .map(|tool| LanguageModelRequestTool {
 75                        name: tool.name(),
 76                        description: tool.description(),
 77                        input_schema: tool.input_schema(),
 78                    })
 79                    .collect();
 80            }
 81
 82            thread.stream_completion(request, model, cx)
 83        });
 84
 85        None
 86    }
 87}
 88
 89impl FocusableView for MessageEditor {
 90    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
 91        self.editor.focus_handle(cx)
 92    }
 93}
 94
 95impl Render for MessageEditor {
 96    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 97        let font_size = TextSize::Default.rems(cx);
 98        let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
 99        let focus_handle = self.editor.focus_handle(cx);
100
101        v_flex()
102            .key_context("MessageEditor")
103            .on_action(cx.listener(Self::chat))
104            .size_full()
105            .gap_2()
106            .p_2()
107            .bg(cx.theme().colors().editor_background)
108            .child(
109                h_flex().gap_2().child(ContextPicker::new(
110                    cx.view().downgrade(),
111                    IconButton::new("add-context", IconName::Plus)
112                        .shape(IconButtonShape::Square)
113                        .icon_size(IconSize::Small),
114                )),
115            )
116            .child({
117                let settings = ThemeSettings::get_global(cx);
118                let text_style = TextStyle {
119                    color: cx.theme().colors().editor_foreground,
120                    font_family: settings.ui_font.family.clone(),
121                    font_features: settings.ui_font.features.clone(),
122                    font_size: font_size.into(),
123                    font_weight: settings.ui_font.weight,
124                    line_height: line_height.into(),
125                    ..Default::default()
126                };
127
128                EditorElement::new(
129                    &self.editor,
130                    EditorStyle {
131                        background: cx.theme().colors().editor_background,
132                        local_player: cx.theme().players().local(),
133                        text: text_style,
134                        ..Default::default()
135                    },
136                )
137            })
138            .child(
139                h_flex()
140                    .justify_between()
141                    .child(h_flex().gap_2().child(CheckboxWithLabel::new(
142                        "use-tools",
143                        Label::new("Tools"),
144                        self.use_tools.into(),
145                        cx.listener(|this, selection, _cx| {
146                            this.use_tools = match selection {
147                                Selection::Selected => true,
148                                Selection::Unselected | Selection::Indeterminate => false,
149                            };
150                        }),
151                    )))
152                    .child(
153                        h_flex()
154                            .gap_2()
155                            .child(Button::new("codebase", "Codebase").style(ButtonStyle::Filled))
156                            .child(Label::new("or"))
157                            .child(
158                                ButtonLike::new("chat")
159                                    .style(ButtonStyle::Filled)
160                                    .layer(ElevationIndex::ModalSurface)
161                                    .child(Label::new("Chat"))
162                                    .children(
163                                        KeyBinding::for_action_in(&Chat, &focus_handle, cx)
164                                            .map(|binding| binding.into_any_element()),
165                                    )
166                                    .on_click(move |_event, cx| {
167                                        focus_handle.dispatch_action(&Chat, cx);
168                                    }),
169                            ),
170                    ),
171            )
172    }
173}