message_editor.rs

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