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