1use editor::{Editor, EditorElement, EditorStyle};
2use gpui::{AppContext, FocusableView, Model, TextStyle, View};
3use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
4use picker::Picker;
5use settings::Settings;
6use theme::ThemeSettings;
7use ui::{
8 prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
9 PopoverMenuHandle,
10};
11
12use crate::context_picker::{ContextPicker, ContextPickerDelegate};
13use crate::thread::{RequestKind, Thread};
14use crate::Chat;
15
16pub struct MessageEditor {
17 thread: Model<Thread>,
18 editor: View<Editor>,
19 pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
20 use_tools: bool,
21}
22
23impl MessageEditor {
24 pub fn new(thread: Model<Thread>, cx: &mut ViewContext<Self>) -> Self {
25 Self {
26 thread,
27 editor: cx.new_view(|cx| {
28 let mut editor = Editor::auto_height(80, cx);
29 editor.set_placeholder_text("Ask anything…", cx);
30
31 editor
32 }),
33 context_picker_handle: PopoverMenuHandle::default(),
34 use_tools: false,
35 }
36 }
37
38 fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
39 self.send_to_model(RequestKind::Chat, cx);
40 }
41
42 fn send_to_model(
43 &mut self,
44 request_kind: RequestKind,
45 cx: &mut ViewContext<Self>,
46 ) -> Option<()> {
47 let provider = LanguageModelRegistry::read_global(cx).active_provider();
48 if provider
49 .as_ref()
50 .map_or(false, |provider| provider.must_accept_terms(cx))
51 {
52 cx.notify();
53 return None;
54 }
55
56 let model_registry = LanguageModelRegistry::read_global(cx);
57 let model = model_registry.active_model()?;
58
59 let user_message = self.editor.update(cx, |editor, cx| {
60 let text = editor.text(cx);
61 editor.clear(cx);
62 text
63 });
64
65 self.thread.update(cx, |thread, cx| {
66 thread.insert_user_message(user_message, cx);
67 let mut request = thread.to_completion_request(request_kind, cx);
68
69 if self.use_tools {
70 request.tools = thread
71 .tools()
72 .tools(cx)
73 .into_iter()
74 .map(|tool| LanguageModelRequestTool {
75 name: tool.name(),
76 description: tool.description(),
77 input_schema: tool.input_schema(),
78 })
79 .collect();
80 }
81
82 thread.stream_completion(request, model, cx)
83 });
84
85 None
86 }
87}
88
89impl FocusableView for MessageEditor {
90 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
91 self.editor.focus_handle(cx)
92 }
93}
94
95impl Render for MessageEditor {
96 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
97 let font_size = TextSize::Default.rems(cx);
98 let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
99 let focus_handle = self.editor.focus_handle(cx);
100
101 v_flex()
102 .key_context("MessageEditor")
103 .on_action(cx.listener(Self::chat))
104 .size_full()
105 .gap_2()
106 .p_2()
107 .bg(cx.theme().colors().editor_background)
108 .child(
109 h_flex().gap_2().child(ContextPicker::new(
110 cx.view().downgrade(),
111 IconButton::new("add-context", IconName::Plus)
112 .shape(IconButtonShape::Square)
113 .icon_size(IconSize::Small),
114 )),
115 )
116 .child({
117 let settings = ThemeSettings::get_global(cx);
118 let text_style = TextStyle {
119 color: cx.theme().colors().editor_foreground,
120 font_family: settings.ui_font.family.clone(),
121 font_features: settings.ui_font.features.clone(),
122 font_size: font_size.into(),
123 font_weight: settings.ui_font.weight,
124 line_height: line_height.into(),
125 ..Default::default()
126 };
127
128 EditorElement::new(
129 &self.editor,
130 EditorStyle {
131 background: cx.theme().colors().editor_background,
132 local_player: cx.theme().players().local(),
133 text: text_style,
134 ..Default::default()
135 },
136 )
137 })
138 .child(
139 h_flex()
140 .justify_between()
141 .child(h_flex().gap_2().child(CheckboxWithLabel::new(
142 "use-tools",
143 Label::new("Tools"),
144 self.use_tools.into(),
145 cx.listener(|this, selection, _cx| {
146 this.use_tools = match selection {
147 Selection::Selected => true,
148 Selection::Unselected | Selection::Indeterminate => false,
149 };
150 }),
151 )))
152 .child(
153 h_flex()
154 .gap_2()
155 .child(Button::new("codebase", "Codebase").style(ButtonStyle::Filled))
156 .child(Label::new("or"))
157 .child(
158 ButtonLike::new("chat")
159 .style(ButtonStyle::Filled)
160 .layer(ElevationIndex::ModalSurface)
161 .child(Label::new("Chat"))
162 .children(
163 KeyBinding::for_action_in(&Chat, &focus_handle, cx)
164 .map(|binding| binding.into_any_element()),
165 )
166 .on_click(move |_event, cx| {
167 focus_handle.dispatch_action(&Chat, cx);
168 }),
169 ),
170 ),
171 )
172 }
173}