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}