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