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