assistant_configuration.rs

  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}