onboarding_modal.rs

  1use std::sync::Arc;
  2
  3use crate::{EditPredictionStore, ZedPredictUpsell};
  4use ai_onboarding::EditPredictionOnboarding;
  5use client::{Client, UserStore};
  6use db::kvp::Dismissable;
  7use fs::Fs;
  8use gpui::{
  9    ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
 10    linear_color_stop, linear_gradient,
 11};
 12use language::language_settings::EditPredictionProvider;
 13use settings::update_settings_file;
 14use ui::{Vector, VectorName, prelude::*};
 15use workspace::{ModalView, Workspace};
 16
 17#[macro_export]
 18macro_rules! onboarding_event {
 19    ($name:expr) => {
 20        telemetry::event!($name, source = "Edit Prediction Onboarding");
 21    };
 22    ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
 23        telemetry::event!($name, source = "Edit Prediction Onboarding", $($key $(= $value)?),+);
 24    };
 25}
 26
 27/// Introduces user to Zed's Edit Prediction feature
 28pub struct ZedPredictModal {
 29    onboarding: Entity<EditPredictionOnboarding>,
 30    focus_handle: FocusHandle,
 31}
 32
 33pub(crate) fn set_edit_prediction_provider(provider: EditPredictionProvider, cx: &mut App) {
 34    let fs = <dyn Fs>::global(cx);
 35    update_settings_file(fs, cx, move |settings, _| {
 36        settings
 37            .project
 38            .all_languages
 39            .features
 40            .get_or_insert(Default::default())
 41            .edit_prediction_provider = Some(provider);
 42    });
 43}
 44
 45impl ZedPredictModal {
 46    pub fn toggle(
 47        workspace: &mut Workspace,
 48        user_store: Entity<UserStore>,
 49        client: Arc<Client>,
 50        window: &mut Window,
 51        cx: &mut Context<Workspace>,
 52    ) {
 53        let project = workspace.project().clone();
 54        workspace.toggle_modal(window, cx, |_window, cx| {
 55            let weak_entity = cx.weak_entity();
 56            let copilot = EditPredictionStore::try_global(cx)
 57                .and_then(|store| store.read(cx).copilot_for_project(&project));
 58            Self {
 59                onboarding: cx.new(|cx| {
 60                    EditPredictionOnboarding::new(
 61                        user_store.clone(),
 62                        client.clone(),
 63                        copilot
 64                            .as_ref()
 65                            .is_some_and(|copilot| copilot.read(cx).status().is_configured()),
 66                        Arc::new({
 67                            let this = weak_entity.clone();
 68                            move |_window, cx| {
 69                                ZedPredictUpsell::set_dismissed(true, cx);
 70                                set_edit_prediction_provider(EditPredictionProvider::Zed, cx);
 71                                this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
 72                            }
 73                        }),
 74                        Arc::new({
 75                            let this = weak_entity.clone();
 76                            move |window, cx| {
 77                                ZedPredictUpsell::set_dismissed(true, cx);
 78                                set_edit_prediction_provider(EditPredictionProvider::Copilot, cx);
 79                                this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
 80                                if let Some(copilot) = copilot.clone() {
 81                                    copilot_ui::initiate_sign_in(copilot, window, cx);
 82                                }
 83                            }
 84                        }),
 85                        cx,
 86                    )
 87                }),
 88                focus_handle: cx.focus_handle(),
 89            }
 90        });
 91    }
 92
 93    fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 94        ZedPredictUpsell::set_dismissed(true, cx);
 95        cx.emit(DismissEvent);
 96    }
 97}
 98
 99impl EventEmitter<DismissEvent> for ZedPredictModal {}
100
101impl Focusable for ZedPredictModal {
102    fn focus_handle(&self, _cx: &App) -> FocusHandle {
103        self.focus_handle.clone()
104    }
105}
106
107impl ModalView for ZedPredictModal {
108    fn on_before_dismiss(
109        &mut self,
110        _window: &mut Window,
111        cx: &mut Context<Self>,
112    ) -> workspace::DismissDecision {
113        ZedPredictUpsell::set_dismissed(true, cx);
114        workspace::DismissDecision::Dismiss(true)
115    }
116}
117
118impl Render for ZedPredictModal {
119    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
120        let window_height = window.viewport_size().height;
121        let max_height = window_height - px(200.);
122
123        v_flex()
124            .id("edit-prediction-onboarding")
125            .key_context("ZedPredictModal")
126            .relative()
127            .w(px(550.))
128            .h_full()
129            .max_h(max_height)
130            .p_4()
131            .gap_2()
132            .elevation_3(cx)
133            .track_focus(&self.focus_handle(cx))
134            .overflow_hidden()
135            .on_action(cx.listener(Self::cancel))
136            .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
137                onboarding_event!("Cancelled", trigger = "Action");
138                cx.emit(DismissEvent);
139            }))
140            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
141                this.focus_handle.focus(window, cx);
142            }))
143            .child(
144                div()
145                    .opacity(0.5)
146                    .absolute()
147                    .top(px(-8.0))
148                    .right_0()
149                    .w(px(400.))
150                    .h(px(92.))
151                    .child(
152                        Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.))
153                            .color(Color::Custom(cx.theme().colors().text.alpha(0.32))),
154                    ),
155            )
156            .child(
157                div()
158                    .absolute()
159                    .top_0()
160                    .right_0()
161                    .w(px(660.))
162                    .h(px(401.))
163                    .overflow_hidden()
164                    .bg(linear_gradient(
165                        75.,
166                        linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0),
167                        linear_color_stop(cx.theme().colors().panel_background, 0.45),
168                    )),
169            )
170            .child(h_flex().absolute().top_2().right_2().child(
171                IconButton::new("cancel", IconName::Close).on_click(cx.listener(
172                    |_, _: &ClickEvent, _window, cx| {
173                        onboarding_event!("Cancelled", trigger = "X click");
174                        cx.emit(DismissEvent);
175                    },
176                )),
177            ))
178            .child(self.onboarding.clone())
179    }
180}