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