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}