composer.rs
1use assistant_tooling::ToolRegistry;
2use client::User;
3use editor::{Editor, EditorElement, EditorStyle};
4use gpui::{FontStyle, FontWeight, TextStyle, View, WeakView, WhiteSpace};
5use settings::Settings;
6use std::sync::Arc;
7use theme::ThemeSettings;
8use ui::{popover_menu, prelude::*, Avatar, ButtonLike, ContextMenu, Tooltip};
9
10use crate::{AssistantChat, CompletionProvider, Submit, SubmitMode};
11
12#[derive(IntoElement)]
13pub struct Composer {
14 assistant_chat: WeakView<AssistantChat>,
15 model: String,
16 editor: View<Editor>,
17 player: Option<Arc<User>>,
18 can_submit: bool,
19 tool_registry: Arc<ToolRegistry>,
20}
21
22impl Composer {
23 pub fn new(
24 assistant_chat: WeakView<AssistantChat>,
25 model: String,
26 editor: View<Editor>,
27 player: Option<Arc<User>>,
28 can_submit: bool,
29 tool_registry: Arc<ToolRegistry>,
30 ) -> Self {
31 Self {
32 assistant_chat,
33 model,
34 editor,
35 player,
36 can_submit,
37 tool_registry,
38 }
39 }
40}
41
42impl RenderOnce for Composer {
43 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
44 let mut player_avatar = div().size(rems(20.0 / 16.0)).into_any_element();
45 if let Some(player) = self.player.clone() {
46 player_avatar = Avatar::new(player.avatar_uri.clone())
47 .size(rems(20.0 / 16.0))
48 .into_any_element();
49 }
50
51 let font_size = rems(0.875);
52 let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
53
54 h_flex()
55 .w_full()
56 .items_start()
57 .mt_4()
58 .gap_3()
59 .child(player_avatar)
60 .child(
61 v_flex()
62 .size_full()
63 .gap_1()
64 .child(
65 v_flex()
66 .w_full()
67 .p_4()
68 .bg(cx.theme().colors().editor_background)
69 .rounded_lg()
70 .child(
71 v_flex()
72 .justify_between()
73 .w_full()
74 .gap_1()
75 .min_h(line_height * 4 + px(74.0))
76 .child({
77 let settings = ThemeSettings::get_global(cx);
78 let text_style = TextStyle {
79 color: cx.theme().colors().editor_foreground,
80 font_family: settings.buffer_font.family.clone(),
81 font_features: settings.buffer_font.features.clone(),
82 font_size: font_size.into(),
83 font_weight: FontWeight::NORMAL,
84 font_style: FontStyle::Normal,
85 line_height: line_height.into(),
86 background_color: None,
87 underline: None,
88 strikethrough: None,
89 white_space: WhiteSpace::Normal,
90 };
91
92 EditorElement::new(
93 &self.editor,
94 EditorStyle {
95 background: cx.theme().colors().editor_background,
96 local_player: cx.theme().players().local(),
97 text: text_style,
98 ..Default::default()
99 },
100 )
101 })
102 .child(
103 h_flex()
104 .flex_none()
105 .gap_2()
106 .justify_between()
107 .w_full()
108 .child(
109 h_flex().gap_1().child(
110 // IconButton/button
111 // Toggle - if enabled, .selected(true).selected_style(IconButtonStyle::Filled)
112 //
113 // match status
114 // Tooltip::with_meta("some label explaining project index + status", "click to enable")
115 IconButton::new(
116 "add-context",
117 IconName::FileDoc,
118 )
119 .icon_color(Color::Muted),
120 ), // .child(
121 // IconButton::new(
122 // "add-context",
123 // IconName::Plus,
124 // )
125 // .icon_color(Color::Muted),
126 // ),
127 )
128 .child(
129 Button::new("send-button", "Send")
130 .style(ButtonStyle::Filled)
131 .disabled(!self.can_submit)
132 .on_click(|_, cx| {
133 cx.dispatch_action(Box::new(Submit(
134 SubmitMode::Codebase,
135 )))
136 })
137 .tooltip(|cx| {
138 Tooltip::for_action(
139 "Submit message",
140 &Submit(SubmitMode::Codebase),
141 cx,
142 )
143 }),
144 ),
145 ),
146 ),
147 )
148 .child(
149 h_flex()
150 .w_full()
151 .justify_between()
152 .child(ModelSelector::new(self.assistant_chat, self.model))
153 .children(self.tool_registry.status_views().iter().cloned()),
154 ),
155 )
156 }
157}
158
159#[derive(IntoElement)]
160struct ModelSelector {
161 assistant_chat: WeakView<AssistantChat>,
162 model: String,
163}
164
165impl ModelSelector {
166 pub fn new(assistant_chat: WeakView<AssistantChat>, model: String) -> Self {
167 Self {
168 assistant_chat,
169 model,
170 }
171 }
172}
173
174impl RenderOnce for ModelSelector {
175 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
176 popover_menu("model-switcher")
177 .menu(move |cx| {
178 ContextMenu::build(cx, |mut menu, cx| {
179 for model in CompletionProvider::get(cx).available_models() {
180 menu = menu.custom_entry(
181 {
182 let model = model.clone();
183 move |_| Label::new(model.clone()).into_any_element()
184 },
185 {
186 let assistant_chat = self.assistant_chat.clone();
187 move |cx| {
188 _ = assistant_chat.update(cx, |assistant_chat, cx| {
189 assistant_chat.model = model.clone();
190 cx.notify();
191 });
192 }
193 },
194 );
195 }
196 menu
197 })
198 .into()
199 })
200 .trigger(
201 ButtonLike::new("active-model")
202 .child(
203 h_flex()
204 .w_full()
205 .gap_0p5()
206 .child(
207 div()
208 .overflow_x_hidden()
209 .flex_grow()
210 .whitespace_nowrap()
211 .child(Label::new(self.model)),
212 )
213 .child(
214 div().child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
215 ),
216 )
217 .style(ButtonStyle::Subtle)
218 .tooltip(move |cx| Tooltip::text("Change Model", cx)),
219 )
220 .anchor(gpui::AnchorCorner::BottomRight)
221 }
222}