assistant_configuration.rs

  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}