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(|_| {
67 let time = Utc::now().to_rfc3339();
68 db::kvp::KEY_VALUE_STORE.write_kvp(DISMISSED_AT_KEY.into(), time)
69 })
70 .detach_and_log_err(cx);
71}
72
73pub(crate) fn clear_dismissed(cx: &mut App) {
74 cx.spawn(|_| db::kvp::KEY_VALUE_STORE.delete_kvp(DISMISSED_AT_KEY.into()))
75 .detach_and_log_err(cx);
76}
77
78impl Render for ZedPredictBanner {
79 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
80 if !self.should_show(cx) {
81 return div();
82 }
83
84 let border_color = cx.theme().colors().editor_foreground.opacity(0.3);
85 let banner = h_flex()
86 .rounded_md()
87 .border_1()
88 .border_color(border_color)
89 .child(
90 ButtonLike::new("try-zed-predict")
91 .child(
92 h_flex()
93 .h_full()
94 .items_center()
95 .gap_1p5()
96 .child(Icon::new(IconName::ZedPredict).size(IconSize::Small))
97 .child(
98 h_flex()
99 .gap_0p5()
100 .child(
101 Label::new("Introducing:")
102 .size(LabelSize::Small)
103 .color(Color::Muted),
104 )
105 .child(Label::new("Edit Prediction").size(LabelSize::Small)),
106 ),
107 )
108 .on_click(|_, window, cx| {
109 onboarding_event!("Banner Clicked");
110 window.dispatch_action(Box::new(zed_actions::OpenZedPredictOnboarding), cx)
111 }),
112 )
113 .child(
114 div().border_l_1().border_color(border_color).child(
115 IconButton::new("close", IconName::Close)
116 .icon_size(IconSize::Indicator)
117 .on_click(cx.listener(|this, _, _window, cx| this.dismiss(cx)))
118 .tooltip(|window, cx| {
119 Tooltip::with_meta(
120 "Close Announcement Banner",
121 None,
122 "It won't show again for this feature",
123 window,
124 cx,
125 )
126 }),
127 ),
128 );
129
130 div().pr_2().child(banner)
131 }
132}