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