assistant2: Adjust empty state layout (#25745)

Going for a different, arguably simpler design for the Assistant 2 empty
state here. Also took the opportunity to adjust other elements like the
toolbar, message editor, and some items in the configuration page.

<img
src="https://github.com/user-attachments/assets/03fd1d48-a675-4eac-b694-bbe4eeaf06e9"
width="700px"/>

Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2025-02-27 11:33:53 -03:00 committed by GitHub
parent 635b80ed51
commit 5c400dac8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 189 additions and 139 deletions

View file

@ -158,8 +158,16 @@ impl Render for AssistantConfiguration {
.child( .child(
v_flex() v_flex()
.p(DynamicSpacing::Base16.rems(cx)) .p(DynamicSpacing::Base16.rems(cx))
.gap_1() .gap_2()
.child(Headline::new("Prompt Library").size(HeadlineSize::Small)) .child(
v_flex()
.gap_0p5()
.child(Headline::new("Prompt Library").size(HeadlineSize::Small))
.child(
Label::new("Create reusable prompts and tag which ones you want sent in every LLM interaction.")
.color(Color::Muted),
),
)
.child( .child(
Button::new("open-prompt-library", "Open Prompt Library") Button::new("open-prompt-library", "Open Prompt Library")
.style(ButtonStyle::Filled) .style(ButtonStyle::Filled)

View file

@ -14,7 +14,7 @@ use client::zed_urls;
use editor::Editor; use editor::Editor;
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
prelude::*, px, svg, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter, prelude::*, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter,
FocusHandle, Focusable, FontWeight, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, FocusHandle, Focusable, FontWeight, Pixels, Subscription, Task, UpdateGlobal, WeakEntity,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
@ -596,7 +596,6 @@ impl AssistantPanel {
h_flex() h_flex()
.id("assistant-toolbar") .id("assistant-toolbar")
.px(DynamicSpacing::Base08.rems(cx))
.h(Tab::container_height(cx)) .h(Tab::container_height(cx))
.flex_none() .flex_none()
.justify_between() .justify_between()
@ -604,72 +603,86 @@ impl AssistantPanel {
.bg(cx.theme().colors().tab_bar_background) .bg(cx.theme().colors().tab_bar_background)
.border_b_1() .border_b_1()
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.child(
div()
.id("title")
.overflow_x_scroll()
.px(DynamicSpacing::Base08.rems(cx))
.child(Label::new(title).text_ellipsis()),
)
.child( .child(
h_flex() h_flex()
.w_full() .h_full()
.gap_1() .pl_2()
.justify_between() .gap_2()
.child(Label::new(title)) .bg(cx.theme().colors().tab_bar_background)
.children(if matches!(self.active_view, ActiveView::PromptEditor) { .children(if matches!(self.active_view, ActiveView::PromptEditor) {
self.context_editor self.context_editor
.as_ref() .as_ref()
.and_then(|editor| render_remaining_tokens(editor, cx)) .and_then(|editor| render_remaining_tokens(editor, cx))
} else { } else {
None None
}), })
)
.child(
h_flex()
.h_full()
.pl_1p5()
.border_l_1()
.border_color(cx.theme().colors().border)
.gap(DynamicSpacing::Base02.rems(cx))
.child( .child(
PopoverMenu::new("assistant-toolbar-new-popover-menu") h_flex()
.trigger_with_tooltip( .h_full()
IconButton::new("new", IconName::Plus) .px(DynamicSpacing::Base08.rems(cx))
.icon_size(IconSize::Small) .border_l_1()
.style(ButtonStyle::Subtle), .border_color(cx.theme().colors().border)
Tooltip::text("New…"), .gap(DynamicSpacing::Base02.rems(cx))
) .child(
.anchor(Corner::TopRight) PopoverMenu::new("assistant-toolbar-new-popover-menu")
.with_handle(self.new_item_context_menu_handle.clone()) .trigger_with_tooltip(
.menu(move |window, cx| { IconButton::new("new", IconName::Plus)
Some(ContextMenu::build(window, cx, |menu, _window, _cx| { .icon_size(IconSize::Small)
menu.action("New Thread", NewThread.boxed_clone()) .style(ButtonStyle::Subtle),
.action("New Prompt Editor", NewPromptEditor.boxed_clone()) Tooltip::text("New…"),
}))
}),
)
.child(
IconButton::new("open-history", IconName::HistoryRerun)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip({
let focus_handle = self.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"History",
&OpenHistory,
&focus_handle,
window,
cx,
) )
} .anchor(Corner::TopRight)
}) .with_handle(self.new_item_context_menu_handle.clone())
.on_click(move |_event, window, cx| { .menu(move |window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx); Some(ContextMenu::build(
}), window,
) cx,
.child( |menu, _window, _cx| {
IconButton::new("configure-assistant", IconName::Settings) menu.action("New Thread", NewThread.boxed_clone())
.icon_size(IconSize::Small) .action(
.style(ButtonStyle::Subtle) "New Prompt Editor",
.tooltip(Tooltip::text("Assistant Settings")) NewPromptEditor.boxed_clone(),
.on_click(move |_event, window, cx| { )
window.dispatch_action(OpenConfiguration.boxed_clone(), cx); },
}), ))
}),
)
.child(
IconButton::new("open-history", IconName::HistoryRerun)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip({
let focus_handle = self.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"History",
&OpenHistory,
&focus_handle,
window,
cx,
)
}
})
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
}),
)
.child(
IconButton::new("configure-assistant", IconName::Settings)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip(Tooltip::text("Assistant Settings"))
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenConfiguration.boxed_clone(), cx);
}),
),
), ),
) )
} }
@ -711,12 +724,11 @@ impl AssistantPanel {
) -> impl IntoElement { ) -> impl IntoElement {
let recent_history = self let recent_history = self
.history_store .history_store
.update(cx, |this, cx| this.recent_entries(3, cx)); .update(cx, |this, cx| this.recent_entries(6, cx));
let create_welcome_heading = || { let create_welcome_heading = || {
h_flex() h_flex()
.w_full() .w_full()
.justify_center()
.child(Headline::new("Welcome to the Assistant Panel").size(HeadlineSize::Small)) .child(Headline::new("Welcome to the Assistant Panel").size(HeadlineSize::Small))
}; };
@ -724,36 +736,27 @@ impl AssistantPanel {
let no_error = configuration_error.is_none(); let no_error = configuration_error.is_none();
v_flex() v_flex()
.gap_2() .p_1p5()
.child( .size_full()
v_flex().w_full().child( .justify_end()
svg() .gap_1()
.path("icons/logo_96.svg")
.text_color(cx.theme().colors().text)
.w(px(40.))
.h(px(40.))
.mx_auto()
.mb_4(),
),
)
.map(|parent| { .map(|parent| {
match configuration_error { match configuration_error {
Some(ConfigurationError::ProviderNotAuthenticated) Some(ConfigurationError::ProviderNotAuthenticated)
| Some(ConfigurationError::NoProvider) => { | Some(ConfigurationError::NoProvider) => {
parent.child( parent.child(
v_flex() v_flex()
.px_1p5()
.gap_0p5() .gap_0p5()
.child(create_welcome_heading()) .child(create_welcome_heading())
.child( .child(
h_flex().mb_2().w_full().justify_center().child( Label::new(
Label::new( "To start using the assistant, configure at least one LLM provider.",
"To start using the assistant, configure at least one LLM provider.", )
) .color(Color::Muted),
.color(Color::Muted),
),
) )
.child( .child(
h_flex().w_full().justify_center().child( h_flex().mt_1().w_full().child(
Button::new("open-configuration", "Configure a Provider") Button::new("open-configuration", "Configure a Provider")
.size(ButtonSize::Compact) .size(ButtonSize::Compact)
.icon(Some(IconName::Sliders)) .icon(Some(IconName::Sliders))
@ -767,7 +770,7 @@ impl AssistantPanel {
) )
} }
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent
.child(v_flex().gap_0p5().child(create_welcome_heading()).children( .child(v_flex().px_1p5().gap_0p5().child(create_welcome_heading()).children(
provider.render_accept_terms( provider.render_accept_terms(
LanguageModelProviderTosView::ThreadEmptyState, LanguageModelProviderTosView::ThreadEmptyState,
cx, cx,
@ -778,21 +781,40 @@ impl AssistantPanel {
}) })
.when(recent_history.is_empty() && no_error, |parent| { .when(recent_history.is_empty() && no_error, |parent| {
parent.child(v_flex().gap_0p5().child(create_welcome_heading()).child( parent.child(v_flex().gap_0p5().child(create_welcome_heading()).child(
h_flex().w_full().justify_center().child( Label::new("Start typing to chat with your codebase").color(Color::Muted),
Label::new("Start typing to chat with your codebase").color(Color::Muted),
),
)) ))
}) })
.when(!recent_history.is_empty(), |parent| { .when(!recent_history.is_empty(), |parent| {
parent parent
.child( .child(
h_flex().w_full().justify_center().child( h_flex()
Label::new("Recent Threads:") .pl_1p5()
.size(LabelSize::Small) .pb_1()
.color(Color::Muted), .w_full()
), .justify_between()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(
Label::new("Past Interactions")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Button::new("view-history", "View All")
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action_in(
&OpenHistory,
&self.focus_handle(cx),
window,
cx,
))
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
}),
),
) )
.child(v_flex().mx_auto().w_4_5().gap_2().children( .child(v_flex().gap_1().children(
recent_history.into_iter().map(|entry| { recent_history.into_iter().map(|entry| {
// TODO: Add keyboard navigation. // TODO: Add keyboard navigation.
match entry { match entry {
@ -807,22 +829,6 @@ impl AssistantPanel {
} }
}), }),
)) ))
.child(
h_flex().w_full().justify_center().child(
Button::new("view-all-past-threads", "View All Past Threads")
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action_in(
&OpenHistory,
&self.focus_handle(cx),
window,
cx,
))
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
}),
),
)
}) })
} }

View file

@ -314,7 +314,7 @@ impl Render for MessageEditor {
.child(self.context_strip.clone()) .child(self.context_strip.clone())
.child( .child(
v_flex() v_flex()
.gap_4() .gap_5()
.child({ .child({
let settings = ThemeSettings::get_global(cx); let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle { let text_style = TextStyle {

View file

@ -254,18 +254,28 @@ impl RenderOnce for PastThread {
); );
ListItem::new(SharedString::from(self.thread.id.to_string())) ListItem::new(SharedString::from(self.thread.id.to_string()))
.outlined() .rounded()
.toggle_state(self.selected) .toggle_state(self.selected)
.start_slot(
Icon::new(IconName::MessageCircle)
.size(IconSize::Small)
.color(Color::Muted),
)
.spacing(ListItemSpacing::Sparse) .spacing(ListItemSpacing::Sparse)
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis()) .start_slot(
div()
.max_w_4_5()
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis()),
)
.end_slot( .end_slot(
h_flex() h_flex()
.gap_1p5() .gap_1p5()
.child(
Label::new("Thread")
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.child(
div()
.size(px(3.))
.rounded_full()
.bg(cx.theme().colors().text_disabled),
)
.child( .child(
Label::new(thread_timestamp) Label::new(thread_timestamp)
.color(Color::Muted) .color(Color::Muted)
@ -340,18 +350,28 @@ impl RenderOnce for PastContext {
ListItem::new(SharedString::from( ListItem::new(SharedString::from(
self.context.path.to_string_lossy().to_string(), self.context.path.to_string_lossy().to_string(),
)) ))
.outlined() .rounded()
.toggle_state(self.selected) .toggle_state(self.selected)
.start_slot(
Icon::new(IconName::Code)
.size(IconSize::Small)
.color(Color::Muted),
)
.spacing(ListItemSpacing::Sparse) .spacing(ListItemSpacing::Sparse)
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis()) .start_slot(
div()
.max_w_4_5()
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis()),
)
.end_slot( .end_slot(
h_flex() h_flex()
.gap_1p5() .gap_1p5()
.child(
Label::new("Prompt Editor")
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.child(
div()
.size(px(3.))
.rounded_full()
.bg(cx.theme().colors().text_disabled),
)
.child( .child(
Label::new(context_timestamp) Label::new(context_timestamp)
.color(Color::Muted) .color(Color::Muted)

View file

@ -960,17 +960,30 @@ impl Render for ConfigurationView {
]; ];
let env_var_set = self.state.read(cx).credentials_from_env; let env_var_set = self.state.read(cx).credentials_from_env;
let bg_color = cx.theme().colors().editor_background;
let border_color = cx.theme().colors().border_variant;
let input_base_styles = || {
h_flex()
.w_full()
.px_2()
.py_1()
.bg(bg_color)
.border_1()
.border_color(border_color)
.rounded_md()
};
if self.load_credentials_task.is_some() { if self.load_credentials_task.is_some() {
div().child(Label::new("Loading credentials...")).into_any() div().child(Label::new("Loading credentials...")).into_any()
} else if self.should_render_editor(cx) { } else if self.should_render_editor(cx) {
v_flex() v_flex()
.size_full() .size_full()
.on_action(cx.listener(Self::save_credentials)) .on_action(cx.listener(ConfigurationView::save_credentials))
.child(Label::new(INSTRUCTIONS[0])) .child(Label::new(INSTRUCTIONS[0]))
.child(h_flex().child(Label::new(INSTRUCTIONS[1])).child( .child(h_flex().child(Label::new(INSTRUCTIONS[1])).child(
Button::new("iam_console", IAM_CONSOLE_URL) Button::new("iam_console", IAM_CONSOLE_URL)
.style(ButtonStyle::Subtle) .style(ButtonStyle::Subtle)
.icon(IconName::ExternalLink) .icon(IconName::ArrowUpRight)
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.on_click(move |_, _window, cx| cx.open_url(IAM_CONSOLE_URL)) .on_click(move |_, _window, cx| cx.open_url(IAM_CONSOLE_URL))
@ -978,11 +991,12 @@ impl Render for ConfigurationView {
) )
.child(Label::new(INSTRUCTIONS[2])) .child(Label::new(INSTRUCTIONS[2]))
.child( .child(
h_flex() v_flex()
.my_2()
.gap_1() .gap_1()
.child(self.render_aa_id_editor(cx)) .child(input_base_styles().child(self.render_aa_id_editor(cx)))
.child(self.render_sk_editor(cx)) .child(input_base_styles().child(self.render_sk_editor(cx)))
.child(self.render_region_editor(cx)) .child(input_base_styles().child(self.render_region_editor(cx)))
) )
.child( .child(
Label::new( Label::new(

View file

@ -386,17 +386,10 @@ fn render_accept_terms(
let form = v_flex() let form = v_flex()
.w_full() .w_full()
.gap_2() .gap_2()
.when(
view_kind == LanguageModelProviderTosView::ThreadEmptyState,
|form| form.items_center(),
)
.child( .child(
h_flex() h_flex()
.flex_wrap() .flex_wrap()
.when( .items_start()
view_kind == LanguageModelProviderTosView::ThreadEmptyState,
|form| form.justify_center(),
)
.child(Label::new(text)) .child(Label::new(text))
.child(terms_button), .child(terms_button),
) )
@ -416,9 +409,11 @@ fn render_accept_terms(
); );
match view_kind { match view_kind {
LanguageModelProviderTosView::ThreadEmptyState => button_container.justify_center(),
LanguageModelProviderTosView::PromptEditorPopup => button_container.justify_end(), LanguageModelProviderTosView::PromptEditorPopup => button_container.justify_end(),
LanguageModelProviderTosView::Configuration => button_container.justify_start(), LanguageModelProviderTosView::Configuration
| LanguageModelProviderTosView::ThreadEmptyState => {
button_container.justify_start()
}
} }
}); });

View file

@ -38,6 +38,7 @@ pub struct ListItem {
children: SmallVec<[AnyElement; 2]>, children: SmallVec<[AnyElement; 2]>,
selectable: bool, selectable: bool,
outlined: bool, outlined: bool,
rounded: bool,
overflow_x: bool, overflow_x: bool,
focused: Option<bool>, focused: Option<bool>,
} }
@ -63,6 +64,7 @@ impl ListItem {
children: SmallVec::new(), children: SmallVec::new(),
selectable: true, selectable: true,
outlined: false, outlined: false,
rounded: false,
overflow_x: false, overflow_x: false,
focused: None, focused: None,
} }
@ -147,6 +149,11 @@ impl ListItem {
self self
} }
pub fn rounded(mut self) -> Self {
self.rounded = true;
self
}
pub fn overflow_x(mut self) -> Self { pub fn overflow_x(mut self) -> Self {
self.overflow_x = true; self.overflow_x = true;
self self
@ -210,13 +217,13 @@ impl RenderOnce for ListItem {
}) })
}) })
}) })
.when(self.rounded, |this| this.rounded_md())
.child( .child(
h_flex() h_flex()
.id("inner_list_item") .id("inner_list_item")
.group("list_item") .group("list_item")
.w_full() .w_full()
.relative() .relative()
.items_center()
.gap_1() .gap_1()
.px(DynamicSpacing::Base06.rems(cx)) .px(DynamicSpacing::Base06.rems(cx))
.map(|this| match self.spacing { .map(|this| match self.spacing {