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}