ai_setup_page.rs

  1use std::sync::Arc;
  2
  3use ai_onboarding::{AiUpsellCard, SignInStatus};
  4use client::DisableAiSettings;
  5use fs::Fs;
  6use gpui::{
  7    Action, AnyView, App, DismissEvent, EventEmitter, FocusHandle, Focusable, Window, prelude::*,
  8};
  9use itertools;
 10
 11use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
 12use settings::{Settings, update_settings_file};
 13use ui::{
 14    Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState,
 15    prelude::*,
 16};
 17use workspace::ModalView;
 18
 19use util::ResultExt;
 20use zed_actions::agent::OpenSettings;
 21
 22use crate::Onboarding;
 23
 24const FEATURED_PROVIDERS: [&'static str; 4] = ["anthropic", "google", "openai", "ollama"];
 25
 26fn render_llm_provider_section(
 27    onboarding: &Onboarding,
 28    disabled: bool,
 29    window: &mut Window,
 30    cx: &mut App,
 31) -> impl IntoElement {
 32    v_flex()
 33        .gap_4()
 34        .child(
 35            v_flex()
 36                .child(Label::new("Or use other LLM providers").size(LabelSize::Large))
 37                .child(
 38                    Label::new("Bring your API keys to use the available providers with Zed's UI for free.")
 39                        .color(Color::Muted),
 40                ),
 41        )
 42        .child(render_llm_provider_card(onboarding, disabled, window, cx))
 43}
 44
 45fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement {
 46    let privacy_badge = || Badge::new("Privacy").icon(IconName::ShieldCheck);
 47
 48    v_flex()
 49        .relative()
 50        .pt_2()
 51        .pb_2p5()
 52        .pl_3()
 53        .pr_2()
 54        .border_1()
 55        .border_dashed()
 56        .border_color(cx.theme().colors().border.opacity(0.5))
 57        .bg(cx.theme().colors().surface_background.opacity(0.3))
 58        .rounded_lg()
 59        .overflow_hidden()
 60        .map(|this| {
 61            if disabled {
 62                this.child(
 63                    h_flex()
 64                        .gap_2()
 65                        .justify_between()
 66                        .child(
 67                            h_flex()
 68                                .gap_1()
 69                                .child(Label::new("AI is disabled across Zed"))
 70                                .child(
 71                                    Icon::new(IconName::Check)
 72                                        .color(Color::Success)
 73                                        .size(IconSize::XSmall),
 74                                ),
 75                        )
 76                        .child(privacy_badge()),
 77                )
 78                .child(
 79                    Label::new("Re-enable it any time in Settings.")
 80                        .size(LabelSize::Small)
 81                        .color(Color::Muted),
 82                )
 83            } else {
 84                this.child(
 85                    h_flex()
 86                        .gap_2()
 87                        .justify_between()
 88                        .child(Label::new("We don't train models using your data"))
 89                        .child(
 90                            h_flex().gap_1().child(privacy_badge()).child(
 91                                Button::new("learn_more", "Learn More")
 92                                    .style(ButtonStyle::Outlined)
 93                                    .label_size(LabelSize::Small)
 94                                    .icon(IconName::ArrowUpRight)
 95                                    .icon_size(IconSize::XSmall)
 96                                    .icon_color(Color::Muted)
 97                                    .on_click(|_, _, cx| {
 98                                        cx.open_url("https://zed.dev/docs/ai/privacy-and-security");
 99                                    }),
100                            ),
101                        ),
102                )
103                .child(
104                    Label::new(
105                        "Feel confident in the security and privacy of your projects using Zed.",
106                    )
107                    .size(LabelSize::Small)
108                    .color(Color::Muted),
109                )
110            }
111        })
112}
113
114fn render_llm_provider_card(
115    onboarding: &Onboarding,
116    disabled: bool,
117    _: &mut Window,
118    cx: &mut App,
119) -> impl IntoElement {
120    let registry = LanguageModelRegistry::read_global(cx);
121
122    v_flex()
123        .border_1()
124        .border_color(cx.theme().colors().border)
125        .bg(cx.theme().colors().surface_background.opacity(0.5))
126        .rounded_lg()
127        .overflow_hidden()
128        .children(itertools::intersperse_with(
129            FEATURED_PROVIDERS
130                .into_iter()
131                .flat_map(|provider_name| {
132                    registry.provider(&LanguageModelProviderId::new(provider_name))
133                })
134                .enumerate()
135                .map(|(index, provider)| {
136                    let group_name = SharedString::new(format!("onboarding-hover-group-{}", index));
137                    let is_authenticated = provider.is_authenticated(cx);
138
139                    ButtonLike::new(("onboarding-ai-setup-buttons", index))
140                        .size(ButtonSize::Large)
141                        .child(
142                            h_flex()
143                                .group(&group_name)
144                                .px_0p5()
145                                .w_full()
146                                .gap_2()
147                                .justify_between()
148                                .child(
149                                    h_flex()
150                                        .gap_1()
151                                        .child(
152                                            Icon::new(provider.icon())
153                                                .color(Color::Muted)
154                                                .size(IconSize::XSmall),
155                                        )
156                                        .child(Label::new(provider.name().0)),
157                                )
158                                .child(
159                                    h_flex()
160                                        .gap_1()
161                                        .when(!is_authenticated, |el| {
162                                            el.visible_on_hover(group_name.clone())
163                                                .child(
164                                                    Icon::new(IconName::Settings)
165                                                        .color(Color::Muted)
166                                                        .size(IconSize::XSmall),
167                                                )
168                                                .child(
169                                                    Label::new("Configure")
170                                                        .color(Color::Muted)
171                                                        .size(LabelSize::Small),
172                                                )
173                                        })
174                                        .when(is_authenticated && !disabled, |el| {
175                                            el.child(
176                                                Icon::new(IconName::Check)
177                                                    .color(Color::Success)
178                                                    .size(IconSize::XSmall),
179                                            )
180                                            .child(
181                                                Label::new("Configured")
182                                                    .color(Color::Muted)
183                                                    .size(LabelSize::Small),
184                                            )
185                                        }),
186                                ),
187                        )
188                        .on_click({
189                            let workspace = onboarding.workspace.clone();
190                            move |_, window, cx| {
191                                workspace
192                                    .update(cx, |workspace, cx| {
193                                        workspace.toggle_modal(window, cx, |window, cx| {
194                                            let modal = AiConfigurationModal::new(
195                                                provider.clone(),
196                                                window,
197                                                cx,
198                                            );
199                                            window.focus(&modal.focus_handle(cx));
200                                            modal
201                                        });
202                                    })
203                                    .log_err();
204                            }
205                        })
206                        .into_any_element()
207                }),
208            || Divider::horizontal().into_any_element(),
209        ))
210        .child(Divider::horizontal())
211        .child(
212            Button::new("agent_settings", "Add Many Others")
213                .size(ButtonSize::Large)
214                .icon(IconName::Plus)
215                .icon_position(IconPosition::Start)
216                .icon_color(Color::Muted)
217                .icon_size(IconSize::XSmall)
218                .on_click(|_event, window, cx| {
219                    window.dispatch_action(OpenSettings.boxed_clone(), cx)
220                }),
221        )
222}
223
224pub(crate) fn render_ai_setup_page(
225    onboarding: &Onboarding,
226    window: &mut Window,
227    cx: &mut App,
228) -> impl IntoElement {
229    let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
230
231    let backdrop = div()
232        .id("backdrop")
233        .size_full()
234        .absolute()
235        .inset_0()
236        .bg(cx.theme().colors().editor_background)
237        .opacity(0.8)
238        .block_mouse_except_scroll();
239
240    v_flex()
241        .gap_2()
242        .child(SwitchField::new(
243            "enable_ai",
244            "Enable AI features",
245            None,
246            if is_ai_disabled {
247                ToggleState::Unselected
248            } else {
249                ToggleState::Selected
250            },
251            |toggle_state, _, cx| {
252                let enabled = match toggle_state {
253                    ToggleState::Indeterminate => {
254                        return;
255                    }
256                    ToggleState::Unselected => false,
257                    ToggleState::Selected => true,
258                };
259
260                let fs = <dyn Fs>::global(cx);
261                update_settings_file::<DisableAiSettings>(
262                    fs,
263                    cx,
264                    move |ai_settings: &mut Option<bool>, _| {
265                        *ai_settings = Some(!enabled);
266                    },
267                );
268            },
269        ))
270        .child(render_privacy_card(is_ai_disabled, cx))
271        .child(
272            v_flex()
273                .mt_2()
274                .gap_6()
275                .child(AiUpsellCard {
276                    sign_in_status: SignInStatus::SignedIn,
277                    sign_in: Arc::new(|_, _| {}),
278                    user_plan: onboarding.user_store.read(cx).plan(),
279                })
280                .child(render_llm_provider_section(
281                    onboarding,
282                    is_ai_disabled,
283                    window,
284                    cx,
285                ))
286                .when(is_ai_disabled, |this| this.child(backdrop)),
287        )
288}
289
290struct AiConfigurationModal {
291    focus_handle: FocusHandle,
292    selected_provider: Arc<dyn LanguageModelProvider>,
293    configuration_view: AnyView,
294}
295
296impl AiConfigurationModal {
297    fn new(
298        selected_provider: Arc<dyn LanguageModelProvider>,
299        window: &mut Window,
300        cx: &mut Context<Self>,
301    ) -> Self {
302        let focus_handle = cx.focus_handle();
303        let configuration_view = selected_provider.configuration_view(window, cx);
304
305        Self {
306            focus_handle,
307            configuration_view,
308            selected_provider,
309        }
310    }
311}
312
313impl ModalView for AiConfigurationModal {}
314
315impl EventEmitter<DismissEvent> for AiConfigurationModal {}
316
317impl Focusable for AiConfigurationModal {
318    fn focus_handle(&self, _cx: &App) -> FocusHandle {
319        self.focus_handle.clone()
320    }
321}
322
323impl Render for AiConfigurationModal {
324    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
325        v_flex()
326            .w(rems(34.))
327            .elevation_3(cx)
328            .track_focus(&self.focus_handle)
329            .child(
330                Modal::new("onboarding-ai-setup-modal", None)
331                    .header(
332                        ModalHeader::new()
333                            .icon(
334                                Icon::new(self.selected_provider.icon())
335                                    .color(Color::Muted)
336                                    .size(IconSize::Small),
337                            )
338                            .headline(self.selected_provider.name().0),
339                    )
340                    .section(Section::new().child(self.configuration_view.clone()))
341                    .footer(
342                        ModalFooter::new().end_slot(
343                            h_flex()
344                                .gap_1()
345                                .child(
346                                    Button::new("onboarding-closing-cancel", "Cancel")
347                                        .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
348                                )
349                                .child(Button::new("save-btn", "Done").on_click(cx.listener(
350                                    |_, _, window, cx| {
351                                        window.dispatch_action(menu::Confirm.boxed_clone(), cx);
352                                        cx.emit(DismissEvent);
353                                    },
354                                ))),
355                        ),
356                    ),
357            )
358    }
359}