1use std::rc::Rc;
2
3use editor::{Editor, EditorElement, EditorStyle};
4use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakModel, WeakView};
5use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
6use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
7use settings::Settings;
8use theme::ThemeSettings;
9use ui::{
10 prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
11 PopoverMenu, PopoverMenuHandle, Tooltip,
12};
13use workspace::Workspace;
14
15use crate::context::{Context, ContextId, ContextKind};
16use crate::context_picker::ContextPicker;
17use crate::thread::{RequestKind, Thread};
18use crate::thread_store::ThreadStore;
19use crate::ui::ContextPill;
20use crate::{Chat, ToggleModelSelector};
21
22pub struct MessageEditor {
23 thread: Model<Thread>,
24 editor: View<Editor>,
25 context: Vec<Context>,
26 next_context_id: ContextId,
27 context_picker: View<ContextPicker>,
28 pub(crate) context_picker_handle: PopoverMenuHandle<ContextPicker>,
29 language_model_selector: View<LanguageModelSelector>,
30 use_tools: bool,
31}
32
33impl MessageEditor {
34 pub fn new(
35 workspace: WeakView<Workspace>,
36 thread_store: WeakModel<ThreadStore>,
37 thread: Model<Thread>,
38 cx: &mut ViewContext<Self>,
39 ) -> Self {
40 let weak_self = cx.view().downgrade();
41 Self {
42 thread,
43 editor: cx.new_view(|cx| {
44 let mut editor = Editor::auto_height(80, cx);
45 editor.set_placeholder_text("Ask anything or type @ to add context", cx);
46
47 editor
48 }),
49 context: Vec::new(),
50 next_context_id: ContextId(0),
51 context_picker: cx.new_view(|cx| {
52 ContextPicker::new(workspace.clone(), thread_store.clone(), weak_self, cx)
53 }),
54 context_picker_handle: PopoverMenuHandle::default(),
55 language_model_selector: cx.new_view(|cx| {
56 LanguageModelSelector::new(
57 |model, _cx| {
58 println!("Selected {:?}", model.name());
59 },
60 cx,
61 )
62 }),
63 use_tools: false,
64 }
65 }
66
67 pub fn insert_context(
68 &mut self,
69 kind: ContextKind,
70 name: impl Into<SharedString>,
71 text: impl Into<SharedString>,
72 ) {
73 self.context.push(Context {
74 id: self.next_context_id.post_inc(),
75 name: name.into(),
76 kind,
77 text: text.into(),
78 });
79 }
80
81 fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
82 self.send_to_model(RequestKind::Chat, cx);
83 }
84
85 fn send_to_model(
86 &mut self,
87 request_kind: RequestKind,
88 cx: &mut ViewContext<Self>,
89 ) -> Option<()> {
90 let provider = LanguageModelRegistry::read_global(cx).active_provider();
91 if provider
92 .as_ref()
93 .map_or(false, |provider| provider.must_accept_terms(cx))
94 {
95 cx.notify();
96 return None;
97 }
98
99 let model_registry = LanguageModelRegistry::read_global(cx);
100 let model = model_registry.active_model()?;
101
102 let user_message = self.editor.update(cx, |editor, cx| {
103 let text = editor.text(cx);
104 editor.clear(cx);
105 text
106 });
107 let context = self.context.drain(..).collect::<Vec<_>>();
108
109 self.thread.update(cx, |thread, cx| {
110 thread.insert_user_message(user_message, context, cx);
111 let mut request = thread.to_completion_request(request_kind, cx);
112
113 if self.use_tools {
114 request.tools = thread
115 .tools()
116 .tools(cx)
117 .into_iter()
118 .map(|tool| LanguageModelRequestTool {
119 name: tool.name(),
120 description: tool.description(),
121 input_schema: tool.input_schema(),
122 })
123 .collect();
124 }
125
126 thread.stream_completion(request, model, cx)
127 });
128
129 None
130 }
131
132 fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
133 let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
134 let active_model = LanguageModelRegistry::read_global(cx).active_model();
135
136 LanguageModelSelectorPopoverMenu::new(
137 self.language_model_selector.clone(),
138 ButtonLike::new("active-model")
139 .style(ButtonStyle::Subtle)
140 .child(
141 h_flex()
142 .w_full()
143 .gap_0p5()
144 .child(
145 div()
146 .overflow_x_hidden()
147 .flex_grow()
148 .whitespace_nowrap()
149 .child(match (active_provider, active_model) {
150 (Some(provider), Some(model)) => h_flex()
151 .gap_1()
152 .child(
153 Icon::new(
154 model.icon().unwrap_or_else(|| provider.icon()),
155 )
156 .color(Color::Muted)
157 .size(IconSize::XSmall),
158 )
159 .child(
160 Label::new(model.name().0)
161 .size(LabelSize::Small)
162 .color(Color::Muted),
163 )
164 .into_any_element(),
165 _ => Label::new("No model selected")
166 .size(LabelSize::Small)
167 .color(Color::Muted)
168 .into_any_element(),
169 }),
170 )
171 .child(
172 Icon::new(IconName::ChevronDown)
173 .color(Color::Muted)
174 .size(IconSize::XSmall),
175 ),
176 )
177 .tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
178 )
179 }
180}
181
182impl FocusableView for MessageEditor {
183 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
184 self.editor.focus_handle(cx)
185 }
186}
187
188impl Render for MessageEditor {
189 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
190 let font_size = TextSize::Default.rems(cx);
191 let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
192 let focus_handle = self.editor.focus_handle(cx);
193 let context_picker = self.context_picker.clone();
194
195 v_flex()
196 .key_context("MessageEditor")
197 .on_action(cx.listener(Self::chat))
198 .size_full()
199 .gap_2()
200 .p_2()
201 .bg(cx.theme().colors().editor_background)
202 .child(
203 h_flex()
204 .flex_wrap()
205 .gap_2()
206 .child(
207 PopoverMenu::new("context-picker")
208 .menu(move |_cx| Some(context_picker.clone()))
209 .trigger(
210 IconButton::new("add-context", IconName::Plus)
211 .shape(IconButtonShape::Square)
212 .icon_size(IconSize::Small),
213 )
214 .attach(gpui::AnchorCorner::TopLeft)
215 .anchor(gpui::AnchorCorner::BottomLeft)
216 .offset(gpui::Point {
217 x: px(0.0),
218 y: px(-16.0),
219 })
220 .with_handle(self.context_picker_handle.clone()),
221 )
222 .children(self.context.iter().map(|context| {
223 ContextPill::new(context.clone()).on_remove({
224 let context = context.clone();
225 Rc::new(cx.listener(move |this, _event, cx| {
226 this.context.retain(|other| other.id != context.id);
227 cx.notify();
228 }))
229 })
230 }))
231 .when(!self.context.is_empty(), |parent| {
232 parent.child(
233 IconButton::new("remove-all-context", IconName::Eraser)
234 .shape(IconButtonShape::Square)
235 .icon_size(IconSize::Small)
236 .tooltip(move |cx| Tooltip::text("Remove All Context", cx))
237 .on_click(cx.listener(|this, _event, cx| {
238 this.context.clear();
239 cx.notify();
240 })),
241 )
242 }),
243 )
244 .child({
245 let settings = ThemeSettings::get_global(cx);
246 let text_style = TextStyle {
247 color: cx.theme().colors().editor_foreground,
248 font_family: settings.ui_font.family.clone(),
249 font_features: settings.ui_font.features.clone(),
250 font_size: font_size.into(),
251 font_weight: settings.ui_font.weight,
252 line_height: line_height.into(),
253 ..Default::default()
254 };
255
256 EditorElement::new(
257 &self.editor,
258 EditorStyle {
259 background: cx.theme().colors().editor_background,
260 local_player: cx.theme().players().local(),
261 text: text_style,
262 ..Default::default()
263 },
264 )
265 })
266 .child(
267 h_flex()
268 .justify_between()
269 .child(h_flex().gap_2().child(CheckboxWithLabel::new(
270 "use-tools",
271 Label::new("Tools"),
272 self.use_tools.into(),
273 cx.listener(|this, selection, _cx| {
274 this.use_tools = match selection {
275 ToggleState::Selected => true,
276 ToggleState::Unselected | ToggleState::Indeterminate => false,
277 };
278 }),
279 )))
280 .child(
281 h_flex()
282 .gap_2()
283 .child(self.render_language_model_selector(cx))
284 .child(
285 ButtonLike::new("chat")
286 .style(ButtonStyle::Filled)
287 .layer(ElevationIndex::ModalSurface)
288 .child(Label::new("Submit"))
289 .children(
290 KeyBinding::for_action_in(&Chat, &focus_handle, cx)
291 .map(|binding| binding.into_any_element()),
292 )
293 .on_click(move |_event, cx| {
294 focus_handle.dispatch_action(&Chat, cx);
295 }),
296 ),
297 ),
298 )
299 }
300}