1use std::sync::Arc;
2
3use ai_onboarding::{AiUpsellCard, SignInStatus};
4use 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, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState,
16 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("We don't train models using your data"))
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 "Feel confident in the security and privacy of your projects using Zed.",
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 window: &mut Window,
244 cx: &mut App,
245) -> impl IntoElement {
246 let mut tab_index = 0;
247 let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
248
249 v_flex()
250 .gap_2()
251 .child(
252 SwitchField::new(
253 "enable_ai",
254 "Enable AI features",
255 None,
256 if is_ai_disabled {
257 ToggleState::Unselected
258 } else {
259 ToggleState::Selected
260 },
261 |&toggle_state, _, cx| {
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 = match toggle_state {
268 ToggleState::Indeterminate => None,
269 ToggleState::Unselected => Some(true),
270 ToggleState::Selected => Some(false),
271 };
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(AiUpsellCard {
287 sign_in_status: SignInStatus::SignedIn,
288 sign_in: Arc::new(|_, _| {}),
289 account_too_young: user_store.read(cx).account_too_young(),
290 user_plan: user_store.read(cx).plan(),
291 tab_index: Some({
292 tab_index += 1;
293 tab_index - 1
294 }),
295 })
296 .child(render_llm_provider_section(
297 &mut tab_index,
298 workspace,
299 is_ai_disabled,
300 window,
301 cx,
302 ))
303 .when(is_ai_disabled, |this| {
304 this.child(
305 div()
306 .id("backdrop")
307 .size_full()
308 .absolute()
309 .inset_0()
310 .bg(cx.theme().colors().editor_background)
311 .opacity(0.8)
312 .block_mouse_except_scroll(),
313 )
314 }),
315 )
316}
317
318struct AiConfigurationModal {
319 focus_handle: FocusHandle,
320 selected_provider: Arc<dyn LanguageModelProvider>,
321 configuration_view: AnyView,
322}
323
324impl AiConfigurationModal {
325 fn new(
326 selected_provider: Arc<dyn LanguageModelProvider>,
327 window: &mut Window,
328 cx: &mut Context<Self>,
329 ) -> Self {
330 let focus_handle = cx.focus_handle();
331 let configuration_view = selected_provider.configuration_view(window, cx);
332
333 Self {
334 focus_handle,
335 configuration_view,
336 selected_provider,
337 }
338 }
339}
340
341impl ModalView for AiConfigurationModal {}
342
343impl EventEmitter<DismissEvent> for AiConfigurationModal {}
344
345impl Focusable for AiConfigurationModal {
346 fn focus_handle(&self, _cx: &App) -> FocusHandle {
347 self.focus_handle.clone()
348 }
349}
350
351impl Render for AiConfigurationModal {
352 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
353 v_flex()
354 .w(rems(34.))
355 .elevation_3(cx)
356 .track_focus(&self.focus_handle)
357 .child(
358 Modal::new("onboarding-ai-setup-modal", None)
359 .header(
360 ModalHeader::new()
361 .icon(
362 Icon::new(self.selected_provider.icon())
363 .color(Color::Muted)
364 .size(IconSize::Small),
365 )
366 .headline(self.selected_provider.name().0),
367 )
368 .section(Section::new().child(self.configuration_view.clone()))
369 .footer(
370 ModalFooter::new().end_slot(
371 h_flex()
372 .gap_1()
373 .child(
374 Button::new("onboarding-closing-cancel", "Cancel")
375 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
376 )
377 .child(Button::new("save-btn", "Done").on_click(cx.listener(
378 |_, _, window, cx| {
379 window.dispatch_action(menu::Confirm.boxed_clone(), cx);
380 cx.emit(DismissEvent);
381 },
382 ))),
383 ),
384 ),
385 )
386 }
387}
388
389pub struct AiPrivacyTooltip {}
390
391impl AiPrivacyTooltip {
392 pub fn new() -> Self {
393 Self {}
394 }
395}
396
397impl Render for AiPrivacyTooltip {
398 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
399 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.";
400
401 tooltip_container(window, cx, move |this, _, _| {
402 this.child(
403 h_flex()
404 .gap_1()
405 .child(
406 Icon::new(IconName::ShieldCheck)
407 .size(IconSize::Small)
408 .color(Color::Muted),
409 )
410 .child(Label::new("Privacy Principle")),
411 )
412 .child(
413 div().max_w_64().child(
414 Label::new(DESCRIPTION)
415 .size(LabelSize::Small)
416 .color(Color::Muted),
417 ),
418 )
419 })
420 }
421}