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