├── .changelogrc.js ├── .commitlintrc.js ├── .dumirc.ts ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .fatherrc.ts ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── preview.yml │ └── test.yml ├── .gitignore ├── .gitpod.yml ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .releaserc.js ├── .stylelintrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README.zh-CN.md ├── api ├── chat.ts ├── package.json └── tsconfig.json ├── demos ├── chatgpt-nextjs │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── src │ │ └── app │ │ │ ├── api │ │ │ └── openai │ │ │ │ └── route.ts │ │ │ ├── chatgpt │ │ │ └── index.tsx │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ └── tsconfig.json └── qwen-nextjs │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── src │ └── app │ │ ├── api │ │ └── qwen │ │ │ └── route.ts │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ └── tsconfig.json ├── docs ├── changelog.en-US.md ├── changelog.md ├── guide │ ├── chatItemRenderConfig.en-US.md │ ├── chatItemRenderConfig.md │ ├── chatRef.en-US.md │ ├── chatRef.md │ ├── chatgpt.en-US.md │ ├── chatgpt.md │ ├── cra.en-US.md │ ├── cra.md │ ├── demos │ │ ├── antd-configprovider.tsx │ │ ├── base.tsx │ │ ├── controled-servers-push.tsx │ │ ├── doc-mode.tsx │ │ ├── error-custom.tsx │ │ ├── error.tsx │ │ ├── mocks │ │ │ ├── fullFeature.ts │ │ │ └── streamResponse.ts │ │ ├── render-chats-nextGen.tsx │ │ ├── render-form-chats.tsx │ │ ├── renderInputArea.tsx │ │ ├── styles-chatitem.tsx │ │ ├── styles-className.tsx │ │ ├── styles-darkmode.tsx │ │ └── styles-inputarea.tsx │ ├── docs.en-US.md │ ├── docs.md │ ├── error.en-US.md │ ├── error.md │ ├── initial.en-US.md │ ├── initial.md │ ├── intro-start.en-US.md │ ├── intro-start.md │ ├── multimodal.en-US.md │ ├── multimodal.md │ ├── nextjs.en-US.md │ ├── nextjs.md │ ├── qwen.en-US.md │ ├── qwen.md │ ├── request-api-intro.en-US.md │ ├── request-api-intro.md │ ├── request-params-intro.en-US.md │ ├── request-params-intro.md │ ├── request.en-US.md │ ├── request.md │ ├── return-params-intro.en-US.md │ ├── return-params-intro.md │ ├── servers-push.en-US.md │ ├── servers-push.md │ ├── sse.en-US.md │ ├── sse.md │ ├── storage.en-US.md │ ├── storage.md │ ├── styles.en-US.md │ ├── styles.md │ ├── umi.en-US.md │ ├── umi.md │ ├── useProChat.en-US.md │ ├── useProChat.md │ ├── whyUseProChat.en-US.md │ └── whyUseProChat.md ├── index.en-US.md └── index.md ├── package.json ├── pnpm-lock.yaml ├── src ├── ActionIcon │ ├── index.tsx │ └── style.ts ├── ActionIconGroup │ └── index.tsx ├── BackBottom │ ├── demos │ │ └── index.tsx │ ├── index.en-US.md │ ├── index.md │ ├── index.tsx │ └── style.ts ├── ChatItem │ ├── components │ │ ├── Actions.tsx │ │ ├── Avatar.tsx │ │ ├── BorderSpacing.tsx │ │ ├── ErrorContent.tsx │ │ ├── Loading.tsx │ │ ├── MessageContent.tsx │ │ └── Title.tsx │ ├── demos │ │ ├── Alert.tsx │ │ ├── data.tsx │ │ └── index.tsx │ ├── index.en-US.md │ ├── index.md │ ├── index.tsx │ ├── style.ts │ ├── type.ts │ └── utils │ │ └── formatTime.ts ├── ChatList │ ├── ActionsBar.tsx │ ├── ChatListItem.tsx │ ├── HistoryDivider.tsx │ ├── ShouldUpdateItem.tsx │ ├── index.tsx │ └── style.ts ├── EditableMessage │ ├── demos │ │ └── index.tsx │ ├── index.en-US.md │ ├── index.md │ └── index.tsx ├── EditableMessageList │ ├── demos │ │ ├── data.ts │ │ └── index.tsx │ ├── index.en-US.md │ ├── index.md │ ├── index.tsx │ └── messageReducer.ts ├── Emoji │ ├── index.tsx │ └── style.ts ├── Icon │ ├── index.tsx │ └── style.ts ├── List │ ├── ListItem │ │ ├── index.tsx │ │ ├── style.ts │ │ └── time.ts │ └── index.ts ├── MessageInput │ ├── demos │ │ └── index.tsx │ ├── index.en-US.md │ ├── index.md │ ├── index.tsx │ └── style.ts ├── MessageModal │ ├── demos │ │ └── index.tsx │ ├── index.en-US.md │ ├── index.md │ └── index.tsx ├── ProChat │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── demo.test.tsx.snap │ │ ├── demo.test.tsx │ │ └── index.test.tsx │ ├── components │ │ ├── ChatList │ │ │ ├── Actions │ │ │ │ ├── Assistant.tsx │ │ │ │ ├── Error.tsx │ │ │ │ ├── Fallback.tsx │ │ │ │ ├── Function.tsx │ │ │ │ ├── User.tsx │ │ │ │ └── index.ts │ │ │ ├── Extras │ │ │ │ ├── Assistant.tsx │ │ │ │ ├── User.tsx │ │ │ │ └── index.ts │ │ │ ├── Loading.tsx │ │ │ ├── Messages │ │ │ │ ├── Assistant.tsx │ │ │ │ ├── Default.tsx │ │ │ │ ├── Hello.tsx │ │ │ │ └── index.ts │ │ │ ├── OTPInput.tsx │ │ │ ├── SkeletonList.tsx │ │ │ └── index.tsx │ │ ├── InputArea │ │ │ ├── ActionBar.tsx │ │ │ ├── AutoCompleteTextArea.tsx │ │ │ ├── StopLoading.tsx │ │ │ └── index.tsx │ │ └── ScrollAnchor │ │ │ ├── index.tsx │ │ │ └── useAtBottom.ts │ ├── const │ │ ├── message.ts │ │ └── meta.ts │ ├── container │ │ ├── App.tsx │ │ ├── OverrideStyle │ │ │ ├── global.ts │ │ │ └── index.ts │ │ ├── Provider.tsx │ │ ├── StoreUpdater.tsx │ │ └── index.tsx │ ├── demos │ │ ├── actions-chat-item.tsx │ │ ├── actions.tsx │ │ ├── bigData.tsx │ │ ├── callbacks.tsx │ │ ├── control.tsx │ │ ├── customeClassName.tsx │ │ ├── default.tsx │ │ ├── doc-mode.tsx │ │ ├── error.tsx │ │ ├── float-drawer.tsx │ │ ├── helloMessage.tsx │ │ ├── i18n.tsx │ │ ├── initialChats.tsx │ │ ├── listener.tsx │ │ ├── loading.tsx │ │ ├── meta.tsx │ │ ├── modal.tsx │ │ ├── no-stream.tsx │ │ ├── renderInputArea.tsx │ │ ├── request.tsx │ │ ├── sse-trans.tsx │ │ ├── sse.tsx │ │ ├── toBottomConfig.tsx │ │ ├── use-pro-chat.tsx │ │ └── use-ref.tsx │ ├── hooks │ │ ├── useProChat.ts │ │ ├── useProChatLocale.ts │ │ └── useRefFunction.ts │ ├── index.en-US.md │ ├── index.md │ ├── index.tsx │ ├── mocks │ │ ├── basic.ts │ │ ├── customeClassName.ts │ │ ├── fullFeature.ts │ │ ├── sseResponse.ts │ │ ├── streamResponse.ts │ │ └── threebody.ts │ ├── store │ │ ├── action.ts │ │ ├── index.ts │ │ ├── initialState.ts │ │ ├── reducers │ │ │ ├── message.test.ts │ │ │ └── message.ts │ │ ├── selectors │ │ │ ├── chat.ts │ │ │ └── index.ts │ │ └── store.ts │ ├── types │ │ ├── chat.ts │ │ ├── config.ts │ │ └── meta.ts │ └── utils │ │ ├── fetch.ts │ │ ├── merge.ts │ │ ├── message.ts │ │ ├── storeDebug.ts │ │ └── uuid.ts ├── TokenTag │ ├── demos │ │ └── index.tsx │ ├── index.en-US.md │ ├── index.md │ ├── index.tsx │ └── style.ts ├── components │ ├── Avatar │ │ ├── getEmojiByCharacter.ts │ │ ├── index.tsx │ │ └── style.ts │ ├── ControlInput.tsx │ ├── CopyButton │ │ └── index.tsx │ ├── Form │ │ ├── components │ │ │ ├── FormDivider.tsx │ │ │ ├── FormFooter.tsx │ │ │ ├── FormGroup.tsx │ │ │ ├── FormItem.tsx │ │ │ ├── FormTitle.tsx │ │ │ └── style.ts │ │ ├── index.tsx │ │ └── style.ts │ ├── Input │ │ ├── index.tsx │ │ └── style.ts │ ├── SliderWithInput │ │ └── index.tsx │ ├── Spotlight │ │ ├── index.tsx │ │ └── style.ts │ └── Tag │ │ └── index.tsx ├── hooks │ ├── languageMap.ts │ ├── useChatListActionsBar.tsx │ ├── useCopied.ts │ └── useCustomChatListAction.tsx ├── index.ts ├── locale │ ├── cs-CZ.ts │ ├── de-DE.ts │ ├── en-US.ts │ ├── hu-HU.ts │ ├── index.ts │ ├── pl-PL.ts │ ├── sk-SK.ts │ ├── zh-CN.ts │ └── zh-HK.ts ├── styles │ ├── colors.ts │ ├── index.ts │ └── stylish.ts └── types │ ├── customStylish.ts │ ├── customToken.ts │ ├── error.ts │ ├── global.d.ts │ ├── index.ts │ ├── llm.ts │ ├── locale.ts │ ├── message.ts │ └── meta.ts ├── tests ├── __snapshots__ │ └── index.test.ts.snap ├── demo.tsx ├── test-setup.ts └── utils.ts ├── tsconfig-check.json ├── tsconfig.json └── vitest.config.ts /.changelogrc.js: -------------------------------------------------------------------------------- 1 | // 详情配置查看 https://github.com/arvinxx/gitmoji-commit-workflow/tree/master/packages/changelog#readme 2 | module.exports = { 3 | displayTypes: ['feat', 'fix', 'styles', 'pref', 'build'], 4 | titleLanguage: 'zh-CN', 5 | showAuthor: true, 6 | showAuthorAvatar: true, 7 | showSummary: true, 8 | reduceHeadingLevel: true, 9 | newlineTimestamp: true, 10 | addBackToTop: true, 11 | }; 12 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['gitmoji'], 3 | }; 4 | -------------------------------------------------------------------------------- /.dumirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi'; 2 | import { homepage } from './package.json'; 3 | 4 | export default defineConfig({ 5 | favicons: [ 6 | 'https://mdn.alipayobjects.com/huamei_re70wt/afts/img/A*Mo27Sr3kS4kAAAAAAAAAAAAADmuEAQ/original', 7 | ], 8 | themeConfig: { 9 | name: '@ant-design/pro-chat', 10 | github: homepage, 11 | socialLinks: { 12 | github: 'https://github.com/ant-design/pro-chat', 13 | }, 14 | footer: 'Made with ❤️ by 蚂蚁集团 - AFX & 数字科技', 15 | logo: 'https://mdn.alipayobjects.com/huamei_re70wt/afts/img/A*Mo27Sr3kS4kAAAAAAAAAAAAADmuEAQ/original', 16 | }, 17 | mfsu: false, 18 | outputPath: 'docs-dist', 19 | html2sketch: {}, 20 | extraBabelPlugins: ['antd-style'], 21 | locales: [ 22 | { id: 'zh-CN', name: '中文' }, 23 | { id: 'en-US', name: 'EN' }, 24 | ], 25 | }); 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lambda/mock/** 2 | /scripts 3 | /config 4 | /example 5 | _test_ 6 | __test__ 7 | 8 | /node_modules 9 | jest* 10 | /es 11 | /lib 12 | /docs 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@umijs/lint/dist/config/eslint'); 2 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | esm: { output: 'es' }, 5 | }); 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '报告Bug 🐛' 3 | about: 报告 @ant-design/pro-chat 的 bug 4 | title: '🐛[BUG]' 5 | labels: '🐛 BUG' 6 | assignees: '' 7 | --- 8 | 9 | ### 🐛 bug 描述 10 | 11 | 14 | 15 | ### 📷 复现步骤 16 | 17 | 20 | 21 | ### 🏞 期望结果 22 | 23 | 26 | 27 | ### 💻 复现代码 28 | 29 | 33 | 34 | [可复现 demo](https://codesandbox.io/s/html2ksetch-demo-m53be?file=/src/Demo.tsx) 35 | 36 | ### © 版本信息 37 | 38 | - @ant-design/pro-chat 版本: [e.g. 1.0.0] 39 | - 浏览器环境 40 | - 开发环境 [e.g. mac OS] 41 | 42 | ### 🚑 其他信息 43 | 44 | 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '功能需求 ✨' 3 | about: 对 @ant-design/pro-chat 的需求或建议 4 | title: '👑 [需求]' 5 | labels: '👑 Feature' 6 | assignees: '' 7 | --- 8 | 9 | ### 🥰 需求描述 10 | 11 | 14 | 15 | ### 🧐 解决方案 16 | 17 | 20 | 21 | ### 🚑 其他信息 22 | 23 | 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '疑问或需要帮助 ❓' 3 | about: 对 @ant-design/pro-chat 使用的疑问或需要帮助 4 | title: '🧐[问题]' 5 | labels: '🧐 Question' 6 | assignees: '' 7 | --- 8 | 9 | ### 🧐 问题描述 10 | 11 | 14 | 15 | ### 💻 示例代码 16 | 17 | 20 | 21 | ### 🚑 其他信息 22 | 23 | 26 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### 💻 变更类型 | Change Type 2 | 3 | 4 | 5 | - \[ ] ✨ feat 6 | - \[ ] 🐛 fix 7 | - \[ ] 💄 style 8 | - \[ ] 🔨 chore 9 | - \[ ] 📝 docs 10 | 11 | #### 🔀 变更说明 | Description of Change 12 | 13 | 14 | 15 | #### 📝 补充信息 | Additional Information 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/preview.yml: -------------------------------------------------------------------------------- 1 | name: Surge PR Preview 2 | 3 | on: 4 | pull_request: 5 | 6 | workflow_dispatch: 7 | 8 | jobs: 9 | preview: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Install pnpm 15 | uses: pnpm/action-setup@v4 16 | 17 | - uses: afc163/surge-preview@v1 18 | id: preview_step 19 | with: 20 | surge_token: ${{ secrets.SURGE_TOKEN }} 21 | github_token: ${{ secrets.GITHUB_TOKEN }} 22 | build: | 23 | pnpm i 24 | pnpm run docs:build 25 | dist: docs-dist 26 | 27 | - name: Get the preview_url 28 | run: echo "url => ${{ steps.preview_step.outputs.preview_url }}" 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test CI 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | 7 | steps: 8 | - uses: actions/checkout@v3 9 | 10 | - name: Install pnpm 11 | uses: pnpm/action-setup@v4 12 | 13 | - name: Setup Node.js environment 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: '18' 17 | 18 | - name: Install deps 19 | run: pnpm install 20 | 21 | - name: lint 22 | run: pnpm run ci 23 | 24 | - name: Test and coverage 25 | run: pnpm run test:coverage 26 | 27 | - name: Upload coverage to Codecov 28 | uses: codecov/codecov-action@v3 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | 6 | # production 7 | **/dist 8 | **/docs-dist 9 | /.vscode 10 | /es 11 | /lib 12 | 13 | # misc 14 | .DS_Store 15 | storybook-static 16 | npm-debug.log* 17 | yarn-error.log 18 | 19 | /coverage 20 | .idea 21 | package-lock.json 22 | *bak 23 | .vscode 24 | 25 | # visual studio code 26 | .history 27 | *.log 28 | functions/* 29 | lambda/mock/index.js 30 | .temp/** 31 | 32 | # umi 33 | .dumi/tmp* 34 | 35 | # screenshot 36 | screenshot 37 | .firebase 38 | example/.temp/* 39 | .eslintcache 40 | techUI* 41 | .vercel 42 | bun.lockb 43 | yarn.lock -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: pnpm install 3 | command: pnpm run start 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | resolution-mode=highest 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | .umi 3 | .umi-production 4 | /dist 5 | .dockerignore 6 | .DS_Store 7 | .eslintignore 8 | *.png 9 | *.toml 10 | docker 11 | .editorconfig 12 | Dockerfile* 13 | .gitignore 14 | .prettierignore 15 | LICENSE 16 | .eslintcache 17 | *.lock 18 | yarn-error.log 19 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pluginSearchDirs: false, 3 | plugins: [ 4 | require.resolve('prettier-plugin-organize-imports'), 5 | require.resolve('prettier-plugin-packagejson'), 6 | ], 7 | printWidth: 100, 8 | proseWrap: 'never', 9 | singleQuote: true, 10 | trailingComma: 'all', 11 | }; 12 | -------------------------------------------------------------------------------- /.releaserc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['semantic-release-config-gitmoji'], 3 | }; 4 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@umijs/lint/dist/config/stylelint'); 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ant Design 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api/chat.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIStream, StreamingTextResponse } from 'ai'; 2 | import OpenAI from 'openai'; 3 | 4 | export const config = { 5 | runtime: 'edge', 6 | }; 7 | 8 | export default async (req: Request) => { 9 | const openai = new OpenAI(); 10 | const payload = (await req.json()) as any; 11 | const { messages, ...params } = payload; 12 | 13 | const formatMessages = messages.map((m) => ({ 14 | content: m.content, 15 | name: m.name, 16 | role: m.role, 17 | })); 18 | const response = await openai.chat.completions.create( 19 | { 20 | messages: formatMessages, 21 | ...params, 22 | stream: true, 23 | }, 24 | { headers: { Accept: '*/*' } }, 25 | ); 26 | const stream = OpenAIStream(response); 27 | return new StreamingTextResponse(stream); 28 | }; 29 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "0.0.0", 4 | "description": "test openai api for pro chat", 5 | "dependencies": { 6 | "ai": "^2", 7 | "openai": "^4" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": false, 4 | "declaration": false, 5 | "skipLibCheck": true, 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demos/chatgpt-nextjs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /demos/chatgpt-nextjs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /demos/chatgpt-nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | transpilePackages: [ 4 | '@ant-design/pro-editor', 5 | '@ant-design/pro-chat', 6 | 'react-intersection-observer', 7 | ], 8 | }; 9 | 10 | module.exports = nextConfig; 11 | -------------------------------------------------------------------------------- /demos/chatgpt-nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "lint": "next lint", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@ai-sdk/openai": "^0.0.33", 13 | "@ant-design/pro-chat": "^1.2.0", 14 | "@ant-design/pro-editor": "^1.2.1", 15 | "ai": "^3.2.0", 16 | "antd": "^5.12.8", 17 | "antd-style": "^3.6.1", 18 | "next": "14.0.4", 19 | "openai": "^4.24.7", 20 | "react": "^18", 21 | "react-dom": "^18" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^20", 25 | "@types/react": "^18", 26 | "@types/react-dom": "^18", 27 | "eslint": "^8", 28 | "eslint-config-next": "14.0.4", 29 | "typescript": "^5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /demos/chatgpt-nextjs/src/app/api/openai/route.ts: -------------------------------------------------------------------------------- 1 | import { createOpenAI } from '@ai-sdk/openai'; 2 | import { StreamingTextResponse, streamText } from 'ai'; 3 | 4 | export async function POST(request: Request) { 5 | const { messages = [] }: Partial<{ messages: Array }> = await request.json(); 6 | 7 | const PickMessages = messages.map((message) => { 8 | return { 9 | role: message.role, 10 | content: message.content, 11 | }; 12 | }); 13 | 14 | const openai = createOpenAI({ 15 | // custom settings, e.g. 16 | apiKey: 'OpenAI Key', // your openai key 17 | baseURL: 'base url', // if u dont need change baseUrl,you can delete this line 18 | compatibility: 'compatible', 19 | }); 20 | const stream = await streamText({ 21 | model: openai('gpt-3.5-turbo'), 22 | messages: [...PickMessages], 23 | }); 24 | return new StreamingTextResponse(stream.textStream); 25 | } 26 | -------------------------------------------------------------------------------- /demos/chatgpt-nextjs/src/app/chatgpt/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { ProChat } from '@ant-design/pro-chat'; 3 | import { useTheme } from 'antd-style'; 4 | import { useEffect, useState } from 'react'; 5 | 6 | export default function Home() { 7 | const theme = useTheme(); 8 | const [showComponent, setShowComponent] = useState(false); 9 | 10 | useEffect(() => setShowComponent(true), []); 11 | 12 | return ( 13 |
18 | {showComponent && ( 19 | { 25 | const response = await fetch('/api/openai', { 26 | method: 'POST', 27 | body: JSON.stringify({ messages: messages }), 28 | }); 29 | return response; 30 | }} 31 | /> 32 | )} 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /demos/chatgpt-nextjs/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Inter } from 'next/font/google'; 3 | import './globals.css'; 4 | 5 | const inter = Inter({ subsets: ['latin'] }); 6 | 7 | export const metadata: Metadata = { 8 | title: 'Create Next App', 9 | description: 'Generated by create next app', 10 | }; 11 | 12 | export default function RootLayout({ children }: { children: React.ReactNode }) { 13 | return ( 14 | 15 | {children} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /demos/chatgpt-nextjs/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Page from './chatgpt'; 2 | 3 | const Index = () => ( 4 | <> 5 | 6 | 7 | ); 8 | 9 | export default Index; 10 | -------------------------------------------------------------------------------- /demos/chatgpt-nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /demos/qwen-nextjs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /demos/qwen-nextjs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /demos/qwen-nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | transpilePackages: [ 4 | '@ant-design/pro-editor', 5 | '@ant-design/pro-chat', 6 | 'react-intersection-observer', 7 | ], 8 | }; 9 | 10 | module.exports = nextConfig; 11 | -------------------------------------------------------------------------------- /demos/qwen-nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qwen-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "lint": "next lint", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@ant-design/pro-chat": "^1.2.0", 13 | "@ant-design/pro-editor": "^0.38.0", 14 | "antd": "^5.12.8", 15 | "antd-style": "^3.6.1", 16 | "next": "14.0.4", 17 | "react": "^18", 18 | "react-dom": "^18" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^20", 22 | "@types/react": "^18", 23 | "@types/react-dom": "^18", 24 | "eslint": "^8", 25 | "eslint-config-next": "14.0.4", 26 | "typescript": "^5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demos/qwen-nextjs/src/app/api/qwen/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function POST(request: Request) { 4 | const { messages = [] }: Partial<{ messages: Array }> = await request.json(); 5 | try { 6 | const apiKey = 'Your-Api-Key'; // 你的 API 密钥 7 | const response = await fetch( 8 | 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', 9 | { 10 | method: 'POST', 11 | headers: { 12 | Authorization: 'Bearer ' + apiKey, 13 | 'Content-Type': 'application/json', 14 | }, 15 | body: JSON.stringify({ 16 | model: 'qwen-turbo', 17 | input: { 18 | messages: [ 19 | { 20 | role: 'system', 21 | content: 'You are a helpful assistant.', 22 | }, 23 | ...messages, 24 | ], 25 | }, 26 | parameters: {}, 27 | }), 28 | }, 29 | ); 30 | const data = await response.json(); 31 | return NextResponse.json(data); 32 | } catch (error) { 33 | return NextResponse.error(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /demos/qwen-nextjs/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Inter } from 'next/font/google'; 3 | import './globals.css'; 4 | 5 | const inter = Inter({ subsets: ['latin'] }); 6 | 7 | export const metadata: Metadata = { 8 | title: 'Create Next App', 9 | description: 'Generated by create next app', 10 | }; 11 | 12 | export default function RootLayout({ children }: { children: React.ReactNode }) { 13 | return ( 14 | 15 | {children} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /demos/qwen-nextjs/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useState, useEffect } from 'react' 3 | import { ProChat } from '@ant-design/pro-chat'; 4 | import { useTheme } from 'antd-style'; 5 | 6 | export default function Home() { 7 | 8 | const theme = useTheme(); 9 | const [showComponent, setShowComponent] = useState(false) 10 | 11 | useEffect(() => setShowComponent(true), []) 12 | 13 | return ( 14 |
19 | { 20 | showComponent && { 26 | const response = await fetch('/api/qwen', { 27 | method: 'POST', 28 | body: JSON.stringify({ messages: messages }), 29 | }); 30 | const data = await response.json(); 31 | return new Response(data.output?.text); 32 | }} 33 | /> 34 | } 35 |
36 | ); 37 | } 38 | 39 | -------------------------------------------------------------------------------- /demos/qwen-nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /docs/changelog.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Changelog 3 | nav: 4 | title: Changelog 5 | order: 999 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 更新日志 3 | nav: 4 | title: 更新日志 5 | order: 999 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/guide/chatItemRenderConfig.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 深度自定义对话内容 3 | order: 21 4 | group: 5 | title: 使用案例 6 | nav: 7 | title: 文档 8 | order: 0 9 | --- 10 | 11 | ## 深度自定义对话内容 12 | 13 | 很多时候业务场景下并不会那么理想,永远返回一些数据。 14 | 15 | 他可能会想要展示一个图表、甚至是一个表单,或者任意一个带有业务属性的 React 组件,这个时候该怎么办? 16 | 17 | 我们提供了一个 `chatItemRenderConfig` 的 api 来帮助你解决这一类问题。 18 | 19 | ## chatItemRenderConfig 参数说明 20 | 21 | 这个 `api` 包含 5 个参数 22 | 23 | - `titleRender`: 标题渲染函数 24 | - `contentRender`: 内容渲染函数 25 | - `actionsRender`: 操作渲染函数 26 | - `avatarRender`: 头像渲染函数 27 | - `render`: 自定义渲染函数 28 | 29 | ## 使用案例 30 | 31 | 以下是如何在ProChat组件中使用 `chatItemRenderConfig` 的示例: 32 | 33 | ### 特殊通知 34 | 35 | 36 | 37 | 在上述代码中,我们通过设置 `render` 函数来自定义通知类消息的展示方式。当检测到消息来源角色为 `notification`时,将默认展示替换成带有警告图标和信息提示的 `Alert` 组件。 38 | 39 | ### 表单提交 40 | 41 | 42 | 43 | 在上述代码中,我们通过设置 `contentRender` 函数配合特殊的元数据,做到了代入参数让 AI 执行填写表单的逻辑。 44 | 45 | ### 深度交互逻辑 46 | 47 | 48 | 49 | 这个代码展示了一个非常特殊的交互场景,你可以依靠上下文非常动态的调整你需要的渲染内容。 50 | 51 | ## 源码中影响范围 52 | 53 | 源码里面关于如何根据配置对象 `chatItemRenderConfig` 中提供的相关渲染方法来构建每个聊天项(即每条消息)。以下是关键点解释: 54 | 55 | 1. 对于头像 (`avatar`)、标题 (`title`)、内容 (`content`) 和操作按钮 (`actions`) 等部分均有独立的 useMemo 钩子进行处理,并且可通过相应参数进行自定义。 56 | 2. 如果提供了总体自定义渲染方法 (`render`) 并且返回非空结果,则会优先使用该方法直接返回最终 DOM 结构。 57 | 3. 所有 useMemo 钩子和最后返回结构都依赖于外部传入的 `chatItemRenderConfig` 参数。 58 | 59 | 注意:若要正确地使用此API,请确保你对React以及Ant Design库有一定程度上的理解,并且能够编写相应类型符合预期要求与逻辑业务场景匹配的自定义函数。 60 | -------------------------------------------------------------------------------- /docs/guide/chatRef.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: chatRef 3 | group: 4 | title: Hooks 5 | order: 100 6 | nav: 7 | title: Documents 8 | order: 0 9 | --- 10 | -------------------------------------------------------------------------------- /docs/guide/chatRef.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: chatRef 3 | group: 4 | title: Hooks 5 | order: 100 6 | nav: 7 | title: 文档 8 | order: 0 9 | --- 10 | -------------------------------------------------------------------------------- /docs/guide/demos/base.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ProChat } from '@ant-design/pro-chat'; 5 | 6 | import { useTheme } from 'antd-style'; 7 | 8 | export default () => { 9 | const theme = useTheme(); 10 | return ( 11 |
12 | { 17 | const mockedData: string = `这是一段模拟的对话数据。本次会话传入了${messages.length}条消息`; 18 | return new Response(mockedData); 19 | }} 20 | /> 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /docs/guide/demos/controled-servers-push.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ChatMessage, ProChat } from '@ant-design/pro-chat'; 5 | 6 | import { Alert } from 'antd'; 7 | import { useTheme } from 'antd-style'; 8 | import { useEffect, useState } from 'react'; 9 | 10 | import { example } from './mocks/fullFeature'; 11 | import { MockResponse } from './mocks/streamResponse'; 12 | 13 | export default () => { 14 | const theme = useTheme(); 15 | const [chats, setChats] = useState>[]>( 16 | Object.values(example.initialChats), 17 | ); 18 | useEffect(() => { 19 | setTimeout(() => { 20 | setChats([ 21 | ...chats, 22 | { 23 | id: 'VbtDpzsi', 24 | content: `当前对话剩余的 token 数量为 100`, 25 | role: 'notification', 26 | }, 27 | ]); 28 | }, 3000); 29 | }, []); 30 | 31 | return ( 32 |
33 | { 36 | setChats(chats); 37 | }} 38 | chatItemRenderConfig={{ 39 | render: (item, dom, defaultDom) => { 40 | if (item?.originData?.role === 'notification') { 41 | return ( 42 |
49 | 50 |
51 | ); 52 | } 53 | return defaultDom; 54 | }, 55 | }} 56 | request={async (messages) => { 57 | const mockedData: string = `这是一段模拟的流式字符串数据。本次会话传入了${messages.length}条消息`; 58 | 59 | const mockResponse = new MockResponse(mockedData, 100); 60 | 61 | return mockResponse.getResponse(); 62 | }} 63 | /> 64 |
65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /docs/guide/demos/doc-mode.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * iframe: 800 3 | */ 4 | import { ProChat } from '@ant-design/pro-chat'; 5 | import { useTheme } from 'antd-style'; 6 | 7 | import { example } from './mocks/fullFeature'; 8 | import { MockResponse } from './mocks/streamResponse'; 9 | 10 | export default () => { 11 | const theme = useTheme(); 12 | return ( 13 |
14 | { 18 | const mockedData: string = `这是一段模拟的流式字符串数据。本次会话传入了${messages.length}条消息`; 19 | 20 | const mockResponse = new MockResponse(mockedData, 100); 21 | 22 | return mockResponse.getResponse(); 23 | }} 24 | chats={example.chats} 25 | config={example.config} 26 | /> 27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /docs/guide/demos/error-custom.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | * iframe: 1000 4 | */ 5 | import { ProChat, ProChatInstance } from '@ant-design/pro-chat'; 6 | import { Button, Card, Result } from 'antd'; 7 | import { useTheme } from 'antd-style'; 8 | import { useEffect, useRef } from 'react'; 9 | import { MockResponse } from './mocks/streamResponse'; 10 | 11 | export default () => { 12 | const theme = useTheme(); 13 | 14 | const chatRef1 = useRef(); 15 | 16 | useEffect(() => { 17 | setTimeout(() => { 18 | chatRef1.current.sendMessage('Hello!'); 19 | }, 500); 20 | }, []); 21 | 22 | return ( 23 | <> 24 |
25 | { 27 | const mockResponse = new MockResponse('', 1000, true); 28 | return mockResponse.getResponse(); 29 | }} 30 | chatRef={chatRef1} 31 | style={{ height: 500 }} 32 | renderErrorMessages={(errorResponse) => { 33 | return ( 34 | 35 | 41 | Try Again 42 | , 43 | , 44 | ]} 45 | /> 46 | 47 | ); 48 | }} 49 | /> 50 |
51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /docs/guide/demos/error.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ProChat, ProChatInstance } from '@ant-design/pro-chat'; 5 | import { useTheme } from 'antd-style'; 6 | import { useEffect, useRef } from 'react'; 7 | 8 | export default () => { 9 | const theme = useTheme(); 10 | 11 | const chatRef1 = useRef(); 12 | 13 | useEffect(() => { 14 | chatRef1.current.sendMessage('Hello!'); 15 | }); 16 | 17 | return ( 18 | <> 19 |
20 | { 23 | return new Response('token 长度超限', { status: 500, statusText: 'limited of token' }); 24 | }} 25 | /> 26 |
27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /docs/guide/demos/mocks/streamResponse.ts: -------------------------------------------------------------------------------- 1 | export class MockResponse { 2 | private controller!: ReadableStreamDefaultController; 3 | private encoder = new TextEncoder(); 4 | private stream: ReadableStream; 5 | private error: boolean; 6 | 7 | constructor( 8 | private data: string, 9 | private delay: number = 300, 10 | error: boolean = false, // 新增参数,默认为false 11 | ) { 12 | this.error = error; 13 | 14 | this.stream = new ReadableStream({ 15 | start: (controller) => { 16 | this.controller = controller; 17 | if (!this.error) { 18 | // 如果不是错误情况,则开始推送数据 19 | setTimeout(() => this.pushData(), this.delay); // 延迟开始推送数据 20 | } 21 | }, 22 | cancel(reason) { 23 | console.log('Stream canceled', reason); 24 | }, 25 | }); 26 | } 27 | 28 | private pushData() { 29 | if (this.data.length === 0) { 30 | this.controller.close(); 31 | return; 32 | } 33 | 34 | const characters = Array.from(this.data); 35 | if (characters.length === 0) { 36 | this.controller.close(); 37 | return; 38 | } 39 | 40 | const chunk = characters.shift(); 41 | this.data = characters.join(''); 42 | 43 | this.controller.enqueue(this.encoder.encode(chunk)); 44 | 45 | if (this.data.length > 0) { 46 | setTimeout(() => this.pushData(), this.delay); 47 | } else { 48 | // 数据全部发送完毕后关闭流 49 | setTimeout(() => this.controller.close(), this.delay); 50 | } 51 | } 52 | 53 | getResponse(): Promise { 54 | return new Promise((resolve) => { 55 | // 使用setTimeout来模拟网络延迟 56 | setTimeout(() => { 57 | if (this.error) { 58 | const errorResponseOptions = { status: 500, statusText: 'Internal Server Error' }; 59 | 60 | // 返回模拟的网络错误响应,这里我们使用500状态码作为示例 61 | resolve(new Response(null, errorResponseOptions)); 62 | } else { 63 | resolve(new Response(this.stream)); 64 | } 65 | }, this.delay); // 使用构造函数中设置的delay值作为延迟时间 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docs/guide/demos/renderInputArea.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | * iframe: 800 4 | */ 5 | import { PlusOutlined } from '@ant-design/icons'; 6 | import { ProChat } from '@ant-design/pro-chat'; 7 | import { Button, Form, Input, Space, Upload, message } from 'antd'; 8 | import { useTheme } from 'antd-style'; 9 | import React from 'react'; 10 | 11 | export default () => { 12 | const theme = useTheme(); 13 | 14 | const inputAreaRender = ( 15 | _: React.ReactNode, 16 | onMessageSend: (message: string) => void | Promise, 17 | onClear: () => void, 18 | ) => { 19 | return ( 20 |
{ 22 | const { question, files } = value; 23 | const FilesBase64List = files?.fileList.map( 24 | (file: any) => `![${file.name}](${file.thumbUrl})`, 25 | ); 26 | const Prompt = `${question} ${FilesBase64List?.join('\n')}`; 27 | await onMessageSend(Prompt); 28 | }} 29 | initialValues={{ question: '下面的图片是什么意思?' }} 30 | > 31 | 36 | 37 | 38 | 39 | 44 | { 47 | if (file.type === 'image/png') { 48 | return true; 49 | } else { 50 | message.error('请上传png格式的图片'); 51 | return Upload.LIST_IGNORE; 52 | } 53 | }} 54 | action="https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188" 55 | > 56 | 60 | 61 | 62 | 63 | 64 | 65 | 68 | 71 | 72 | 73 |
74 | ); 75 | }; 76 | 77 | return ( 78 |
79 | 80 |
81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /docs/guide/demos/styles-chatitem.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ChatMessage, ProChat } from '@ant-design/pro-chat'; 5 | import { useTheme } from 'antd-style'; 6 | import { useState } from 'react'; 7 | import { example } from './mocks/fullFeature'; 8 | 9 | export default () => { 10 | const theme = useTheme(); 11 | const [chats, setChats] = useState>[]>(example.initialChats); 12 | 13 | return ( 14 |
15 | { 18 | setChats(chats); 19 | }} 20 | chatItemRenderConfig={{ 21 | contentRender: (_, defaultContent) => { 22 | return ( 23 |
29 | {defaultContent} 30 |
31 | ); 32 | }, 33 | }} 34 | request={async (messages) => { 35 | const mockedData: string = `这是一段模拟的对话数据。本次会话传入了${messages.length}条消息`; 36 | return new Response(mockedData); 37 | }} 38 | /> 39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /docs/guide/demos/styles-className.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ChatMessage, ProChat } from '@ant-design/pro-chat'; 5 | import { css, cx, useTheme } from 'antd-style'; 6 | import { useState } from 'react'; 7 | import { example } from './mocks/fullFeature'; 8 | 9 | export default () => { 10 | const theme = useTheme(); 11 | const [chats, setChats] = useState>[]>(example.initialChats); 12 | 13 | const CustomClassName = cx( 14 | css(` 15 | .ant-pro-chat-list-item-message-content{ 16 | background-color: rgb(51 221 19 / 24%); 17 | } 18 | `), 19 | ); 20 | 21 | return ( 22 |
23 | { 26 | setChats(chats); 27 | }} 28 | chatItemRenderConfig={{ 29 | contentRender: (_, defaultContent) => { 30 | return ( 31 |
37 | {defaultContent} 38 |
39 | ); 40 | }, 41 | }} 42 | request={async (messages) => { 43 | const mockedData: string = `这是一段模拟的对话数据。本次会话传入了${messages.length}条消息`; 44 | return new Response(mockedData); 45 | }} 46 | /> 47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /docs/guide/demos/styles-darkmode.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ChatMessage, ProChat } from '@ant-design/pro-chat'; 5 | import { theme } from 'antd'; 6 | import { ThemeProvider } from 'antd-style'; 7 | import { useState } from 'react'; 8 | import { example } from './mocks/fullFeature'; 9 | 10 | export default () => { 11 | const [chats, setChats] = useState>[]>(example.initialChats); 12 | 13 | return ( 14 | 25 |
26 | { 29 | setChats(chats); 30 | }} 31 | request={async (messages) => { 32 | const mockedData: string = `这是一段模拟的对话数据。本次会话传入了${messages.length}条消息`; 33 | return new Response(mockedData); 34 | }} 35 | /> 36 |
37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /docs/guide/demos/styles-inputarea.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ProChat } from '@ant-design/pro-chat'; 5 | 6 | import { useTheme } from 'antd-style'; 7 | 8 | export default () => { 9 | const theme = useTheme(); 10 | return ( 11 |
12 | { 24 | const mockedData: string = `这是一段模拟的对话数据。本次会话传入了${messages.length}条消息`; 25 | return new Response(mockedData); 26 | }} 27 | /> 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /docs/guide/docs.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Details of Document Mode and Normal Mode 3 | order: 25 4 | group: 5 | title: Use Cases 6 | nav: 7 | title: Documents 8 | order: 0 9 | --- 10 | 11 | # Details of Document Mode and Normal Mode 12 | 13 | > Working on Progress 14 | -------------------------------------------------------------------------------- /docs/guide/docs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 文档模式和普通模式的细节 3 | order: 25 4 | group: 5 | title: 使用案例 6 | nav: 7 | title: 文档 8 | order: 0 9 | --- 10 | 11 | # 文档模式和普通模式的细节 12 | 13 | > Working on Progress 14 | -------------------------------------------------------------------------------- /docs/guide/error.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Error handling 3 | order: 99 4 | group: 5 | title: Get Started 6 | order: 2 7 | --- 8 | 9 | # Error handling 10 | 11 | > Working on Progress 12 | -------------------------------------------------------------------------------- /docs/guide/error.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 错误处理 3 | order: 99 4 | group: 5 | title: 快速上手 6 | order: 2 7 | --- 8 | 9 | # 错误处理 10 | 11 | > Working on Progress 12 | -------------------------------------------------------------------------------- /docs/guide/intro-start.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Get Started 3 | group: 4 | title: Get Started 5 | order: 0 6 | nav: 7 | title: Documents 8 | order: 0 9 | --- 10 | 11 | # Get started 12 | 13 | ProEditor is positioned as a component library for the front-end to quickly build dialogue content in Chat conversation mode 14 | 15 | ## Install 16 | 17 | ```bash 18 | # @ant-design/pro-editor based on antd and antd-style,which need to be installed in the project 19 | $ npm install antd antd-style -S 20 | $ npm install @ant-design/pro-chat -S 21 | ``` 22 | 23 | Due to the underlying dependency on antd, there are version requirements 24 | 25 | ```json 26 | "peerDependencies": { 27 | "antd": "^5", 28 | "antd-style": "^3", 29 | "react": "^18" 30 | }, 31 | ``` 32 | 33 | ### Using ProChat components 34 | 35 | The most critical component provided by ProChat is the ProChat component, which you can easily use. 36 | 37 | 38 | 39 | ### 🚧 Using atomization ability 40 | 41 | > Working in Progress At present, this part of the capability is still under high-speed development, please stay tuned。 42 | 43 | ProChat will provide a series of atomized components in the future. In special cases, you may want to use some independent components in ProChat. We will also provide similar components to help you better build applications. 44 | 45 | If you have more ideas and needs,Welcome to [Issue](https://github.com/ant-design/pro-chat/issues) and contact us on [Discussions](https://github.com/ant-design/pro-chat/discussions) 46 | 47 | > Our next plan is to provide a complex model parameter panel: Welcome to watch [「RFC」New Component:ModalConfig Model Parameters Panel](https://github.com/ant-design/pro-chat/discussions/58) 48 | 49 | Some of the underlying components, such as Markdown, Highlight, etc., we rely on the [ProEditor UI Component Library](https://github.com/ant-design/pro-editor)So if you also have a scenario of building an editor, you can come here to take a look. 50 | 51 | ## Engineering capability 52 | 53 | ### On demand loading 54 | 55 | ProChat supports tree Shaking based on ES modules by default, directly introducing `import {ProChat} from' @ ant design/pro chat` ; There will be an on-demand loading effect. 56 | 57 | ### TypeScript 58 | 59 | ProChat is developed using TypeScript, thus providing a complete type definition. 60 | -------------------------------------------------------------------------------- /docs/guide/intro-start.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 快速开始 3 | group: 4 | title: 快速上手 5 | order: 0 6 | nav: 7 | title: 文档 8 | order: 0 9 | --- 10 | 11 | # 快速开始 12 | 13 | ProEditor 定位为 Chat 对话模式下,给前端提供快速搭建对话内容的组件库 14 | 15 | ## 安装 16 | 17 | ```bash 18 | # @ant-design/pro-editor 基于 antd 和 antd-style,需要在项目中安装 19 | $ npm install antd antd-style -S 20 | $ npm install @ant-design/pro-chat -S 21 | ``` 22 | 23 | 因为底层依赖于 antd ,因此对版本有所要求 24 | 25 | ```json 26 | "peerDependencies": { 27 | "antd": "^5", 28 | "antd-style": "^3", 29 | "react": "^18" 30 | }, 31 | ``` 32 | 33 | ### 使用 ProChat 组件 34 | 35 | ProChat 提供的最关键的组件就是 ProChat 组件,你可以你可以非常简单的使用它。 36 | 37 | 38 | 39 | ### 🚧 使用原子化能力 40 | 41 | > Working in Progress 当前该部分能力仍处于高速开发中,敬请期待。 42 | 43 | ProChat 后续会提供一系列原子化的组件,在特殊情况下你可能会想要使用某些 ProChat 中的独立组件,这些需求我们也会提供类似的组件来帮助大家更好的搭建应用。 44 | 45 | 如果你有更多想法和需求,欢迎来 [Issue](https://github.com/ant-design/pro-chat/issues) 和 [讨论区](https://github.com/ant-design/pro-chat/discussions) 和我们沟通! 46 | 47 | > 我们下一个计划是提供一个复杂的模型参数面板:欢迎围观 [「RFC」New Component:ModalConfig 模型参数面板](https://github.com/ant-design/pro-chat/discussions/58) 48 | 49 | 有部分的底层组件,例如 Markdown、Highlight 之类的,我们会依赖于 [ProEditor - 编辑器 UI 组件库](https://github.com/ant-design/pro-editor),因此如果你也有搭建编辑器的场景,可以来这里看看。 50 | 51 | ## 工程化能力 52 | 53 | ### 按需加载 54 | 55 | ProChat 默认支持基于 ES modules 的 tree shaking,直接引入 `import { ProChat } from '@ant-design/pro-chat`; 就会有按需加载的效果。 56 | 57 | ### TypeScript 58 | 59 | ProChat 使用 TypeScript 进行开发,因此提供了完整的类型定义。 60 | -------------------------------------------------------------------------------- /docs/guide/multimodal.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 多模态怎么接入 3 | order: 19 4 | group: 5 | title: 使用案例 6 | nav: 7 | title: 文档 8 | order: 0 9 | --- 10 | 11 | # 多模态怎么接入 12 | 13 | 一开始我们打算直接让 InputArea(即下方的输入框)支持上传各种各样的文件 14 | 15 | 但是一旦这么设计,就会导致更多的问题 16 | 17 | - 图片我们是直接转 Base64 还是 Cdn?如果是 Cdn 是不是还需要提供一个 Cdn 的 api 给开发者? 18 | - 图片还好说,但是除开图片之外的其他文件呢?各种文件是否需要预览? 19 | - 这些文件到底以怎么样的形式拼接到 Prompt 中去呢?怎么定义这个 Prompt 的位置? 20 | 21 | 等等这些设计细节数不甚数,而且对于一些模型来说,并不一定支持多模态,默认提供分析下来并不是一个好的设计。 22 | 23 | ## 自定义输入部分 24 | 25 | 我们提供了一个 inputAreaRender 的 api,来帮助你对多模态的情况下进行支持,以及和 ProChat 的数据流进行接入和交互 26 | 27 | ```ts 28 | inputAreaRender?: ( 29 | defaultDom: ReactNode, 30 | onMessageSend: (message: string) => void | Promise, 31 | onClearAllHistory: () => void, 32 | ) => ReactNode; 33 | ``` 34 | 35 | inputAreaRender 共有三个参数: 36 | 37 | - defaultDom :即默认渲染的 dom,你如果是想包裹或者添加一些小内容,可以直接在这个基础上进行组合 38 | - onMessageSend :发送数据的方法,这个方法和 ProChat.sendMessage(Hooks) 本质上是一个方法,用于向 ProChat 的数据流发送一条数据 39 | - onClearAllHistory : 清空当前对话的方法,这个方法和 ProChat.clearMessage(Hooks) 本质上是一个方法 40 | 41 | 这下子你就可以随意组合当前的内容,以及你打算做的各种需求,例如:阻止一些不好的对话、上传内容的前置校验等 42 | 43 | ## 一个图片上传的演示案例 44 | 45 | 46 | 47 | 我们来详细拆解下这个案例 48 | 49 | ### 默认使用Base64 50 | 51 | 案例中使用了 antd 的 Upload 组件,我们可以轻易拿到当前内容的 Base64,然后在 onMessageSend 将其进行组合 52 | 53 | 如果你想用 CDN 代替 Base64,你需要做的事情就是在数据流上做处理。 54 | 55 | > 下面这个改动是建立在,Upload 组件配置的 actions 接口如果有 response 返回,里面有一个 cdnUrl 返回告诉当前文件上传完毕后的 Cdn 链接在哪里 56 | 57 | ```js 58 | onFinish={async (value) => { 59 | const { question, files } = value; 60 | const FilesCdnList = files?.fileList.map( 61 | (file: any) => `![${file.name}](${file.response.cdnUrl})`, 62 | ); 63 | const Prompt = `${question} ${FilesCdnList?.join('\n')}`; 64 | await onMessageSend(Prompt); 65 | }} 66 | ``` 67 | 68 | ### 非图片的内容支持 69 | 70 | 可以看到,本质上预览是依赖于 Markdown 的预览能力进行支持的,如果遇到了内容的文件,我们建议采用 `` 来进行渲染,然后使用 messageItemExtraRender 在下方进行额外文件的预览渲染 71 | 72 | > 其实 Markdown 是支持 Html 渲染的,但是我们默认并没有开启这个能力,考虑各方面我们并不打算默认打开这个,我们建议你采用 messageItemExtraRender 73 | 74 | ```ts 75 | messageItemExtraRender: (message: ChatMessage, type: 'assistant' | 'user') => React.ReactNode; 76 | ``` 77 | 78 | messageItemExtraRender 可以拿到当前的 message,可以做很多自定义渲染的工作。 79 | -------------------------------------------------------------------------------- /docs/guide/servers-push.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: The method of server push 3 | order: 20 4 | group: 5 | title: Use Cases 6 | nav: 7 | title: Documents 8 | order: 0 9 | --- 10 | 11 | ## The method of server push 12 | 13 | Many times, we need scenarios for server-side push, such as: 14 | 15 | -The token limit has been reached, and users need to be prompted to recharge -Some of the calls behind FunctionCall will be executed for a long time, and a message will be pushed after the execution 16 | 17 | In this case, it is somewhat different from a regular Request or SSE, and even the triggering time may not necessarily be related to the conversation request 18 | 19 | This problem can be simplified as: 20 | 21 | -How to send/receive a piece of content (timing uncertain) 22 | 23 | ProChat is very flexible in this situation, and we offer several methods to help you 24 | 25 | ### Controlled mode 26 | 27 | In this case, we waited for 3 seconds before actively pushing a notification, and then controlled it through chats and placed it in the last piece of content 28 | 29 | > In this case, we also used the render in chatitemRenderConfig to customize a special format of information. For more information about this API, please refer to [Deep Customization Conversation Content](./chatItemRenderConfig.md) 30 | 31 | 32 | 33 | ### Hooks sendMessage 34 | 35 | > Working on Progress 36 | -------------------------------------------------------------------------------- /docs/guide/servers-push.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 服务端推送的方式 3 | order: 20 4 | group: 5 | title: 使用案例 6 | nav: 7 | title: 文档 8 | order: 0 9 | --- 10 | 11 | ## 服务端推送的方式 12 | 13 | 很多时候,我们会需要一些服务端推送的场景,例如: 14 | 15 | - 使用 Token 上限了,需要提示用户去充值 16 | - 有一些背后调用了 FunctionCall 会执行很久,执行结束后 Push 一条内容 17 | 18 | 这种情况下和普通的 Request 或 SSE 有些不一样,甚至触发时机都不一定和对话请求有关 19 | 20 | 这个问题可以简化为: 21 | 22 | - 如何 发送/接收 一条内容(时机不确定) 23 | 24 | ProChat 在这种情况下很灵活,我们提供几种方法来帮助你 25 | 26 | ### 受控模式 27 | 28 | 在这个案例里面,我们等待 3s 后主动推送了一条通知,然后通过 chats 进行受控放到了最后一条内容中 29 | 30 | > 这个案例里面我们还使用了 chatItemRenderConfig 里面的 render 自定义了一条特殊格式的信息,关于这个 api 的更多信息可见[深度自定义对话内容](./chatItemRenderConfig.md) 31 | 32 | 33 | 34 | ### Hooks sendMessage 35 | 36 | > working on Progress 37 | -------------------------------------------------------------------------------- /docs/guide/styles.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom Style 3 | order: 20 4 | group: 5 | title: Use Cases 6 | nav: 7 | title: Documents 8 | order: 0 9 | --- 10 | 11 | ## Custom Style 12 | 13 | Overall, for ProChat, due to the use of AntDesign components and design, the style remains consistent 14 | 15 | At the same time, we also support different degrees of custom styles 16 | 17 | ### Input box area style 18 | 19 | InputAreaProps allows you to pass Props (i.e. Props supported by antd Input. Area) through the input box, where ClassName or Style can be inserted to modify the style. 20 | 21 | Below, I have changed the border color and font color of the input box 22 | 23 | 24 | 25 | ### Conversation Record Style 26 | 27 | We have a chatitemRenderConfiguration method that allows you to customize all the content of a conversation. This API is very powerful, and rendering of conversation content will come to this point. Avatar, content area, and operation area can all be modified by magic 28 | 29 | I have individually wrapped a blue border for the content area below 30 | 31 | 32 | 33 | ### Overwrite with ClassName style 34 | 35 | This is the simplest and most convenient style overlay method. We have added ClassNames to many places, and you only need to open devtools to see some names 36 | 37 | > Note: If you find that this ClassName does not look like a normal class name, it indicates that this class is using Hash's ClassName, which will dynamically change. Please do not overwrite this class name 38 | 39 | In the following case, I used ant styles (an ant css in js enterprise solution) to style overlay the background of the content area 40 | 41 | 42 | 43 | ### Theme customization 44 | 45 | If you want global style customization, such as leveraging antd's custom themes and algorithmic capabilities, we provide a set of code below in conjunction with antd styles. For more details, please refer to [antd styles](https://ant-design.github.io/antd-style/zh-CN/guide) 46 | 47 | 48 | -------------------------------------------------------------------------------- /docs/guide/styles.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 自定义样式 3 | order: 20 4 | group: 5 | title: 使用案例 6 | nav: 7 | title: 文档 8 | order: 0 9 | --- 10 | 11 | ## 自定义样式 12 | 13 | 整体对于 ProChat 来说,因为使用了 AntDesign 组件和设计,风格上是保持统一的 14 | 15 | 同时我们也支持不同程度的自定义样式 16 | 17 | ### 输入框区域样式 18 | 19 | inputAreaProps 允许你给输入框透传 Props(即 antd Input.Area 支持的 Props),里面可以穿入 ClassName 或 Style 来修改样式。 20 | 21 | 下面我改动了输入框的边框颜色和字体颜色 22 | 23 | 24 | 25 | ### 对话记录样式 26 | 27 | 我们有一个 chatItemRenderConfig 方法,可以让你自定义对话的所有内容,这个 api 很强大,涉及到对话内容的渲染都会走到这里,头像、内容区域、操作区域都可以进行魔改 28 | 29 | 下面我单独给内容区包裹了一个蓝色边框 border 30 | 31 | 32 | 33 | ### 使用 ClassName 样式覆盖 34 | 35 | 这个是最简单最方便的样式覆盖方法,我们给很多地方添加了 ClassName,你只需要打开 devtools 就可以看到一些名称 36 | 37 | > 注意:如果你发现这个 ClassName 看上去不像是一个正常的类名,说明这类用的是 Hash 的 ClassName,会动态变化,请不要覆盖这一类类名 38 | 39 | 下面的这个案例中我使用 antd-styles(一个 antd 的 css-in-js 企业级解决方案)来对内容区域的背景做了样式覆盖 40 | 41 | 42 | 43 | ### 主题定制 44 | 45 | 如果你想要全局的样式定制,例如想要利用到 antd 的自定义主题、算法能力,我们结合 antd-styles 在下面提供这样一套代码,详情的使用可见 [antd-styles](https://ant-design.github.io/antd-style/zh-CN/guide) 46 | 47 | 48 | -------------------------------------------------------------------------------- /docs/guide/umi.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Umi 3 | group: 4 | title: Frontend framework 5 | order: 51 6 | nav: 7 | title: Documents 8 | order: 0 9 | --- 10 | 11 | ## Integrate with umi 12 | 13 | In the R&D scenario of the backend, [umi](https://umijs.org/)It is a very good choice. The integration of ProChat and umi is very easy. After installation, it can be used directly. 14 | 15 | ```bash 16 | npx create-umi@latest 17 | or 18 | yarn create umi 19 | pnpm dlx create-umi@latest 20 | ``` 21 | 22 | ### Installation dependencies 23 | 24 | After creation 25 | 26 | ```bash 27 | npm install @ant-design/pro-chat --save 28 | or 29 | pnpm install @ant-design/pro-chat 30 | ``` 31 | 32 | ### Usage 33 | 34 | ```js 35 | import { useState, useEffect } from 'react'; 36 | import { ProChat } from '@ant-design/pro-chat'; 37 | export default () => ( 38 | { 47 | const mockedData: string = `这是一段模拟的对话数据。本次会话传入了${messages.length}条消息`; 48 | return new Response(mockedData); 49 | }} 50 | /> 51 | ); 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/guide/umi.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Umi 3 | group: 4 | title: 前端框架 5 | order: 51 6 | nav: 7 | title: 文档 8 | order: 0 9 | --- 10 | 11 | ## 与 Umi 集成 12 | 13 | 在中后台的研发场景, [umi](https://umijs.org/) 是一个非常不错的选择。ProChat 与 umi 的集成非常容易。安装后直接使用即可。 14 | 15 | ```bash 16 | npx create-umi@latest 17 | or 18 | yarn create umi 19 | pnpm dlx create-umi@latest 20 | ``` 21 | 22 | ### 依赖安装 23 | 24 | 创建好后 25 | 26 | ```bash 27 | npm install @ant-design/pro-chat --save 28 | or 29 | pnpm install @ant-design/pro-chat 30 | ``` 31 | 32 | ### 使用 33 | 34 | ```js 35 | import { useState, useEffect } from 'react'; 36 | import { ProChat } from '@ant-design/pro-chat'; 37 | export default () => ( 38 | { 47 | const mockedData: string = `这是一段模拟的对话数据。本次会话传入了${messages.length}条消息`; 48 | return new Response(mockedData); 49 | }} 50 | /> 51 | ); 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/guide/useProChat.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useProChat 3 | group: 4 | title: Hooks 5 | nav: 6 | title: Documents 7 | order: 0 8 | --- 9 | -------------------------------------------------------------------------------- /docs/guide/useProChat.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useProChat 3 | group: 4 | title: Hooks 5 | nav: 6 | title: 文档 7 | order: 0 8 | --- 9 | -------------------------------------------------------------------------------- /docs/guide/whyUseProChat.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 为什么要使用 ProChat 3 | group: 4 | title: 快速上手 5 | nav: 6 | title: 文档 7 | order: 0 8 | --- 9 | 10 | # 为什么要使用 ProChat 11 | 12 | 能够触达到这里的同学,都是想看看 ProChat 是什么,以及可能会需要在业务中使用的情况。 13 | 14 | 我们抛开那些因为 AI 或者 LLM 火起来的前置原因,从一个前端开发者的角度来说这个问题。 15 | 16 | ## 大模型复杂的结构 17 | 18 | 大模型的结构不能说很复杂,只能说非常复杂,里面有很多人工智能的专有名词,想要一个前端开发者理解并学习里面的知识成本实在是太高了。 19 | 20 | 但是从「前端页面」这个角度出发,前端开发者其实最关心的是「对话」 21 | 22 | 详细的出入参数可见:[大模型入参介绍](../request-params-intro) [大模型出参介绍](../return-params-intro) 23 | 24 | ## ProChat 体验细节 25 | 26 | ### 默认的流式输出支持 27 | 28 | 由 ChatGPT 率先实现的流式输出在用户体验上远超传统的 HTTP 请求,SSE(Server Send Event)这项技术也可以说正式登上了主流技术圈的视野中。 29 | 30 | ProChat 作为 AI 会话的前端解决方案,自然默认集成了这项流式输出的能力。只需要在 request 中配置一个返回流式文本的 Response (Web标准的 Response 对象),就可以轻松实现流式效果的集成。 31 | 32 | 效果如下 33 | 34 | 35 | 36 | 而同样的,ProChat 的 request api 也兼容传统的非流式请求: 37 | 38 | ```js 39 | import { ProChat } from '@ant-design/pro-chat'; 40 | import { useTheme } from 'antd-style'; 41 | 42 | const delay = (text: string) => 43 | new Promise((resolve) => { 44 | setTimeout(() => { 45 | resolve(text); 46 | }, 5000); 47 | }); 48 | 49 | export default () => { 50 | const theme = useTheme(); 51 | return ( 52 |
53 | { 55 | const text = await delay( 56 | `这是一条模拟非流式输出的消息的消息。本次会话传入了${messages.length}条消息`, 57 | ); 58 | return new Response(text); 59 | }} 60 | style={{ height: '300px' }} 61 | /> 62 |
63 | ); 64 | }; 65 | 66 | ``` 67 | 68 | ### 多种渲染支持 69 | 70 | 还有一点对前端来说比较头疼的在于「解析 - 渲染」 71 | 72 | 像是下面这种,如果你自己写,你需要对 String 部分的内容解析,然后决定哪些用什么渲染,我们已经帮你内置好了一些渲染器:Markdown 渲染、终端命令拼接、跳转链接等等常用的渲染器 73 | 74 | 75 | 76 | 而针对多行代码块,我们则强化了代码块组件的交互能力,使之具有折叠展开、更换高亮语言等进阶功能,进而帮助你在日常使用 AI 大模型中更好地查看AI生成的代码。 77 | 78 | 79 | 80 | ### 快速编辑、重试、更多能力 81 | 82 | 如果问错了问题?我想从某个地方开始,修改我之前的问题呢?或者我觉得他回复的不太好,后续的问题我希望帮他修改一下他的回答? 83 | 84 | 我们支持快速编辑、删除、重新生成这些能力,这些都是集成在 `ProChat` 这个组件中,开发者完全不需要有心智负担如何去实现这些能力,因为我们把这套数据流给你维护好啦! 85 | -------------------------------------------------------------------------------- /docs/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | title: ProChat 4 | description: 🤖 use Chat Components like Pro! 5 | actions: 6 | - text: 'Quick Start →' 7 | link: /en-US/guide/intro-start 8 | - text: 'Github' 9 | link: 'https://github.com/ant-design/pro-chat' 10 | features: 11 | - image: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/q48YQ5X4ytAAAAAAAAAAAAAAFl94AQBr' 12 | title: 'Simple and easy-to-use' 13 | description: 'Encapsulated on Ant Design, making it easier to use' 14 | - image: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg' 15 | title: 'Ant Design' 16 | description: 'Consistent with the Ant Design design system, seamlessly integrating with ant projects' 17 | - image: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/UKqDTIp55HYAAAAAAAAAAAAAFl94AQBr' 18 | title: 'Large model dialogue component' 19 | description: 'Common default basic operations of built-in dialogue models include data editing, resending, deleting conversations, etc' 20 | - image: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/Y_NMQKxw7OgAAAAAAAAAAAAAFl94AQBr' 21 | title: 'Preset styles' 22 | description: 'The style and style are in line with Antd, without the need for magic modification, and are naturally created. Default and user-friendly theme system' 23 | - image: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/U3XjS5IA1tUAAAAAAAAAAAAAFl94AQBr' 24 | title: 'The data structure of AI Friendly' 25 | description: 'Referring to mainstream large models such as ChatGPT, GLM, and Tongyi Qianwen on the market for input and output parameters, reduce the processing of these input and output parameters by front-end developers' 26 | - image: 'https://gw.alipayobjects.com/zos/antfincdn/Eb8IHpb9jE/Typescript_logo_2020.svg' 27 | title: 'TypeScript' 28 | description: 'Developed using TypeScript, providing complete type definition files without frequent opening of official websites' 29 | --- 30 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | title: ProChat 4 | description: 🤖 use Chat Components like Pro! 5 | actions: 6 | - text: '快速开始 →' 7 | link: /guide/intro-start 8 | - text: 'Github' 9 | link: 'https://github.com/ant-design/pro-chat' 10 | features: 11 | - image: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/q48YQ5X4ytAAAAAAAAAAAAAAFl94AQBr' 12 | title: '简单易用' 13 | description: '在 Ant Design 上进行了自己的封装,更加易用' 14 | - image: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg' 15 | title: 'Ant Design' 16 | description: '与 Ant Design 设计体系一脉相承,无缝对接 antd 项目' 17 | - image: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/UKqDTIp55HYAAAAAAAAAAAAAFl94AQBr' 18 | title: '大模型对话组件' 19 | description: '内置对话模型常用的:数据编辑、重新发送、删除对话等这些默认的基本操作' 20 | - image: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/Y_NMQKxw7OgAAAAAAAAAAAAAFl94AQBr' 21 | title: '预设样式' 22 | description: '样式风格与 antd 一脉相承,无需魔改,浑然天成。默认好用的主题系统' 23 | - image: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/U3XjS5IA1tUAAAAAAAAAAAAAFl94AQBr' 24 | title: 'AI Friendly 的数据结构' 25 | description: '参照 ChatGPT、GLM、通义千问等市面上主流的大模型入参出参,减少前端开发者对这些入参和出参的处理' 26 | - image: 'https://gw.alipayobjects.com/zos/antfincdn/Eb8IHpb9jE/Typescript_logo_2020.svg' 27 | title: 'TypeScript' 28 | description: '使用 TypeScript 开发,提供完整的类型定义文件,无需频繁打开官网' 29 | --- 30 | -------------------------------------------------------------------------------- /src/ActionIcon/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles( 4 | ({ css, stylish, cx }, { glass }: { active: boolean; glass: boolean }) => { 5 | return { 6 | block: cx( 7 | glass && stylish?.blur, 8 | css` 9 | cursor: pointer; 10 | 11 | position: relative; 12 | 13 | display: flex; 14 | flex: none; 15 | align-items: center; 16 | justify-content: center; 17 | `, 18 | ), 19 | }; 20 | }, 21 | ); 22 | -------------------------------------------------------------------------------- /src/BackBottom/demos/index.tsx: -------------------------------------------------------------------------------- 1 | import { BackBottom } from '@ant-design/pro-chat'; 2 | import { useRef } from 'react'; 3 | 4 | export default () => { 5 | const ref = useRef(null); 6 | return ( 7 |
8 |
9 | {Array.from({ length: 40 }) 10 | .fill('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') 11 | .map((item: any, index) => ( 12 |

{item}

13 | ))} 14 |
15 | 16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/BackBottom/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: Components 3 | group: Chat 4 | title: BackBottom 5 | order: 10 6 | --- 7 | 8 | ## Default 9 | 10 | 11 | 12 | ## APIs 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/BackBottom/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 组件 3 | group: Chat 4 | title: BackBottom 5 | order: 10 6 | --- 7 | 8 | ## Default 9 | 10 | 11 | 12 | ## APIs 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/BackBottom/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token, css, stylish, cx }, visible: boolean) => 4 | cx( 5 | stylish?.blur, 6 | css` 7 | pointer-events: ${visible ? 'all' : 'none'}; 8 | 9 | transform: translateY(${visible ? 0 : '16px'}); 10 | 11 | padding-inline: 12px !important; 12 | 13 | opacity: ${visible ? 1 : 0}; 14 | background: ${token.colorFillSecondary}; 15 | border-color: ${token.colorFillTertiary} !important; 16 | border-radius: 16px !important; 17 | backdrop-filter: blur(16px); 18 | `, 19 | ), 20 | ); 21 | -------------------------------------------------------------------------------- /src/ChatItem/components/Actions.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { Flexbox } from 'react-layout-kit'; 3 | 4 | import { ChatItemProps } from '@/ChatItem'; 5 | 6 | export interface ActionsProps { 7 | actions: ChatItemProps['actions']; 8 | className?: string; 9 | } 10 | 11 | const Actions = memo(({ actions, className }) => { 12 | return ( 13 | 14 | {actions} 15 | 16 | ); 17 | }); 18 | 19 | export default Actions; 20 | -------------------------------------------------------------------------------- /src/ChatItem/components/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { Flexbox } from 'react-layout-kit'; 3 | 4 | import AvatarComponent from '@/components/Avatar'; 5 | 6 | import { useStyles } from '../style'; 7 | import type { ChatItemProps } from '../type'; 8 | import Loading from './Loading'; 9 | 10 | export interface AvatarProps { 11 | addon?: ChatItemProps['avatarAddon']; 12 | avatar: ChatItemProps['avatar']; 13 | loading?: ChatItemProps['loading']; 14 | onClick?: ChatItemProps['onAvatarClick']; 15 | placement?: ChatItemProps['placement']; 16 | size?: number; 17 | } 18 | 19 | const Avatar = memo( 20 | ({ loading, avatar = {}, placement, addon, onClick, size = 40 }) => { 21 | const { styles } = useStyles({ avatarSize: size }); 22 | const avatarContent = ( 23 |
24 | 32 | 33 |
34 | ); 35 | 36 | if (!addon) return avatarContent; 37 | return ( 38 | 39 | {avatarContent} 40 | {addon} 41 | 42 | ); 43 | }, 44 | ); 45 | 46 | export default Avatar; 47 | -------------------------------------------------------------------------------- /src/ChatItem/components/BorderSpacing.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | export interface BorderSpacingProps { 4 | borderSpacing?: number; 5 | } 6 | 7 | const BorderSpacing = memo(({ borderSpacing }) => { 8 | if (!borderSpacing) return null; 9 | 10 | return
; 11 | }); 12 | 13 | export default BorderSpacing; 14 | -------------------------------------------------------------------------------- /src/ChatItem/components/ErrorContent.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from 'antd'; 2 | import { memo } from 'react'; 3 | import { Flexbox } from 'react-layout-kit'; 4 | 5 | import { ChatItemProps } from '@/ChatItem'; 6 | 7 | import { useStyles } from '../style'; 8 | 9 | export interface ErrorContentProps { 10 | message?: string; 11 | placement?: ChatItemProps['placement']; 12 | } 13 | 14 | const ErrorContent = memo(({ message, placement }) => { 15 | const { styles } = useStyles({ placement }); 16 | 17 | return ( 18 | 19 | 20 | 21 | ); 22 | }); 23 | 24 | export default ErrorContent; 25 | -------------------------------------------------------------------------------- /src/ChatItem/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2 } from 'lucide-react'; 2 | import { memo } from 'react'; 3 | import { Flexbox } from 'react-layout-kit'; 4 | 5 | import { ChatItemProps } from '@/ChatItem'; 6 | import Icon from '@/Icon'; 7 | 8 | import { useStyles } from '../style'; 9 | 10 | export interface LoadingProps { 11 | loading?: ChatItemProps['loading']; 12 | placement?: ChatItemProps['placement']; 13 | } 14 | 15 | const Loading = memo(({ loading, placement }) => { 16 | const { styles } = useStyles({ placement }); 17 | 18 | if (!loading) return null; 19 | 20 | return ( 21 | 22 | 23 | 24 | ); 25 | }); 26 | 27 | export default Loading; 28 | -------------------------------------------------------------------------------- /src/ChatItem/components/Title.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { Flexbox } from 'react-layout-kit'; 3 | 4 | import { ChatItemProps } from '@/ChatItem'; 5 | import { formatTime } from '@/ChatItem/utils/formatTime'; 6 | 7 | export interface TitleProps { 8 | avatar: ChatItemProps['avatar']; 9 | placement?: ChatItemProps['placement']; 10 | showTitle?: ChatItemProps['showTitle']; 11 | time?: ChatItemProps['time']; 12 | className?: string; 13 | } 14 | 15 | const Title = memo(({ showTitle, className, placement, time, avatar }) => { 16 | return ( 17 | 22 | {showTitle ? avatar.title || 'untitled' : undefined} 23 | {time && } 24 | 25 | ); 26 | }); 27 | 28 | export default Title; 29 | -------------------------------------------------------------------------------- /src/ChatItem/demos/Alert.tsx: -------------------------------------------------------------------------------- 1 | import { ChatItem } from '@ant-design/pro-chat'; 2 | 3 | import { avatar } from './data'; 4 | 5 | export default () => ; 6 | -------------------------------------------------------------------------------- /src/ChatItem/demos/data.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionIconGroupProps, MetaData } from '@ant-design/pro-chat'; 2 | import { Copy, Edit, RotateCw, Trash } from 'lucide-react'; 3 | 4 | export const avatar: MetaData = { 5 | avatar: '😎', 6 | backgroundColor: '#E8DA5A', 7 | title: 'Advertiser', 8 | }; 9 | 10 | export const items: ActionIconGroupProps['items'] = [ 11 | { 12 | icon: Edit, 13 | key: 'edit', 14 | label: 'Edit', 15 | }, 16 | ]; 17 | 18 | export const dropdownMenu: ActionIconGroupProps['dropdownMenu'] = [ 19 | { 20 | icon: Copy, 21 | key: 'copy', 22 | label: 'Copy', 23 | }, 24 | { 25 | icon: RotateCw, 26 | key: 'regenerate', 27 | label: 'Regenerate', 28 | }, 29 | { 30 | type: 'divider', 31 | }, 32 | { 33 | icon: Trash, 34 | key: 'delete', 35 | label: 'Delete', 36 | }, 37 | ]; 38 | -------------------------------------------------------------------------------- /src/ChatItem/demos/index.tsx: -------------------------------------------------------------------------------- 1 | import { ActionIconGroup, ChatItem, ChatItemProps } from '@ant-design/pro-chat'; 2 | import { useState } from 'react'; 3 | 4 | import { avatar, dropdownMenu, items } from './data'; 5 | 6 | export default () => { 7 | const [edit, setEdit] = useState(false); 8 | const control: ChatItemProps | any = { 9 | loading: false, 10 | message: 11 | "要使用 dayjs 的 fromNow 函数,需要先安装 dayjs 库并在代码中引入它。然后,可以使用以下语法来获取当前时间与给定时间之间的相对时间:\n\n```javascript\ndayjs().fromNow();\ndayjs('2021-05-01').fromNow();\n```", 12 | placement: { 13 | options: ['left', 'right'], 14 | value: 'left', 15 | }, 16 | primary: false, 17 | showTitle: false, 18 | time: 1_686_538_950_084, 19 | type: { 20 | options: ['block', 'pure'], 21 | value: 'block', 22 | }, 23 | }; 24 | 25 | return ( 26 | { 33 | if (action.key === 'edit') { 34 | setEdit(true); 35 | } 36 | }} 37 | type="ghost" 38 | /> 39 | } 40 | avatar={avatar} 41 | editing={edit} 42 | onEditingChange={setEdit} 43 | /> 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/ChatItem/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: Components 3 | group: Chat 4 | title: ChatItem 5 | description: ChatItem is a React component that represents a single item in a chat conversation. It displays the user's avatar, name, and message. It can also display a loading indicator if the message is still being sent. 6 | order: 10 7 | --- 8 | 9 | ## Default 10 | 11 | 12 | 13 | ## Alert 14 | 15 | 16 | 17 | ## APIs 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/ChatItem/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 组件 3 | group: Chat 4 | title: ChatItem 5 | description: ChatItem is a React component that represents a single item in a chat conversation. It displays the user's avatar, name, and message. It can also display a loading indicator if the message is still being sent. 6 | order: 10 7 | --- 8 | 9 | ## Default 10 | 11 | 12 | 13 | ## Alert 14 | 15 | 16 | 17 | ## APIs 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/ChatItem/utils/formatTime.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | 3 | export const formatTime = (time: number): string => { 4 | if (typeof process !== 'undefined' && process.env.NODE_ENV === 'TEST') { 5 | return '2024-02-27 17:20:00'; 6 | } 7 | 8 | const now = dayjs(); 9 | const target = dayjs(time); 10 | 11 | if (target.isSame(now, 'day')) { 12 | return target.format('HH:mm:ss'); 13 | } else if (target.isSame(now, 'year')) { 14 | return target.format('MM-DD HH:mm:ss'); 15 | } else { 16 | return target.format('YYYY-MM-DD HH:mm:ss'); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/ChatList/ActionsBar.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | import ActionIconGroup, { 4 | ActionIconGroupItems, 5 | type ActionIconGroupProps, 6 | } from '@/ActionIconGroup'; 7 | import { useChatListActionsBar } from '@/hooks/useChatListActionsBar'; 8 | 9 | /** 10 | * ActionsBar组件的属性类型定义 11 | */ 12 | 13 | export interface ActionsProps { 14 | actions?: Array; 15 | moreActions?: Array; 16 | } 17 | export interface ActionsBarProps extends ActionIconGroupProps { 18 | /** 19 | * 文本内容 20 | */ 21 | text?: { 22 | /** 23 | * 复制文本 24 | */ 25 | copy?: string; 26 | /** 27 | * 删除文本 28 | */ 29 | delete?: string; 30 | /** 31 | * 编辑文本 32 | */ 33 | edit?: string; 34 | /** 35 | * 重新生成文本 36 | */ 37 | regenerate?: string; 38 | }; 39 | /** 40 | * 内容 41 | */ 42 | content?: React.ReactNode | undefined; 43 | /** 44 | * 操作栏属性 45 | */ 46 | actionsProps?: ActionsProps; 47 | } 48 | 49 | /** 50 | * ActionsBar 组件 51 | * 用于渲染操作按钮组。 52 | */ 53 | const ActionsBar = memo(({ text, ...rest }) => { 54 | const { regenerate, edit, copy, divider, del } = useChatListActionsBar(text); 55 | return ( 56 | 62 | ); 63 | }); 64 | 65 | export default ActionsBar; 66 | -------------------------------------------------------------------------------- /src/ChatList/HistoryDivider.tsx: -------------------------------------------------------------------------------- 1 | import { Divider } from 'antd'; 2 | import { Timer } from 'lucide-react'; 3 | import { memo } from 'react'; 4 | 5 | import Icon from '@/Icon'; 6 | import Tag from '@/components/Tag'; 7 | 8 | /** 9 | * 历史记录分割线组件的属性。 10 | */ 11 | interface HistoryDividerProps { 12 | /** 13 | * 是否启用分割线。 14 | */ 15 | enable?: boolean; 16 | /** 17 | * 分割线文本。 18 | */ 19 | text?: string; 20 | } 21 | 22 | /** 23 | * 历史记录分割线组件。 24 | */ 25 | const HistoryDivider = memo(({ enable, text }) => { 26 | if (!enable) return null; 27 | 28 | return ( 29 |
30 | 31 | }>{text || 'History Message'} 32 | 33 |
34 | ); 35 | }); 36 | 37 | export default HistoryDivider; 38 | -------------------------------------------------------------------------------- /src/ChatList/ShouldUpdateItem.tsx: -------------------------------------------------------------------------------- 1 | import isEqual from 'fast-deep-equal'; 2 | import { Component } from 'react'; 3 | /** 4 | * 组件用于判断是否需要更新的辅助类。 5 | */ 6 | class ShouldUpdateItem extends Component< 7 | { 8 | shouldUpdate?: (prevProps: any, nextProps: any) => boolean; 9 | children: React.ReactNode; 10 | [key: string]: any; 11 | }, 12 | any 13 | > { 14 | /** 15 | * 判断组件是否需要更新。 16 | * @param nextProps - 下一个属性对象。 17 | * @returns 如果需要更新则返回 true,否则返回 false。 18 | */ 19 | shouldComponentUpdate(nextProps: any) { 20 | if (nextProps.shouldUpdate) { 21 | return nextProps.shouldUpdate(this.props, nextProps); 22 | } 23 | try { 24 | return ( 25 | !isEqual(this.props.content, nextProps?.content) || 26 | !isEqual(this.props.loading, nextProps?.loading) || 27 | !isEqual(this.props.chatItemRenderConfig, nextProps?.chatItemRenderConfig) || 28 | !isEqual(this.props.meta, nextProps?.meta) 29 | ); 30 | } catch (error) { 31 | return true; 32 | } 33 | } 34 | 35 | render() { 36 | return this.props.children; 37 | } 38 | } 39 | 40 | export default ShouldUpdateItem; 41 | -------------------------------------------------------------------------------- /src/ChatList/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ css }) => { 4 | return { 5 | container: css` 6 | position: relative; 7 | `, 8 | }; 9 | }); 10 | -------------------------------------------------------------------------------- /src/EditableMessage/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: Components 3 | group: Message 4 | title: EditableMessage 5 | description: The EditableMessage component is used to display a message that can be edited by the user. It consists of a Markdown component and an optional modal for editing the message. When the user clicks on the message, it enters editing mode and displays an input field for editing the message. 6 | --- 7 | 8 | ## Default 9 | 10 | 11 | 12 | ## APIs 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/EditableMessage/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 组件 3 | group: Message 4 | title: EditableMessage 5 | description: The EditableMessage component is used to display a message that can be edited by the user. It consists of a Markdown component and an optional modal for editing the message. When the user clicks on the message, it enters editing mode and displays an input field for editing the message. 6 | --- 7 | 8 | ## Default 9 | 10 | 11 | 12 | ## APIs 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/EditableMessageList/demos/index.tsx: -------------------------------------------------------------------------------- 1 | import { EditableMessageList } from '@ant-design/pro-chat'; 2 | 3 | import { data } from './data'; 4 | 5 | export default () => ; 6 | -------------------------------------------------------------------------------- /src/EditableMessageList/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: Components 3 | group: Message 4 | title: EditableMessageList 5 | description: EditableMessageList is a React component that allows users to edit a list of chat messages, including their content and role. It is designed to be used in chatbot building applications. 6 | --- 7 | 8 | ## Default 9 | 10 | 11 | 12 | ## APIs 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/EditableMessageList/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 组件 3 | group: Message 4 | title: EditableMessageList 5 | description: EditableMessageList is a React component that allows users to edit a list of chat messages, including their content and role. It is designed to be used in chatbot building applications. 6 | --- 7 | 8 | ## Default 9 | 10 | 11 | 12 | ## APIs 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Emoji/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | import { DivProps } from '../types'; 4 | 5 | import { useStyles } from './style'; 6 | 7 | export interface EmojiProps extends DivProps { 8 | /** 9 | * @description The emoji character to be rendered 10 | */ 11 | emoji: string; 12 | /** 13 | * @description Size of the emoji 14 | * @default 40 15 | */ 16 | size?: number; 17 | } 18 | 19 | const Emoji = memo(({ emoji, className, style, size = 40 }) => { 20 | const { cx, styles } = useStyles(); 21 | 22 | return ( 23 |
27 | {emoji} 28 |
29 | ); 30 | }); 31 | 32 | export default Emoji; 33 | -------------------------------------------------------------------------------- /src/Emoji/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ css, token }) => { 4 | return { 5 | container: css` 6 | position: relative; 7 | 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | 12 | line-height: 1; 13 | text-align: center; 14 | `, 15 | loading: css` 16 | position: absolute; 17 | inset: 0; 18 | 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | 23 | width: 100%; 24 | height: 100%; 25 | 26 | color: ${token.colorText}; 27 | `, 28 | }; 29 | }); 30 | -------------------------------------------------------------------------------- /src/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import { LucideIcon } from 'lucide-react'; 2 | import { memo, useMemo } from 'react'; 3 | 4 | import { DivProps } from '@/types'; 5 | 6 | import { useStyles } from './style'; 7 | 8 | export type IconSize = 9 | | 'large' 10 | | 'normal' 11 | | 'small' 12 | | { 13 | fontSize?: number; 14 | strokeWidth?: number; 15 | }; 16 | 17 | const calcSize = (size?: IconSize) => { 18 | let fontSize: number | string; 19 | let strokeWidth: number; 20 | 21 | switch (size) { 22 | case 'large': { 23 | fontSize = 24; 24 | strokeWidth = 2; 25 | break; 26 | } 27 | case 'normal': { 28 | fontSize = 20; 29 | strokeWidth = 2; 30 | break; 31 | } 32 | case 'small': { 33 | fontSize = 14; 34 | strokeWidth = 1.5; 35 | break; 36 | } 37 | default: { 38 | if (size) { 39 | fontSize = size?.fontSize || 24; 40 | strokeWidth = size?.strokeWidth || 2; 41 | } else { 42 | fontSize = '1em'; 43 | strokeWidth = 2; 44 | } 45 | break; 46 | } 47 | } 48 | return { fontSize, strokeWidth }; 49 | }; 50 | 51 | export interface IconProps extends DivProps { 52 | color?: string; 53 | fill?: string; 54 | /** 55 | * @description The icon element to be rendered 56 | * @type LucideIcon 57 | */ 58 | icon: LucideIcon; 59 | /** 60 | * @description Size of the icon 61 | * @default 'normal' 62 | */ 63 | size?: IconSize; 64 | /** 65 | * @description Rotate icon with animation 66 | * @default false 67 | */ 68 | spin?: boolean; 69 | } 70 | 71 | const Icon = memo(({ icon, size, color, fill, className, spin, ...props }) => { 72 | const { styles, cx } = useStyles(); 73 | const SvgIcon = icon; 74 | 75 | const { fontSize, strokeWidth } = useMemo(() => calcSize(size), [size]); 76 | 77 | return ( 78 | 79 | 88 | 89 | ); 90 | }); 91 | 92 | export default Icon; 93 | -------------------------------------------------------------------------------- /src/Icon/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles, keyframes } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ css }) => { 4 | const spin = keyframes` 5 | 0% { 6 | rotate: 0deg; 7 | } 8 | 100% { 9 | rotate: 360deg; 10 | } 11 | `; 12 | return { 13 | spin: css` 14 | animation: ${spin} 1s linear infinite; 15 | `, 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /src/List/ListItem/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ css, token }) => { 4 | return { 5 | actions: css` 6 | position: absolute; 7 | top: 50%; 8 | right: 16px; 9 | transform: translateY(-50%); 10 | `, 11 | active: css` 12 | color: ${token.colorText}; 13 | background-color: ${token.colorFillSecondary}; 14 | 15 | &:hover { 16 | background-color: ${token.colorFill}; 17 | } 18 | `, 19 | container: css` 20 | cursor: pointer; 21 | color: ${token.colorTextTertiary}; 22 | background: transparent; 23 | transition: background-color 200ms ${token.motionEaseOut}; 24 | 25 | &:active { 26 | background-color: ${token.colorFillSecondary}; 27 | } 28 | 29 | &:hover { 30 | background-color: ${token.colorFillTertiary}; 31 | } 32 | `, 33 | content: css` 34 | position: relative; 35 | overflow: hidden; 36 | flex: 1; 37 | align-self: center; 38 | `, 39 | desc: css` 40 | overflow: hidden; 41 | 42 | width: 100%; 43 | 44 | font-size: 12px; 45 | line-height: 1; 46 | color: ${token.colorTextDescription}; 47 | text-overflow: ellipsis; 48 | white-space: nowrap; 49 | `, 50 | 51 | pin: css` 52 | background-color: ${token.colorFillTertiary}; 53 | 54 | &:active { 55 | background-color: ${token.colorFill} !important; 56 | } 57 | 58 | &:hover { 59 | background-color: ${token.colorFill}; 60 | } 61 | `, 62 | 63 | time: css` 64 | font-size: 12px; 65 | color: ${token.colorTextPlaceholder}; 66 | `, 67 | title: css` 68 | overflow: hidden; 69 | 70 | width: 100%; 71 | 72 | font-size: 16px; 73 | line-height: 1; 74 | color: ${token.colorText}; 75 | text-overflow: ellipsis; 76 | white-space: nowrap; 77 | `, 78 | }; 79 | }); 80 | -------------------------------------------------------------------------------- /src/List/ListItem/time.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import 'dayjs/locale/zh-cn'; 3 | 4 | dayjs.locale('zh-cn'); 5 | 6 | export const getChatItemTime = (updateAt: number) => { 7 | const time = dayjs(updateAt); 8 | const diff = dayjs().day() - time.day(); 9 | 10 | if (time.isSame(dayjs(), 'day')) return time.format('HH:mm'); 11 | 12 | if (diff === 1) return '昨天'; 13 | 14 | return time.format('MM-DD'); 15 | }; 16 | -------------------------------------------------------------------------------- /src/List/index.ts: -------------------------------------------------------------------------------- 1 | import ListItem from './ListItem'; 2 | 3 | const List = { 4 | Item: ListItem, 5 | }; 6 | 7 | export default List; 8 | -------------------------------------------------------------------------------- /src/MessageInput/demos/index.tsx: -------------------------------------------------------------------------------- 1 | import { content } from '@/EditableMessage/demos'; 2 | import { MessageInput } from '@ant-design/pro-chat'; 3 | 4 | export default () => { 5 | return ; 6 | }; 7 | -------------------------------------------------------------------------------- /src/MessageInput/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: Components 3 | group: Message 4 | title: MessageInput 5 | description: CopyButton is a React component used to copy text content to the clipboard. It provides a button with a copy icon that, when clicked, copies the specified content to the user's clipboard. It also displays a tooltip indicating whether the copy action was successful or not. 6 | --- 7 | 8 | ## Default 9 | 10 | 11 | 12 | ## APIs 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/MessageInput/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 组件 3 | group: Message 4 | title: MessageInput 5 | description: CopyButton is a React component used to copy text content to the clipboard. It provides a button with a copy icon that, when clicked, copies the specified content to the user's clipboard. It also displays a tooltip indicating whether the copy action was successful or not. 6 | --- 7 | 8 | ## Default 9 | 10 | 11 | 12 | ## APIs 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/MessageInput/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles( 4 | ({ css, token }) => css` 5 | position: relative; 6 | 7 | height: 100%; 8 | 9 | font-family: ${token.fontFamilyCode}; 10 | font-size: 13px; 11 | line-height: 1.8; 12 | `, 13 | ); 14 | -------------------------------------------------------------------------------- /src/MessageModal/demos/index.tsx: -------------------------------------------------------------------------------- 1 | import { MessageModal } from '@ant-design/pro-chat'; 2 | import { Button } from 'antd'; 3 | import { useState } from 'react'; 4 | 5 | export default () => { 6 | const [open, setOpen] = useState(false); 7 | 8 | return ( 9 | <> 10 | 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/MessageModal/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: Components 3 | group: Message 4 | title: MessageModal 5 | description: The MessageModal component is a modal window that can display either a message in Markdown format or a message input field for editing the message. 6 | --- 7 | 8 | ## Default 9 | 10 | 11 | 12 | ## APIs 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/MessageModal/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 组件 3 | group: Message 4 | title: MessageModal 5 | description: The MessageModal component is a modal window that can display either a message in Markdown format or a message input field for editing the message. 6 | --- 7 | 8 | ## Default 9 | 10 | 11 | 12 | ## APIs 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/ProChat/__test__/demo.test.tsx: -------------------------------------------------------------------------------- 1 | import demoTest from '../../../tests/demo'; 2 | 3 | demoTest('ProChat'); 4 | -------------------------------------------------------------------------------- /src/ProChat/components/ChatList/Actions/Assistant.tsx: -------------------------------------------------------------------------------- 1 | import ActionIconGroup from '@/ActionIconGroup'; 2 | import { RenderAction } from '@/ChatList'; 3 | import { useChatListActionsBar } from '@/hooks/useChatListActionsBar'; 4 | import { memo } from 'react'; 5 | 6 | import useCustomChatListAction from '@/hooks/useCustomChatListAction'; 7 | import { ErrorActionsBar } from '../Actions/Error'; 8 | 9 | export const AssistantActionsBar: RenderAction = memo( 10 | ({ text, id, onActionClick, error, actionsProps }) => { 11 | const { regenerate, edit, copy, divider, del } = useChatListActionsBar(text); 12 | 13 | const { dropdownMenu, items } = useCustomChatListAction({ 14 | dropdownMenu: [ 15 | edit, 16 | copy, 17 | regenerate, 18 | // divider, 19 | // TODO: need a translate 20 | divider, 21 | del, 22 | ], 23 | items: [regenerate, copy], 24 | actionsProps, 25 | }); 26 | 27 | if (id === 'default') return; 28 | 29 | if (error) return ; 30 | 31 | return ( 32 | 38 | ); 39 | }, 40 | ); 41 | -------------------------------------------------------------------------------- /src/ProChat/components/ChatList/Actions/Error.tsx: -------------------------------------------------------------------------------- 1 | import ActionIconGroup from '@/ActionIconGroup'; 2 | import { ActionsBarProps } from '@/ChatList/ActionsBar'; 3 | import { useChatListActionsBar } from '@/hooks/useChatListActionsBar'; 4 | import useCustomChatListAction from '@/hooks/useCustomChatListAction'; 5 | import { memo } from 'react'; 6 | 7 | export const ErrorActionsBar = memo(({ text, onActionClick, actionsProps }) => { 8 | const { regenerate, del } = useChatListActionsBar(text); 9 | const { items } = useCustomChatListAction({ 10 | dropdownMenu: [], 11 | items: [regenerate, del], 12 | actionsProps, 13 | }); 14 | 15 | return ; 16 | }); 17 | -------------------------------------------------------------------------------- /src/ProChat/components/ChatList/Actions/Fallback.tsx: -------------------------------------------------------------------------------- 1 | import ActionIconGroup from '@/ActionIconGroup'; 2 | import { RenderAction } from '@/ChatList'; 3 | import { useChatListActionsBar } from '@/hooks/useChatListActionsBar'; 4 | import useCustomChatListAction from '@/hooks/useCustomChatListAction'; 5 | import { memo } from 'react'; 6 | 7 | export const DefaultActionsBar: RenderAction = memo(({ text, onActionClick, actionsProps }) => { 8 | const { del } = useChatListActionsBar(text); 9 | const { dropdownMenu, items } = useCustomChatListAction({ 10 | dropdownMenu: [del], 11 | items: [], 12 | actionsProps, 13 | }); 14 | return ( 15 | 21 | ); 22 | }); 23 | -------------------------------------------------------------------------------- /src/ProChat/components/ChatList/Actions/Function.tsx: -------------------------------------------------------------------------------- 1 | import ActionIconGroup from '@/ActionIconGroup'; 2 | import { RenderAction } from '@/ChatList'; 3 | import { useChatListActionsBar } from '@/hooks/useChatListActionsBar'; 4 | import useCustomChatListAction from '@/hooks/useCustomChatListAction'; 5 | import { memo } from 'react'; 6 | 7 | export const FunctionActionsBar: RenderAction = memo(({ text, onActionClick, actionsProps }) => { 8 | const { regenerate, divider, del } = useChatListActionsBar(text); 9 | const { dropdownMenu, items } = useCustomChatListAction({ 10 | dropdownMenu: [regenerate, divider, del], 11 | items: [regenerate], 12 | actionsProps, 13 | }); 14 | return ( 15 | 21 | ); 22 | }); 23 | -------------------------------------------------------------------------------- /src/ProChat/components/ChatList/Actions/User.tsx: -------------------------------------------------------------------------------- 1 | import ActionIconGroup from '@/ActionIconGroup'; 2 | import { RenderAction } from '@/ChatList'; 3 | import { useChatListActionsBar } from '@/hooks/useChatListActionsBar'; 4 | import useCustomChatListAction from '@/hooks/useCustomChatListAction'; 5 | import { memo } from 'react'; 6 | 7 | export const UserActionsBar: RenderAction = memo(({ text, onActionClick, actionsProps }) => { 8 | const { regenerate, edit, copy, divider, del } = useChatListActionsBar(text); 9 | const { dropdownMenu, items } = useCustomChatListAction({ 10 | dropdownMenu: [ 11 | edit, 12 | copy, 13 | regenerate, 14 | // divider, 15 | // TODO: need a translate 16 | divider, 17 | del, 18 | ], 19 | items: [regenerate, edit], 20 | actionsProps, 21 | }); 22 | return ( 23 | 29 | ); 30 | }); 31 | -------------------------------------------------------------------------------- /src/ProChat/components/ChatList/Actions/index.ts: -------------------------------------------------------------------------------- 1 | import { ChatListProps } from '@/ChatList'; 2 | 3 | import { AssistantActionsBar } from './Assistant'; 4 | import { DefaultActionsBar } from './Fallback'; 5 | import { FunctionActionsBar } from './Function'; 6 | import { UserActionsBar } from './User'; 7 | 8 | export const renderActions: ChatListProps['renderActions'] = { 9 | assistant: AssistantActionsBar, 10 | function: FunctionActionsBar, 11 | system: DefaultActionsBar, 12 | user: UserActionsBar, 13 | hello: () => undefined, 14 | }; 15 | -------------------------------------------------------------------------------- /src/ProChat/components/ChatList/Extras/Assistant.tsx: -------------------------------------------------------------------------------- 1 | import Tag from '@/components/Tag'; 2 | import { RenderMessageExtra } from '@/index'; 3 | 4 | import { memo } from 'react'; 5 | import { Flexbox } from 'react-layout-kit'; 6 | 7 | import { useStore } from '@/ProChat/store'; 8 | 9 | export const AssistantMessageExtra: RenderMessageExtra = memo(({ extra, ...rest }) => { 10 | const [model, messageItemExtraRender] = useStore((s) => [ 11 | s.config.model, 12 | s.messageItemExtraRender, 13 | ]); 14 | 15 | const showModelTag = extra?.fromModel && model !== extra?.fromModel; 16 | const hasTranslate = !!extra?.translate; 17 | 18 | const showExtra = showModelTag || hasTranslate; 19 | 20 | const dom = messageItemExtraRender?.({ extra, ...rest }, 'assistant'); 21 | if (!showExtra && !dom) return; 22 | 23 | return ( 24 | 25 | {showModelTag && ( 26 |
27 | {/*TODO: need a model icons */} 28 | {extra?.fromModel as string} 29 |
30 | )} 31 | {dom} 32 |
33 | ); 34 | }); 35 | -------------------------------------------------------------------------------- /src/ProChat/components/ChatList/Extras/User.tsx: -------------------------------------------------------------------------------- 1 | import { RenderMessageExtra } from '@/index'; 2 | import { Divider } from 'antd'; 3 | import { memo } from 'react'; 4 | import { Flexbox } from 'react-layout-kit'; 5 | 6 | import { useStore } from '@/ProChat/store'; 7 | 8 | export const UserMessageExtra: RenderMessageExtra = memo(({ extra, ...rest }) => { 9 | const hasTranslate = !!extra?.translate; 10 | 11 | const [messageItemExtraRender] = useStore((s) => [s.messageItemExtraRender]); 12 | 13 | const dom = messageItemExtraRender?.({ extra, ...rest }, 'user'); 14 | 15 | if (!dom) return; 16 | return ( 17 | 18 | {extra?.translate && ( 19 |
20 | 21 |
22 | )} 23 | {dom} 24 |
25 | ); 26 | }); 27 | -------------------------------------------------------------------------------- /src/ProChat/components/ChatList/Extras/index.ts: -------------------------------------------------------------------------------- 1 | import { ChatListProps } from '@/ChatList'; 2 | 3 | import { AssistantMessageExtra } from './Assistant'; 4 | import { UserMessageExtra } from './User'; 5 | 6 | export const renderMessagesExtra: ChatListProps['renderMessagesExtra'] = { 7 | assistant: AssistantMessageExtra, 8 | user: UserMessageExtra, 9 | }; 10 | -------------------------------------------------------------------------------- /src/ProChat/components/ChatList/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from 'antd-style'; 2 | 3 | const Svg = () => ( 4 | 5 | 6 | 16 | 17 | 18 | 28 | 29 | 30 | 40 | 41 | 42 | ); 43 | 44 | const BubblesLoading = () => { 45 | const { colorTextTertiary } = useTheme(); 46 | return ( 47 |
48 | 49 |
50 | ); 51 | }; 52 | 53 | export default BubblesLoading; 54 | -------------------------------------------------------------------------------- /src/ProChat/components/ChatList/Messages/Assistant.tsx: -------------------------------------------------------------------------------- 1 | import { RenderMessage } from '@/ChatList'; 2 | import { memo } from 'react'; 3 | 4 | import { DefaultMessage } from './Default'; 5 | 6 | export const AssistantMessage: RenderMessage = memo(({ id, content, ...props }) => { 7 | // todo: need a custom render 8 | return ; 9 | }); 10 | -------------------------------------------------------------------------------- /src/ProChat/components/ChatList/Messages/Default.tsx: -------------------------------------------------------------------------------- 1 | import { RenderMessage } from '@/ChatList'; 2 | import { memo } from 'react'; 3 | 4 | import { LOADING_FLAT } from '@/ProChat/const/message'; 5 | 6 | import BubblesLoading from '../Loading'; 7 | 8 | export const DefaultMessage: RenderMessage = memo(({ id, editableContent, content }) => { 9 | if (content === LOADING_FLAT) return ; 10 | 11 | return
{editableContent}
; 12 | }); 13 | -------------------------------------------------------------------------------- /src/ProChat/components/ChatList/Messages/Hello.tsx: -------------------------------------------------------------------------------- 1 | import { RenderMessage } from '@/ChatList'; 2 | import { memo } from 'react'; 3 | import { DefaultMessage } from './Default'; 4 | 5 | export const HelloMessage: RenderMessage = memo((props) => { 6 | const { content } = props; 7 | if (typeof content === 'string') return ; 8 | return content; 9 | }); 10 | -------------------------------------------------------------------------------- /src/ProChat/components/ChatList/Messages/index.ts: -------------------------------------------------------------------------------- 1 | import { ChatListProps } from '@/ChatList'; 2 | 3 | import { AssistantMessage } from './Assistant'; 4 | import { DefaultMessage } from './Default'; 5 | import { HelloMessage } from './Hello'; 6 | 7 | export const renderMessages: ChatListProps['renderMessages'] = { 8 | hello: HelloMessage, 9 | assistant: AssistantMessage, 10 | default: DefaultMessage, 11 | }; 12 | -------------------------------------------------------------------------------- /src/ProChat/components/ChatList/SkeletonList.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from 'antd'; 2 | import { createStyles } from 'antd-style'; 3 | import { memo } from 'react'; 4 | import { Flexbox } from 'react-layout-kit'; 5 | 6 | const useStyles = createStyles(({ css, prefixCls }) => ({ 7 | user: css` 8 | display: flex; 9 | flex-direction: row-reverse; 10 | gap: 16px; 11 | 12 | .${prefixCls}-skeleton-paragraph { 13 | display: flex; 14 | flex-direction: column; 15 | align-items: flex-end; 16 | } 17 | `, 18 | })); 19 | const SkeletonList = memo(() => { 20 | const { styles } = useStyles(); 21 | 22 | return ( 23 | 24 | 31 | 32 | 33 | ); 34 | }); 35 | export default SkeletonList; 36 | -------------------------------------------------------------------------------- /src/ProChat/components/InputArea/ActionBar.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles, cx } from 'antd-style'; 2 | import { Flexbox } from 'react-layout-kit'; 3 | 4 | import ActionIcon from '@/ActionIcon'; 5 | import { ConfigProvider, Popconfirm } from 'antd'; 6 | import { Trash2 } from 'lucide-react'; 7 | 8 | import useProChatLocale from '@/ProChat/hooks/useProChatLocale'; 9 | import { useStore } from '../../store'; 10 | 11 | const useStyles = createStyles(({ css, token }) => ({ 12 | extra: css` 13 | color: ${token.colorTextTertiary}; 14 | `, 15 | })); 16 | 17 | export const ActionBar = ({ className }: { className?: string }) => { 18 | const [clearMessage, actionsRender, flexConfig] = useStore((s) => [ 19 | s.clearMessage, 20 | s.actions?.render, 21 | s.actions?.flexConfig, 22 | ]); 23 | 24 | const { localeObject } = useProChatLocale(); 25 | 26 | const { styles, theme } = useStyles(); 27 | const defaultDoms = [ 28 | { 35 | clearMessage(); 36 | }} 37 | > 38 | 39 | , 40 | ]; 41 | 42 | return ( 43 | 44 | 52 | {actionsRender?.(defaultDoms) ?? defaultDoms} 53 | 54 | 55 | ); 56 | }; 57 | 58 | export default ActionBar; 59 | -------------------------------------------------------------------------------- /src/ProChat/components/InputArea/AutoCompleteTextArea.tsx: -------------------------------------------------------------------------------- 1 | import { AutoComplete, AutoCompleteProps, Input } from 'antd'; 2 | import { TextAreaProps } from 'antd/es/input'; 3 | import { TextAreaRef } from 'antd/es/input/TextArea'; 4 | import React, { useState } from 'react'; 5 | import { useStore } from '../../store'; 6 | 7 | type AutoCompleteTextAreaProps = TextAreaProps & { 8 | autoCompleteProps?: AutoCompleteProps; 9 | }; 10 | 11 | export const AutoCompleteTextArea: React.FC = React.forwardRef< 12 | TextAreaRef, 13 | AutoCompleteTextAreaProps 14 | >((props, ref) => { 15 | const [autocompleteRequest] = useStore((s) => [s.autocompleteRequest]); 16 | 17 | const { disabled, autoCompleteProps = {}, ...rest } = props; 18 | 19 | const [options, setOptions] = useState<{ value: string; label: string }[]>([]); 20 | const [open, setOpen] = useState(false); 21 | return ( 22 | { 31 | setOpen(open); 32 | }} 33 | value={props.value} 34 | onSelect={(value) => { 35 | props.onChange?.({ target: { value } } as any); 36 | setOptions([]); 37 | }} 38 | onSearch={async (value) => { 39 | const result = await autocompleteRequest?.(value); 40 | setOptions((result as any[]) || []); 41 | }} 42 | {...autoCompleteProps} 43 | > 44 | { 51 | setOpen(false); 52 | props.onFocus?.(e); 53 | }} 54 | onPressEnter={(e) => { 55 | if (open && options.length > 0) return; 56 | props.onPressEnter?.(e); 57 | }} 58 | /> 59 | 60 | ); 61 | }); 62 | -------------------------------------------------------------------------------- /src/ProChat/components/InputArea/StopLoading.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from 'antd-style'; 2 | import { memo } from 'react'; 3 | 4 | const StopLoadingIcon = memo(() => { 5 | const theme = useTheme(); 6 | return ( 7 | 16 | 17 | 18 | 19 | 25 | 33 | 34 | 35 | 36 | ); 37 | }); 38 | export default StopLoadingIcon; 39 | -------------------------------------------------------------------------------- /src/ProChat/components/ScrollAnchor/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useEffect, useMemo, useState } from 'react'; 2 | import { useInView } from 'react-intersection-observer'; 3 | 4 | import { useStore } from '@/ProChat/store'; 5 | import { chatSelectors } from '../../store/selectors'; 6 | 7 | const ChatScrollAnchor = memo(({ target }: { target: React.RefObject }) => { 8 | const trackVisibility = useStore((s) => !!s.chatLoadingId); 9 | const str = useStore(chatSelectors.currentChats); 10 | 11 | const [isWindowAvailable, setIsWindowAvailable] = useState(false); 12 | 13 | useEffect(() => { 14 | // 检查window对象是否已经可用 15 | if (typeof window !== 'undefined') { 16 | setIsWindowAvailable(true); 17 | } 18 | }, []); 19 | 20 | // 获取上方列表的实例化 ref,会传入给 useAtBottom 用于判断当前是否在滚动 21 | const current = useMemo(() => { 22 | if (target.current) { 23 | return target.current; 24 | } 25 | return document.body; 26 | }, [isWindowAvailable]); 27 | 28 | const [scrollOffset, setScrollOffset] = useState(0); 29 | 30 | useEffect(() => { 31 | if (isWindowAvailable) { 32 | // 如果是移动端,可能100太多了,认为超过 1/3 即可,PC默认100 33 | setScrollOffset(window.innerHeight / 3 > 100 ? 100 : window.innerHeight / 4); 34 | } 35 | }, [isWindowAvailable]); 36 | 37 | const { ref, entry, inView } = useInView({ 38 | root: target.current, 39 | delay: 100, 40 | rootMargin: `0px 0px ${scrollOffset}px 0px`, 41 | trackVisibility, 42 | }); 43 | 44 | useEffect(() => { 45 | if (trackVisibility && inView) { 46 | current?.scrollTo({ 47 | behavior: 'smooth', 48 | left: 0, 49 | top: current?.scrollHeight || 99999, 50 | }); 51 | } 52 | }, [inView, entry, trackVisibility, str]); 53 | 54 | return
; 55 | }); 56 | 57 | export default ChatScrollAnchor; 58 | -------------------------------------------------------------------------------- /src/ProChat/components/ScrollAnchor/useAtBottom.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export const useAtBottom = (offset = 0, target: HTMLElement) => { 4 | const [isAtBottom, setIsAtBottom] = useState(false); 5 | useEffect(() => { 6 | if (target) { 7 | const handleScroll = () => { 8 | setIsAtBottom(target.scrollTop + target.clientHeight >= target.scrollHeight - offset); 9 | }; 10 | target.addEventListener('scroll', handleScroll, { passive: true }); 11 | handleScroll(); 12 | return () => { 13 | target.removeEventListener('scroll', handleScroll); 14 | }; 15 | } else { 16 | const handleScroll = () => { 17 | setIsAtBottom(window.innerHeight + window.scrollY >= document.body.offsetHeight - offset); 18 | }; 19 | window.addEventListener('scroll', handleScroll, { passive: true }); 20 | handleScroll(); 21 | 22 | return () => { 23 | window.removeEventListener('scroll', handleScroll); 24 | }; 25 | } 26 | }, [offset, target]); 27 | 28 | return isAtBottom; 29 | }; 30 | -------------------------------------------------------------------------------- /src/ProChat/const/message.ts: -------------------------------------------------------------------------------- 1 | export const LOADING_FLAT = '...'; 2 | 3 | // 只要 start with 这个,就可以判断为 function message 4 | export const FUNCTION_MESSAGE_FLAG = '{"function'; 5 | -------------------------------------------------------------------------------- /src/ProChat/const/meta.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_AVATAR = '🤖'; 2 | export const DEFAULT_USER_AVATAR = '😀'; 3 | -------------------------------------------------------------------------------- /src/ProChat/container/OverrideStyle/global.ts: -------------------------------------------------------------------------------- 1 | import { FullToken, css } from 'antd-style'; 2 | 3 | export default (token: FullToken, rootClassName: string) => css` 4 | line-height: 1; 5 | text-size-adjust: none; 6 | text-rendering: optimizelegibility; 7 | vertical-align: baseline; 8 | 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | -webkit-overflow-scrolling: touch; 12 | -webkit-tap-highlight-color: transparent; 13 | } 14 | 15 | .${rootClassName} { 16 | code { 17 | font-family: ${token.fontFamilyCode} !important; 18 | 19 | span { 20 | font-family: ${token.fontFamilyCode} !important; 21 | } 22 | } 23 | 24 | p { 25 | word-wrap: break-word; 26 | } 27 | 28 | *::selection { 29 | color: #000; 30 | background: ${token.blue3}; 31 | 32 | -webkit-text-fill-color: unset !important; 33 | } 34 | 35 | * { 36 | box-sizing: border-box; 37 | vertical-align: baseline; 38 | } 39 | } 40 | `; 41 | -------------------------------------------------------------------------------- /src/ProChat/container/OverrideStyle/index.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | import global from './global'; 3 | 4 | export const useOverrideStyles = createStyles(({ token, prefixCls, cx }) => { 5 | const rootClassName = `${prefixCls}-pro-chat`; 6 | 7 | return { 8 | container: cx(rootClassName, global(token, rootClassName)), 9 | }; 10 | }); 11 | -------------------------------------------------------------------------------- /src/ProChat/container/Provider.tsx: -------------------------------------------------------------------------------- 1 | import StoreUpdater, { ProChatChatReference } from '@/ProChat/container/StoreUpdater'; 2 | import { memo, ReactNode } from 'react'; 3 | import { DevtoolsOptions } from 'zustand/middleware'; 4 | import { ChatProps, createStore, Provider, useStoreApi } from '../store'; 5 | 6 | interface ProChatProviderProps = Record> 7 | extends ChatProps { 8 | children: ReactNode; 9 | devtoolOptions?: boolean | DevtoolsOptions; 10 | chatRef?: ProChatChatReference; 11 | } 12 | 13 | export const ProChatProvider = memo>( 14 | ({ 15 | children, 16 | devtoolOptions, 17 | chats, 18 | onChatsChange, 19 | loading, 20 | helloMessage, 21 | userMeta, 22 | inputAreaProps, 23 | assistantMeta, 24 | actions, 25 | transformToChatMessage, 26 | request, 27 | locale, 28 | ...props 29 | }) => { 30 | let isWrapped = true; 31 | 32 | const Content = ( 33 | <> 34 | {children} 35 | 49 | 50 | ); 51 | 52 | try { 53 | useStoreApi(); 54 | } catch (e) { 55 | isWrapped = false; 56 | } 57 | 58 | if (isWrapped) { 59 | return Content; 60 | } 61 | 62 | return createStore(props, devtoolOptions)}>{Content}; 63 | }, 64 | ); 65 | -------------------------------------------------------------------------------- /src/ProChat/container/StoreUpdater.tsx: -------------------------------------------------------------------------------- 1 | import { memo, MutableRefObject, useImperativeHandle } from 'react'; 2 | import { createStoreUpdater } from 'zustand-utils'; 3 | 4 | import { ProChatInstance, useProChat } from '../hooks/useProChat'; 5 | import { ChatProps, ChatState, useStoreApi } from '../store'; 6 | 7 | export type ProChatChatReference = MutableRefObject; 8 | 9 | export interface StoreUpdaterProps 10 | extends Partial< 11 | Pick< 12 | ChatState, 13 | | 'chats' 14 | | 'config' 15 | | 'init' 16 | | 'onChatsChange' 17 | | 'helloMessage' 18 | | 'request' 19 | | 'locale' 20 | | 'inputAreaProps' 21 | | 'actions' 22 | | 'transformToChatMessage' 23 | > 24 | >, 25 | Pick { 26 | chatRef?: ProChatChatReference; 27 | } 28 | 29 | const StoreUpdater = memo( 30 | ({ 31 | init, 32 | onChatsChange, 33 | chatRef, 34 | request, 35 | userMeta, 36 | assistantMeta, 37 | helloMessage, 38 | transformToChatMessage, 39 | actions, 40 | inputAreaProps, 41 | chats, 42 | config, 43 | locale, 44 | }) => { 45 | const storeApi = useStoreApi(); 46 | const useStoreUpdater = createStoreUpdater(storeApi); 47 | 48 | useStoreUpdater('init', init); 49 | 50 | useStoreUpdater('userMeta', userMeta); 51 | useStoreUpdater('assistantMeta', assistantMeta); 52 | 53 | useStoreUpdater('inputAreaProps', inputAreaProps); 54 | useStoreUpdater('helloMessage', helloMessage); 55 | useStoreUpdater('config', config); 56 | 57 | useStoreUpdater('transformToChatMessage', transformToChatMessage); 58 | useStoreUpdater('actions', actions); 59 | 60 | useStoreUpdater('chats', chats); 61 | useStoreUpdater('onChatsChange', onChatsChange); 62 | 63 | useStoreUpdater('request', request); 64 | 65 | useStoreUpdater('locale', locale); 66 | const instance = useProChat(); 67 | useImperativeHandle(chatRef, () => instance); 68 | 69 | return null; 70 | }, 71 | ); 72 | 73 | export default StoreUpdater; 74 | -------------------------------------------------------------------------------- /src/ProChat/demos/actions-chat-item.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ProChat } from '@ant-design/pro-chat'; 5 | import { useTheme } from 'antd-style'; 6 | 7 | import { Button } from 'antd'; 8 | import { MockResponse } from '../mocks/streamResponse'; 9 | 10 | export default () => { 11 | const theme = useTheme(); 12 | 13 | return ( 14 |
15 | { 18 | if (props?.editing) { 19 | return null; 20 | } 21 | return ( 22 | 30 | ); 31 | }, 32 | }} 33 | request={async (messages) => { 34 | const mockedData: string = `这是一段模拟的流式字符串数据。本次会话传入了${messages.length}条消息`; 35 | 36 | const mockResponse = new MockResponse(mockedData); 37 | 38 | return mockResponse.getResponse(); 39 | }} 40 | /> 41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/ProChat/demos/actions.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ProChat } from '@ant-design/pro-chat'; 5 | import { useTheme } from 'antd-style'; 6 | 7 | import { MockResponse } from '../mocks/streamResponse'; 8 | 9 | export default () => { 10 | const theme = useTheme(); 11 | 12 | return ( 13 |
14 | { 25 | return [ 26 | { 29 | window.open('https://github.com/ant-design/pro-chat'); 30 | }} 31 | > 32 | 人工服务 33 | , 34 | ...defaultDoms, 35 | ]; 36 | }, 37 | flexConfig: { 38 | gap: 24, 39 | direction: 'horizontal', 40 | justify: 'space-between', 41 | }, 42 | }} 43 | request={async (messages) => { 44 | const mockedData: string = `这是一段模拟的流式字符串数据。本次会话传入了${messages.length}条消息`; 45 | 46 | const mockResponse = new MockResponse(mockedData); 47 | 48 | return mockResponse.getResponse(); 49 | }} 50 | /> 51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/ProChat/demos/callbacks.tsx: -------------------------------------------------------------------------------- 1 | import { ProChat, ProChatInstance } from '@ant-design/pro-chat'; 2 | import { message } from 'antd'; 3 | import React from 'react'; 4 | 5 | const Callbacks: React.FC = () => { 6 | const chatRef = React.useRef(null); 7 | return ( 8 | { 32 | return new Response(message.at(-1).content.toString()); 33 | }} 34 | /> 35 | ); 36 | }; 37 | 38 | export default Callbacks; 39 | -------------------------------------------------------------------------------- /src/ProChat/demos/control.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ChatMessage, ProChat } from '@ant-design/pro-chat'; 5 | 6 | import { useTheme } from 'antd-style'; 7 | import { useState } from 'react'; 8 | 9 | import { example } from '../mocks/basic'; 10 | import { MockResponse } from '../mocks/streamResponse'; 11 | 12 | export default () => { 13 | const theme = useTheme(); 14 | 15 | const [chats, setChats] = useState>[]>(example.chats); 16 | 17 | return ( 18 |
19 | { 22 | setChats(chats); 23 | }} 24 | request={async (messages) => { 25 | const mockedData: string = `这是一段模拟的流式字符串数据。本次会话传入了${messages.length}条消息`; 26 | 27 | const mockResponse = new MockResponse(mockedData, 100); 28 | 29 | return mockResponse.getResponse(); 30 | }} 31 | /> 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/ProChat/demos/customeClassName.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { example } from '@/ProChat/mocks/customeClassName'; 5 | import { ProChat } from '@ant-design/pro-chat'; 6 | import { useTheme } from 'antd-style'; 7 | 8 | export default () => { 9 | const theme = useTheme(); 10 | 11 | return ( 12 |
13 | 23 | 37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/ProChat/demos/default.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ProChat } from '@ant-design/pro-chat'; 5 | 6 | import { useTheme } from 'antd-style'; 7 | import { example } from '../mocks/basic'; 8 | 9 | export default () => { 10 | const theme = useTheme(); 11 | return ( 12 |
13 | { 24 | if (value === '/') { 25 | return [{ value: '你可以帮助我列出问题吗?', label: '你可以帮助我列出问题吗?' }]; 26 | } 27 | return []; 28 | }} 29 | inputAreaProps={{ 30 | autoCompleteProps: { 31 | placement: 'topRight', 32 | }, 33 | }} 34 | userMeta={{ 35 | extra: 'extra', 36 | }} 37 | messageItemExtraRender={(_, type) => { 38 | if (type === 'user') return 🦐; 39 | return 👍; 40 | }} 41 | placeholder="输入 / 查看推荐问题,或者直接输入你的问题" 42 | onResetMessage={async () => { 43 | console.log('数据清空'); 44 | }} 45 | /> 46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/ProChat/demos/doc-mode.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * iframe: 1000 3 | * compact: true 4 | */ 5 | import { ProChat } from '@ant-design/pro-chat'; 6 | import { useTheme } from 'antd-style'; 7 | 8 | import { example } from '../mocks/fullFeature'; 9 | import { MockResponse } from '../mocks/streamResponse'; 10 | 11 | export default () => { 12 | const theme = useTheme(); 13 | return ( 14 |
15 | { 19 | console.log('chat start', chat); 20 | }} 21 | onChatEnd={(id, type) => { 22 | console.log('chat end', id); 23 | console.log('chat end type', type); 24 | }} 25 | onChatGenerate={(chunkText) => { 26 | console.log('chat generate', chunkText); 27 | }} 28 | request={async (messages) => { 29 | const mockedData: string = `这是一段模拟的流式字符串数据。本次会话传入了${messages.length}条消息`; 30 | 31 | const mockResponse = new MockResponse(mockedData, 100); 32 | 33 | return mockResponse.getResponse(); 34 | }} 35 | chats={example.chats} 36 | config={example.config} 37 | /> 38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/ProChat/demos/error.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | * iframe: 1000 4 | */ 5 | import { ProChat, ProChatInstance } from '@ant-design/pro-chat'; 6 | import { Button, Card, Result } from 'antd'; 7 | import { useTheme } from 'antd-style'; 8 | import { useEffect, useRef } from 'react'; 9 | import { MockResponse } from '../mocks/streamResponse'; 10 | 11 | export default () => { 12 | const theme = useTheme(); 13 | 14 | const chatRef1 = useRef(); 15 | const chatRef2 = useRef(); 16 | 17 | useEffect(() => { 18 | if (chatRef1?.current && chatRef2?.current) { 19 | setTimeout(async () => { 20 | await chatRef1.current?.sendMessage('Hello!'); 21 | await chatRef2.current?.sendMessage('Hello!'); 22 | }, 500); 23 | } 24 | }, []); 25 | 26 | return ( 27 | <> 28 |
29 | { 33 | const mockResponse = new MockResponse('', 1000, true); 34 | return mockResponse.getResponse(); 35 | }} 36 | /> 37 |
38 |
39 | { 41 | const mockResponse = new MockResponse('', 1000, true); 42 | return mockResponse.getResponse(); 43 | }} 44 | chatRef={chatRef2} 45 | style={{ height: 500 }} 46 | renderErrorMessages={(errorResponse) => { 47 | return ( 48 | 49 | 55 | Try Again 56 | , 57 | , 58 | ]} 59 | /> 60 | 61 | ); 62 | }} 63 | /> 64 |
65 | 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /src/ProChat/demos/float-drawer.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * iframe: 500 3 | */ 4 | import { MockResponse } from '@/ProChat/mocks/streamResponse'; 5 | import { QuestionCircleOutlined } from '@ant-design/icons'; 6 | import { ProChat } from '@ant-design/pro-chat'; 7 | import { Drawer, FloatButton } from 'antd'; 8 | import { useTheme } from 'antd-style'; 9 | import { useState } from 'react'; 10 | 11 | export default () => { 12 | const theme = useTheme(); 13 | 14 | const [open, setOpen] = useState(true); 15 | 16 | const showDrawer = () => { 17 | setOpen(true); 18 | }; 19 | 20 | const onClose = () => { 21 | setOpen(false); 22 | }; 23 | 24 | return ( 25 | <> 26 | } type="primary" onClick={showDrawer} /> 27 | 37 | { 40 | const mockedData: string = `这是一段模拟的流式字符串数据。本次会话传入了${messages.length}条消息`; 41 | 42 | const mockResponse = new MockResponse(mockedData); 43 | 44 | return mockResponse.getResponse(); 45 | }} 46 | /> 47 | 48 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/ProChat/demos/helloMessage.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ProChat } from '@ant-design/pro-chat'; 5 | 6 | import { useTheme } from 'antd-style'; 7 | 8 | export default () => { 9 | const theme = useTheme(); 10 | return ( 11 |
12 | {'这是一条自定义 ReactNode 消息'}
} /> 13 | 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/ProChat/demos/i18n.tsx: -------------------------------------------------------------------------------- 1 | import { ProChat, type Locale } from '@ant-design/pro-chat'; 2 | import { Segmented } from 'antd'; 3 | import { useTheme } from 'antd-style'; 4 | import React from 'react'; 5 | 6 | export default () => { 7 | const theme = useTheme(); 8 | const [language, setLanguage] = React.useState('en-US'); 9 | return ( 10 |
11 | 12 | options={['en-US', 'zh-CN', 'zh-HK', 'cs-CZ', 'de-DE', 'hu-HU', 'pl-PL', 'sk-SK']} 13 | defaultValue="en-US" 14 | onChange={(v) => { 15 | setLanguage(v); 16 | }} 17 | /> 18 | { 21 | return new Response('this is mock data'); 22 | }} 23 | /> 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/ProChat/demos/initialChats.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ProChat } from '@ant-design/pro-chat'; 5 | 6 | import { useTheme } from 'antd-style'; 7 | import { example } from '../mocks/basic'; 8 | 9 | export default () => { 10 | const theme = useTheme(); 11 | return ( 12 |
13 | 14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/ProChat/demos/listener.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * iframe: 300 3 | * compact: true 4 | */ 5 | import { ProChat } from '@ant-design/pro-chat'; 6 | 7 | import { useTheme } from 'antd-style'; 8 | 9 | export default () => { 10 | const theme = useTheme(); 11 | return ( 12 |
13 | { 18 | console.log('chat start', chat); 19 | }} 20 | onChatEnd={(id, type) => { 21 | console.log('chat end', id); 22 | console.log('chat end type', type); 23 | }} 24 | onChatGenerate={(chunkText) => { 25 | console.log('chat generate', chunkText); 26 | }} 27 | request={async (messages) => { 28 | const mockedData: string = `这是一段模拟的对话数据。本次会话传入了${messages.length}条消息`; 29 | return new Response(mockedData); 30 | }} 31 | /> 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/ProChat/demos/loading.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ProChat } from '@ant-design/pro-chat'; 5 | import { Button, Divider } from 'antd'; 6 | import { useTheme } from 'antd-style'; 7 | import { useState } from 'react'; 8 | import { Flexbox } from 'react-layout-kit'; 9 | 10 | export default () => { 11 | const [loading, setLoading] = useState(true); 12 | const theme = useTheme(); 13 | 14 | return ( 15 | 16 | 17 | 25 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/ProChat/demos/meta.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { chats } from '@/ProChat/mocks/threebody'; 5 | import { ProChat } from '@ant-design/pro-chat'; 6 | import { useTheme } from 'antd-style'; 7 | 8 | export default () => { 9 | const theme = useTheme(); 10 | 11 | return ( 12 |
13 | 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/ProChat/demos/modal.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * iframe 3 | */ 4 | import { ProChat } from '@ant-design/pro-chat'; 5 | 6 | export default () => { 7 | return ; 8 | }; 9 | -------------------------------------------------------------------------------- /src/ProChat/demos/no-stream.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * iframe: 500 3 | */ 4 | import { ProChat } from '@ant-design/pro-chat'; 5 | import { useTheme } from 'antd-style'; 6 | 7 | const delay = (text: string) => 8 | new Promise((resolve) => { 9 | setTimeout(() => { 10 | resolve(text); 11 | }, 5000); 12 | }); 13 | 14 | export default () => { 15 | const theme = useTheme(); 16 | 17 | return ( 18 |
19 | { 21 | const text = await delay( 22 | `这是一条模拟非流式输出的消息的消息。本次会话传入了${messages.length}条消息`, 23 | ); 24 | 25 | return new Response(text); 26 | }} 27 | style={{ height: '100vh' }} 28 | /> 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/ProChat/demos/request.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ProChat } from '@ant-design/pro-chat'; 5 | import { useTheme } from 'antd-style'; 6 | import { useState } from 'react'; 7 | 8 | export default () => { 9 | const theme = useTheme(); 10 | const [value, setValue] = useState(); 11 | return ( 12 |
13 | { 18 | setValue(e); 19 | }, 20 | }} 21 | /> 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/ProChat/demos/toBottomConfig.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ProChat } from '@ant-design/pro-chat'; 5 | import { useTheme } from 'antd-style'; 6 | 7 | import { Button } from 'antd'; 8 | import { example } from '../mocks/fullFeature'; 9 | 10 | export default () => { 11 | const theme = useTheme(); 12 | return ( 13 | <> 14 |
15 | { 22 | return ( 23 | 34 | ); 35 | }, 36 | }} 37 | /> 38 |
39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/ProChat/demos/use-ref.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ProChat, ProChatInstance } from '@ant-design/pro-chat'; 5 | import { useTheme } from 'antd-style'; 6 | import { useRef } from 'react'; 7 | 8 | import { MockResponse } from '@/ProChat/mocks/streamResponse'; 9 | import { Button } from 'antd'; 10 | import { example } from '../mocks/basic'; 11 | 12 | export default () => { 13 | const theme = useTheme(); 14 | const proChatRef = useRef(); 15 | 16 | return ( 17 |
18 | 31 | { 35 | const mockedData: string = `这是一段模拟的流式字符串数据。本次会话传入了${messages.length}条消息`; 36 | 37 | const mockResponse = new MockResponse(mockedData, 100); 38 | 39 | return mockResponse.getResponse(); 40 | }} 41 | /> 42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/ProChat/hooks/useProChatLocale.ts: -------------------------------------------------------------------------------- 1 | import { Locale, gLocaleObject } from '@/locale'; 2 | import { useMemo } from 'react'; 3 | import { useStore } from '../store'; 4 | 5 | const useProChatLocale = () => { 6 | const locale = useStore((s) => s.locale); 7 | 8 | const localeObject = useMemo(() => { 9 | return gLocaleObject(locale as Locale); 10 | }, [locale]); 11 | 12 | return { 13 | locale, 14 | localeObject, 15 | }; 16 | }; 17 | 18 | export default useProChatLocale; 19 | -------------------------------------------------------------------------------- /src/ProChat/hooks/useRefFunction.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react'; 2 | 3 | const useRefFunction = any>(reFunction: T) => { 4 | const ref = useRef(null); 5 | ref.current = reFunction; 6 | return useCallback((...rest: Parameters): ReturnType => { 7 | return ref.current?.(...(rest as any)); 8 | }, []); 9 | }; 10 | 11 | export { useRefFunction }; 12 | -------------------------------------------------------------------------------- /src/ProChat/index.tsx: -------------------------------------------------------------------------------- 1 | export * from '../types/message'; 2 | 3 | export { type Locale } from '../locale'; 4 | export { ProChat } from './container'; 5 | export { ProChatProvider } from './container/Provider'; 6 | export { useProChat, type ProChatInstance } from './hooks/useProChat'; 7 | -------------------------------------------------------------------------------- /src/ProChat/mocks/basic.ts: -------------------------------------------------------------------------------- 1 | export const example = { 2 | chats: [ 3 | { 4 | content: '昨天的当天是明天的什么?', 5 | createAt: 1697862242452, 6 | id: 'ZGxiX2p4', 7 | role: 'user', 8 | updateAt: 1697862243540, 9 | extra: { 10 | test: 'Test Extra', 11 | }, 12 | }, 13 | { 14 | content: '昨天的当天是明天的昨天。', 15 | createAt: 1697862247302, 16 | id: 'Sb5pAzLL', 17 | parentId: 'ZGxiX2p4', 18 | role: 'assistant', 19 | updateAt: 1697862249387, 20 | model: 'gpt-3.5-turbo', 21 | }, 22 | ], 23 | config: { 24 | model: 'gpt-3.5-turbo', 25 | params: { 26 | frequency_penalty: 0, 27 | presence_penalty: 0, 28 | temperature: 0.6, 29 | top_p: 1, 30 | }, 31 | systemRole: '', 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/ProChat/mocks/customeClassName.ts: -------------------------------------------------------------------------------- 1 | export const example = { 2 | chats: [ 3 | { 4 | content: '我想添加自定义类名', 5 | createAt: 1697862242452, 6 | id: 'ZGxiX2p4', 7 | role: 'user', 8 | updateAt: 1697862243540, 9 | }, 10 | { 11 | content: ` 12 | 如下 13 | 14 | \`\`\`javascript 15 | userMeta={{ 16 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', 17 | title: 'Ant Design', 18 | className: 'my-pro-chat-user', 19 | }} 20 | \`\`\` 21 | 22 | \`\`\`javascript 23 | assistantMeta={{ 24 | avatar: '🛸', 25 | title: '自定义类名', 26 | backgroundColor: '#67dedd', 27 | className: 'my-pro-chat-assistant', 28 | }} 29 | \`\`\` 30 | `, 31 | createAt: 1697862247302, 32 | id: 'Sb5pAzLL', 33 | parentId: 'ZGxiX2p4', 34 | role: 'assistant', 35 | updateAt: 1697862249387, 36 | model: 'gpt-3.5-turbo', 37 | }, 38 | ], 39 | config: { 40 | model: 'gpt-3.5-turbo', 41 | params: { 42 | frequency_penalty: 0, 43 | presence_penalty: 0, 44 | temperature: 0.6, 45 | top_p: 1, 46 | }, 47 | systemRole: '', 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/ProChat/mocks/fullFeature.ts: -------------------------------------------------------------------------------- 1 | export const example = { 2 | chats: [ 3 | { 4 | content: '请展示完整的会话高亮效果?', 5 | createAt: 1697862242452, 6 | id: 'ZGxiX2p4', 7 | role: 'user', 8 | updateAt: 1697862243540, 9 | }, 10 | { 11 | content: ` 12 | # This is an H1 13 | ## This is an H2 14 | ### This is an H3 15 | #### This is an H4 16 | ##### This is an H5 17 | 18 | The point of reference-style links is not that they’re easier to write. The point is that with reference-style links, your document source is vastly more readable. Compare the above examples: using reference-style links, the paragraph itself is only 81 characters long; with inline-style links, it’s 176 characters; and as raw \`HTML\`, it’s 234 characters. In the raw \`HTML\`, there’s more markup than there is text. 19 | 20 | --- 21 | 22 | > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet. 23 | 24 | --- 25 | 26 | an example | *an example* | **an example** 27 | 28 | --- 29 | 30 | 1. Bird 31 | 1. McHale 32 | 1. Parish 33 | 1. Bird 34 | 1. McHale 35 | 1. Parish 36 | 37 | --- 38 | 39 | - Red 40 | - Green 41 | - Blue 42 | - Red 43 | - Green 44 | - Blue 45 | 46 | --- 47 | 48 | This is [an example](http://example.com/ "Title") inline link. 49 | 50 | 51 | 52 | 53 | | title | title | title | 54 | | --- | --- | --- | 55 | | content | content | content | 56 | 57 | 58 | \`\`\`bash 59 | $ pnpm install 60 | \`\`\` 61 | 62 | 63 | \`\`\`javascript 64 | import { renderHook } from '@testing-library/react-hooks'; 65 | import { act } from 'react-dom/test-utils'; 66 | import { useDropNodeOnCanvas } from './useDropNodeOnCanvas'; 67 | \`\`\` 68 | 69 | --- 70 | 71 | 以下是一段Markdown格式的LaTeX数学公式: 72 | 73 | 我是一个行内公式:$E=mc^2$ 74 | 75 | 我是一个独立公式: 76 | $$ 77 | \\sum_{i=1}^{n} x_i = x_1 + x_2 + \\ldots + x_n 78 | $$ 79 | 80 | 我是一个带有分式的公式: 81 | $$ 82 | \\frac{{n!}}{{k!(n-k)!}} = \\binom{n}{k} 83 | $$ 84 | 85 | 我是一个带有上下标的公式: 86 | $$ 87 | x^{2} + y^{2} = r^{2} 88 | $$ 89 | 90 | 我是一个带有积分符号的公式: 91 | $$ 92 | \\int_{a}^{b} f(x) \\, dx 93 | $$ 94 | `, 95 | createAt: 1697862247302, 96 | id: 'Sb5pAzLL', 97 | parentId: 'ZGxiX2p4', 98 | role: 'assistant', 99 | updateAt: 1697862249387, 100 | model: 'gpt-3.5-turbo', 101 | }, 102 | ], 103 | config: { 104 | model: 'gpt-3.5-turbo', 105 | params: { 106 | frequency_penalty: 0, 107 | presence_penalty: 0, 108 | temperature: 0.6, 109 | top_p: 1, 110 | }, 111 | systemRole: '', 112 | }, 113 | }; 114 | -------------------------------------------------------------------------------- /src/ProChat/mocks/sseResponse.ts: -------------------------------------------------------------------------------- 1 | export class MockSSEResponse { 2 | private controller!: ReadableStreamDefaultController; 3 | private encoder = new TextEncoder(); 4 | 5 | private stream: ReadableStream; 6 | 7 | constructor( 8 | private dataArray: string[], 9 | private delay: number = 300, 10 | ) { 11 | this.stream = new ReadableStream({ 12 | start: (controller) => { 13 | this.controller = controller; 14 | this.pushData(); 15 | }, 16 | }); 17 | } 18 | 19 | private pushData() { 20 | if (this.dataArray.length === 0) { 21 | this.controller.close(); 22 | return; 23 | } 24 | 25 | const chunk = this.dataArray.shift(); 26 | 27 | this.controller.enqueue(this.encoder.encode(chunk)); 28 | 29 | setTimeout(() => this.pushData(), this.delay); 30 | } 31 | 32 | getResponse() { 33 | return new Response(this.stream); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ProChat/mocks/streamResponse.ts: -------------------------------------------------------------------------------- 1 | export class MockResponse { 2 | private controller!: ReadableStreamDefaultController; 3 | private encoder = new TextEncoder(); 4 | private stream: ReadableStream; 5 | private error: boolean; 6 | 7 | constructor( 8 | private data: string, 9 | private delay: number = 100, 10 | error: boolean = false, // 新增参数,默认为false 11 | ) { 12 | this.error = error; 13 | 14 | this.stream = new ReadableStream({ 15 | start: (controller) => { 16 | this.controller = controller; 17 | if (!this.error) { 18 | // 如果不是错误情况,则开始推送数据 19 | setTimeout(() => this.pushData(), this.delay); // 延迟开始推送数据 20 | } 21 | }, 22 | cancel(reason) { 23 | console.log('Stream canceled', reason); 24 | }, 25 | }); 26 | } 27 | 28 | private pushData() { 29 | if (this.data.length === 0) { 30 | this.controller.close(); 31 | return; 32 | } 33 | 34 | const characters = Array.from(this.data); 35 | if (characters.length === 0) { 36 | this.controller.close(); 37 | return; 38 | } 39 | 40 | const chunk = characters.shift(); 41 | this.data = characters.join(''); 42 | 43 | this.controller.enqueue(this.encoder.encode(chunk)); 44 | 45 | if (this.data.length > 0) { 46 | setTimeout(() => this.pushData(), this.delay); 47 | } else { 48 | // 数据全部发送完毕后关闭流 49 | setTimeout(() => this.controller.close(), this.delay); 50 | } 51 | } 52 | 53 | getResponse(): Promise { 54 | return new Promise((resolve) => { 55 | // 使用setTimeout来模拟网络延迟 56 | setTimeout(() => { 57 | if (this.error) { 58 | const errorResponseOptions = { status: 500, statusText: 'Internal Server Error' }; 59 | // 返回模拟的网络错误响应,这里我们使用500状态码作为示例 60 | resolve(new Response(null, errorResponseOptions)); 61 | } else { 62 | resolve(new Response(this.stream)); 63 | } 64 | }, this.delay); // 使用构造函数中设置的delay值作为延迟时间 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/ProChat/mocks/threebody.ts: -------------------------------------------------------------------------------- 1 | export const chats = { 2 | chats: { 3 | ZGxiX2p4: { 4 | content: '我对三体世界说话。', 5 | createAt: 1697862242452, 6 | id: 'ZGxiX2p4', 7 | role: 'user', 8 | updateAt: 1697862243540, 9 | }, 10 | Sb5pAzLL: { 11 | content: '保持静默,不要回答,不要回答。', 12 | createAt: 1697862247302, 13 | id: 'Sb5pAzLL', 14 | parentId: 'ZGxiX2p4', 15 | role: 'assistant', 16 | updateAt: 1697862249387, 17 | model: 'gpt-3.5-turbo', 18 | }, 19 | }, 20 | config: { 21 | model: 'gpt-3.5-turbo', 22 | params: { 23 | frequency_penalty: 0, 24 | presence_penalty: 0, 25 | temperature: 0.6, 26 | top_p: 1, 27 | }, 28 | systemRole: '', 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/ProChat/store/index.ts: -------------------------------------------------------------------------------- 1 | import { StoreApi } from 'zustand'; 2 | import { createContext } from 'zustand-utils'; 3 | import { ChatStore } from './store'; 4 | 5 | export type { ChatState } from './initialState'; 6 | export * from './store'; 7 | 8 | export const { useStore, useStoreApi, Provider } = createContext>(); 9 | -------------------------------------------------------------------------------- /src/ProChat/store/selectors/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | chatsMessageString, 3 | currentChats, 4 | currentChatsWithGuideMessage, 5 | currentChatsWithHistoryConfig, 6 | } from './chat'; 7 | 8 | export const chatSelectors = { 9 | chatsMessageString, 10 | currentChats, 11 | currentChatsWithGuideMessage, 12 | currentChatsWithHistoryConfig, 13 | }; 14 | -------------------------------------------------------------------------------- /src/ProChat/store/store.ts: -------------------------------------------------------------------------------- 1 | import { StateCreator } from 'zustand/vanilla'; 2 | 3 | import { ChatListProps } from '@/ChatList'; 4 | import { MetaData } from '@/ProChat/types/meta'; 5 | import { MarkdownProps } from '@ant-design/pro-editor'; 6 | import isEqual from 'fast-deep-equal'; 7 | import { merge } from 'lodash-es'; 8 | import { optionalDevtools } from 'zustand-utils'; 9 | import { DevtoolsOptions } from 'zustand/middleware'; 10 | import { createWithEqualityFn } from 'zustand/traditional'; 11 | import { ChatAction, chatAction } from './action'; 12 | import { ChatPropsState, ChatState, initialState } from './initialState'; 13 | 14 | export interface ChatProps = Record> 15 | extends Partial> { 16 | // init 17 | loading?: boolean; 18 | initialChats?: ChatPropsState['chats']; 19 | userMeta?: MetaData; 20 | assistantMeta?: MetaData; 21 | /** 22 | * @description 聊天项的渲染函数 23 | */ 24 | chatItemRenderConfig?: ChatListProps['chatItemRenderConfig']; 25 | /** 26 | * @description markdown组件的参数 27 | */ 28 | markdownProps?: MarkdownProps; 29 | /** 30 | * @description 判断聊天项的更新函数 31 | */ 32 | itemShouldUpdate?: ChatListProps['itemShouldUpdate']; 33 | } 34 | 35 | // =============== 聚合 createStoreFn ============ // 36 | 37 | export type ChatStore = ChatAction & ChatState; 38 | 39 | const vanillaStore = 40 | ({ 41 | loading, 42 | initialChats, 43 | chats, 44 | ...props 45 | }: ChatProps): StateCreator => 46 | (...parameters) => { 47 | // initState = innerState + props 48 | 49 | const finalInitChats = chats ?? initialChats; 50 | 51 | const state = merge({}, initialState, { 52 | init: !loading, 53 | chats: Array.isArray(finalInitChats) ? finalInitChats : Object.values(finalInitChats || {}), 54 | ...props, 55 | } as ChatState); 56 | 57 | return { 58 | ...state, 59 | ...chatAction(...parameters), 60 | }; 61 | }; 62 | // 63 | 64 | // =============== 封装 createStore ============ // 65 | 66 | const PRO_CHAT = 'PRO_CHAT'; 67 | 68 | const isDev = process.env.NODE_ENV === 'development'; 69 | 70 | export const createStore = (props: ChatProps, options: boolean | DevtoolsOptions = false) => { 71 | const devtools = optionalDevtools(options !== false); 72 | 73 | const devtoolOptions = 74 | options === false 75 | ? undefined 76 | : options === true 77 | ? { name: PRO_CHAT + (isDev ? '_DEV' : '') } 78 | : options; 79 | 80 | return createWithEqualityFn()(devtools(vanillaStore(props), devtoolOptions), isEqual); 81 | }; 82 | -------------------------------------------------------------------------------- /src/ProChat/types/chat.ts: -------------------------------------------------------------------------------- 1 | import { LLMRoleType } from '@/types/llm'; 2 | import { OpenAIFunctionCall } from '@/types/message'; 3 | 4 | export interface OpenAIChatMessage { 5 | /** 6 | * @title 内容 7 | * @description 消息内容 8 | */ 9 | content: React.ReactNode; 10 | 11 | function_call?: OpenAIFunctionCall; 12 | name?: string; 13 | /** 14 | * 角色 15 | * @description 消息发送者的角色 16 | */ 17 | role: LLMRoleType | string; 18 | } 19 | 20 | /** 21 | * @title OpenAI Stream Payload 22 | */ 23 | export interface ChatStreamPayload { 24 | /** 25 | * @title 控制生成文本中的惩罚系数,用于减少重复性 26 | * @default 0 27 | */ 28 | frequency_penalty?: number; 29 | /** 30 | * @title 生成文本的最大长度 31 | */ 32 | max_tokens?: number; 33 | /** 34 | * @title 聊天信息列表 35 | */ 36 | messages: OpenAIChatMessage[]; 37 | /** 38 | * @title 模型名称 39 | */ 40 | model: string; 41 | /** 42 | * @title 返回的文本数量 43 | */ 44 | n?: number; 45 | /** 46 | * 开启的插件列表 47 | */ 48 | plugins?: string[]; 49 | /** 50 | * @title 控制生成文本中的惩罚系数,用于减少主题的变化 51 | * @default 0 52 | */ 53 | presence_penalty?: number; 54 | /** 55 | * @title 是否开启流式请求 56 | * @default true 57 | */ 58 | stream?: boolean; 59 | /** 60 | * @title 生成文本的随机度量,用于控制文本的创造性和多样性 61 | * @default 0.5 62 | */ 63 | temperature: number; 64 | 65 | /** 66 | * @title 控制生成文本中最高概率的单个令牌 67 | * @default 1 68 | */ 69 | top_p?: number; 70 | } 71 | -------------------------------------------------------------------------------- /src/ProChat/types/config.ts: -------------------------------------------------------------------------------- 1 | // 语言模型的设置参数 2 | export interface ModelParams { 3 | /** 4 | * 控制生成文本中的惩罚系数,用于减少重复性 5 | * @default 0 6 | */ 7 | frequency_penalty?: number; 8 | /** 9 | * 生成文本的最大长度 10 | */ 11 | max_tokens?: number; 12 | /** 13 | * 控制生成文本中的惩罚系数,用于减少主题的变化 14 | * @default 0 15 | */ 16 | presence_penalty?: number; 17 | /** 18 | * 生成文本的随机度量,用于控制文本的创造性和多样性 19 | * @default 0.6 20 | */ 21 | temperature?: number; 22 | /** 23 | * 控制生成文本中最高概率的单个 token 24 | * @default 1 25 | */ 26 | top_p?: number; 27 | 28 | [key: string]: any; 29 | } 30 | 31 | export type ModelRoleType = 'user' | 'system' | 'assistant' | 'function'; 32 | 33 | export interface LLMMessage { 34 | content: string; 35 | role: ModelRoleType; 36 | } 37 | 38 | export type LLMFewShots = LLMMessage[]; 39 | 40 | export interface ModelConfig { 41 | compressThreshold?: number; 42 | /** 43 | * 历史消息长度压缩阈值 44 | */ 45 | enableCompressThreshold?: boolean; 46 | /** 47 | * 开启历史记录条数 48 | */ 49 | enableHistoryCount?: boolean; 50 | enableMaxTokens?: boolean; 51 | /** 52 | * 语言模型示例 53 | */ 54 | fewShots?: LLMFewShots; 55 | /** 56 | * 历史消息条数 57 | */ 58 | historyCount?: number; 59 | inputTemplate?: string; 60 | /** 61 | * 角色所使用的语言模型 62 | */ 63 | model?: string; 64 | /** 65 | * 语言模型参数 66 | */ 67 | params?: ModelParams; 68 | /** 69 | * 系统角色 70 | */ 71 | systemRole?: string; 72 | } 73 | -------------------------------------------------------------------------------- /src/ProChat/types/meta.ts: -------------------------------------------------------------------------------- 1 | export interface MetaData { 2 | /** 3 | * 角色头像 4 | * @description 可选参数,如果不传则使用默认头像 5 | */ 6 | avatar?: string; 7 | /** 8 | * 背景色 9 | * @description 可选参数,如果不传则使用默认背景色 10 | */ 11 | backgroundColor?: string; 12 | /** 13 | * 名称 14 | * @description 可选参数,如果不传则使用默认名称 15 | */ 16 | title?: string; 17 | 18 | /** 19 | * 附加类名 20 | * @description 可选参数,如果不传则无 21 | */ 22 | className?: string; 23 | 24 | /** 25 | * 附加数据 26 | * @description 可选参数,如果不传则使用默认名称 27 | */ 28 | [key: string]: any; 29 | } 30 | -------------------------------------------------------------------------------- /src/ProChat/utils/merge.ts: -------------------------------------------------------------------------------- 1 | import { merge as _merge, mergeWith } from 'lodash-es'; 2 | 3 | /** 4 | * 用于合并对象,如果是数组则直接替换 5 | * @param target 6 | * @param source 7 | */ 8 | export const merge: typeof _merge = (target: T, source: T) => 9 | mergeWith({}, target, source, (obj, src) => { 10 | if (Array.isArray(obj)) return src; 11 | }); 12 | -------------------------------------------------------------------------------- /src/ProChat/utils/message.ts: -------------------------------------------------------------------------------- 1 | import { FUNCTION_MESSAGE_FLAG } from '@/ProChat/const/message'; 2 | import { ModelConfig } from '@/ProChat/types/config'; 3 | import { ChatMessage } from '@/types/message'; 4 | 5 | export const isFunctionMessage = (content: string) => { 6 | return content.startsWith(FUNCTION_MESSAGE_FLAG); 7 | }; 8 | 9 | export const getSlicedMessagesWithConfig = ( 10 | messages: ChatMessage[], 11 | config: ModelConfig, 12 | ): ChatMessage[] => { 13 | // 如果没有开启历史消息数限制,或者限制为 0,则直接返回 14 | if (!config.enableHistoryCount || !config.historyCount) return messages; 15 | 16 | // 如果开启了,则返回尾部的N条消息 17 | return messages.reverse().slice(0, config.historyCount).reverse(); 18 | }; 19 | -------------------------------------------------------------------------------- /src/ProChat/utils/storeDebug.ts: -------------------------------------------------------------------------------- 1 | export const setNamespace = (namespace: string) => { 2 | return (type: string, payload?: any) => { 3 | const name = [namespace, type].filter(Boolean).join('/'); 4 | return payload 5 | ? { 6 | payload, 7 | type: name, 8 | } 9 | : name; 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/ProChat/utils/uuid.ts: -------------------------------------------------------------------------------- 1 | // generate('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 16); //=> "4f90d13a42" 2 | import { customAlphabet } from 'nanoid/non-secure'; 3 | 4 | export const nanoid = customAlphabet( 5 | '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 6 | 8, 7 | ); 8 | -------------------------------------------------------------------------------- /src/TokenTag/demos/index.tsx: -------------------------------------------------------------------------------- 1 | import { TokenTag, TokenTagProps } from '@ant-design/pro-chat'; 2 | 3 | export default () => { 4 | const control: TokenTagProps | any = { 5 | maxValue: { 6 | step: 1, 7 | value: 5000, 8 | }, 9 | value: { 10 | step: 1, 11 | value: 1000, 12 | }, 13 | }; 14 | 15 | return ; 16 | }; 17 | -------------------------------------------------------------------------------- /src/TokenTag/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: Components 3 | group: Chat 4 | title: TokenTag 5 | description: The TokenTag component is used to display a token tag with a FluentEmoji icon and a text indicating the remaining tokens. 6 | order: 10 7 | --- 8 | 9 | ## Default 10 | 11 | The remaining tokens are calculated based on the `maxValue` and `value` props. The component has three types of visual styles: normal, low, and overload, which are determined by the percentage of remaining tokens. The component is memoized for performance optimization. 12 | 13 | 14 | 15 | ## APIs 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/TokenTag/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 组件 3 | group: Chat 4 | title: TokenTag 5 | description: The TokenTag component is used to display a token tag with a FluentEmoji icon and a text indicating the remaining tokens. 6 | order: 10 7 | --- 8 | 9 | ## Default 10 | 11 | The remaining tokens are calculated based on the `maxValue` and `value` props. The component has three types of visual styles: normal, low, and overload, which are determined by the percentage of remaining tokens. The component is memoized for performance optimization. 12 | 13 | 14 | 15 | ## APIs 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/TokenTag/index.tsx: -------------------------------------------------------------------------------- 1 | import { useResponsive } from 'antd-style'; 2 | import { forwardRef } from 'react'; 3 | 4 | import Emoji from '@/Emoji'; 5 | import { DivProps } from '@/types'; 6 | 7 | import { ICON_SIZE, useStyles } from './style'; 8 | 9 | export interface TokenTagProps extends DivProps { 10 | /** 11 | * @default 'left' 12 | */ 13 | displayMode?: 'remained' | 'used'; 14 | /** 15 | * @description Maximum value for the token 16 | */ 17 | maxValue: number; 18 | shape?: 'round' | 'square'; 19 | text?: { 20 | overload?: string; 21 | remained?: string; 22 | used?: string; 23 | }; 24 | /** 25 | * @description Current value of the token 26 | */ 27 | value: number; 28 | } 29 | 30 | const TokenTag = forwardRef( 31 | ( 32 | { className, displayMode = 'remained', maxValue, value, text, shape = 'round', ...props }, 33 | ref, 34 | ) => { 35 | const { mobile } = useResponsive(); 36 | const valueLeft = maxValue - value; 37 | const percent = valueLeft / maxValue; 38 | let type: 'normal' | 'low' | 'overload'; 39 | let emoji; 40 | 41 | if (percent > 0.3) { 42 | type = 'normal'; 43 | emoji = '😀'; 44 | } else if (percent > 0) { 45 | type = 'low'; 46 | emoji = '😅'; 47 | } else { 48 | type = 'overload'; 49 | emoji = '🤯'; 50 | } 51 | 52 | const { styles, cx } = useStyles({ shape, type }); 53 | 54 | return ( 55 |
56 | 57 | {valueLeft > 0 58 | ? [ 59 | mobile 60 | ? '' 61 | : displayMode === 'remained' 62 | ? text?.remained || 'Remained' 63 | : text?.used || 'Used', 64 | displayMode === 'remained' ? valueLeft : value, 65 | ].join(' ') 66 | : text?.overload || 'Overload'} 67 |
68 | ); 69 | }, 70 | ); 71 | 72 | export default TokenTag; 73 | -------------------------------------------------------------------------------- /src/TokenTag/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | const HEIGHT = 28; 4 | export const ICON_SIZE = 20; 5 | 6 | export const useStyles = createStyles( 7 | ( 8 | { cx, css, token, isDarkMode }, 9 | { type, shape }: { shape: 'round' | 'square'; type: 'normal' | 'low' | 'overload' }, 10 | ) => { 11 | let percentStyle; 12 | 13 | switch (type) { 14 | case 'normal': { 15 | percentStyle = css` 16 | color: ${token.colorSuccessText}; 17 | `; 18 | break; 19 | } 20 | case 'low': { 21 | percentStyle = css` 22 | color: ${token.colorWarningText}; 23 | `; 24 | break; 25 | } 26 | case 'overload': { 27 | percentStyle = css` 28 | color: ${token.colorErrorText}; 29 | `; 30 | break; 31 | } 32 | } 33 | 34 | const roundStylish = css` 35 | padding: 0 ${(HEIGHT - ICON_SIZE) * 1.2}px 0 ${(HEIGHT - ICON_SIZE) / 2}px; 36 | background: ${isDarkMode ? token.colorFillSecondary : token.colorFillTertiary}; 37 | border-radius: ${HEIGHT / 2}px; 38 | `; 39 | 40 | const squareStylish = css` 41 | border-radius: ${token.borderRadiusSM}px; 42 | `; 43 | 44 | return { 45 | container: cx( 46 | percentStyle, 47 | shape === 'round' ? roundStylish : squareStylish, 48 | css` 49 | user-select: none; 50 | 51 | overflow: hidden; 52 | display: flex; 53 | flex: 0; 54 | gap: 4px; 55 | align-items: center; 56 | 57 | min-width: fit-content; 58 | height: ${HEIGHT}px; 59 | 60 | font-family: ${token.fontFamilyCode}; 61 | font-size: 13px; 62 | line-height: 1; 63 | `, 64 | ), 65 | }; 66 | }, 67 | ); 68 | -------------------------------------------------------------------------------- /src/components/Avatar/getEmojiByCharacter.ts: -------------------------------------------------------------------------------- 1 | import emojiRegex from 'emoji-regex'; 2 | 3 | export const getEmoji = (emoji: string): string | undefined => { 4 | const regex = emojiRegex(); 5 | return emoji.match(regex)?.[0]; 6 | }; 7 | -------------------------------------------------------------------------------- /src/components/Avatar/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | import { readableColor } from 'polished'; 3 | 4 | export const useStyles = createStyles( 5 | ( 6 | { css, token, prefixCls }, 7 | { background, size, isEmoji }: { background?: string; isEmoji?: boolean; size: number }, 8 | ) => { 9 | const backgroundColor = background ?? token.colorBgContainer; 10 | const color = readableColor(backgroundColor); 11 | 12 | return { 13 | avatar: css` 14 | cursor: pointer; 15 | 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | 20 | background: ${backgroundColor}; 21 | border: 1px solid ${background ? 'transparent' : token.colorSplit}; 22 | 23 | > .${prefixCls}-avatar-string { 24 | font-size: ${size * (isEmoji ? 0.7 : 0.5)}px; 25 | font-weight: 700; 26 | line-height: 1 !important; 27 | color: ${color}; 28 | } 29 | 30 | > * { 31 | cursor: pointer; 32 | } 33 | `, 34 | }; 35 | }, 36 | ); 37 | -------------------------------------------------------------------------------- /src/components/CopyButton/index.tsx: -------------------------------------------------------------------------------- 1 | import copy from 'copy-to-clipboard'; 2 | import { Copy } from 'lucide-react'; 3 | import { memo } from 'react'; 4 | 5 | import ActionIcon, { type ActionIconSize } from '@/ActionIcon'; 6 | import { useCopied } from '@/hooks/useCopied'; 7 | import { DivProps } from '@/types'; 8 | import { type TooltipProps } from 'antd'; 9 | 10 | export interface CopyButtonProps extends DivProps { 11 | /** 12 | * @description Additional class name 13 | */ 14 | className?: string; 15 | /** 16 | * @description The text content to be copied 17 | */ 18 | content: string; 19 | /** 20 | * @description The placement of the tooltip 21 | * @enum ['top', 'left', 'right', 'bottom', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight', 'leftTop', 'leftBottom', 'rightTop', 'rightBottom'] 22 | * @default 'right' 23 | */ 24 | placement?: TooltipProps['placement']; 25 | /** 26 | * @description The size of the icon 27 | * @enum ['large', 'normal', 'small', 'site'] 28 | * @default 'site' 29 | */ 30 | size?: ActionIconSize; 31 | } 32 | 33 | const CopyButton = memo( 34 | ({ content, className, placement = 'right', size = 'site', ...props }) => { 35 | const { copied, setCopied } = useCopied(); 36 | 37 | return ( 38 | { 44 | copy(content); 45 | setCopied(); 46 | }} 47 | placement={placement} 48 | size={size} 49 | title={copied ? '✅ Success' : 'Copy'} 50 | /> 51 | ); 52 | }, 53 | ); 54 | 55 | export default CopyButton; 56 | -------------------------------------------------------------------------------- /src/components/Form/components/FormDivider.tsx: -------------------------------------------------------------------------------- 1 | import { Divider as AntDivider, DividerProps } from 'antd'; 2 | import { memo } from 'react'; 3 | 4 | export type FormDividerProps = DividerProps; 5 | const Divider = memo(({ style, ...props }) => ( 6 | 7 | )); 8 | export default Divider; 9 | -------------------------------------------------------------------------------- /src/components/Form/components/FormFooter.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | import { DivProps } from '@/types'; 4 | 5 | import { useStyles } from './style'; 6 | 7 | export type FormFooterProps = DivProps; 8 | 9 | const FormFooter = memo(({ className, children, ...props }) => { 10 | const { cx, styles } = useStyles(); 11 | return ( 12 |
13 | {children} 14 |
15 | ); 16 | }); 17 | 18 | export default FormFooter; 19 | -------------------------------------------------------------------------------- /src/components/Form/components/FormGroup.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { type IconProps } from '@/Icon'; 2 | import { Collapse, type CollapseProps } from 'antd'; 3 | import { useResponsive } from 'antd-style'; 4 | import { ChevronDown } from 'lucide-react'; 5 | import { memo, type ReactNode } from 'react'; 6 | import { Flexbox } from 'react-layout-kit'; 7 | 8 | import { useStyles } from './style'; 9 | 10 | export interface FormGroupProps extends CollapseProps { 11 | children: ReactNode; 12 | extra?: ReactNode; 13 | icon?: IconProps['icon']; 14 | title: string; 15 | } 16 | 17 | const FormGroup = memo(({ className, icon, title, children, extra, ...props }) => { 18 | const { mobile } = useResponsive(); 19 | const { cx, styles } = useStyles(); 20 | 21 | const titleContent = ( 22 |
23 | {icon && } 24 | {title} 25 |
26 | ); 27 | 28 | if (mobile) 29 | return ( 30 | 31 | 32 | {titleContent} 33 | {extra} 34 | 35 |
{children}
36 |
37 | ); 38 | 39 | return ( 40 | ( 44 | 50 | )} 51 | items={[ 52 | { 53 | children, 54 | extra, 55 | key: 1, 56 | label: titleContent, 57 | }, 58 | ]} 59 | key={1} 60 | {...props} 61 | /> 62 | ); 63 | }); 64 | 65 | export default FormGroup; 66 | -------------------------------------------------------------------------------- /src/components/Form/components/FormItem.tsx: -------------------------------------------------------------------------------- 1 | import { FormItemProps as AntdFormItemProps, Form } from 'antd'; 2 | import { memo } from 'react'; 3 | 4 | import FormDivider from './FormDivider'; 5 | import FormTitle, { type FormTitleProps } from './FormTitle'; 6 | import { useStyles } from './style'; 7 | 8 | const { Item } = Form; 9 | 10 | export interface FormItemProps extends AntdFormItemProps { 11 | avatar?: FormTitleProps['avatar']; 12 | desc?: FormTitleProps['desc']; 13 | divider?: boolean; 14 | hidden?: boolean; 15 | minWidth?: string | number; 16 | tag?: FormTitleProps['tag']; 17 | } 18 | 19 | const FormItem = memo( 20 | ({ desc, tag, minWidth, avatar, className, label, children, divider, ...props }) => { 21 | const { cx, styles } = useStyles(minWidth); 22 | return ( 23 | <> 24 | {divider && } 25 | } 28 | {...props} 29 | > 30 | {children} 31 | 32 | 33 | ); 34 | }, 35 | ); 36 | 37 | export default FormItem; 38 | -------------------------------------------------------------------------------- /src/components/Form/components/FormTitle.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, memo } from 'react'; 2 | import { Flexbox } from 'react-layout-kit'; 3 | 4 | import Tag from '@/components/Tag'; 5 | import { DivProps } from '@/types'; 6 | 7 | import { useStyles } from './style'; 8 | 9 | export interface FormTitleProps extends DivProps { 10 | avatar?: ReactNode; 11 | desc?: ReactNode; 12 | tag?: string; 13 | title: string; 14 | } 15 | 16 | const FormTitle = memo(({ className, tag, title, desc, avatar }) => { 17 | const { cx, styles } = useStyles(); 18 | const titleNode = ( 19 |
20 | 21 | {title} 22 | {tag && {tag}} 23 | 24 | {desc && {desc}} 25 |
26 | ); 27 | 28 | if (avatar) { 29 | return ( 30 | 31 | {avatar} 32 | {titleNode} 33 | 34 | ); 35 | } 36 | return titleNode; 37 | }); 38 | 39 | export default FormTitle; 40 | -------------------------------------------------------------------------------- /src/components/Form/index.tsx: -------------------------------------------------------------------------------- 1 | import { Form as AntForm, FormProps as AntFormProps, type FormInstance } from 'antd'; 2 | import { RefAttributes, forwardRef, type ReactNode } from 'react'; 3 | 4 | import FormFooter from './components/FormFooter'; 5 | import FormGroup, { type FormGroupProps } from './components/FormGroup'; 6 | import FormItem, { type FormItemProps } from './components/FormItem'; 7 | import { useStyles } from './style'; 8 | 9 | export interface ItemGroup { 10 | children: FormItemProps[]; 11 | extra?: FormGroupProps['extra']; 12 | icon?: FormGroupProps['icon']; 13 | title: FormGroupProps['title']; 14 | } 15 | 16 | export interface FormProps extends AntFormProps { 17 | children?: ReactNode; 18 | footer?: ReactNode; 19 | itemMinWidth?: FormItemProps['minWidth']; 20 | items?: ItemGroup[]; 21 | } 22 | 23 | const FormParent = forwardRef( 24 | ({ className, itemMinWidth, footer, form, items, children, ...props }, ref) => { 25 | const { cx, styles } = useStyles(); 26 | return ( 27 | 35 | {items?.map((group, groupIndex) => ( 36 | 37 | {group.children 38 | .filter((item) => !item.hidden) 39 | .map((item, itemIndex) => { 40 | return ( 41 | 47 | ); 48 | })} 49 | 50 | ))} 51 | {children} 52 | {footer && {footer}} 53 | 54 | ); 55 | }, 56 | ); 57 | 58 | export interface IForm { 59 | (props: FormProps & RefAttributes): ReactNode; 60 | Group: typeof FormGroup; 61 | Item: typeof FormItem; 62 | } 63 | 64 | const Form = FormParent as unknown as IForm; 65 | 66 | Form.Item = FormItem; 67 | Form.Group = FormGroup; 68 | 69 | export default Form; 70 | -------------------------------------------------------------------------------- /src/components/Form/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ css, token, prefixCls, responsive }) => ({ 4 | footer: css` 5 | display: flex; 6 | gap: 8px; 7 | justify-content: flex-end; 8 | `, 9 | form: css` 10 | display: flex; 11 | flex-direction: column; 12 | gap: 16px; 13 | 14 | ${responsive.mobile} { 15 | gap: 0; 16 | } 17 | 18 | .${prefixCls}-form-item { 19 | margin: 0 !important; 20 | } 21 | 22 | .${prefixCls}-form-item .${prefixCls}-form-item-label > label { 23 | height: unset; 24 | } 25 | 26 | .${prefixCls}-row { 27 | position: relative; 28 | flex-wrap: nowrap; 29 | } 30 | 31 | .${prefixCls}-form-item-label { 32 | position: relative; 33 | flex: 1; 34 | max-width: 100%; 35 | } 36 | 37 | .${prefixCls}-form-item-control { 38 | position: relative; 39 | flex: 0; 40 | min-width: unset !important; 41 | } 42 | 43 | .${prefixCls}-collapse-item { 44 | overflow: hidden !important; 45 | border-radius: ${token.borderRadius}px !important; 46 | } 47 | `, 48 | })); 49 | -------------------------------------------------------------------------------- /src/components/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import { Input as AntInput, type InputProps as AntdInputProps, type InputRef } from 'antd'; 2 | import { TextAreaProps as AntdTextAreaProps, type TextAreaRef } from 'antd/es/input/TextArea'; 3 | import { forwardRef } from 'react'; 4 | 5 | import { useStyles } from './style'; 6 | 7 | export interface InputProps extends AntdInputProps { 8 | /** 9 | * @description Type of the input 10 | * @default 'ghost' 11 | */ 12 | type?: 'ghost' | 'block' | 'pure'; 13 | } 14 | 15 | export const Input = forwardRef( 16 | ({ className, type = 'ghost', ...props }, reference) => { 17 | const { styles, cx } = useStyles({ type }); 18 | 19 | return ( 20 | 26 | ); 27 | }, 28 | ); 29 | 30 | export interface TextAreaProps extends AntdTextAreaProps { 31 | /** 32 | * @description Whether to enable resizing of the textarea 33 | * @default true 34 | */ 35 | resize?: boolean; 36 | /** 37 | * @description Type of the textarea 38 | * @default 'ghost' 39 | */ 40 | type?: 'ghost' | 'block' | 'pure'; 41 | } 42 | 43 | export const TextArea = forwardRef( 44 | ({ className, type = 'ghost', resize = true, style, ...props }, reference) => { 45 | const { styles, cx } = useStyles({ type }); 46 | 47 | return ( 48 | 55 | ); 56 | }, 57 | ); 58 | -------------------------------------------------------------------------------- /src/components/Input/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles( 4 | ({ cx, css, token, prefixCls }, { type }: { type: 'ghost' | 'block' | 'pure' }) => { 5 | const typeStylish = css` 6 | background-color: ${type === 'block' ? token.colorFillTertiary : 'transparent'}; 7 | border: 1px solid ${type === 'block' ? 'transparent' : token.colorBorder}; 8 | transition: 9 | background-color 100ms ${token.motionEaseOut}, 10 | border-color 200ms ${token.motionEaseOut}; 11 | 12 | &:hover { 13 | background-color: ${token.colorFillTertiary}; 14 | } 15 | 16 | &:focus { 17 | border-color: ${token.colorTextQuaternary}; 18 | } 19 | 20 | &.${prefixCls}-input-affix-wrapper-focused { 21 | border-color: ${token.colorTextQuaternary}; 22 | } 23 | `; 24 | 25 | return { 26 | input: cx( 27 | type !== 'pure' && typeStylish, 28 | css` 29 | position: relative; 30 | max-width: 100%; 31 | height: ${type === 'pure' ? 'unset' : '36px'}; 32 | padding: ${type === 'pure' ? '0' : '0 12px'}; 33 | 34 | input { 35 | background: transparent; 36 | } 37 | `, 38 | ), 39 | textarea: cx( 40 | type !== 'pure' && typeStylish, 41 | css` 42 | position: relative; 43 | max-width: 100%; 44 | padding: ${type === 'pure' ? '0' : '8px 12px'}; 45 | border-radius: ${type === 'pure' ? '0' : `${token.borderRadius}px`}; 46 | 47 | textarea { 48 | background: transparent; 49 | } 50 | `, 51 | ), 52 | }; 53 | }, 54 | ); 55 | -------------------------------------------------------------------------------- /src/components/SliderWithInput/index.tsx: -------------------------------------------------------------------------------- 1 | import { InputNumber, Slider, type InputNumberProps } from 'antd'; 2 | import { SliderSingleProps } from 'antd/es/slider'; 3 | import { isNull } from 'lodash-es'; 4 | import { memo, useCallback } from 'react'; 5 | import { Flexbox } from 'react-layout-kit'; 6 | 7 | export interface SliderWithInputProps extends SliderSingleProps { 8 | controls?: InputNumberProps['controls']; 9 | size?: InputNumberProps['size']; 10 | } 11 | 12 | const SliderWithInput = memo( 13 | ({ 14 | step, 15 | value, 16 | onChange, 17 | max, 18 | min, 19 | defaultValue, 20 | size, 21 | controls, 22 | style, 23 | className, 24 | disabled, 25 | ...props 26 | }) => { 27 | const handleOnchange = useCallback((value: number | null) => { 28 | if (Number.isNaN(value) || isNull(value)) return; 29 | onChange?.(value); 30 | }, []); 31 | 32 | return ( 33 | 40 | 52 | 64 | 65 | ); 66 | }, 67 | ); 68 | 69 | export default SliderWithInput; 70 | -------------------------------------------------------------------------------- /src/components/Spotlight/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useEffect, useRef, useState } from 'react'; 2 | 3 | import { DivProps } from '@/types'; 4 | 5 | import { useStyles } from './style'; 6 | 7 | const useMouseOffset = (): any => { 8 | const [offset, setOffset] = useState<{ x: number; y: number }>(); 9 | const [outside, setOutside] = useState(true); 10 | const reference = useRef(); 11 | 12 | useEffect(() => { 13 | if (reference.current && reference.current.parentElement) { 14 | const element = reference.current.parentElement; 15 | 16 | // debounce? 17 | const onMouseMove = (e: MouseEvent) => { 18 | const bound = element.getBoundingClientRect(); 19 | setOffset({ x: e.clientX - bound.x, y: e.clientY - bound.y }); 20 | setOutside(false); 21 | }; 22 | 23 | const onMouseLeave = () => { 24 | setOutside(true); 25 | }; 26 | element.addEventListener('mousemove', onMouseMove); 27 | element.addEventListener('mouseleave', onMouseLeave); 28 | return () => { 29 | element.removeEventListener('mousemove', onMouseMove); 30 | element.removeEventListener('mouseleave', onMouseLeave); 31 | }; 32 | } 33 | }, []); 34 | 35 | return [offset, outside, reference] as const; 36 | }; 37 | 38 | export interface SpotlightProps extends DivProps { 39 | /** 40 | * @description The size of the spotlight circle 41 | * @default 64 42 | */ 43 | size?: number; 44 | } 45 | 46 | const Spotlight = memo(({ className, size = 64, ...properties }) => { 47 | const [offset, outside, reference] = useMouseOffset(); 48 | const { styles, cx } = useStyles({ offset, outside, size }); 49 | 50 | return
; 51 | }); 52 | 53 | export default Spotlight; 54 | -------------------------------------------------------------------------------- /src/components/Spotlight/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles( 4 | ( 5 | { css, token, isDarkMode }, 6 | { offset, outside, size }: { offset: { x: number; y: number }; outside: boolean; size: number }, 7 | ) => { 8 | const spotlightX = (offset?.x ?? 0) + 'px'; 9 | const spotlightY = (offset?.y ?? 0) + 'px'; 10 | const spotlightOpacity = outside ? '0' : '.1'; 11 | const spotlightSize = size + 'px'; 12 | return css` 13 | pointer-events: none; 14 | 15 | position: absolute; 16 | z-index: 1; 17 | inset: 0; 18 | 19 | opacity: ${spotlightOpacity}; 20 | background: radial-gradient( 21 | ${spotlightSize} circle at ${spotlightX} ${spotlightY}, 22 | ${isDarkMode ? token.colorText : '#fff'}, 23 | ${isDarkMode ? 'transparent' : token.colorTextQuaternary} 24 | ); 25 | border-radius: inherit; 26 | 27 | transition: all 0.2s; 28 | `; 29 | }, 30 | ); 31 | -------------------------------------------------------------------------------- /src/components/Tag/index.tsx: -------------------------------------------------------------------------------- 1 | import { Tag as AntTag, type TagProps as AntTagProps } from 'antd'; 2 | import { createStyles } from 'antd-style'; 3 | import { ReactNode, memo } from 'react'; 4 | import { Flexbox } from 'react-layout-kit'; 5 | 6 | const useStyles = createStyles(({ cx, css, token }) => ({ 7 | small: css` 8 | padding: 2px 6px; 9 | line-height: 1; 10 | `, 11 | tag: cx(css` 12 | color: ${token.colorTextSecondary} !important; 13 | background: ${token.colorFillSecondary}; 14 | border: ${token.borderRadius}px; 15 | 16 | &:hover { 17 | color: ${token.colorText}; 18 | background: ${token.colorFill}; 19 | } 20 | `), 21 | })); 22 | 23 | export interface TagProps extends AntTagProps { 24 | icon?: ReactNode; 25 | size?: 'default' | 'small'; 26 | } 27 | 28 | const Tag = memo(({ icon, children, size = 'default', ...props }) => { 29 | const { styles, cx } = useStyles(); 30 | 31 | return ( 32 | 37 | 38 | {icon} 39 | {children} 40 | 41 | 42 | ); 43 | }); 44 | 45 | export default Tag; 46 | -------------------------------------------------------------------------------- /src/hooks/useChatListActionsBar.tsx: -------------------------------------------------------------------------------- 1 | import { Copy, Edit, RotateCw, Trash } from 'lucide-react'; 2 | 3 | import { ActionIconGroupItems } from '@/ActionIconGroup'; 4 | 5 | interface ChatListActionsBar { 6 | copy: ActionIconGroupItems; 7 | del: ActionIconGroupItems; 8 | divider: { key: 'divider'; type: 'divider' }; 9 | edit: ActionIconGroupItems; 10 | regenerate: ActionIconGroupItems; 11 | } 12 | 13 | export const useChatListActionsBar = (text?: { 14 | copy?: string; 15 | delete?: string; 16 | edit?: string; 17 | regenerate?: string; 18 | }): ChatListActionsBar => { 19 | return { 20 | copy: { 21 | icon: Copy, 22 | key: 'copy', 23 | label: text?.copy || 'Copy', 24 | }, 25 | del: { 26 | icon: Trash, 27 | key: 'del', 28 | label: text?.delete || 'Delete', 29 | }, 30 | divider: { 31 | type: 'divider', 32 | key: 'divider', 33 | }, 34 | edit: { 35 | icon: Edit, 36 | key: 'edit', 37 | label: text?.edit || 'Edit', 38 | }, 39 | regenerate: { 40 | icon: RotateCw, 41 | key: 'regenerate', 42 | label: text?.regenerate || 'Regenerate', 43 | }, 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /src/hooks/useCopied.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo, useState } from 'react'; 2 | 3 | export const useCopied = () => { 4 | const [copied, setCopy] = useState(false); 5 | 6 | useEffect(() => { 7 | if (!copied) return; 8 | 9 | const timer = setTimeout(() => { 10 | setCopy(false); 11 | }, 2000); 12 | 13 | return () => { 14 | clearTimeout(timer); 15 | }; 16 | }, [copied]); 17 | 18 | const setCopied = useCallback(() => setCopy(true), []); 19 | 20 | return useMemo(() => ({ copied, setCopied }), [copied]); 21 | }; 22 | -------------------------------------------------------------------------------- /src/hooks/useCustomChatListAction.tsx: -------------------------------------------------------------------------------- 1 | import { ActionIconGroupItems } from '@/ActionIconGroup'; 2 | import { ActionsProps } from '@/ChatList/ActionsBar'; 3 | 4 | interface CustomChatListActionProps { 5 | dropdownMenu: Array; 6 | items: Array; 7 | actionsProps?: ActionsProps; 8 | } 9 | 10 | const useCustomChatListAction = ({ 11 | dropdownMenu, 12 | items, 13 | actionsProps, 14 | }: CustomChatListActionProps) => { 15 | if (!actionsProps) { 16 | // 没有自定义内容的时候使用默认 17 | return { 18 | dropdownMenu, 19 | items, 20 | }; 21 | } 22 | 23 | return { 24 | dropdownMenu: 25 | actionsProps?.moreActions 26 | ?.map((item) => dropdownMenu.find((i) => i.key === item)) 27 | .filter((v) => !!v) || [], 28 | items: 29 | actionsProps?.actions 30 | ?.map((item) => items.find((i) => i.key === item)) 31 | .filter((v) => !!v && v.key !== 'divider') || [], 32 | }; 33 | }; 34 | 35 | export default useCustomChatListAction; 36 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ActionIcon, type ActionIconProps, type ActionIconSize } from './ActionIcon'; 2 | export { default as ActionIconGroup, type ActionIconGroupProps } from './ActionIconGroup'; 3 | export { default as BackBottom, type BackBottomProps } from './BackBottom'; 4 | export { default as ChatItem, type ChatItemProps } from './ChatItem'; 5 | export { default as ChatList } from './ChatList'; 6 | export type { 7 | ChatListProps, 8 | OnActionClick, 9 | OnMessageChange, 10 | RenderAction, 11 | RenderErrorMessage, 12 | RenderItem, 13 | RenderMessage, 14 | RenderMessageExtra, 15 | } from './ChatList'; 16 | export { default as ActionsBar, type ActionsBarProps } from './ChatList/ActionsBar'; 17 | export * from './ProChat'; 18 | 19 | export { default as EditableMessage, type EditableMessageProps } from './EditableMessage'; 20 | export { 21 | default as EditableMessageList, 22 | type EditableMessageListProps, 23 | } from './EditableMessageList'; 24 | export { default as CopyButton, type CopyButtonProps } from './components/CopyButton'; 25 | 26 | export { default as List } from './List'; 27 | 28 | export { default as MessageInput, type MessageInputProps } from './MessageInput'; 29 | export { default as MessageModal, type MessageModalProps } from './MessageModal'; 30 | 31 | export { default as TokenTag, type TokenTagProps } from './TokenTag'; 32 | export { useChatListActionsBar } from './hooks/useChatListActionsBar'; 33 | export * from './styles'; 34 | export type * from './types'; 35 | -------------------------------------------------------------------------------- /src/locale/cs-CZ.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | placeholder: 'Zadejte zprávu...', 3 | backToBottom: 'Zpět na konec', 4 | clearCurrentDialogue: 'Vymazat aktuální dialog', 5 | clearDialogue: 'Vymazat dialog', 6 | clearModalTitle: 7 | 'Chystáte se vymazat relaci a po vymazání ji nebude možné obnovit. Chcete vymazat aktuální relaci?', 8 | defaultHelloMessage: 'Začněme si povídat', 9 | cancel: 'Zrušit', 10 | confirm: 'Potvrdit', 11 | copy: 'Kopírovat', 12 | copySuccess: 'Kopírování úspěšné', 13 | delete: 'Smazat', 14 | edit: 'Upravit', 15 | history: 'Historie', 16 | regenerate: 'Znovu vygenerovat', 17 | }; -------------------------------------------------------------------------------- /src/locale/de-DE.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | placeholder: 'Bitte geben Sie eine Nachricht ein...', 3 | backToBottom: 'Zurück nach unten', 4 | clearCurrentDialogue: 'Aktuellen Dialog löschen', 5 | clearDialogue: 'Dialog löschen', 6 | clearModalTitle: 7 | 'Sie sind dabei, die Sitzung zu löschen, und Sie können sie nach dem Löschen nicht wiederherstellen. Möchten Sie die aktuelle Sitzung löschen?', 8 | defaultHelloMessage: 'Lassen Sie uns chatten', 9 | cancel: 'Abbrechen', 10 | confirm: 'Bestätigen', 11 | copy: 'Kopieren', 12 | copySuccess: 'Kopieren erfolgreich', 13 | delete: 'Löschen', 14 | edit: 'Bearbeiten', 15 | history: 'Verlauf', 16 | regenerate: 'Neu generieren', 17 | }; -------------------------------------------------------------------------------- /src/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | placeholder: 'Please enter a message...', 3 | backToBottom: 'Back to bottom', 4 | clearCurrentDialogue: 'Clear current dialogue', 5 | clearDialogue: 'Clear dialogue', 6 | clearModalTitle: 7 | 'You are about to clear the session, and you will not be able to retrieve it after clearing. Do you want to clear the current session?', 8 | defaultHelloMessage: 'Let us start chatting', 9 | cancel: 'Cancel', 10 | confirm: 'Confirm', 11 | copy: 'Copy', 12 | copySuccess: 'Copy Success', 13 | delete: 'Delete', 14 | edit: 'Edit', 15 | history: 'History', 16 | regenerate: 'Regenerate', 17 | }; 18 | -------------------------------------------------------------------------------- /src/locale/hu-HU.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | placeholder: 'Kérjük, írjon be egy üzenetet...', 3 | backToBottom: 'Vissza az aljára', 4 | clearCurrentDialogue: 'Aktuális párbeszéd törlése', 5 | clearDialogue: 'Párbeszéd törlése', 6 | clearModalTitle: 7 | 'Ön éppen törölni készül a munkamenetet, és a törlés után nem tudja visszaállítani. Biztosan törölni szeretné az aktuális munkamenetet?', 8 | defaultHelloMessage: 'Kezdjünk el beszélgetni', 9 | cancel: 'Mégse', 10 | confirm: 'Megerősít', 11 | copy: 'Másolás', 12 | copySuccess: 'Másolás sikeres', 13 | delete: 'Törlés', 14 | edit: 'Szerkesztés', 15 | history: 'Előzmények', 16 | regenerate: 'Újragenerálás', 17 | }; -------------------------------------------------------------------------------- /src/locale/index.ts: -------------------------------------------------------------------------------- 1 | import { LocaleProps } from '@/types/locale'; 2 | import enUSLocal from './en-US'; 3 | import zhCNLocal from './zh-CN'; 4 | import zhHKLocal from './zh-HK'; 5 | import csCZLocal from './cs-CZ'; 6 | import deLocal from './de-DE'; 7 | import huLocal from './hu-HU'; 8 | import plLocal from './pl-PL'; 9 | import skLocal from './sk-SK'; 10 | export type Locale = 'zh-CN' | 'en-US' | 'zh-HK' | 'cs-CZ' | 'de-DE' | 'hu-HU' | 'pl-PL' | 'sk-SK'; 11 | 12 | const locales = { 13 | 'en-US': enUSLocal, 14 | 'zh-CN': zhCNLocal, 15 | 'zh-HK': zhHKLocal, 16 | 'cs-CZ': csCZLocal, 17 | 'de-DE': deLocal, 18 | 'hu-HU': huLocal, 19 | 'pl-PL': plLocal, 20 | 'sk-SK': skLocal, 21 | en: enUSLocal, 22 | }; 23 | 24 | export const gLocaleObject = (gLocale: Locale): LocaleProps => { 25 | return locales[gLocale as 'zh-CN'] || locales['zh-CN']; 26 | }; 27 | -------------------------------------------------------------------------------- /src/locale/pl-PL.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | placeholder: 'Proszę wpisać wiadomość...', 3 | backToBottom: 'Powrót na dół', 4 | clearCurrentDialogue: 'Wyczyść bieżący dialog', 5 | clearDialogue: 'Wyczyść dialog', 6 | clearModalTitle: 7 | 'Zamierzasz wyczyścić sesję i nie będziesz mógł jej odzyskać po wyczyszczeniu. Czy chcesz wyczyścić bieżącą sesję?', 8 | defaultHelloMessage: 'Zacznijmy rozmawiać', 9 | cancel: 'Anuluj', 10 | confirm: 'Potwierdź', 11 | copy: 'Kopiuj', 12 | copySuccess: 'Kopiowanie zakończone sukcesem', 13 | delete: 'Usuń', 14 | edit: 'Edytuj', 15 | history: 'Historia', 16 | regenerate: 'Wygeneruj ponownie', 17 | }; -------------------------------------------------------------------------------- /src/locale/sk-SK.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | placeholder: 'Zadajte správu...', 3 | backToBottom: 'Späť na koniec', 4 | clearCurrentDialogue: 'Vymazať aktuálny dialóg', 5 | clearDialogue: 'Vymazať dialóg', 6 | clearModalTitle: 7 | 'Chystáte sa vymazať reláciu a po vymazaní ju nebude možné obnoviť. Chcete vymazať aktuálnu reláciu?', 8 | defaultHelloMessage: 'Začnime si rozprávať', 9 | cancel: 'Zrušiť', 10 | confirm: 'Potvrdiť', 11 | copy: 'Kopírovať', 12 | copySuccess: 'Kopírovanie úspešné', 13 | delete: 'Vymazať', 14 | edit: 'Upraviť', 15 | history: 'História', 16 | regenerate: 'Znovu vygenerovať', 17 | }; -------------------------------------------------------------------------------- /src/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | placeholder: '请输入消息...', 3 | backToBottom: '返回底部', 4 | clearCurrentDialogue: '清空当前对话', 5 | clearDialogue: '清空对话', 6 | clearModalTitle: '你即将要清空会话,清空后将无法找回。是否清空当前会话?', 7 | defaultHelloMessage: '让我们开始对话吧', 8 | cancel: '取消', 9 | confirm: '确认', 10 | copy: '复制', 11 | copySuccess: '复制成功', 12 | delete: '删除', 13 | edit: '编辑', 14 | history: '历史范围', 15 | regenerate: '重新生成', 16 | }; 17 | -------------------------------------------------------------------------------- /src/locale/zh-HK.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | placeholder: '請輸入訊息...', 3 | backToBottom: '回到底部', 4 | clearCurrentDialogue: '清除當前對話', 5 | clearDialogue: '清除對話', 6 | clearModalTitle: '您即將清除會話,清除後將無法恢復。您確定要清除當前會話嗎?', 7 | defaultHelloMessage: '讓我們開始聊天吧', 8 | cancel: '取消', 9 | confirm: '確認', 10 | copy: '複製', 11 | copySuccess: '複製成功', 12 | delete: '刪除', 13 | edit: '編輯', 14 | history: '歷史', 15 | regenerate: '重新生成', 16 | }; 17 | -------------------------------------------------------------------------------- /src/styles/index.ts: -------------------------------------------------------------------------------- 1 | export { colorScales } from './colors'; 2 | -------------------------------------------------------------------------------- /src/types/customStylish.ts: -------------------------------------------------------------------------------- 1 | export interface LobeCustomStylish { 2 | blur: string; 3 | blurStrong: string; 4 | bottomScrollbar: string; 5 | gradientAnimation: string; 6 | markdown: string; 7 | noScrollbar: string; 8 | resetLinkColor: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/types/customToken.ts: -------------------------------------------------------------------------------- 1 | const PresetColors = [ 2 | 'red', 3 | 'volcano', 4 | 'orange', 5 | 'gold', 6 | 'yellow', 7 | 'lime', 8 | 'green', 9 | 'cyan', 10 | 'blue', 11 | 'geekblue', 12 | 'purple', 13 | 'magenta', 14 | 'gray', 15 | ] as const; 16 | 17 | export type PresetColorKey = (typeof PresetColors)[number]; 18 | 19 | export type PresetColorType = Record; 20 | 21 | type ColorPaletteKeyIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; 22 | 23 | type ColorTokenKey = 24 | | 'Bg' 25 | | 'BgHover' 26 | | 'Border' 27 | | 'BorderSecondary' 28 | | 'BorderHover' 29 | | 'Hover' 30 | | '' 31 | | 'Active' 32 | | 'TextHover' 33 | | 'Text' 34 | | 'TextActive'; 35 | 36 | export type ColorToken = { 37 | [key in `${keyof PresetColorType}${ColorTokenKey}`]: string; 38 | }; 39 | 40 | export type ColorPalettes = { 41 | [key in `${keyof PresetColorType}${ColorPaletteKeyIndex}`]: string; 42 | }; 43 | 44 | export type ColorPalettesAlpha = { 45 | [key in `${keyof PresetColorType}${ColorPaletteKeyIndex}A`]: string; 46 | }; 47 | 48 | export interface LobeCustomToken extends ColorToken, ColorPalettes, ColorPalettesAlpha {} 49 | -------------------------------------------------------------------------------- /src/types/error.ts: -------------------------------------------------------------------------------- 1 | export const ChatErrorType = { 2 | // ******* 业务错误语义 ******* // 3 | 4 | InvalidAccessCode: 'InvalidAccessCode', // 密码无效 5 | OpenAIBizError: 'OpenAIBizError', // OpenAI 返回的业务错误 6 | NoAPIKey: 'NoAPIKey', 7 | 8 | // ******* 客户端错误 ******* // 9 | BadRequest: 400, 10 | Unauthorized: 401, 11 | Forbidden: 403, 12 | ContentNotFound: 404, // 没找到接口 13 | MethodNotAllowed: 405, // 不支持 14 | TooManyRequests: 429, 15 | 16 | // ******* 服务端错误 ******* // 17 | InternalServerError: 500, 18 | BadGateway: 502, 19 | ServiceUnavailable: 503, 20 | GatewayTimeout: 504, 21 | } as const; 22 | /* eslint-enable */ 23 | 24 | export type ErrorType = (typeof ChatErrorType)[keyof typeof ChatErrorType]; 25 | 26 | export interface ErrorResponse { 27 | body: any; 28 | errorType: ErrorType; 29 | } 30 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | import 'antd-style'; 2 | 3 | import { LobeCustomStylish } from './customStylish'; 4 | import { LobeCustomToken } from './customToken'; 5 | 6 | declare module 'antd-style' { 7 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 8 | export interface CustomToken extends LobeCustomToken {} 9 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 10 | export interface CustomStylish extends LobeCustomStylish {} 11 | } 12 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { type HTMLAttributes } from 'react'; 2 | 3 | export * from './customStylish'; 4 | export * from './customToken'; 5 | export * from './llm'; 6 | export * from './message'; 7 | export * from './meta'; 8 | 9 | export type DivProps = HTMLAttributes; 10 | 11 | export type SvgProps = HTMLAttributes; 12 | 13 | export type ImgProps = HTMLAttributes; 14 | 15 | export type InputProps = HTMLAttributes; 16 | 17 | export type TextAreaProps = HTMLAttributes; 18 | -------------------------------------------------------------------------------- /src/types/llm.ts: -------------------------------------------------------------------------------- 1 | // 语言模型的设置参数 2 | export interface LLMParams { 3 | /** 4 | * 控制生成文本中的惩罚系数,用于减少重复性 5 | * @default 0 6 | */ 7 | frequency_penalty?: number; 8 | /** 9 | * 生成文本的最大长度 10 | */ 11 | max_tokens?: number; 12 | /** 13 | * 控制生成文本中的惩罚系数,用于减少主题的变化 14 | * @default 0 15 | */ 16 | presence_penalty?: number; 17 | /** 18 | * 生成文本的随机度量,用于控制文本的创造性和多样性 19 | * @default 0.6 20 | */ 21 | temperature?: number; 22 | /** 23 | * 控制生成文本中最高概率的单个 token 24 | * @default 1 25 | */ 26 | top_p?: number; 27 | } 28 | 29 | export type LLMRoleType = 'user' | 'system' | 'assistant' | 'function'; 30 | 31 | export interface LLMMessage { 32 | content: string; 33 | role: LLMRoleType; 34 | } 35 | 36 | export type LLMExample = LLMMessage[]; 37 | -------------------------------------------------------------------------------- /src/types/locale.ts: -------------------------------------------------------------------------------- 1 | export interface LocaleProps { 2 | placeholder: string; 3 | backToBottom: string; 4 | clearCurrentDialogue: string; 5 | clearDialogue: string; 6 | clearModalTitle: string; 7 | defaultHelloMessage: string; 8 | cancel: string; 9 | confirm: string; 10 | copy: string; 11 | copySuccess: string; 12 | delete: string; 13 | edit: string; 14 | history: string; 15 | regenerate: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/types/message.ts: -------------------------------------------------------------------------------- 1 | import { ModelRoleType } from '@/ProChat/types/config'; 2 | import { ReactNode } from 'react'; 3 | 4 | /** 5 | * 聊天消息错误对象 6 | */ 7 | export interface ChatMessageError { 8 | body?: any; 9 | message: string; 10 | type: string | number; 11 | } 12 | 13 | export interface ChatMessage = Record> { 14 | /** 15 | * @title 内容 16 | * @description 消息内容 17 | */ 18 | content: ReactNode; 19 | error?: any; 20 | model?: string; 21 | name?: string; 22 | parentId?: string; 23 | /** 24 | * 角色 25 | * @description 消息发送者的角色 26 | */ 27 | role: ModelRoleType | string; 28 | createAt: number; 29 | id: string; 30 | updateAt: number; 31 | extra?: T; 32 | } 33 | 34 | export interface OpenAIFunctionCall { 35 | arguments?: string; 36 | name: string; 37 | } 38 | -------------------------------------------------------------------------------- /src/types/meta.ts: -------------------------------------------------------------------------------- 1 | export interface MetaData { 2 | /** 3 | * 角色头像 4 | * @description 可选参数,如果不传则使用默认头像 5 | */ 6 | avatar?: string; 7 | /** 8 | * 背景色 9 | * @description 可选参数,如果不传则使用默认背景色 10 | */ 11 | backgroundColor?: string; 12 | /** 13 | * 名称 14 | * @description 可选参数,如果不传则使用默认名称 15 | */ 16 | title?: string; 17 | /** 18 | * 自定义类名 19 | * @description 可选参数,如果不传则使用默认类名 20 | */ 21 | className?: string; 22 | } 23 | 24 | export interface BaseDataModel { 25 | createAt: number; 26 | id: string; 27 | meta: MetaData; 28 | updateAt: number; 29 | } 30 | -------------------------------------------------------------------------------- /tests/demo.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/react'; 2 | import { theme } from 'antd'; 3 | import { glob } from 'glob'; 4 | import path from 'path'; 5 | import { afterEach, beforeEach, describe, it, vi } from 'vitest'; 6 | import { resetMockDate, setMockDate } from './utils'; 7 | 8 | theme.defaultConfig.hashed = false; 9 | // 特殊情况略过 snapshot 的文件 10 | const NotSnapshotFileList = ['renderInputArea.tsx', 'float-drawer.tsx']; 11 | 12 | function demoTest(component: string) { 13 | beforeEach(() => { 14 | theme.defaultConfig.hashed = false; 15 | process.env.NODE_ENV = 'TEST'; 16 | setMockDate('2020-07-15T05:20:00.795'); 17 | }); 18 | 19 | afterEach(() => { 20 | vi.useRealTimers(); 21 | resetMockDate(); 22 | }); 23 | 24 | describe(`<${component} />`, () => { 25 | const files = glob.sync(path.resolve(__dirname, `../src/${component}/demos/*.tsx`)); 26 | 27 | files.forEach((file) => { 28 | const demoName = file.split('/').pop(); 29 | 30 | it(`renders ${demoName} correctly`, async () => { 31 | const Demo = await import(file); 32 | 33 | if (!NotSnapshotFileList.includes(demoName)) { 34 | if (!demoName) return; 35 | // 快照一致 36 | const wrapper = render(); 37 | expect(wrapper.asFragment()).toMatchSnapshot(); 38 | cleanup(); 39 | } 40 | }); 41 | }); 42 | }); 43 | } 44 | 45 | export default demoTest; 46 | -------------------------------------------------------------------------------- /tests/test-setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest'; 2 | import { theme } from 'antd'; 3 | // Not use dynamic hashed for test env since version will change hash dynamically. 4 | theme.defaultConfig.hashed = false; 5 | 6 | process.env.TZ = 'UTC'; 7 | 8 | beforeEach(() => { 9 | // Mocking global.fetch before each test run 10 | vi.spyOn(global, 'fetch').mockImplementation(async (url) => { 11 | if (url === '/api/openai/chat') { 12 | return await new Response('expected respons'); 13 | } 14 | 15 | // For unhandled requests in tests: 16 | return await new Response(`expected respons in ${url}`); 17 | }); 18 | }); 19 | 20 | afterEach(() => { 21 | vi.restoreAllMocks(); 22 | }); 23 | 24 | if (typeof window !== 'undefined') { 25 | global.window.resizeTo = (width, height) => { 26 | // @ts-ignore-next-line 27 | global.window.innerWidth = width || global.window.innerWidth; 28 | // @ts-ignore-next-line 29 | global.window.innerHeight = height || global.window.innerHeight; 30 | global.window.dispatchEvent(new Event('resize')); 31 | }; 32 | 33 | // ref: https://github.com/ant-design/ant-design/issues/18774 34 | if (!window.matchMedia) { 35 | Object.defineProperty(global.window, 'matchMedia', { 36 | value: vi.fn((query) => ({ 37 | matches: query.includes('max-width'), 38 | addListener: () => {}, 39 | addEventListener: () => {}, 40 | removeListener: () => {}, 41 | removeEventListener: () => {}, 42 | })), 43 | }); 44 | } 45 | 46 | window.ResizeObserver = 47 | window.ResizeObserver || 48 | vi.fn().mockImplementation(() => ({ 49 | disconnect: vi.fn(), 50 | observe: vi.fn(), 51 | unobserve: vi.fn(), 52 | })); 53 | 54 | global.window.HTMLElement.prototype.scrollIntoView = () => {}; 55 | } 56 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import MockDate from 'mockdate'; 3 | 4 | export function setMockDate(dateString = '2017-09-18T03:30:07.795') { 5 | // @ts-ignore 6 | MockDate.set(dayjs(dateString).toString()); 7 | } 8 | 9 | export function resetMockDate() { 10 | MockDate.reset(); 11 | } 12 | 13 | export * from '@testing-library/react'; 14 | -------------------------------------------------------------------------------- /tsconfig-check.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "tests", ".dumirc.ts", "*.ts"], 3 | "compilerOptions": { 4 | "strict": false, 5 | "declaration": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "jsx": "react-jsx", 10 | "types": ["vitest/globals", "@testing-library/jest-dom"], 11 | "baseUrl": "./", 12 | "paths": { 13 | "@@/*": [".dumi/tmp/*"], 14 | "@/*": ["src/*"], 15 | "@ant-design/pro-chat": ["src"], 16 | "@ant-design/pro-chat/*": ["src/*"] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | esbuild: { 6 | jsxInject: "import React from 'react'", 7 | }, 8 | resolve: {}, 9 | test: { 10 | setupFiles: './tests/test-setup.ts', 11 | environment: 'jsdom', 12 | globals: true, 13 | alias: { 14 | '@ant-design/pro-chat': path.join(__dirname, './src'), 15 | '@/ProChat/mocks': path.join(__dirname, './src/ProChat/mocks'), 16 | '@': path.join(__dirname, './src'), 17 | }, 18 | coverage: { 19 | provider: 'v8', 20 | reporter: ['text', 'text-summary', 'json', 'lcov'], 21 | }, 22 | }, 23 | }); 24 | --------------------------------------------------------------------------------