mirror of
https://github.com/langgenius/dify.git
synced 2024-11-16 11:42:29 +08:00
feat: storybook (#9324)
This commit is contained in:
parent
423df67042
commit
5df1cb0566
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"extends": [
|
||||
"next",
|
||||
"@antfu"
|
||||
"@antfu",
|
||||
"plugin:storybook/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/consistent-type-definitions": [
|
||||
|
|
1
web/.gitignore
vendored
1
web/.gitignore
vendored
|
@ -50,3 +50,4 @@ package-lock.json
|
|||
pnpm-lock.yaml
|
||||
|
||||
.favorites.json
|
||||
*storybook.log
|
19
web/.storybook/main.ts
Normal file
19
web/.storybook/main.ts
Normal file
|
@ -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
web/.storybook/preview.tsx
Normal file
37
web/.storybook/preview.tsx
Normal file
|
@ -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
web/.storybook/storybook.css
Normal file
6
web/.storybook/storybook.css
Normal file
|
@ -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.
|
||||
|
|
107
web/app/components/base/button/index.stories.tsx
Normal file
107
web/app/components/base/button/index.stories.tsx
Normal file
|
@ -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
|
||||
|
||||
![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
|
||||
|
||||
---
|
||||
`
|
|
@ -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
|
96
web/app/components/base/chat/chat/answer/index.stories.tsx
Normal file
96
web/app/components/base/chat/chat/answer/index.stories.tsx
Normal file
|
@ -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
web/app/components/base/chat/chat/question.stories.tsx
Normal file
33
web/app/components/base/chat/chat/question.stories.tsx
Normal file
|
@ -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",
|
||||
|
|
5387
web/yarn.lock
5387
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user