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