ai_setup_page.rs

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