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:
parent
96a3021b12
commit
f2a415135b
6 changed files with 211 additions and 181 deletions
1
crates/assistant2/evals/list-of-into-element.md
Normal file
1
crates/assistant2/evals/list-of-into-element.md
Normal 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 {})
|
1
crates/assistant2/evals/new-gpui-element.md
Normal file
1
crates/assistant2/evals/new-gpui-element.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
> What are all the places we define a new gpui element in my project? (impl Element for {})
|
1
crates/assistant2/evals/what-is-the-assistant2-crate.md
Normal file
1
crates/assistant2/evals/what-is-the-assistant2-crate.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
> Can you tell me what the assistant2 crate is for in my project? Tell me in 100 words or less.
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue