ai_setup_page.rs

  1use std::sync::Arc;
  2
  3use ai_onboarding::AiUpsellCard;
  4use client::{Client, UserStore, zed_urls};
  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: [&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 (title, description) = if disabled {
 46        (
 47            "AI is disabled across Zed",
 48            "Re-enable it any time in Settings.",
 49        )
 50    } else {
 51        (
 52            "Privacy is the default for Zed",
 53            "Any use or storage of your data is with your explicit, single-use, opt-in consent.",
 54        )
 55    };
 56
 57    v_flex()
 58        .relative()
 59        .pt_2()
 60        .pb_2p5()
 61        .pl_3()
 62        .pr_2()
 63        .border_1()
 64        .border_dashed()
 65        .border_color(cx.theme().colors().border.opacity(0.5))
 66        .bg(cx.theme().colors().surface_background.opacity(0.3))
 67        .rounded_lg()
 68        .overflow_hidden()
 69        .child(
 70            h_flex()
 71                .gap_2()
 72                .justify_between()
 73                .child(Label::new(title))
 74                .child(
 75                    h_flex()
 76                        .gap_1()
 77                        .child(
 78                            Badge::new("Privacy")
 79                                .icon(IconName::ShieldCheck)
 80                                .tooltip(move |_, cx| cx.new(|_| AiPrivacyTooltip::new()).into()),
 81                        )
 82                        .child(
 83                            Button::new("learn_more", "Learn More")
 84                                .style(ButtonStyle::Outlined)
 85                                .label_size(LabelSize::Small)
 86                                .icon(IconName::ArrowUpRight)
 87                                .icon_size(IconSize::XSmall)
 88                                .icon_color(Color::Muted)
 89                                .on_click(|_, _, cx| {
 90                                    cx.open_url(&zed_urls::ai_privacy_and_security(cx))
 91                                })
 92                                .tab_index({
 93                                    *tab_index += 1;
 94                                    *tab_index - 1
 95                                }),
 96                        ),
 97                ),
 98        )
 99        .child(
100            Label::new(description)
101                .size(LabelSize::Small)
102                .color(Color::Muted),
103        )
104}
105
106fn render_llm_provider_card(
107    tab_index: &mut isize,
108    workspace: WeakEntity<Workspace>,
109    disabled: bool,
110    _: &mut Window,
111    cx: &mut App,
112) -> impl IntoElement {
113    let registry = LanguageModelRegistry::read_global(cx);
114
115    v_flex()
116        .border_1()
117        .border_color(cx.theme().colors().border)
118        .bg(cx.theme().colors().surface_background.opacity(0.5))
119        .rounded_lg()
120        .overflow_hidden()
121        .children(itertools::intersperse_with(
122            FEATURED_PROVIDERS
123                .into_iter()
124                .flat_map(|provider_name| {
125                    registry.provider(&LanguageModelProviderId::new(provider_name))
126                })
127                .enumerate()
128                .map(|(index, provider)| {
129                    let group_name = SharedString::new(format!("onboarding-hover-group-{}", index));
130                    let is_authenticated = provider.is_authenticated(cx);
131
132                    ButtonLike::new(("onboarding-ai-setup-buttons", index))
133                        .size(ButtonSize::Large)
134                        .tab_index({
135                            *tab_index += 1;
136                            *tab_index - 1
137                        })
138                        .child(
139                            h_flex()
140                                .group(&group_name)
141                                .px_0p5()
142                                .w_full()
143                                .gap_2()
144                                .justify_between()
145                                .child(
146                                    h_flex()
147                                        .gap_1()
148                                        .child(
149                                            Icon::new(provider.icon())
150                                                .color(Color::Muted)
151                                                .size(IconSize::XSmall),
152                                        )
153                                        .child(Label::new(provider.name().0)),
154                                )
155                                .child(
156                                    h_flex()
157                                        .gap_1()
158                                        .when(!is_authenticated, |el| {
159                                            el.visible_on_hover(group_name.clone())
160                                                .child(
161                                                    Icon::new(IconName::Settings)
162                                                        .color(Color::Muted)
163                                                        .size(IconSize::XSmall),
164                                                )
165                                                .child(
166                                                    Label::new("Configure")
167                                                        .color(Color::Muted)
168                                                        .size(LabelSize::Small),
169                                                )
170                                        })
171                                        .when(is_authenticated && !disabled, |el| {
172                                            el.child(
173                                                Icon::new(IconName::Check)
174                                                    .color(Color::Success)
175                                                    .size(IconSize::XSmall),
176                                            )
177                                            .child(
178                                                Label::new("Configured")
179                                                    .color(Color::Muted)
180                                                    .size(LabelSize::Small),
181                                            )
182                                        }),
183                                ),
184                        )
185                        .on_click({
186                            let workspace = workspace.clone();
187                            move |_, window, cx| {
188                                workspace
189                                    .update(cx, |workspace, cx| {
190                                        workspace.toggle_modal(window, cx, |window, cx| {
191                                            telemetry::event!(
192                                                "Welcome AI Modal Opened",
193                                                provider = provider.name().0,
194                                            );
195
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                .tab_index({
224                    *tab_index += 1;
225                    *tab_index - 1
226                }),
227        )
228}
229
230pub(crate) fn render_ai_setup_page(
231    workspace: WeakEntity<Workspace>,
232    user_store: Entity<UserStore>,
233    client: Arc<Client>,
234    window: &mut Window,
235    cx: &mut App,
236) -> impl IntoElement {
237    let mut tab_index = 0;
238    let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
239
240    v_flex()
241        .gap_2()
242        .child(
243            SwitchField::new(
244                "enable_ai",
245                "Enable AI features",
246                None,
247                if is_ai_disabled {
248                    ToggleState::Unselected
249                } else {
250                    ToggleState::Selected
251                },
252                |&toggle_state, _, cx| {
253                    let enabled = match toggle_state {
254                        ToggleState::Indeterminate => {
255                            return;
256                        }
257                        ToggleState::Unselected => true,
258                        ToggleState::Selected => false,
259                    };
260
261                    telemetry::event!(
262                        "Welcome AI Enabled",
263                        toggle = if enabled { "on" } else { "off" },
264                    );
265
266                    let fs = <dyn Fs>::global(cx);
267                    update_settings_file::<DisableAiSettings>(
268                        fs,
269                        cx,
270                        move |ai_settings: &mut Option<bool>, _| {
271                            *ai_settings = Some(enabled);
272                        },
273                    );
274                },
275            )
276            .tab_index({
277                tab_index += 1;
278                tab_index - 1
279            }),
280        )
281        .child(render_privacy_card(&mut tab_index, is_ai_disabled, cx))
282        .child(
283            v_flex()
284                .mt_2()
285                .gap_6()
286                .child(
287                    AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx)
288                        .tab_index(Some({
289                            tab_index += 1;
290                            tab_index - 1
291                        })),
292                )
293                .child(render_llm_provider_section(
294                    &mut tab_index,
295                    workspace,
296                    is_ai_disabled,
297                    window,
298                    cx,
299                ))
300                .when(is_ai_disabled, |this| {
301                    this.child(
302                        div()
303                            .id("backdrop")
304                            .size_full()
305                            .absolute()
306                            .inset_0()
307                            .bg(cx.theme().colors().editor_background)
308                            .opacity(0.8)
309                            .block_mouse_except_scroll(),
310                    )
311                }),
312        )
313}
314
315struct AiConfigurationModal {
316    focus_handle: FocusHandle,
317    selected_provider: Arc<dyn LanguageModelProvider>,
318    configuration_view: AnyView,
319}
320
321impl AiConfigurationModal {
322    fn new(
323        selected_provider: Arc<dyn LanguageModelProvider>,
324        window: &mut Window,
325        cx: &mut Context<Self>,
326    ) -> Self {
327        let focus_handle = cx.focus_handle();
328        let configuration_view = selected_provider.configuration_view(
329            language_model::ConfigurationViewTargetAgent::ZedAgent,
330            window,
331            cx,
332        );
333
334        Self {
335            focus_handle,
336            configuration_view,
337            selected_provider,
338        }
339    }
340
341    fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
342        cx.emit(DismissEvent);
343    }
344}
345
346impl ModalView for AiConfigurationModal {}
347
348impl EventEmitter<DismissEvent> for AiConfigurationModal {}
349
350impl Focusable for AiConfigurationModal {
351    fn focus_handle(&self, _cx: &App) -> FocusHandle {
352        self.focus_handle.clone()
353    }
354}
355
356impl Render for AiConfigurationModal {
357    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
358        v_flex()
359            .key_context("OnboardingAiConfigurationModal")
360            .w(rems(34.))
361            .elevation_3(cx)
362            .track_focus(&self.focus_handle)
363            .on_action(
364                cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
365            )
366            .child(
367                Modal::new("onboarding-ai-setup-modal", None)
368                    .header(
369                        ModalHeader::new()
370                            .icon(
371                                Icon::new(self.selected_provider.icon())
372                                    .color(Color::Muted)
373                                    .size(IconSize::Small),
374                            )
375                            .headline(self.selected_provider.name().0),
376                    )
377                    .section(Section::new().child(self.configuration_view.clone()))
378                    .footer(
379                        ModalFooter::new().end_slot(
380                            Button::new("ai-onb-modal-Done", "Done")
381                                .key_binding(
382                                    KeyBinding::for_action_in(
383                                        &menu::Cancel,
384                                        &self.focus_handle.clone(),
385                                        window,
386                                        cx,
387                                    )
388                                    .map(|kb| kb.size(rems_from_px(12.))),
389                                )
390                                .on_click(cx.listener(|this, _event, _window, cx| {
391                                    this.cancel(&menu::Cancel, cx)
392                                })),
393                        ),
394                    ),
395            )
396    }
397}
398
399pub struct AiPrivacyTooltip {}
400
401impl AiPrivacyTooltip {
402    pub fn new() -> Self {
403        Self {}
404    }
405}
406
407impl Render for AiPrivacyTooltip {
408    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
409        const DESCRIPTION: &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. ";
410
411        tooltip_container(window, cx, move |this, _, _| {
412            this.child(
413                h_flex()
414                    .gap_1()
415                    .child(
416                        Icon::new(IconName::ShieldCheck)
417                            .size(IconSize::Small)
418                            .color(Color::Muted),
419                    )
420                    .child(Label::new("Privacy First")),
421            )
422            .child(
423                div().max_w_64().child(
424                    Label::new(DESCRIPTION)
425                        .size(LabelSize::Small)
426                        .color(Color::Muted),
427                ),
428            )
429        })
430    }
431}