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