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