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