ai_setup_page.rs

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