assistant2: Add `ChatNotice` component (#11262)

Marshall Bowers created

This PR adds a new `ChatNotice` component for displaying notices within
the assistant.

We will be able to use this for allowing the user to confirm a project
index request.

<img width="518" alt="Screenshot 2024-05-01 at 2 41 31 PM"
src="https://github.com/zed-industries/zed/assets/1486634/3beaeeaf-6046-4284-ac0a-4248e1f9ac20">

Release Notes:

- N/A

Change summary

crates/assistant2/src/ui.rs                     |  2 
crates/assistant2/src/ui/chat_message.rs        |  2 
crates/assistant2/src/ui/chat_notice.rs         | 71 +++++++++++++++++++
crates/assistant2/src/ui/composer.rs            |  4 
crates/assistant2/src/ui/stories.rs             |  2 
crates/assistant2/src/ui/stories/chat_notice.rs | 22 +++++
crates/storybook/src/story_selector.rs          |  2 
7 files changed, 102 insertions(+), 3 deletions(-)

Detailed changes

crates/assistant2/src/ui.rs 🔗

@@ -1,10 +1,12 @@
 mod chat_message;
+mod chat_notice;
 mod composer;
 
 #[cfg(feature = "stories")]
 mod stories;
 
 pub use chat_message::*;
+pub use chat_notice::*;
 pub use composer::*;
 
 #[cfg(feature = "stories")]

crates/assistant2/src/ui/chat_message.rs 🔗

@@ -118,7 +118,7 @@ impl RenderOnce for ChatMessageHeader {
                 h_flex()
                     .gap_3()
                     .map(|this| {
-                        let avatar_size = rems(20.0 / 16.0);
+                        let avatar_size = rems_from_px(20.);
                         if let Some(avatar_uri) = avatar_uri {
                             this.child(Avatar::new(avatar_uri).size(avatar_size))
                         } else {

crates/assistant2/src/ui/chat_notice.rs 🔗

@@ -0,0 +1,71 @@
+use ui::{prelude::*, Avatar, IconButtonShape};
+
+#[derive(IntoElement)]
+pub struct ChatNotice {
+    message: SharedString,
+    meta: Option<SharedString>,
+}
+
+impl ChatNotice {
+    pub fn new(message: impl Into<SharedString>) -> Self {
+        Self {
+            message: message.into(),
+            meta: None,
+        }
+    }
+
+    pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
+        self.meta = Some(meta.into());
+        self
+    }
+}
+
+impl RenderOnce for ChatNotice {
+    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
+        h_flex()
+            .w_full()
+            .items_start()
+            .mt_4()
+            .gap_3()
+            .child(
+                // TODO: Replace with question mark.
+                Avatar::new("https://zed.dev/assistant_avatar.png").size(rems_from_px(20.)),
+            )
+            .child(
+                v_flex()
+                    .size_full()
+                    .gap_1()
+                    .pr_4()
+                    .overflow_hidden()
+                    .child(
+                        h_flex()
+                            .justify_between()
+                            .overflow_hidden()
+                            .child(
+                                h_flex()
+                                    .flex_none()
+                                    .overflow_hidden()
+                                    .child(Label::new(self.message)),
+                            )
+                            .child(
+                                h_flex()
+                                    .flex_shrink_0()
+                                    .gap_1()
+                                    .child(Button::new("allow", "Allow"))
+                                    .child(
+                                        IconButton::new("deny", IconName::Close)
+                                            .shape(IconButtonShape::Square)
+                                            .icon_color(Color::Muted)
+                                            .size(ButtonSize::None)
+                                            .icon_size(IconSize::XSmall),
+                                    ),
+                            ),
+                    )
+                    .children(
+                        self.meta.map(|meta| {
+                            Label::new(meta).size(LabelSize::Small).color(Color::Muted)
+                        }),
+                    ),
+            )
+    }
+}

crates/assistant2/src/ui/composer.rs 🔗

@@ -38,10 +38,10 @@ impl Composer {
 
 impl RenderOnce for Composer {
     fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        let mut player_avatar = div().size(rems(20.0 / 16.0)).into_any_element();
+        let mut player_avatar = div().size(rems_from_px(20.)).into_any_element();
         if let Some(player) = self.player.clone() {
             player_avatar = Avatar::new(player.avatar_uri.clone())
-                .size(rems(20.0 / 16.0))
+                .size(rems_from_px(20.))
                 .into_any_element();
         }
 

crates/assistant2/src/ui/stories/chat_notice.rs 🔗

@@ -0,0 +1,22 @@
+use story::{StoryContainer, StoryItem, StorySection};
+use ui::prelude::*;
+
+use crate::ui::ChatNotice;
+
+pub struct ChatNoticeStory;
+
+impl Render for ChatNoticeStory {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+        StoryContainer::new(
+            "ChatNotice Story",
+            "crates/assistant2/src/ui/stories/chat_notice.rs",
+        )
+        .child(
+            StorySection::new().child(StoryItem::new(
+                "Project index request",
+                ChatNotice::new("Allow assistant to index your project?")
+                    .meta("Enabling will allow responses more relevant to this project."),
+            )),
+        )
+    }
+}

crates/storybook/src/story_selector.rs 🔗

@@ -13,6 +13,7 @@ use ui::prelude::*;
 #[strum(serialize_all = "snake_case")]
 pub enum ComponentStory {
     AssistantChatMessage,
+    AssistantChatNotice,
     AutoHeightEditor,
     Avatar,
     Button,
@@ -46,6 +47,7 @@ impl ComponentStory {
             Self::AssistantChatMessage => {
                 cx.new_view(|_cx| assistant2::ui::ChatMessageStory).into()
             }
+            Self::AssistantChatNotice => cx.new_view(|_cx| assistant2::ui::ChatNoticeStory).into(),
             Self::AutoHeightEditor => AutoHeightEditorStory::new(cx).into(),
             Self::Avatar => cx.new_view(|_| ui::AvatarStory).into(),
             Self::Button => cx.new_view(|_| ui::ButtonStory).into(),