assistant2: Setup storybook (#11228)

This PR sets up the `assistant2` crate with the storybook so that UI
elements can be iterated on in isolation.

To start, we have some stories for the `ChatMessage` component:

```sh
cargo run -p storybook -- components/assistant_chat_message
```

<img width="1233" alt="Screenshot 2024-04-30 at 5 20 03 PM"
src="https://github.com/zed-industries/zed/assets/1486634/510967ea-0e9b-4fa9-94fb-421ee74bcc45">

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2024-04-30 17:33:32 -04:00 committed by GitHub
parent 96b1fc4650
commit f2a1226e18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 139 additions and 19 deletions

2
Cargo.lock generated
View file

@ -401,6 +401,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"settings", "settings",
"story",
"theme", "theme",
"ui", "ui",
"util", "util",
@ -9498,6 +9499,7 @@ name = "storybook"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assistant2",
"clap 4.4.4", "clap 4.4.4",
"collab_ui", "collab_ui",
"ctrlc", "ctrlc",

View file

@ -5,9 +5,16 @@ edition = "2021"
publish = false publish = false
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib] [lib]
path = "src/assistant2.rs" path = "src/assistant2.rs"
[features]
default = []
stories = ["dep:story"]
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
assistant_tooling.workspace = true assistant_tooling.workspace = true
@ -29,6 +36,7 @@ semantic_index.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
settings.workspace = true settings.workspace = true
story = { workspace = true, optional = true }
theme.workspace = true theme.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true
@ -49,6 +57,3 @@ settings = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] } theme = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] } util = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] }
[lints]
workspace = true

View file

@ -1,7 +1,7 @@
mod assistant_settings; mod assistant_settings;
mod completion_provider; mod completion_provider;
mod tools; mod tools;
mod ui; pub mod ui;
use ::ui::{div, prelude::*, Color, ViewContext}; use ::ui::{div, prelude::*, Color, ViewContext};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
@ -222,7 +222,7 @@ impl FocusableView for AssistantPanel {
} }
} }
struct AssistantChat { pub struct AssistantChat {
model: String, model: String,
messages: Vec<ChatMessage>, messages: Vec<ChatMessage>,
list_state: ListState, list_state: ListState,
@ -574,12 +574,15 @@ impl AssistantChat {
.map(|element| { .map(|element| {
if self.editing_message_id.as_ref() == Some(id) { if self.editing_message_id.as_ref() == Some(id) {
element.child(Composer::new( element.child(Composer::new(
cx.view().downgrade(),
self.model.clone(),
body.clone(), body.clone(),
self.user_store.read(cx).current_user(), self.user_store.read(cx).current_user(),
self.can_submit(), self.can_submit(),
self.tool_registry.clone(), self.tool_registry.clone(),
crate::ui::ModelSelector::new(
cx.view().downgrade(),
self.model.clone(),
)
.into_any_element(),
)) ))
} else { } else {
element element
@ -744,18 +747,18 @@ impl Render for AssistantChat {
.text_color(Color::Default.color(cx)) .text_color(Color::Default.color(cx))
.child(list(self.list_state.clone()).flex_1()) .child(list(self.list_state.clone()).flex_1())
.child(Composer::new( .child(Composer::new(
cx.view().downgrade(),
self.model.clone(),
self.composer_editor.clone(), self.composer_editor.clone(),
self.user_store.read(cx).current_user(), self.user_store.read(cx).current_user(),
self.can_submit(), self.can_submit(),
self.tool_registry.clone(), self.tool_registry.clone(),
crate::ui::ModelSelector::new(cx.view().downgrade(), self.model.clone())
.into_any_element(),
)) ))
} }
} }
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
struct MessageId(usize); pub struct MessageId(usize);
impl MessageId { impl MessageId {
fn post_inc(&mut self) -> Self { fn post_inc(&mut self) -> Self {

View file

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

View file

@ -1,7 +1,7 @@
use assistant_tooling::ToolRegistry; use assistant_tooling::ToolRegistry;
use client::User; use client::User;
use editor::{Editor, EditorElement, EditorStyle}; use editor::{Editor, EditorElement, EditorStyle};
use gpui::{FontStyle, FontWeight, TextStyle, View, WeakView, WhiteSpace}; use gpui::{AnyElement, FontStyle, FontWeight, TextStyle, View, WeakView, WhiteSpace};
use settings::Settings; use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use theme::ThemeSettings; use theme::ThemeSettings;
@ -11,30 +11,27 @@ use crate::{AssistantChat, CompletionProvider, Submit, SubmitMode};
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Composer { pub struct Composer {
assistant_chat: WeakView<AssistantChat>,
model: String,
editor: View<Editor>, editor: View<Editor>,
player: Option<Arc<User>>, player: Option<Arc<User>>,
can_submit: bool, can_submit: bool,
tool_registry: Arc<ToolRegistry>, tool_registry: Arc<ToolRegistry>,
model_selector: AnyElement,
} }
impl Composer { impl Composer {
pub fn new( pub fn new(
assistant_chat: WeakView<AssistantChat>,
model: String,
editor: View<Editor>, editor: View<Editor>,
player: Option<Arc<User>>, player: Option<Arc<User>>,
can_submit: bool, can_submit: bool,
tool_registry: Arc<ToolRegistry>, tool_registry: Arc<ToolRegistry>,
model_selector: AnyElement,
) -> Self { ) -> Self {
Self { Self {
assistant_chat,
model,
editor, editor,
player, player,
can_submit, can_submit,
tool_registry, tool_registry,
model_selector,
} }
} }
} }
@ -150,7 +147,7 @@ impl RenderOnce for Composer {
h_flex() h_flex()
.w_full() .w_full()
.justify_between() .justify_between()
.child(ModelSelector::new(self.assistant_chat, self.model)) .child(self.model_selector)
.children(self.tool_registry.status_views().iter().cloned()), .children(self.tool_registry.status_views().iter().cloned()),
), ),
) )
@ -158,7 +155,7 @@ impl RenderOnce for Composer {
} }
#[derive(IntoElement)] #[derive(IntoElement)]
struct ModelSelector { pub struct ModelSelector {
assistant_chat: WeakView<AssistantChat>, assistant_chat: WeakView<AssistantChat>,
model: String, model: String,
} }

View file

@ -0,0 +1,3 @@
mod chat_message;
pub use chat_message::*;

View file

@ -0,0 +1,99 @@
use std::sync::Arc;
use client::User;
use story::{StoryContainer, StoryItem, StorySection};
use ui::prelude::*;
use crate::ui::{ChatMessage, UserOrAssistant};
use crate::MessageId;
pub struct ChatMessageStory;
impl Render for ChatMessageStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
let user_1 = Arc::new(User {
id: 12345,
github_login: "iamnbutler".into(),
avatar_uri: "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
});
StoryContainer::new(
"ChatMessage Story",
"crates/assistant2/src/ui/stories/chat_message.rs",
)
.child(
StorySection::new()
.child(StoryItem::new(
"User chat message",
ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
Some(div().child("What can I do here?").into_any_element()),
false,
Box::new(|_, _| {}),
),
))
.child(StoryItem::new(
"User chat message (collapsed)",
ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
Some(div().child("What can I do here?").into_any_element()),
true,
Box::new(|_, _| {}),
),
)),
)
.child(
StorySection::new()
.child(StoryItem::new(
"Assistant chat message",
ChatMessage::new(
MessageId(0),
UserOrAssistant::Assistant,
Some(div().child("You can talk to me!").into_any_element()),
false,
Box::new(|_, _| {}),
),
))
.child(StoryItem::new(
"Assistant chat message (collapsed)",
ChatMessage::new(
MessageId(0),
UserOrAssistant::Assistant,
Some(div().child("You can talk to me!").into_any_element()),
true,
Box::new(|_, _| {}),
),
)),
)
.child(
StorySection::new().child(StoryItem::new(
"Conversation between user and assistant",
v_flex()
.gap_2()
.child(ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
Some(div().child("What is Rust??").into_any_element()),
false,
Box::new(|_, _| {}),
))
.child(ChatMessage::new(
MessageId(0),
UserOrAssistant::Assistant,
Some(div().child("Rust is a multi-paradigm programming language focused on performance and safety").into_any_element()),
false,
Box::new(|_, _| {}),
))
.child(ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1)),
Some(div().child("Sounds pretty cool!").into_any_element()),
false,
Box::new(|_, _| {}),
)),
)),
)
}
}

