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_2()
73 .child({
74 let settings = ThemeSettings::get_global(cx);
75 let text_style = TextStyle {
76 color: cx.theme().colors().editor_foreground,
77 font_family: settings.buffer_font.family.clone(),
78 font_features: settings.buffer_font.features.clone(),
79 font_size: font_size.into(),
80 font_weight: FontWeight::NORMAL,
81 font_style: FontStyle::Normal,
82 line_height: line_height.into(),
83 background_color: None,
84 underline: None,
85 strikethrough: None,
86 white_space: WhiteSpace::Normal,
87 };
88
89 EditorElement::new(
90 &self.editor,
91 EditorStyle {
92 background: cx.theme().colors().editor_background,
93 local_player: cx.theme().players().local(),
94 text: text_style,
95 ..Default::default()
96 },
97 )
98 })
99 .child(
100 h_flex()
101 .flex_none()
102 .gap_2()
103 .justify_between()
104 .w_full()
105 .child(
106 h_flex().gap_1().child(
107 // IconButton/button
108 // Toggle - if enabled, .selected(true).selected_style(IconButtonStyle::Filled)
109 //
110 // match status
111 // Tooltip::with_meta("some label explaining project index + status", "click to enable")
112 IconButton::new(
113 "add-context",
114 IconName::FileDoc,
115 )
116 .icon_color(Color::Muted),
117 ), // .child(
118 // IconButton::new(
119 // "add-context",
120 // IconName::Plus,
121 // )
122 // .icon_color(Color::Muted),
123 // ),
124 )
125 .child(
126 Button::new("send-button", "Send")
127 .style(ButtonStyle::Filled)
128 .disabled(!self.can_submit)
129 .on_click(|_, cx| {
130 cx.dispatch_action(Box::new(Submit(
131 SubmitMode::Codebase,
132 )))
133 })
134 .tooltip(|cx| {
135 Tooltip::for_action(
136 "Submit message",
137 &Submit(SubmitMode::Codebase),
138 cx,
139 )
140 }),
141 ),
142 ),
143 ),
144 )
145 .child(
146 h_flex()
147 .w_full()
148 .justify_between()
149 .child(self.model_selector)
150 .children(self.tool_registry.status_views().iter().cloned()),
151 ),
152 )
153 }
154}
155
156#[derive(IntoElement)]
157pub struct ModelSelector {
158 assistant_chat: WeakView<AssistantChat>,
159 model: String,
160}
161
162impl ModelSelector {
163 pub fn new(assistant_chat: WeakView<AssistantChat>, model: String) -> Self {
164 Self {
165 assistant_chat,
166 model,
167 }
168 }
169}
170
171impl RenderOnce for ModelSelector {
172 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
173 popover_menu("model-switcher")
174 .menu(move |cx| {
175 ContextMenu::build(cx, |mut menu, cx| {
176 for model in CompletionProvider::get(cx).available_models() {
177 menu = menu.custom_entry(
178 {
179 let model = model.clone();
180 move |_| Label::new(model.clone()).into_any_element()
181 },
182 {
183 let assistant_chat = self.assistant_chat.clone();
184 move |cx| {
185 _ = assistant_chat.update(cx, |assistant_chat, cx| {
186 assistant_chat.model = model.clone();
187 cx.notify();
188 });
189 }
190 },
191 );
192 }
193 menu
194 })
195 .into()
196 })
197 .trigger(
198 ButtonLike::new("active-model")
199 .child(
200 h_flex()
201 .w_full()
202 .gap_0p5()
203 .child(
204 div()
205 .overflow_x_hidden()
206 .flex_grow()
207 .whitespace_nowrap()
208 .child(Label::new(self.model)),
209 )
210 .child(
211 div().child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
212 ),
213 )
214 .style(ButtonStyle::Subtle)
215 .tooltip(move |cx| Tooltip::text("Change Model", cx)),
216 )
217 .anchor(gpui::AnchorCorner::BottomRight)
218 }
219}