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