message_editor.rs

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