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