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