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