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