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::*, tooltip_container,
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 = || {
45 Badge::new("Privacy")
46 .icon(IconName::ShieldCheck)
47 .tooltip(move |_, cx| cx.new(|_| AiPrivacyTooltip::new()).into())
48 };
49
50 v_flex()
51 .relative()
52 .pt_2()
53 .pb_2p5()
54 .pl_3()
55 .pr_2()
56 .border_1()
57 .border_dashed()
58 .border_color(cx.theme().colors().border.opacity(0.5))
59 .bg(cx.theme().colors().surface_background.opacity(0.3))
60 .rounded_lg()
61 .overflow_hidden()
62 .map(|this| {
63 if disabled {
64 this.child(
65 h_flex()
66 .gap_2()
67 .justify_between()
68 .child(
69 h_flex()
70 .gap_1()
71 .child(Label::new("AI is disabled across Zed"))
72 .child(
73 Icon::new(IconName::Check)
74 .color(Color::Success)
75 .size(IconSize::XSmall),
76 ),
77 )
78 .child(privacy_badge()),
79 )
80 .child(
81 Label::new("Re-enable it any time in Settings.")
82 .size(LabelSize::Small)
83 .color(Color::Muted),
84 )
85 } else {
86 this.child(
87 h_flex()
88 .gap_2()
89 .justify_between()
90 .child(Label::new("We don't train models using your data"))
91 .child(
92 h_flex().gap_1().child(privacy_badge()).child(
93 Button::new("learn_more", "Learn More")
94 .style(ButtonStyle::Outlined)
95 .label_size(LabelSize::Small)
96 .icon(IconName::ArrowUpRight)
97 .icon_size(IconSize::XSmall)
98 .icon_color(Color::Muted)
99 .on_click(|_, _, cx| {
100 cx.open_url("https://zed.dev/docs/ai/privacy-and-security");
101 }),
102 ),
103 ),
104 )
105 .child(
106 Label::new(
107 "Feel confident in the security and privacy of your projects using Zed.",
108 )
109 .size(LabelSize::Small)
110 .color(Color::Muted),
111 )
112 }
113 })
114}
115
116fn render_llm_provider_card(
117 onboarding: &Onboarding,
118 disabled: bool,
119 _: &mut Window,
120 cx: &mut App,
121) -> impl IntoElement {
122 let registry = LanguageModelRegistry::read_global(cx);
123
124 v_flex()
125 .border_1()
126 .border_color(cx.theme().colors().border)
127 .bg(cx.theme().colors().surface_background.opacity(0.5))
128 .rounded_lg()
129 .overflow_hidden()
130 .children(itertools::intersperse_with(
131 FEATURED_PROVIDERS
132 .into_iter()
133 .flat_map(|provider_name| {
134 registry.provider(&LanguageModelProviderId::new(provider_name))
135 })
136 .enumerate()
137 .map(|(index, provider)| {
138 let group_name = SharedString::new(format!("onboarding-hover-group-{}", index));
139 let is_authenticated = provider.is_authenticated(cx);
140
141 ButtonLike::new(("onboarding-ai-setup-buttons", index))
142 .size(ButtonSize::Large)
143 .child(
144 h_flex()
145 .group(&group_name)
146 .px_0p5()
147 .w_full()
148 .gap_2()
149 .justify_between()
150 .child(
151 h_flex()
152 .gap_1()
153 .child(
154 Icon::new(provider.icon())
155 .color(Color::Muted)
156 .size(IconSize::XSmall),
157 )
158 .child(Label::new(provider.name().0)),
159 )
160 .child(
161 h_flex()
162 .gap_1()
163 .when(!is_authenticated, |el| {
164 el.visible_on_hover(group_name.clone())
165 .child(
166 Icon::new(IconName::Settings)
167 .color(Color::Muted)
168 .size(IconSize::XSmall),
169 )
170 .child(
171 Label::new("Configure")
172 .color(Color::Muted)
173 .size(LabelSize::Small),
174 )
175 })
176 .when(is_authenticated && !disabled, |el| {
177 el.child(
178 Icon::new(IconName::Check)
179 .color(Color::Success)
180 .size(IconSize::XSmall),
181 )
182 .child(
183 Label::new("Configured")
184 .color(Color::Muted)
185 .size(LabelSize::Small),
186 )
187 }),
188 ),
189 )
190 .on_click({
191 let workspace = onboarding.workspace.clone();
192 move |_, window, cx| {
193 workspace
194 .update(cx, |workspace, cx| {
195 workspace.toggle_modal(window, cx, |window, cx| {
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 )
224}
225
226pub(crate) fn render_ai_setup_page(
227 onboarding: &Onboarding,
228 window: &mut Window,
229 cx: &mut App,
230) -> impl IntoElement {
231 let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
232
233 let backdrop = div()
234 .id("backdrop")
235 .size_full()
236 .absolute()
237 .inset_0()
238 .bg(cx.theme().colors().editor_background)
239 .opacity(0.8)
240 .block_mouse_except_scroll();
241
242 v_flex()
243 .gap_2()
244 .child(SwitchField::new(
245 "enable_ai",
246 "Enable AI features",
247 None,
248 if is_ai_disabled {
249 ToggleState::Unselected
250 } else {
251 ToggleState::Selected
252 },
253 |toggle_state, _, cx| {
254 let enabled = match toggle_state {
255 ToggleState::Indeterminate => {
256 return;
257 }
258 ToggleState::Unselected => false,
259 ToggleState::Selected => true,
260 };
261
262 let fs = <dyn Fs>::global(cx);
263 update_settings_file::<DisableAiSettings>(
264 fs,
265 cx,
266 move |ai_settings: &mut Option<bool>, _| {
267 *ai_settings = Some(!enabled);
268 },
269 );
270 },
271 ))
272 .child(render_privacy_card(is_ai_disabled, cx))
273 .child(
274 v_flex()
275 .mt_2()
276 .gap_6()
277 .child(AiUpsellCard {
278 sign_in_status: SignInStatus::SignedIn,
279 sign_in: Arc::new(|_, _| {}),
280 user_plan: onboarding.user_store.read(cx).plan(),
281 })
282 .child(render_llm_provider_section(
283 onboarding,
284 is_ai_disabled,
285 window,
286 cx,
287 ))
288 .when(is_ai_disabled, |this| this.child(backdrop)),
289 )
290}
291
292struct AiConfigurationModal {
293 focus_handle: FocusHandle,
294 selected_provider: Arc<dyn LanguageModelProvider>,
295 configuration_view: AnyView,
296}
297
298impl AiConfigurationModal {
299 fn new(
300 selected_provider: Arc<dyn LanguageModelProvider>,
301 window: &mut Window,
302 cx: &mut Context<Self>,
303 ) -> Self {
304 let focus_handle = cx.focus_handle();
305 let configuration_view = selected_provider.configuration_view(window, cx);
306
307 Self {
308 focus_handle,
309 configuration_view,
310 selected_provider,
311 }
312 }
313}
314
315impl ModalView for AiConfigurationModal {}
316
317impl EventEmitter<DismissEvent> for AiConfigurationModal {}
318
319impl Focusable for AiConfigurationModal {
320 fn focus_handle(&self, _cx: &App) -> FocusHandle {
321 self.focus_handle.clone()
322 }
323}
324
325impl Render for AiConfigurationModal {
326 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
327 v_flex()
328 .w(rems(34.))
329 .elevation_3(cx)
330 .track_focus(&self.focus_handle)
331 .child(
332 Modal::new("onboarding-ai-setup-modal", None)
333 .header(
334 ModalHeader::new()
335 .icon(
336 Icon::new(self.selected_provider.icon())
337 .color(Color::Muted)
338 .size(IconSize::Small),
339 )
340 .headline(self.selected_provider.name().0),
341 )
342 .section(Section::new().child(self.configuration_view.clone()))
343 .footer(
344 ModalFooter::new().end_slot(
345 h_flex()
346 .gap_1()
347 .child(
348 Button::new("onboarding-closing-cancel", "Cancel")
349 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
350 )
351 .child(Button::new("save-btn", "Done").on_click(cx.listener(
352 |_, _, window, cx| {
353 window.dispatch_action(menu::Confirm.boxed_clone(), cx);
354 cx.emit(DismissEvent);
355 },
356 ))),
357 ),
358 ),
359 )
360 }
361}
362
363pub struct AiPrivacyTooltip {}
364
365impl AiPrivacyTooltip {
366 pub fn new() -> Self {
367 Self {}
368 }
369}
370
371impl Render for AiPrivacyTooltip {
372 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
373 const DESCRIPTION: &'static str = "One of Zed's most important principles is transparency. This is why we are and value open-source so much. And it wouldn't be any different with AI.";
374
375 tooltip_container(window, cx, move |this, _, _| {
376 this.child(
377 h_flex()
378 .gap_1()
379 .child(
380 Icon::new(IconName::ShieldCheck)
381 .size(IconSize::Small)
382 .color(Color::Muted),
383 )
384 .child(Label::new("Privacy Principle")),
385 )
386 .child(
387 div().max_w_64().child(
388 Label::new(DESCRIPTION)
389 .size(LabelSize::Small)
390 .color(Color::Muted),
391 ),
392 )
393 })
394 }
395}