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}