From f2a415135b9d4af9f4c854c74edc5348f926b9fc Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 6 May 2024 18:44:34 -0400 Subject: [PATCH] Continue Assistant 2 Messages Layout (#11465) Release Notes: - N/A --------- Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> Co-authored-by: Kyle Kelley --- .../assistant2/evals/list-of-into-element.md | 1 + crates/assistant2/evals/new-gpui-element.md | 1 + .../evals/what-is-the-assistant2-crate.md | 1 + crates/assistant2/src/assistant2.rs | 103 ++++++----- crates/assistant2/src/ui/chat_message.rs | 164 ++++++++++-------- crates/assistant2/src/ui/composer.rs | 120 ++++++------- 6 files changed, 210 insertions(+), 180 deletions(-) create mode 100644 crates/assistant2/evals/list-of-into-element.md create mode 100644 crates/assistant2/evals/new-gpui-element.md create mode 100644 crates/assistant2/evals/what-is-the-assistant2-crate.md diff --git a/crates/assistant2/evals/list-of-into-element.md b/crates/assistant2/evals/list-of-into-element.md new file mode 100644 index 0000000000000000000000000000000000000000..562fac844b9cf89d045604c5eba4451657d6cea2 --- /dev/null +++ b/crates/assistant2/evals/list-of-into-element.md @@ -0,0 +1 @@ +> Give me a comprehensive list of all the elements define in my project (impl Element for {}, impl Element for {}, impl IntoElement for {}) diff --git a/crates/assistant2/evals/new-gpui-element.md b/crates/assistant2/evals/new-gpui-element.md new file mode 100644 index 0000000000000000000000000000000000000000..51452cb36e689824708cad3a0bef4e1b3954d62a --- /dev/null +++ b/crates/assistant2/evals/new-gpui-element.md @@ -0,0 +1 @@ +> What are all the places we define a new gpui element in my project? (impl Element for {}) diff --git a/crates/assistant2/evals/what-is-the-assistant2-crate.md b/crates/assistant2/evals/what-is-the-assistant2-crate.md new file mode 100644 index 0000000000000000000000000000000000000000..5d39684c5a6c078c45c8f777d6d975437f064c1c --- /dev/null +++ b/crates/assistant2/evals/what-is-the-assistant2-crate.md @@ -0,0 +1 @@ +> Can you tell me what the assistant2 crate is for in my project? Tell me in 100 words or less. diff --git a/crates/assistant2/src/assistant2.rs b/crates/assistant2/src/assistant2.rs index 1bae10f737dc685cf29aaf3be2983eef279f5447..99bf9a68841cabfdc5ec7c6cac82f004fc7dab21 100644 --- a/crates/assistant2/src/assistant2.rs +++ b/crates/assistant2/src/assistant2.rs @@ -182,8 +182,7 @@ impl Render for AssistantPanel { div() .size_full() .v_flex() - .p_2() - .bg(cx.theme().colors().background) + .bg(cx.theme().colors().panel_background) .child(self.chat.clone()) } } @@ -634,8 +633,13 @@ impl AssistantChat { } fn render_message(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { + let is_first = ix == 0; let is_last = ix == self.messages.len() - 1; + let padding = Spacing::Large.rems(cx); + + // Whenever there's a run of assistant messages, group as one Assistant UI element + match &self.messages[ix] { ChatMessage::User(UserMessage { id, @@ -643,7 +647,7 @@ impl AssistantChat { attachments, }) => div() .id(SharedString::from(format!("message-{}-container", id.0))) - .when(!is_last, |element| element.mb_2()) + .when(is_first, |this| this.pt(padding)) .map(|element| { if self.editing_message_id() == Some(*id) { element.child(Composer::new( @@ -672,35 +676,39 @@ impl AssistantChat { } } })) - .child(crate::ui::ChatMessage::new( - *id, - UserOrAssistant::User(self.user_store.read(cx).current_user()), - Some( - RichText::new( - body.read(cx).text(cx), - &[], - &self.language_registry, - ) - .element(ElementId::from(id.0), cx), - ), - Some( - h_flex() - .gap_2() - .children( - attachments - .iter() - .map(|attachment| attachment.view.clone()), + .child( + crate::ui::ChatMessage::new( + *id, + UserOrAssistant::User(self.user_store.read(cx).current_user()), + Some( + RichText::new( + body.read(cx).text(cx), + &[], + &self.language_registry, ) - .into_any_element(), - ), - self.is_message_collapsed(id), - Box::new(cx.listener({ - let id = *id; - move |assistant_chat, _event, _cx| { - assistant_chat.toggle_message_collapsed(id) - } - })), - )) + .element(ElementId::from(id.0), cx), + ), + Some( + h_flex() + .gap_2() + .children( + attachments + .iter() + .map(|attachment| attachment.view.clone()), + ) + .into_any_element(), + ), + self.is_message_collapsed(id), + Box::new(cx.listener({ + let id = *id; + move |assistant_chat, _event, _cx| { + assistant_chat.toggle_message_collapsed(id) + } + })), + ) + // TODO: Wire up selections. + .selected(is_last), + ) } }) .into_any(), @@ -716,7 +724,6 @@ impl AssistantChat { } else { Some( div() - .p_2() .child(body.element(ElementId::from(id.0), cx)) .into_any_element(), ) @@ -734,20 +741,24 @@ impl AssistantChat { }; div() - .when(!is_last, |element| element.mb_2()) - .child(crate::ui::ChatMessage::new( - *id, - UserOrAssistant::Assistant, - assistant_body, - tools_body, - self.is_message_collapsed(id), - Box::new(cx.listener({ - let id = *id; - move |assistant_chat, _event, _cx| { - assistant_chat.toggle_message_collapsed(id) - } - })), - )) + .when(is_first, |this| this.pt(padding)) + .child( + crate::ui::ChatMessage::new( + *id, + UserOrAssistant::Assistant, + assistant_body, + tools_body, + self.is_message_collapsed(id), + Box::new(cx.listener({ + let id = *id; + move |assistant_chat, _event, _cx| { + assistant_chat.toggle_message_collapsed(id) + } + })), + ) + // TODO: Wire up selections. + .selected(is_last), + ) .child(self.render_error(error.clone(), ix, cx)) .into_any() } diff --git a/crates/assistant2/src/ui/chat_message.rs b/crates/assistant2/src/ui/chat_message.rs index adfdb272753329929bc53e44334b4bb289bc7d69..7d8043ca61ed5a609e825beaad324877a0a1effa 100644 --- a/crates/assistant2/src/ui/chat_message.rs +++ b/crates/assistant2/src/ui/chat_message.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use client::User; -use gpui::{AnyElement, ClickEvent}; -use ui::{prelude::*, Avatar}; +use gpui::{hsla, AnyElement, ClickEvent}; +use ui::{prelude::*, Avatar, Tooltip}; use crate::MessageId; @@ -17,6 +17,7 @@ pub struct ChatMessage { player: UserOrAssistant, message: Option, tools_used: Option, + selected: bool, collapsed: bool, on_collapse_handle_click: Box, } @@ -35,79 +36,36 @@ impl ChatMessage { player, message, tools_used, + selected: false, collapsed, on_collapse_handle_click, } } } +impl Selectable for ChatMessage { + fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } +} + impl RenderOnce for ChatMessage { fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let message_group = SharedString::from(format!("{}_group", self.id.0)); + let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0)); - let collapse_handle = h_flex() - .id(collapse_handle_id.clone()) - .group(collapse_handle_id.clone()) - .flex_none() - .justify_center() - .w_1() - .mx_2() - .h_full() - .on_click(self.on_collapse_handle_click) - .child( - div() - .w_px() - .h_full() - .rounded_lg() - .overflow_hidden() - .bg(cx.theme().colors().element_background) - .group_hover(collapse_handle_id, |this| { - this.bg(cx.theme().colors().element_hover) - }), - ); - let content_padding = rems(1.); + let content_padding = Spacing::Small.rems(cx); // Clamp the message height to exactly 1.5 lines when collapsed. let collapsed_height = content_padding.to_pixels(cx.rem_size()) + cx.line_height() * 1.5; - v_flex() - .gap_1() - .child(ChatMessageHeader::new(self.player)) - .when(self.message.is_some() || self.tools_used.is_some(), |el| { - el.child( - h_flex().gap_3().child(collapse_handle).child( - div() - .overflow_hidden() - .w_full() - .p(content_padding) - .gap_3() - .rounded_lg() - .when(self.collapsed, |this| this.h(collapsed_height)) - .bg(cx.theme().colors().surface_background) - .children(self.message) - .children(self.tools_used), - ), - ) - }) - } -} - -#[derive(IntoElement)] -struct ChatMessageHeader { - player: UserOrAssistant, - contexts: Vec<()>, -} - -impl ChatMessageHeader { - fn new(player: UserOrAssistant) -> Self { - Self { - player, - contexts: Vec::new(), - } - } -} + let background_color = if let UserOrAssistant::User(_) = &self.player { + Some(cx.theme().colors().surface_background) + } else { + None + }; -impl RenderOnce for ChatMessageHeader { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { let (username, avatar_uri) = match self.player { UserOrAssistant::Assistant => ( "Assistant".into(), @@ -119,23 +77,77 @@ impl RenderOnce for ChatMessageHeader { UserOrAssistant::User(None) => ("You".into(), None), }; - h_flex() - .justify_between() + v_flex() + .group(message_group.clone()) + .gap(Spacing::XSmall.rems(cx)) + .p(Spacing::XSmall.rems(cx)) + .when(self.selected, |element| { + element.bg(hsla(0.6, 0.67, 0.46, 0.12)) + }) + .rounded_lg() .child( h_flex() - .gap_3() - .map(|this| { - let avatar_size = rems_from_px(20.); - if let Some(avatar_uri) = avatar_uri { - this.child(Avatar::new(avatar_uri).size(avatar_size)) - } else { - this.child(div().size(avatar_size)) - } - }) - .child(Label::new(username).color(Color::Default)), + .justify_between() + .px(content_padding) + .child( + h_flex() + .gap_2() + .map(|this| { + let avatar_size = rems_from_px(20.); + if let Some(avatar_uri) = avatar_uri { + this.child(Avatar::new(avatar_uri).size(avatar_size)) + } else { + this.child(div().size(avatar_size)) + } + }) + .child(Label::new(username).color(Color::Muted)), + ) + .child( + h_flex().visible_on_hover(message_group).child( + // temp icons + IconButton::new( + collapse_handle_id.clone(), + if self.collapsed { + IconName::ArrowUp + } else { + IconName::ArrowDown + }, + ) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .on_click(self.on_collapse_handle_click) + .tooltip(|cx| Tooltip::text("Collapse Message", cx)), + ), // .child( + // IconButton::new("copy-message", IconName::Copy) + // .icon_color(Color::Muted) + // .icon_size(IconSize::XSmall), + // ) + // .child( + // IconButton::new("menu", IconName::Ellipsis) + // .icon_color(Color::Muted) + // .icon_size(IconSize::XSmall), + // ), + ), ) - .child(div().when(!self.contexts.is_empty(), |this| { - this.child(Label::new(self.contexts.len().to_string()).color(Color::Muted)) - })) + .when(self.message.is_some() || self.tools_used.is_some(), |el| { + el.child( + h_flex().child( + v_flex() + .relative() + .overflow_hidden() + .w_full() + .p(content_padding) + .gap_3() + .text_ui(cx) + .rounded_lg() + .when_some(background_color, |this, background_color| { + this.bg(background_color) + }) + .when(self.collapsed, |this| this.h(collapsed_height)) + .children(self.message) + .when_some(self.tools_used, |this, tools_used| this.child(tools_used)), + ), + ) + }) } } diff --git a/crates/assistant2/src/ui/composer.rs b/crates/assistant2/src/ui/composer.rs index b08f0f6ec137c1ef2cc196289a32a4d016319754..2b866e3ad55e72d0548b6e9692352a3e573532e6 100644 --- a/crates/assistant2/src/ui/composer.rs +++ b/crates/assistant2/src/ui/composer.rs @@ -6,7 +6,7 @@ use editor::{Editor, EditorElement, EditorStyle}; use gpui::{AnyElement, FontStyle, FontWeight, TextStyle, View, WeakView, WhiteSpace}; use settings::Settings; use theme::ThemeSettings; -use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, Divider, Tooltip}; +use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, Divider, TextSize, Tooltip}; #[derive(IntoElement)] pub struct Composer { @@ -50,67 +50,71 @@ impl Composer { impl RenderOnce for Composer { fn render(mut self, cx: &mut WindowContext) -> impl IntoElement { - let font_size = rems(0.875); + let font_size = TextSize::Default.rems(cx); let line_height = font_size.to_pixels(cx.rem_size()) * 1.3; - h_flex().w_full().items_start().mt_2().child( - v_flex().size_full().gap_1().child( - v_flex() - .w_full() - .p_3() - .bg(cx.theme().colors().editor_background) - .rounded_lg() - .child( - v_flex() - .justify_between() - .w_full() - .gap_2() - .child({ - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: cx.theme().colors().editor_foreground, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_size: font_size.into(), - font_weight: FontWeight::NORMAL, - font_style: FontStyle::Normal, - line_height: line_height.into(), - background_color: None, - underline: None, - strikethrough: None, - white_space: WhiteSpace::Normal, - }; + h_flex() + .p(Spacing::Small.rems(cx)) + .w_full() + .items_start() + .child( + v_flex().size_full().gap_1().child( + v_flex() + .w_full() + .p_3() + .bg(cx.theme().colors().editor_background) + .rounded_lg() + .child( + v_flex() + .justify_between() + .w_full() + .gap_2() + .child({ + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: cx.theme().colors().editor_foreground, + font_family: settings.buffer_font.family.clone(), + font_features: settings.buffer_font.features.clone(), + font_size: font_size.into(), + font_weight: FontWeight::NORMAL, + font_style: FontStyle::Normal, + line_height: line_height.into(), + background_color: None, + underline: None, + strikethrough: None, + white_space: WhiteSpace::Normal, + }; - EditorElement::new( - &self.editor, - EditorStyle { - background: cx.theme().colors().editor_background, - local_player: cx.theme().players().local(), - text: text_style, - ..Default::default() - }, - ) - }) - .child( - h_flex() - .flex_none() - .gap_2() - .justify_between() - .w_full() - .child( - h_flex().gap_1().child( - h_flex() - .gap_2() - .child(self.render_tools(cx)) - .child(Divider::vertical()) - .child(self.render_attachment_tools(cx)), - ), + EditorElement::new( + &self.editor, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, ) - .child(h_flex().gap_1().child(self.model_selector)), - ), - ), - ), - ) + }) + .child( + h_flex() + .flex_none() + .gap_2() + .justify_between() + .w_full() + .child( + h_flex().gap_1().child( + h_flex() + .gap_2() + .child(self.render_tools(cx)) + .child(Divider::vertical()) + .child(self.render_attachment_tools(cx)), + ), + ) + .child(h_flex().gap_1().child(self.model_selector)), + ), + ), + ), + ) } }