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