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