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