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