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