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