diff --git a/assets/images/ai_grid.svg b/assets/images/ai_grid.svg new file mode 100644 index 0000000000..49e8c4139e --- /dev/null +++ b/assets/images/ai_grid.svg @@ -0,0 +1,334 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/grid.svg b/assets/images/grid.svg new file mode 100644 index 0000000000..fc70470024 --- /dev/null +++ b/assets/images/grid.svg @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs index 2320c305ed..cda2e44cae 100644 --- a/crates/agent/src/active_thread.rs +++ b/crates/agent/src/active_thread.rs @@ -177,7 +177,7 @@ fn parse_markdown( cx.new(|cx| Markdown::new(text, Some(language_registry), None, cx)) } -fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { +pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { let theme_settings = ThemeSettings::get_global(cx); let colors = cx.theme().colors(); let ui_font_size = TextSize::Default.rems(cx); diff --git a/crates/agent/src/assistant.rs b/crates/agent/src/assistant.rs index 815f20b5b8..70304b539f 100644 --- a/crates/agent/src/assistant.rs +++ b/crates/agent/src/assistant.rs @@ -80,7 +80,8 @@ actions!( Reject, RejectAll, KeepAll, - Follow + Follow, + ResetTrialUpsell, ] ); diff --git a/crates/agent/src/assistant_panel.rs b/crates/agent/src/assistant_panel.rs index bf9fe04b88..c16b6a67d0 100644 --- a/crates/agent/src/assistant_panel.rs +++ b/crates/agent/src/assistant_panel.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use std::time::Duration; use db::kvp::KEY_VALUE_STORE; +use markdown::Markdown; use serde::{Deserialize, Serialize}; use anyhow::{Result, anyhow}; @@ -22,7 +23,8 @@ use fs::Fs; use gpui::{ Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, - Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, + Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop, linear_gradient, + prelude::*, pulsating_between, }; use language::LanguageRegistry; use language_model::{LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage}; @@ -36,8 +38,8 @@ use settings::{Settings, update_settings_file}; use theme::ThemeSettings; use time::UtcOffset; use ui::{ - Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, - prelude::*, + Banner, CheckboxWithLabel, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, + ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*, }; use util::{ResultExt as _, maybe}; use workspace::dock::{DockPosition, Panel, PanelEvent}; @@ -58,7 +60,8 @@ use crate::thread_store::{TextThreadStore, ThreadStore}; use crate::{ AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, - OpenHistory, ThreadEvent, ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu, + OpenHistory, ResetTrialUpsell, ThreadEvent, ToggleContextPicker, ToggleNavigationMenu, + ToggleOptionsMenu, }; const AGENT_PANEL_KEY: &str = "agent_panel"; @@ -139,6 +142,9 @@ pub fn init(cx: &mut App) { panel.toggle_options_menu(&ToggleOptionsMenu, window, cx); }); } + }) + .register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| { + set_trial_upsell_dismissed(false, cx); }); }, ) @@ -331,6 +337,8 @@ pub struct AssistantPanel { width: Option, height: Option, pending_serialization: Option>>, + hide_trial_upsell: bool, + _trial_markdown: Entity, } impl AssistantPanel { @@ -625,6 +633,15 @@ impl AssistantPanel { }, ); + let trial_markdown = cx.new(|cx| { + Markdown::new( + include_str!("trial_markdown.md").into(), + Some(language_registry.clone()), + None, + cx, + ) + }); + Self { active_view, workspace, @@ -659,6 +676,8 @@ impl AssistantPanel { width: None, height: None, pending_serialization: None, + hide_trial_upsell: false, + _trial_markdown: trial_markdown, } } @@ -1809,6 +1828,166 @@ impl AssistantPanel { } } + fn should_render_upsell(&self, cx: &mut Context) -> bool { + if self.hide_trial_upsell || dismissed_trial_upsell() { + return false; + } + + let plan = self.user_store.read(cx).current_plan(); + if matches!(plan, Some(Plan::ZedPro | Plan::ZedProTrial)) { + return false; + } + + let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some(); + if has_previous_trial { + return false; + } + + true + } + + fn render_trial_upsell( + &self, + _window: &mut Window, + cx: &mut Context, + ) -> Option { + if !self.should_render_upsell(cx) { + return None; + } + + let checkbox = CheckboxWithLabel::new( + "dont-show-again", + Label::new("Don't show again").color(Color::Muted), + ToggleState::Unselected, + move |toggle_state, _window, cx| { + let toggle_state_bool = toggle_state.selected(); + + set_trial_upsell_dismissed(toggle_state_bool, cx); + }, + ); + + Some( + div().p_2().child( + v_flex() + .w_full() + .elevation_2(cx) + .rounded(px(8.)) + .bg(cx.theme().colors().background.alpha(0.5)) + .p(px(3.)) + + .child( + div() + .gap_2() + .flex() + .flex_col() + .size_full() + .border_1() + .rounded(px(5.)) + .border_color(cx.theme().colors().text.alpha(0.1)) + .overflow_hidden() + .relative() + .bg(cx.theme().colors().panel_background) + .px_4() + .py_3() + .child( + div() + .absolute() + .top_0() + .right(px(-1.0)) + .w(px(441.)) + .h(px(167.)) + .child( + Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))) + ) + ) + .child( + div() + .absolute() + .top(px(-8.0)) + .right_0() + .w(px(400.)) + .h(px(92.)) + .child( + Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))) + ) + ) + // .child( + // div() + // .absolute() + // .top_0() + // .right(px(360.)) + // .size(px(401.)) + // .overflow_hidden() + // .bg(cx.theme().colors().panel_background) + // ) + .child( + div() + .absolute() + .top_0() + .right_0() + .w(px(660.)) + .h(px(401.)) + .overflow_hidden() + .bg(linear_gradient( + 75., + linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0), + linear_color_stop(cx.theme().colors().panel_background, 0.45), + )) + ) + .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small)) + .child(Label::new("Try Zed Pro for free for 14 days - no credit card required.").size(LabelSize::Small)) + .child(Label::new("Use your own API keys or enable usage-based billing once you hit the cap.").color(Color::Muted)) + .child( + h_flex() + .w_full() + .px_neg_1() + .justify_between() + .items_center() + .child(h_flex().items_center().gap_1().child(checkbox)) + .child( + h_flex() + .gap_2() + .child( + Button::new("dismiss-button", "Not Now") + .style(ButtonStyle::Transparent) + .color(Color::Muted) + .on_click({ + let assistant_panel = cx.entity(); + move |_, _, cx| { + assistant_panel.update( + cx, + |this, cx| { + let hidden = + this.hide_trial_upsell; + println!("hidden: {}", hidden); + this.hide_trial_upsell = true; + let new_hidden = + this.hide_trial_upsell; + println!( + "new_hidden: {}", + new_hidden + ); + + cx.notify(); + }, + ); + } + }), + ) + .child( + Button::new("cta-button", "Start Trial") + .style(ButtonStyle::Transparent) + .on_click(|_, _, cx| { + cx.open_url(&zed_urls::account_url(cx)) + }), + ), + ), + ), + ), + ), + ) + } + fn render_active_thread_or_empty_state( &self, window: &mut Window, @@ -2425,6 +2604,7 @@ impl Render for AssistantPanel { .on_action(cx.listener(Self::decrease_font_size)) .on_action(cx.listener(Self::reset_font_size)) .child(self.render_toolbar(window, cx)) + .children(self.render_trial_upsell(window, cx)) .map(|parent| match &self.active_view { ActiveView::Thread { .. } => parent .child(self.render_active_thread_or_empty_state(window, cx)) @@ -2627,3 +2807,26 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate { }); } } + +const DISMISSED_TRIAL_UPSELL_KEY: &str = "dismissed-trial-upsell"; + +fn dismissed_trial_upsell() -> bool { + db::kvp::KEY_VALUE_STORE + .read_kvp(DISMISSED_TRIAL_UPSELL_KEY) + .log_err() + .map_or(false, |s| s.is_some()) +} + +fn set_trial_upsell_dismissed(is_dismissed: bool, cx: &mut App) { + db::write_and_log(cx, move || async move { + if is_dismissed { + db::kvp::KEY_VALUE_STORE + .write_kvp(DISMISSED_TRIAL_UPSELL_KEY.into(), "1".into()) + .await + } else { + db::kvp::KEY_VALUE_STORE + .delete_kvp(DISMISSED_TRIAL_UPSELL_KEY.into()) + .await + } + }) +} diff --git a/crates/agent/src/trial_markdown.md b/crates/agent/src/trial_markdown.md new file mode 100644 index 0000000000..b2a6e515cd --- /dev/null +++ b/crates/agent/src/trial_markdown.md @@ -0,0 +1,3 @@ +# Build better with Zed Pro + +Try [Zed Pro](https://zed.dev/pricing) for free for 14 days - no credit card required. Only $20/month afterward. Cancel anytime. diff --git a/crates/agent/src/ui/upsell.rs b/crates/agent/src/ui/upsell.rs index d66d8c5ebb..f311aade22 100644 --- a/crates/agent/src/ui/upsell.rs +++ b/crates/agent/src/ui/upsell.rs @@ -15,7 +15,7 @@ use ui::{ /// ``` /// let upsell = Upsell::new( /// "Upgrade to Zed Pro", -/// "Get unlimited access to AI features and more", +/// "Get access to advanced AI features and more", /// "Upgrade Now", /// Box::new(|_, _window, cx| { /// cx.open_url("https://zed.dev/pricing"); @@ -33,9 +33,9 @@ pub struct Upsell { title: SharedString, message: SharedString, cta_text: SharedString, - on_click: Box, - on_dismiss: Box, - on_dont_show_again: Box, + on_click: Box, + on_dismiss: Box, + on_dont_show_again: Box, } impl Upsell { @@ -44,9 +44,9 @@ impl Upsell { title: impl Into, message: impl Into, cta_text: impl Into, - on_click: Box, - on_dismiss: Box, - on_dont_show_again: Box, + on_click: Box, + on_dismiss: Box, + on_dont_show_again: Box, ) -> Self { Self { title: title.into(), @@ -105,7 +105,7 @@ impl RenderOnce for Upsell { h_flex() .gap_2() .child( - Button::new("dismiss-button", "Dismiss") + Button::new("dismiss-button", "No Thanks") .style(ButtonStyle::Subtle) .on_click(self.on_dismiss), ) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 75a15a3e19..1a1ce35152 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -713,6 +713,10 @@ impl UserStore { self.current_plan } + pub fn trial_started_at(&self) -> Option> { + self.trial_started_at + } + pub fn usage_based_billing_enabled(&self) -> Option { self.is_usage_based_billing_enabled } diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs index f22335b9cb..009faac128 100644 --- a/crates/ui/src/components/image.rs +++ b/crates/ui/src/components/image.rs @@ -24,6 +24,8 @@ use crate::prelude::*; pub enum VectorName { ZedLogo, ZedXCopilot, + Grid, + AiGrid, } /// A vector image, such as an SVG.