1use std::sync::Arc;
2
3use collections::HashMap;
4use gpui::{AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription, canvas};
5use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
6use ui::{ElevationIndex, prelude::*};
7use workspace::Item;
8
9pub struct ConfigurationView {
10 focus_handle: FocusHandle,
11 configuration_views: HashMap<LanguageModelProviderId, AnyView>,
12 _registry_subscription: Subscription,
13}
14
15impl ConfigurationView {
16 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
17 let focus_handle = cx.focus_handle();
18
19 let registry_subscription = cx.subscribe_in(
20 &LanguageModelRegistry::global(cx),
21 window,
22 |this, _, event: &language_model::Event, window, cx| match event {
23 language_model::Event::AddedProvider(provider_id) => {
24 let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
25 if let Some(provider) = provider {
26 this.add_configuration_view(&provider, window, cx);
27 }
28 }
29 language_model::Event::RemovedProvider(provider_id) => {
30 this.remove_configuration_view(provider_id);
31 }
32 _ => {}
33 },
34 );
35
36 let mut this = Self {
37 focus_handle,
38 configuration_views: HashMap::default(),
39 _registry_subscription: registry_subscription,
40 };
41 this.build_configuration_views(window, cx);
42 this
43 }
44
45 fn build_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
46 let providers = LanguageModelRegistry::read_global(cx).providers();
47 for provider in providers {
48 self.add_configuration_view(&provider, window, cx);
49 }
50 }
51
52 fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
53 self.configuration_views.remove(provider_id);
54 }
55
56 fn add_configuration_view(
57 &mut self,
58 provider: &Arc<dyn LanguageModelProvider>,
59 window: &mut Window,
60 cx: &mut Context<Self>,
61 ) {
62 let configuration_view = provider.configuration_view(window, cx);
63 self.configuration_views
64 .insert(provider.id(), configuration_view);
65 }
66
67 fn render_provider_view(
68 &mut self,
69 provider: &Arc<dyn LanguageModelProvider>,
70 cx: &mut Context<Self>,
71 ) -> Div {
72 let provider_id = provider.id().0.clone();
73 let provider_name = provider.name().0.clone();
74 let configuration_view = self.configuration_views.get(&provider.id()).cloned();
75
76 let open_new_context = cx.listener({
77 let provider = provider.clone();
78 move |_, _, _window, cx| {
79 cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
80 provider.clone(),
81 ))
82 }
83 });
84
85 v_flex()
86 .gap_2()
87 .child(
88 h_flex()
89 .justify_between()
90 .child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
91 .when(provider.is_authenticated(cx), move |this| {
92 this.child(
93 h_flex().justify_end().child(
94 Button::new(
95 SharedString::from(format!("new-context-{provider_id}")),
96 "Open New Chat",
97 )
98 .icon_position(IconPosition::Start)
99 .icon(IconName::Plus)
100 .style(ButtonStyle::Filled)
101 .layer(ElevationIndex::ModalSurface)
102 .on_click(open_new_context),
103 ),
104 )
105 }),
106 )
107 .child(
108 div()
109 .p(DynamicSpacing::Base08.rems(cx))
110 .bg(cx.theme().colors().surface_background)
111 .border_1()
112 .border_color(cx.theme().colors().border_variant)
113 .rounded_sm()
114 .when(configuration_view.is_none(), |this| {
115 this.child(div().child(Label::new(format!(
116 "No configuration view for {}",
117 provider_name
118 ))))
119 })
120 .when_some(configuration_view, |this, configuration_view| {
121 this.child(configuration_view)
122 }),
123 )
124 }
125}
126
127impl Render for ConfigurationView {
128 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
129 let providers = LanguageModelRegistry::read_global(cx).providers();
130 let provider_views = providers
131 .into_iter()
132 .map(|provider| self.render_provider_view(&provider, cx))
133 .collect::<Vec<_>>();
134
135 let mut element = v_flex()
136 .id("assistant-configuration-view")
137 .track_focus(&self.focus_handle(cx))
138 .bg(cx.theme().colors().editor_background)
139 .size_full()
140 .overflow_y_scroll()
141 .child(
142 v_flex()
143 .p(DynamicSpacing::Base16.rems(cx))
144 .border_b_1()
145 .border_color(cx.theme().colors().border)
146 .gap_1()
147 .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
148 .child(
149 Label::new(
150 "At least one LLM provider must be configured to use the Assistant.",
151 )
152 .color(Color::Muted),
153 ),
154 )
155 .child(
156 v_flex()
157 .p(DynamicSpacing::Base16.rems(cx))
158 .mt_1()
159 .gap_6()
160 .flex_1()
161 .children(provider_views),
162 )
163 .into_any();
164
165 // We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround
166 // because we couldn't the element to take up the size of the parent.
167 canvas(
168 move |bounds, window, cx| {
169 element.prepaint_as_root(bounds.origin, bounds.size.into(), window, cx);
170 element
171 },
172 |_, mut element, window, cx| {
173 element.paint(window, cx);
174 },
175 )
176 .flex_1()
177 .w_full()
178 }
179}
180
181pub enum ConfigurationViewEvent {
182 NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
183}
184
185impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
186
187impl Focusable for ConfigurationView {
188 fn focus_handle(&self, _: &App) -> FocusHandle {
189 self.focus_handle.clone()
190 }
191}
192
193impl Item for ConfigurationView {
194 type Event = ConfigurationViewEvent;
195
196 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
197 "Configuration".into()
198 }
199}