message_editor.rs

  1use std::sync::Arc;
  2
  3use editor::{Editor, EditorElement, EditorStyle};
  4use fs::Fs;
  5use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakModel, WeakView};
  6use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
  7use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
  8use settings::{update_settings_file, Settings};
  9use theme::ThemeSettings;
 10use ui::{
 11    prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, PopoverMenuHandle,
 12    Tooltip,
 13};
 14use workspace::Workspace;
 15
 16use crate::assistant_settings::AssistantSettings;
 17use crate::context_picker::ContextPicker;
 18use crate::context_store::ContextStore;
 19use crate::context_strip::ContextStrip;
 20use crate::thread::{RequestKind, Thread};
 21use crate::thread_store::ThreadStore;
 22use crate::{Chat, ToggleContextPicker, ToggleModelSelector};
 23
 24pub struct MessageEditor {
 25    thread: Model<Thread>,
 26    editor: View<Editor>,
 27    context_store: Model<ContextStore>,
 28    context_strip: View<ContextStrip>,
 29    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
 30    language_model_selector: View<LanguageModelSelector>,
 31    language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
 32    use_tools: bool,
 33}
 34
 35impl MessageEditor {
 36    pub fn new(
 37        fs: Arc<dyn Fs>,
 38        workspace: WeakView<Workspace>,
 39        thread_store: WeakModel<ThreadStore>,
 40        thread: Model<Thread>,
 41        cx: &mut ViewContext<Self>,
 42    ) -> Self {
 43        let context_store = cx.new_model(|_cx| ContextStore::new());
 44        let context_picker_menu_handle = PopoverMenuHandle::default();
 45
 46        let editor = cx.new_view(|cx| {
 47            let mut editor = Editor::auto_height(80, cx);
 48            editor.set_placeholder_text("Ask anything, @ to add context", cx);
 49            editor.set_show_indent_guides(false, cx);
 50
 51            editor
 52        });
 53
 54        Self {
 55            thread,
 56            editor: editor.clone(),
 57            context_store: context_store.clone(),
 58            context_strip: cx.new_view(|cx| {
 59                ContextStrip::new(
 60                    context_store,
 61                    workspace.clone(),
 62                    Some(thread_store.clone()),
 63                    editor.focus_handle(cx),
 64                    context_picker_menu_handle.clone(),
 65                    cx,
 66                )
 67            }),
 68            context_picker_menu_handle,
 69            language_model_selector: cx.new_view(|cx| {
 70                let fs = fs.clone();
 71                LanguageModelSelector::new(
 72                    move |model, cx| {
 73                        update_settings_file::<AssistantSettings>(
 74                            fs.clone(),
 75                            cx,
 76                            move |settings, _cx| settings.set_model(model.clone()),
 77                        );
 78                    },
 79                    cx,
 80                )
 81            }),
 82            language_model_selector_menu_handle: PopoverMenuHandle::default(),
 83            use_tools: false,
 84        }
 85    }
 86
 87    fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
 88        self.language_model_selector_menu_handle.toggle(cx);
 89    }
 90
 91    fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
 92        self.context_picker_menu_handle.toggle(cx);
 93    }
 94
 95    fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
 96        self.send_to_model(RequestKind::Chat, cx);
 97    }
 98
 99    fn send_to_model(
100        &mut self,
101        request_kind: RequestKind,
102        cx: &mut ViewContext<Self>,
103    ) -> Option<()> {
104        let provider = LanguageModelRegistry::read_global(cx).active_provider();
105        if provider
106            .as_ref()
107            .map_or(false, |provider| provider.must_accept_terms(cx))
108        {
109            cx.notify();
110            return None;
111        }
112
113        let model_registry = LanguageModelRegistry::read_global(cx);
114        let model = model_registry.active_model()?;
115
116        let user_message = self.editor.update(cx, |editor, cx| {
117            let text = editor.text(cx);
118            editor.clear(cx);
119            text
120        });
121        let context = self.context_store.update(cx, |this, _cx| this.drain());
122
123        self.thread.update(cx, |thread, cx| {
124            thread.insert_user_message(user_message, context, cx);
125            let mut request = thread.to_completion_request(request_kind, cx);
126
127            if self.use_tools {
128                request.tools = thread
129                    .tools()
130                    .tools(cx)
131                    .into_iter()
132                    .map(|tool| LanguageModelRequestTool {
133                        name: tool.name(),
134                        description: tool.description(),
135                        input_schema: tool.input_schema(),
136                    })
137                    .collect();
138            }
139
140            thread.stream_completion(request, model, cx)
141        });
142
143        None
144    }
145
146    fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
147        let active_model = LanguageModelRegistry::read_global(cx).active_model();
148        let focus_handle = self.language_model_selector.focus_handle(cx).clone();
149
150        LanguageModelSelectorPopoverMenu::new(
151            self.language_model_selector.clone(),
152            ButtonLike::new("active-model")
153                .style(ButtonStyle::Subtle)
154                .child(
155                    h_flex()
156                        .w_full()
157                        .gap_0p5()
158                        .child(
159                            div()
160                                .overflow_x_hidden()
161                                .flex_grow()
162                                .whitespace_nowrap()
163                                .child(match active_model {
164                                    Some(model) => h_flex()
165                                        .child(
166                                            Label::new(model.name().0)
167                                                .size(LabelSize::Small)
168                                                .color(Color::Muted),
169                                        )
170                                        .into_any_element(),
171                                    _ => Label::new("No model selected")
172                                        .size(LabelSize::Small)
173                                        .color(Color::Muted)
174                                        .into_any_element(),
175                                }),
176                        )
177                        .child(
178                            Icon::new(IconName::ChevronDown)
179                                .color(Color::Muted)
180                                .size(IconSize::XSmall),
181                        ),
182                )
183                .tooltip(move |cx| {
184                    Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
185                }),
186        )
187        .with_handle(self.language_model_selector_menu_handle.clone())
188    }
189}
190
191impl FocusableView for MessageEditor {
192    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
193        self.editor.focus_handle(cx)
194    }
195}
196
197impl Render for MessageEditor {
198    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
199        let font_size = TextSize::Default.rems(cx);
200        let line_height = font_size.to_pixels(cx.rem_size()) * 1.5;
201        let focus_handle = self.editor.focus_handle(cx);
202        let bg_color = cx.theme().colors().editor_background;
203
204        v_flex()
205            .key_context("MessageEditor")
206            .on_action(cx.listener(Self::chat))
207            .on_action(cx.listener(Self::toggle_model_selector))
208            .on_action(cx.listener(Self::toggle_context_picker))
209            .size_full()
210            .gap_2()
211            .p_2()
212            .bg(bg_color)
213            .child(self.context_strip.clone())
214            .child(div().id("thread_editor").overflow_y_scroll().h_12().child({
215                let settings = ThemeSettings::get_global(cx);
216                let text_style = TextStyle {
217                    color: cx.theme().colors().editor_foreground,
218                    font_family: settings.ui_font.family.clone(),
219                    font_features: settings.ui_font.features.clone(),
220                    font_size: font_size.into(),
221                    font_weight: settings.ui_font.weight,
222                    line_height: line_height.into(),
223                    ..Default::default()
224                };
225
226                EditorElement::new(
227                    &self.editor,
228                    EditorStyle {
229                        background: bg_color,
230                        local_player: cx.theme().players().local(),
231                        text: text_style,
232                        ..Default::default()
233                    },
234                )
235            }))
236            .child(
237                h_flex()
238                    .justify_between()
239                    .child(CheckboxWithLabel::new(
240                        "use-tools",
241                        Label::new("Tools"),
242                        self.use_tools.into(),
243                        cx.listener(|this, selection, _cx| {
244                            this.use_tools = match selection {
245                                ToggleState::Selected => true,
246                                ToggleState::Unselected | ToggleState::Indeterminate => false,
247                            };
248                        }),
249                    ))
250                    .child(
251                        h_flex()
252                            .gap_1()
253                            .child(self.render_language_model_selector(cx))
254                            .child(
255                                ButtonLike::new("chat")
256                                    .style(ButtonStyle::Filled)
257                                    .layer(ElevationIndex::ModalSurface)
258                                    .child(Label::new("Submit"))
259                                    .children(
260                                        KeyBinding::for_action_in(&Chat, &focus_handle, cx)
261                                            .map(|binding| binding.into_any_element()),
262                                    )
263                                    .on_click(move |_event, cx| {
264                                        focus_handle.dispatch_action(&Chat, cx);
265                                    }),
266                            ),
267                    ),
268            )
269    }
270}