1use std::{sync::Arc, time::Duration};
2
3use client::{Client, UserStore};
4use feature_flags::FeatureFlagAppExt as _;
5use fs::Fs;
6use gpui::{
7 ease_in_out, svg, Animation, AnimationExt as _, ClickEvent, DismissEvent, Entity, EventEmitter,
8 FocusHandle, Focusable, MouseDownEvent, Render,
9};
10use language::language_settings::{AllLanguageSettings, InlineCompletionProvider};
11use settings::{update_settings_file, Settings};
12use ui::{prelude::*, CheckboxWithLabel, TintColor};
13use workspace::{notifications::NotifyTaskExt, ModalView, Workspace};
14
15/// Introduces user to AI inline prediction feature and terms of service
16pub struct ZedPredictModal {
17 user_store: Entity<UserStore>,
18 client: Arc<Client>,
19 fs: Arc<dyn Fs>,
20 focus_handle: FocusHandle,
21 sign_in_status: SignInStatus,
22 terms_of_service: bool,
23}
24
25#[derive(PartialEq, Eq)]
26enum SignInStatus {
27 /// Signed out or signed in but not from this modal
28 Idle,
29 /// Authentication triggered from this modal
30 Waiting,
31 /// Signed in after authentication from this modal
32 SignedIn,
33}
34
35impl ZedPredictModal {
36 fn new(
37 user_store: Entity<UserStore>,
38 client: Arc<Client>,
39 fs: Arc<dyn Fs>,
40 cx: &mut Context<Self>,
41 ) -> Self {
42 ZedPredictModal {
43 user_store,
44 client,
45 fs,
46 focus_handle: cx.focus_handle(),
47 sign_in_status: SignInStatus::Idle,
48 terms_of_service: false,
49 }
50 }
51
52 pub fn toggle(
53 workspace: Entity<Workspace>,
54 user_store: Entity<UserStore>,
55 client: Arc<Client>,
56 fs: Arc<dyn Fs>,
57 window: &mut Window,
58 cx: &mut App,
59 ) {
60 workspace.update(cx, |this, cx| {
61 this.toggle_modal(window, cx, |_window, cx| {
62 ZedPredictModal::new(user_store, client, fs, cx)
63 });
64 });
65 }
66
67 fn view_terms(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
68 cx.open_url("https://zed.dev/terms-of-service");
69 cx.notify();
70 }
71
72 fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
73 cx.open_url("https://zed.dev/blog/"); // TODO Add the link when live
74 cx.notify();
75 }
76
77 fn accept_and_enable(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
78 let task = self
79 .user_store
80 .update(cx, |this, cx| this.accept_terms_of_service(cx));
81
82 cx.spawn(|this, mut cx| async move {
83 task.await?;
84
85 this.update(&mut cx, |this, cx| {
86 update_settings_file::<AllLanguageSettings>(this.fs.clone(), cx, move |file, _| {
87 file.features
88 .get_or_insert(Default::default())
89 .inline_completion_provider = Some(InlineCompletionProvider::Zed);
90 });
91
92 cx.emit(DismissEvent);
93 })
94 })
95 .detach_and_notify_err(window, cx);
96 }
97
98 fn sign_in(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
99 let client = self.client.clone();
100 self.sign_in_status = SignInStatus::Waiting;
101
102 cx.spawn(move |this, mut cx| async move {
103 let result = client.authenticate_and_connect(true, &cx).await;
104
105 let status = match result {
106 Ok(_) => SignInStatus::SignedIn,
107 Err(_) => SignInStatus::Idle,
108 };
109
110 this.update(&mut cx, |this, cx| {
111 this.sign_in_status = status;
112 cx.notify()
113 })?;
114
115 result
116 })
117 .detach_and_notify_err(window, cx);
118 }
119
120 fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
121 cx.emit(DismissEvent);
122 }
123}
124
125impl EventEmitter<DismissEvent> for ZedPredictModal {}
126
127impl Focusable for ZedPredictModal {
128 fn focus_handle(&self, _cx: &App) -> FocusHandle {
129 self.focus_handle.clone()
130 }
131}
132
133impl ModalView for ZedPredictModal {}
134
135impl Render for ZedPredictModal {
136 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
137 let base = v_flex()
138 .w(px(420.))
139 .p_4()
140 .relative()
141 .gap_2()
142 .overflow_hidden()
143 .elevation_3(cx)
144 .id("zed predict tos")
145 .track_focus(&self.focus_handle(cx))
146 .on_action(cx.listener(Self::cancel))
147 .key_context("ZedPredictModal")
148 .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
149 cx.emit(DismissEvent);
150 }))
151 .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
152 this.focus_handle.focus(window);
153 }))
154 .child(
155 div()
156 .p_1p5()
157 .absolute()
158 .top_0()
159 .left_0()
160 .right_0()
161 .h(px(200.))
162 .child(
163 svg()
164 .path("icons/zed_predict_bg.svg")
165 .text_color(cx.theme().colors().icon_disabled)
166 .w(px(416.))
167 .h(px(128.))
168 .overflow_hidden(),
169 ),
170 )
171 .child(
172 h_flex()
173 .w_full()
174 .mb_2()
175 .justify_between()
176 .child(
177 v_flex()
178 .gap_1()
179 .child(
180 Label::new("Introducing Zed AI's")
181 .size(LabelSize::Small)
182 .color(Color::Muted),
183 )
184 .child(Headline::new("Edit Prediction").size(HeadlineSize::Large)),
185 )
186 .child({
187 let tab = |n: usize| {
188 let text_color = cx.theme().colors().text;
189 let border_color = cx.theme().colors().text_accent.opacity(0.4);
190
191 h_flex().child(
192 h_flex()
193 .px_4()
194 .py_0p5()
195 .bg(cx.theme().colors().editor_background)
196 .border_1()
197 .border_color(border_color)
198 .rounded_md()
199 .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
200 .text_size(TextSize::XSmall.rems(cx))
201 .text_color(text_color)
202 .child("tab")
203 .with_animation(
204 ElementId::Integer(n),
205 Animation::new(Duration::from_secs(2)).repeat(),
206 move |tab, delta| {
207 let delta = (delta - 0.15 * n as f32) / 0.7;
208 let delta = 1.0 - (0.5 - delta).abs() * 2.;
209 let delta = ease_in_out(delta.clamp(0., 1.));
210 let delta = 0.1 + 0.9 * delta;
211
212 tab.border_color(border_color.opacity(delta))
213 .text_color(text_color.opacity(delta))
214 },
215 ),
216 )
217 };
218
219 v_flex()
220 .gap_2()
221 .items_center()
222 .pr_4()
223 .child(tab(0).ml_neg_20())
224 .child(tab(1))
225 .child(tab(2).ml_20())
226 }),
227 )
228 .child(h_flex().absolute().top_2().right_2().child(
229 IconButton::new("cancel", IconName::X).on_click(cx.listener(
230 |_, _: &ClickEvent, _window, cx| {
231 cx.emit(DismissEvent);
232 },
233 )),
234 ));
235
236 let blog_post_button = if cx.is_staff() {
237 Some(
238 Button::new("view-blog", "Read the Blog Post")
239 .full_width()
240 .icon(IconName::ArrowUpRight)
241 .icon_size(IconSize::Indicator)
242 .icon_color(Color::Muted)
243 .on_click(cx.listener(Self::view_blog)),
244 )
245 } else {
246 // TODO: put back when blog post is published
247 None
248 };
249
250 if self.user_store.read(cx).current_user().is_some() {
251 let copy = match self.sign_in_status {
252 SignInStatus::Idle => "Get accurate and helpful edit predictions at every keystroke. To set Zed as your inline completions provider, ensure you:",
253 SignInStatus::SignedIn => "Almost there! Ensure you:",
254 SignInStatus::Waiting => unreachable!(),
255 };
256
257 base.child(Label::new(copy).color(Color::Muted))
258 .child(
259 h_flex()
260 .gap_0p5()
261 .child(CheckboxWithLabel::new(
262 "tos-checkbox",
263 Label::new("Have read and accepted the").color(Color::Muted),
264 self.terms_of_service.into(),
265 cx.listener(move |this, state, _window, cx| {
266 this.terms_of_service = *state == ToggleState::Selected;
267 cx.notify()
268 }),
269 ))
270 .child(
271 Button::new("view-tos", "Terms of Service")
272 .icon(IconName::ArrowUpRight)
273 .icon_size(IconSize::Indicator)
274 .icon_color(Color::Muted)
275 .on_click(cx.listener(Self::view_terms)),
276 ),
277 )
278 .child(
279 v_flex()
280 .mt_2()
281 .gap_2()
282 .w_full()
283 .child(
284 Button::new("accept-tos", "Enable Edit Predictions")
285 .disabled(!self.terms_of_service)
286 .style(ButtonStyle::Tinted(TintColor::Accent))
287 .full_width()
288 .on_click(cx.listener(Self::accept_and_enable)),
289 )
290 .children(blog_post_button),
291 )
292 } else {
293 base.child(
294 Label::new("To set Zed as your inline completions provider, please sign in.")
295 .color(Color::Muted),
296 )
297 .child(
298 v_flex()
299 .mt_2()
300 .gap_2()
301 .w_full()
302 .child(
303 Button::new("accept-tos", "Sign in with GitHub")
304 .disabled(self.sign_in_status == SignInStatus::Waiting)
305 .style(ButtonStyle::Tinted(TintColor::Accent))
306 .full_width()
307 .on_click(cx.listener(Self::sign_in)),
308 )
309 .children(blog_post_button),
310 )
311 }
312 }
313}