chat_message.rs

  1use std::sync::Arc;
  2
  3use client::User;
  4use gpui::{hsla, AnyElement, ClickEvent};
  5use ui::{prelude::*, Avatar, Tooltip};
  6
  7use crate::MessageId;
  8
  9pub enum UserOrAssistant {
 10    User(Option<Arc<User>>),
 11    Assistant,
 12}
 13
 14#[derive(IntoElement)]
 15pub struct ChatMessage {
 16    id: MessageId,
 17    player: UserOrAssistant,
 18    message: Option<AnyElement>,
 19    tools_used: Option<AnyElement>,
 20    selected: bool,
 21    collapsed: bool,
 22    on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
 23}
 24
 25impl ChatMessage {
 26    pub fn new(
 27        id: MessageId,
 28        player: UserOrAssistant,
 29        message: Option<AnyElement>,
 30        tools_used: Option<AnyElement>,
 31        collapsed: bool,
 32        on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
 33    ) -> Self {
 34        Self {
 35            id,
 36            player,
 37            message,
 38            tools_used,
 39            selected: false,
 40            collapsed,
 41            on_collapse_handle_click,
 42        }
 43    }
 44}
 45
 46impl Selectable for ChatMessage {
 47    fn selected(mut self, selected: bool) -> Self {
 48        self.selected = selected;
 49        self
 50    }
 51}
 52
 53impl RenderOnce for ChatMessage {
 54    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
 55        let message_group = SharedString::from(format!("{}_group", self.id.0));
 56
 57        let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0));
 58
 59        let content_padding = Spacing::Small.rems(cx);
 60        // Clamp the message height to exactly 1.5 lines when collapsed.
 61        let collapsed_height = content_padding.to_pixels(cx.rem_size()) + cx.line_height() * 1.5;
 62
 63        let background_color = if let UserOrAssistant::User(_) = &self.player {
 64            Some(cx.theme().colors().surface_background)
 65        } else {
 66            None
 67        };
 68
 69        let (username, avatar_uri) = match self.player {
 70            UserOrAssistant::Assistant => (
 71                "Assistant".into(),
 72                Some("https://zed.dev/assistant_avatar.png".into()),
 73            ),
 74            UserOrAssistant::User(Some(user)) => {
 75                (user.github_login.clone(), Some(user.avatar_uri.clone()))
 76            }
 77            UserOrAssistant::User(None) => ("You".into(), None),
 78        };
 79
 80        v_flex()
 81            .group(message_group.clone())
 82            .gap(Spacing::XSmall.rems(cx))
 83            .p(Spacing::XSmall.rems(cx))
 84            .when(self.selected, |element| {
 85                element.bg(hsla(0.6, 0.67, 0.46, 0.12))
 86            })
 87            .rounded_lg()
 88            .child(
 89                h_flex()
 90                    .justify_between()
 91                    .px(content_padding)
 92                    .child(
 93                        h_flex()
 94                            .gap_2()
 95                            .map(|this| {
 96                                let avatar_size = rems_from_px(20.);
 97                                if let Some(avatar_uri) = avatar_uri {
 98                                    this.child(Avatar::new(avatar_uri).size(avatar_size))
 99                                } else {
100                                    this.child(div().size(avatar_size))
101                                }
102                            })
103                            .child(Label::new(username).color(Color::Muted)),
104                    )
105                    .child(
106                        h_flex().visible_on_hover(message_group).child(
107                            // temp icons
108                            IconButton::new(
109                                collapse_handle_id.clone(),
110                                if self.collapsed {
111                                    IconName::ArrowUp
112                                } else {
113                                    IconName::ArrowDown
114                                },
115                            )
116                            .icon_size(IconSize::XSmall)
117                            .icon_color(Color::Muted)
118                            .on_click(self.on_collapse_handle_click)
119                            .tooltip(|cx| Tooltip::text("Collapse Message", cx)),
120                        ), // .child(
121                           //     IconButton::new("copy-message", IconName::Copy)
122                           //         .icon_color(Color::Muted)
123                           //         .icon_size(IconSize::XSmall),
124                           // )
125                           // .child(
126                           //     IconButton::new("menu", IconName::Ellipsis)
127                           //         .icon_color(Color::Muted)
128                           //         .icon_size(IconSize::XSmall),
129                           // ),
130                    ),
131            )
132            .when(self.message.is_some() || self.tools_used.is_some(), |el| {
133                el.child(
134                    h_flex().child(
135                        v_flex()
136                            .relative()
137                            .overflow_hidden()
138                            .w_full()
139                            .p(content_padding)
140                            .gap_3()
141                            .text_ui(cx)
142                            .rounded_lg()
143                            .when_some(background_color, |this, background_color| {
144                                this.bg(background_color)
145                            })
146                            .when(self.collapsed, |this| this.h(collapsed_height))
147                            .children(self.message)
148                            .when_some(self.tools_used, |this, tools_used| this.child(tools_used)),
149                    ),
150                )
151            })
152    }
153}