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        let tools_used = self
 73            .tools_used
 74            .map(|attachment| div().mt_3().child(attachment));
 75
 76        let content = self.message.map(|message| {
 77            div()
 78                .overflow_hidden()
 79                .w_full()
 80                .p(content_padding)
 81                .rounded_lg()
 82                .when(self.collapsed, |this| this.h(collapsed_height))
 83                .bg(cx.theme().colors().surface_background)
 84                .child(message)
 85                .children(tools_used)
 86        });
 87
 88        v_flex()
 89            .gap_1()
 90            .child(ChatMessageHeader::new(self.player))
 91            .child(h_flex().gap_3().child(collapse_handle).children(content))
 92    }
 93}
 94
 95#[derive(IntoElement)]
 96struct ChatMessageHeader {
 97    player: UserOrAssistant,
 98    contexts: Vec<()>,
 99}
100
101impl ChatMessageHeader {
102    fn new(player: UserOrAssistant) -> Self {
103        Self {
104            player,
105            contexts: Vec::new(),
106        }
107    }
108}
109
110impl RenderOnce for ChatMessageHeader {
111    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
112        let (username, avatar_uri) = match self.player {
113            UserOrAssistant::Assistant => (
114                "Assistant".into(),
115                Some("https://zed.dev/assistant_avatar.png".into()),
116            ),
117            UserOrAssistant::User(Some(user)) => {
118                (user.github_login.clone(), Some(user.avatar_uri.clone()))
119            }
120            UserOrAssistant::User(None) => ("You".into(), None),
121        };
122
123        h_flex()
124            .justify_between()
125            .child(
126                h_flex()
127                    .gap_3()
128                    .map(|this| {
129                        let avatar_size = rems_from_px(20.);
130                        if let Some(avatar_uri) = avatar_uri {
131                            this.child(Avatar::new(avatar_uri).size(avatar_size))
132                        } else {
133                            this.child(div().size(avatar_size))
134                        }
135                    })
136                    .child(Label::new(username).color(Color::Default)),
137            )
138            .child(div().when(!self.contexts.is_empty(), |this| {
139                this.child(Label::new(self.contexts.len().to_string()).color(Color::Muted))
140            }))
141    }
142}