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