ai onboarding: Add overall fixes to the whole flow (#34996)
Closes https://github.com/zed-industries/zed/issues/34979 Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga <hi@aguz.me> Co-authored-by: Ben Kunkle <Ben.kunkle@gmail.com>
This commit is contained in:
parent
fab450e39d
commit
29332c1962
12 changed files with 150 additions and 250 deletions
|
@ -41,6 +41,9 @@ use std::{
|
||||||
};
|
};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
|
||||||
|
pub static ZED_STATELESS: std::sync::LazyLock<bool> =
|
||||||
|
std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum DataType {
|
pub enum DataType {
|
||||||
#[serde(rename = "json")]
|
#[serde(rename = "json")]
|
||||||
|
@ -874,7 +877,11 @@ impl ThreadsDatabase {
|
||||||
|
|
||||||
let needs_migration_from_heed = mdb_path.exists();
|
let needs_migration_from_heed = mdb_path.exists();
|
||||||
|
|
||||||
let connection = Connection::open_file(&sqlite_path.to_string_lossy());
|
let connection = if *ZED_STATELESS {
|
||||||
|
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
|
||||||
|
} else {
|
||||||
|
Connection::open_file(&sqlite_path.to_string_lossy())
|
||||||
|
};
|
||||||
|
|
||||||
connection.exec(indoc! {"
|
connection.exec(indoc! {"
|
||||||
CREATE TABLE IF NOT EXISTS threads (
|
CREATE TABLE IF NOT EXISTS threads (
|
||||||
|
|
|
@ -185,6 +185,13 @@ impl AgentConfiguration {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let is_signed_in = self
|
||||||
|
.workspace
|
||||||
|
.read_with(cx, |workspace, _| {
|
||||||
|
workspace.client().status().borrow().is_connected()
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.when(is_expanded, |this| this.mb_2())
|
.when(is_expanded, |this| this.mb_2())
|
||||||
|
@ -233,8 +240,8 @@ impl AgentConfiguration {
|
||||||
.size(LabelSize::Large),
|
.size(LabelSize::Large),
|
||||||
)
|
)
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if is_zed_provider {
|
if is_zed_provider && is_signed_in {
|
||||||
this.gap_2().child(
|
this.child(
|
||||||
self.render_zed_plan_info(current_plan, cx),
|
self.render_zed_plan_info(current_plan, cx),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -564,6 +564,17 @@ impl AgentPanel {
|
||||||
let inline_assist_context_store =
|
let inline_assist_context_store =
|
||||||
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
|
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
|
||||||
|
|
||||||
|
let thread_id = thread.read(cx).id().clone();
|
||||||
|
|
||||||
|
let history_store = cx.new(|cx| {
|
||||||
|
HistoryStore::new(
|
||||||
|
thread_store.clone(),
|
||||||
|
context_store.clone(),
|
||||||
|
[HistoryEntryId::Thread(thread_id)],
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let message_editor = cx.new(|cx| {
|
let message_editor = cx.new(|cx| {
|
||||||
MessageEditor::new(
|
MessageEditor::new(
|
||||||
fs.clone(),
|
fs.clone(),
|
||||||
|
@ -573,22 +584,13 @@ impl AgentPanel {
|
||||||
prompt_store.clone(),
|
prompt_store.clone(),
|
||||||
thread_store.downgrade(),
|
thread_store.downgrade(),
|
||||||
context_store.downgrade(),
|
context_store.downgrade(),
|
||||||
|
Some(history_store.downgrade()),
|
||||||
thread.clone(),
|
thread.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let thread_id = thread.read(cx).id().clone();
|
|
||||||
let history_store = cx.new(|cx| {
|
|
||||||
HistoryStore::new(
|
|
||||||
thread_store.clone(),
|
|
||||||
context_store.clone(),
|
|
||||||
[HistoryEntryId::Thread(thread_id)],
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
|
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
|
||||||
|
|
||||||
let active_thread = cx.new(|cx| {
|
let active_thread = cx.new(|cx| {
|
||||||
|
@ -851,6 +853,7 @@ impl AgentPanel {
|
||||||
self.prompt_store.clone(),
|
self.prompt_store.clone(),
|
||||||
self.thread_store.downgrade(),
|
self.thread_store.downgrade(),
|
||||||
self.context_store.downgrade(),
|
self.context_store.downgrade(),
|
||||||
|
Some(self.history_store.downgrade()),
|
||||||
thread.clone(),
|
thread.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -1124,6 +1127,7 @@ impl AgentPanel {
|
||||||
self.prompt_store.clone(),
|
self.prompt_store.clone(),
|
||||||
self.thread_store.downgrade(),
|
self.thread_store.downgrade(),
|
||||||
self.context_store.downgrade(),
|
self.context_store.downgrade(),
|
||||||
|
Some(self.history_store.downgrade()),
|
||||||
thread.clone(),
|
thread.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -2283,20 +2287,21 @@ impl AgentPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
match &self.active_view {
|
match &self.active_view {
|
||||||
ActiveView::Thread { thread, .. } => thread
|
ActiveView::Thread { .. } | ActiveView::TextThread { .. } => {
|
||||||
.read(cx)
|
let history_is_empty = self
|
||||||
.thread()
|
.history_store
|
||||||
.read(cx)
|
.update(cx, |store, cx| store.recent_entries(1, cx).is_empty());
|
||||||
.configured_model()
|
|
||||||
.map_or(true, |model| {
|
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
|
||||||
model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID
|
.providers()
|
||||||
}),
|
.iter()
|
||||||
ActiveView::TextThread { .. } => LanguageModelRegistry::global(cx)
|
.any(|provider| {
|
||||||
.read(cx)
|
provider.is_authenticated(cx)
|
||||||
.default_model()
|
&& provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
|
||||||
.map_or(true, |model| {
|
});
|
||||||
model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID
|
|
||||||
}),
|
history_is_empty || !has_configured_non_zed_providers
|
||||||
|
}
|
||||||
ActiveView::ExternalAgentThread { .. }
|
ActiveView::ExternalAgentThread { .. }
|
||||||
| ActiveView::History
|
| ActiveView::History
|
||||||
| ActiveView::Configuration => false,
|
| ActiveView::Configuration => false,
|
||||||
|
@ -2317,9 +2322,8 @@ impl AgentPanel {
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
div()
|
div()
|
||||||
.size_full()
|
|
||||||
.when(thread_view, |this| {
|
.when(thread_view, |this| {
|
||||||
this.bg(cx.theme().colors().panel_background)
|
this.size_full().bg(cx.theme().colors().panel_background)
|
||||||
})
|
})
|
||||||
.when(text_thread_view, |this| {
|
.when(text_thread_view, |this| {
|
||||||
this.bg(cx.theme().colors().editor_background)
|
this.bg(cx.theme().colors().editor_background)
|
||||||
|
|
|
@ -9,6 +9,7 @@ use crate::ui::{
|
||||||
MaxModeTooltip,
|
MaxModeTooltip,
|
||||||
preview::{AgentPreview, UsageCallout},
|
preview::{AgentPreview, UsageCallout},
|
||||||
};
|
};
|
||||||
|
use agent::history_store::HistoryStore;
|
||||||
use agent::{
|
use agent::{
|
||||||
context::{AgentContextKey, ContextLoadResult, load_context},
|
context::{AgentContextKey, ContextLoadResult, load_context},
|
||||||
context_store::ContextStoreEvent,
|
context_store::ContextStoreEvent,
|
||||||
|
@ -29,8 +30,9 @@ use fs::Fs;
|
||||||
use futures::future::Shared;
|
use futures::future::Shared;
|
||||||
use futures::{FutureExt as _, future};
|
use futures::{FutureExt as _, future};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, KeyContext, Subscription, Task,
|
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, IntoElement, KeyContext,
|
||||||
TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
|
Subscription, Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point,
|
||||||
|
pulsating_between,
|
||||||
};
|
};
|
||||||
use language::{Buffer, Language, Point};
|
use language::{Buffer, Language, Point};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
|
@ -80,6 +82,7 @@ pub struct MessageEditor {
|
||||||
user_store: Entity<UserStore>,
|
user_store: Entity<UserStore>,
|
||||||
context_store: Entity<ContextStore>,
|
context_store: Entity<ContextStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
|
history_store: Option<WeakEntity<HistoryStore>>,
|
||||||
context_strip: Entity<ContextStrip>,
|
context_strip: Entity<ContextStrip>,
|
||||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||||
model_selector: Entity<AgentModelSelector>,
|
model_selector: Entity<AgentModelSelector>,
|
||||||
|
@ -161,6 +164,7 @@ impl MessageEditor {
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
thread_store: WeakEntity<ThreadStore>,
|
thread_store: WeakEntity<ThreadStore>,
|
||||||
text_thread_store: WeakEntity<TextThreadStore>,
|
text_thread_store: WeakEntity<TextThreadStore>,
|
||||||
|
history_store: Option<WeakEntity<HistoryStore>>,
|
||||||
thread: Entity<Thread>,
|
thread: Entity<Thread>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
@ -233,6 +237,7 @@ impl MessageEditor {
|
||||||
workspace,
|
workspace,
|
||||||
context_store,
|
context_store,
|
||||||
prompt_store,
|
prompt_store,
|
||||||
|
history_store,
|
||||||
context_strip,
|
context_strip,
|
||||||
context_picker_menu_handle,
|
context_picker_menu_handle,
|
||||||
load_context_task: None,
|
load_context_task: None,
|
||||||
|
@ -1661,32 +1666,36 @@ impl Render for MessageEditor {
|
||||||
|
|
||||||
let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
|
let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
|
||||||
|
|
||||||
let in_pro_trial = matches!(
|
let has_configured_providers = LanguageModelRegistry::read_global(cx)
|
||||||
self.user_store.read(cx).current_plan(),
|
.providers()
|
||||||
Some(proto::Plan::ZedProTrial)
|
.iter()
|
||||||
);
|
.filter(|provider| {
|
||||||
|
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
> 0;
|
||||||
|
|
||||||
let pro_user = matches!(
|
let is_signed_out = self
|
||||||
self.user_store.read(cx).current_plan(),
|
.workspace
|
||||||
Some(proto::Plan::ZedPro)
|
.read_with(cx, |workspace, _| {
|
||||||
);
|
workspace.client().status().borrow().is_signed_out()
|
||||||
|
})
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
let configured_providers: Vec<(IconName, SharedString)> =
|
let has_history = self
|
||||||
LanguageModelRegistry::read_global(cx)
|
.history_store
|
||||||
.providers()
|
.as_ref()
|
||||||
.iter()
|
.and_then(|hs| hs.update(cx, |hs, cx| hs.entries(cx).len() > 0).ok())
|
||||||
.filter(|provider| {
|
.unwrap_or(false)
|
||||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
|| self
|
||||||
})
|
.thread
|
||||||
.map(|provider| (provider.icon(), provider.name().0.clone()))
|
.read_with(cx, |thread, _| thread.messages().len() > 0);
|
||||||
.collect();
|
|
||||||
let has_existing_providers = configured_providers.len() > 0;
|
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.bg(cx.theme().colors().panel_background)
|
.bg(cx.theme().colors().panel_background)
|
||||||
.when(
|
.when(
|
||||||
has_existing_providers && !in_pro_trial && !pro_user,
|
!has_history && is_signed_out && has_configured_providers,
|
||||||
|this| this.child(cx.new(ApiKeysWithProviders::new)),
|
|this| this.child(cx.new(ApiKeysWithProviders::new)),
|
||||||
)
|
)
|
||||||
.when(changed_buffers.len() > 0, |parent| {
|
.when(changed_buffers.len() > 0, |parent| {
|
||||||
|
@ -1778,6 +1787,7 @@ impl AgentPreview for MessageEditor {
|
||||||
None,
|
None,
|
||||||
thread_store.downgrade(),
|
thread_store.downgrade(),
|
||||||
text_thread_store.downgrade(),
|
text_thread_store.downgrade(),
|
||||||
|
None,
|
||||||
thread,
|
thread,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
|
@ -5,7 +5,6 @@ mod end_trial_upsell;
|
||||||
mod new_thread_button;
|
mod new_thread_button;
|
||||||
mod onboarding_modal;
|
mod onboarding_modal;
|
||||||
pub mod preview;
|
pub mod preview;
|
||||||
mod upsell;
|
|
||||||
|
|
||||||
pub use agent_notification::*;
|
pub use agent_notification::*;
|
||||||
pub use burn_mode_tooltip::*;
|
pub use burn_mode_tooltip::*;
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||||
use ai_onboarding::{AgentPanelOnboardingCard, BulletItem};
|
use ai_onboarding::{AgentPanelOnboardingCard, BulletItem};
|
||||||
use client::zed_urls;
|
use client::zed_urls;
|
||||||
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
||||||
use ui::{Divider, List, prelude::*};
|
use ui::{Divider, List, Tooltip, prelude::*};
|
||||||
|
|
||||||
#[derive(IntoElement, RegisterComponent)]
|
#[derive(IntoElement, RegisterComponent)]
|
||||||
pub struct EndTrialUpsell {
|
pub struct EndTrialUpsell {
|
||||||
|
@ -33,14 +33,19 @@ impl RenderOnce for EndTrialUpsell {
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
List::new()
|
List::new()
|
||||||
.child(BulletItem::new("500 prompts per month with Claude models"))
|
.child(BulletItem::new("500 prompts with Claude models"))
|
||||||
.child(BulletItem::new("Unlimited edit predictions")),
|
.child(BulletItem::new(
|
||||||
|
"Unlimited edit predictions with Zeta, our open-source model",
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("cta-button", "Upgrade to Zed Pro")
|
Button::new("cta-button", "Upgrade to Zed Pro")
|
||||||
.full_width()
|
.full_width()
|
||||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))),
|
.on_click(move |_, _window, cx| {
|
||||||
|
telemetry::event!("Upgrade To Pro Clicked", state = "end-of-trial");
|
||||||
|
cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let free_section = v_flex()
|
let free_section = v_flex()
|
||||||
|
@ -55,37 +60,43 @@ impl RenderOnce for EndTrialUpsell {
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
.buffer_font(cx),
|
.buffer_font(cx),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
Label::new("(Current Plan)")
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6)))
|
||||||
|
.buffer_font(cx),
|
||||||
|
)
|
||||||
.child(Divider::horizontal()),
|
.child(Divider::horizontal()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
List::new()
|
List::new()
|
||||||
.child(BulletItem::new(
|
.child(BulletItem::new("50 prompts with the Claude models"))
|
||||||
"50 prompts per month with the Claude models",
|
.child(BulletItem::new("2,000 accepted edit predictions")),
|
||||||
))
|
|
||||||
.child(BulletItem::new(
|
|
||||||
"2000 accepted edit predictions using our open-source Zeta model",
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("dismiss-button", "Stay on Free")
|
|
||||||
.full_width()
|
|
||||||
.style(ButtonStyle::Outlined)
|
|
||||||
.on_click({
|
|
||||||
let callback = self.dismiss_upsell.clone();
|
|
||||||
move |_, window, cx| callback(window, cx)
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
AgentPanelOnboardingCard::new()
|
AgentPanelOnboardingCard::new()
|
||||||
.child(Headline::new("Your Zed Pro trial has expired."))
|
.child(Headline::new("Your Zed Pro Trial has expired"))
|
||||||
.child(
|
.child(
|
||||||
Label::new("You've been automatically reset to the Free plan.")
|
Label::new("You've been automatically reset to the Free plan.")
|
||||||
.size(LabelSize::Small)
|
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
.mb_1(),
|
.mb_2(),
|
||||||
)
|
)
|
||||||
.child(pro_section)
|
.child(pro_section)
|
||||||
.child(free_section)
|
.child(free_section)
|
||||||
|
.child(
|
||||||
|
h_flex().absolute().top_4().right_4().child(
|
||||||
|
IconButton::new("dismiss_onboarding", IconName::Close)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.tooltip(Tooltip::text("Dismiss"))
|
||||||
|
.on_click({
|
||||||
|
let callback = self.dismiss_upsell.clone();
|
||||||
|
move |_, window, cx| {
|
||||||
|
telemetry::event!("Banner Dismissed", source = "AI Onboarding");
|
||||||
|
callback(window, cx)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,163 +0,0 @@
|
||||||
use component::{Component, ComponentScope, single_example};
|
|
||||||
use gpui::{
|
|
||||||
AnyElement, App, ClickEvent, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
|
|
||||||
Window,
|
|
||||||
};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::{
|
|
||||||
Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, Color, Label, LabelCommon,
|
|
||||||
RegisterComponent, ToggleState, h_flex, v_flex,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A component that displays an upsell message with a call-to-action button
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// let upsell = Upsell::new(
|
|
||||||
/// "Upgrade to Zed Pro",
|
|
||||||
/// "Get access to advanced AI features and more",
|
|
||||||
/// "Upgrade Now",
|
|
||||||
/// Box::new(|_, _window, cx| {
|
|
||||||
/// cx.open_url("https://zed.dev/pricing");
|
|
||||||
/// }),
|
|
||||||
/// Box::new(|_, _window, cx| {
|
|
||||||
/// // Handle dismiss
|
|
||||||
/// }),
|
|
||||||
/// Box::new(|checked, window, cx| {
|
|
||||||
/// // Handle don't show again
|
|
||||||
/// }),
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
#[derive(IntoElement, RegisterComponent)]
|
|
||||||
pub struct Upsell {
|
|
||||||
title: SharedString,
|
|
||||||
message: SharedString,
|
|
||||||
cta_text: SharedString,
|
|
||||||
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
|
||||||
on_dismiss: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
|
||||||
on_dont_show_again: Box<dyn Fn(bool, &mut Window, &mut App) + 'static>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Upsell {
|
|
||||||
/// Create a new upsell component
|
|
||||||
pub fn new(
|
|
||||||
title: impl Into<SharedString>,
|
|
||||||
message: impl Into<SharedString>,
|
|
||||||
cta_text: impl Into<SharedString>,
|
|
||||||
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
|
||||||
on_dismiss: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
|
||||||
on_dont_show_again: Box<dyn Fn(bool, &mut Window, &mut App) + 'static>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
title: title.into(),
|
|
||||||
message: message.into(),
|
|
||||||
cta_text: cta_text.into(),
|
|
||||||
on_click,
|
|
||||||
on_dismiss,
|
|
||||||
on_dont_show_again,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for Upsell {
|
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.w_full()
|
|
||||||
.p_4()
|
|
||||||
.gap_3()
|
|
||||||
.bg(cx.theme().colors().surface_background)
|
|
||||||
.rounded_md()
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().colors().border)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
Label::new(self.title)
|
|
||||||
.size(ui::LabelSize::Large)
|
|
||||||
.weight(gpui::FontWeight::BOLD),
|
|
||||||
)
|
|
||||||
.child(Label::new(self.message).color(Color::Muted)),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.w_full()
|
|
||||||
.justify_between()
|
|
||||||
.items_center()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.items_center()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
Checkbox::new("dont-show-again", ToggleState::Unselected).on_click(
|
|
||||||
move |_, window, cx| {
|
|
||||||
(self.on_dont_show_again)(true, window, cx);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Label::new("Don't show again")
|
|
||||||
.color(Color::Muted)
|
|
||||||
.size(ui::LabelSize::Small),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
Button::new("dismiss-button", "No Thanks")
|
|
||||||
.style(ButtonStyle::Subtle)
|
|
||||||
.on_click(self.on_dismiss),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("cta-button", self.cta_text)
|
|
||||||
.style(ButtonStyle::Filled)
|
|
||||||
.on_click(self.on_click),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Upsell {
|
|
||||||
fn scope() -> ComponentScope {
|
|
||||||
ComponentScope::Agent
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name() -> &'static str {
|
|
||||||
"Upsell"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description() -> Option<&'static str> {
|
|
||||||
Some("A promotional component that displays a message with a call-to-action.")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
|
||||||
let examples = vec![
|
|
||||||
single_example(
|
|
||||||
"Default",
|
|
||||||
Upsell::new(
|
|
||||||
"Upgrade to Zed Pro",
|
|
||||||
"Get unlimited access to AI features and more with Zed Pro. Unlock advanced AI capabilities and other premium features.",
|
|
||||||
"Upgrade Now",
|
|
||||||
Box::new(|_, _, _| {}),
|
|
||||||
Box::new(|_, _, _| {}),
|
|
||||||
Box::new(|_, _, _| {}),
|
|
||||||
).render(window, cx).into_any_element(),
|
|
||||||
),
|
|
||||||
single_example(
|
|
||||||
"Short Message",
|
|
||||||
Upsell::new(
|
|
||||||
"Try Zed Pro for free",
|
|
||||||
"Start your 7-day trial today.",
|
|
||||||
"Start Trial",
|
|
||||||
Box::new(|_, _, _| {}),
|
|
||||||
Box::new(|_, _, _| {}),
|
|
||||||
Box::new(|_, _, _| {}),
|
|
||||||
).render(window, cx).into_any_element(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
Some(v_flex().gap_4().children(examples).into_any_element())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -61,6 +61,11 @@ impl Render for AgentPanelOnboarding {
|
||||||
Some(proto::Plan::ZedProTrial)
|
Some(proto::Plan::ZedProTrial)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let is_pro_user = matches!(
|
||||||
|
self.user_store.read(cx).current_plan(),
|
||||||
|
Some(proto::Plan::ZedPro)
|
||||||
|
);
|
||||||
|
|
||||||
AgentPanelOnboardingCard::new()
|
AgentPanelOnboardingCard::new()
|
||||||
.child(
|
.child(
|
||||||
ZedAiOnboarding::new(
|
ZedAiOnboarding::new(
|
||||||
|
@ -75,7 +80,7 @@ impl Render for AgentPanelOnboarding {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if enrolled_in_trial || self.configured_providers.len() >= 1 {
|
if enrolled_in_trial || is_pro_user || self.configured_providers.len() >= 1 {
|
||||||
this
|
this
|
||||||
} else {
|
} else {
|
||||||
this.child(ApiKeysWithoutProviders::new())
|
this.child(ApiKeysWithoutProviders::new())
|
||||||
|
|
|
@ -16,6 +16,7 @@ use client::{Client, UserStore, zed_urls};
|
||||||
use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
|
use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
|
||||||
use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*};
|
use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*};
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
pub struct BulletItem {
|
pub struct BulletItem {
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
}
|
}
|
||||||
|
@ -28,18 +29,27 @@ impl BulletItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoElement for BulletItem {
|
impl RenderOnce for BulletItem {
|
||||||
type Element = AnyElement;
|
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
|
let line_height = 0.85 * window.line_height();
|
||||||
|
|
||||||
fn into_element(self) -> Self::Element {
|
|
||||||
ListItem::new("list-item")
|
ListItem::new("list-item")
|
||||||
.selectable(false)
|
.selectable(false)
|
||||||
.start_slot(
|
.child(
|
||||||
Icon::new(IconName::Dash)
|
h_flex()
|
||||||
.size(IconSize::XSmall)
|
.w_full()
|
||||||
.color(Color::Hidden),
|
.min_w_0()
|
||||||
|
.gap_1()
|
||||||
|
.items_start()
|
||||||
|
.child(
|
||||||
|
h_flex().h(line_height).justify_center().child(
|
||||||
|
Icon::new(IconName::Dash)
|
||||||
|
.size(IconSize::XSmall)
|
||||||
|
.color(Color::Hidden),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(div().w_full().min_w_0().child(Label::new(self.label))),
|
||||||
)
|
)
|
||||||
.child(div().w_full().child(Label::new(self.label)))
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -373,7 +383,9 @@ impl ZedAiOnboarding {
|
||||||
.child(
|
.child(
|
||||||
List::new()
|
List::new()
|
||||||
.child(BulletItem::new("500 prompts with Claude models"))
|
.child(BulletItem::new("500 prompts with Claude models"))
|
||||||
.child(BulletItem::new("Unlimited edit predictions")),
|
.child(BulletItem::new(
|
||||||
|
"Unlimited edit predictions with Zeta, our open-source model",
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("pro", "Continue with Zed Pro")
|
Button::new("pro", "Continue with Zed Pro")
|
||||||
|
|
|
@ -767,6 +767,11 @@ impl ContextStore {
|
||||||
fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
|
pub static ZED_STATELESS: LazyLock<bool> =
|
||||||
|
LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
|
||||||
|
if *ZED_STATELESS {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
fs.create_dir(contexts_dir()).await?;
|
fs.create_dir(contexts_dir()).await?;
|
||||||
|
|
||||||
let mut paths = fs.read_dir(contexts_dir()).await?;
|
let mut paths = fs.read_dir(contexts_dir()).await?;
|
||||||
|
|
|
@ -765,12 +765,14 @@ impl UserStore {
|
||||||
|
|
||||||
pub fn current_plan(&self) -> Option<proto::Plan> {
|
pub fn current_plan(&self) -> Option<proto::Plan> {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
if let Ok(plan) = std::env::var("ZED_SIMULATE_ZED_PRO_PLAN").as_ref() {
|
if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() {
|
||||||
return match plan.as_str() {
|
return match plan.as_str() {
|
||||||
"free" => Some(proto::Plan::Free),
|
"free" => Some(proto::Plan::Free),
|
||||||
"trial" => Some(proto::Plan::ZedProTrial),
|
"trial" => Some(proto::Plan::ZedProTrial),
|
||||||
"pro" => Some(proto::Plan::ZedPro),
|
"pro" => Some(proto::Plan::ZedPro),
|
||||||
_ => None,
|
_ => {
|
||||||
|
panic!("ZED_SIMULATE_PLAN must be one of 'free', 'trial', or 'pro'");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1159,19 +1159,20 @@ impl RenderOnce for ZedAiConfiguration {
|
||||||
|
|
||||||
let manage_subscription_buttons = if is_pro {
|
let manage_subscription_buttons = if is_pro {
|
||||||
Button::new("manage_settings", "Manage Subscription")
|
Button::new("manage_settings", "Manage Subscription")
|
||||||
|
.full_width()
|
||||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))
|
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else if self.plan.is_none() || self.eligible_for_trial {
|
} else if self.plan.is_none() || self.eligible_for_trial {
|
||||||
Button::new("start_trial", "Start 14-day Free Pro Trial")
|
Button::new("start_trial", "Start 14-day Free Pro Trial")
|
||||||
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
|
|
||||||
.full_width()
|
.full_width()
|
||||||
|
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx)))
|
.on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx)))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
Button::new("upgrade", "Upgrade to Pro")
|
Button::new("upgrade", "Upgrade to Pro")
|
||||||
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
|
|
||||||
.full_width()
|
.full_width()
|
||||||
|
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)))
|
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue