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