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 <rgbkrk@gmail.com>
This commit is contained in:
Nate Butler 2024-05-06 18:44:34 -04:00 committed by GitHub
parent 96a3021b12
commit f2a415135b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 211 additions and 181 deletions

View file

@ -0,0 +1 @@
> Give me a comprehensive list of all the elements define in my project (impl Element for {}, impl<T: 'static> Element for {}, impl IntoElement for {})

View file

@ -0,0 +1 @@
> What are all the places we define a new gpui element in my project? (impl Element for {})

View file

@ -0,0 +1 @@
> Can you tell me what the assistant2 crate is for in my project? Tell me in 100 words or less.

View file

@ -182,8 +182,7 @@ impl Render for AssistantPanel {
div() div()
.size_full() .size_full()
.v_flex() .v_flex()
.p_2() .bg(cx.theme().colors().panel_background)
.bg(cx.theme().colors().background)
.child(self.chat.clone()) .child(self.chat.clone())
} }
} }
@ -634,8 +633,13 @@ impl AssistantChat {
} }
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement { fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
let is_first = ix == 0;
let is_last = ix == self.messages.len() - 1; 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] { match &self.messages[ix] {
ChatMessage::User(UserMessage { ChatMessage::User(UserMessage {
id, id,
@ -643,7 +647,7 @@ impl AssistantChat {
attachments, attachments,
}) => div() }) => div()
.id(SharedString::from(format!("message-{}-container", id.0))) .id(SharedString::from(format!("message-{}-container", id.0)))
.when(!is_last, |element| element.mb_2()) .when(is_first, |this| this.pt(padding))
.map(|element| { .map(|element| {
if self.editing_message_id() == Some(*id) { if self.editing_message_id() == Some(*id) {
element.child(Composer::new( element.child(Composer::new(
@ -672,35 +676,39 @@ impl AssistantChat {
} }
} }
})) }))
.child(crate::ui::ChatMessage::new( .child(
*id, crate::ui::ChatMessage::new(
UserOrAssistant::User(self.user_store.read(cx).current_user()), *id,
Some( UserOrAssistant::User(self.user_store.read(cx).current_user()),
RichText::new( Some(
body.read(cx).text(cx), RichText::new(
&[], body.read(cx).text(cx),
&self.language_registry, &[],
) &self.language_registry,
.element(ElementId::from(id.0), cx),
),
Some(
h_flex()
.gap_2()
.children(
attachments
.iter()
.map(|attachment| attachment.view.clone()),
) )
.into_any_element(), .element(ElementId::from(id.0), cx),
), ),
self.is_message_collapsed(id), Some(
Box::new(cx.listener({ h_flex()
let id = *id; .gap_2()
move |assistant_chat, _event, _cx| { .children(
assistant_chat.toggle_message_collapsed(id) 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(), .into_any(),
@ -716,7 +724,6 @@ impl AssistantChat {
} else { } else {
Some( Some(
div() div()
.p_2()
.child(body.element(ElementId::from(id.0), cx)) .child(body.element(ElementId::from(id.0), cx))
.into_any_element(), .into_any_element(),
) )
@ -734,20 +741,24 @@ impl AssistantChat {
}; };
div() div()
.when(!is_last, |element| element.mb_2()) .when(is_first, |this| this.pt(padding))
.child(crate::ui::ChatMessage::new( .child(
*id, crate::ui::ChatMessage::new(
UserOrAssistant::Assistant, *id,
assistant_body, UserOrAssistant::Assistant,
tools_body, assistant_body,
self.is_message_collapsed(id), tools_body,
Box::new(cx.listener({ self.is_message_collapsed(id),
let id = *id; Box::new(cx.listener({
move |assistant_chat, _event, _cx| { let id = *id;
assistant_chat.toggle_message_collapsed(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)) .child(self.render_error(error.clone(), ix, cx))
.into_any() .into_any()
} }

View file

@ -1,8 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use client::User; use client::User;
use gpui::{AnyElement, ClickEvent}; use gpui::{hsla, AnyElement, ClickEvent};
use ui::{prelude::*, Avatar}; use ui::{prelude::*, Avatar, Tooltip};
use crate::MessageId; use crate::MessageId;
@ -17,6 +17,7 @@ pub struct ChatMessage {
player: UserOrAssistant, player: UserOrAssistant,
message: Option<AnyElement>, message: Option<AnyElement>,
tools_used: Option<AnyElement>, tools_used: Option<AnyElement>,
selected: bool,
collapsed: bool, collapsed: bool,
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>, on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
} }
@ -35,79 +36,36 @@ impl ChatMessage {
player, player,
message, message,
tools_used, tools_used,
selected: false,
collapsed, collapsed,
on_collapse_handle_click, on_collapse_handle_click,
} }
} }
} }
impl Selectable for ChatMessage {
fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
impl RenderOnce for ChatMessage { impl RenderOnce for ChatMessage {
fn render(self, cx: &mut WindowContext) -> impl IntoElement { fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0)); let message_group = SharedString::from(format!("{}_group", 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 collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0));
let content_padding = Spacing::Small.rems(cx);
// Clamp the message height to exactly 1.5 lines when collapsed. // 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; let collapsed_height = content_padding.to_pixels(cx.rem_size()) + cx.line_height() * 1.5;
v_flex() let background_color = if let UserOrAssistant::User(_) = &self.player {
.gap_1() Some(cx.theme().colors().surface_background)
.child(ChatMessageHeader::new(self.player)) } else {
.when(self.message.is_some() || self.tools_used.is_some(), |el| { None
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(),
}
}
}
impl RenderOnce for ChatMessageHeader {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let (username, avatar_uri) = match self.player { let (username, avatar_uri) = match self.player {
UserOrAssistant::Assistant => ( UserOrAssistant::Assistant => (
"Assistant".into(), "Assistant".into(),
@ -119,23 +77,77 @@ impl RenderOnce for ChatMessageHeader {
UserOrAssistant::User(None) => ("You".into(), None), UserOrAssistant::User(None) => ("You".into(), None),
}; };
h_flex() v_flex()
.justify_between() .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( .child(
h_flex() h_flex()
.gap_3() .justify_between()
.map(|this| { .px(content_padding)
let avatar_size = rems_from_px(20.); .child(
if let Some(avatar_uri) = avatar_uri { h_flex()
this.child(Avatar::new(avatar_uri).size(avatar_size)) .gap_2()
} else { .map(|this| {
this.child(div().size(avatar_size)) let avatar_size = rems_from_px(20.);
} if let Some(avatar_uri) = avatar_uri {
}) this.child(Avatar::new(avatar_uri).size(avatar_size))
.child(Label::new(username).color(Color::Default)), } 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| { .when(self.message.is_some() || self.tools_used.is_some(), |el| {
this.child(Label::new(self.contexts.len().to_string()).color(Color::Muted)) 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)),
),
)
})
} }
} }

View file

@ -6,7 +6,7 @@ use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AnyElement, FontStyle, FontWeight, TextStyle, View, WeakView, WhiteSpace}; use gpui::{AnyElement, FontStyle, FontWeight, TextStyle, View, WeakView, WhiteSpace};
use settings::Settings; use settings::Settings;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, Divider, Tooltip}; use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, Divider, TextSize, Tooltip};
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Composer { pub struct Composer {
@ -50,67 +50,71 @@ impl Composer {
impl RenderOnce for Composer { impl RenderOnce for Composer {
fn render(mut self, cx: &mut WindowContext) -> impl IntoElement { 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; let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
h_flex().w_full().items_start().mt_2().child( h_flex()
v_flex().size_full().gap_1().child( .p(Spacing::Small.rems(cx))
v_flex() .w_full()
.w_full() .items_start()
.p_3() .child(
.bg(cx.theme().colors().editor_background) v_flex().size_full().gap_1().child(
.rounded_lg() v_flex()
.child( .w_full()
v_flex() .p_3()
.justify_between() .bg(cx.theme().colors().editor_background)
.w_full() .rounded_lg()
.gap_2() .child(
.child({ v_flex()
let settings = ThemeSettings::get_global(cx); .justify_between()
let text_style = TextStyle { .w_full()
color: cx.theme().colors().editor_foreground, .gap_2()
font_family: settings.buffer_font.family.clone(), .child({
font_features: settings.buffer_font.features.clone(), let settings = ThemeSettings::get_global(cx);
font_size: font_size.into(), let text_style = TextStyle {
font_weight: FontWeight::NORMAL, color: cx.theme().colors().editor_foreground,
font_style: FontStyle::Normal, font_family: settings.buffer_font.family.clone(),
line_height: line_height.into(), font_features: settings.buffer_font.features.clone(),
background_color: None, font_size: font_size.into(),
underline: None, font_weight: FontWeight::NORMAL,
strikethrough: None, font_style: FontStyle::Normal,
white_space: WhiteSpace::Normal, line_height: line_height.into(),
}; background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};
EditorElement::new( EditorElement::new(
&self.editor, &self.editor,
EditorStyle { EditorStyle {
background: cx.theme().colors().editor_background, background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(), local_player: cx.theme().players().local(),
text: text_style, text: text_style,
..Default::default() ..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)),
),
) )
.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)),
),
),
),
)
} }
} }