message_editor.rs

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