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(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.gap_1()
.child(Headline::new("Prompt Library").size(HeadlineSize::Small))
.gap_2()
.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(
Button::new("open-prompt-library", "Open Prompt Library")
.style(ButtonStyle::Filled)

View file

@ -14,7 +14,7 @@ use client::zed_urls;
use editor::Editor;
use fs::Fs;
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,
};
use language::LanguageRegistry;
@ -596,7 +596,6 @@ impl AssistantPanel {
h_flex()
.id("assistant-toolbar")
.px(DynamicSpacing::Base08.rems(cx))
.h(Tab::container_height(cx))
.flex_none()
.justify_between()
@ -604,72 +603,86 @@ impl AssistantPanel {
.bg(cx.theme().colors().tab_bar_background)
.border_b_1()
.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(
h_flex()
.w_full()
.gap_1()
.justify_between()
.child(Label::new(title))
.h_full()
.pl_2()
.gap_2()
.bg(cx.theme().colors().tab_bar_background)
.children(if matches!(self.active_view, ActiveView::PromptEditor) {
self.context_editor
.as_ref()
.and_then(|editor| render_remaining_tokens(editor, cx))
} else {
None
}),
)
.child(
h_flex()
.h_full()
.pl_1p5()
.border_l_1()
.border_color(cx.theme().colors().border)
.gap(DynamicSpacing::Base02.rems(cx))
})
.child(
PopoverMenu::new("assistant-toolbar-new-popover-menu")
.trigger_with_tooltip(
IconButton::new("new", IconName::Plus)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle),
Tooltip::text("New…"),
)
.anchor(Corner::TopRight)
.with_handle(self.new_item_context_menu_handle.clone())
.menu(move |window, cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
menu.action("New Thread", NewThread.boxed_clone())
.action("New Prompt Editor", NewPromptEditor.boxed_clone())
}))
}),
)
.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,
h_flex()
.h_full()
.px(DynamicSpacing::Base08.rems(cx))
.border_l_1()
.border_color(cx.theme().colors().border)
.gap(DynamicSpacing::Base02.rems(cx))
.child(
PopoverMenu::new("assistant-toolbar-new-popover-menu")
.trigger_with_tooltip(
IconButton::new("new", IconName::Plus)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle),
Tooltip::text("New…"),
)
}
})
.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);
}),
.anchor(Corner::TopRight)
.with_handle(self.new_item_context_menu_handle.clone())
.menu(move |window, cx| {
Some(ContextMenu::build(
window,
cx,
|menu, _window, _cx| {
menu.action("New Thread", NewThread.boxed_clone())
.action(
"New Prompt Editor",
NewPromptEditor.boxed_clone(),
)
},
))
}),
)
.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 {
let recent_history = self
.history_store
.update(cx, |this, cx| this.recent_entries(3, cx));
.update(cx, |this, cx| this.recent_entries(6, cx));
let create_welcome_heading = || {
h_flex()
.w_full()
.justify_center()
.child(Headline::new("Welcome to the Assistant Panel").size(HeadlineSize::Small))
};
@ -724,36 +736,27 @@ impl AssistantPanel {
let no_error = configuration_error.is_none();
v_flex()
.gap_2()
.child(
v_flex().w_full().child(
svg()
.path("icons/logo_96.svg")
.text_color(cx.theme().colors().text)
.w(px(40.))
.h(px(40.))
.mx_auto()
.mb_4(),
),
)
.p_1p5()
.size_full()
.justify_end()
.gap_1()
.map(|parent| {
match configuration_error {
Some(ConfigurationError::ProviderNotAuthenticated)
| Some(ConfigurationError::NoProvider) => {
parent.child(
v_flex()
.px_1p5()
.gap_0p5()
.child(create_welcome_heading())
.child(
h_flex().mb_2().w_full().justify_center().child(
Label::new(
"To start using the assistant, configure at least one LLM provider.",
)
.color(Color::Muted),
),
Label::new(
"To start using the assistant, configure at least one LLM provider.",
)
.color(Color::Muted),
)
.child(
h_flex().w_full().justify_center().child(
h_flex().mt_1().w_full().child(
Button::new("open-configuration", "Configure a Provider")
.size(ButtonSize::Compact)
.icon(Some(IconName::Sliders))
@ -767,7 +770,7 @@ impl AssistantPanel {
)
}
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(
LanguageModelProviderTosView::ThreadEmptyState,
cx,
@ -778,21 +781,40 @@ impl AssistantPanel {
})
.when(recent_history.is_empty() && no_error, |parent| {
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| {
parent
.child(
h_flex().w_full().justify_center().child(
Label::new("Recent Threads:")
.size(LabelSize::Small)
.color(Color::Muted),
),
h_flex()
.pl_1p5()
.pb_1()
.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| {
// TODO: Add keyboard navigation.
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(
v_flex()
.gap_4()
.gap_5()
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {

View file

@ -254,18 +254,28 @@ impl RenderOnce for PastThread {
);
ListItem::new(SharedString::from(self.thread.id.to_string()))
.outlined()
.rounded()
.toggle_state(self.selected)
.start_slot(
Icon::new(IconName::MessageCircle)
.size(IconSize::Small)
.color(Color::Muted),
)
.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(
h_flex()
.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(
Label::new(thread_timestamp)
.color(Color::Muted)
@ -340,18 +350,28 @@ impl RenderOnce for PastContext {
ListItem::new(SharedString::from(
self.context.path.to_string_lossy().to_string(),
))
.outlined()
.rounded()
.toggle_state(self.selected)
.start_slot(
Icon::new(IconName::Code)
.size(IconSize::Small)
.color(Color::Muted),
)
.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(
h_flex()
.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(
Label::new(context_timestamp)
.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 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() {
div().child(Label::new("Loading credentials...")).into_any()
} else if self.should_render_editor(cx) {
v_flex()
.size_full()
.on_action(cx.listener(Self::save_credentials))
.on_action(cx.listener(ConfigurationView::save_credentials))
.child(Label::new(INSTRUCTIONS[0]))
.child(h_flex().child(Label::new(INSTRUCTIONS[1])).child(
Button::new("iam_console", IAM_CONSOLE_URL)
.style(ButtonStyle::Subtle)
.icon(IconName::ExternalLink)
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.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(
h_flex()
v_flex()
.my_2()
.gap_1()
.child(self.render_aa_id_editor(cx))
.child(self.render_sk_editor(cx))
.child(self.render_region_editor(cx))
.child(input_base_styles().child(self.render_aa_id_editor(cx)))
.child(input_base_styles().child(self.render_sk_editor(cx)))
.child(input_base_styles().child(self.render_region_editor(cx)))
)
.child(
Label::new(

View file

@ -386,17 +386,10 @@ fn render_accept_terms(
let form = v_flex()
.w_full()
.gap_2()
.when(
view_kind == LanguageModelProviderTosView::ThreadEmptyState,
|form| form.items_center(),
)
.child(
h_flex()
.flex_wrap()
.when(
view_kind == LanguageModelProviderTosView::ThreadEmptyState,
|form| form.justify_center(),
)
.items_start()
.child(Label::new(text))
.child(terms_button),
)
@ -416,9 +409,11 @@ fn render_accept_terms(
);
match view_kind {
LanguageModelProviderTosView::ThreadEmptyState => button_container.justify_center(),
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]>,
selectable: bool,
outlined: bool,
rounded: bool,
overflow_x: bool,
focused: Option<bool>,
}
@ -63,6 +64,7 @@ impl ListItem {
children: SmallVec::new(),
selectable: true,
outlined: false,
rounded: false,
overflow_x: false,
focused: None,
}
@ -147,6 +149,11 @@ impl ListItem {
self
}
pub fn rounded(mut self) -> Self {
self.rounded = true;
self
}
pub fn overflow_x(mut self) -> Self {
self.overflow_x = true;
self
@ -210,13 +217,13 @@ impl RenderOnce for ListItem {
})
})
})
.when(self.rounded, |this| this.rounded_md())
.child(
h_flex()
.id("inner_list_item")
.group("list_item")
.w_full()
.relative()
.items_center()
.gap_1()
.px(DynamicSpacing::Base06.rems(cx))
.map(|this| match self.spacing {