onboarding: Add the AI page (#35351)
This PR starts the work on the AI onboarding page as well as the configuration modal Release Notes: - N/A --------- Co-authored-by: Danilo Leal <daniloleal09@gmail.com> Co-authored-by: Anthony <anthony@zed.dev>
This commit is contained in:
parent
e5c6a596a9
commit
b01d1872cc
12 changed files with 550 additions and 63 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -10923,6 +10923,7 @@ dependencies = [
|
|||
name = "onboarding"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ai_onboarding",
|
||||
"anyhow",
|
||||
"client",
|
||||
"command_palette_hooks",
|
||||
|
@ -10933,7 +10934,10 @@ dependencies = [
|
|||
"feature_flags",
|
||||
"fs",
|
||||
"gpui",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"language_model",
|
||||
"menu",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
|
|
|
@ -406,7 +406,9 @@ impl AgentConfiguration {
|
|||
SwitchField::new(
|
||||
"always-allow-tool-actions-switch",
|
||||
"Allow running commands without asking for confirmation",
|
||||
"The agent can perform potentially destructive actions without asking for your confirmation.",
|
||||
Some(
|
||||
"The agent can perform potentially destructive actions without asking for your confirmation.".into(),
|
||||
),
|
||||
always_allow_tool_actions,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
|
@ -424,7 +426,7 @@ impl AgentConfiguration {
|
|||
SwitchField::new(
|
||||
"single-file-review",
|
||||
"Enable single-file agent reviews",
|
||||
"Agent edits are also displayed in single-file editors for review.",
|
||||
Some("Agent edits are also displayed in single-file editors for review.".into()),
|
||||
single_file_review,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
|
@ -442,7 +444,9 @@ impl AgentConfiguration {
|
|||
SwitchField::new(
|
||||
"sound-notification",
|
||||
"Play sound when finished generating",
|
||||
"Hear a notification sound when the agent is done generating changes or needs your input.",
|
||||
Some(
|
||||
"Hear a notification sound when the agent is done generating changes or needs your input.".into(),
|
||||
),
|
||||
play_sound_when_agent_done,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
|
@ -460,7 +464,9 @@ impl AgentConfiguration {
|
|||
SwitchField::new(
|
||||
"modifier-send",
|
||||
"Use modifier to submit a message",
|
||||
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.",
|
||||
Some(
|
||||
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.".into(),
|
||||
),
|
||||
use_modifier_to_send,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use client::{Client, zed_urls};
|
||||
use cloud_llm_client::Plan;
|
||||
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
||||
use ui::{Divider, List, Vector, VectorName, prelude::*};
|
||||
|
||||
|
@ -10,13 +11,15 @@ use crate::{BulletItem, SignInStatus};
|
|||
pub struct AiUpsellCard {
|
||||
pub sign_in_status: SignInStatus,
|
||||
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
pub user_plan: Option<Plan>,
|
||||
}
|
||||
|
||||
impl AiUpsellCard {
|
||||
pub fn new(client: Arc<Client>) -> Self {
|
||||
pub fn new(client: Arc<Client>, user_plan: Option<Plan>) -> Self {
|
||||
let status = *client.status().borrow();
|
||||
|
||||
Self {
|
||||
user_plan,
|
||||
sign_in_status: status.into(),
|
||||
sign_in: Arc::new(move |_window, cx| {
|
||||
cx.spawn({
|
||||
|
@ -34,6 +37,7 @@ impl AiUpsellCard {
|
|||
impl RenderOnce for AiUpsellCard {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let pro_section = v_flex()
|
||||
.flex_grow()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
|
@ -56,6 +60,7 @@ impl RenderOnce for AiUpsellCard {
|
|||
);
|
||||
|
||||
let free_section = v_flex()
|
||||
.flex_grow()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
|
@ -71,7 +76,7 @@ impl RenderOnce for AiUpsellCard {
|
|||
)
|
||||
.child(
|
||||
List::new()
|
||||
.child(BulletItem::new("50 prompts with the Claude models"))
|
||||
.child(BulletItem::new("50 prompts with Claude models"))
|
||||
.child(BulletItem::new("2,000 accepted edit predictions")),
|
||||
);
|
||||
|
||||
|
@ -132,22 +137,28 @@ impl RenderOnce for AiUpsellCard {
|
|||
|
||||
v_flex()
|
||||
.relative()
|
||||
.p_6()
|
||||
.pt_4()
|
||||
.p_4()
|
||||
.pt_3()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.child(grid_bg)
|
||||
.child(gradient_bg)
|
||||
.child(Headline::new("Try Zed AI"))
|
||||
.child(Label::new(DESCRIPTION).color(Color::Muted).mb_2())
|
||||
.child(Label::new("Try Zed AI").size(LabelSize::Large))
|
||||
.child(
|
||||
div()
|
||||
.max_w_3_4()
|
||||
.mb_2()
|
||||
.child(Label::new(DESCRIPTION).color(Color::Muted)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.mt_1p5()
|
||||
.mb_2p5()
|
||||
.items_start()
|
||||
.gap_12()
|
||||
.gap_6()
|
||||
.child(free_section)
|
||||
.child(pro_section),
|
||||
)
|
||||
|
@ -183,6 +194,7 @@ impl Component for AiUpsellCard {
|
|||
AiUpsellCard {
|
||||
sign_in_status: SignInStatus::SignedOut,
|
||||
sign_in: Arc::new(|_, _| {}),
|
||||
user_plan: None,
|
||||
}
|
||||
.into_any_element(),
|
||||
),
|
||||
|
@ -191,6 +203,7 @@ impl Component for AiUpsellCard {
|
|||
AiUpsellCard {
|
||||
sign_in_status: SignInStatus::SignedIn,
|
||||
sign_in: Arc::new(|_, _| {}),
|
||||
user_plan: None,
|
||||
}
|
||||
.into_any_element(),
|
||||
),
|
||||
|
|
|
@ -16,6 +16,7 @@ default = []
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
ai_onboarding.workspace = true
|
||||
client.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
|
@ -25,7 +26,10 @@ editor.workspace = true
|
|||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
menu.workspace = true
|
||||
project.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
|
362
crates/onboarding/src/ai_setup_page.rs
Normal file
362
crates/onboarding/src/ai_setup_page.rs
Normal file
|
@ -0,0 +1,362 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use ai_onboarding::{AiUpsellCard, SignInStatus};
|
||||
use client::DisableAiSettings;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, AnyView, App, DismissEvent, EventEmitter, FocusHandle, Focusable, Window, prelude::*,
|
||||
};
|
||||
use itertools;
|
||||
|
||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use ui::{
|
||||
Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState,
|
||||
prelude::*,
|
||||
};
|
||||
use workspace::ModalView;
|
||||
|
||||
use util::ResultExt;
|
||||
use zed_actions::agent::OpenSettings;
|
||||
|
||||
use crate::Onboarding;
|
||||
|
||||
const FEATURED_PROVIDERS: [&'static str; 4] = ["anthropic", "google", "openai", "ollama"];
|
||||
|
||||
fn render_llm_provider_section(
|
||||
onboarding: &Onboarding,
|
||||
disabled: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
v_flex()
|
||||
.gap_4()
|
||||
.child(
|
||||
v_flex()
|
||||
.child(Label::new("Or use other LLM providers").size(LabelSize::Large))
|
||||
.child(
|
||||
Label::new("Bring your API keys to use the available providers with Zed's UI for free.")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(render_llm_provider_card(onboarding, disabled, window, cx))
|
||||
}
|
||||
|
||||
fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement {
|
||||
v_flex()
|
||||
.relative()
|
||||
.pt_2()
|
||||
.pb_2p5()
|
||||
.pl_3()
|
||||
.pr_2()
|
||||
.border_1()
|
||||
.border_dashed()
|
||||
.border_color(cx.theme().colors().border.opacity(0.5))
|
||||
.bg(cx.theme().colors().surface_background.opacity(0.3))
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.map(|this| {
|
||||
if disabled {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Label::new("AI is disabled across Zed"))
|
||||
.child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Success)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.child(Badge::new("PRIVACY").icon(IconName::FileLock)),
|
||||
)
|
||||
.child(
|
||||
Label::new("Re-enable it any time in Settings.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("We don't train models using your data"))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Badge::new("Privacy").icon(IconName::FileLock))
|
||||
.child(
|
||||
Button::new("learn_more", "Learn More")
|
||||
.style(ButtonStyle::Outlined)
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(|_, _, cx| {
|
||||
cx.open_url(
|
||||
"https://zed.dev/docs/ai/privacy-and-security",
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
"Feel confident in the security and privacy of your projects using Zed.",
|
||||
)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render_llm_provider_card(
|
||||
onboarding: &Onboarding,
|
||||
disabled: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
let registry = LanguageModelRegistry::read_global(cx);
|
||||
|
||||
v_flex()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().surface_background.opacity(0.5))
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.children(itertools::intersperse_with(
|
||||
FEATURED_PROVIDERS
|
||||
.into_iter()
|
||||
.flat_map(|provider_name| {
|
||||
registry.provider(&LanguageModelProviderId::new(provider_name))
|
||||
})
|
||||
.enumerate()
|
||||
.map(|(index, provider)| {
|
||||
let group_name = SharedString::new(format!("onboarding-hover-group-{}", index));
|
||||
let is_authenticated = provider.is_authenticated(cx);
|
||||
|
||||
ButtonLike::new(("onboarding-ai-setup-buttons", index))
|
||||
.size(ButtonSize::Large)
|
||||
.child(
|
||||
h_flex()
|
||||
.group(&group_name)
|
||||
.px_0p5()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(provider.icon())
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(Label::new(provider.name().0)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(!is_authenticated, |el| {
|
||||
el.visible_on_hover(group_name.clone())
|
||||
.child(
|
||||
Icon::new(IconName::Settings)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
Label::new("Configure")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
})
|
||||
.when(is_authenticated && !disabled, |el| {
|
||||
el.child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Success)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
Label::new("Configured")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let workspace = onboarding.workspace.clone();
|
||||
move |_, window, cx| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
let modal = AiConfigurationModal::new(
|
||||
provider.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
window.focus(&modal.focus_handle(cx));
|
||||
modal
|
||||
});
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}),
|
||||
|| Divider::horizontal().into_any_element(),
|
||||
))
|
||||
.child(Divider::horizontal())
|
||||
.child(
|
||||
Button::new("agent_settings", "Add Many Others")
|
||||
.size(ButtonSize::Large)
|
||||
.icon(IconName::Plus)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(OpenSettings.boxed_clone(), cx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn render_ai_setup_page(
|
||||
onboarding: &Onboarding,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
|
||||
|
||||
let backdrop = div()
|
||||
.id("backdrop")
|
||||
.size_full()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.opacity(0.8)
|
||||
.block_mouse_except_scroll();
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(SwitchField::new(
|
||||
"enable_ai",
|
||||
"Enable AI features",
|
||||
None,
|
||||
if is_ai_disabled {
|
||||
ToggleState::Unselected
|
||||
} else {
|
||||
ToggleState::Selected
|
||||
},
|
||||
|toggle_state, _, cx| {
|
||||
let enabled = match toggle_state {
|
||||
ToggleState::Indeterminate => {
|
||||
return;
|
||||
}
|
||||
ToggleState::Unselected => false,
|
||||
ToggleState::Selected => true,
|
||||
};
|
||||
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
update_settings_file::<DisableAiSettings>(
|
||||
fs,
|
||||
cx,
|
||||
move |ai_settings: &mut Option<bool>, _| {
|
||||
*ai_settings = Some(!enabled);
|
||||
},
|
||||
);
|
||||
},
|
||||
))
|
||||
.child(render_privacy_card(is_ai_disabled, cx))
|
||||
.child(
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.gap_6()
|
||||
.child(AiUpsellCard {
|
||||
sign_in_status: SignInStatus::SignedIn,
|
||||
sign_in: Arc::new(|_, _| {}),
|
||||
user_plan: onboarding.cloud_user_store.read(cx).plan(),
|
||||
})
|
||||
.child(render_llm_provider_section(
|
||||
onboarding,
|
||||
is_ai_disabled,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.when(is_ai_disabled, |this| this.child(backdrop)),
|
||||
)
|
||||
}
|
||||
|
||||
struct AiConfigurationModal {
|
||||
focus_handle: FocusHandle,
|
||||
selected_provider: Arc<dyn LanguageModelProvider>,
|
||||
configuration_view: AnyView,
|
||||
}
|
||||
|
||||
impl AiConfigurationModal {
|
||||
fn new(
|
||||
selected_provider: Arc<dyn LanguageModelProvider>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let configuration_view = selected_provider.configuration_view(window, cx);
|
||||
|
||||
Self {
|
||||
focus_handle,
|
||||
configuration_view,
|
||||
selected_provider,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for AiConfigurationModal {}
|
||||
|
||||
impl EventEmitter<DismissEvent> for AiConfigurationModal {}
|
||||
|
||||
impl Focusable for AiConfigurationModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AiConfigurationModal {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w(rems(34.))
|
||||
.elevation_3(cx)
|
||||
.track_focus(&self.focus_handle)
|
||||
.child(
|
||||
Modal::new("onboarding-ai-setup-modal", None)
|
||||
.header(
|
||||
ModalHeader::new()
|
||||
.icon(
|
||||
Icon::new(self.selected_provider.icon())
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.headline(self.selected_provider.name().0),
|
||||
)
|
||||
.section(Section::new().child(self.configuration_view.clone()))
|
||||
.footer(
|
||||
ModalFooter::new().end_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("onboarding-closing-cancel", "Cancel")
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
||||
)
|
||||
.child(Button::new("save-btn", "Done").on_click(cx.listener(
|
||||
|_, _, window, cx| {
|
||||
window.dispatch_action(menu::Confirm.boxed_clone(), cx);
|
||||
cx.emit(DismissEvent);
|
||||
},
|
||||
))),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -242,7 +242,7 @@ fn render_telemetry_section(cx: &App) -> impl IntoElement {
|
|||
.child(SwitchField::new(
|
||||
"onboarding-telemetry-metrics",
|
||||
"Help Improve Zed",
|
||||
"Sending anonymous usage data helps us build the right features and create the best experience.",
|
||||
Some("Sending anonymous usage data helps us build the right features and create the best experience.".into()),
|
||||
if TelemetrySettings::get_global(cx).metrics {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
|
@ -267,7 +267,7 @@ fn render_telemetry_section(cx: &App) -> impl IntoElement {
|
|||
.child(SwitchField::new(
|
||||
"onboarding-telemetry-crash-reports",
|
||||
"Help Fix Zed",
|
||||
"Send crash reports so we can fix critical issues fast.",
|
||||
Some("Send crash reports so we can fix critical issues fast.".into()),
|
||||
if TelemetrySettings::get_global(cx).diagnostics {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
|
@ -338,10 +338,10 @@ pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl Into
|
|||
.style(ui::ToggleButtonGroupStyle::Outlined)
|
||||
),
|
||||
)
|
||||
.child(v_flex().justify_center().child(div().h_0().child("hack").invisible()).child(SwitchField::new(
|
||||
.child(SwitchField::new(
|
||||
"onboarding-vim-mode",
|
||||
"Vim Mode",
|
||||
"Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back.",
|
||||
Some("Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back.".into()),
|
||||
if VimModeSetting::get_global(cx).0 {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
|
@ -363,6 +363,6 @@ pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl Into
|
|||
);
|
||||
}
|
||||
},
|
||||
)))
|
||||
))
|
||||
.child(render_telemetry_section(cx))
|
||||
}
|
||||
|
|
|
@ -349,7 +349,7 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In
|
|||
.child(SwitchField::new(
|
||||
"onboarding-enable-inlay-hints",
|
||||
"Inlay Hints",
|
||||
"See parameter names for function and method calls inline.",
|
||||
Some("See parameter names for function and method calls inline.".into()),
|
||||
if read_inlay_hints(cx) {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
|
@ -362,7 +362,7 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In
|
|||
.child(SwitchField::new(
|
||||
"onboarding-git-blame-switch",
|
||||
"Git Blame",
|
||||
"See who committed each line on a given file.",
|
||||
Some("See who committed each line on a given file.".into()),
|
||||
if read_git_blame(cx) {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::welcome::{ShowWelcome, WelcomePage};
|
||||
use client::{Client, UserStore};
|
||||
use client::{Client, CloudUserStore, UserStore};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
|
||||
|
@ -25,6 +25,7 @@ use workspace::{
|
|||
open_new, with_active_or_new_workspace,
|
||||
};
|
||||
|
||||
mod ai_setup_page;
|
||||
mod basics_page;
|
||||
mod editing_page;
|
||||
mod theme_preview;
|
||||
|
@ -78,11 +79,7 @@ pub fn init(cx: &mut App) {
|
|||
if let Some(existing) = existing {
|
||||
workspace.activate_item(&existing, true, true, window, cx);
|
||||
} else {
|
||||
let settings_page = Onboarding::new(
|
||||
workspace.weak_handle(),
|
||||
workspace.user_store().clone(),
|
||||
cx,
|
||||
);
|
||||
let settings_page = Onboarding::new(workspace, cx);
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(settings_page),
|
||||
None,
|
||||
|
@ -198,8 +195,7 @@ pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyh
|
|||
|workspace, window, cx| {
|
||||
{
|
||||
workspace.toggle_dock(DockPosition::Left, window, cx);
|
||||
let onboarding_page =
|
||||
Onboarding::new(workspace.weak_handle(), workspace.user_store().clone(), cx);
|
||||
let onboarding_page = Onboarding::new(workspace, cx);
|
||||
workspace.add_item_to_center(Box::new(onboarding_page.clone()), window, cx);
|
||||
|
||||
window.focus(&onboarding_page.focus_handle(cx));
|
||||
|
@ -224,21 +220,19 @@ struct Onboarding {
|
|||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
selected_page: SelectedPage,
|
||||
cloud_user_store: Entity<CloudUserStore>,
|
||||
user_store: Entity<UserStore>,
|
||||
_settings_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl Onboarding {
|
||||
fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
fn new(workspace: &Workspace, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| Self {
|
||||
workspace,
|
||||
user_store,
|
||||
workspace: workspace.weak_handle(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
selected_page: SelectedPage::Basics,
|
||||
cloud_user_store: workspace.app_state().cloud_user_store.clone(),
|
||||
user_store: workspace.user_store().clone(),
|
||||
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
|
||||
})
|
||||
}
|
||||
|
@ -391,13 +385,11 @@ impl Onboarding {
|
|||
SelectedPage::Editing => {
|
||||
crate::editing_page::render_editing_page(window, cx).into_any_element()
|
||||
}
|
||||
SelectedPage::AiSetup => self.render_ai_setup_page(window, cx).into_any_element(),
|
||||
SelectedPage::AiSetup => {
|
||||
crate::ai_setup_page::render_ai_setup_page(&self, window, cx).into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_ai_setup_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
div().child("ai setup page")
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Onboarding {
|
||||
|
@ -418,7 +410,9 @@ impl Render for Onboarding {
|
|||
.gap_12()
|
||||
.child(self.render_nav(window, cx))
|
||||
.child(
|
||||
div()
|
||||
v_flex()
|
||||
.max_w_full()
|
||||
.min_w_0()
|
||||
.pl_12()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant.opacity(0.5))
|
||||
|
@ -458,11 +452,9 @@ impl Item for Onboarding {
|
|||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Entity<Self>> {
|
||||
Some(Onboarding::new(
|
||||
self.workspace.clone(),
|
||||
self.user_store.clone(),
|
||||
cx,
|
||||
))
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| Onboarding::new(workspace, cx))
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
mod avatar;
|
||||
mod badge;
|
||||
mod banner;
|
||||
mod button;
|
||||
mod callout;
|
||||
|
@ -41,6 +42,7 @@ mod tooltip;
|
|||
mod stories;
|
||||
|
||||
pub use avatar::*;
|
||||
pub use badge::*;
|
||||
pub use banner::*;
|
||||
pub use button::*;
|
||||
pub use callout::*;
|
||||
|
|
71
crates/ui/src/components/badge.rs
Normal file
71
crates/ui/src/components/badge.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use crate::Divider;
|
||||
use crate::DividerColor;
|
||||
use crate::component_prelude::*;
|
||||
use crate::prelude::*;
|
||||
use gpui::{AnyElement, IntoElement, SharedString, Window};
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct Badge {
|
||||
label: SharedString,
|
||||
icon: IconName,
|
||||
}
|
||||
|
||||
impl Badge {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
icon: IconName::Check,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: IconName) -> Self {
|
||||
self.icon = icon;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Badge {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
h_flex()
|
||||
.h_full()
|
||||
.gap_1()
|
||||
.pl_1()
|
||||
.pr_2()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.rounded_sm()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
Icon::new(self.icon)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Divider::vertical().color(DividerColor::Border))
|
||||
.child(
|
||||
Label::new(self.label.clone())
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx)
|
||||
.ml_1(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Badge {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::DataDisplay
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
Some(
|
||||
"A compact, labeled component with optional icon for displaying status, categories, or metadata.",
|
||||
)
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
Some(
|
||||
single_example("Basic Badge", Badge::new("Default").into_any_element())
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
Clickable, Color, DynamicSpacing, Headline, HeadlineSize, IconButton, IconButtonShape,
|
||||
Clickable, Color, DynamicSpacing, Headline, HeadlineSize, Icon, IconButton, IconButtonShape,
|
||||
IconName, Label, LabelCommon, LabelSize, h_flex, v_flex,
|
||||
};
|
||||
use gpui::{prelude::FluentBuilder, *};
|
||||
|
@ -92,6 +92,7 @@ impl RenderOnce for Modal {
|
|||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModalHeader {
|
||||
icon: Option<Icon>,
|
||||
headline: Option<SharedString>,
|
||||
description: Option<SharedString>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
|
@ -108,6 +109,7 @@ impl Default for ModalHeader {
|
|||
impl ModalHeader {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
icon: None,
|
||||
headline: None,
|
||||
description: None,
|
||||
children: SmallVec::new(),
|
||||
|
@ -116,6 +118,11 @@ impl ModalHeader {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: Icon) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the headline of the modal.
|
||||
///
|
||||
/// This will insert the headline as the first item
|
||||
|
@ -179,12 +186,17 @@ impl RenderOnce for ModalHeader {
|
|||
)
|
||||
})
|
||||
.child(
|
||||
v_flex().flex_1().children(children).when_some(
|
||||
self.description,
|
||||
|this, description| {
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when_some(self.icon, |this, icon| this.child(icon))
|
||||
.children(children),
|
||||
)
|
||||
.when_some(self.description, |this, description| {
|
||||
this.child(Label::new(description).color(Color::Muted).mb_2())
|
||||
},
|
||||
),
|
||||
}),
|
||||
)
|
||||
.when(self.show_dismiss_button, |this| {
|
||||
this.child(
|
||||
|
|
|
@ -566,7 +566,7 @@ impl RenderOnce for Switch {
|
|||
pub struct SwitchField {
|
||||
id: ElementId,
|
||||
label: SharedString,
|
||||
description: SharedString,
|
||||
description: Option<SharedString>,
|
||||
toggle_state: ToggleState,
|
||||
on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
|
||||
disabled: bool,
|
||||
|
@ -577,14 +577,14 @@ impl SwitchField {
|
|||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
label: impl Into<SharedString>,
|
||||
description: impl Into<SharedString>,
|
||||
description: Option<SharedString>,
|
||||
toggle_state: impl Into<ToggleState>,
|
||||
on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
label: label.into(),
|
||||
description: description.into(),
|
||||
description: description,
|
||||
toggle_state: toggle_state.into(),
|
||||
on_click: Arc::new(on_click),
|
||||
disabled: false,
|
||||
|
@ -592,6 +592,11 @@ impl SwitchField {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
|
@ -616,13 +621,15 @@ impl RenderOnce for SwitchField {
|
|||
.gap_4()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
.child(
|
||||
v_flex()
|
||||
.child(match &self.description {
|
||||
Some(description) => v_flex()
|
||||
.gap_0p5()
|
||||
.max_w_5_6()
|
||||
.child(Label::new(self.label))
|
||||
.child(Label::new(self.description).color(Color::Muted)),
|
||||
)
|
||||
.child(Label::new(self.label.clone()))
|
||||
.child(Label::new(description.clone()).color(Color::Muted))
|
||||
.into_any_element(),
|
||||
None => Label::new(self.label.clone()).into_any_element(),
|
||||
})
|
||||
.child(
|
||||
Switch::new(
|
||||
SharedString::from(format!("{}-switch", self.id)),
|
||||
|
@ -671,7 +678,7 @@ impl Component for SwitchField {
|
|||
SwitchField::new(
|
||||
"switch_field_unselected",
|
||||
"Enable notifications",
|
||||
"Receive notifications when new messages arrive.",
|
||||
Some("Receive notifications when new messages arrive.".into()),
|
||||
ToggleState::Unselected,
|
||||
|_, _, _| {},
|
||||
)
|
||||
|
@ -682,7 +689,7 @@ impl Component for SwitchField {
|
|||
SwitchField::new(
|
||||
"switch_field_selected",
|
||||
"Enable notifications",
|
||||
"Receive notifications when new messages arrive.",
|
||||
Some("Receive notifications when new messages arrive.".into()),
|
||||
ToggleState::Selected,
|
||||
|_, _, _| {},
|
||||
)
|
||||
|
@ -698,7 +705,7 @@ impl Component for SwitchField {
|
|||
SwitchField::new(
|
||||
"switch_field_default",
|
||||
"Default color",
|
||||
"This uses the default switch color.",
|
||||
Some("This uses the default switch color.".into()),
|
||||
ToggleState::Selected,
|
||||
|_, _, _| {},
|
||||
)
|
||||
|
@ -709,7 +716,7 @@ impl Component for SwitchField {
|
|||
SwitchField::new(
|
||||
"switch_field_accent",
|
||||
"Accent color",
|
||||
"This uses the accent color scheme.",
|
||||
Some("This uses the accent color scheme.".into()),
|
||||
ToggleState::Selected,
|
||||
|_, _, _| {},
|
||||
)
|
||||
|
@ -725,7 +732,7 @@ impl Component for SwitchField {
|
|||
SwitchField::new(
|
||||
"switch_field_disabled",
|
||||
"Disabled field",
|
||||
"This field is disabled and cannot be toggled.",
|
||||
Some("This field is disabled and cannot be toggled.".into()),
|
||||
ToggleState::Selected,
|
||||
|_, _, _| {},
|
||||
)
|
||||
|
@ -733,6 +740,20 @@ impl Component for SwitchField {
|
|||
.into_any_element(),
|
||||
)],
|
||||
),
|
||||
example_group_with_title(
|
||||
"No Description",
|
||||
vec![single_example(
|
||||
"No Description",
|
||||
SwitchField::new(
|
||||
"switch_field_disabled",
|
||||
"Disabled field",
|
||||
None,
|
||||
ToggleState::Selected,
|
||||
|_, _, _| {},
|
||||
)
|
||||
.into_any_element(),
|
||||
)],
|
||||
),
|
||||
])
|
||||
.into_any_element(),
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue