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