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