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}