| @@ -1,7 +1,8 @@ | |||
| { | |||
| "extends": [ | |||
| "next", | |||
| "@antfu" | |||
| "@antfu", | |||
| "plugin:storybook/recommended" | |||
| ], | |||
| "rules": { | |||
| "@typescript-eslint/consistent-type-definitions": [ | |||
| @@ -49,4 +49,5 @@ package-lock.json | |||
| # pmpm | |||
| pnpm-lock.yaml | |||
| .favorites.json | |||
| .favorites.json | |||
| *storybook.log | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -0,0 +1,6 @@ | |||
| html, | |||
| body { | |||
| max-width: unset; | |||
| overflow: auto; | |||
| user-select: text; | |||
| } | |||
| @@ -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. | |||
| @@ -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 | |||
| </> | |||
| ), | |||
| }, | |||
| } | |||
| @@ -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 | |||
|  | |||
| # 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 | |||
| --- | |||
| ` | |||
| @@ -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> | |||
| \`\`\` | |||
| ` | |||
| @@ -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 | |||
| @@ -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> | |||
| }, | |||
| } | |||
| @@ -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>, | |||
| }, | |||
| } | |||
| @@ -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", | |||