message_editor.rs

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