Browse Source

feat: storybook (#9324)

Hash Brown 5 months ago
parent
commit
5df1cb0566

+ 2 - 1
web/.eslintrc.json

@@ -1,7 +1,8 @@
 {
   "extends": [
     "next",
-    "@antfu"
+    "@antfu",
+    "plugin:storybook/recommended"
   ],
   "rules": {
     "@typescript-eslint/consistent-type-definitions": [

+ 2 - 1
web/.gitignore

@@ -49,4 +49,5 @@ package-lock.json
 # pmpm
 pnpm-lock.yaml
 
-.favorites.json
+.favorites.json
+*storybook.log

+ 19 - 0
web/.storybook/main.ts

@@ -0,0 +1,19 @@
+import type { StorybookConfig } from '@storybook/nextjs'
+
+const config: StorybookConfig = {
+    // stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
+    stories: ['../app/components/**/*.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/nextjs',
+        options: {},
+    },
+    staticDirs: ['../public'],
+}
+export default config

+ 37 - 0
web/.storybook/preview.tsx

@@ -0,0 +1,37 @@
+import React from 'react'
+import type { Preview } from '@storybook/react'
+import { withThemeByDataAttribute } from '@storybook/addon-themes';
+import I18nServer from '../app/components/i18n-server'
+
+import '../app/styles/globals.css'
+import '../app/styles/markdown.scss'
+import './storybook.css'
+
+export const decorators = [
+    withThemeByDataAttribute({
+      themes: {
+        light: 'light',
+        dark: 'dark',
+      },
+      defaultTheme: 'light',
+      attributeName: 'data-theme',
+    }),
+    Story => {
+      return <I18nServer>
+        <Story />
+      </I18nServer>
+    }
+  ];
+
+const preview: Preview = {
+  parameters: {
+        controls: {
+            matchers: {
+                color: /(background|color)$/i,
+                date: /Date$/i,
+            },
+        },
+    },
+}
+
+export default preview

+ 6 - 0
web/.storybook/storybook.css

@@ -0,0 +1,6 @@
+html,
+body {
+  max-width: unset;
+  overflow: auto;
+  user-select: text;
+}

+ 12 - 0
web/README.md

@@ -74,6 +74,18 @@ If you want to customize the host and port:
 npm run start --port=3001 --host=0.0.0.0
 ```
 
+## Storybook
+
+This project uses [Storybook](https://storybook.js.org/) for UI component development.
+
+To start the storybook server, run:
+
+```bash
+yarn storybook
+```
+
+Open [http://localhost:6006](http://localhost:6006) with your browser to see the result.
+
 ## Lint Code
 
 If your IDE is VSCode, rename `web/.vscode/settings.example.json` to `web/.vscode/settings.json` for lint code setting.

+ 107 - 0
web/app/components/base/button/index.stories.tsx

@@ -0,0 +1,107 @@
+import type { Meta, StoryObj } from '@storybook/react'
+import { fn } from '@storybook/test'
+
+import { RocketLaunchIcon } from '@heroicons/react/20/solid'
+import { Button } from '.'
+
+const meta = {
+  title: 'Base/Button',
+  component: Button,
+  parameters: {
+    layout: 'centered',
+  },
+  tags: ['autodocs'],
+  argTypes: {
+    loading: { control: 'boolean' },
+    variant: {
+      control: 'select',
+      options: ['primary', 'warning', 'secondary', 'secondary-accent', 'ghost', 'ghost-accent', 'tertiary'],
+    },
+  },
+  args: {
+    variant: 'ghost',
+    onClick: fn(),
+    children: 'adsf',
+  },
+} satisfies Meta<typeof Button>
+
+export default meta
+type Story = StoryObj<typeof meta>
+
+export const Default: Story = {
+  args: {
+    variant: 'primary',
+    loading: false,
+    children: 'Primary Button',
+  },
+}
+
+export const Secondary: Story = {
+  args: {
+    variant: 'secondary',
+    children: 'Secondary Button',
+  },
+}
+
+export const SecondaryAccent: Story = {
+  args: {
+    variant: 'secondary-accent',
+    children: 'Secondary Accent Button',
+  },
+}
+
+export const Ghost: Story = {
+  args: {
+    variant: 'ghost',
+    children: 'Ghost Button',
+  },
+}
+
+export const GhostAccent: Story = {
+  args: {
+    variant: 'ghost-accent',
+    children: 'Ghost Accent Button',
+  },
+}
+
+export const Tertiary: Story = {
+  args: {
+    variant: 'tertiary',
+    children: 'Tertiary Button',
+  },
+}
+
+export const Warning: Story = {
+  args: {
+    variant: 'warning',
+    children: 'Warning Button',
+  },
+}
+
+export const Disabled: Story = {
+  args: {
+    variant: 'primary',
+    disabled: true,
+    children: 'Disabled Button',
+  },
+}
+
+export const Loading: Story = {
+  args: {
+    variant: 'primary',
+    loading: true,
+    children: 'Loading Button',
+  },
+}
+
+export const WithIcon: Story = {
+  args: {
+    variant: 'primary',
+    children: (
+      <>
+        <RocketLaunchIcon className="h-4 w-4 mr-1.5 stroke-[1.8px]" />
+        Launch
+      </>
+    ),
+  },
+}

+ 61 - 0
web/app/components/base/chat/chat/answer/__mocks__/markdownContent.ts

@@ -0,0 +1,61 @@
+export const markdownContent = `
+# Heading 1
+
+## Heading 2
+
+### Heading 3
+
+#### Heading 4
+
+##### Heading 5
+
+###### Heading 6
+
+# Basic markdown content.
+
+Should support **bold**, *italic*, and ~~strikethrough~~.
+Should support [links](https://www.google.com).
+Should support inline \`code\` blocks.
+
+# Number list
+
+1. First item
+2. Second item
+3. Third item
+
+# Bullet list
+
+- First item
+- Second item
+- Third item
+
+# Link
+
+[Google](https://www.google.com)
+
+# Image
+
+![Alt text](https://picsum.photos/200/300)
+
+# Table
+
+| Column 1 | Column 2 | Column 3 |
+| -------- | -------- | -------- |
+| Cell 1   | Cell 2   | Cell 3   |
+| Cell 4   | Cell 5   | Cell 6   |
+| Cell 7   | Cell 8   | Cell 9   |
+
+# Code
+
+\`\`\`JavaScript
+const code = "code"
+\`\`\`
+
+# Blockquote
+
+> This is a blockquote.
+
+# Horizontal rule
+
+---
+`

+ 27 - 0
web/app/components/base/chat/chat/answer/__mocks__/markdownContentSVG.ts

@@ -0,0 +1,27 @@
+export const markdownContentSVG = `
+\`\`\`svg
+<svg width="400" height="600" xmlns="http://www.w3.org/2000/svg">
+  <rect width="100%" height="100%" fill="#F0F8FF"/>
+  
+  <text x="50%" y="60" font-family="楷体" font-size="32" fill="#4682B4" text-anchor="middle">创意Logo设计</text>
+  
+  <line x1="50" y1="80" x2="350" y2="80" stroke="#B0C4DE" stroke-width="2"/>
+  
+  <text x="50%" y="120" font-family="Arial" font-size="24" fill="#708090" text-anchor="middle">科研</text>
+  <text x="50%" y="150" font-family="MS Mincho" font-size="20" fill="#778899" text-anchor="middle">科学研究</text>
+  
+  <text x="50%" y="200" font-family="汇文明朝体" font-size="18" fill="#696969" text-anchor="middle">
+    <tspan x="50%" dy="25">探索未知的灯塔,</tspan>
+    <tspan x="50%" dy="25">照亮人类前进的道路。</tspan>
+    <tspan x="50%" dy="25">科研,是永不熄灭的好奇心,</tspan>
+    <tspan x="50%" dy="25">也是推动世界进步的引擎。</tspan>
+  </text>
+  
+  <circle cx="200" cy="400" r="80" fill="none" stroke="#4169E1" stroke-width="3"/>
+  <line x1="200" y1="320" x2="200" y2="480" stroke="#4169E1" stroke-width="3"/>
+  <line x1="120" y1="400" x2="280" y2="400" stroke="#4169E1" stroke-width="3"/>
+  
+  <text x="50%" y="550" font-family="微软雅黑" font-size="16" fill="#1E90FF" text-anchor="middle">探索 • 创新 • 进步</text>
+</svg>
+\`\`\`
+`

+ 136 - 0
web/app/components/base/chat/chat/answer/__mocks__/workflowProcess.ts

@@ -0,0 +1,136 @@
+import type { WorkflowProcess } from '@/app/components/base/chat/types'
+import { WorkflowRunningStatus } from '@/app/components/workflow/types'
+
+export const mockedWorkflowProcess = {
+  status: WorkflowRunningStatus.Succeeded,
+  resultText: 'Hello, how can I assist you today?',
+  tracing: [
+    {
+      extras: {},
+      id: 'f6337dc9-e280-4915-965f-10b0552dd917',
+      node_id: '1724232060789',
+      node_type: 'start',
+      title: 'Start',
+      index: 1,
+      predecessor_node_id: null,
+      inputs: {
+        'sys.query': 'hi',
+        'sys.files': [],
+        'sys.conversation_id': '92ce0a3e-8f15-43d1-b31d-32716c4b10a7',
+        'sys.user_id': 'fbff43f9-d5a4-4e85-b63b-d3a91d806c6f',
+        'sys.dialogue_count': 1,
+        'sys.app_id': 'b2e8906a-aad3-43a0-9ace-0e44cc7315e1',
+        'sys.workflow_id': '70004abe-561f-418b-b9e8-8c957ce55140',
+        'sys.workflow_run_id': '69db9267-aaee-42e1-9581-dbfb67e8eeb5',
+      },
+      process_data: null,
+      outputs: {
+        'sys.query': 'hi',
+        'sys.files': [],
+        'sys.conversation_id': '92ce0a3e-8f15-43d1-b31d-32716c4b10a7',
+        'sys.user_id': 'fbff43f9-d5a4-4e85-b63b-d3a91d806c6f',
+        'sys.dialogue_count': 1,
+        'sys.app_id': 'b2e8906a-aad3-43a0-9ace-0e44cc7315e1',
+        'sys.workflow_id': '70004abe-561f-418b-b9e8-8c957ce55140',
+        'sys.workflow_run_id': '69db9267-aaee-42e1-9581-dbfb67e8eeb5',
+      },
+      status: 'succeeded',
+      error: null,
+      elapsed_time: 0.035744,
+      execution_metadata: null,
+      created_at: 1728980002,
+      finished_at: 1728980002,
+      files: [],
+      parallel_id: null,
+      parallel_start_node_id: null,
+      parent_parallel_id: null,
+      parent_parallel_start_node_id: null,
+      iteration_id: null,
+    },
+    {
+      extras: {},
+      id: '92204d8d-4198-4c46-aa02-c2754b11dec9',
+      node_id: 'llm',
+      node_type: 'llm',
+      title: 'LLM',
+      index: 2,
+      predecessor_node_id: '1724232060789',
+      inputs: null,
+      process_data: {
+        model_mode: 'chat',
+        prompts: [
+          {
+            role: 'system',
+            text: 'hi',
+            files: [],
+          },
+          {
+            role: 'user',
+            text: 'hi',
+            files: [],
+          },
+        ],
+        model_provider: 'openai',
+        model_name: 'gpt-4o-mini',
+      },
+      outputs: {
+        text: 'Hello! How can I assist you today?',
+        usage: {
+          prompt_tokens: 13,
+          prompt_unit_price: '0.15',
+          prompt_price_unit: '0.000001',
+          prompt_price: '0.0000020',
+          completion_tokens: 9,
+          completion_unit_price: '0.60',
+          completion_price_unit: '0.000001',
+          completion_price: '0.0000054',
+          total_tokens: 22,
+          total_price: '0.0000074',
+          currency: 'USD',
+          latency: 1.8902503330027685,
+        },
+        finish_reason: 'stop',
+      },
+      status: 'succeeded',
+      error: null,
+      elapsed_time: 5.089409,
+      execution_metadata: {
+        total_tokens: 22,
+        total_price: '0.0000074',
+        currency: 'USD',
+      },
+      created_at: 1728980002,
+      finished_at: 1728980007,
+      files: [],
+      parallel_id: null,
+      parallel_start_node_id: null,
+      parent_parallel_id: null,
+      parent_parallel_start_node_id: null,
+      iteration_id: null,
+    },
+    {
+      extras: {},
+      id: '7149bac6-60f9-4e06-a5ed-1d9d3764c06b',
+      node_id: 'answer',
+      node_type: 'answer',
+      title: 'Answer',
+      index: 3,
+      predecessor_node_id: 'llm',
+      inputs: null,
+      process_data: null,
+      outputs: {
+        answer: 'Hello! How can I assist you today?',
+      },
+      status: 'succeeded',
+      error: null,
+      elapsed_time: 0.015339,
+      execution_metadata: null,
+      created_at: 1728980007,
+      finished_at: 1728980007,
+      parallel_id: null,
+      parallel_start_node_id: null,
+      parent_parallel_id: null,
+      parent_parallel_start_node_id: null,
+    },
+  ],
+} as unknown as WorkflowProcess

+ 96 - 0
web/app/components/base/chat/chat/answer/index.stories.tsx

@@ -0,0 +1,96 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import type { ChatItem } from '../../types'
+import { mockedWorkflowProcess } from './__mocks__/workflowProcess'
+import { markdownContent } from './__mocks__/markdownContent'
+import { markdownContentSVG } from './__mocks__/markdownContentSVG'
+import Answer from '.'
+
+const meta = {
+  title: 'Base/Chat Answer',
+  component: Answer,
+  parameters: {
+    layout: 'fullscreen',
+  },
+  tags: ['autodocs'],
+  argTypes: {
+    noChatInput: { control: 'boolean', description: 'If set to true, some buttons that are supposed to be shown on hover will not be displayed.' },
+    responding: { control: 'boolean', description: 'Indicates if the answer is being generated.' },
+    showPromptLog: { control: 'boolean', description: 'If set to true, the prompt log button will be shown on hover.' },
+  },
+  args: {
+    noChatInput: false,
+    responding: false,
+    showPromptLog: false,
+  },
+} satisfies Meta<typeof Answer>
+
+export default meta
+type Story = StoryObj<typeof meta>
+
+const mockedBaseChatItem = {
+  id: '1',
+  isAnswer: true,
+  content: 'Hello, how can I assist you today?',
+} satisfies ChatItem
+
+export const Basic: Story = {
+  args: {
+    item: mockedBaseChatItem,
+    question: mockedBaseChatItem.content,
+    index: 0,
+  },
+  render: (args) => {
+    return <div className="w-full px-10 py-5">
+      <Answer {...args} />
+    </div>
+  },
+}
+
+export const WithWorkflowProcess: Story = {
+  args: {
+    item: {
+      ...mockedBaseChatItem,
+      workflowProcess: mockedWorkflowProcess,
+    },
+    question: mockedBaseChatItem.content,
+    index: 0,
+  },
+  render: (args) => {
+    return <div className="w-full px-10 py-5">
+      <Answer {...args} />
+    </div>
+  },
+}
+
+export const WithMarkdownContent: Story = {
+  args: {
+    item: {
+      ...mockedBaseChatItem,
+      content: markdownContent,
+    },
+    question: mockedBaseChatItem.content,
+    index: 0,
+  },
+  render: (args) => {
+    return <div className="w-full px-10 py-5">
+      <Answer {...args} />
+    </div>
+  },
+}
+
+export const WithMarkdownSVG: Story = {
+  args: {
+    item: {
+      ...mockedBaseChatItem,
+      content: markdownContentSVG,
+    },
+    question: mockedBaseChatItem.content,
+    index: 0,
+  },
+  render: (args) => {
+    return <div className="w-full px-10 py-5">
+      <Answer {...args} />
+    </div>
+  },
+}

+ 33 - 0
web/app/components/base/chat/chat/question.stories.tsx

@@ -0,0 +1,33 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import type { ChatItem } from '../types'
+import Question from './question'
+import { User } from '@/app/components/base/icons/src/public/avatar'
+
+const meta = {
+  title: 'Base/Chat Question',
+  component: Question,
+  parameters: {
+    layout: 'centered',
+  },
+  tags: ['autodocs'],
+  argTypes: {},
+  args: {},
+} satisfies Meta<typeof Question>
+
+export default meta
+type Story = StoryObj<typeof meta>
+
+export const Default: Story = {
+  args: {
+    item: {
+      id: '1',
+      isAnswer: false,
+      content: 'You are a helpful assistant.',
+    } satisfies ChatItem,
+    theme: undefined,
+    questionIcon: <div className='w-full h-full rounded-full border-[0.5px] border-black/5'>
+      <User className='w-full h-full' />
+    </div>,
+  },
+}

+ 15 - 1
web/package.json

@@ -18,7 +18,9 @@
     "check-i18n": "node ./i18n/check-i18n.js",
     "auto-gen-i18n": "node ./i18n/auto-gen-i18n.js",
     "test": "jest",
-    "test:watch": "jest --watch"
+    "test:watch": "jest --watch",
+    "storybook": "storybook dev -p 6006",
+    "build-storybook": "storybook build"
   },
   "dependencies": {
     "@babel/runtime": "^7.22.3",
@@ -106,8 +108,18 @@
   },
   "devDependencies": {
     "@antfu/eslint-config": "^0.36.0",
+    "@chromatic-com/storybook": "^1.9.0",
     "@faker-js/faker": "^7.6.0",
     "@rgrove/parse-xml": "^4.1.0",
+    "@storybook/addon-essentials": "^8.3.5",
+    "@storybook/addon-interactions": "^8.3.5",
+    "@storybook/addon-links": "^8.3.5",
+    "@storybook/addon-onboarding": "^8.3.5",
+    "@storybook/addon-themes": "^8.3.5",
+    "@storybook/blocks": "^8.3.5",
+    "@storybook/nextjs": "^8.3.5",
+    "@storybook/react": "^8.3.5",
+    "@storybook/test": "^8.3.5",
     "@testing-library/dom": "^10.3.2",
     "@testing-library/jest-dom": "^6.4.6",
     "@testing-library/react": "^16.0.0",
@@ -134,6 +146,7 @@
     "cross-env": "^7.0.3",
     "eslint": "^8.36.0",
     "eslint-config-next": "^14.0.4",
+    "eslint-plugin-storybook": "^0.9.0",
     "husky": "^8.0.3",
     "jest": "^29.7.0",
     "jest-environment-jsdom": "^29.7.0",
@@ -141,6 +154,7 @@
     "magicast": "^0.3.4",
     "postcss": "^8.4.31",
     "sass": "^1.61.0",
+    "storybook": "^8.3.5",
     "tailwindcss": "^3.4.4",
     "ts-node": "^10.9.2",
     "typescript": "4.9.5",

File diff suppressed because it is too large
+ 927 - 44
web/yarn.lock


Some files were not shown because too many files changed in this diff