Add `ChatPanel` component

Marshall Bowers created

Change summary

Cargo.lock                                             |   1 
crates/storybook2/Cargo.toml                           |   1 
crates/storybook2/src/stories/components.rs            |   1 
crates/storybook2/src/stories/components/chat_panel.rs |  56 ++++++
crates/storybook2/src/story_selector.rs                |   2 
crates/ui2/src/components.rs                           |   2 
crates/ui2/src/components/chat_panel.rs                | 108 ++++++++++++
crates/ui2/src/components/workspace.rs                 |  67 +++----
8 files changed, 199 insertions(+), 39 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7818,6 +7818,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "backtrace-on-stack-overflow",
+ "chrono",
  "clap 4.4.4",
  "gpui3",
  "itertools 0.11.0",

crates/storybook2/Cargo.toml 🔗

@@ -13,6 +13,7 @@ anyhow.workspace = true
 # TODO: Remove after diagnosing stack overflow.
 backtrace-on-stack-overflow = "0.3.0"
 clap = { version = "4.4", features = ["derive", "string"] }
+chrono = "0.4"
 gpui3 = { path = "../gpui3" }
 itertools = "0.11.0"
 log.workspace = true

crates/storybook2/src/stories/components/chat_panel.rs 🔗

@@ -0,0 +1,56 @@
+use std::marker::PhantomData;
+
+use chrono::DateTime;
+use ui::prelude::*;
+use ui::{ChatMessage, ChatPanel, Panel};
+
+use crate::story::Story;
+
+#[derive(Element)]
+pub struct ChatPanelStory<S: 'static + Send + Sync + Clone> {
+    state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync + Clone> ChatPanelStory<S> {
+    pub fn new() -> Self {
+        Self {
+            state_type: PhantomData,
+        }
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        Story::container(cx)
+            .child(Story::title_for::<_, ChatPanel<S>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Panel::new(
+                ScrollState::default(),
+                |_, _| vec![ChatPanel::new(ScrollState::default()).into_any()],
+                Box::new(()),
+            ))
+            .child(Story::label(cx, "With Mesages"))
+            .child(Panel::new(
+                ScrollState::default(),
+                |_, _| {
+                    vec![ChatPanel::new(ScrollState::default())
+                        .with_messages(vec![
+                            ChatMessage::new(
+                                "osiewicz".to_string(),
+                                "is this thing on?".to_string(),
+                                DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
+                                    .unwrap()
+                                    .naive_local(),
+                            ),
+                            ChatMessage::new(
+                                "maxdeviant".to_string(),
+                                "Reading you loud and clear!".to_string(),
+                                DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
+                                    .unwrap()
+                                    .naive_local(),
+                            ),
+                        ])
+                        .into_any()]
+                },
+                Box::new(()),
+            ))
+    }
+}

crates/storybook2/src/story_selector.rs 🔗

@@ -39,6 +39,7 @@ pub enum ComponentStory {
     AssistantPanel,
     Breadcrumb,
     Buffer,
+    ChatPanel,
     Panel,
     ProjectPanel,
     Tab,
@@ -58,6 +59,7 @@ impl ComponentStory {
             }
             Self::Buffer => components::buffer::BufferStory::new().into_any(),
             Self::Breadcrumb => components::breadcrumb::BreadcrumbStory::new().into_any(),
+            Self::ChatPanel => components::chat_panel::ChatPanelStory::new().into_any(),
             Self::Panel => components::panel::PanelStory::new().into_any(),
             Self::ProjectPanel => components::project_panel::ProjectPanelStory::new().into_any(),
             Self::Tab => components::tab::TabStory::new().into_any(),

crates/ui2/src/components.rs 🔗

@@ -1,6 +1,7 @@
 mod assistant_panel;
 mod breadcrumb;
 mod buffer;
+mod chat_panel;
 mod editor_pane;
 mod icon_button;
 mod list;
@@ -17,6 +18,7 @@ mod workspace;
 pub use assistant_panel::*;
 pub use breadcrumb::*;
 pub use buffer::*;
+pub use chat_panel::*;
 pub use editor_pane::*;
 pub use icon_button::*;
 pub use list::*;

crates/ui2/src/components/chat_panel.rs 🔗

@@ -0,0 +1,108 @@
+use std::marker::PhantomData;
+
+use chrono::NaiveDateTime;
+
+use crate::prelude::*;
+use crate::theme::theme;
+use crate::{Icon, IconButton, Input, Label, LabelColor};
+
+#[derive(Element)]
+pub struct ChatPanel<S: 'static + Send + Sync + Clone> {
+    scroll_state: ScrollState,
+    messages: Vec<ChatMessage<S>>,
+}
+
+impl<S: 'static + Send + Sync + Clone> ChatPanel<S> {
+    pub fn new(scroll_state: ScrollState) -> Self {
+        Self {
+            scroll_state,
+            messages: Vec::new(),
+        }
+    }
+
+    pub fn with_messages(mut self, messages: Vec<ChatMessage<S>>) -> Self {
+        self.messages = messages;
+        self
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        let theme = theme(cx);
+
+        div()
+            .flex()
+            .flex_col()
+            .justify_between()
+            .h_full()
+            .px_2()
+            .gap_2()
+            // Header
+            .child(
+                div()
+                    .flex()
+                    .justify_between()
+                    .py_2()
+                    .child(div().flex().child(Label::new("#design")))
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_px()
+                            .child(IconButton::new(Icon::File))
+                            .child(IconButton::new(Icon::AudioOn)),
+                    ),
+            )
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    // Chat Body
+                    .child(
+                        div()
+                            .w_full()
+                            .flex()
+                            .flex_col()
+                            .gap_3()
+                            .overflow_y_scroll(self.scroll_state.clone())
+                            .children(self.messages.clone()),
+                    )
+                    // Composer
+                    .child(div().flex().my_2().child(Input::new("Message #design"))),
+            )
+    }
+}
+
+#[derive(Element, Clone)]
+pub struct ChatMessage<S: 'static + Send + Sync + Clone> {
+    state_type: PhantomData<S>,
+    author: String,
+    text: String,
+    sent_at: NaiveDateTime,
+}
+
+impl<S: 'static + Send + Sync + Clone> ChatMessage<S> {
+    pub fn new(author: String, text: String, sent_at: NaiveDateTime) -> Self {
+        Self {
+            state_type: PhantomData,
+            author,
+            text,
+            sent_at,
+        }
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        div()
+            .flex()
+            .flex_col()
+            .child(
+                div()
+                    .flex()
+                    .gap_2()
+                    .child(Label::new(self.author.clone()))
+                    .child(
+                        Label::new(self.sent_at.format("%m/%d/%Y").to_string())
+                            .color(LabelColor::Muted),
+                    ),
+            )
+            .child(div().child(Label::new(self.text.clone())))
+    }
+}

