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