diff --git a/.gitignore b/.gitignore index dcdc56d..4db19df 100644 --- a/.gitignore +++ b/.gitignore @@ -132,4 +132,5 @@ dist **/dist/ dist/ -.idea/ \ No newline at end of file +.idea/ +*storybook.log \ No newline at end of file diff --git a/packages/chat-ui/.storybook/main.ts b/packages/chat-ui/.storybook/main.ts new file mode 100644 index 0000000..c151699 --- /dev/null +++ b/packages/chat-ui/.storybook/main.ts @@ -0,0 +1,14 @@ +export default { + stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], + addons: [ + "@storybook/addon-onboarding", + "@storybook/addon-links", + "@storybook/addon-essentials", + "@chromatic-com/storybook", + "@storybook/addon-interactions", + ], + framework: { + name: "@storybook/react-vite", + options: {}, + }, +}; diff --git a/packages/chat-ui/.storybook/preview.ts b/packages/chat-ui/.storybook/preview.ts new file mode 100644 index 0000000..79ff764 --- /dev/null +++ b/packages/chat-ui/.storybook/preview.ts @@ -0,0 +1,24 @@ +import { Preview } from "@storybook/react"; +import { Reshaped } from "reshaped"; +import "reshaped/themes/reshaped/theme.css" +const preview: Preview = { + decorators: [ + (story) => + Reshaped({ + children: story(), + theme: "reshaped", + defaultTheme:"reshaped", + defaultColorMode: "light", + }), + ], + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, +}; + +export default preview; diff --git a/packages/chat-ui/package.json b/packages/chat-ui/package.json index 57d95e3..e9b5534 100644 --- a/packages/chat-ui/package.json +++ b/packages/chat-ui/package.json @@ -1,49 +1,73 @@ { - "name": "@payload-llm-plugins/chat-ui", - "type": "module", - "types": "./dist/index.d.ts", - "module": "./dist/index.mjs", - "sources": "src/index.ts", - "files": ["dist/", "*.css"], - "exports": { - "./ChatView": { - "types": "./src/ui/page/ChatView.ts", - "import": "./dist/ui/page/ChatView.js", - "default": "./dist/ui/page/ChatView.cjs" - }, - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "default": "./dist/index.cjs" - } - }, - "scripts": { - "test": "vitest --run", - "bench": "vitest --run bench", - "build:ui": "vite build", - "build:plugin": "unbuild --sourcemaps", - "build": "pnpm build:plugin && pnpm build:ui", - "generate": "payload generate:types" - }, - "dependencies": { - "defu": "^6.1.4", - "radash": "^12.1.0" - }, - "peerDependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1" - }, - "devDependencies": { - "tsup": "^8.3.0", - "@types/react": "18.3.8", - "@types/react-dom": "^18.3.0", - "vite-plugin-lib-inject-css": "^2.1.1", - "type-fest": "^4.26.1", - "@vitejs/plugin-react": "^4.3.1", - "vite": "^5.4.7", - "vitest": "^2.1.1", - "reshaped": "^3.2.0-canary.5", - "postcss": "^8.4.47", - "vite-plugin-externalize-deps": "^0.8.0" - } + "name": "@payload-llm-plugins/chat-ui", + "type": "module", + "types": "./dist/index.d.ts", + "module": "./dist/index.mjs", + "sources": "src/index.ts", + "files": [ + "dist/", + "*.css" + ], + "exports": { + "./ChatView": { + "types": "./src/ui/page/ChatView.ts", + "import": "./dist/ui/page/ChatView.js", + "default": "./dist/ui/page/ChatView.cjs" + }, + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "default": "./dist/index.cjs" + } + }, + "scripts": { + "test": "vitest --run", + "bench": "vitest --run bench", + "build:ui": "vite build", + "build:plugin": "unbuild --sourcemaps", + "build": "pnpm build:plugin && pnpm build:ui", + "generate": "payload generate:types", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "dependencies": { + "@heroicons/react": "^2.1.5", + "chancejs": "^0.0.8", + "defu": "^6.1.4", + "feather-icons": "^4.29.2", + "radash": "^12.1.0", + "reshaped": "^3.2.0-canary.6", + "ts-pattern": "^5.3.1" + }, + "peerDependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@chromatic-com/storybook": "^1.9.0", + "@storybook/addon-essentials": "^8.3.2", + "@storybook/addon-interactions": "^8.3.2", + "@storybook/addon-links": "^8.3.2", + "@storybook/addon-onboarding": "^8.3.2", + "@storybook/blocks": "^8.3.2", + "@storybook/react": "^8.3.2", + "@storybook/react-vite": "^8.3.2", + "@storybook/test": "^8.3.2", + "@types/chance": "^1.1.6", + "@types/feather-icons": "^4.29.4", + "@types/react": "18.3.8", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "chance": "^1.1.12", + "postcss": "^8.4.47", + "prop-types": "^15.8.1", + "reshaped": "^3.2.0-canary.5", + "storybook": "^8.3.2", + "tsup": "^8.3.0", + "type-fest": "^4.26.1", + "vite": "^5.4.7", + "vite-plugin-externalize-deps": "^0.8.0", + "vite-plugin-lib-inject-css": "^2.1.1", + "vitest": "^2.1.1" + } } diff --git a/packages/chat-ui/src/narrative/messages/plain/Message.stories.tsx b/packages/chat-ui/src/narrative/messages/plain/Message.stories.tsx new file mode 100644 index 0000000..dc6d255 --- /dev/null +++ b/packages/chat-ui/src/narrative/messages/plain/Message.stories.tsx @@ -0,0 +1,30 @@ +import { Message } from "./Message"; +import { Meta, StoryObj } from "@storybook/react"; +import { Avatar } from "reshaped"; + +export default { + parameters:{ + layout: "centered" + }, + component: Message, +} as Meta; + +export const UserExample: StoryObj = { + args: { + children:

A super important message
It's a small world after all

, + speaker: { + role: "user", + avatar: , + }, + }, +}; + +export const AssistantExample: StoryObj = { + args: { + children:

A super important message
It's a small world after all

, + speaker: { + role: "assistant", + avatar: , + }, + }, +}; diff --git a/packages/chat-ui/src/narrative/messages/plain/Message.tsx b/packages/chat-ui/src/narrative/messages/plain/Message.tsx new file mode 100644 index 0000000..be95497 --- /dev/null +++ b/packages/chat-ui/src/narrative/messages/plain/Message.tsx @@ -0,0 +1,50 @@ +import * as React from "react"; + +import { PropsWithChildren, ReactNode } from "react"; +import { View, ViewProps } from "reshaped"; + +type Role = "assistant" | "user" | "member"; + +interface Props { + speaker: { + role: Role; + avatar: ReactNode; + }; +} + +const Options = { + user: { + color: "primary-faded", + side: "right", + }, + member: { + color: "primary-faded", + side: "left", + }, + assistant: { + color: "neutral-faded", + side: "left", + }, +} satisfies Record< + Role, + { color: ViewProps["backgroundColor"]; side: "left" | "right" } +>; + +export const Message = ({ children, speaker }: PropsWithChildren) => { + const { color, side } = Options[speaker.role]; + const direction = side === "left" ? "row" : "row-reverse"; + + return ( + + {speaker.avatar} + + {children} + + + ); +}; diff --git a/packages/chat-ui/src/narrative/messages/plain/fixtures.tsx b/packages/chat-ui/src/narrative/messages/plain/fixtures.tsx new file mode 100644 index 0000000..8ac8b96 --- /dev/null +++ b/packages/chat-ui/src/narrative/messages/plain/fixtures.tsx @@ -0,0 +1,63 @@ +import { Role, Speaker } from "./type"; +import { Avatar } from "reshaped"; +import { match } from "ts-pattern"; +import { draw, list } from "radash"; +import { Message } from "./Message"; +import Chance from "chance"; +import feather from "feather-icons"; + +const givenASpeaker = (role: Role = "user"): Speaker => { + const initials = match(role) + .with("user", () => "AW") + .with("assistant", () => "GPT") + .with("member", () => "SS") + .otherwise(() => "?"); + + return { + avatar: ( + + ), + role: role, + }; +}; + +export const givenSomeMessages = (count = 5) => { + const chance = new Chance(); + const assistant = givenASpeaker("assistant"); + const user = givenASpeaker("user"); + + return ( + <> + {list(0, count, (i) => ( + +

{chance.sentence()}

+
+ ))} + + ); +}; + +export const givenAnIceBreaker = () => { + const chance = new Chance(); + return { + title: chance.sentence({ words: 4 }), + subtitle: chance.sentence({ words: 8 }), + icon: ( + + ), + }; +}; + +export const FeatherIcon = ({ icon }: { icon: keyof typeof feather.icons }) => { + const data = feather.icons[icon]; + return ( + + ); +}; diff --git a/packages/chat-ui/src/narrative/messages/plain/type.ts b/packages/chat-ui/src/narrative/messages/plain/type.ts new file mode 100644 index 0000000..5620cdc --- /dev/null +++ b/packages/chat-ui/src/narrative/messages/plain/type.ts @@ -0,0 +1,11 @@ +import { ReactNode } from "react"; + +export type Role = "assistant" | "user" | "member"; +export type Speaker = { + role: Role; + avatar: ReactNode; +} + +export interface Props { + speaker: Speaker +} diff --git a/packages/chat-ui/src/narrative/panes/chat/Chat.stories.tsx b/packages/chat-ui/src/narrative/panes/chat/Chat.stories.tsx new file mode 100644 index 0000000..875aa8a --- /dev/null +++ b/packages/chat-ui/src/narrative/panes/chat/Chat.stories.tsx @@ -0,0 +1,28 @@ +import { ChatPane, WithMessages } from "./ChatPane"; +import { Meta, StoryObj } from "@storybook/react"; +import { + givenAnIceBreaker, + givenSomeMessages, +} from "../../messages/plain/fixtures"; +import { Icebreaker, IceBreakers } from "./elements/Icebreakers"; + +export default { + component: ChatPane, + parameters: { layout: "full" }, +} as Meta; + +export const ChatExample: StoryObj = { + args: { children: }, +}; + +export const NewChat: StoryObj = { + args: { + children: ( + + + + + + ), + }, +}; diff --git a/packages/chat-ui/src/narrative/panes/chat/ChatPane.tsx b/packages/chat-ui/src/narrative/panes/chat/ChatPane.tsx new file mode 100644 index 0000000..973f4e0 --- /dev/null +++ b/packages/chat-ui/src/narrative/panes/chat/ChatPane.tsx @@ -0,0 +1,30 @@ +import { MessageList } from "./elements/MessageList"; +import { Divider, View } from "reshaped"; +import { MessageInput } from "./elements/MessageInput"; +import { PropsWithChildren } from "react"; + +interface Props { + messages: JSX.Element; +} + +export const WithMessages = ({ messages }: Props) => { + return {messages}; +}; + + +export const ChatPane = ({ children }: PropsWithChildren) => { + return ( + + {children} + + + + ); +}; diff --git a/packages/chat-ui/src/narrative/panes/chat/elements/Icebreakers.tsx b/packages/chat-ui/src/narrative/panes/chat/elements/Icebreakers.tsx new file mode 100644 index 0000000..f1c68a4 --- /dev/null +++ b/packages/chat-ui/src/narrative/panes/chat/elements/Icebreakers.tsx @@ -0,0 +1,26 @@ +import { Card, Icon, Text, View } from "reshaped"; +import React, { PropsWithChildren, ReactElement, ReactNode } from "react"; + +interface Props { + title: string; + subtitle: string; +} + +export const Icebreaker = (props: Props) => { + return ( + alert('hi')}> + + {props.title} + {props.subtitle} + + + ); +}; + +export const IceBreakers = ({ children }: PropsWithChildren) => { + return ( + + {children} + + ); +}; diff --git a/packages/chat-ui/src/narrative/panes/chat/elements/MessageInput.tsx b/packages/chat-ui/src/narrative/panes/chat/elements/MessageInput.tsx new file mode 100644 index 0000000..c809c60 --- /dev/null +++ b/packages/chat-ui/src/narrative/panes/chat/elements/MessageInput.tsx @@ -0,0 +1,20 @@ +import { Button, TextArea, TextField, View } from "reshaped"; + +export const MessageInput = () => { + return ( + + +