chat_message.rs

  1use std::sync::Arc;
  2
  3use client::User;
  4use gpui::{AnyElement, ClickEvent};
  5use ui::{prelude::*, Avatar};
  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    collapsed: bool,
 20    on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
 21}
 22
 23impl ChatMessage {
 24    pub fn new(
 25        id: MessageId,
 26        player: UserOrAssistant,
 27        message: Option<AnyElement>,
 28        collapsed: bool,
 29        on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
 30    ) -> Self {
 31        Self {
 32            id,
 33            player,
 34            message,
 35            collapsed,
 36            on_collapse_handle_click,
 37        }
 38    }
 39}
 40
 41impl RenderOnce for ChatMessage {
 42    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
 43        // TODO: This should be top padding + 1.5x line height
 44        // Set the message height to cut off at exactly 1.5 lines when collapsed
 45        let collapsed_height = rems(2.875);
 46
 47        let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0));
 48        let collapse_handle = h_flex()
 49            .id(collapse_handle_id.clone())
 50            .group(collapse_handle_id.clone())
 51            .flex_none()
 52            .justify_center()
 53            .w_1()
 54            .mx_2()
 55            .h_full()
 56            .on_click(self.on_collapse_handle_click)
 57            .child(
 58                div()
 59                    .w_px()
 60                    .h_full()
 61                    .rounded_lg()
 62                    .overflow_hidden()
 63                    .bg(cx.theme().colors().element_background)
 64                    .group_hover(collapse_handle_id, |this| {
 65                        this.bg(cx.theme().colors().element_hover)
 66                    }),
 67            );
 68        let content = self.message.map(|message| {
 69            div()
 70                .overflow_hidden()
 71                .w_full()
 72                .p_4()
 73                .rounded_lg()
 74                .when(self.collapsed, |this| this.h(collapsed_height))
 75                .bg(cx.theme().colors().surface_background)
 76                .child(message)
 77        });
 78
 79        v_flex()
 80            .gap_1()
 81            .child(ChatMessageHeader::new(self.player))
 82            .child(h_flex().gap_3().child(collapse_handle).children(content))
 83    }
 84}
 85
 86#[derive(IntoElement)]
 87struct ChatMessageHeader {
 88    player: UserOrAssistant,
 89    contexts: Vec<()>,
 90}
 91
 92impl ChatMessageHeader {
 93    fn new(player: UserOrAssistant) -> Self {
 94        Self {
 95            player,
 96            contexts: Vec::new(),
 97        }
 98    }
 99}
100
101impl RenderOnce for ChatMessageHeader {
102    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
103        let (username, avatar_uri) = match self.player {
104            UserOrAssistant::Assistant => (
105                "Assistant".into(),
106                Some("https://zed.dev/assistant_avatar.png".into()),
107            ),
108            UserOrAssistant::User(Some(user)) => {
109                (user.github_login.clone(), Some(user.avatar_uri.clone()))
110            }
111            UserOrAssistant::User(None) => ("You".into(), None),
112        };
113
114        h_flex()
115            .justify_between()
116            .child(
117                h_flex()
118                    .gap_3()
119                    .map(|this| {
120                        let avatar_size = rems(20.0 / 16.0);
121                        if let Some(avatar_uri) = avatar_uri {
122                            this.child(Avatar::new(avatar_uri).size(avatar_size))
123                        } else {
124                            this.child(div().size(avatar_size))
125                        }
126                    })
127                    .child(Label::new(username).color(Color::Default)),
128            )
129            .child(div().when(!self.contexts.is_empty(), |this| {
130                this.child(Label::new(self.contexts.len().to_string()).color(Color::Muted))
131            }))
132    }
133}