crates/ui2/src/components/workspace.rs 🔗

@@ -6,8 +6,9 @@ use gpui3::{relative, rems, Size};
 
 use crate::prelude::*;
 use crate::{
-    hello_world_rust_editor_with_status_example, theme, v_stack, EditorPane, Pane, PaneGroup,
-    Panel, PanelAllowedSides, PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal,
+    hello_world_rust_editor_with_status_example, theme, v_stack, ChatMessage, ChatPanel,
+    EditorPane, Pane, PaneGroup, Panel, PanelAllowedSides, PanelSide, ProjectPanel, SplitDirection,
+    StatusBar, Terminal,
 };
 
 #[derive(Element)]
@@ -139,11 +140,7 @@ impl<S: 'static + Send + Sync + Clone> WorkspaceElement<S> {
                             .child(
                                 Panel::new(
                                     self.bottom_panel_scroll_state.clone(),
-                                    |_, _| {
-                                        vec![
-                                            // Terminal::new().into_any()
-                                        ]
-                                    },
+                                    |_, _| vec![Terminal::new().into_any()],
                                     Box::new(()),
                                 )
                                 .allowed_sides(PanelAllowedSides::BottomOnly)
@@ -153,42 +150,34 @@ impl<S: 'static + Send + Sync + Clone> WorkspaceElement<S> {
                     .child(
                         Panel::new(
                             self.right_panel_scroll_state.clone(),
-                            |_, payload| vec![ProjectPanel::new(ScrollState::default()).into_any()],
+                            |_, payload| {
+                                vec![ChatPanel::new(ScrollState::default())
+                                    .with_messages(vec![
+                                        ChatMessage::new(
+                                            "osiewicz".to_string(),
+                                            "is this thing on?".to_string(),
+                                            DateTime::parse_from_rfc3339(
+                                                "2023-09-27T15:40:52.707Z",
+                                            )
+                                            .unwrap()
+                                            .naive_local(),
+                                        ),
+                                        ChatMessage::new(
+                                            "maxdeviant".to_string(),
+                                            "Reading you loud and clear!".to_string(),
+                                            DateTime::parse_from_rfc3339(
+                                                "2023-09-28T15:40:52.707Z",
+                                            )
+                                            .unwrap()
+                                            .naive_local(),
+                                        ),
+                                    ])
+                                    .into_any()]
+                            },
                             Box::new(()),
                         )
                         .side(PanelSide::Right),
                     ),
-                // .child(
-                //     Panel::new(
-                //         self.right_panel_scroll_state.clone(),
-                //         |_, payload| {
-                //             vec![ChatPanel::new(ScrollState::default())
-                //                 .with_messages(vec![
-                //                     ChatMessage::new(
-                //                         "osiewicz".to_string(),
-                //                         "is this thing on?".to_string(),
-                //                         DateTime::parse_from_rfc3339(
-                //                             "2023-09-27T15:40:52.707Z",
-                //                         )
-                //                         .unwrap()
-                //                         .naive_local(),
-                //                     ),
-                //                     ChatMessage::new(
-                //                         "maxdeviant".to_string(),
-                //                         "Reading you loud and clear!".to_string(),
-                //                         DateTime::parse_from_rfc3339(
-                //                             "2023-09-28T15:40:52.707Z",
-                //                         )
-                //                         .unwrap()
-                //                         .naive_local(),
-                //                     ),
-                //                 ])
-                //                 .into_any()]
-                //         },
-                //         Box::new(()),
-                //     )
-                //     .side(PanelSide::Right),
-                // ),
             )
             .child(StatusBar::new())
         // An example of a toast is below