View file

@ -14,6 +14,7 @@ path = "src/storybook.rs"
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
assistant2 = { workspace = true, features = ["stories"] }
clap = { workspace = true, features = ["derive", "string"] } clap = { workspace = true, features = ["derive", "string"] }
collab_ui = { workspace = true, features = ["stories"] } collab_ui = { workspace = true, features = ["stories"] }
ctrlc = "3.4" ctrlc = "3.4"

View file

@ -12,6 +12,7 @@ use ui::prelude::*;
#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)] #[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
#[strum(serialize_all = "snake_case")] #[strum(serialize_all = "snake_case")]
pub enum ComponentStory { pub enum ComponentStory {
AssistantChatMessage,
AutoHeightEditor, AutoHeightEditor,
Avatar, Avatar,
Button, Button,
@ -42,6 +43,9 @@ pub enum ComponentStory {
impl ComponentStory { impl ComponentStory {
pub fn story(&self, cx: &mut WindowContext) -> AnyView { pub fn story(&self, cx: &mut WindowContext) -> AnyView {
match self { match self {
Self::AssistantChatMessage => {
cx.new_view(|_cx| assistant2::ui::ChatMessageStory).into()
}
Self::AutoHeightEditor => AutoHeightEditorStory::new(cx).into(), Self::AutoHeightEditor => AutoHeightEditorStory::new(cx).into(),
Self::Avatar => cx.new_view(|_| ui::AvatarStory).into(), Self::Avatar => cx.new_view(|_| ui::AvatarStory).into(),
Self::Button => cx.new_view(|_| ui::ButtonStory).into(), Self::Button => cx.new_view(|_| ui::ButtonStory).into(),