message_editor.rs

  1use std::rc::Rc;
  2
  3use editor::{Editor, EditorElement, EditorStyle};
  4use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakView};
  5use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
  6use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
  7use settings::Settings;
  8use theme::ThemeSettings;
  9use ui::{
 10    prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
 11    PopoverMenu, PopoverMenuHandle, Tooltip,
 12};
 13use workspace::Workspace;
 14
 15use crate::context::{Context, ContextId, ContextKind};
 16use crate::context_picker::ContextPicker;
 17use crate::thread::{RequestKind, Thread};
 18use crate::ui::ContextPill;
 19use crate::{Chat, ToggleModelSelector};
 20
 21pub struct MessageEditor {
 22    thread: Model<Thread>,
 23    editor: View<Editor>,
 24    context: Vec<Context>,
 25    next_context_id: ContextId,
 26    context_picker: View<ContextPicker>,
 27    pub(crate) context_picker_handle: PopoverMenuHandle<ContextPicker>,
 28    language_model_selector: View<LanguageModelSelector>,
 29    use_tools: bool,
 30}
 31
 32impl MessageEditor {
 33    pub fn new(
 34        workspace: WeakView<Workspace>,
 35        thread: Model<Thread>,
 36        cx: &mut ViewContext<Self>,
 37    ) -> Self {
 38        let weak_self = cx.view().downgrade();
 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 or type @ to add context", cx);
 44
 45                editor
 46            }),
 47            context: Vec::new(),
 48            next_context_id: ContextId(0),
 49            context_picker: cx.new_view(|cx| ContextPicker::new(workspace.clone(), weak_self, cx)),
 50            context_picker_handle: PopoverMenuHandle::default(),
 51            language_model_selector: cx.new_view(|cx| {
 52                LanguageModelSelector::new(
 53                    |model, _cx| {
 54                        println!("Selected {:?}", model.name());
 55                    },
 56                    cx,
 57                )
 58            }),
 59            use_tools: false,
 60        }
 61    }
 62
 63    pub fn insert_context(
 64        &mut self,
 65        kind: ContextKind,
 66        name: impl Into<SharedString>,
 67        text: impl Into<SharedString>,
 68    ) {
 69        self.context.push(Context {
 70            id: self.next_context_id.post_inc(),
 71            name: name.into(),
 72            kind,
 73            text: text.into(),
 74        });
 75    }
 76
 77    fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
 78        self.send_to_model(RequestKind::Chat, cx);
 79    }
 80
 81    fn send_to_model(
 82        &mut self,
 83        request_kind: RequestKind,
 84        cx: &mut ViewContext<Self>,
 85    ) -> Option<()> {
 86        let provider = LanguageModelRegistry::read_global(cx).active_provider();
 87        if provider
 88            .as_ref()
 89            .map_or(false, |provider| provider.must_accept_terms(cx))
 90        {
 91            cx.notify();
 92            return None;
 93        }
 94
 95        let model_registry = LanguageModelRegistry::read_global(cx);
 96        let model = model_registry.active_model()?;
 97
 98        let user_message = self.editor.update(cx, |editor, cx| {
 99            let text = editor.text(cx);
100            editor.clear(cx);
101            text
102        });
103        let context = self.context.drain(..).collect::<Vec<_>>();
104
105        self.thread.update(cx, |thread, cx| {
106            thread.insert_user_message(user_message, context, cx);
107            let mut request = thread.to_completion_request(request_kind, cx);
108
109            if self.use_tools {
110                request.tools = thread
111                    .tools()
112                    .tools(cx)
113                    .into_iter()
114                    .map(|tool| LanguageModelRequestTool {
115                        name: tool.name(),
116                        description: tool.description(),
117                        input_schema: tool.input_schema(),
118                    })
119                    .collect();
120            }
121
122            thread.stream_completion(request, model, cx)
123        });
124
125        None
126    }
127
128    fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
129        let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
130        let active_model = LanguageModelRegistry::read_global(cx).active_model();
131
132        LanguageModelSelectorPopoverMenu::new(
133            self.language_model_selector.clone(),
134            ButtonLike::new("active-model")
135                .style(ButtonStyle::Subtle)
136                .child(
137                    h_flex()
138                        .w_full()
139                        .gap_0p5()
140                        .child(
141                            div()
142                                .overflow_x_hidden()
143                                .flex_grow()
144                                .whitespace_nowrap()
145                                .child(match (active_provider, active_model) {
146                                    (Some(provider), Some(model)) => h_flex()
147                                        .gap_1()
148                                        .child(
149                                            Icon::new(
150                                                model.icon().unwrap_or_else(|| provider.icon()),
151                                            )
152                                            .color(Color::Muted)
153                                            .size(IconSize::XSmall),
154                                        )
155                                        .child(
156                                            Label::new(model.name().0)
157                                                .size(LabelSize::Small)
158                                                .color(Color::Muted),
159                                        )
160                                        .into_any_element(),
161                                    _ => Label::new("No model selected")
162                                        .size(LabelSize::Small)
163                                        .color(Color::Muted)
164                                        .into_any_element(),
165                                }),
166                        )
167                        .child(
168                            Icon::new(IconName::ChevronDown)
169                                .color(Color::Muted)
170                                .size(IconSize::XSmall),
171                        ),
172                )
173                .tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
174        )
175    }
176}
177
178impl FocusableView for MessageEditor {
179    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
180        self.editor.focus_handle(cx)
181    }
182}
183
184impl Render for MessageEditor {
185    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
186        let font_size = TextSize::Default.rems(cx);
187        let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
188        let focus_handle = self.editor.focus_handle(cx);
189        let context_picker = self.context_picker.clone();
190
191        v_flex()
192            .key_context("MessageEditor")
193            .on_action(cx.listener(Self::chat))
194            .size_full()
195            .gap_2()
196            .p_2()
197            .bg(cx.theme().colors().editor_background)
198            .child(
199                h_flex()
200                    .flex_wrap()
201                    .gap_2()
202                    .child(
203                        PopoverMenu::new("context-picker")
204                            .menu(move |_cx| Some(context_picker.clone()))
205                            .trigger(
206                                IconButton::new("add-context", IconName::Plus)
207                                    .shape(IconButtonShape::Square)
208                                    .icon_size(IconSize::Small),
209                            )
210                            .attach(gpui::AnchorCorner::TopLeft)
211                            .anchor(gpui::AnchorCorner::BottomLeft)
212                            .offset(gpui::Point {
213                                x: px(0.0),
214                                y: px(-16.0),
215                            })
216                            .with_handle(self.context_picker_handle.clone()),
217                    )
218                    .children(self.context.iter().map(|context| {
219                        ContextPill::new(context.clone()).on_remove({
220                            let context = context.clone();
221                            Rc::new(cx.listener(move |this, _event, cx| {
222                                this.context.retain(|other| other.id != context.id);
223                                cx.notify();
224                            }))
225                        })
226                    }))
227                    .when(!self.context.is_empty(), |parent| {
228                        parent.child(
229                            IconButton::new("remove-all-context", IconName::Eraser)
230                                .shape(IconButtonShape::Square)
231                                .icon_size(IconSize::Small)
232                                .tooltip(move |cx| Tooltip::text("Remove All Context", cx))
233                                .on_click(cx.listener(|this, _event, cx| {
234                                    this.context.clear();
235                                    cx.notify();
236                                })),
237                        )
238                    }),
239            )
240            .child({
241                let settings = ThemeSettings::get_global(cx);
242                let text_style = TextStyle {
243                    color: cx.theme().colors().editor_foreground,
244                    font_family: settings.ui_font.family.clone(),
245                    font_features: settings.ui_font.features.clone(),
246                    font_size: font_size.into(),
247                    font_weight: settings.ui_font.weight,
248                    line_height: line_height.into(),
249                    ..Default::default()
250                };
251
252                EditorElement::new(
253                    &self.editor,
254                    EditorStyle {
255                        background: cx.theme().colors().editor_background,
256                        local_player: cx.theme().players().local(),
257                        text: text_style,
258                        ..Default::default()
259                    },
260                )
261            })
262            .child(
263                h_flex()
264                    .justify_between()
265                    .child(h_flex().gap_2().child(CheckboxWithLabel::new(
266                        "use-tools",
267                        Label::new("Tools"),
268                        self.use_tools.into(),
269                        cx.listener(|this, selection, _cx| {
270                            this.use_tools = match selection {
271                                ToggleState::Selected => true,
272                                ToggleState::Unselected | ToggleState::Indeterminate => false,
273                            };
274                        }),
275                    )))
276                    .child(
277                        h_flex()
278                            .gap_2()
279                            .child(self.render_language_model_selector(cx))
280                            .child(
281                                ButtonLike::new("chat")
282                                    .style(ButtonStyle::Filled)
283                                    .layer(ElevationIndex::ModalSurface)
284                                    .child(Label::new("Submit"))
285                                    .children(
286                                        KeyBinding::for_action_in(&Chat, &focus_handle, cx)
287                                            .map(|binding| binding.into_any_element()),
288                                    )
289                                    .on_click(move |_event, cx| {
290                                        focus_handle.dispatch_action(&Chat, cx);
291                                    }),
292                            ),
293                    ),
294            )
295    }
296}