1use std::sync::Arc;
2
3use ai_onboarding::{AiUpsellCard, SignInStatus};
4use client::DisableAiSettings;
5use fs::Fs;
6use gpui::{
7 Action, AnyView, App, DismissEvent, EventEmitter, FocusHandle, Focusable, Window, prelude::*,
8};
9use itertools;
10
11use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
12use settings::{Settings, update_settings_file};
13use ui::{
14 Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState,
15 prelude::*,
16};
17use workspace::ModalView;
18
19use util::ResultExt;
20use zed_actions::agent::OpenSettings;
21
22use crate::Onboarding;
23
24const FEATURED_PROVIDERS: [&'static str; 4] = ["anthropic", "google", "openai", "ollama"];
25
26fn render_llm_provider_section(
27 onboarding: &Onboarding,
28 disabled: bool,
29 window: &mut Window,
30 cx: &mut App,
31) -> impl IntoElement {
32 v_flex()
33 .gap_4()
34 .child(
35 v_flex()
36 .child(Label::new("Or use other LLM providers").size(LabelSize::Large))
37 .child(
38 Label::new("Bring your API keys to use the available providers with Zed's UI for free.")
39 .color(Color::Muted),
40 ),
41 )
42 .child(render_llm_provider_card(onboarding, disabled, window, cx))
43}
44
45fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement {
46 v_flex()
47 .relative()
48 .pt_2()
49 .pb_2p5()
50 .pl_3()
51 .pr_2()
52 .border_1()
53 .border_dashed()
54 .border_color(cx.theme().colors().border.opacity(0.5))
55 .bg(cx.theme().colors().surface_background.opacity(0.3))
56 .rounded_lg()
57 .overflow_hidden()
58 .map(|this| {
59 if disabled {
60 this.child(
61 h_flex()
62 .gap_2()
63 .justify_between()
64 .child(
65 h_flex()
66 .gap_1()
67 .child(Label::new("AI is disabled across Zed"))
68 .child(
69 Icon::new(IconName::Check)
70 .color(Color::Success)
71 .size(IconSize::XSmall),
72 ),
73 )
74 .child(Badge::new("PRIVACY").icon(IconName::FileLock)),
75 )
76 .child(
77 Label::new("Re-enable it any time in Settings.")
78 .size(LabelSize::Small)
79 .color(Color::Muted),
80 )
81 } else {
82 this.child(
83 h_flex()
84 .gap_2()
85 .justify_between()
86 .child(Label::new("We don't train models using your data"))
87 .child(
88 h_flex()
89 .gap_1()
90 .child(Badge::new("Privacy").icon(IconName::FileLock))
91 .child(
92 Button::new("learn_more", "Learn More")
93 .style(ButtonStyle::Outlined)
94 .label_size(LabelSize::Small)
95 .icon(IconName::ArrowUpRight)
96 .icon_size(IconSize::XSmall)
97 .icon_color(Color::Muted)
98 .on_click(|_, _, cx| {
99 cx.open_url(
100 "https://zed.dev/docs/ai/privacy-and-security",
101 );
102 }),
103 ),
104 ),
105 )
106 .child(
107 Label::new(
108 "Feel confident in the security and privacy of your projects using Zed.",
109 )
110 .size(LabelSize::Small)
111 .color(Color::Muted),
112 )
113 }
114 })
115}
116
117fn render_llm_provider_card(
118 onboarding: &Onboarding,
119 disabled: bool,
120 _: &mut Window,
121 cx: &mut App,
122) -> impl IntoElement {
123 let registry = LanguageModelRegistry::read_global(cx);
124
125 v_flex()
126 .border_1()
127 .border_color(cx.theme().colors().border)
128 .bg(cx.theme().colors().surface_background.opacity(0.5))
129 .rounded_lg()
130 .overflow_hidden()
131 .children(itertools::intersperse_with(
132 FEATURED_PROVIDERS
133 .into_iter()
134 .flat_map(|provider_name| {
135 registry.provider(&LanguageModelProviderId::new(provider_name))
136 })
137 .enumerate()
138 .map(|(index, provider)| {
139 let group_name = SharedString::new(format!("onboarding-hover-group-{}", index));
140 let is_authenticated = provider.is_authenticated(cx);
141
142 ButtonLike::new(("onboarding-ai-setup-buttons", index))
143 .size(ButtonSize::Large)
144 .child(
145 h_flex()
146 .group(&group_name)
147 .px_0p5()
148 .w_full()
149 .gap_2()
150 .justify_between()
151 .child(
152 h_flex()
153 .gap_1()
154 .child(
155 Icon::new(provider.icon())
156 .color(Color::Muted)
157 .size(IconSize::XSmall),
158 )
159 .child(Label::new(provider.name().0)),
160 )
161 .child(
162 h_flex()
163 .gap_1()
164 .when(!is_authenticated, |el| {
165 el.visible_on_hover(group_name.clone())
166 .child(
167 Icon::new(IconName::Settings)
168 .color(Color::Muted)
169 .size(IconSize::XSmall),
170 )
171 .child(
172 Label::new("Configure")
173 .color(Color::Muted)
174 .size(LabelSize::Small),
175 )
176 })
177 .when(is_authenticated && !disabled, |el| {
178 el.child(
179 Icon::new(IconName::Check)
180 .color(Color::Success)
181 .size(IconSize::XSmall),
182 )
183 .child(
184 Label::new("Configured")
185 .color(Color::Muted)
186 .size(LabelSize::Small),
187 )
188 }),
189 ),
190 )
191 .on_click({
192 let workspace = onboarding.workspace.clone();
193 move |_, window, cx| {
194 workspace
195 .update(cx, |workspace, cx| {
196 workspace.toggle_modal(window, cx, |window, cx| {
197 let modal = AiConfigurationModal::new(
198 provider.clone(),
199 window,
200 cx,
201 );
202 window.focus(&modal.focus_handle(cx));
203 modal
204 });
205 })
206 .log_err();
207 }
208 })
209 .into_any_element()
210 }),
211 || Divider::horizontal().into_any_element(),
212 ))
213 .child(Divider::horizontal())
214 .child(
215 Button::new("agent_settings", "Add Many Others")
216 .size(ButtonSize::Large)
217 .icon(IconName::Plus)
218 .icon_position(IconPosition::Start)
219 .icon_color(Color::Muted)
220 .icon_size(IconSize::XSmall)
221 .on_click(|_event, window, cx| {
222 window.dispatch_action(OpenSettings.boxed_clone(), cx)
223 }),
224 )
225}
226
227pub(crate) fn render_ai_setup_page(
228 onboarding: &Onboarding,
229 window: &mut Window,
230 cx: &mut App,
231) -> impl IntoElement {
232 let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
233
234 let backdrop = div()
235 .id("backdrop")
236 .size_full()
237 .absolute()
238 .inset_0()
239 .bg(cx.theme().colors().editor_background)
240 .opacity(0.8)
241 .block_mouse_except_scroll();
242
243 v_flex()
244 .gap_2()
245 .child(SwitchField::new(
246 "enable_ai",
247 "Enable AI features",
248 None,
249 if is_ai_disabled {
250 ToggleState::Unselected
251 } else {
252 ToggleState::Selected
253 },
254 |toggle_state, _, cx| {
255 let enabled = match toggle_state {
256 ToggleState::Indeterminate => {
257 return;
258 }
259 ToggleState::Unselected => false,
260 ToggleState::Selected => true,
261 };
262
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 = Some(!enabled);
269 },
270 );
271 },
272 ))
273 .child(render_privacy_card(is_ai_disabled, cx))
274 .child(
275 v_flex()
276 .mt_2()
277 .gap_6()
278 .child(AiUpsellCard {
279 sign_in_status: SignInStatus::SignedIn,
280 sign_in: Arc::new(|_, _| {}),
281 user_plan: onboarding.user_store.read(cx).plan(),
282 })
283 .child(render_llm_provider_section(
284 onboarding,
285 is_ai_disabled,
286 window,
287 cx,
288 ))
289 .when(is_ai_disabled, |this| this.child(backdrop)),
290 )
291}
292
293struct AiConfigurationModal {
294 focus_handle: FocusHandle,
295 selected_provider: Arc<dyn LanguageModelProvider>,
296 configuration_view: AnyView,
297}
298
299impl AiConfigurationModal {
300 fn new(
301 selected_provider: Arc<dyn LanguageModelProvider>,
302 window: &mut Window,
303 cx: &mut Context<Self>,
304 ) -> Self {
305 let focus_handle = cx.focus_handle();
306 let configuration_view = selected_provider.configuration_view(window, cx);
307
308 Self {
309 focus_handle,
310 configuration_view,
311 selected_provider,
312 }
313 }
314}
315
316impl ModalView for AiConfigurationModal {}
317
318impl EventEmitter<DismissEvent> for AiConfigurationModal {}
319
320impl Focusable for AiConfigurationModal {
321 fn focus_handle(&self, _cx: &App) -> FocusHandle {
322 self.focus_handle.clone()
323 }
324}
325
326impl Render for AiConfigurationModal {
327 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
328 v_flex()
329 .w(rems(34.))
330 .elevation_3(cx)
331 .track_focus(&self.focus_handle)
332 .child(
333 Modal::new("onboarding-ai-setup-modal", None)
334 .header(
335 ModalHeader::new()
336 .icon(
337 Icon::new(self.selected_provider.icon())
338 .color(Color::Muted)
339 .size(IconSize::Small),
340 )
341 .headline(self.selected_provider.name().0),
342 )
343 .section(Section::new().child(self.configuration_view.clone()))
344 .footer(
345 ModalFooter::new().end_slot(
346 h_flex()
347 .gap_1()
348 .child(
349 Button::new("onboarding-closing-cancel", "Cancel")
350 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
351 )
352 .child(Button::new("save-btn", "Done").on_click(cx.listener(
353 |_, _, window, cx| {
354 window.dispatch_action(menu::Confirm.boxed_clone(), cx);
355 cx.emit(DismissEvent);
356 },
357 ))),
358 ),
359 ),
360 )
361 }
362}