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