1use chrono::Utc;
2use feature_flags::{FeatureFlagAppExt as _, PredictEditsFeatureFlag};
3use gpui::Subscription;
4use language::language_settings::{all_language_settings, EditPredictionProvider};
5use settings::SettingsStore;
6use ui::{prelude::*, ButtonLike, Tooltip};
7use util::ResultExt;
8
9use crate::onboarding_event;
10
11/// Prompts the user to try Zed's Edit Prediction feature
12pub struct ZedPredictBanner {
13 dismissed: bool,
14 provider: EditPredictionProvider,
15 _subscription: Subscription,
16}
17
18impl ZedPredictBanner {
19 pub fn new(cx: &mut Context<Self>) -> Self {
20 Self {
21 dismissed: get_dismissed(),
22 provider: all_language_settings(None, cx).edit_predictions.provider,
23 _subscription: cx.observe_global::<SettingsStore>(Self::handle_settings_changed),
24 }
25 }
26
27 fn should_show(&self, cx: &mut App) -> bool {
28 cx.has_flag::<PredictEditsFeatureFlag>() && !self.dismissed && !self.provider.is_zed()
29 }
30
31 fn handle_settings_changed(&mut self, cx: &mut Context<Self>) {
32 let new_provider = all_language_settings(None, cx).edit_predictions.provider;
33
34 if new_provider == self.provider {
35 return;
36 }
37
38 if new_provider.is_zed() {
39 self.dismiss(cx);
40 } else {
41 self.dismissed = get_dismissed();
42 }
43
44 self.provider = new_provider;
45 cx.notify();
46 }
47
48 fn dismiss(&mut self, cx: &mut Context<Self>) {
49 onboarding_event!("Banner Dismissed");
50 persist_dismissed(cx);
51 self.dismissed = true;
52 cx.notify();
53 }
54}
55
56const DISMISSED_AT_KEY: &str = "zed_predict_banner_dismissed_at";
57
58fn get_dismissed() -> bool {
59 db::kvp::KEY_VALUE_STORE
60 .read_kvp(DISMISSED_AT_KEY)
61 .log_err()
62 .map_or(false, |dismissed| dismissed.is_some())
63}
64
65fn persist_dismissed(cx: &mut App) {
66 cx.spawn(async |_| {
67 let time = Utc::now().to_rfc3339();
68 db::kvp::KEY_VALUE_STORE
69 .write_kvp(DISMISSED_AT_KEY.into(), time)
70 .await
71 })
72 .detach_and_log_err(cx);
73}
74
75pub(crate) fn clear_dismissed(cx: &mut App) {
76 cx.spawn(async |_| {
77 db::kvp::KEY_VALUE_STORE
78 .delete_kvp(DISMISSED_AT_KEY.into())
79 .await
80 })
81 .detach_and_log_err(cx);
82}
83
84impl Render for ZedPredictBanner {
85 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
86 if !self.should_show(cx) {
87 return div();
88 }
89
90 let border_color = cx.theme().colors().editor_foreground.opacity(0.3);
91 let banner = h_flex()
92 .rounded_sm()
93 .border_1()
94 .border_color(border_color)
95 .child(
96 ButtonLike::new("try-zed-predict")
97 .child(
98 h_flex()
99 .h_full()
100 .items_center()
101 .gap_1p5()
102 .child(Icon::new(IconName::ZedPredict).size(IconSize::Small))
103 .child(
104 h_flex()
105 .gap_0p5()
106 .child(
107 Label::new("Introducing:")
108 .size(LabelSize::Small)
109 .color(Color::Muted),
110 )
111 .child(Label::new("Edit Prediction").size(LabelSize::Small)),
112 ),
113 )
114 .on_click(|_, window, cx| {
115 onboarding_event!("Banner Clicked");
116 window.dispatch_action(Box::new(zed_actions::OpenZedPredictOnboarding), cx)
117 }),
118 )
119 .child(
120 div().border_l_1().border_color(border_color).child(
121 IconButton::new("close", IconName::Close)
122 .icon_size(IconSize::Indicator)
123 .on_click(cx.listener(|this, _, _window, cx| this.dismiss(cx)))
124 .tooltip(|window, cx| {
125 Tooltip::with_meta(
126 "Close Announcement Banner",
127 None,
128 "It won't show again for this feature",
129 window,
130 cx,
131 )
132 }),
133 ),
134 );
135
136 div().pr_2().child(banner)
137 }
138}