Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3d74ec4a6 | ||
|
|
df9ac99ef2 | ||
|
|
ef1e8aef5c | ||
|
|
56dab7abba | ||
|
|
ed1f93d836 | ||
|
|
5ec8aac3ac | ||
|
|
837c132d24 | ||
|
|
8239c58494 | ||
|
|
456686f3d0 | ||
|
|
2099a87908 | ||
|
|
2b2c70e53d | ||
|
|
f32c557bdd | ||
|
|
713332522f | ||
|
|
586607a9ce | ||
|
|
8c70205940 | ||
|
|
c3ccbcb7f6 | ||
|
|
7a6d0ea650 | ||
|
|
7fb6f62cf6 | ||
|
|
af385b1b42 | ||
|
|
5249297cb1 | ||
|
|
9280a21d12 | ||
|
|
650a9dd651 | ||
|
|
42e12d7db1 | ||
|
|
249ed18d15 | ||
|
|
3e4487ad9a | ||
|
|
888642f154 | ||
|
|
963e590dfd | ||
|
|
e547893ac1 | ||
|
|
9125910cfe | ||
|
|
98c458dcf8 | ||
|
|
41b6401c13 | ||
|
|
936e36205e | ||
|
|
e289c4ec53 | ||
|
|
1cbcc62494 | ||
|
|
da31ef286b | ||
|
|
60f62507bd | ||
|
|
75cf3d1e9f | ||
|
|
02cee35a45 | ||
|
|
0cee404c7f | ||
|
|
274ece1d91 | ||
|
|
6bba859060 | ||
|
|
4eaf3a1be0 | ||
|
|
3db690773f | ||
|
|
8a9f1ed29b | ||
|
|
c0dc5a74c9 | ||
|
|
a35cda6873 | ||
|
|
30678d8ebf | ||
|
|
6ce727f9ea | ||
|
|
81e6821174 | ||
|
|
d8290f0809 | ||
|
|
8b72dca533 | ||
|
|
4d64068591 | ||
|
|
af35e17fdb | ||
|
|
5ec303610c | ||
|
|
984baf60f0 | ||
|
|
d065539707 | ||
|
|
129f3a2a30 | ||
|
|
42c26bd155 | ||
|
|
dc467c26b5 | ||
|
|
3aeb510f43 | ||
|
|
405a75e23b | ||
|
|
be47169fa8 | ||
|
|
58a010c12c |
@@ -7,4 +7,4 @@ README.md
|
||||
.git
|
||||
|
||||
.yalc/
|
||||
yalc.lock
|
||||
yalc.lock
|
||||
|
||||
@@ -3,4 +3,6 @@ AXIOS_PROXY_PORT=33210
|
||||
MONGODB_URI=
|
||||
MY_MAIL=
|
||||
MAILE_CODE=
|
||||
TOKEN_KEY=
|
||||
TOKEN_KEY=
|
||||
OPENAIKEY=
|
||||
REDIS_URL=
|
||||
10
.husky/pre-commit
Normal file → Executable file
10
.husky/pre-commit
Normal file → Executable file
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
if command -v npx >/dev/null 2>&1; then
|
||||
npx lint-staged
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
if command -v npx >/dev/null 2>&1; then
|
||||
npx lint-staged
|
||||
fi
|
||||
@@ -1,4 +1,4 @@
|
||||
dist
|
||||
.vscode
|
||||
**/.DS_Store
|
||||
node_modules
|
||||
node_modules
|
||||
@@ -1,21 +1,20 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
quoteProps: 'as-needed',
|
||||
jsxSingleQuote: false,
|
||||
trailingComma: 'none',
|
||||
bracketSpacing: true,
|
||||
jsxBracketSameLine: false,
|
||||
arrowParens: 'always',
|
||||
rangeStart: 0,
|
||||
rangeEnd: Infinity,
|
||||
requirePragma: false,
|
||||
insertPragma: false,
|
||||
proseWrap: 'preserve',
|
||||
htmlWhitespaceSensitivity: 'css',
|
||||
endOfLine: 'lf'
|
||||
printWidth: 100,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
quoteProps: 'as-needed',
|
||||
jsxSingleQuote: false,
|
||||
trailingComma: 'none',
|
||||
bracketSpacing: true,
|
||||
jsxBracketSameLine: false,
|
||||
arrowParens: 'always',
|
||||
rangeStart: 0,
|
||||
rangeEnd: Infinity,
|
||||
requirePragma: false,
|
||||
insertPragma: false,
|
||||
proseWrap: 'preserve',
|
||||
htmlWhitespaceSensitivity: 'css',
|
||||
endOfLine: 'lf'
|
||||
};
|
||||
|
||||
14
README.md
14
README.md
@@ -1,6 +1,6 @@
|
||||
# Fast GPT
|
||||
|
||||
Fast GPT 允许你是用自己的 openai API KEY 来快速的调用 openai 接口,包括 GPT3 及其微调方法,以及最新的 gpt3.5 接口。
|
||||
Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接口,包括 GPT3 及其微调方法,以及最新的 gpt3.5 接口。
|
||||
|
||||
## 初始化
|
||||
复制 .env.template 成 .env.local ,填写核心参数
|
||||
@@ -11,7 +11,7 @@ AXIOS_PROXY_PORT=代理端口
|
||||
MONGODB_URI=mongo数据库地址(例如:mongodb://username:password@ip:27017/?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&directConnection=true&ssl=false)
|
||||
MY_MAIL=发送验证码邮箱
|
||||
MAILE_CODE=邮箱秘钥(代理里设置的是QQ邮箱,不知道怎么找这个 code 的,可以百度搜"nodemailer发送邮件")
|
||||
TOKEN_KEY=随便填一个,用于生成和校验token
|
||||
TOKEN_KEY=随便填一个,用于生成和校验 token
|
||||
```
|
||||
|
||||
```bash
|
||||
@@ -100,4 +100,12 @@ echo "Restart clash"
|
||||
```
|
||||
|
||||
#### 软件教程:Nginx
|
||||
...没写,这个百度吧。
|
||||
...没写,这个百度吧。
|
||||
|
||||
#### redis
|
||||
|
||||
```bash
|
||||
# 索引
|
||||
# FT.CREATE idx:model:data ON JSON PREFIX 1 model:data: SCHEMA $.modelId AS modelId TAG $.dataId AS dataId TAG $.vector AS vector VECTOR FLAT 6 DIM 1536 DISTANCE_METRIC COSINE TYPE FLOAT32
|
||||
FT.CREATE idx:model:data:hash ON HASH PREFIX 1 model:data: SCHEMA modelId TAG dataId TAG vector VECTOR FLAT 6 DIM 1536 DISTANCE_METRIC COSINE TYPE FLOAT32
|
||||
```
|
||||
@@ -1,4 +0,0 @@
|
||||
{"prompt": "sealos的介绍", "completion": "sealos 是以 kubernetes 为内核的云操作系统发行版"}
|
||||
{"prompt": "sealos是什么", "completion": "sealos 是以 kubernetes 为内核的云操作系统发行版"}
|
||||
{"prompt": "sealos安装的先决条件", "completion": "sealos 是一个简单的 go 二进制文件,可以安装在大多数 Linux 操作系统中。"}
|
||||
{"prompt": "sealos的CPU架构", "completion": "目前支持 amd64 和 arm64 架构。"}
|
||||
@@ -24,10 +24,14 @@
|
||||
"eventsource-parser": "^0.1.0",
|
||||
"formidable": "^2.1.1",
|
||||
"framer-motion": "^9.0.6",
|
||||
"gpt-token-utils": "^1.2.0",
|
||||
"hyperdown": "^2.4.29",
|
||||
"immer": "^9.0.19",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mammoth": "^1.5.1",
|
||||
"mongoose": "^6.10.0",
|
||||
"nanoid": "^4.0.1",
|
||||
"next": "13.1.6",
|
||||
"nodemailer": "^6.9.1",
|
||||
"nprogress": "^0.2.0",
|
||||
@@ -37,19 +41,20 @@
|
||||
"react-hook-form": "^7.43.1",
|
||||
"react-markdown": "^8.0.5",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"redis": "^4.6.5",
|
||||
"rehype-katex": "^6.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"sass": "^1.58.3",
|
||||
"sharp": "^0.31.3",
|
||||
"tunnel": "^0.0.6",
|
||||
"uuid": "^9.0.0",
|
||||
"zustand": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"@types/formidable": "^2.0.5",
|
||||
"@types/jsonwebtoken": "^9.0.1",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "18.14.0",
|
||||
"@types/nodemailer": "^6.4.7",
|
||||
"@types/react": "18.0.28",
|
||||
|
||||
299
pnpm-lock.yaml
generated
299
pnpm-lock.yaml
generated
@@ -10,6 +10,7 @@ specifiers:
|
||||
'@tanstack/react-query': ^4.24.10
|
||||
'@types/formidable': ^2.0.5
|
||||
'@types/jsonwebtoken': ^9.0.1
|
||||
'@types/lodash': ^4.14.191
|
||||
'@types/node': 18.14.0
|
||||
'@types/nodemailer': ^6.4.7
|
||||
'@types/nprogress': ^0.2.0
|
||||
@@ -26,12 +27,16 @@ specifiers:
|
||||
eventsource-parser: ^0.1.0
|
||||
formidable: ^2.1.1
|
||||
framer-motion: ^9.0.6
|
||||
gpt-token-utils: ^1.2.0
|
||||
husky: ^8.0.3
|
||||
hyperdown: ^2.4.29
|
||||
immer: ^9.0.19
|
||||
jsonwebtoken: ^9.0.0
|
||||
lint-staged: ^13.1.2
|
||||
lodash: ^4.17.21
|
||||
mammoth: ^1.5.1
|
||||
mongoose: ^6.10.0
|
||||
nanoid: ^4.0.1
|
||||
next: 13.1.6
|
||||
nodemailer: ^6.9.1
|
||||
nprogress: ^0.2.0
|
||||
@@ -42,6 +47,7 @@ specifiers:
|
||||
react-hook-form: ^7.43.1
|
||||
react-markdown: ^8.0.5
|
||||
react-syntax-highlighter: ^15.5.0
|
||||
redis: ^4.6.5
|
||||
rehype-katex: ^6.0.2
|
||||
remark-gfm: ^3.0.1
|
||||
remark-math: ^5.1.1
|
||||
@@ -49,7 +55,6 @@ specifiers:
|
||||
sharp: ^0.31.3
|
||||
tunnel: ^0.0.6
|
||||
typescript: 4.9.5
|
||||
uuid: ^9.0.0
|
||||
zustand: ^4.3.5
|
||||
|
||||
dependencies:
|
||||
@@ -66,10 +71,14 @@ dependencies:
|
||||
eventsource-parser: registry.npmmirror.com/eventsource-parser/0.1.0
|
||||
formidable: registry.npmmirror.com/formidable/2.1.1
|
||||
framer-motion: registry.npmmirror.com/framer-motion/9.0.6_biqbaboplfbrettd7655fr4n2y
|
||||
gpt-token-utils: registry.npmmirror.com/gpt-token-utils/1.2.0
|
||||
hyperdown: registry.npmmirror.com/hyperdown/2.4.29
|
||||
immer: registry.npmmirror.com/immer/9.0.19
|
||||
jsonwebtoken: registry.npmmirror.com/jsonwebtoken/9.0.0
|
||||
lodash: registry.npmmirror.com/lodash/4.17.21
|
||||
mammoth: registry.npmmirror.com/mammoth/1.5.1
|
||||
mongoose: registry.npmmirror.com/mongoose/6.10.0
|
||||
nanoid: registry.npmmirror.com/nanoid/4.0.1
|
||||
next: registry.npmmirror.com/next/13.1.6_wiv434v7erz4aedd5whhdwmpv4
|
||||
nodemailer: registry.npmmirror.com/nodemailer/6.9.1
|
||||
nprogress: registry.npmmirror.com/nprogress/0.2.0
|
||||
@@ -79,19 +88,20 @@ dependencies:
|
||||
react-hook-form: registry.npmmirror.com/react-hook-form/7.43.1_react@18.2.0
|
||||
react-markdown: registry.npmmirror.com/react-markdown/8.0.5_pmekkgnqduwlme35zpnqhenc34
|
||||
react-syntax-highlighter: registry.npmmirror.com/react-syntax-highlighter/15.5.0_react@18.2.0
|
||||
redis: registry.npmmirror.com/redis/4.6.5
|
||||
rehype-katex: registry.npmmirror.com/rehype-katex/6.0.2
|
||||
remark-gfm: registry.npmmirror.com/remark-gfm/3.0.1
|
||||
remark-math: registry.npmmirror.com/remark-math/5.1.1
|
||||
sass: registry.npmmirror.com/sass/1.58.3
|
||||
sharp: registry.npmmirror.com/sharp/0.31.3
|
||||
tunnel: registry.npmmirror.com/tunnel/0.0.6
|
||||
uuid: registry.npmmirror.com/uuid/9.0.0
|
||||
zustand: registry.npmmirror.com/zustand/4.3.5_immer@9.0.19+react@18.2.0
|
||||
|
||||
devDependencies:
|
||||
'@svgr/webpack': registry.npmmirror.com/@svgr/webpack/6.5.1
|
||||
'@types/formidable': registry.npmmirror.com/@types/formidable/2.0.5
|
||||
'@types/jsonwebtoken': registry.npmmirror.com/@types/jsonwebtoken/9.0.1
|
||||
'@types/lodash': registry.npmmirror.com/@types/lodash/4.14.191
|
||||
'@types/node': registry.npmmirror.com/@types/node/18.14.0
|
||||
'@types/nodemailer': registry.npmmirror.com/@types/nodemailer/6.4.7
|
||||
'@types/react': registry.npmmirror.com/@types/react/18.0.28
|
||||
@@ -4496,6 +4506,72 @@ packages:
|
||||
version: 2.11.6
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@redis/bloom/1.2.0_@redis+client@1.5.6:
|
||||
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@redis/bloom/-/bloom-1.2.0.tgz}
|
||||
id: registry.npmmirror.com/@redis/bloom/1.2.0
|
||||
name: '@redis/bloom'
|
||||
version: 1.2.0
|
||||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
dependencies:
|
||||
'@redis/client': registry.npmmirror.com/@redis/client/1.5.6
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@redis/client/1.5.6:
|
||||
resolution: {integrity: sha512-dFD1S6je+A47Lj22jN/upVU2fj4huR7S9APd7/ziUXsIXDL+11GPYti4Suv5y8FuXaN+0ZG4JF+y1houEJ7ToA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@redis/client/-/client-1.5.6.tgz}
|
||||
name: '@redis/client'
|
||||
version: 1.5.6
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
cluster-key-slot: registry.npmmirror.com/cluster-key-slot/1.1.2
|
||||
generic-pool: registry.npmmirror.com/generic-pool/3.9.0
|
||||
yallist: registry.npmmirror.com/yallist/4.0.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@redis/graph/1.1.0_@redis+client@1.5.6:
|
||||
resolution: {integrity: sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@redis/graph/-/graph-1.1.0.tgz}
|
||||
id: registry.npmmirror.com/@redis/graph/1.1.0
|
||||
name: '@redis/graph'
|
||||
version: 1.1.0
|
||||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
dependencies:
|
||||
'@redis/client': registry.npmmirror.com/@redis/client/1.5.6
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@redis/json/1.0.4_@redis+client@1.5.6:
|
||||
resolution: {integrity: sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@redis/json/-/json-1.0.4.tgz}
|
||||
id: registry.npmmirror.com/@redis/json/1.0.4
|
||||
name: '@redis/json'
|
||||
version: 1.0.4
|
||||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
dependencies:
|
||||
'@redis/client': registry.npmmirror.com/@redis/client/1.5.6
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@redis/search/1.1.2_@redis+client@1.5.6:
|
||||
resolution: {integrity: sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@redis/search/-/search-1.1.2.tgz}
|
||||
id: registry.npmmirror.com/@redis/search/1.1.2
|
||||
name: '@redis/search'
|
||||
version: 1.1.2
|
||||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
dependencies:
|
||||
'@redis/client': registry.npmmirror.com/@redis/client/1.5.6
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@redis/time-series/1.0.4_@redis+client@1.5.6:
|
||||
resolution: {integrity: sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@redis/time-series/-/time-series-1.0.4.tgz}
|
||||
id: registry.npmmirror.com/@redis/time-series/1.0.4
|
||||
name: '@redis/time-series'
|
||||
version: 1.0.4
|
||||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
dependencies:
|
||||
'@redis/client': registry.npmmirror.com/@redis/client/1.5.6
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@rushstack/eslint-patch/1.2.0:
|
||||
resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz}
|
||||
name: '@rushstack/eslint-patch'
|
||||
@@ -4792,7 +4868,6 @@ packages:
|
||||
resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.191.tgz}
|
||||
name: '@types/lodash'
|
||||
version: 4.14.191
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@types/mdast/3.0.10:
|
||||
resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/mdast/-/mdast-3.0.10.tgz}
|
||||
@@ -5085,6 +5160,14 @@ packages:
|
||||
picomatch: registry.npmmirror.com/picomatch/2.3.1
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/argparse/1.0.10:
|
||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz}
|
||||
name: argparse
|
||||
version: 1.0.10
|
||||
dependencies:
|
||||
sprintf-js: registry.npmmirror.com/sprintf-js/1.0.3
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/argparse/2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz}
|
||||
name: argparse
|
||||
@@ -5333,6 +5416,12 @@ packages:
|
||||
readable-stream: registry.npmmirror.com/readable-stream/3.6.1
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/bluebird/3.4.7:
|
||||
resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/bluebird/-/bluebird-3.4.7.tgz}
|
||||
name: bluebird
|
||||
version: 3.4.7
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/boolbase/1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz}
|
||||
name: boolbase
|
||||
@@ -5541,6 +5630,13 @@ packages:
|
||||
version: 0.0.1
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/cluster-key-slot/1.1.2:
|
||||
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz}
|
||||
name: cluster-key-slot
|
||||
version: 1.1.2
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/color-convert/1.9.3:
|
||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz}
|
||||
name: color-convert
|
||||
@@ -5672,6 +5768,12 @@ packages:
|
||||
browserslist: registry.npmmirror.com/browserslist/4.21.5
|
||||
dev: true
|
||||
|
||||
registry.npmmirror.com/core-util-is/1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz}
|
||||
name: core-util-is
|
||||
version: 1.0.3
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/cosmiconfig/7.1.0:
|
||||
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz}
|
||||
name: cosmiconfig
|
||||
@@ -5912,6 +6014,12 @@ packages:
|
||||
engines: {node: '>=0.3.1'}
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/dingbat-to-unicode/1.0.1:
|
||||
resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz}
|
||||
name: dingbat-to-unicode
|
||||
version: 1.0.1
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/dir-glob/3.0.1:
|
||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz}
|
||||
name: dir-glob
|
||||
@@ -5974,6 +6082,14 @@ packages:
|
||||
domhandler: registry.npmmirror.com/domhandler/4.3.1
|
||||
dev: true
|
||||
|
||||
registry.npmmirror.com/duck/0.1.12:
|
||||
resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/duck/-/duck-0.1.12.tgz}
|
||||
name: duck
|
||||
version: 0.1.12
|
||||
dependencies:
|
||||
underscore: registry.npmmirror.com/underscore/1.13.6
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/eastasianwidth/0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz}
|
||||
name: eastasianwidth
|
||||
@@ -6758,6 +6874,13 @@ packages:
|
||||
version: 1.2.3
|
||||
dev: true
|
||||
|
||||
registry.npmmirror.com/generic-pool/3.9.0:
|
||||
resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/generic-pool/-/generic-pool-3.9.0.tgz}
|
||||
name: generic-pool
|
||||
version: 3.9.0
|
||||
engines: {node: '>= 4'}
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/gensync/1.0.0-beta.2:
|
||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz}
|
||||
name: gensync
|
||||
@@ -6925,6 +7048,12 @@ packages:
|
||||
get-intrinsic: registry.npmmirror.com/get-intrinsic/1.2.0
|
||||
dev: true
|
||||
|
||||
registry.npmmirror.com/gpt-token-utils/1.2.0:
|
||||
resolution: {integrity: sha512-s8twaU38UE2Vp65JhQEjz8qvWhWY8KZYvmvYHapxlPT03Ok35Clq+gm9eE27wQILdFisseMVRSiC5lJR9GBklA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/gpt-token-utils/-/gpt-token-utils-1.2.0.tgz}
|
||||
name: gpt-token-utils
|
||||
version: 1.2.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/graceful-fs/4.2.10:
|
||||
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.10.tgz}
|
||||
name: graceful-fs
|
||||
@@ -7133,6 +7262,12 @@ packages:
|
||||
engines: {node: '>= 4'}
|
||||
dev: true
|
||||
|
||||
registry.npmmirror.com/immediate/3.0.6:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz}
|
||||
name: immediate
|
||||
version: 3.0.6
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/immer/9.0.19:
|
||||
resolution: {integrity: sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/immer/-/immer-9.0.19.tgz}
|
||||
name: immer
|
||||
@@ -7506,6 +7641,12 @@ packages:
|
||||
is-docker: registry.npmmirror.com/is-docker/2.2.1
|
||||
dev: true
|
||||
|
||||
registry.npmmirror.com/isarray/1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz}
|
||||
name: isarray
|
||||
version: 1.0.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/isarray/2.0.5:
|
||||
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz}
|
||||
name: isarray
|
||||
@@ -7609,6 +7750,17 @@ packages:
|
||||
object.assign: registry.npmmirror.com/object.assign/4.1.4
|
||||
dev: true
|
||||
|
||||
registry.npmmirror.com/jszip/3.10.1:
|
||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz}
|
||||
name: jszip
|
||||
version: 3.10.1
|
||||
dependencies:
|
||||
lie: registry.npmmirror.com/lie/3.3.0
|
||||
pako: registry.npmmirror.com/pako/1.0.11
|
||||
readable-stream: registry.npmmirror.com/readable-stream/2.3.8
|
||||
setimmediate: registry.npmmirror.com/setimmediate/1.0.5
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/jwa/1.4.1:
|
||||
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/jwa/-/jwa-1.4.1.tgz}
|
||||
name: jwa
|
||||
@@ -7684,6 +7836,14 @@ packages:
|
||||
type-check: registry.npmmirror.com/type-check/0.4.0
|
||||
dev: true
|
||||
|
||||
registry.npmmirror.com/lie/3.3.0:
|
||||
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz}
|
||||
name: lie
|
||||
version: 3.3.0
|
||||
dependencies:
|
||||
immediate: registry.npmmirror.com/immediate/3.0.6
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/lilconfig/2.0.6:
|
||||
resolution: {integrity: sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/lilconfig/-/lilconfig-2.0.6.tgz}
|
||||
name: lilconfig
|
||||
@@ -7801,6 +7961,16 @@ packages:
|
||||
dependencies:
|
||||
js-tokens: registry.npmmirror.com/js-tokens/4.0.0
|
||||
|
||||
registry.npmmirror.com/lop/0.4.1:
|
||||
resolution: {integrity: sha512-9xyho9why2A2tzm5aIcMWKvzqKsnxrf9B5I+8O30olh6lQU8PH978LqZoI4++37RBgS1Em5i54v1TFs/3wnmXQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/lop/-/lop-0.4.1.tgz}
|
||||
name: lop
|
||||
version: 0.4.1
|
||||
dependencies:
|
||||
duck: registry.npmmirror.com/duck/0.1.12
|
||||
option: registry.npmmirror.com/option/0.2.4
|
||||
underscore: registry.npmmirror.com/underscore/1.13.6
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/lowlight/1.20.0:
|
||||
resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/lowlight/-/lowlight-1.20.0.tgz}
|
||||
name: lowlight
|
||||
@@ -7826,6 +7996,23 @@ packages:
|
||||
dependencies:
|
||||
yallist: registry.npmmirror.com/yallist/4.0.0
|
||||
|
||||
registry.npmmirror.com/mammoth/1.5.1:
|
||||
resolution: {integrity: sha512-7ZioZBf/1HjYrm1qZJOO+DD+rYxLvwrHS+HVOwW89hwIp+r6ZqJ/Eq2rXSS+8ezZ3/DuW6FUUp2Dfz6e7B2pBQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/mammoth/-/mammoth-1.5.1.tgz}
|
||||
name: mammoth
|
||||
version: 1.5.1
|
||||
hasBin: true
|
||||
dependencies:
|
||||
argparse: registry.npmmirror.com/argparse/1.0.10
|
||||
bluebird: registry.npmmirror.com/bluebird/3.4.7
|
||||
dingbat-to-unicode: registry.npmmirror.com/dingbat-to-unicode/1.0.1
|
||||
jszip: registry.npmmirror.com/jszip/3.10.1
|
||||
lop: registry.npmmirror.com/lop/0.4.1
|
||||
path-is-absolute: registry.npmmirror.com/path-is-absolute/1.0.1
|
||||
sax: registry.npmmirror.com/sax/1.1.6
|
||||
underscore: registry.npmmirror.com/underscore/1.13.6
|
||||
xmlbuilder: registry.npmmirror.com/xmlbuilder/10.1.1
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/markdown-table/3.0.3:
|
||||
resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/markdown-table/-/markdown-table-3.0.3.tgz}
|
||||
name: markdown-table
|
||||
@@ -8496,6 +8683,14 @@ packages:
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/nanoid/4.0.1:
|
||||
resolution: {integrity: sha512-udKGtCCUafD3nQtJg9wBhRP3KMbPglUsgV5JVsXhvyBs/oefqb4sqMEhKBBgqZncYowu58p1prsZQBYvAj/Gww==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/nanoid/-/nanoid-4.0.1.tgz}
|
||||
name: nanoid
|
||||
version: 4.0.1
|
||||
engines: {node: ^14 || ^16 || >=18}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/napi-build-utils/1.0.2:
|
||||
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz}
|
||||
name: napi-build-utils
|
||||
@@ -8742,6 +8937,12 @@ packages:
|
||||
- debug
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/option/0.2.4:
|
||||
resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/option/-/option-0.2.4.tgz}
|
||||
name: option
|
||||
version: 0.2.4
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/optionator/0.9.1:
|
||||
resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/optionator/-/optionator-0.9.1.tgz}
|
||||
name: optionator
|
||||
@@ -8783,6 +8984,12 @@ packages:
|
||||
aggregate-error: registry.npmmirror.com/aggregate-error/3.1.0
|
||||
dev: true
|
||||
|
||||
registry.npmmirror.com/pako/1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz}
|
||||
name: pako
|
||||
version: 1.0.11
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/parent-module/1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz}
|
||||
name: parent-module
|
||||
@@ -8833,7 +9040,6 @@ packages:
|
||||
name: path-is-absolute
|
||||
version: 1.0.1
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
registry.npmmirror.com/path-key/3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz}
|
||||
@@ -8940,6 +9146,12 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/process-nextick-args/2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz}
|
||||
name: process-nextick-args
|
||||
version: 2.0.1
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/prop-types/15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz}
|
||||
name: prop-types
|
||||
@@ -9203,6 +9415,20 @@ packages:
|
||||
loose-envify: registry.npmmirror.com/loose-envify/1.4.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/readable-stream/2.3.8:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz}
|
||||
name: readable-stream
|
||||
version: 2.3.8
|
||||
dependencies:
|
||||
core-util-is: registry.npmmirror.com/core-util-is/1.0.3
|
||||
inherits: registry.npmmirror.com/inherits/2.0.4
|
||||
isarray: registry.npmmirror.com/isarray/1.0.0
|
||||
process-nextick-args: registry.npmmirror.com/process-nextick-args/2.0.1
|
||||
safe-buffer: registry.npmmirror.com/safe-buffer/5.1.2
|
||||
string_decoder: registry.npmmirror.com/string_decoder/1.1.1
|
||||
util-deprecate: registry.npmmirror.com/util-deprecate/1.0.2
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/readable-stream/3.6.1:
|
||||
resolution: {integrity: sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.1.tgz}
|
||||
name: readable-stream
|
||||
@@ -9223,6 +9449,19 @@ packages:
|
||||
picomatch: registry.npmmirror.com/picomatch/2.3.1
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/redis/4.6.5:
|
||||
resolution: {integrity: sha512-O0OWA36gDQbswOdUuAhRL6mTZpHFN525HlgZgDaVNgCJIAZR3ya06NTESb0R+TUZ+BFaDpz6NnnVvoMx9meUFg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/redis/-/redis-4.6.5.tgz}
|
||||
name: redis
|
||||
version: 4.6.5
|
||||
dependencies:
|
||||
'@redis/bloom': registry.npmmirror.com/@redis/bloom/1.2.0_@redis+client@1.5.6
|
||||
'@redis/client': registry.npmmirror.com/@redis/client/1.5.6
|
||||
'@redis/graph': registry.npmmirror.com/@redis/graph/1.1.0_@redis+client@1.5.6
|
||||
'@redis/json': registry.npmmirror.com/@redis/json/1.0.4_@redis+client@1.5.6
|
||||
'@redis/search': registry.npmmirror.com/@redis/search/1.1.2_@redis+client@1.5.6
|
||||
'@redis/time-series': registry.npmmirror.com/@redis/time-series/1.0.4_@redis+client@1.5.6
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/refractor/3.6.0:
|
||||
resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/refractor/-/refractor-3.6.0.tgz}
|
||||
name: refractor
|
||||
@@ -9459,6 +9698,12 @@ packages:
|
||||
mri: registry.npmmirror.com/mri/1.2.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/safe-buffer/5.1.2:
|
||||
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz}
|
||||
name: safe-buffer
|
||||
version: 5.1.2
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/safe-buffer/5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz}
|
||||
name: safe-buffer
|
||||
@@ -9498,6 +9743,12 @@ packages:
|
||||
source-map-js: registry.npmmirror.com/source-map-js/1.0.2
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/sax/1.1.6:
|
||||
resolution: {integrity: sha512-8zci48uUQyfqynGDSkUMD7FCJB96hwLnlZOXlgs1l3TX+LW27t3psSWKUxC0fxVgA86i8tL4NwGcY1h/6t3ESg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/sax/-/sax-1.1.6.tgz}
|
||||
name: sax
|
||||
version: 1.1.6
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/scheduler/0.23.0:
|
||||
resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/scheduler/-/scheduler-0.23.0.tgz}
|
||||
name: scheduler
|
||||
@@ -9522,6 +9773,12 @@ packages:
|
||||
dependencies:
|
||||
lru-cache: registry.npmmirror.com/lru-cache/6.0.0
|
||||
|
||||
registry.npmmirror.com/setimmediate/1.0.5:
|
||||
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz}
|
||||
name: setimmediate
|
||||
version: 1.0.5
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/sharp/0.31.3:
|
||||
resolution: {integrity: sha512-XcR4+FCLBFKw1bdB+GEhnUNXNXvnt0tDo4WsBsraKymuo/IAuPuCBVAL2wIkUw2r/dwFW5Q5+g66Kwl2dgDFVg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/sharp/-/sharp-0.31.3.tgz}
|
||||
name: sharp
|
||||
@@ -9705,6 +9962,12 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
registry.npmmirror.com/sprintf-js/1.0.3:
|
||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz}
|
||||
name: sprintf-js
|
||||
version: 1.0.3
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/stable/0.1.8:
|
||||
resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/stable/-/stable-0.1.8.tgz}
|
||||
name: stable
|
||||
@@ -9785,6 +10048,14 @@ packages:
|
||||
es-abstract: registry.npmmirror.com/es-abstract/1.21.1
|
||||
dev: true
|
||||
|
||||
registry.npmmirror.com/string_decoder/1.1.1:
|
||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz}
|
||||
name: string_decoder
|
||||
version: 1.1.1
|
||||
dependencies:
|
||||
safe-buffer: registry.npmmirror.com/safe-buffer/5.1.2
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/string_decoder/1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz}
|
||||
name: string_decoder
|
||||
@@ -10141,6 +10412,12 @@ packages:
|
||||
which-boxed-primitive: registry.npmmirror.com/which-boxed-primitive/1.0.2
|
||||
dev: true
|
||||
|
||||
registry.npmmirror.com/underscore/1.13.6:
|
||||
resolution: {integrity: sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/underscore/-/underscore-1.13.6.tgz}
|
||||
name: underscore
|
||||
version: 1.13.6
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/unicode-canonical-property-names-ecmascript/2.0.0:
|
||||
resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz}
|
||||
name: unicode-canonical-property-names-ecmascript
|
||||
@@ -10335,13 +10612,6 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
registry.npmmirror.com/uuid/9.0.0:
|
||||
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/uuid/-/uuid-9.0.0.tgz}
|
||||
name: uuid
|
||||
version: 9.0.0
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/uvu/0.5.6:
|
||||
resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/uvu/-/uvu-0.5.6.tgz}
|
||||
name: uvu
|
||||
@@ -10488,6 +10758,13 @@ packages:
|
||||
name: wrappy
|
||||
version: 1.0.2
|
||||
|
||||
registry.npmmirror.com/xmlbuilder/10.1.1:
|
||||
resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz}
|
||||
name: xmlbuilder
|
||||
version: 10.1.1
|
||||
engines: {node: '>=4.0'}
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/xtend/4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz}
|
||||
name: xtend
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
{"prompt":"什么是死锁?如何避免死锁?\n\n###\n\n","completion":" 死锁是指两个或多个进程在执行过程中,由于互相等待对方释放资源而造成的一种僵局。避免死锁的常用方法包括:破坏互斥条件、破坏请求和保持条件、破坏不剥夺条件、破坏循环等待条件。###"}
|
||||
{"prompt":"什么是虚拟内存?\n\n###\n\n","completion":" 虚拟内存是指一种计算机内存管理技术,它使得操作系统可以将进程所使用的虚拟地址空间映射到物理内存中,从而可以让多个进程共享物理内存。当物理内存不足时,操作系统会将一部分不常用的内存页面暂时存储到硬盘上,这样就可以释放出更多的物理内存供其他进程使用。###"}
|
||||
{"prompt":"什么是分页和分段?有什么区别?\n\n###\n\n","completion":" 分页和分段是虚拟内存管理中常用的两种技术。分页是将虚拟地址空间划分为固定大小的页面,而分段则是将虚拟地址空间划分为不同大小的段。区别在于,分页的页面大小是固定的,而分段的段大小是可变的;另外,分页是为了实现虚拟内存,而分段则是为了实现逻辑地址空间的管理。###"}
|
||||
{"prompt":"什么是lru算法?\n\n###\n\n","completion":" lru算法是一种页面置换算法,它的全称是least recently used,即最近最少使用算法。lru算法的基本思想是在物理内存中维护一个页面访问历史记录,当需要替换一页时,选择最近最少被访问的页面进行替换。###"}
|
||||
{"prompt":"什么是文件系统?\n\n###\n\n","completion":" 文件系统是一种用于管理计算机存储设备上文件和目录的组织方式。文件系统通常包括存储管理、目录管理、文件访问控制、文件系统维护等功能。常见的文件系统包括fat、ntfs、ext等。###"}
|
||||
{"prompt":"什么是并发和并行?\n\n###\n\n","completion":" 并发是指多个任务交替执行的情况,而并行是指多个任务同时执行的情况。并发的实现方式是通过时间分片技术,而并行的实现方式是通过多核处理器等硬件设备。###"}
|
||||
{"prompt":"什么是系统调用?\n\n###\n\n","completion":" 系统调用是操作系统中的一种机制,它允许用户空间程序请求操作系统内核提供一些服务,如读写文件、创建进程等。系统调用通常是通过软中断实现的###"}
|
||||
{"prompt":"什么是进程间通信?\n\n###\n\n","completion":" 进程间通信是指不同进程之间进行数据交换和通信的机制,常见的方式有管道、共享内存、消息队列、信号量等。###"}
|
||||
{"prompt":"什么是文件描述符?\n\n###\n\n","completion":" 文件描述符是一个非负整数,它是操作系统内核为了管理打开的文件而维护的一种抽象概念。应用程序使用文件描述符来访问文件或其他i\/o设备###"}
|
||||
{"prompt":"什么是中断? \n\n###\n\n","completion":" 中断是指计算机硬件或软件在执行程序时,暂停当前程序的执行,转而去执行另一个程序或处理器所需的其他任务的过程。中断可以是外部中断、内部中断或软中断。###"}
|
||||
{"prompt":"什么是页表?\n\n###\n\n","completion":" 页表是一种数据结构,它用于将虚拟地址转换为物理地址。在虚拟内存中,每个进程都有自己的页表,它描述了虚拟地址空间中每个页面对应的物理地址。 ###"}
|
||||
{"prompt":"什么是僵尸进程?\n\n###\n\n","completion":" 僵尸进程是指一个已经完成执行但其父进程还没有对其进行善后处理的进程,它会一直占用系统资源,直到其父进程将其处理为止。###"}
|
||||
{"prompt":"什么是守护进程? \n\n###\n\n","completion":" 守护进程是一种在后台运行的特殊进程,它通常用于提供某种服务或执行某种系统任务。守护进程通常不与任何用户界面交互,也不会终止,直到操作系统关机或重启。###"}
|
||||
{"prompt":"计算机操作系统是什么?\n\n###\n\n","completion":" 计算机操作系统是一种控制计算机硬件和协调应用程序运行的系统软件。###"}
|
||||
{"prompt":"什么是进程?\n\n###\n\n","completion":" 进程是一个正在执行中的程序实例,它包括了程序计数器、堆栈、数据区等信息。###"}
|
||||
{"prompt":"什么是线程?\n\n###\n\n","completion":" 线程是操作系统能够进行运算调度的最小单位,它是进程的一个执行流程,共享进程的地址空间。###"}
|
||||
{"prompt":"什么是虚拟内存?\n\n###\n\n","completion":" 虚拟内存是一种计算机系统内存管理技术,它使得应用程序认为它拥有连续的可用的内存,而实际上它通常是被分成多个物理内存碎片。###"}
|
||||
{"prompt":"什么是死锁?\n\n###\n\n","completion":" 死锁是指两个或多个进程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致所有进程都无法继续执行。###"}
|
||||
{"prompt":"什么是缓存?\n\n###\n\n","completion":" 缓存是指在计算机中暂存数据的高速存储器,它可以提高计算机对数据的访问速度,避免频繁地访问较慢的主存储器。###"}
|
||||
{"prompt":"什么是文件系统?\n\n###\n\n","completion":" 文件系统是计算机中用来管理和组织文件的一种机制,它通过一系列的数据结构来描述文件和目录的组织方式,以及文件如何存储和访问。###"}
|
||||
{"prompt":"什么是调度算法?\n\n###\n\n","completion":" 调度算法是指操作系统中用来决定进程或线程在cpu上执行顺序的一种算法,它的目标是最大化系统吞吐量、最小化响应时间或最大化资源利用率等。###"}
|
||||
{"prompt":"什么是百度plato?</s>","completion":" 百度PLATO是一种聊天机器人,发布于2021年10月,参数量为百亿,是GPT-3的1/17。</s>"}
|
||||
{"prompt":"与百度plato相比,chatgpt有哪些优点?</s>","completion":" 与百度PLATO相比,ChatGPT体现出耐心、体贴和妥当,被网友们普遍称赞。ChatGPT可以用于个性化聊天机器人,例如生成一个很相似的数字人,提供一位温柔健谈的朋友,指导语言学习等。</s>"}
|
||||
{"prompt":"replika是什么?</s>","completion":" Replika是一个产品,使用ChatGPT的技术,可以根据自己的需求生成一个虚拟女友、男友或单纯的朋友。目前还引起了不少有关性骚扰、家暴、隐私等问题的争议。</s>"}
|
||||
{"prompt":"聊天机器人可以用于哪些场景?</s>","completion":" 聊天机器人可以用于很多陪伴的场景,例如亲人去世、抑郁症或自闭症患者、语言学习、小朋友、养老院或独居在家的老人等。</s>"}
|
||||
{"prompt":"有人反对使用聊天机器人解决孤独困境,为什么?</s>","completion":" 有人反对使用聊天机器人解决孤独困境,认为孤独困境应该用人与人真正的交往解决,机器是冰冷的。</s>"}
|
||||
{"prompt":"为什么聊天机器人仍然需要解决当下的需求?</s>","completion":" 在社会结构性的困局还没有解开时,长夜漫漫的独身打工人、养老院里卧床的老人、父母在外留守在家的儿童,当下的需求仍需要解决。</s>"}
|
||||
{"prompt":"ai在未来会被戴上什么样的帽子?</s>","completion":" AI在未来各个场景下都会被戴上一个很虚无的帽子:“机器是冰冷的”、“没有灵魂”、“它并不是真正在思考”。</s>"}
|
||||
{"prompt":"怎么回答“ai是否真正思考”的问题?</s>","completion":" 引用乔姆斯基老爷子的那句名言:“思考是人类特性。AI有天会思考吗?那就像是在问潜水艇会不会游泳一样。”</s>"}
|
||||
{"prompt":"聊天机器人与人类完成任务的方式有何不同?</s>","completion":" 聊天机器人与人类完成任务的方式不同,但并不重要。它们能跟人一样完成任务,这是最重要的。</s>"}
|
||||
{"prompt":"聊天机器人的发展前景如何?</s>","completion":" 聊天机器人的发展前景广阔,可以应用于很多领域,例如客服、教育、医疗等。</s>"}
|
||||
{"prompt":"什么是百度plato?</s>","completion":" 百度PLATO是一种聊天机器人,发布于2021年10月,参数量为百亿,是GPT-3的1/17。</s>"}
|
||||
{"prompt":"与百度plato相比,chatgpt有哪些优点?</s>","completion":" 与百度PLATO相比,ChatGPT体现出耐心、体贴和妥当,被网友们普遍称赞。ChatGPT还可以用于很多陪伴的场景,如亲人去世、抑郁症或自闭症患者、语言学习、小朋友、养老院或独居在家的老人等。</s>"}
|
||||
{"prompt":"replika是什么产品?</s>","completion":" Replika是一个产品,用的是ChatGPT的技术。用户可以根据自己的需求生成一个虚拟女友、男友或单纯的朋友。目前还引起了不少有关性骚扰、家暴、隐私等问题的争议。</s>"}
|
||||
{"prompt":"聊天机器人能够解决哪些现实问题?</s>","completion":" 聊天机器人能够解决现代人面临的孤独困境,如长夜漫漫的独身打工人、养老院里卧床的老人、父母在外留守在家的儿童等。</s>"}
|
||||
{"prompt":"有人反对使用聊天机器人解决孤独困境,为什么?</s>","completion":" 有人反对使用聊天机器人解决孤独困境,认为孤独困境应该用人与人真正的交往解决,机器是冰冷的。</s>"}
|
||||
|
||||
BIN
public/imgs/wxcode.jpg
Normal file
BIN
public/imgs/wxcode.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 320 KiB |
15792
public/js/pdf.js
Normal file
15792
public/js/pdf.js
Normal file
File diff suppressed because it is too large
Load Diff
64521
public/js/pdf.worker.js
vendored
Normal file
64521
public/js/pdf.worker.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/js/qrcode.min.js
vendored
Normal file
1
public/js/qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -5,7 +5,8 @@ import type { InitChatResponse } from './response/chat';
|
||||
/**
|
||||
* 获取一个聊天框的ID
|
||||
*/
|
||||
export const getChatSiteId = (modelId: string) => GET<string>(`/chat/generate?modelId=${modelId}`);
|
||||
export const getChatSiteId = (modelId: string, isShare = false) =>
|
||||
GET<string>(`/chat/generate?modelId=${modelId}&isShare=${isShare ? 'true' : 'false'}`);
|
||||
|
||||
/**
|
||||
* 获取初始化聊天内容
|
||||
@@ -38,6 +39,7 @@ export const postSaveChat = (data: { chatId: string; prompts: ChatItemType[] })
|
||||
POST('/chat/saveChat', data);
|
||||
|
||||
/**
|
||||
* 删除最后一句
|
||||
* 删除一句对话
|
||||
*/
|
||||
export const delLastMessage = (chatId: string) => DELETE(`/chat/delLastMessage?chatId=${chatId}`);
|
||||
export const delChatRecordByIndex = (chatId: string, index: number) =>
|
||||
DELETE(`/chat/delChatRecordByIndex?chatId=${chatId}&index=${index}`);
|
||||
|
||||
25
src/api/data.ts
Normal file
25
src/api/data.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { GET, POST, DELETE, PUT } from './request';
|
||||
import { RequestPaging } from '../types/index';
|
||||
import { Obj2Query } from '@/utils/tools';
|
||||
import type { DataListItem } from '@/types/data';
|
||||
import type { PagingData } from '../types/index';
|
||||
import type { DataItemSchema } from '@/types/mongoSchema';
|
||||
import type { CreateDataProps } from '@/pages/data/components/CreateDataModal';
|
||||
|
||||
export const getDataList = () => GET<DataListItem[]>(`/data/getDataList`);
|
||||
|
||||
export const postData = (data: CreateDataProps) => POST<string>(`/data/postData`, data);
|
||||
|
||||
export const postSplitData = (dataId: string, text: string) =>
|
||||
POST(`/data/splitData`, { dataId, text });
|
||||
|
||||
export const updateDataName = (dataId: string, name: string) =>
|
||||
PUT(`/data/putDataName?dataId=${dataId}&name=${name}`);
|
||||
|
||||
export const delData = (dataId: string) => DELETE(`/data/delData?dataId=${dataId}`);
|
||||
|
||||
type GetDataItemsProps = RequestPaging & {
|
||||
dataId: string;
|
||||
};
|
||||
export const getDataItems = (data: GetDataItemsProps) =>
|
||||
GET<PagingData<DataItemSchema>>(`/data/getDataItems?${Obj2Query(data)}`);
|
||||
@@ -1,17 +1,21 @@
|
||||
import { getToken } from '../utils/user';
|
||||
interface StreamFetchProps {
|
||||
url: string;
|
||||
data: any;
|
||||
onMessage: (text: string) => void;
|
||||
abortSignal: AbortController;
|
||||
}
|
||||
export const streamFetch = ({ url, data, onMessage }: StreamFetchProps) =>
|
||||
export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchProps) =>
|
||||
new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: getToken() || ''
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
body: JSON.stringify(data),
|
||||
signal: abortSignal.signal
|
||||
});
|
||||
const reader = res.body?.getReader();
|
||||
if (!reader) return;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { GET, POST, DELETE, PUT } from './request';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import type { ModelSchema, ModelDataSchema, ModelSplitDataSchema } from '@/types/mongoSchema';
|
||||
import { ModelUpdateParams } from '@/types/model';
|
||||
import { TrainingItemType } from '../types/training';
|
||||
import { RequestPaging } from '../types/index';
|
||||
import { Obj2Query } from '@/utils/tools';
|
||||
|
||||
export const getMyModels = () => GET<ModelSchema[]>('/model/list');
|
||||
|
||||
@@ -16,13 +18,38 @@ export const putModelById = (id: string, data: ModelUpdateParams) =>
|
||||
PUT(`/model/update?modelId=${id}`, data);
|
||||
|
||||
export const postTrainModel = (id: string, form: FormData) =>
|
||||
POST(`/model/train?modelId=${id}`, form, {
|
||||
POST(`/model/train/train?modelId=${id}`, form, {
|
||||
headers: {
|
||||
'content-type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
export const putModelTrainingStatus = (id: string) => PUT(`/model/putTrainStatus?modelId=${id}`);
|
||||
export const putModelTrainingStatus = (id: string) =>
|
||||
PUT(`/model/train/putTrainStatus?modelId=${id}`);
|
||||
|
||||
export const getModelTrainings = (id: string) =>
|
||||
GET<TrainingItemType[]>(`/model/getTrainings?modelId=${id}`);
|
||||
GET<TrainingItemType[]>(`/model/train/getTrainings?modelId=${id}`);
|
||||
|
||||
/* 模型 data */
|
||||
|
||||
type GetModelDataListProps = RequestPaging & {
|
||||
modelId: string;
|
||||
};
|
||||
export const getModelDataList = (props: GetModelDataListProps) =>
|
||||
GET(`/model/data/getModelData?${Obj2Query(props)}`);
|
||||
|
||||
export const getModelSplitDataList = (modelId: string) =>
|
||||
GET<ModelSplitDataSchema[]>(`/model/data/getSplitData?modelId=${modelId}`);
|
||||
|
||||
export const postModelDataInput = (data: {
|
||||
modelId: string;
|
||||
data: { text: ModelDataSchema['text']; q: ModelDataSchema['q'] }[];
|
||||
}) => POST(`/model/data/pushModelDataInput`, data);
|
||||
|
||||
export const postModelDataFileText = (modelId: string, text: string) =>
|
||||
POST(`/model/data/splitData`, { modelId, text });
|
||||
|
||||
export const putModelDataById = (data: { dataId: string; text: string }) =>
|
||||
PUT('/model/data/putModelData', data);
|
||||
export const delOneModelData = (dataId: string) =>
|
||||
DELETE(`/model/data/delModelDataById?dataId=${dataId}`);
|
||||
|
||||
@@ -37,7 +37,7 @@ function checkRes(data: ResponseDataType) {
|
||||
console.log('error->', data, 'data is empty');
|
||||
return Promise.reject('服务器异常');
|
||||
} else if (data.code < 200 || data.code >= 400) {
|
||||
return Promise.reject(data.message);
|
||||
return Promise.reject(data);
|
||||
}
|
||||
return data.data;
|
||||
}
|
||||
|
||||
5
src/api/response/chat.d.ts
vendored
5
src/api/response/chat.d.ts
vendored
@@ -7,8 +7,7 @@ export type InitChatResponse = {
|
||||
name: string;
|
||||
avatar: string;
|
||||
intro: string;
|
||||
secret: ModelSchema.secret;
|
||||
chatModel: ModelSchema.service.ChatModel; // 模型名
|
||||
chatModel: ModelSchema.service.chatModel; // 对话模型名
|
||||
modelName: ModelSchema.service.modelName; // 底层模型
|
||||
history: ChatItemType[];
|
||||
isExpiredTime: boolean;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { GET, POST, PUT } from './request';
|
||||
import { createHashPassword } from '@/utils/tools';
|
||||
import { createHashPassword, Obj2Query } from '@/utils/tools';
|
||||
import { ResLogin } from './response/user';
|
||||
import { EmailTypeEnum } from '@/constants/common';
|
||||
import { UserType, UserUpdateParams } from '@/types/user';
|
||||
import type { PagingData, RequestPaging } from '@/types';
|
||||
import { BillSchema, PaySchema } from '@/types/mongoSchema';
|
||||
import { adaptBill } from '@/utils/adapt';
|
||||
|
||||
export const sendCodeToEmail = ({ email, type }: { email: string; type: `${EmailTypeEnum}` }) =>
|
||||
GET('/user/sendEmail', { email, type });
|
||||
@@ -46,3 +49,19 @@ export const postLogin = ({ email, password }: { email: string; password: string
|
||||
});
|
||||
|
||||
export const putUserInfo = (data: UserUpdateParams) => PUT('/user/update', data);
|
||||
|
||||
export const getUserBills = (data: RequestPaging) =>
|
||||
GET<PagingData<BillSchema>>(`/user/getBill?${Obj2Query(data)}`).then((res) => ({
|
||||
...res,
|
||||
data: res.data.map((bill) => adaptBill(bill))
|
||||
}));
|
||||
|
||||
export const getPayOrders = () => GET<PaySchema[]>(`/user/getPayOrders`);
|
||||
|
||||
export const getPayCode = (amount: number) =>
|
||||
GET<{
|
||||
codeUrl: string;
|
||||
payId: string;
|
||||
}>(`/user/getPayCode?amount=${amount}`);
|
||||
|
||||
export const checkPayResult = (payId: string) => GET<number>(`/user/checkPayResult?payId=${payId}`);
|
||||
|
||||
1
src/components/Icon/icons/chatSend.svg
Normal file
1
src/components/Icon/icons/chatSend.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679805359001" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1328" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M416.583186 1022.194004c-5.417989 0-10.835979-1.203998-16.253968-3.611993-15.049971-6.621987-24.681952-21.069959-24.681952-37.323927l0-299.795414c0-12.641975 5.417989-24.079953 15.651969-31.905938 9.631981-7.825985 22.273956-10.23398 34.915932-7.825985l417.787184 99.931805 84.279835-599.590829c1.203998-9.631981-8.427984-16.253968-16.855967-11.437978L147.489712 573.102881l139.061728 35.517931c19.865961 4.815991 34.313933 22.875955 32.507937 43.343915-2.407995 25.885949-26.487948 42.139918-50.567901 36.119929L30.70194 627.282775c-16.253968-4.213992-27.691946-17.457966-30.099941-33.711934-2.407995-16.253968 5.417989-32.507937 19.865961-40.93592L962.59612 6.621987c13.243974-7.825985 30.099941-7.223986 43.343915 1.203998 12.641975 8.427984 19.865961 24.079953 17.457966 39.129924l-105.349794 750.090535c-1.805996 11.437978-7.825985 21.671958-17.457966 28.293945-9.631981 6.621987-21.069959 8.427984-32.507937 6.019988l-411.165197-98.125808 0 154.111699 81.87184-76.453851c15.049971-13.845973 37.925926-16.855967 54.179894-4.213992 20.46796 15.651969 21.069959 45.149912 3.009994 62.005879L444.275132 1011.358025C436.449148 1018.582011 426.817166 1022.194004 416.583186 1022.194004L416.583186 1022.194004z" p-id="1329"></path><path d="M416.583186 722.398589c-9.631981 0-19.263962-3.611993-27.089947-10.23398-16.855967-15.049971-18.059965-40.93592-3.009994-57.791887l216.117578-242.003527c15.049971-16.855967 40.93592-18.059965 57.791887-3.009994 16.855967 15.049971 18.059965 40.93592 3.009994 57.791887l-216.117578 242.003527C438.857143 718.184597 427.419165 722.398589 416.583186 722.398589L416.583186 722.398589z" p-id="1330"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
src/components/Icon/icons/copy.svg
Normal file
1
src/components/Icon/icons/copy.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679805221456" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1173" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M267.3 834.6h-96.5c-27.4 0-49.7-22.3-49.7-49.7V115.2c0-27.4 22.3-49.7 49.7-49.7H727c27.4 0 49.7 22.3 49.7 49.7v96.5h-42.6v-96.5c0-3.9-3.2-7.1-7.1-7.1H170.8c-3.9 0-7.1 3.2-7.1 7.1v669.7c0 3.9 3.2 7.1 7.1 7.1h96.5v42.6z" p-id="1174"></path><path d="M851.9 959.5H295.7c-27.4 0-49.7-22.3-49.7-49.7V240.1c0-27.4 22.3-49.7 49.7-49.7h556.2c27.4 0 49.7 22.3 49.7 49.7v669.7c-0.1 27.4-22.3 49.7-49.7 49.7zM295.7 233c-3.9 0-7.1 3.2-7.1 7.1v669.7c0 3.9 3.2 7.1 7.1 7.1h556.2c3.9 0 7.1-3.2 7.1-7.1V240.1c0-3.9-3.2-7.1-7.1-7.1H295.7z" p-id="1175"></path></svg>
|
||||
|
After Width: | Height: | Size: 878 B |
1
src/components/Icon/icons/menu.svg
Normal file
1
src/components/Icon/icons/menu.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679316084227" class="icon" viewBox="0 0 1305 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1173" xmlns:xlink="http://www.w3.org/1999/xlink" width="61.171875" height="48"><path d="M0.837818 75.218317c0 19.642164 8.098902 39.191237 21.969435 53.06177 13.963624 13.963624 33.512697 22.062525 53.247951 22.062525a76.055204 76.055204 0 0 0 53.06177-21.969434c13.963624-13.963624 22.062525-33.512697 22.062526-53.154861A76.055204 76.055204 0 0 0 129.303156 21.970365 76.055204 76.055204 0 0 0 76.055204 0.000931a76.055204 76.055204 0 0 0-53.247951 21.969434A76.706839 76.706839 0 0 0 0.837818 75.218317M0.837818 476.160498c0 19.642164 8.005811 39.377419 21.969435 53.247952 13.963624 13.963624 33.419606 21.969435 53.247951 21.969434a76.241385 76.241385 0 0 0 53.154861-21.969434 75.962113 75.962113 0 0 0 21.969435-53.247952 76.241385 76.241385 0 0 0-21.969435-53.154861 75.962113 75.962113 0 0 0-53.154861-21.969434 76.241385 76.241385 0 0 0-53.247951 21.969434 75.962113 75.962113 0 0 0-21.969435 53.154861M0.837818 877.19577c0 19.642164 8.005811 39.284328 21.969435 53.247951 13.963624 13.963624 33.419606 21.969435 53.247951 21.969435a76.241385 76.241385 0 0 0 53.154861-21.969435 75.962113 75.962113 0 0 0 21.969435-53.247951 76.241385 76.241385 0 0 0-21.969435-53.247952 75.962113 75.962113 0 0 0-53.154861-21.969434 76.241385 76.241385 0 0 0-53.247951 21.969434 76.520658 76.520658 0 0 0-21.969435 53.247952M1304.109361 75.218317c0 41.518508-32.395607 75.124295-72.331571 75.124295H373.945843c-40.029055 0-72.331571-33.512697-72.331571-75.124295C301.521181 33.513628 333.916788 0.000931 373.945843 0.000931h857.831947c40.029055 0 72.331571 33.605788 72.331571 75.217386M1231.77779 551.377884H373.945843c-40.029055 0-72.331571-33.605788-72.331571-75.217386 0-41.518508 32.302516-75.124295 72.331571-75.124295h857.831947c40.029055-0.186182 72.331571 33.512697 72.331571 75.124295 0 41.425417-32.395607 75.217386-72.331571 75.217386zM1304.109361 877.102679c0 41.611599-32.395607 75.310477-72.331571 75.310477H373.945843c-40.029055 0-72.331571-33.698878-72.331571-75.310477 0-41.425417 32.302516-75.124295 72.331571-75.124295h857.831947c40.029055-0.093091 72.331571 33.698878 72.331571 75.124295" p-id="1174"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
1
src/components/Icon/icons/pay.svg
Normal file
1
src/components/Icon/icons/pay.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679410564438" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2824" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M693.095316 281.760857l-131.632817 223.935003 103.718481 0 0 49.478312-120.846571 0 0 68.193688 120.846571 0 0 50.115659-120.846571 0 0 99.276514-62.164435 0L482.169975 673.483519 356.88022 673.483519l0-50.115659 125.289755 0 0-68.193688L356.88022 555.174172l0-49.478312 106.893053 0-130.364204-223.935003 70.099647 0c60.895822 111.230417 97.898433 181.748475 111.012698 211.562689l1.268612 0c4.441967-12.262847 16.596562-37.002611 36.474732-74.219292l74.536749-137.343396L693.095316 281.760857 693.095316 281.760857zM693.095316 281.760857" p-id="2825"></path><path d="M784.470674 621.448522c-15.061578 0-27.247797 12.187435-27.247797 27.247797s12.187435 27.247797 27.247797 27.247797l71.98128 0c-61.204765 128.843816-192.338895 217.986027-344.464118 217.986027-210.6687 0-381.478892-170.782216-381.478892-381.475243 0-210.696675 170.810191-381.465512 381.478892-381.465512 192.121175 0 350.635679 142.189179 377.137878 326.968701l55.08064 0C917.333181 242.953241 734.255278 76.493794 511.987837 76.493794 271.197197 76.493794 76.012135 271.688586 76.012135 512.456117c0 240.762665 195.185062 435.972053 435.975702 435.972053 164.236031 0 307.128238-90.894915 381.475243-225.064956l0 61.57574c0 15.061578 12.187435 27.247797 27.276989 27.247797 15.004412 0 27.247797-12.187435 27.247797-27.247797L947.987865 648.697535c0-3.297419 0-27.247797-27.247797-27.247797L784.470674 621.449738 784.470674 621.448522zM784.470674 621.448522" p-id="2826"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -4,18 +4,21 @@ import { Icon } from '@chakra-ui/react';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const map = {
|
||||
model: dynamic(() => import('./icons/model.svg')),
|
||||
share: dynamic(() => import('./icons/share.svg')),
|
||||
home: dynamic(() => import('./icons/home.svg'))
|
||||
model: require('./icons/model.svg').default,
|
||||
share: require('./icons/share.svg').default,
|
||||
home: require('./icons/home.svg').default,
|
||||
menu: require('./icons/menu.svg').default,
|
||||
pay: require('./icons/pay.svg').default,
|
||||
copy: require('./icons/copy.svg').default,
|
||||
chatSend: require('./icons/chatSend.svg').default
|
||||
};
|
||||
|
||||
const MyIcon = ({
|
||||
name,
|
||||
w = 'auto',
|
||||
h = 'auto',
|
||||
...props
|
||||
}: { name: keyof typeof map } & IconProps) => {
|
||||
return map[name] ? <Icon as={map[name]} w={w} h={h} {...props} /> : null;
|
||||
export type IconName = keyof typeof map;
|
||||
|
||||
const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconName } & IconProps) => {
|
||||
return map[name] ? (
|
||||
<Icon as={map[name]} w={w} h={h} boxSizing={'content-box'} verticalAlign={'top'} {...props} />
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default MyIcon;
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { getTokenLogin } from '@/api/user';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
const unAuthPage: { [key: string]: boolean } = {
|
||||
'/': true,
|
||||
'/login': true,
|
||||
'/chat': true
|
||||
'/login': true
|
||||
};
|
||||
|
||||
const Auth = ({ children }: { children: JSX.Element }) => {
|
||||
@@ -19,7 +17,7 @@ const Auth = ({ children }: { children: JSX.Element }) => {
|
||||
position: 'top',
|
||||
status: 'warning'
|
||||
});
|
||||
const { userInfo, setUserInfo } = useUserStore();
|
||||
const { userInfo, initUserInfo } = useUserStore();
|
||||
const { setLoading } = useGlobalStore();
|
||||
|
||||
useQuery(
|
||||
@@ -29,15 +27,10 @@ const Auth = ({ children }: { children: JSX.Element }) => {
|
||||
return setLoading(false);
|
||||
} else {
|
||||
setLoading(true);
|
||||
return getTokenLogin();
|
||||
return initUserInfo();
|
||||
}
|
||||
},
|
||||
{
|
||||
onSuccess(user) {
|
||||
if (user) {
|
||||
setUserInfo(user);
|
||||
}
|
||||
},
|
||||
onError(error) {
|
||||
console.log('error->', error);
|
||||
router.push('/login');
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import Link from 'next/link';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Box, useColorMode, Flex } from '@chakra-ui/react';
|
||||
import Navbar from './navbar';
|
||||
import NavbarPhone from './navbarPhone';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -30,8 +29,8 @@ const navbarList = [
|
||||
// {
|
||||
// label: '数据',
|
||||
// icon: 'icon-datafull',
|
||||
// link: '/training/dataList',
|
||||
// activeLink: ['/training/dataList']
|
||||
// link: '/data/list',
|
||||
// activeLink: ['/data/list', '/data/detail']
|
||||
// },
|
||||
{
|
||||
label: '账号',
|
||||
@@ -44,9 +43,16 @@ const navbarList = [
|
||||
const Layout = ({ children }: { children: JSX.Element }) => {
|
||||
const { isPc } = useScreen();
|
||||
const router = useRouter();
|
||||
const { colorMode, setColorMode } = useColorMode();
|
||||
const { Loading } = useLoading({ defaultLoading: true });
|
||||
const { loading } = useGlobalStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (colorMode === 'dark' && router.pathname !== '/chat') {
|
||||
setColorMode('light');
|
||||
}
|
||||
}, [colorMode, router.pathname, setColorMode]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!unShowLayoutRoute[router.pathname] ? (
|
||||
@@ -56,29 +62,21 @@ const Layout = ({ children }: { children: JSX.Element }) => {
|
||||
<Box h={'100%'} position={'fixed'} left={0} top={0} w={'80px'}>
|
||||
<Navbar navbarList={navbarList} />
|
||||
</Box>
|
||||
<Box ml={'80px'} p={7}>
|
||||
<Box maxW={'1100px'} m={'auto'}>
|
||||
<Box h={'100%'} ml={'80px'}>
|
||||
<Box h={'100%'} py={7} px={'5vw'} m={'auto'} overflowY={'auto'}>
|
||||
<Auth>{children}</Auth>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Box pt={'60px'}>
|
||||
<Box
|
||||
h={'60px'}
|
||||
position={'fixed'}
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
zIndex={100}
|
||||
borderBottom={'1px solid rgba(0,0,0,0.1)'}
|
||||
>
|
||||
<Flex h={'100%'} flexDirection={'column'}>
|
||||
<Box h={'60px'} borderBottom={'1px solid rgba(0,0,0,0.1)'}>
|
||||
<NavbarPhone navbarList={navbarList} />
|
||||
</Box>
|
||||
<Box py={3} px={4}>
|
||||
<Box flex={'1 0 0'} h={0} py={3} px={4} overflowY={'auto'}>
|
||||
<Auth>{children}</Auth>
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
width: 4px;
|
||||
height: 14px;
|
||||
transform: translate(4px, 2px) scaleY(1.3);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
background-color: var(--chakra-colors-chakra-body-text);
|
||||
animation: blink 0.6s infinite;
|
||||
}
|
||||
.animation {
|
||||
@@ -14,7 +14,7 @@
|
||||
width: 4px;
|
||||
height: 14px;
|
||||
transform: translate(4px, 2px) scaleY(1.3);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
background-color: var(--chakra-colors-chakra-body-text);
|
||||
animation: blink 0.6s infinite;
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@
|
||||
.markdown h6 {
|
||||
cursor: text;
|
||||
font-weight: bold;
|
||||
margin: 20px 0 10px;
|
||||
margin: 10px 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
@@ -65,7 +65,6 @@
|
||||
.markdown h4 .mini-icon-link,
|
||||
.markdown h5 .mini-icon-link,
|
||||
.markdown h6 .mini-icon-link {
|
||||
color: #000000;
|
||||
display: none;
|
||||
}
|
||||
.markdown h1:hover a.anchor,
|
||||
@@ -103,11 +102,9 @@
|
||||
font-size: inherit;
|
||||
}
|
||||
.markdown h1 {
|
||||
color: #000000;
|
||||
font-size: 28px;
|
||||
}
|
||||
.markdown h2 {
|
||||
color: #000000;
|
||||
font-size: 24px;
|
||||
}
|
||||
.markdown h3 {
|
||||
@@ -120,8 +117,7 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
.markdown h6 {
|
||||
color: #777777;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.markdown p,
|
||||
.markdown blockquote,
|
||||
@@ -130,15 +126,7 @@
|
||||
.markdown dl,
|
||||
.markdown table,
|
||||
.markdown pre {
|
||||
margin: 15px 0;
|
||||
}
|
||||
.markdown hr {
|
||||
background: url('https://a248.e.akamai.net/assets.github.com/assets/primer/markdown/dirty-shade-350cca8f57223ebd53603021b2e670f4f319f1b7.png')
|
||||
repeat-x scroll 0 0 transparent;
|
||||
border: 0 none;
|
||||
color: #cccccc;
|
||||
height: 4px;
|
||||
padding: 0;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.markdown > h2:first-child,
|
||||
.markdown > h1:first-child,
|
||||
@@ -321,7 +309,6 @@
|
||||
}
|
||||
.markdown code,
|
||||
.markdown tt {
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 3px 3px 3px 3px;
|
||||
margin: 0 2px;
|
||||
@@ -336,7 +323,6 @@
|
||||
}
|
||||
.markdown .highlight pre,
|
||||
.markdown pre {
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 3px 3px 3px 3px;
|
||||
font-size: max(0.9em, 14px);
|
||||
@@ -351,10 +337,14 @@
|
||||
}
|
||||
.markdown {
|
||||
text-align: justify;
|
||||
word-break: break-all;
|
||||
overflow-y: hidden;
|
||||
tab-size: 4;
|
||||
word-spacing: normal;
|
||||
word-break: break-all;
|
||||
|
||||
p {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
@@ -365,11 +355,11 @@
|
||||
border-radius: 0;
|
||||
background-color: #222 !important;
|
||||
overflow-x: auto;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background-color: #222 !important;
|
||||
color: #fff;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -381,6 +371,7 @@
|
||||
table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0px;
|
||||
color: var(--chakra-colors-gray-700);
|
||||
|
||||
thead tr:first-child th {
|
||||
border-bottom-width: 1px;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
|
||||
import { useCopyData } from '@/utils/tools';
|
||||
import Icon from '@/components/Iconfont';
|
||||
import Icon from '@/components/Icon';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
@@ -34,14 +34,14 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
|
||||
<Flex
|
||||
py={2}
|
||||
px={5}
|
||||
backgroundColor={'#323641'}
|
||||
backgroundColor={useColorModeValue('#323641', 'gray.600')}
|
||||
color={'#fff'}
|
||||
fontSize={'sm'}
|
||||
userSelect={'none'}
|
||||
>
|
||||
<Box flex={1}>{match?.[1]}</Box>
|
||||
<Flex cursor={'pointer'} onClick={() => copyData(code)} alignItems={'center'}>
|
||||
<Icon name={'icon-fuzhi'} width={15} height={15} color={'#fff'}></Icon>
|
||||
<Icon name={'copy'} width={15} height={15} fill={'#fff'}></Icon>
|
||||
<Box ml={1}>复制代码</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
76
src/components/ScrollData/index.tsx
Normal file
76
src/components/ScrollData/index.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { useRef, useEffect, useMemo } from 'react';
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { throttle } from 'lodash';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
|
||||
interface Props extends BoxProps {
|
||||
nextPage: () => void;
|
||||
isLoadAll: boolean;
|
||||
requesting: boolean;
|
||||
children: React.ReactNode;
|
||||
initRequesting?: boolean;
|
||||
}
|
||||
|
||||
const ScrollData = ({
|
||||
children,
|
||||
nextPage,
|
||||
isLoadAll,
|
||||
requesting,
|
||||
initRequesting,
|
||||
...props
|
||||
}: Props) => {
|
||||
const { Loading } = useLoading({ defaultLoading: true });
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const loadText = useMemo(() => {
|
||||
if (requesting) return '请求中……';
|
||||
if (isLoadAll) return '已加载全部';
|
||||
return '点击加载更多';
|
||||
}, [isLoadAll, requesting]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!elementRef.current) return;
|
||||
|
||||
const scrolling = throttle((e: Event) => {
|
||||
const element = e.target as HTMLDivElement;
|
||||
if (!element) return;
|
||||
// 当前滚动位置
|
||||
const scrollTop = element.scrollTop;
|
||||
// 可视高度
|
||||
const clientHeight = element.clientHeight;
|
||||
// 内容总高度
|
||||
const scrollHeight = element.scrollHeight;
|
||||
// 判断是否滚动到底部
|
||||
if (scrollTop + clientHeight + 100 >= scrollHeight) {
|
||||
nextPage();
|
||||
}
|
||||
}, 100);
|
||||
elementRef.current.addEventListener('scroll', scrolling);
|
||||
return () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
elementRef.current?.removeEventListener('scroll', scrolling);
|
||||
};
|
||||
}, [elementRef, nextPage]);
|
||||
|
||||
return (
|
||||
<Box {...props} ref={elementRef} overflowY={'auto'} position={'relative'}>
|
||||
{children}
|
||||
<Box
|
||||
mt={2}
|
||||
fontSize={'xs'}
|
||||
color={'blackAlpha.500'}
|
||||
textAlign={'center'}
|
||||
cursor={loadText === '点击加载更多' ? 'pointer' : 'default'}
|
||||
onClick={() => {
|
||||
if (loadText !== '点击加载更多') return;
|
||||
nextPage();
|
||||
}}
|
||||
>
|
||||
{loadText}
|
||||
</Box>
|
||||
{initRequesting && <Loading fixed={false} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScrollData;
|
||||
49
src/components/WxConcat/index.tsx
Normal file
49
src/components/WxConcat/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
useColorModeValue
|
||||
} from '@chakra-ui/react';
|
||||
import Image from 'next/image';
|
||||
|
||||
const WxConcat = ({ onClose }: { onClose: () => void }) => {
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent color={useColorModeValue('blackAlpha.700', 'white')}>
|
||||
<ModalHeader>wx交流群</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody textAlign={'center'}>
|
||||
<Image
|
||||
style={{ margin: 'auto' }}
|
||||
src={'/imgs/wxcode.jpg'}
|
||||
width={200}
|
||||
height={200}
|
||||
alt=""
|
||||
/>
|
||||
<Box mt={2}>
|
||||
微信号:{' '}
|
||||
<Box as={'span'} userSelect={'all'}>
|
||||
YNyiqi
|
||||
</Box>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'outline'} onClick={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WxConcat;
|
||||
@@ -3,57 +3,58 @@ export enum EmailTypeEnum {
|
||||
findPassword = 'findPassword'
|
||||
}
|
||||
|
||||
export const PRICE_SCALE = 100000;
|
||||
|
||||
export const introPage = `
|
||||
## 欢迎使用 Fast GPT
|
||||
|
||||
[Git 仓库](https://github.com/c121914yu/FastGPT)
|
||||
|
||||
时间比较赶,介绍没来得及完善,先直接上怎么使用:
|
||||
### 快速开始
|
||||
1. 使用邮箱注册账号。
|
||||
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
|
||||
3. 进入模型页,创建一个模型,建议直接用 ChatGPT。
|
||||
4. 在模型列表点击【对话】,即可使用 API 进行聊天。
|
||||
3. 如果填写了自己的 openai 账号,使用时会直接用你的账号。如果没有填写,需要付费使用平台的账号。
|
||||
4. 进入模型页,创建一个模型,建议直接用 ChatGPT。
|
||||
5. 在模型列表点击【对话】,即可使用 API 进行聊天。
|
||||
|
||||
### 模型配置
|
||||
### 定制 prompt
|
||||
|
||||
1. **提示语**:会在每个对话框的第一句自动加入,用于限定该模型的对话内容。
|
||||
1. 进入模型编辑页
|
||||
2. 调整温度和提示词
|
||||
3. 使用该模型对话。每次对话时,提示词和温度都会自动注入,方便管理个人的模型。建议把自己日常经常需要使用的 5~10 个方向预设好。
|
||||
|
||||
### 知识库
|
||||
|
||||
2. **单句最大长度**:每个聊天,单次输入内容的最大长度。
|
||||
1. 创建模型时选择【知识库】
|
||||
2. 进入模型编辑页
|
||||
3. 导入数据,可以选择手动导入,或者选择文件导入。文件导入会自动调用 chatGPT 理解文件内容,并生成知识库。
|
||||
4. 使用该模型对话。
|
||||
|
||||
|
||||
3. **上下文最大长度**:每个聊天,最多的轮数除以2,建议设置为偶数。可以持续聊天,但是旧的聊天内容会被截断,AI 就不会知道被截取的内容。
|
||||
例如:上下文最大长度为6。在第 4 轮对话时,第一轮对话的内容不会被计入。
|
||||
|
||||
4. **过期时间**:生成对话框后,这个对话框多久过期。
|
||||
|
||||
5. **聊天最大加载次数**:单个对话框最多被加载几次,设置为-1代表不限制,正数代表只能加载 n 次,防止被盗刷。
|
||||
|
||||
### 对话框介绍
|
||||
|
||||
1. 每个对话框以 chatId 作为标识。
|
||||
2. 每次点击【对话】,都会生成新的对话框,无法回到旧的对话框。对话框内刷新,会恢复对话内容。
|
||||
3. 直接分享对话框(网页)的链接给朋友,会共享同一个对话内容。但是!!!千万不要两个人同时用一个链接,会串味,还没解决这个问题。
|
||||
4. 如果想分享一个纯的对话框,请点击侧边栏的分享按键。例如:
|
||||
注意:使用知识库模型对话时,tokens 消耗会加快。
|
||||
|
||||
### 其他问题
|
||||
还有其他问题,可以加我 wx: YNyiqi,拉个交流群大家一起聊聊。
|
||||
|
||||
`;
|
||||
|
||||
export const chatProblem = `
|
||||
**代理出错**
|
||||
**内容长度**
|
||||
单次最长 4000 tokens, 上下文最长 8000 tokens, 上下文超长时会被截断。
|
||||
|
||||
**模型问题**
|
||||
一般情况下,请直接选择 chatGPT 模型,价格低效果好。
|
||||
|
||||
**代理出错**
|
||||
服务器代理不稳定,可以过一会儿再尝试。
|
||||
|
||||
**API key 问题**
|
||||
**API key 问题**
|
||||
请把 openai 的 API key 粘贴到账号里再创建对话。如果是使用分享的对话,不需要填写 API key。
|
||||
`;
|
||||
|
||||
export const versionIntro = `
|
||||
* 分享对话:使用的是分享者的 Api Key 生成一个对话窗口进行分享。
|
||||
* 分享空白对话:为该模型创建一个空白的聊天分享出去。
|
||||
* 分享当前对话:会把当前聊天的内容也分享出去,但是要注意不要多个人同时用一个聊天内容。
|
||||
* 增加模型介绍:可以在模型编辑页添加对模型的介绍,方便提示模型的范围。
|
||||
* 温度调整:可以在模型编辑页调整模型温度,以便适应不同类型的对话。例如,翻译类的模型可以把温度拉低;创作类的模型可以把温度拉高。
|
||||
## Fast GPT V2.2
|
||||
* 定制知识库:创建模型时可以选择【知识库】模型, 可以手动导入知识点或者直接导入一个文件自动学习。
|
||||
* 删除和复制功能:点击对话头像,可以选择复制或删除该条内容。
|
||||
`;
|
||||
|
||||
export const shareHint = `
|
||||
|
||||
6
src/constants/data.ts
Normal file
6
src/constants/data.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { DataType } from '@/types/data';
|
||||
|
||||
export const DataTypeTextMap: Record<DataType, string> = {
|
||||
QA: '问答拆分',
|
||||
abstract: '摘要总结'
|
||||
};
|
||||
@@ -1,37 +1,64 @@
|
||||
import type { ServiceName } from '@/types/mongoSchema';
|
||||
import { ModelSchema } from '../types/mongoSchema';
|
||||
import type { ServiceName, ModelDataType, ModelSchema } from '@/types/mongoSchema';
|
||||
|
||||
export enum ChatModelNameEnum {
|
||||
GPT35 = 'gpt-3.5-turbo',
|
||||
VECTOR_GPT = 'VECTOR_GPT',
|
||||
GPT3 = 'text-davinci-003'
|
||||
}
|
||||
|
||||
export const ChatModelNameMap = {
|
||||
[ChatModelNameEnum.GPT35]: 'gpt-3.5-turbo',
|
||||
[ChatModelNameEnum.VECTOR_GPT]: 'gpt-3.5-turbo',
|
||||
[ChatModelNameEnum.GPT3]: 'text-davinci-003'
|
||||
};
|
||||
|
||||
export type ModelConstantsData = {
|
||||
serviceCompany: `${ServiceName}`;
|
||||
name: string;
|
||||
model: `${ChatModelNameEnum}`;
|
||||
trainName: string; // 空字符串代表不能训练
|
||||
maxToken: number;
|
||||
contextMaxToken: number;
|
||||
maxTemperature: number;
|
||||
trainedMaxToken: number; // 训练后最大多少tokens
|
||||
price: number; // 多少钱 / 1token,单位: 0.00001元
|
||||
};
|
||||
|
||||
export const ModelList: Record<ServiceName, ModelConstantsData[]> = {
|
||||
openai: [
|
||||
{
|
||||
name: 'chatGPT',
|
||||
model: ChatModelNameEnum.GPT35,
|
||||
trainName: 'turbo',
|
||||
maxToken: 4000,
|
||||
maxTemperature: 2
|
||||
},
|
||||
{
|
||||
name: 'GPT3',
|
||||
model: ChatModelNameEnum.GPT3,
|
||||
trainName: 'davinci',
|
||||
maxToken: 4000,
|
||||
maxTemperature: 2
|
||||
}
|
||||
]
|
||||
};
|
||||
export const modelList: ModelConstantsData[] = [
|
||||
{
|
||||
serviceCompany: 'openai',
|
||||
name: 'chatGPT',
|
||||
model: ChatModelNameEnum.GPT35,
|
||||
trainName: '',
|
||||
maxToken: 4000,
|
||||
contextMaxToken: 7500,
|
||||
trainedMaxToken: 2000,
|
||||
maxTemperature: 2,
|
||||
price: 3
|
||||
},
|
||||
{
|
||||
serviceCompany: 'openai',
|
||||
name: '知识库',
|
||||
model: ChatModelNameEnum.VECTOR_GPT,
|
||||
trainName: 'vector',
|
||||
maxToken: 4000,
|
||||
contextMaxToken: 7500,
|
||||
trainedMaxToken: 2000,
|
||||
maxTemperature: 1,
|
||||
price: 3
|
||||
}
|
||||
// {
|
||||
// serviceCompany: 'openai',
|
||||
// name: 'GPT3',
|
||||
// model: ChatModelNameEnum.GPT3,
|
||||
// trainName: 'davinci',
|
||||
// maxToken: 4000,
|
||||
// contextMaxToken: 7500,
|
||||
// trainedMaxToken: 2000,
|
||||
// maxTemperature: 2,
|
||||
// price: 30
|
||||
// }
|
||||
];
|
||||
|
||||
export enum TrainingStatusEnum {
|
||||
pending = 'pending',
|
||||
@@ -66,6 +93,11 @@ export const formatModelStatus = {
|
||||
}
|
||||
};
|
||||
|
||||
export const ModelDataStatusMap = {
|
||||
0: '训练完成',
|
||||
1: '训练中'
|
||||
};
|
||||
|
||||
export const defaultModel: ModelSchema = {
|
||||
_id: '',
|
||||
userId: '',
|
||||
|
||||
1
src/constants/redis.ts
Normal file
1
src/constants/redis.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const VecModelDataIndex = 'model:data';
|
||||
@@ -90,6 +90,13 @@ export const theme = extendTheme({
|
||||
fonts: {
|
||||
body: '-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"'
|
||||
},
|
||||
breakpoints: {
|
||||
sm: '900px',
|
||||
md: '1200px',
|
||||
lg: '1500px',
|
||||
xl: '1800',
|
||||
'2xl': '2100'
|
||||
},
|
||||
components: {
|
||||
Modal: ModalTheme,
|
||||
Button
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
export enum BillTypeEnum {
|
||||
chat = 'chat',
|
||||
splitData = 'splitData',
|
||||
QA = 'QA',
|
||||
abstract = 'abstract',
|
||||
return = 'return'
|
||||
}
|
||||
export enum PageTypeEnum {
|
||||
login = 'login',
|
||||
register = 'register',
|
||||
forgetPassword = 'forgetPassword'
|
||||
}
|
||||
|
||||
export const BillTypeMap: Record<`${BillTypeEnum}`, string> = {
|
||||
[BillTypeEnum.chat]: '对话',
|
||||
[BillTypeEnum.splitData]: 'QA拆分',
|
||||
[BillTypeEnum.QA]: 'QA拆分',
|
||||
[BillTypeEnum.abstract]: '摘要总结',
|
||||
[BillTypeEnum.return]: '退款'
|
||||
};
|
||||
|
||||
@@ -19,9 +19,10 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont
|
||||
return {
|
||||
openConfirm: useCallback(
|
||||
(confirm?: any, cancel?: any) => {
|
||||
onOpen();
|
||||
confirmCb.current = confirm;
|
||||
cancelCb.current = cancel;
|
||||
|
||||
return onOpen;
|
||||
},
|
||||
[onOpen]
|
||||
),
|
||||
|
||||
85
src/hooks/usePagination.tsx
Normal file
85
src/hooks/usePagination.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import type { PagingData } from '../types/index';
|
||||
import { IconButton, Flex, Box } from '@chakra-ui/react';
|
||||
import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons';
|
||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||
import { useToast } from './useToast';
|
||||
|
||||
export const usePagination = <T = any,>({
|
||||
api,
|
||||
pageSize = 10,
|
||||
params = {}
|
||||
}: {
|
||||
api: (data: any) => any;
|
||||
pageSize?: number;
|
||||
params?: Record<string, any>;
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const [pageNum, setPageNum] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const maxPage = useMemo(() => Math.ceil(total / pageSize), [pageSize, total]);
|
||||
|
||||
const {
|
||||
mutate,
|
||||
data = [],
|
||||
isLoading
|
||||
} = useMutation({
|
||||
mutationFn: async (num: number = pageNum) => {
|
||||
try {
|
||||
const res: PagingData<T> = await api({
|
||||
pageNum: num,
|
||||
pageSize,
|
||||
...params
|
||||
});
|
||||
setPageNum(num);
|
||||
setTotal(res.total);
|
||||
return res.data;
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: error?.message || '获取数据异常',
|
||||
status: 'error'
|
||||
});
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useQuery(['init'], () => {
|
||||
mutate(1);
|
||||
return null;
|
||||
});
|
||||
|
||||
const Pagination = useCallback(() => {
|
||||
return (
|
||||
<Flex alignItems={'center'} justifyContent={'end'}>
|
||||
<IconButton
|
||||
isDisabled={pageNum === 1}
|
||||
icon={<ArrowBackIcon />}
|
||||
aria-label={'left'}
|
||||
size={'sm'}
|
||||
onClick={() => mutate(pageNum - 1)}
|
||||
/>
|
||||
<Box mx={2}>
|
||||
{pageNum}/{maxPage}
|
||||
</Box>
|
||||
<IconButton
|
||||
isDisabled={pageNum === maxPage}
|
||||
icon={<ArrowForwardIcon />}
|
||||
aria-label={'left'}
|
||||
size={'sm'}
|
||||
onClick={() => mutate(pageNum + 1)}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}, [maxPage, mutate, pageNum]);
|
||||
|
||||
return {
|
||||
pageNum,
|
||||
pageSize,
|
||||
total,
|
||||
data,
|
||||
isLoading,
|
||||
Pagination,
|
||||
getData: mutate
|
||||
};
|
||||
};
|
||||
81
src/hooks/usePaging.ts
Normal file
81
src/hooks/usePaging.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import type { PagingData } from '../types/index';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useToast } from './useToast';
|
||||
|
||||
export const usePaging = <T = any>({
|
||||
api,
|
||||
pageSize = 10,
|
||||
params = {}
|
||||
}: {
|
||||
api: (data: any) => any;
|
||||
pageSize?: number;
|
||||
params?: Record<string, any>;
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const [data, setData] = useState<T[]>([]);
|
||||
const [pageNum, setPageNum] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [isLoadAll, setIsLoadAll] = useState(false);
|
||||
const [requesting, setRequesting] = useState(false);
|
||||
const [initRequesting, setInitRequesting] = useState(false);
|
||||
|
||||
const getData = useCallback(
|
||||
async (num: number, init = false) => {
|
||||
if (requesting) return;
|
||||
if (!init && isLoadAll) return;
|
||||
if (init) {
|
||||
setInitRequesting(true);
|
||||
}
|
||||
setRequesting(true);
|
||||
|
||||
try {
|
||||
const res: PagingData<T> = await api({
|
||||
pageNum: num,
|
||||
pageSize,
|
||||
...params
|
||||
});
|
||||
setData((state) => {
|
||||
const data = init ? res.data : state.concat(res.data);
|
||||
if (data.length >= res.total) {
|
||||
setIsLoadAll(true);
|
||||
}
|
||||
setTotal(res.total);
|
||||
setPageNum(num);
|
||||
return data;
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: error?.message || '获取数据异常',
|
||||
status: 'error'
|
||||
});
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
setRequesting(false);
|
||||
setInitRequesting(false);
|
||||
return null;
|
||||
},
|
||||
[api, isLoadAll, pageSize, params, requesting, toast]
|
||||
);
|
||||
|
||||
const nextPage = useCallback(() => {
|
||||
if (requesting || isLoadAll) return;
|
||||
getData(pageNum + 1);
|
||||
}, [getData, isLoadAll, pageNum, requesting]);
|
||||
|
||||
useQuery(['init'], () => getData(1, true));
|
||||
|
||||
return {
|
||||
pageNum,
|
||||
pageSize,
|
||||
total,
|
||||
data,
|
||||
getData,
|
||||
requesting,
|
||||
isLoadAll,
|
||||
nextPage,
|
||||
initRequesting,
|
||||
setData
|
||||
};
|
||||
};
|
||||
33
src/hooks/useRequest.tsx
Normal file
33
src/hooks/useRequest.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { UseMutationOptions } from '@tanstack/react-query';
|
||||
|
||||
interface Props extends UseMutationOptions<any, any, any, any> {
|
||||
successToast?: string;
|
||||
errorToast?: string;
|
||||
}
|
||||
|
||||
export const useRequest = ({ successToast, errorToast, onSuccess, onError, ...props }: Props) => {
|
||||
const { toast } = useToast();
|
||||
const mutation = useMutation<unknown, unknown, any, unknown>({
|
||||
...props,
|
||||
onSuccess(res, variables: void, context: unknown) {
|
||||
onSuccess?.(res, variables, context);
|
||||
successToast &&
|
||||
toast({
|
||||
title: successToast,
|
||||
status: 'success'
|
||||
});
|
||||
},
|
||||
onError(err: any, variables: void, context: unknown) {
|
||||
onError?.(err, variables, context);
|
||||
errorToast &&
|
||||
toast({
|
||||
title: typeof err === 'string' ? err : err?.message || errorToast,
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return mutation;
|
||||
};
|
||||
34
src/hooks/useSelectFile.tsx
Normal file
34
src/hooks/useSelectFile.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { useRef, useCallback } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
export const useSelectFile = (props?: { fileType?: string; multiple?: boolean }) => {
|
||||
const { fileType = '*', multiple = false } = props || {};
|
||||
const SelectFileDom = useRef<HTMLInputElement>(null);
|
||||
|
||||
const File = useCallback(
|
||||
({ onSelect }: { onSelect: (e: File[]) => void }) => (
|
||||
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
|
||||
<input
|
||||
ref={SelectFileDom}
|
||||
type="file"
|
||||
accept={fileType}
|
||||
multiple={multiple}
|
||||
onChange={(e) => {
|
||||
if (!e.target.files || e.target.files?.length === 0) return;
|
||||
onSelect(Array.from(e.target.files));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
[fileType, multiple]
|
||||
);
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
SelectFileDom.current && SelectFileDom.current.click();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
File,
|
||||
onOpen
|
||||
};
|
||||
};
|
||||
18
src/hooks/useTabs.tsx
Normal file
18
src/hooks/useTabs.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
|
||||
export const useTabs = ({
|
||||
tabs = []
|
||||
}: {
|
||||
tabs: {
|
||||
id: string;
|
||||
label: string;
|
||||
}[];
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState(tabs[0].id);
|
||||
|
||||
return {
|
||||
tabs,
|
||||
activeTab,
|
||||
setActiveTab
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AppProps, NextWebVitalsMetric } from 'next/app';
|
||||
import Script from 'next/script';
|
||||
import Head from 'next/head';
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
import { ChakraProvider, ColorModeScript } from '@chakra-ui/react';
|
||||
import Layout from '@/components/Layout';
|
||||
import { theme } from '@/constants/theme';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
@@ -38,9 +38,12 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Script src="/iconfont.js" strategy="afterInteractive"></Script>
|
||||
<Script src="/js/iconfont.js" strategy="afterInteractive"></Script>
|
||||
<Script src="/js/qrcode.min.js" strategy="afterInteractive"></Script>
|
||||
<Script src="/js/pdf.js" strategy="afterInteractive"></Script>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChakraProvider theme={theme}>
|
||||
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
|
||||
import { connectToDatabase, Chat } from '@/service/mongo';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { getOpenAIApi, authChat } from '@/service/utils/chat';
|
||||
import { httpsAgent } from '@/service/utils/tools';
|
||||
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
@@ -8,35 +8,63 @@ import { ChatItemType } from '@/types/chat';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { PassThrough } from 'stream';
|
||||
import { ModelList } from '@/constants/model';
|
||||
import { modelList } from '@/constants/model';
|
||||
import { pushChatBill } from '@/service/events/pushBill';
|
||||
import { openaiChatFilter } from '@/service/utils/tools';
|
||||
|
||||
/* 发送提示词 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { chatId, prompt } = req.body as {
|
||||
prompt: ChatItemType;
|
||||
chatId: string;
|
||||
};
|
||||
let step = 0; // step=1时,表示开始了流响应
|
||||
const stream = new PassThrough();
|
||||
stream.on('error', () => {
|
||||
console.log('error: ', 'stream error');
|
||||
stream.destroy();
|
||||
});
|
||||
res.on('close', () => {
|
||||
stream.destroy();
|
||||
});
|
||||
res.on('error', () => {
|
||||
console.log('error: ', 'request error');
|
||||
stream.destroy();
|
||||
});
|
||||
|
||||
try {
|
||||
const { chatId, prompt } = req.body as {
|
||||
prompt: ChatItemType;
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
const { authorization } = req.headers;
|
||||
if (!chatId || !prompt) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
let startTime = Date.now();
|
||||
|
||||
const { chat, userApiKey } = await authChat(chatId);
|
||||
const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization);
|
||||
|
||||
const model: ModelSchema = chat.modelId;
|
||||
const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
|
||||
if (!modelConstantsData) {
|
||||
throw new Error('模型加载异常');
|
||||
}
|
||||
|
||||
// 读取对话内容
|
||||
const prompts = [...chat.content, prompt];
|
||||
|
||||
// 上下文长度过滤
|
||||
const maxContext = model.security.contextMaxLen;
|
||||
const filterPrompts =
|
||||
prompts.length > maxContext ? prompts.slice(prompts.length - maxContext) : prompts;
|
||||
// 如果有系统提示词,自动插入
|
||||
if (model.systemPrompt) {
|
||||
prompts.unshift({
|
||||
obj: 'SYSTEM',
|
||||
value: model.systemPrompt
|
||||
});
|
||||
}
|
||||
|
||||
// 格式化文本内容
|
||||
// 控制在 tokens 数量,防止超出
|
||||
const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
|
||||
|
||||
// 格式化文本内容成 chatgpt 格式
|
||||
const map = {
|
||||
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
@@ -48,35 +76,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
content: item.value
|
||||
})
|
||||
);
|
||||
|
||||
// 如果有系统提示词,自动插入
|
||||
if (model.systemPrompt) {
|
||||
formatPrompts.unshift({
|
||||
role: 'system',
|
||||
content: model.systemPrompt
|
||||
});
|
||||
}
|
||||
|
||||
// console.log(formatPrompts);
|
||||
// 计算温度
|
||||
const modelConstantsData = ModelList['openai'].find(
|
||||
(item) => item.model === model.service.modelName
|
||||
);
|
||||
if (!modelConstantsData) {
|
||||
throw new Error('模型异常');
|
||||
}
|
||||
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
|
||||
|
||||
// 获取 chatAPI
|
||||
const chatAPI = getOpenAIApi(userApiKey);
|
||||
let startTime = Date.now();
|
||||
const chatAPI = getOpenAIApi(userApiKey || systemKey);
|
||||
// 发出请求
|
||||
const chatResponse = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: model.service.chatModel,
|
||||
temperature: temperature,
|
||||
max_tokens: modelConstantsData.maxToken,
|
||||
// max_tokens: modelConstantsData.maxToken,
|
||||
messages: formatPrompts,
|
||||
stream: true
|
||||
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
stream: true,
|
||||
stop: ['.!?。']
|
||||
},
|
||||
{
|
||||
timeout: 40000,
|
||||
@@ -84,21 +100,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
httpsAgent
|
||||
}
|
||||
);
|
||||
console.log(
|
||||
'response success',
|
||||
`time: ${(Date.now() - startTime) / 1000}s`,
|
||||
`promptLen: ${formatPrompts.length}`,
|
||||
`contentLen: ${formatPrompts.reduce((sum, item) => sum + item.content.length, 0)}`
|
||||
);
|
||||
|
||||
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
||||
|
||||
// 创建响应流
|
||||
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
step = 1;
|
||||
|
||||
const pass = new PassThrough();
|
||||
pass.pipe(res);
|
||||
let responseContent = '';
|
||||
stream.pipe(res);
|
||||
|
||||
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
|
||||
if (event.type !== 'event') return;
|
||||
@@ -107,9 +120,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const content: string = json?.choices?.[0].delta.content || '';
|
||||
if (!content) return;
|
||||
if (!content || (responseContent === '' && content === '\n')) return;
|
||||
|
||||
responseContent += content;
|
||||
// console.log('content:', content)
|
||||
pass.push(content.replace(/\n/g, '<br/>'));
|
||||
!stream.destroyed && stream.push(content.replace(/\n/g, '<br/>'));
|
||||
} catch (error) {
|
||||
error;
|
||||
}
|
||||
@@ -118,18 +133,40 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const decoder = new TextDecoder();
|
||||
try {
|
||||
for await (const chunk of chatResponse.data as any) {
|
||||
if (stream.destroyed) {
|
||||
// 流被中断了,直接忽略后面的内容
|
||||
break;
|
||||
}
|
||||
const parser = createParser(onParse);
|
||||
parser.feed(decoder.decode(chunk));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('pipe error', error);
|
||||
}
|
||||
pass.push(null);
|
||||
} catch (err: any) {
|
||||
res.status(500);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
// close stream
|
||||
!stream.destroyed && stream.push(null);
|
||||
stream.destroy();
|
||||
|
||||
const promptsContent = formatPrompts.map((item) => item.content).join('');
|
||||
// 只有使用平台的 key 才计费
|
||||
pushChatBill({
|
||||
isPay: !userApiKey,
|
||||
modelName: model.service.modelName,
|
||||
userId,
|
||||
chatId,
|
||||
text: promptsContent + responseContent
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (step === 1) {
|
||||
// 直接结束流
|
||||
console.log('error,结束');
|
||||
stream.destroy();
|
||||
} else {
|
||||
res.status(500);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
62
src/pages/api/chat/delChatRecordByIndex.ts
Normal file
62
src/pages/api/chat/delChatRecordByIndex.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Chat } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { chatId, index } = req.query as { chatId: string; index: string };
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
if (!chatId || !index) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 凭证校验
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
const chatRecord = await Chat.findById(chatId);
|
||||
|
||||
if (!chatRecord) {
|
||||
throw new Error('找不到对话');
|
||||
}
|
||||
|
||||
// 重新计算 index,跳过已经被删除的内容
|
||||
let unDeleteIndex = +index;
|
||||
let deletedIndex = 0;
|
||||
for (deletedIndex = 0; deletedIndex < chatRecord.content.length; deletedIndex++) {
|
||||
if (!chatRecord.content[deletedIndex].deleted) {
|
||||
unDeleteIndex--;
|
||||
if (unDeleteIndex < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除最一条数据库记录, 也就是预发送的那一条
|
||||
await Chat.updateOne(
|
||||
{
|
||||
_id: chatId,
|
||||
userId
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
[`content.${deletedIndex}.deleted`]: true,
|
||||
updateTime: Date.now()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Chat } from '@/service/mongo';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { chatId } = req.query as { chatId: string };
|
||||
|
||||
if (!chatId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 删除最一条数据库记录, 也就是预发送的那一条
|
||||
await Chat.findByIdAndUpdate(chatId, {
|
||||
$pop: { content: 1 },
|
||||
updateTime: Date.now()
|
||||
});
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,14 @@ import type { ModelSchema } from '@/types/mongoSchema';
|
||||
/* 获取我的模型 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { modelId } = req.query;
|
||||
const { modelId, isShare = 'false' } = req.query as {
|
||||
modelId: string;
|
||||
isShare?: 'true' | 'false';
|
||||
};
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('无权操作');
|
||||
throw new Error('无权生成对话');
|
||||
}
|
||||
|
||||
if (!modelId) {
|
||||
@@ -39,7 +42,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
modelId,
|
||||
expiredTime: Date.now() + model.security.expiredTime,
|
||||
loadAmount: model.security.maxLoadAmount,
|
||||
updateTime: Date.now(),
|
||||
isShare: isShare === 'true',
|
||||
content: []
|
||||
});
|
||||
|
||||
|
||||
@@ -1,68 +1,173 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { getOpenAIApi, authChat } from '@/service/utils/chat';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { httpsAgent } from '@/service/utils/tools';
|
||||
import { ModelList } from '@/constants/model';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { PassThrough } from 'stream';
|
||||
import { modelList } from '@/constants/model';
|
||||
import { pushChatBill } from '@/service/events/pushBill';
|
||||
|
||||
/* 发送提示词 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { prompt, chatId } = req.body as { prompt: ChatItemType[]; chatId: string };
|
||||
let step = 0; // step=1时,表示开始了流响应
|
||||
const stream = new PassThrough();
|
||||
stream.on('error', () => {
|
||||
console.log('error: ', 'stream error');
|
||||
stream.destroy();
|
||||
});
|
||||
res.on('close', () => {
|
||||
stream.destroy();
|
||||
});
|
||||
res.on('error', () => {
|
||||
console.log('error: ', 'request error');
|
||||
stream.destroy();
|
||||
});
|
||||
|
||||
if (!prompt || !chatId) {
|
||||
try {
|
||||
const { chatId, prompt } = req.body as {
|
||||
prompt: ChatItemType;
|
||||
chatId: string;
|
||||
};
|
||||
const { authorization } = req.headers;
|
||||
if (!chatId || !prompt) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const { chat, userApiKey } = await authChat(chatId);
|
||||
const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization);
|
||||
|
||||
const model = chat.modelId;
|
||||
const model: ModelSchema = chat.modelId;
|
||||
|
||||
// 获取 chatAPI
|
||||
const chatAPI = getOpenAIApi(userApiKey);
|
||||
// 读取对话内容
|
||||
const prompts = [...chat.content, prompt];
|
||||
|
||||
// prompt处理
|
||||
const formatPrompt = prompt.map((item) => `${item.value}\n\n###\n\n`).join('');
|
||||
// 上下文长度过滤
|
||||
const maxContext = model.security.contextMaxLen;
|
||||
const filterPrompts =
|
||||
prompts.length > maxContext ? prompts.slice(prompts.length - maxContext) : prompts;
|
||||
|
||||
// 格式化文本内容
|
||||
const formatPrompts: string[] = filterPrompts.map((item: ChatItemType) => item.value);
|
||||
// 如果有系统提示词,自动插入
|
||||
if (model.systemPrompt) {
|
||||
formatPrompts.unshift(`${model.systemPrompt}`);
|
||||
}
|
||||
|
||||
const promptText = formatPrompts.join('</s>');
|
||||
|
||||
// 计算温度
|
||||
const modelConstantsData = ModelList['openai'].find(
|
||||
(item) => item.model === model.service.modelName
|
||||
);
|
||||
const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
|
||||
if (!modelConstantsData) {
|
||||
throw new Error('模型异常');
|
||||
throw new Error('模型异常,请用 chatgpt 模型');
|
||||
}
|
||||
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
|
||||
|
||||
// 发送请求
|
||||
const response = await chatAPI.createCompletion(
|
||||
// 获取 chatAPI
|
||||
const chatAPI = getOpenAIApi(userApiKey || systemKey);
|
||||
let startTime = Date.now();
|
||||
// console.log({
|
||||
// model: model.service.chatModel,
|
||||
// temperature: temperature,
|
||||
// prompt: promptText,
|
||||
// stream: true,
|
||||
// max_tokens:
|
||||
// model.trainingTimes > 0 ? modelConstantsData.trainedMaxToken : modelConstantsData.maxToken,
|
||||
// presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
// frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
// stop: [`###`]
|
||||
// });
|
||||
// 发出请求
|
||||
const chatResponse = await chatAPI.createCompletion(
|
||||
{
|
||||
model: model.service.modelName,
|
||||
prompt: formatPrompt,
|
||||
model: model.service.chatModel,
|
||||
temperature: temperature,
|
||||
max_tokens: modelConstantsData.maxToken,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0.6,
|
||||
stop: ['###']
|
||||
prompt: promptText,
|
||||
stream: true,
|
||||
max_tokens:
|
||||
model.trainingTimes > 0
|
||||
? modelConstantsData.trainedMaxToken
|
||||
: modelConstantsData.maxToken,
|
||||
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
stop: [`###`, '。!?.!.']
|
||||
},
|
||||
{
|
||||
timeout: 40000,
|
||||
responseType: 'stream',
|
||||
httpsAgent
|
||||
}
|
||||
);
|
||||
|
||||
const responseMessage = response.data.choices[0]?.text;
|
||||
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
||||
|
||||
jsonRes(res, {
|
||||
data: responseMessage
|
||||
// 创建响应流
|
||||
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
step = 1;
|
||||
|
||||
let responseContent = '';
|
||||
stream.pipe(res);
|
||||
|
||||
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
|
||||
if (event.type !== 'event') return;
|
||||
const data = event.data;
|
||||
if (data === '[DONE]') return;
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const content: string = json?.choices?.[0].text || '';
|
||||
// console.log('content:', content);
|
||||
if (!content || (responseContent === '' && content === '\n')) return;
|
||||
|
||||
responseContent += content;
|
||||
!stream.destroyed && stream.push(content.replace(/\n/g, '<br/>'));
|
||||
} catch (error) {
|
||||
error;
|
||||
}
|
||||
};
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
try {
|
||||
for await (const chunk of chatResponse.data as any) {
|
||||
if (stream.destroyed) {
|
||||
// 流被中断了,直接忽略后面的内容
|
||||
break;
|
||||
}
|
||||
const parser = createParser(onParse);
|
||||
parser.feed(decoder.decode(chunk));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('pipe error', error);
|
||||
}
|
||||
// close stream
|
||||
!stream.destroyed && stream.push(null);
|
||||
stream.destroy();
|
||||
|
||||
// 只有使用平台的 key 才计费
|
||||
pushChatBill({
|
||||
isPay: !userApiKey,
|
||||
modelName: model.service.modelName,
|
||||
userId,
|
||||
chatId,
|
||||
text: promptText + responseContent
|
||||
});
|
||||
} catch (err: any) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
// console.log(err?.response);
|
||||
if (step === 1) {
|
||||
// 直接结束流
|
||||
console.log('error,结束');
|
||||
stream.destroy();
|
||||
} else {
|
||||
res.status(500);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,14 @@ import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Chat } from '@/service/mongo';
|
||||
import type { ChatPopulate } from '@/types/mongoSchema';
|
||||
import type { InitChatResponse } from '@/api/response/chat';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
|
||||
/* 获取我的模型 */
|
||||
/* 初始化我的聊天框,需要身份验证 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
const { chatId } = req.query as { chatId: string };
|
||||
|
||||
if (!chatId) {
|
||||
@@ -16,7 +20,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
await connectToDatabase();
|
||||
|
||||
// 获取 chat 数据
|
||||
const chat = await Chat.findById<ChatPopulate>(chatId).populate({
|
||||
const chat = await Chat.findOne<ChatPopulate>({
|
||||
_id: chatId,
|
||||
userId
|
||||
}).populate({
|
||||
path: 'modelId',
|
||||
options: {
|
||||
strictPopulate: false
|
||||
@@ -27,28 +34,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
throw new Error('聊天框不存在');
|
||||
}
|
||||
|
||||
if (chat.loadAmount > 0) {
|
||||
await Chat.updateOne(
|
||||
{
|
||||
_id: chat._id
|
||||
},
|
||||
{
|
||||
$inc: { loadAmount: -1 }
|
||||
}
|
||||
);
|
||||
}
|
||||
// filter 掉被 deleted 的内容
|
||||
chat.content = chat.content.filter((item) => item.deleted !== true);
|
||||
|
||||
const model = chat.modelId;
|
||||
jsonRes<InitChatResponse>(res, {
|
||||
code: 201,
|
||||
data: {
|
||||
chatId: chat._id,
|
||||
isExpiredTime: chat.loadAmount === 0 || chat.expiredTime <= Date.now(),
|
||||
modelId: model._id,
|
||||
name: model.name,
|
||||
avatar: model.avatar,
|
||||
intro: model.intro,
|
||||
secret: model.security,
|
||||
modelName: model.service.modelName,
|
||||
chatModel: model.service.chatModel,
|
||||
history: chat.content
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}))
|
||||
}
|
||||
},
|
||||
updateTime: Date.now()
|
||||
updateTime: new Date()
|
||||
});
|
||||
|
||||
jsonRes(res);
|
||||
|
||||
244
src/pages/api/chat/vectorGpt.ts
Normal file
244
src/pages/api/chat/vectorGpt.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
|
||||
import { connectToDatabase, ModelData } from '@/service/mongo';
|
||||
import { getOpenAIApi, authChat } from '@/service/utils/chat';
|
||||
import { httpsAgent, openaiChatFilter, systemPromptFilter } from '@/service/utils/tools';
|
||||
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { PassThrough } from 'stream';
|
||||
import { modelList } from '@/constants/model';
|
||||
import { pushChatBill } from '@/service/events/pushBill';
|
||||
import { connectRedis } from '@/service/redis';
|
||||
import { VecModelDataIndex } from '@/constants/redis';
|
||||
import { vectorToBuffer } from '@/utils/tools';
|
||||
|
||||
/* 发送提示词 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
let step = 0; // step=1时,表示开始了流响应
|
||||
const stream = new PassThrough();
|
||||
stream.on('error', () => {
|
||||
console.log('error: ', 'stream error');
|
||||
stream.destroy();
|
||||
});
|
||||
res.on('close', () => {
|
||||
stream.destroy();
|
||||
});
|
||||
res.on('error', () => {
|
||||
console.log('error: ', 'request error');
|
||||
stream.destroy();
|
||||
});
|
||||
|
||||
try {
|
||||
const { chatId, prompt } = req.body as {
|
||||
prompt: ChatItemType;
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
const { authorization } = req.headers;
|
||||
if (!chatId || !prompt) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
const redis = await connectRedis();
|
||||
let startTime = Date.now();
|
||||
|
||||
const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization);
|
||||
|
||||
const model: ModelSchema = chat.modelId;
|
||||
const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
|
||||
if (!modelConstantsData) {
|
||||
throw new Error('模型加载异常');
|
||||
}
|
||||
|
||||
// 读取对话内容
|
||||
const prompts = [...chat.content, prompt];
|
||||
|
||||
// 获取 chatAPI
|
||||
const chatAPI = getOpenAIApi(userApiKey || systemKey);
|
||||
|
||||
// 把输入的内容转成向量
|
||||
const promptVector = await chatAPI
|
||||
.createEmbedding(
|
||||
{
|
||||
model: 'text-embedding-ada-002',
|
||||
input: prompt.value
|
||||
},
|
||||
{
|
||||
timeout: 120000,
|
||||
httpsAgent
|
||||
}
|
||||
)
|
||||
.then((res) => res?.data?.data?.[0]?.embedding || []);
|
||||
|
||||
// 搜索系统提示词, 按相似度从 redis 中搜出前3条不同 dataId 的数据
|
||||
const redisData: any[] = await redis.sendCommand([
|
||||
'FT.SEARCH',
|
||||
`idx:${VecModelDataIndex}:hash`,
|
||||
`@modelId:{${String(
|
||||
chat.modelId._id
|
||||
)}} @vector:[VECTOR_RANGE 0.15 $blob]=>{$YIELD_DISTANCE_AS: score}`,
|
||||
// `@modelId:{${String(chat.modelId._id)}}=>[KNN 10 @vector $blob AS score]`,
|
||||
'RETURN',
|
||||
'1',
|
||||
'dataId',
|
||||
'SORTBY',
|
||||
'score',
|
||||
'PARAMS',
|
||||
'2',
|
||||
'blob',
|
||||
vectorToBuffer(promptVector),
|
||||
'LIMIT',
|
||||
'0',
|
||||
'20',
|
||||
'DIALECT',
|
||||
'2'
|
||||
]);
|
||||
|
||||
// 格式化响应值,获取去重后的id
|
||||
let formatIds = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
|
||||
.map((i) => {
|
||||
if (!redisData[i] || !redisData[i][1]) return '';
|
||||
return redisData[i][1];
|
||||
})
|
||||
.filter((item) => item);
|
||||
formatIds = Array.from(new Set(formatIds));
|
||||
|
||||
if (formatIds.length === 0) {
|
||||
throw new Error('对不起,我没有找到你的问题');
|
||||
}
|
||||
|
||||
// 从 mongo 中取出原文作为提示词
|
||||
const textArr = (
|
||||
await Promise.all(
|
||||
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20].map((i) => {
|
||||
if (!redisData[i] || !redisData[i][1]) return '';
|
||||
return ModelData.findById(redisData[i][1])
|
||||
.select('text q')
|
||||
.then((res) => {
|
||||
if (!res) return '';
|
||||
const questions = res.q.map((item) => item.text).join(' ');
|
||||
const answer = res.text;
|
||||
return `${questions} ${answer}`;
|
||||
});
|
||||
})
|
||||
)
|
||||
).filter((item) => item);
|
||||
|
||||
// textArr 筛选,最多 3000 tokens
|
||||
const systemPrompt = systemPromptFilter(textArr, 2800);
|
||||
|
||||
prompts.unshift({
|
||||
obj: 'SYSTEM',
|
||||
value: `根据下面的知识回答问题: ${systemPrompt}`
|
||||
});
|
||||
|
||||
// 控制在 tokens 数量,防止超出
|
||||
const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
|
||||
|
||||
// 格式化文本内容成 chatgpt 格式
|
||||
const map = {
|
||||
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
||||
};
|
||||
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
|
||||
(item: ChatItemType) => ({
|
||||
role: map[item.obj],
|
||||
content: item.value
|
||||
})
|
||||
);
|
||||
// console.log(formatPrompts);
|
||||
// 计算温度
|
||||
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
|
||||
|
||||
// 发出请求
|
||||
const chatResponse = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: model.service.chatModel,
|
||||
temperature: temperature,
|
||||
// max_tokens: modelConstantsData.maxToken,
|
||||
messages: formatPrompts,
|
||||
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
stream: true
|
||||
},
|
||||
{
|
||||
timeout: 40000,
|
||||
responseType: 'stream',
|
||||
httpsAgent
|
||||
}
|
||||
);
|
||||
|
||||
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
||||
|
||||
// 创建响应流
|
||||
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
step = 1;
|
||||
|
||||
let responseContent = '';
|
||||
stream.pipe(res);
|
||||
|
||||
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
|
||||
if (event.type !== 'event') return;
|
||||
const data = event.data;
|
||||
if (data === '[DONE]') return;
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const content: string = json?.choices?.[0].delta.content || '';
|
||||
if (!content || (responseContent === '' && content === '\n')) return;
|
||||
|
||||
responseContent += content;
|
||||
// console.log('content:', content)
|
||||
!stream.destroyed && stream.push(content.replace(/\n/g, '<br/>'));
|
||||
} catch (error) {
|
||||
error;
|
||||
}
|
||||
};
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
try {
|
||||
for await (const chunk of chatResponse.data as any) {
|
||||
if (stream.destroyed) {
|
||||
// 流被中断了,直接忽略后面的内容
|
||||
break;
|
||||
}
|
||||
const parser = createParser(onParse);
|
||||
parser.feed(decoder.decode(chunk));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('pipe error', error);
|
||||
}
|
||||
// close stream
|
||||
!stream.destroyed && stream.push(null);
|
||||
stream.destroy();
|
||||
|
||||
const promptsContent = formatPrompts.map((item) => item.content).join('');
|
||||
// 只有使用平台的 key 才计费
|
||||
pushChatBill({
|
||||
isPay: !userApiKey,
|
||||
modelName: model.service.modelName,
|
||||
userId,
|
||||
chatId,
|
||||
text: promptsContent + responseContent
|
||||
});
|
||||
// jsonRes(res);
|
||||
} catch (err: any) {
|
||||
if (step === 1) {
|
||||
// 直接结束流
|
||||
console.log('error,结束');
|
||||
stream.destroy();
|
||||
} else {
|
||||
res.status(500);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/pages/api/data/delData.ts
Normal file
47
src/pages/api/data/delData.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Data, DataItem } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import type { DataListItem } from '@/types/data';
|
||||
import type { PagingData } from '@/types';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('缺少登录凭证');
|
||||
}
|
||||
|
||||
await authToken(authorization);
|
||||
|
||||
const { dataId } = req.query as { dataId: string };
|
||||
if (!dataId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
await Data.findByIdAndUpdate(dataId, {
|
||||
isDeleted: true
|
||||
});
|
||||
|
||||
// 改变 dataItem 状态为 0
|
||||
await DataItem.updateMany(
|
||||
{
|
||||
dataId
|
||||
},
|
||||
{
|
||||
status: 0
|
||||
}
|
||||
);
|
||||
|
||||
jsonRes<PagingData<DataListItem>>(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
48
src/pages/api/data/getDataItems.ts
Normal file
48
src/pages/api/data/getDataItems.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, DataItem } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
let {
|
||||
dataId,
|
||||
pageNum = 1,
|
||||
pageSize = 10
|
||||
} = req.query as { dataId: string; pageNum: string; pageSize: string };
|
||||
pageNum = +pageNum;
|
||||
pageSize = +pageSize;
|
||||
|
||||
if (!dataId) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
await connectToDatabase();
|
||||
|
||||
const { authorization } = req.headers;
|
||||
|
||||
await authToken(authorization);
|
||||
|
||||
const dataItems = await DataItem.find({
|
||||
dataId
|
||||
})
|
||||
.sort({ _id: -1 }) // 按照创建时间倒序排列
|
||||
.skip((pageNum - 1) * pageSize)
|
||||
.limit(pageSize);
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
pageNum,
|
||||
pageSize,
|
||||
data: dataItems,
|
||||
total: await DataItem.countDocuments({
|
||||
dataId
|
||||
})
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
71
src/pages/api/data/getDataList.ts
Normal file
71
src/pages/api/data/getDataList.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Data, DataItem } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import type { DataListItem } from '@/types/data';
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('缺少登录凭证');
|
||||
}
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const datalist = await Data.aggregate<DataListItem>([
|
||||
{
|
||||
$match: {
|
||||
userId: new mongoose.Types.ObjectId(userId),
|
||||
isDeleted: false
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: { createTime: -1 } // 按照创建时间倒序排列
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: 'dataitems',
|
||||
localField: '_id',
|
||||
foreignField: 'dataId',
|
||||
as: 'items'
|
||||
}
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
totalData: {
|
||||
$size: '$items' // 统计dataItem的总数
|
||||
},
|
||||
trainingData: {
|
||||
$size: {
|
||||
$filter: {
|
||||
input: '$items',
|
||||
as: 'item',
|
||||
cond: { $ne: ['$$item.status', 0] } // 统计 status 不为0的数量
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
items: 0 // 不返回 items 字段
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
jsonRes(res, {
|
||||
data: datalist
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
35
src/pages/api/data/postData.ts
Normal file
35
src/pages/api/data/postData.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Data } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import type { DataType } from '@/types/data';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
let { name, type } = req.body as { name: string; type: DataType };
|
||||
if (!name || !type) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
await connectToDatabase();
|
||||
|
||||
const { authorization } = req.headers;
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
// 生成 data 集合
|
||||
const data = await Data.create({
|
||||
userId,
|
||||
name,
|
||||
type
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: data._id
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
37
src/pages/api/data/putDataName.ts
Normal file
37
src/pages/api/data/putDataName.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Data } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import type { DataListItem } from '@/types/data';
|
||||
import type { PagingData } from '@/types';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('缺少登录凭证');
|
||||
}
|
||||
|
||||
await authToken(authorization);
|
||||
|
||||
const { dataId, name } = req.query as { dataId: string; name: string };
|
||||
if (!dataId || !name) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
await Data.findByIdAndUpdate(dataId, {
|
||||
name
|
||||
});
|
||||
|
||||
jsonRes<PagingData<DataListItem>>(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
69
src/pages/api/data/splitData.ts
Normal file
69
src/pages/api/data/splitData.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, DataItem, Data } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { generateQA } from '@/service/events/generateQA';
|
||||
import { generateAbstract } from '@/service/events/generateAbstract';
|
||||
import { encode } from 'gpt-token-utils';
|
||||
|
||||
/* 拆分数据成QA */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { text, dataId } = req.body as { text: string; dataId: string };
|
||||
if (!text || !dataId) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
await connectToDatabase();
|
||||
|
||||
const { authorization } = req.headers;
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
const DataRecord = await Data.findById(dataId);
|
||||
|
||||
if (!DataRecord) {
|
||||
throw new Error('找不到数据集');
|
||||
}
|
||||
const replaceText = text.replace(/[\\n]+/g, ' ');
|
||||
|
||||
// 文本拆分成 chunk
|
||||
let chunks = replaceText.match(/[^!?.。]+[!?.。]/g) || [];
|
||||
|
||||
const dataItems: any[] = [];
|
||||
let splitText = '';
|
||||
|
||||
chunks.forEach((chunk) => {
|
||||
splitText += chunk;
|
||||
const tokens = encode(splitText).length;
|
||||
if (tokens >= 780) {
|
||||
dataItems.push({
|
||||
userId,
|
||||
dataId,
|
||||
type: DataRecord.type,
|
||||
text: splitText,
|
||||
status: 1
|
||||
});
|
||||
splitText = '';
|
||||
}
|
||||
});
|
||||
|
||||
// 批量插入数据
|
||||
await DataItem.insertMany(dataItems);
|
||||
|
||||
try {
|
||||
generateQA();
|
||||
generateAbstract();
|
||||
} catch (error) {
|
||||
error;
|
||||
}
|
||||
|
||||
jsonRes(res, {
|
||||
data: { chunks, replaceText }
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,14 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { ModelStatusEnum, ModelList, ChatModelNameEnum } from '@/constants/model';
|
||||
import type { ServiceName } from '@/types/mongoSchema';
|
||||
import { ModelStatusEnum, modelList, ChatModelNameEnum, ChatModelNameMap } from '@/constants/model';
|
||||
import { Model } from '@/service/models/model';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
serviceModelName,
|
||||
serviceModelCompany = 'openai'
|
||||
} = req.body as {
|
||||
const { name, serviceModelName } = req.body as {
|
||||
name: string;
|
||||
serviceModelName: `${ChatModelNameEnum}`;
|
||||
serviceModelCompany: ServiceName;
|
||||
};
|
||||
const { authorization } = req.headers;
|
||||
|
||||
@@ -24,16 +18,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
|
||||
if (!name || !serviceModelName || !serviceModelCompany) {
|
||||
if (!name || !serviceModelName) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
const modelItem = ModelList[serviceModelCompany].find(
|
||||
(item) => item.model === serviceModelName
|
||||
);
|
||||
const modelItem = modelList.find((item) => item.model === serviceModelName);
|
||||
|
||||
if (!modelItem) {
|
||||
throw new Error('模型不存在');
|
||||
@@ -41,15 +33,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 重名校验
|
||||
const authRepeatName = await Model.findOne({
|
||||
name,
|
||||
userId
|
||||
});
|
||||
if (authRepeatName) {
|
||||
throw new Error('模型名重复');
|
||||
}
|
||||
|
||||
// 上限校验
|
||||
const authCount = await Model.countDocuments({
|
||||
userId
|
||||
@@ -64,10 +47,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
userId,
|
||||
status: ModelStatusEnum.running,
|
||||
service: {
|
||||
company: serviceModelCompany,
|
||||
trainId: modelItem.trainName,
|
||||
chatModel: modelItem.model,
|
||||
modelName: modelItem.model
|
||||
company: modelItem.serviceCompany,
|
||||
trainId: '',
|
||||
chatModel: ChatModelNameMap[modelItem.model], // 聊天时用的模型
|
||||
modelName: modelItem.model // 最底层的模型,不会变,用于计费等核心操作
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
53
src/pages/api/model/data/delModelDataById.ts
Normal file
53
src/pages/api/model/data/delModelDataById.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, ModelData } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { connectRedis } from '@/service/redis';
|
||||
import { VecModelDataIndex } from '@/constants/redis';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
let { dataId } = req.query as {
|
||||
dataId: string;
|
||||
};
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
|
||||
if (!dataId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
const redis = await connectRedis();
|
||||
|
||||
const data = await ModelData.findById(dataId);
|
||||
|
||||
await ModelData.deleteOne({
|
||||
_id: dataId,
|
||||
userId
|
||||
});
|
||||
|
||||
// 删除 redis 数据
|
||||
data?.q.forEach(async (item) => {
|
||||
try {
|
||||
await redis.json.del(`${VecModelDataIndex}:${item.id}`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
61
src/pages/api/model/data/getModelData.ts
Normal file
61
src/pages/api/model/data/getModelData.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, ModelData } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
let {
|
||||
modelId,
|
||||
pageNum = 1,
|
||||
pageSize = 10
|
||||
} = req.query as {
|
||||
modelId: string;
|
||||
pageNum: string;
|
||||
pageSize: string;
|
||||
};
|
||||
|
||||
const { authorization } = req.headers;
|
||||
|
||||
pageNum = +pageNum;
|
||||
pageSize = +pageSize;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
|
||||
if (!modelId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const data = await ModelData.find({
|
||||
modelId,
|
||||
userId
|
||||
})
|
||||
.sort({ _id: -1 }) // 按照创建时间倒序排列
|
||||
.skip((pageNum - 1) * pageSize)
|
||||
.limit(pageSize);
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
pageNum,
|
||||
pageSize,
|
||||
data,
|
||||
total: await ModelData.countDocuments({
|
||||
modelId,
|
||||
userId
|
||||
})
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
35
src/pages/api/model/data/getSplitData.ts
Normal file
35
src/pages/api/model/data/getSplitData.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, SplitData, Model } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
|
||||
/* 拆分数据成QA */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { modelId } = req.query as { modelId: string };
|
||||
if (!modelId) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
await connectToDatabase();
|
||||
|
||||
const { authorization } = req.headers;
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
// 找到长度大于0的数据
|
||||
const data = await SplitData.find({
|
||||
userId,
|
||||
modelId,
|
||||
textList: { $exists: true, $not: { $size: 0 } }
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
59
src/pages/api/model/data/pushModelDataInput.ts
Normal file
59
src/pages/api/model/data/pushModelDataInput.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, ModelData, Model } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { ModelDataSchema } from '@/types/mongoSchema';
|
||||
import { generateVector } from '@/service/events/generateVector';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { modelId, data } = req.body as {
|
||||
modelId: string;
|
||||
data: { text: ModelDataSchema['text']; q: ModelDataSchema['q'] }[];
|
||||
};
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
|
||||
if (!modelId || !Array.isArray(data)) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 验证是否是该用户的 model
|
||||
const model = await Model.findOne({
|
||||
_id: modelId,
|
||||
userId
|
||||
});
|
||||
|
||||
if (!model) {
|
||||
throw new Error('无权操作该模型');
|
||||
}
|
||||
|
||||
// push data
|
||||
await ModelData.insertMany(
|
||||
data.map((item) => ({
|
||||
...item,
|
||||
modelId,
|
||||
userId
|
||||
}))
|
||||
);
|
||||
|
||||
generateVector(true);
|
||||
|
||||
jsonRes(res, {
|
||||
data: model
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
57
src/pages/api/model/data/pushModelDataSelectData.ts
Normal file
57
src/pages/api/model/data/pushModelDataSelectData.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, DataItem, ModelData } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
let { dataIds, modelId } = req.body as { dataIds: string[]; modelId: string };
|
||||
|
||||
if (!dataIds) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
await connectToDatabase();
|
||||
|
||||
const { authorization } = req.headers;
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
const dataItems = (
|
||||
await Promise.all(
|
||||
dataIds.map((dataId) =>
|
||||
DataItem.find<{ _id: string; result: { q: string }[]; text: string }>(
|
||||
{
|
||||
userId,
|
||||
dataId
|
||||
},
|
||||
'result text'
|
||||
)
|
||||
)
|
||||
)
|
||||
).flat();
|
||||
|
||||
// push data
|
||||
await ModelData.insertMany(
|
||||
dataItems.map((item) => ({
|
||||
modelId: modelId,
|
||||
userId,
|
||||
text: item.text,
|
||||
q: item.result.map((item) => ({
|
||||
id: nanoid(),
|
||||
text: item.q
|
||||
}))
|
||||
}))
|
||||
);
|
||||
|
||||
jsonRes(res, {
|
||||
data: dataItems
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
44
src/pages/api/model/data/putModelData.ts
Normal file
44
src/pages/api/model/data/putModelData.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, ModelData } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
let { dataId, text } = req.body as {
|
||||
dataId: string;
|
||||
text: string;
|
||||
};
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
|
||||
if (!dataId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
await ModelData.updateOne(
|
||||
{
|
||||
_id: dataId,
|
||||
userId
|
||||
},
|
||||
{
|
||||
text
|
||||
}
|
||||
);
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
69
src/pages/api/model/data/splitData.ts
Normal file
69
src/pages/api/model/data/splitData.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, SplitData, Model } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { generateQA } from '@/service/events/generateQA';
|
||||
import { encode } from 'gpt-token-utils';
|
||||
|
||||
/* 拆分数据成QA */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { text, modelId } = req.body as { text: string; modelId: string };
|
||||
if (!text || !modelId) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
await connectToDatabase();
|
||||
|
||||
const { authorization } = req.headers;
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
// 验证是否是该用户的 model
|
||||
const model = await Model.findOne({
|
||||
_id: modelId,
|
||||
userId
|
||||
});
|
||||
|
||||
if (!model) {
|
||||
throw new Error('无权操作该模型');
|
||||
}
|
||||
|
||||
const replaceText = text.replace(/(\\n|\n)+/g, ' ');
|
||||
|
||||
// 文本拆分成 chunk
|
||||
let chunks = replaceText.match(/[^!?.。]+[!?.。]/g) || [];
|
||||
|
||||
const textList: string[] = [];
|
||||
let splitText = '';
|
||||
|
||||
chunks.forEach((chunk) => {
|
||||
splitText += chunk;
|
||||
const tokens = encode(splitText).length;
|
||||
if (tokens >= 980) {
|
||||
textList.push(splitText);
|
||||
splitText = '';
|
||||
}
|
||||
});
|
||||
|
||||
if (splitText) {
|
||||
textList.push(splitText);
|
||||
}
|
||||
|
||||
// 批量插入数据
|
||||
await SplitData.create({
|
||||
userId,
|
||||
modelId,
|
||||
rawText: text,
|
||||
textList
|
||||
});
|
||||
|
||||
generateQA();
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { Chat, Model, Training, connectToDatabase } from '@/service/mongo';
|
||||
import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
|
||||
import { Chat, Model, Training, connectToDatabase, ModelData } from '@/service/mongo';
|
||||
import { authToken, getUserApiOpenai } from '@/service/utils/tools';
|
||||
import { TrainingStatusEnum } from '@/constants/model';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import { TrainingItemType } from '@/types/training';
|
||||
import { httpsAgent } from '@/service/utils/tools';
|
||||
import { connectRedis } from '@/service/redis';
|
||||
import { VecModelDataIndex } from '@/constants/redis';
|
||||
|
||||
/* 获取我的模型 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
@@ -25,18 +27,38 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
const redis = await connectRedis();
|
||||
|
||||
// 删除模型
|
||||
await Model.deleteOne({
|
||||
_id: modelId,
|
||||
userId
|
||||
});
|
||||
|
||||
// 删除对应的聊天
|
||||
await Chat.deleteMany({
|
||||
const modelDataList = await ModelData.find({
|
||||
modelId
|
||||
});
|
||||
|
||||
// 删除 redis
|
||||
modelDataList?.forEach((modelData) =>
|
||||
modelData.q.forEach(async (item) => {
|
||||
try {
|
||||
await redis.json.del(`${VecModelDataIndex}:${item.id}`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let requestQueue: any[] = [];
|
||||
// 删除对应的聊天
|
||||
requestQueue.push(
|
||||
Chat.deleteMany({
|
||||
modelId
|
||||
})
|
||||
);
|
||||
|
||||
// 删除数据集
|
||||
requestQueue.push(
|
||||
ModelData.deleteMany({
|
||||
modelId
|
||||
})
|
||||
);
|
||||
|
||||
// 查看是否正在训练
|
||||
const training: TrainingItemType | null = await Training.findOne({
|
||||
modelId,
|
||||
@@ -45,7 +67,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
|
||||
// 如果正在训练,需要删除openai上的相关信息
|
||||
if (training) {
|
||||
const openai = getOpenAIApi(await getUserOpenaiKey(userId));
|
||||
const { openai } = await getUserApiOpenai(userId);
|
||||
// 获取训练记录
|
||||
const tuneRecord = await openai.retrieveFineTune(training.tuneId, { httpsAgent });
|
||||
|
||||
@@ -56,9 +78,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
}
|
||||
|
||||
// 删除对应训练记录
|
||||
await Training.deleteMany({
|
||||
modelId
|
||||
});
|
||||
requestQueue.push(
|
||||
Training.deleteMany({
|
||||
modelId
|
||||
})
|
||||
);
|
||||
|
||||
// 删除模型
|
||||
requestQueue.push(
|
||||
Model.deleteOne({
|
||||
_id: modelId,
|
||||
userId
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(requestQueue);
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
|
||||
@@ -21,6 +21,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
// 根据 userId 获取模型信息
|
||||
const models = await Model.find({
|
||||
userId
|
||||
}).sort({
|
||||
_id: -1
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Model, Training } from '@/service/mongo';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import formidable from 'formidable';
|
||||
import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
|
||||
import { join } from 'path';
|
||||
import fs from 'fs';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import type { OpenAIApi } from 'openai';
|
||||
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
|
||||
import { httpsAgent } from '@/service/utils/tools';
|
||||
import { connectToDatabase, Training } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
|
||||
// 关闭next默认的bodyParser处理方式
|
||||
export const config = {
|
||||
@@ -18,7 +10,7 @@ export const config = {
|
||||
}
|
||||
};
|
||||
|
||||
/* 上传文件,开始微调 */
|
||||
/* 获取模型训练记录 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
@@ -30,7 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
if (!modelId) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
const userId = await authToken(authorization);
|
||||
await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Model, Training } from '@/service/mongo';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
|
||||
import { authToken, getUserApiOpenai } from '@/service/utils/tools';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { TrainingItemType } from '@/types/training';
|
||||
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
|
||||
@@ -43,16 +43,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
// 用户的 openai 实例
|
||||
const openai = getOpenAIApi(await getUserOpenaiKey(userId));
|
||||
const { openai } = await getUserApiOpenai(userId);
|
||||
|
||||
// 获取 openai 的训练情况
|
||||
const { data } = await openai.retrieveFineTune(training.tuneId, { httpsAgent });
|
||||
|
||||
// console.log(data);
|
||||
if (data.status === OpenAiTuneStatusEnum.succeeded) {
|
||||
// 删除训练文件
|
||||
openai.deleteFile(data.training_files[0].id, { httpsAgent });
|
||||
|
||||
// 更新模型
|
||||
// 更新模型状态和模型内容
|
||||
await Model.findByIdAndUpdate(modelId, {
|
||||
status: ModelStatusEnum.running,
|
||||
updateTime: new Date(),
|
||||
@@ -72,6 +72,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
}
|
||||
|
||||
/* 取消微调 */
|
||||
if (data.status === OpenAiTuneStatusEnum.cancelled) {
|
||||
// 删除训练文件
|
||||
openai.deleteFile(data.training_files[0].id, { httpsAgent });
|
||||
@@ -87,11 +88,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
|
||||
return jsonRes(res, {
|
||||
data: '模型微调取消'
|
||||
data: '模型微调已取消'
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error('模型还在训练中');
|
||||
jsonRes(res, {
|
||||
data: '模型还在训练中'
|
||||
});
|
||||
} catch (err: any) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
@@ -2,9 +2,8 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Model, Training } from '@/service/mongo';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import formidable from 'formidable';
|
||||
import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
|
||||
import { authToken, getUserApiOpenai } from '@/service/utils/tools';
|
||||
import { join } from 'path';
|
||||
import fs from 'fs';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
@@ -30,6 +29,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
const { modelId } = req.query;
|
||||
|
||||
if (!modelId) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
@@ -48,7 +48,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const trainingType = model.service.trainId; // 目前都默认是 openai text-davinci-03
|
||||
|
||||
// 获取用户的 API Key 实例化后的对象
|
||||
openai = getOpenAIApi(await getUserOpenaiKey(userId));
|
||||
const user = await getUserApiOpenai(userId);
|
||||
openai = user.openai;
|
||||
|
||||
// 接收文件并保存
|
||||
const form = formidable({
|
||||
@@ -67,7 +68,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
const file = files.file;
|
||||
|
||||
// 上传文件
|
||||
// 上传文件到 openai
|
||||
// @ts-ignore
|
||||
const uploadRes = await openai.createFile(
|
||||
// @ts-ignore
|
||||
@@ -82,7 +83,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
{
|
||||
training_file: uploadFileId,
|
||||
model: trainingType,
|
||||
suffix: model.name
|
||||
suffix: model.name,
|
||||
n_epochs: 4
|
||||
},
|
||||
{ httpsAgent }
|
||||
);
|
||||
@@ -37,7 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
systemPrompt,
|
||||
intro,
|
||||
temperature,
|
||||
service,
|
||||
// service,
|
||||
security
|
||||
}
|
||||
);
|
||||
|
||||
@@ -5,6 +5,9 @@ import { AuthCode } from '@/service/models/authCode';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error('不是开发环境');
|
||||
}
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ import { connectToDatabase, Chat } from '@/service/mongo';
|
||||
|
||||
/* 定时删除那些不活跃的内容 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error('不是开发环境');
|
||||
}
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
|
||||
35
src/pages/api/timer/initBill.ts
Normal file
35
src/pages/api/timer/initBill.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Bill } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import type { BillSchema } from '@/types/mongoSchema';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error('不是开发环境');
|
||||
}
|
||||
await connectToDatabase();
|
||||
|
||||
const bills = await Bill.find({
|
||||
tokenLen: { $exists: false }
|
||||
});
|
||||
await Promise.all(
|
||||
bills.map((bill) =>
|
||||
Bill.findByIdAndUpdate(bill._id, {
|
||||
tokenLen: bill.textLen
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
jsonRes(res, {
|
||||
data: {}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
37
src/pages/api/timer/initDataItemTime.ts
Normal file
37
src/pages/api/timer/initDataItemTime.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, DataItem, Data } from '@/service/mongo';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error('不是开发环境');
|
||||
}
|
||||
await connectToDatabase();
|
||||
|
||||
// await DataItem.updateMany(
|
||||
// {},
|
||||
// {
|
||||
// type: 'QA'
|
||||
// // times: 2
|
||||
// }
|
||||
// );
|
||||
|
||||
await Data.updateMany(
|
||||
{},
|
||||
{
|
||||
type: 'QA'
|
||||
}
|
||||
);
|
||||
|
||||
jsonRes(res, {
|
||||
data: {}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
68
src/pages/api/timer/testVector.ts
Normal file
68
src/pages/api/timer/testVector.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Bill } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import type { BillSchema } from '@/types/mongoSchema';
|
||||
import { VecModelDataIndex } from '@/constants/redis';
|
||||
import { connectRedis } from '@/service/redis';
|
||||
import { vectorToBuffer } from '@/utils/tools';
|
||||
|
||||
let vectorData = [
|
||||
-0.025028639, -0.010407282, 0.026523087, -0.0107438695, -0.006967359, 0.010043768, -0.012043097,
|
||||
0.008724345, -0.028919589, -0.0117738275, 0.0050690062, 0.02961969
|
||||
].concat(new Array(1524).fill(0));
|
||||
let vectorData2 = [
|
||||
0.025028639, 0.010407282, 0.026523087, 0.0107438695, -0.006967359, 0.010043768, -0.012043097,
|
||||
0.008724345, 0.028919589, 0.0117738275, 0.0050690062, 0.02961969
|
||||
].concat(new Array(1524).fill(0));
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error('不是开发环境');
|
||||
}
|
||||
await connectToDatabase();
|
||||
|
||||
const redis = await connectRedis();
|
||||
|
||||
await redis.sendCommand([
|
||||
'HMSET',
|
||||
'model:data:333',
|
||||
'vector',
|
||||
vectorToBuffer(vectorData2),
|
||||
'modelId',
|
||||
'1133',
|
||||
'dataId',
|
||||
'safadfa'
|
||||
]);
|
||||
|
||||
// search
|
||||
const response = await redis.sendCommand([
|
||||
'FT.SEARCH',
|
||||
'idx:model:data:hash',
|
||||
'@modelId:{1133} @vector:[VECTOR_RANGE 0.15 $blob]=>{$YIELD_DISTANCE_AS: score}',
|
||||
'RETURN',
|
||||
'2',
|
||||
'modelId',
|
||||
'dataId',
|
||||
'PARAMS',
|
||||
'2',
|
||||
'blob',
|
||||
vectorToBuffer(vectorData2),
|
||||
'SORTBY',
|
||||
'score',
|
||||
'DIALECT',
|
||||
'2'
|
||||
]);
|
||||
|
||||
jsonRes(res, {
|
||||
data: response
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,16 @@ import { connectToDatabase, Training, Model } from '@/service/mongo';
|
||||
import type { TrainingItemType } from '@/types/training';
|
||||
import { TrainingStatusEnum, ModelStatusEnum } from '@/constants/model';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import { getUserOpenaiKey } from '@/service/utils/tools';
|
||||
import { getUserApiOpenai } from '@/service/utils/tools';
|
||||
import { OpenAiTuneStatusEnum } from '@/service/constants/training';
|
||||
import { sendTrainSucceed } from '@/service/utils/sendEmail';
|
||||
import { httpsAgent } from '@/service/utils/tools';
|
||||
import { ModelPopulate } from '@/types/mongoSchema';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error('不是开发环境');
|
||||
}
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
@@ -19,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
status: TrainingStatusEnum.pending
|
||||
});
|
||||
|
||||
const openai = getOpenAIApi(await getUserOpenaiKey('63f9a14228d2a688d8dc9e1b'));
|
||||
const { openai } = await getUserApiOpenai('63f9a14228d2a688d8dc9e1b');
|
||||
|
||||
const response = await Promise.all(
|
||||
trainingRecords.map(async (item) => {
|
||||
@@ -28,7 +32,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
// 删除训练文件
|
||||
openai.deleteFile(data.training_files[0].id, { httpsAgent });
|
||||
|
||||
const model = await Model.findById(item.modelId).populate({
|
||||
const model = await Model.findById<ModelPopulate>(item.modelId).populate({
|
||||
path: 'userId',
|
||||
options: {
|
||||
strictPopulate: false
|
||||
|
||||
82
src/pages/api/user/checkPayResult.ts
Normal file
82
src/pages/api/user/checkPayResult.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import axios from 'axios';
|
||||
import { connectToDatabase, User, Pay } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { PaySchema } from '@/types/mongoSchema';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
let { payId } = req.query as { payId: string };
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 查找订单记录校验
|
||||
const payOrder = await Pay.findById<PaySchema>(payId);
|
||||
|
||||
if (!payOrder) {
|
||||
throw new Error('订单不存在');
|
||||
}
|
||||
if (payOrder.status !== 'NOTPAY') {
|
||||
throw new Error('订单已结算');
|
||||
}
|
||||
|
||||
const { data } = await axios.get(
|
||||
`https://sif268.laf.dev/wechat-order-query?order_number=${payOrder.orderId}&api_key=${process.env.WXPAYCODE}`
|
||||
);
|
||||
|
||||
// 校验下是否超过一天
|
||||
const orderTime = dayjs(payOrder.createTime);
|
||||
const diffInHours = dayjs().diff(orderTime, 'hours');
|
||||
|
||||
if (data.trade_state === 'SUCCESS') {
|
||||
// 订单已支付
|
||||
try {
|
||||
// 更新订单状态
|
||||
const updateRes = await Pay.updateOne(
|
||||
{
|
||||
_id: payId,
|
||||
status: 'NOTPAY'
|
||||
},
|
||||
{
|
||||
status: 'SUCCESS'
|
||||
}
|
||||
);
|
||||
if (updateRes.modifiedCount === 1) {
|
||||
// 给用户账号充钱
|
||||
await User.findByIdAndUpdate(userId, {
|
||||
$inc: { balance: payOrder.price }
|
||||
});
|
||||
jsonRes(res, {
|
||||
data: '支付成功'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
await Pay.findByIdAndUpdate(payId, {
|
||||
status: 'NOTPAY'
|
||||
});
|
||||
console.log(error);
|
||||
}
|
||||
} else if (data.trade_state === 'CLOSED' || diffInHours > 24) {
|
||||
// 订单已关闭
|
||||
await Pay.findByIdAndUpdate(payId, {
|
||||
status: 'CLOSED'
|
||||
});
|
||||
jsonRes(res, {
|
||||
data: '订单已过期'
|
||||
});
|
||||
} else {
|
||||
throw new Error(data.trade_state_desc);
|
||||
}
|
||||
} catch (err) {
|
||||
// console.log(err);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
51
src/pages/api/user/getBill.ts
Normal file
51
src/pages/api/user/getBill.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Bill } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import type { BillSchema } from '@/types/mongoSchema';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
let { pageNum = 1, pageSize = 10 } = req.query as { pageNum: string; pageSize: string };
|
||||
|
||||
pageNum = +pageNum;
|
||||
pageSize = +pageSize;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('缺少登录凭证');
|
||||
}
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 根据 id 获取用户账单
|
||||
const bills = await Bill.find<BillSchema>({
|
||||
userId
|
||||
})
|
||||
.sort({ _id: -1 }) // 按照创建时间倒序排列
|
||||
.skip((pageNum - 1) * pageSize)
|
||||
.limit(pageSize);
|
||||
|
||||
// 获取total
|
||||
const total = await Bill.countDocuments({
|
||||
userId
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
pageNum,
|
||||
pageSize,
|
||||
data: bills,
|
||||
total
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
56
src/pages/api/user/getPayCode.ts
Normal file
56
src/pages/api/user/getPayCode.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import axios from 'axios';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { connectToDatabase, Pay } from '@/service/mongo';
|
||||
import { PRICE_SCALE } from '@/constants/common';
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 20);
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
let { amount = 0 } = req.query as { amount: string };
|
||||
amount = +amount;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('缺少登录凭证');
|
||||
}
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
const id = nanoid();
|
||||
await connectToDatabase();
|
||||
|
||||
const response = await axios({
|
||||
url: 'https://sif268.laf.dev/wechat-pay',
|
||||
method: 'POST',
|
||||
data: {
|
||||
trade_order_number: id,
|
||||
amount: amount * 100,
|
||||
api_key: process.env.WXPAYCODE
|
||||
}
|
||||
});
|
||||
|
||||
// 充值记录 + 1
|
||||
const payOrder = await Pay.create({
|
||||
userId,
|
||||
price: amount * PRICE_SCALE,
|
||||
orderId: id
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
payId: payOrder._id,
|
||||
codeUrl: response.data?.code_url
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
31
src/pages/api/user/getPayOrders.ts
Normal file
31
src/pages/api/user/getPayOrders.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { connectToDatabase, Pay } from '@/service/mongo';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('缺少登录凭证');
|
||||
}
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const records = await Pay.find({
|
||||
userId
|
||||
}).sort({ createTime: -1 });
|
||||
|
||||
jsonRes(res, {
|
||||
data: records
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
// 根据 id 获取用户信息
|
||||
const user = await User.findById(response._id);
|
||||
|
||||
if (!user) {
|
||||
throw new Error('获取用户信息异常');
|
||||
}
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
token: generateToken(user._id),
|
||||
|
||||
@@ -44,6 +44,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
email
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('获取用户信息异常');
|
||||
}
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
token: generateToken(user._id),
|
||||
|
||||
@@ -22,22 +22,17 @@ const Empty = ({ intro }: { intro: string }) => {
|
||||
{!!intro && (
|
||||
<Card p={4} mb={10}>
|
||||
<Header>模型介绍</Header>
|
||||
<Box>{intro}</Box>
|
||||
<Box whiteSpace={'pre-line'}>{intro}</Box>
|
||||
</Card>
|
||||
)}
|
||||
{/* version intro */}
|
||||
<Card p={4} mb={10}>
|
||||
<Markdown source={versionIntro} />
|
||||
</Card>
|
||||
<Card p={4}>
|
||||
<Header>常见问题</Header>
|
||||
<Markdown source={chatProblem} />
|
||||
</Card>
|
||||
{/* version intro */}
|
||||
<Card p={4}>
|
||||
<Header>Fast Gpt version1.4</Header>
|
||||
<Box>
|
||||
聊天的数据结构发生了比较大的改动。如果出现问题,请手动删除左侧旧的历史记录,并重新从模型页生成对话框进入。
|
||||
</Box>
|
||||
<br />
|
||||
<Markdown source={versionIntro} />
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AddIcon, ChatIcon, DeleteIcon } from '@chakra-ui/icons';
|
||||
import { AddIcon, ChatIcon, DeleteIcon, MoonIcon, SunIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -18,7 +18,9 @@ import {
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
useDisclosure
|
||||
useDisclosure,
|
||||
useColorMode,
|
||||
useColorModeValue
|
||||
} from '@chakra-ui/react';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
@@ -30,6 +32,7 @@ import { useCopyData } from '@/utils/tools';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { shareHint } from '@/constants/common';
|
||||
import { getChatSiteId } from '@/api/chat';
|
||||
import WxConcat from '@/components/WxConcat';
|
||||
|
||||
const SlideBar = ({
|
||||
name,
|
||||
@@ -45,11 +48,13 @@ const SlideBar = ({
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
const { copyData } = useCopyData();
|
||||
const { myModels, getMyModels } = useUserStore();
|
||||
const { chatHistory, removeChatHistoryByWindowId } = useChatStore();
|
||||
const [hasReady, setHasReady] = useState(false);
|
||||
const { isOpen: isOpenShare, onOpen: onOpenShare, onClose: onCloseShare } = useDisclosure();
|
||||
const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure();
|
||||
|
||||
const { isSuccess } = useQuery(['init'], getMyModels, {
|
||||
cacheTime: 5 * 60 * 1000
|
||||
@@ -82,7 +87,7 @@ const SlideBar = ({
|
||||
: {})}
|
||||
onClick={() => {
|
||||
if (item.chatId === chatId) return;
|
||||
router.push(`/chat?chatId=${item.chatId}`);
|
||||
router.replace(`/chat?chatId=${item.chatId}`);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
@@ -98,6 +103,9 @@ const SlideBar = ({
|
||||
size={'xs'}
|
||||
onClick={(e) => {
|
||||
removeChatHistoryByWindowId(item.chatId);
|
||||
if (item.chatId === chatId) {
|
||||
resetChat();
|
||||
}
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
@@ -107,7 +115,13 @@ const SlideBar = ({
|
||||
</>
|
||||
);
|
||||
|
||||
const RenderButton = ({ onClick, children }: { onClick: () => void; children: JSX.Element }) => (
|
||||
const RenderButton = ({
|
||||
onClick,
|
||||
children
|
||||
}: {
|
||||
onClick: () => void;
|
||||
children: JSX.Element | string;
|
||||
}) => (
|
||||
<Box px={3} mb={3}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
@@ -130,7 +144,7 @@ const SlideBar = ({
|
||||
w={'100%'}
|
||||
h={'100%'}
|
||||
py={3}
|
||||
backgroundColor={'blackAlpha.800'}
|
||||
backgroundColor={useColorModeValue('blackAlpha.800', 'blackAlpha.500')}
|
||||
color={'white'}
|
||||
>
|
||||
{/* 新对话 */}
|
||||
@@ -150,7 +164,7 @@ const SlideBar = ({
|
||||
|
||||
{/* 我的模型 & 历史记录 折叠框*/}
|
||||
<Box flex={'1 0 0'} px={3} h={0} overflowY={'auto'}>
|
||||
<Accordion defaultIndex={[0]} allowToggle>
|
||||
<Accordion defaultIndex={[0]} allowMultiple>
|
||||
{isSuccess && (
|
||||
<AccordionItem borderTop={0} borderBottom={0}>
|
||||
<AccordionButton borderRadius={'md'} pl={1}>
|
||||
@@ -181,7 +195,7 @@ const SlideBar = ({
|
||||
: {})}
|
||||
onClick={async () => {
|
||||
if (item.name === name) return;
|
||||
router.push(`/chat?chatId=${await getChatSiteId(item._id)}`);
|
||||
router.replace(`/chat?chatId=${await getChatSiteId(item._id)}`);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
@@ -208,7 +222,7 @@ const SlideBar = ({
|
||||
</Accordion>
|
||||
</Box>
|
||||
|
||||
<Divider my={4} />
|
||||
<Divider my={4} colorScheme={useColorModeValue('gray', 'white')} />
|
||||
|
||||
<RenderButton onClick={() => router.push('/')}>
|
||||
<>
|
||||
@@ -217,22 +231,40 @@ const SlideBar = ({
|
||||
</>
|
||||
</RenderButton>
|
||||
|
||||
<RenderButton
|
||||
onClick={() => {
|
||||
onOpenShare();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{/* <RenderButton onClick={onOpenShare}>
|
||||
<>
|
||||
<MyIcon name="share" fill={'white'} w={'16px'} h={'16px'} mr={4} />
|
||||
分享
|
||||
</>
|
||||
</RenderButton> */}
|
||||
<RenderButton onClick={() => router.push('/number/setting')}>
|
||||
<>
|
||||
<MyIcon name="pay" fill={'white'} w={'16px'} h={'16px'} mr={4} />
|
||||
充值
|
||||
</>
|
||||
</RenderButton>
|
||||
|
||||
<Flex alignItems={'center'} mr={4}>
|
||||
<Box flex={1}>
|
||||
<RenderButton onClick={onOpenWx}>交流群</RenderButton>
|
||||
</Box>
|
||||
<IconButton
|
||||
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
|
||||
aria-label={''}
|
||||
variant={'outline'}
|
||||
w={'16px'}
|
||||
colorScheme={'white'}
|
||||
_hover={{
|
||||
backgroundColor: 'rgba(255,255,255,0.2)'
|
||||
}}
|
||||
onClick={toggleColorMode}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{/* 分享提示modal */}
|
||||
<Modal isOpen={isOpenShare} onClose={onCloseShare}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalContent color={useColorModeValue('blackAlpha.700', 'white')}>
|
||||
<ModalHeader>分享对话</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
@@ -241,7 +273,7 @@ const SlideBar = ({
|
||||
|
||||
<ModalFooter>
|
||||
<Button colorScheme="gray" variant={'outline'} mr={3} onClick={onCloseShare}>
|
||||
Close
|
||||
取消
|
||||
</Button>
|
||||
{getToken() && (
|
||||
<Button
|
||||
@@ -249,10 +281,11 @@ const SlideBar = ({
|
||||
mr={3}
|
||||
onClick={async () => {
|
||||
copyData(
|
||||
`${location.origin}/chat?chatId=${await getChatSiteId(modelId)}`,
|
||||
`${location.origin}/chat?chatId=${await getChatSiteId(modelId, true)}`,
|
||||
'已复制分享链接'
|
||||
);
|
||||
onCloseShare();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
分享空白对话
|
||||
@@ -263,13 +296,16 @@ const SlideBar = ({
|
||||
onClick={() => {
|
||||
copyData(`${location.origin}/chat?chatId=${chatId}`, '已复制分享链接');
|
||||
onCloseShare();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
分享当前对话
|
||||
分享聊天记录
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{/* wx 联系 */}
|
||||
{isOpenWx && <WxConcat onClose={onCloseWx} />}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useCallback, useState, useRef, useMemo } from 'react';
|
||||
import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Image from 'next/image';
|
||||
import {
|
||||
getInitChatSiteInfo,
|
||||
getChatSiteId,
|
||||
postGPT3SendPrompt,
|
||||
delLastMessage,
|
||||
delChatRecordByIndex,
|
||||
postSaveChat
|
||||
} from '@/api/chat';
|
||||
import type { InitChatResponse } from '@/api/response/chat';
|
||||
@@ -14,24 +14,30 @@ import {
|
||||
Textarea,
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
useDisclosure,
|
||||
Drawer,
|
||||
DrawerOverlay,
|
||||
DrawerContent
|
||||
DrawerContent,
|
||||
useColorModeValue,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem
|
||||
} from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import Icon from '@/components/Iconfont';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ChatModelNameEnum } from '@/constants/model';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { useCopyData } from '@/utils/tools';
|
||||
import { streamFetch } from '@/api/fetch';
|
||||
import SlideBar from './components/SlideBar';
|
||||
import Empty from './components/Empty';
|
||||
import { getToken } from '@/utils/user';
|
||||
import Icon from '@/components/Icon';
|
||||
import { encode } from 'gpt-token-utils';
|
||||
import { modelList } from '@/constants/model';
|
||||
|
||||
const Markdown = dynamic(() => import('@/components/Markdown'));
|
||||
|
||||
@@ -44,47 +50,31 @@ interface ChatType extends InitChatResponse {
|
||||
const Chat = ({ chatId }: { chatId: string }) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { isPc, media } = useScreen();
|
||||
const { setLoading } = useGlobalStore();
|
||||
const ChatBox = useRef<HTMLDivElement>(null);
|
||||
const TextareaDom = useRef<HTMLTextAreaElement>(null);
|
||||
// 中断请求
|
||||
const controller = useRef(new AbortController());
|
||||
const [chatData, setChatData] = useState<ChatType>({
|
||||
chatId: '',
|
||||
modelId: '',
|
||||
name: '',
|
||||
avatar: '',
|
||||
intro: '',
|
||||
secret: {},
|
||||
chatModel: '',
|
||||
history: [],
|
||||
isExpiredTime: false
|
||||
modelName: '',
|
||||
history: []
|
||||
}); // 聊天框整体数据
|
||||
|
||||
const ChatBox = useRef<HTMLDivElement>(null);
|
||||
const TextareaDom = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const [inputVal, setInputVal] = useState(''); // 输入的内容
|
||||
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
|
||||
|
||||
const isChatting = useMemo(
|
||||
() => chatData.history[chatData.history.length - 1]?.status === 'loading',
|
||||
[chatData.history]
|
||||
);
|
||||
const chatWindowError = useMemo(() => {
|
||||
if (chatData.history[chatData.history.length - 1]?.obj === 'Human') {
|
||||
return {
|
||||
text: '内容出现异常',
|
||||
canDelete: true
|
||||
};
|
||||
}
|
||||
if (chatData.isExpiredTime) {
|
||||
return {
|
||||
text: '聊天框已过期',
|
||||
canDelete: false
|
||||
};
|
||||
}
|
||||
|
||||
return '';
|
||||
}, [chatData]);
|
||||
const { copyData } = useCopyData();
|
||||
const { isPc, media } = useScreen();
|
||||
const { setLoading } = useGlobalStore();
|
||||
|
||||
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
|
||||
const { pushChatHistory } = useChatStore();
|
||||
|
||||
// 滚动到底部
|
||||
@@ -98,6 +88,232 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
// 重置输入内容
|
||||
const resetInputVal = useCallback((val: string) => {
|
||||
setInputVal(val);
|
||||
setTimeout(() => {
|
||||
/* 回到最小高度 */
|
||||
if (TextareaDom.current) {
|
||||
TextareaDom.current.style.height =
|
||||
val === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
|
||||
}
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
// 重载对话
|
||||
const resetChat = useCallback(async () => {
|
||||
if (!chatData) return;
|
||||
try {
|
||||
router.replace(`/chat?chatId=${await getChatSiteId(chatData.modelId)}`);
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: error?.message || '生成新对话失败',
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
onCloseSlider();
|
||||
}, [chatData, onCloseSlider, router, toast]);
|
||||
|
||||
// gpt 对话
|
||||
const gptChatPrompt = useCallback(
|
||||
async (prompts: ChatSiteItemType) => {
|
||||
const urlMap: Record<string, string> = {
|
||||
[ChatModelNameEnum.GPT35]: '/api/chat/chatGpt',
|
||||
[ChatModelNameEnum.VECTOR_GPT]: '/api/chat/vectorGpt',
|
||||
[ChatModelNameEnum.GPT3]: '/api/chat/gpt3'
|
||||
};
|
||||
|
||||
if (!urlMap[chatData.modelName]) return Promise.reject('找不到模型');
|
||||
|
||||
const prompt = {
|
||||
obj: prompts.obj,
|
||||
value: prompts.value
|
||||
};
|
||||
// 流请求,获取数据
|
||||
const res = await streamFetch({
|
||||
url: urlMap[chatData.modelName],
|
||||
data: {
|
||||
prompt,
|
||||
chatId
|
||||
},
|
||||
onMessage: (text: string) => {
|
||||
setChatData((state) => ({
|
||||
...state,
|
||||
history: state.history.map((item, index) => {
|
||||
if (index !== state.history.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
value: item.value + text
|
||||
};
|
||||
})
|
||||
}));
|
||||
},
|
||||
abortSignal: controller.current
|
||||
});
|
||||
|
||||
// 保存对话信息
|
||||
try {
|
||||
await postSaveChat({
|
||||
chatId,
|
||||
prompts: [
|
||||
prompt,
|
||||
{
|
||||
obj: 'AI',
|
||||
value: res as string
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: '对话出现异常, 继续对话会导致上下文丢失,请刷新页面',
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
}
|
||||
|
||||
// 设置完成状态
|
||||
setChatData((state) => ({
|
||||
...state,
|
||||
history: state.history.map((item, index) => {
|
||||
if (index !== state.history.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish'
|
||||
};
|
||||
})
|
||||
}));
|
||||
},
|
||||
[chatData.modelName, chatId, toast]
|
||||
);
|
||||
|
||||
/**
|
||||
* 发送一个内容
|
||||
*/
|
||||
const sendPrompt = useCallback(async () => {
|
||||
const storeInput = inputVal;
|
||||
// 去除空行
|
||||
const val = inputVal
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((val) => val)
|
||||
.join('\n');
|
||||
if (!chatData?.modelId || !val || !ChatBox.current || isChatting) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 长度校验
|
||||
const tokens = encode(val).length;
|
||||
const model = modelList.find((item) => item.model === chatData.modelName);
|
||||
|
||||
if (model && tokens >= model.maxToken) {
|
||||
toast({
|
||||
title: '单次输入超出 4000 tokens',
|
||||
status: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newChatList: ChatSiteItemType[] = [
|
||||
...chatData.history,
|
||||
{
|
||||
obj: 'Human',
|
||||
value: val,
|
||||
status: 'finish'
|
||||
},
|
||||
{
|
||||
obj: 'AI',
|
||||
value: '',
|
||||
status: 'loading'
|
||||
}
|
||||
];
|
||||
|
||||
// 插入内容
|
||||
setChatData((state) => ({
|
||||
...state,
|
||||
history: newChatList
|
||||
}));
|
||||
|
||||
// 清空输入内容
|
||||
resetInputVal('');
|
||||
scrollToBottom();
|
||||
|
||||
try {
|
||||
await gptChatPrompt(newChatList[newChatList.length - 2]);
|
||||
|
||||
// 如果是 Human 第一次发送,插入历史记录
|
||||
const humanChat = newChatList.filter((item) => item.obj === 'Human');
|
||||
if (humanChat.length === 1) {
|
||||
pushChatHistory({
|
||||
chatId,
|
||||
title: humanChat[0].value
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: typeof err === 'string' ? err : err?.message || '聊天出错了~',
|
||||
status: 'warning',
|
||||
duration: 5000,
|
||||
isClosable: true
|
||||
});
|
||||
|
||||
resetInputVal(storeInput);
|
||||
|
||||
setChatData((state) => ({
|
||||
...state,
|
||||
history: newChatList.slice(0, newChatList.length - 2)
|
||||
}));
|
||||
}
|
||||
}, [
|
||||
inputVal,
|
||||
chatData,
|
||||
isChatting,
|
||||
resetInputVal,
|
||||
scrollToBottom,
|
||||
toast,
|
||||
gptChatPrompt,
|
||||
pushChatHistory,
|
||||
chatId
|
||||
]);
|
||||
|
||||
// 删除一句话
|
||||
const delChatRecord = useCallback(
|
||||
async (index: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 删除数据库最后一句
|
||||
await delChatRecordByIndex(chatId, index);
|
||||
|
||||
setChatData((state) => ({
|
||||
...state,
|
||||
history: state.history.filter((_, i) => i !== index)
|
||||
}));
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
[chatId, setLoading]
|
||||
);
|
||||
|
||||
// 复制内容
|
||||
const onclickCopy = useCallback(
|
||||
(chatId: string) => {
|
||||
const dom = document.getElementById(chatId);
|
||||
const innerText = dom?.innerText;
|
||||
innerText && copyData(innerText);
|
||||
},
|
||||
[copyData]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
controller.current = new AbortController();
|
||||
return () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
controller.current?.abort();
|
||||
};
|
||||
}, [chatId]);
|
||||
|
||||
// 初始化聊天框
|
||||
useQuery(
|
||||
['init', chatId],
|
||||
@@ -134,233 +350,12 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
}
|
||||
);
|
||||
|
||||
// 重置输入内容
|
||||
const resetInputVal = useCallback((val: string) => {
|
||||
setInputVal(val);
|
||||
setTimeout(() => {
|
||||
/* 回到最小高度 */
|
||||
if (TextareaDom.current) {
|
||||
TextareaDom.current.style.height =
|
||||
val === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
|
||||
}
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
// 重载对话
|
||||
const resetChat = useCallback(async () => {
|
||||
if (!chatData) return;
|
||||
try {
|
||||
router.push(`/chat?chatId=${await getChatSiteId(chatData.modelId)}`);
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: error?.message || '生成新对话失败',
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
onCloseSlider();
|
||||
}, [chatData, onCloseSlider, router, toast]);
|
||||
|
||||
// gpt3 方法
|
||||
const gpt3ChatPrompt = useCallback(
|
||||
async (newChatList: ChatSiteItemType[]) => {
|
||||
// 请求内容
|
||||
const response = await postGPT3SendPrompt({
|
||||
prompt: newChatList,
|
||||
chatId: chatId as string
|
||||
});
|
||||
|
||||
// 更新 AI 的内容
|
||||
setChatData((state) => ({
|
||||
...state,
|
||||
history: state.history.map((item, index) => {
|
||||
if (index !== state.history.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish',
|
||||
value: response
|
||||
};
|
||||
})
|
||||
}));
|
||||
},
|
||||
[chatId]
|
||||
);
|
||||
|
||||
// chatGPT
|
||||
const chatGPTPrompt = useCallback(
|
||||
async (newChatList: ChatSiteItemType[]) => {
|
||||
const prompt = {
|
||||
obj: newChatList[newChatList.length - 1].obj,
|
||||
value: newChatList[newChatList.length - 1].value
|
||||
};
|
||||
// 流请求,获取数据
|
||||
const res = await streamFetch({
|
||||
url: '/api/chat/chatGpt',
|
||||
data: {
|
||||
prompt,
|
||||
chatId
|
||||
},
|
||||
onMessage: (text: string) => {
|
||||
setChatData((state) => ({
|
||||
...state,
|
||||
history: state.history.map((item, index) => {
|
||||
if (index !== state.history.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
value: item.value + text
|
||||
};
|
||||
})
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// 保存对话信息
|
||||
try {
|
||||
await postSaveChat({
|
||||
chatId,
|
||||
prompts: [
|
||||
prompt,
|
||||
{
|
||||
obj: 'AI',
|
||||
value: res as string
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: '存储对话出现异常, 继续对话会导致上下文丢失,请刷新页面',
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
}
|
||||
|
||||
// 设置完成状态
|
||||
setChatData((state) => ({
|
||||
...state,
|
||||
history: state.history.map((item, index) => {
|
||||
if (index !== state.history.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish'
|
||||
};
|
||||
})
|
||||
}));
|
||||
},
|
||||
[chatId, toast]
|
||||
);
|
||||
|
||||
/**
|
||||
* 发送一个内容
|
||||
*/
|
||||
const sendPrompt = useCallback(async () => {
|
||||
const storeInput = inputVal;
|
||||
// 去除空行
|
||||
const val = inputVal
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((val) => val)
|
||||
.join('\n\n');
|
||||
if (!chatData?.modelId || !val || !ChatBox.current || isChatting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newChatList: ChatSiteItemType[] = [
|
||||
...chatData.history,
|
||||
{
|
||||
obj: 'Human',
|
||||
value: val,
|
||||
status: 'finish'
|
||||
},
|
||||
{
|
||||
obj: 'AI',
|
||||
value: '',
|
||||
status: 'loading'
|
||||
}
|
||||
];
|
||||
|
||||
// 插入内容
|
||||
setChatData((state) => ({
|
||||
...state,
|
||||
history: newChatList
|
||||
}));
|
||||
|
||||
// 清空输入内容
|
||||
resetInputVal('');
|
||||
scrollToBottom();
|
||||
|
||||
const fnMap: { [key: string]: any } = {
|
||||
[ChatModelNameEnum.GPT35]: chatGPTPrompt,
|
||||
[ChatModelNameEnum.GPT3]: gpt3ChatPrompt
|
||||
};
|
||||
|
||||
try {
|
||||
/* 对长度进行限制 */
|
||||
const maxContext = chatData.secret.contextMaxLen;
|
||||
const requestPrompt =
|
||||
newChatList.length > maxContext + 1
|
||||
? newChatList.slice(newChatList.length - maxContext - 1, -1)
|
||||
: newChatList.slice(0, -1);
|
||||
|
||||
if (typeof fnMap[chatData.chatModel] === 'function') {
|
||||
await fnMap[chatData.chatModel](requestPrompt);
|
||||
}
|
||||
|
||||
// 如果是 Human 第一次发送,插入历史记录
|
||||
const humanChat = newChatList.filter((item) => item.obj === 'Human');
|
||||
if (humanChat.length === 1) {
|
||||
pushChatHistory({
|
||||
chatId,
|
||||
title: humanChat[0].value
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: typeof err === 'string' ? err : err?.message || '聊天出错了~',
|
||||
status: 'warning',
|
||||
duration: 5000,
|
||||
isClosable: true
|
||||
});
|
||||
|
||||
resetInputVal(storeInput);
|
||||
|
||||
setChatData((state) => ({
|
||||
...state,
|
||||
history: newChatList.slice(0, newChatList.length - 2)
|
||||
}));
|
||||
}
|
||||
}, [
|
||||
inputVal,
|
||||
chatData.modelId,
|
||||
chatData.history,
|
||||
chatData.secret.contextMaxLen,
|
||||
chatData.chatModel,
|
||||
isChatting,
|
||||
resetInputVal,
|
||||
scrollToBottom,
|
||||
chatGPTPrompt,
|
||||
gpt3ChatPrompt,
|
||||
pushChatHistory,
|
||||
chatId,
|
||||
toast
|
||||
]);
|
||||
|
||||
// 重新编辑
|
||||
const reEdit = useCallback(async () => {
|
||||
if (chatData.history[chatData.history.length - 1]?.obj !== 'Human') return;
|
||||
// 删除数据库最后一句
|
||||
await delLastMessage(chatId);
|
||||
const val = chatData.history[chatData.history.length - 1].value;
|
||||
|
||||
resetInputVal(val);
|
||||
|
||||
setChatData((state) => ({
|
||||
...state,
|
||||
history: state.history.slice(0, -1)
|
||||
}));
|
||||
}, [chatData.history, chatId, resetInputVal]);
|
||||
|
||||
return (
|
||||
<Flex h={'100%'} flexDirection={media('row', 'column')}>
|
||||
<Flex
|
||||
h={'100%'}
|
||||
flexDirection={media('row', 'column')}
|
||||
backgroundColor={useColorModeValue('white', '')}
|
||||
>
|
||||
{isPc ? (
|
||||
<Box flex={'0 0 250px'} w={0} h={'100%'}>
|
||||
<SlideBar
|
||||
@@ -377,12 +372,18 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
alignItems={'center'}
|
||||
h={'100%'}
|
||||
justifyContent={'space-between'}
|
||||
backgroundColor={'white'}
|
||||
backgroundColor={useColorModeValue('white', 'gray.700')}
|
||||
color={useColorModeValue('blackAlpha.700', 'white')}
|
||||
position={'relative'}
|
||||
px={7}
|
||||
>
|
||||
<Box onClick={onOpenSlider}>
|
||||
<Icon name="icon-caidan" width={20} height={20}></Icon>
|
||||
<Icon
|
||||
name={'menu'}
|
||||
w={'20px'}
|
||||
h={'20px'}
|
||||
fill={useColorModeValue('blackAlpha.700', 'white')}
|
||||
/>
|
||||
</Box>
|
||||
<Box>{chatData?.name}</Box>
|
||||
</Flex>
|
||||
@@ -407,25 +408,34 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
flexDirection={'column'}
|
||||
>
|
||||
{/* 聊天内容 */}
|
||||
<Box ref={ChatBox} flex={'1 0 0'} h={0} w={'100%'} overflowY={'auto'}>
|
||||
<Box ref={ChatBox} pb={[4, 0]} flex={'1 0 0'} h={0} w={'100%'} overflowY={'auto'}>
|
||||
{chatData.history.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
py={media(9, 6)}
|
||||
px={media(4, 2)}
|
||||
backgroundColor={index % 2 === 0 ? 'rgba(247,247,248,1)' : '#fff'}
|
||||
backgroundColor={
|
||||
index % 2 !== 0 ? useColorModeValue('blackAlpha.50', 'gray.700') : ''
|
||||
}
|
||||
color={useColorModeValue('blackAlpha.700', 'white')}
|
||||
borderBottom={'1px solid rgba(0,0,0,0.1)'}
|
||||
>
|
||||
<Flex maxW={'750px'} m={'auto'} alignItems={'flex-start'}>
|
||||
<Box mr={media(4, 1)}>
|
||||
<Image
|
||||
src={item.obj === 'Human' ? '/icon/human.png' : '/icon/logo.png'}
|
||||
alt="/icon/logo.png"
|
||||
width={media(30, 20)}
|
||||
height={media(30, 20)}
|
||||
/>
|
||||
</Box>
|
||||
<Box flex={'1 0 0'} w={0} overflow={'hidden'}>
|
||||
<Menu>
|
||||
<MenuButton as={Box} mr={media(4, 1)} cursor={'pointer'}>
|
||||
<Image
|
||||
src={item.obj === 'Human' ? '/icon/human.png' : '/icon/logo.png'}
|
||||
alt="/icon/logo.png"
|
||||
width={media(30, 20)}
|
||||
height={media(30, 20)}
|
||||
/>
|
||||
</MenuButton>
|
||||
<MenuList fontSize={'sm'}>
|
||||
<MenuItem onClick={() => onclickCopy(`chat${index}`)}>复制</MenuItem>
|
||||
<MenuItem onClick={() => delChatRecord(index)}>删除该行</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
<Box flex={'1 0 0'} w={0} overflow={'hidden'} id={`chat${index}`}>
|
||||
{item.obj === 'AI' ? (
|
||||
<Markdown
|
||||
source={item.value}
|
||||
@@ -441,88 +451,76 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
{chatData.history.length === 0 && <Empty intro={chatData.intro} />}
|
||||
</Box>
|
||||
{/* 发送区 */}
|
||||
<Box
|
||||
m={media('20px auto', '0 auto')}
|
||||
w={'100%'}
|
||||
maxW={media('min(750px, 100%)', 'auto')}
|
||||
boxShadow={'0 -14px 30px rgba(255,255,255,0.6)'}
|
||||
borderTop={media('none', '1px solid rgba(0,0,0,0.1)')}
|
||||
>
|
||||
{!!chatWindowError ? (
|
||||
<Box textAlign={'center'}>
|
||||
<Box color={'red'}>{chatWindowError.text}</Box>
|
||||
<Flex py={5} justifyContent={'center'}>
|
||||
{getToken() && <Button onClick={resetChat}>重开对话</Button>}
|
||||
|
||||
{chatWindowError.canDelete && (
|
||||
<Button ml={20} colorScheme={'green'} onClick={reEdit}>
|
||||
重新编辑最后一句
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<Box m={media('20px auto', '0 auto')} w={'100%'} maxW={media('min(750px, 100%)', 'auto')}>
|
||||
<Flex
|
||||
alignItems={'flex-end'}
|
||||
py={5}
|
||||
position={'relative'}
|
||||
boxShadow={`0 0 15px rgba(0,0,0,0.1)`}
|
||||
border={media('1px solid', '0')}
|
||||
borderColor={useColorModeValue('gray.200', 'gray.700')}
|
||||
borderRadius={['none', 'md']}
|
||||
backgroundColor={useColorModeValue('white', 'gray.700')}
|
||||
>
|
||||
{/* 输入框 */}
|
||||
<Textarea
|
||||
ref={TextareaDom}
|
||||
flex={1}
|
||||
w={0}
|
||||
py={0}
|
||||
border={'none'}
|
||||
_focusVisible={{
|
||||
border: 'none'
|
||||
}}
|
||||
placeholder="提问"
|
||||
resize={'none'}
|
||||
value={inputVal}
|
||||
rows={1}
|
||||
height={'22px'}
|
||||
lineHeight={'22px'}
|
||||
maxHeight={'150px'}
|
||||
maxLength={-1}
|
||||
overflowY={'auto'}
|
||||
color={useColorModeValue('blackAlpha.700', 'white')}
|
||||
onChange={(e) => {
|
||||
const textarea = e.target;
|
||||
setInputVal(textarea.value);
|
||||
textarea.style.height = textareaMinH;
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// 触发快捷发送
|
||||
if (isPc && e.keyCode === 13 && !e.shiftKey) {
|
||||
sendPrompt();
|
||||
e.preventDefault();
|
||||
}
|
||||
// 全选内容
|
||||
// @ts-ignore
|
||||
e.key === 'a' && e.ctrlKey && e.target?.select();
|
||||
}}
|
||||
/>
|
||||
{/* 发送和等待按键 */}
|
||||
<Box px={4} onClick={sendPrompt}>
|
||||
{isChatting ? (
|
||||
<Image
|
||||
style={{ transform: 'translateY(4px)' }}
|
||||
src={'/icon/chatting.svg'}
|
||||
width={30}
|
||||
height={30}
|
||||
alt={''}
|
||||
/>
|
||||
) : (
|
||||
<Box cursor={'pointer'}>
|
||||
<Icon
|
||||
name={'chatSend'}
|
||||
width={'20px'}
|
||||
height={'20px'}
|
||||
fill={useColorModeValue('#718096', 'white')}
|
||||
></Icon>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
py={5}
|
||||
position={'relative'}
|
||||
boxShadow={'base'}
|
||||
overflow={'hidden'}
|
||||
borderRadius={media('md', 'none')}
|
||||
>
|
||||
{/* 输入框 */}
|
||||
<Textarea
|
||||
ref={TextareaDom}
|
||||
w={'100%'}
|
||||
pr={'45px'}
|
||||
py={0}
|
||||
border={'none'}
|
||||
_focusVisible={{
|
||||
border: 'none'
|
||||
}}
|
||||
placeholder="提问"
|
||||
resize={'none'}
|
||||
value={inputVal}
|
||||
rows={1}
|
||||
height={'22px'}
|
||||
lineHeight={'22px'}
|
||||
maxHeight={'150px'}
|
||||
maxLength={chatData?.secret.contentMaxLen || -1}
|
||||
overflowY={'auto'}
|
||||
onChange={(e) => {
|
||||
const textarea = e.target;
|
||||
setInputVal(textarea.value);
|
||||
textarea.style.height = textareaMinH;
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// 触发快捷发送
|
||||
if (isPc && e.keyCode === 13 && !e.shiftKey) {
|
||||
sendPrompt();
|
||||
e.preventDefault();
|
||||
}
|
||||
// 全选内容
|
||||
// @ts-ignore
|
||||
e.key === 'a' && e.ctrlKey && e.target?.select();
|
||||
}}
|
||||
/>
|
||||
{/* 发送和等待按键 */}
|
||||
<Box position={'absolute'} bottom={5} right={media('20px', '10px')}>
|
||||
{isChatting ? (
|
||||
<Image
|
||||
style={{ transform: 'translateY(4px)' }}
|
||||
src={'/icon/chatting.svg'}
|
||||
width={30}
|
||||
height={30}
|
||||
alt={''}
|
||||
/>
|
||||
) : (
|
||||
<Box cursor={'pointer'} onClick={sendPrompt}>
|
||||
<Icon name={'icon-fasong'} width={20} height={20} color={'#718096'}></Icon>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
97
src/pages/data/components/CreateDataModal.tsx
Normal file
97
src/pages/data/components/CreateDataModal.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
Button,
|
||||
Input,
|
||||
Select,
|
||||
FormControl,
|
||||
FormErrorMessage
|
||||
} from '@chakra-ui/react';
|
||||
import { postData } from '@/api/data';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||
import { DataType } from '@/types/data';
|
||||
import { DataTypeTextMap } from '@/constants/data';
|
||||
|
||||
export interface CreateDataProps {
|
||||
name: string;
|
||||
type: DataType;
|
||||
}
|
||||
|
||||
const CreateDataModal = ({
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}) => {
|
||||
const [inputVal, setInputVal] = useState('');
|
||||
const {
|
||||
getValues,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
} = useForm<CreateDataProps>({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
type: 'abstract'
|
||||
}
|
||||
});
|
||||
|
||||
const { isLoading, mutate } = useMutation({
|
||||
mutationFn: (e: CreateDataProps) => postData(e),
|
||||
onSuccess() {
|
||||
onSuccess();
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>创建数据集</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
|
||||
<ModalBody>
|
||||
<FormControl mb={8} isInvalid={!!errors.name}>
|
||||
<Input
|
||||
placeholder="数据集名称"
|
||||
{...register('name', {
|
||||
required: '数据集名称不能为空'
|
||||
})}
|
||||
/>
|
||||
<FormErrorMessage position={'absolute'} fontSize="xs">
|
||||
{!!errors.name && errors.name.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Select placeholder="数据集类型" {...register('type', {})}>
|
||||
{Object.entries(DataTypeTextMap).map(([key, value]) => (
|
||||
<option key={key} value={key}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button colorScheme={'gray'} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button ml={3} isLoading={isLoading} onClick={handleSubmit(mutate as any)}>
|
||||
确认
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateDataModal;
|
||||
229
src/pages/data/components/ImportDataModal.tsx
Normal file
229
src/pages/data/components/ImportDataModal.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
Button,
|
||||
Box,
|
||||
Flex,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import { useTabs } from '@/hooks/useTabs';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { useSelectFile } from '@/hooks/useSelectFile';
|
||||
import { readTxtContent, readPdfContent, readDocContent } from '@/utils/tools';
|
||||
import { postSplitData } from '@/api/data';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import { modelList, ChatModelNameEnum } from '@/constants/model';
|
||||
import { encode } from 'gpt-token-utils';
|
||||
|
||||
const fileExtension = '.txt,.doc,.docx,.pdf,.md';
|
||||
|
||||
const ImportDataModal = ({
|
||||
dataId,
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
dataId: string;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}) => {
|
||||
const { openConfirm, ConfirmChild } = useConfirm({
|
||||
content: '确认提交生成任务?该任务无法终止!'
|
||||
});
|
||||
const { toast } = useToast();
|
||||
const { setIsLoading, Loading } = useLoading();
|
||||
const { File, onOpen } = useSelectFile({ fileType: fileExtension, multiple: true });
|
||||
const { tabs, activeTab, setActiveTab } = useTabs({
|
||||
tabs: [
|
||||
{ id: 'text', label: '文本' },
|
||||
{ id: 'doc', label: '文件' }
|
||||
// { id: 'url', label: '链接' }
|
||||
]
|
||||
});
|
||||
|
||||
const [textInput, setTextInput] = useState('');
|
||||
const [fileText, setFileText] = useState('');
|
||||
|
||||
const { mutate: handleClickSubmit, isLoading } = useMutation({
|
||||
mutationFn: async () => {
|
||||
let text = '';
|
||||
if (activeTab === 'text') {
|
||||
text = textInput;
|
||||
} else if (activeTab === 'doc') {
|
||||
text = fileText;
|
||||
} else if (activeTab === 'url') {
|
||||
}
|
||||
if (!text) return;
|
||||
return postSplitData(dataId, text);
|
||||
},
|
||||
onSuccess() {
|
||||
toast({
|
||||
title: '任务提交成功',
|
||||
status: 'success'
|
||||
});
|
||||
onClose();
|
||||
onSuccess();
|
||||
},
|
||||
onError(err: any) {
|
||||
toast({
|
||||
title: err?.message || '提交任务异常',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
async (e: File[]) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const fileTexts = (
|
||||
await Promise.all(
|
||||
e.map((file) => {
|
||||
// @ts-ignore
|
||||
const extension = file?.name?.split('.').pop().toLowerCase();
|
||||
switch (extension) {
|
||||
case 'txt':
|
||||
case 'md':
|
||||
return readTxtContent(file);
|
||||
case 'pdf':
|
||||
return readPdfContent(file);
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return readDocContent(file);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
.join('\n')
|
||||
.replace(/\n+/g, '\n');
|
||||
setFileText(fileTexts);
|
||||
console.log(encode(fileTexts));
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
toast({
|
||||
title: typeof error === 'string' ? error : '解析文件失败',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
[setIsLoading, toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent position={'relative'} maxW={['90vw', '800px']}>
|
||||
<ModalHeader>
|
||||
导入数据,生成QA
|
||||
<Box ml={2} as={'span'} fontSize={'sm'} color={'blackAlpha.600'}>
|
||||
{formatPrice(
|
||||
modelList.find((item) => item.model === ChatModelNameEnum.GPT35)?.price || 0,
|
||||
1000
|
||||
)}
|
||||
元/1K tokens
|
||||
</Box>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
|
||||
<ModalBody display={'flex'}>
|
||||
<Box>
|
||||
{tabs.map((item) => (
|
||||
<Button
|
||||
key={item.id}
|
||||
display={'block'}
|
||||
variant={activeTab === item.id ? 'solid' : 'outline'}
|
||||
_notLast={{
|
||||
mb: 3
|
||||
}}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Box flex={'1 0 0'} w={0} ml={3} minH={'200px'}>
|
||||
{activeTab === 'text' && (
|
||||
<>
|
||||
<Textarea
|
||||
h={'100%'}
|
||||
maxLength={-1}
|
||||
value={textInput}
|
||||
placeholder={'请粘贴或输入需要处理的文本'}
|
||||
onChange={(e) => setTextInput(e.target.value)}
|
||||
/>
|
||||
<Box mt={2}>
|
||||
一共 {textInput.length} 个字,{encode(textInput).length} 个tokens
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{activeTab === 'doc' && (
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
p={2}
|
||||
h={'100%'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
border={'1px solid '}
|
||||
borderColor={'blackAlpha.200'}
|
||||
borderRadius={'md'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Button onClick={onOpen}>选择文件</Button>
|
||||
<Box mt={2}>支持 {fileExtension} 文件</Box>
|
||||
{fileText && (
|
||||
<>
|
||||
<Box mt={2}>
|
||||
一共 {fileText.length} 个字,{encode(fileText).length} 个tokens
|
||||
</Box>
|
||||
<Box
|
||||
maxH={'300px'}
|
||||
w={'100%'}
|
||||
overflow={'auto'}
|
||||
p={2}
|
||||
backgroundColor={'blackAlpha.50'}
|
||||
whiteSpace={'pre'}
|
||||
fontSize={'xs'}
|
||||
>
|
||||
{fileText}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button colorScheme={'gray'} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
ml={3}
|
||||
isLoading={isLoading}
|
||||
isDisabled={!textInput && !fileText}
|
||||
onClick={openConfirm(handleClickSubmit)}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
<Loading />
|
||||
</ModalContent>
|
||||
|
||||
<ConfirmChild />
|
||||
<File onSelect={onSelectFile} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportDataModal;
|
||||
67
src/pages/data/detail.tsx
Normal file
67
src/pages/data/detail.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { Box, Card } from '@chakra-ui/react';
|
||||
import ScrollData from '@/components/ScrollData';
|
||||
import { getDataItems } from '@/api/data';
|
||||
import { usePaging } from '@/hooks/usePaging';
|
||||
import type { DataItemSchema } from '@/types/mongoSchema';
|
||||
|
||||
const DataDetail = ({ dataName, dataId }: { dataName: string; dataId: string }) => {
|
||||
const {
|
||||
nextPage,
|
||||
isLoadAll,
|
||||
requesting,
|
||||
data: dataItems
|
||||
} = usePaging<DataItemSchema>({
|
||||
api: getDataItems,
|
||||
pageSize: 10,
|
||||
params: {
|
||||
dataId
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Card py={4} h={'100%'} display={'flex'} flexDirection={'column'}>
|
||||
<Box px={6} fontSize={'xl'} fontWeight={'bold'}>
|
||||
{dataName} 结果
|
||||
</Box>
|
||||
<ScrollData
|
||||
flex={'1 0 0'}
|
||||
h={0}
|
||||
px={6}
|
||||
mt={3}
|
||||
isLoadAll={isLoadAll}
|
||||
requesting={requesting}
|
||||
nextPage={nextPage}
|
||||
fontSize={'xs'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
>
|
||||
{dataItems.map((item) => (
|
||||
<Box key={item._id}>
|
||||
{item.result.map((result, i) => (
|
||||
<Box key={i} mb={3}>
|
||||
{item.type === 'QA' && (
|
||||
<>
|
||||
<Box fontWeight={'bold'}>Q: {result.q}</Box>
|
||||
<Box>A: {result.a}</Box>
|
||||
</>
|
||||
)}
|
||||
{item.type === 'abstract' && <Box fontSize={'sm'}>{result.abstract}</Box>}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
))}
|
||||
</ScrollData>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataDetail;
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
return {
|
||||
props: {
|
||||
dataName: context.query?.dataName || '',
|
||||
dataId: context.query?.dataId || ''
|
||||
}
|
||||
};
|
||||
}
|
||||
235
src/pages/data/list.tsx
Normal file
235
src/pages/data/list.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
useDisclosure,
|
||||
Input,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem
|
||||
} from '@chakra-ui/react';
|
||||
import { getDataList, updateDataName, delData, getDataItems } from '@/api/data';
|
||||
import type { DataListItem } from '@/types/data';
|
||||
import dayjs from 'dayjs';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import { DataItemSchema } from '@/types/mongoSchema';
|
||||
import { DataTypeTextMap } from '@/constants/data';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
const nanoid = customAlphabet('.,', 1);
|
||||
|
||||
const CreateDataModal = dynamic(() => import('./components/CreateDataModal'));
|
||||
const ImportDataModal = dynamic(() => import('./components/ImportDataModal'));
|
||||
|
||||
export type ExportDataType = 'jsonl' | 'txt';
|
||||
|
||||
const DataList = () => {
|
||||
const router = useRouter();
|
||||
const [ImportDataId, setImportDataId] = useState<string>();
|
||||
const { openConfirm, ConfirmChild } = useConfirm({
|
||||
content: '删除数据集,将删除里面的所有内容,请确认!'
|
||||
});
|
||||
|
||||
const {
|
||||
isOpen: isOpenCreateDataModal,
|
||||
onOpen: onOpenCreateDataModal,
|
||||
onClose: onCloseCreateDataModal
|
||||
} = useDisclosure();
|
||||
|
||||
const { data: dataList = [], refetch } = useQuery(['getDataList'], getDataList, {
|
||||
refetchInterval: 10000
|
||||
});
|
||||
|
||||
const { mutate: handleDelData, isLoading: isDeleting } = useRequest({
|
||||
mutationFn: (dataId: string) => delData(dataId),
|
||||
successToast: '删除数据集成功',
|
||||
errorToast: '删除数据集异常',
|
||||
onSuccess() {
|
||||
refetch();
|
||||
}
|
||||
});
|
||||
|
||||
const { mutate: handleExportData, isLoading: isExporting } = useRequest({
|
||||
mutationFn: async ({ data, type }: { data: DataListItem; type: ExportDataType }) => ({
|
||||
type,
|
||||
data: await getDataItems({ dataId: data._id, pageNum: 1, pageSize: data.totalData }).then(
|
||||
(res) => res.data
|
||||
)
|
||||
}),
|
||||
successToast: '导出数据集成功',
|
||||
errorToast: '导出数据集异常',
|
||||
onSuccess(res: { type: ExportDataType; data: DataItemSchema[] }) {
|
||||
// 合并数据
|
||||
const data = res.data.map((item) => item.result).flat();
|
||||
let text = '';
|
||||
// 生成 jsonl
|
||||
data.forEach((item) => {
|
||||
if (res.type === 'jsonl' && item.q && item.a) {
|
||||
const result = JSON.stringify({
|
||||
prompt: `${item.q.toLocaleLowerCase()}${nanoid()}</s>`,
|
||||
completion: ` ${item.a}###`
|
||||
});
|
||||
text += `${result}\n`;
|
||||
} else if (res.type === 'txt' && item.abstract) {
|
||||
text += `${item.abstract}\n`;
|
||||
}
|
||||
});
|
||||
// 去掉最后一个 \n
|
||||
text = text.substring(0, text.length - 1);
|
||||
|
||||
// 导出为文件
|
||||
const blob = new Blob([text], { type: 'application/json;charset=utf-8' });
|
||||
|
||||
// 创建下载链接
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.href = window.URL.createObjectURL(blob);
|
||||
downloadLink.download = `data.${res.type}`;
|
||||
|
||||
// 添加链接到页面并触发下载
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box display={['block', 'flex']} flexDirection={'column'} h={'100%'}>
|
||||
<Card px={6} py={4}>
|
||||
<Flex>
|
||||
<Box flex={1} mr={1}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||
训练数据管理
|
||||
</Box>
|
||||
<Box fontSize={'xs'} color={'blackAlpha.600'}>
|
||||
允许你将任意文本数据拆分成 QA 形式,或者进行文本摘要总结。
|
||||
</Box>
|
||||
</Box>
|
||||
<Button variant={'outline'} onClick={onOpenCreateDataModal}>
|
||||
创建数据集
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
{/* 数据表 */}
|
||||
<TableContainer
|
||||
mt={3}
|
||||
flex={'1 0 0'}
|
||||
h={['auto', '0']}
|
||||
overflowY={'auto'}
|
||||
px={6}
|
||||
py={4}
|
||||
backgroundColor={'white'}
|
||||
borderRadius={'md'}
|
||||
boxShadow={'base'}
|
||||
>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>集合名</Th>
|
||||
<Th>类型</Th>
|
||||
<Th>创建时间</Th>
|
||||
<Th>训练中 / 总数据</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{dataList.map((item, i) => (
|
||||
<Tr key={item._id}>
|
||||
<Td>
|
||||
<Input
|
||||
minW={'150px'}
|
||||
placeholder="请输入数据集名称"
|
||||
defaultValue={item.name}
|
||||
size={'sm'}
|
||||
onBlur={(e) => {
|
||||
if (!e.target.value || e.target.value === item.name) return;
|
||||
updateDataName(item._id, e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
<Td>{DataTypeTextMap[item.type || 'QA']}</Td>
|
||||
<Td>{dayjs(item.createTime).format('YYYY/MM/DD HH:mm')}</Td>
|
||||
<Td>
|
||||
{item.trainingData} / {item.totalData}
|
||||
</Td>
|
||||
<Td>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'outline'}
|
||||
colorScheme={'gray'}
|
||||
mr={2}
|
||||
onClick={() =>
|
||||
router.push(`/data/detail?dataId=${item._id}&dataName=${item.name}`)
|
||||
}
|
||||
>
|
||||
详细
|
||||
</Button>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'outline'}
|
||||
mr={2}
|
||||
onClick={() => setImportDataId(item._id)}
|
||||
>
|
||||
导入
|
||||
</Button>
|
||||
{/* <Menu>
|
||||
<MenuButton as={Button} mr={2} size={'sm'} isLoading={isExporting}>
|
||||
导出
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
{item.type === 'QA' && (
|
||||
<MenuItem onClick={() => handleExportData({ data: item, type: 'jsonl' })}>
|
||||
jsonl
|
||||
</MenuItem>
|
||||
)}
|
||||
{item.type === 'abstract' && (
|
||||
<MenuItem onClick={() => handleExportData({ data: item, type: 'txt' })}>
|
||||
txt
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu> */}
|
||||
|
||||
<Button
|
||||
size={'sm'}
|
||||
colorScheme={'red'}
|
||||
isLoading={isDeleting}
|
||||
onClick={openConfirm(() => handleDelData(item._id))}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{ImportDataId && (
|
||||
<ImportDataModal
|
||||
dataId={ImportDataId}
|
||||
onClose={() => setImportDataId(undefined)}
|
||||
onSuccess={refetch}
|
||||
/>
|
||||
)}
|
||||
{isOpenCreateDataModal && (
|
||||
<CreateDataModal onClose={onCloseCreateDataModal} onSuccess={refetch} />
|
||||
)}
|
||||
<ConfirmChild />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataList;
|
||||
@@ -6,9 +6,8 @@ import { useScreen } from '@/hooks/useScreen';
|
||||
import type { ResLogin } from '@/api/response/user';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
import LoginForm from './components/LoginForm';
|
||||
import dynamic from 'next/dynamic';
|
||||
const LoginForm = dynamic(() => import('./components/LoginForm'));
|
||||
const RegisterForm = dynamic(() => import('./components/RegisterForm'));
|
||||
const ForgetPasswordForm = dynamic(() => import('./components/ForgetPasswordForm'));
|
||||
|
||||
|
||||
141
src/pages/model/detail/components/InputDataModal.tsx
Normal file
141
src/pages/model/detail/components/InputDataModal.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
Flex,
|
||||
Button,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
Input,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm, useFieldArray } from 'react-hook-form';
|
||||
import { postModelDataInput } from '@/api/model';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { DeleteIcon } from '@chakra-ui/icons';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
|
||||
|
||||
type FormData = { text: string; q: { val: string }[] };
|
||||
|
||||
const InputDataModal = ({
|
||||
onClose,
|
||||
onSuccess,
|
||||
modelId
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
modelId: string;
|
||||
}) => {
|
||||
const [importing, setImporting] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const { register, handleSubmit, control } = useForm<FormData>({
|
||||
defaultValues: {
|
||||
text: '',
|
||||
q: [{ val: '' }]
|
||||
}
|
||||
});
|
||||
const {
|
||||
fields: inputQ,
|
||||
append: appendQ,
|
||||
remove: removeQ
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: 'q'
|
||||
});
|
||||
|
||||
const sureImportData = useCallback(
|
||||
async (e: FormData) => {
|
||||
setImporting(true);
|
||||
|
||||
try {
|
||||
await postModelDataInput({
|
||||
modelId: modelId,
|
||||
data: [
|
||||
{
|
||||
text: e.text,
|
||||
q: e.q.map((item) => ({
|
||||
id: nanoid(),
|
||||
text: item.val
|
||||
}))
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
toast({
|
||||
title: '导入数据成功,需要一段时间训练',
|
||||
status: 'success'
|
||||
});
|
||||
onClose();
|
||||
onSuccess();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
setImporting(false);
|
||||
},
|
||||
[modelId, onClose, onSuccess, toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxW={'min(900px, 90vw)'} maxH={'80vh'} position={'relative'}>
|
||||
<ModalHeader>手动导入</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<Box px={6} pb={2} overflowY={'auto'}>
|
||||
<Box mb={2}>知识点:</Box>
|
||||
<Textarea
|
||||
mb={4}
|
||||
placeholder="知识点"
|
||||
rows={3}
|
||||
maxH={'200px'}
|
||||
{...register(`text`, {
|
||||
required: '知识点'
|
||||
})}
|
||||
/>
|
||||
{inputQ.map((item, index) => (
|
||||
<Box key={item.id} mb={5}>
|
||||
<Box mb={2}>问法{index + 1}:</Box>
|
||||
<Flex>
|
||||
<Input
|
||||
placeholder="问法"
|
||||
{...register(`q.${index}.val`, {
|
||||
required: '问法不能为空'
|
||||
})}
|
||||
></Input>
|
||||
{inputQ.length > 1 && (
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
aria-label={'delete'}
|
||||
colorScheme={'gray'}
|
||||
variant={'unstyled'}
|
||||
onClick={() => removeQ(index)}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Flex px={6} pt={2} pb={4}>
|
||||
<Button alignSelf={'flex-start'} variant={'outline'} onClick={() => appendQ({ val: '' })}>
|
||||
增加问法
|
||||
</Button>
|
||||
<Box flex={1}></Box>
|
||||
<Button variant={'outline'} mr={3} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button isLoading={importing} onClick={handleSubmit(sureImportData)}>
|
||||
确认导入
|
||||
</Button>
|
||||
</Flex>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputDataModal;
|
||||
201
src/pages/model/detail/components/ModelDataCard.tsx
Normal file
201
src/pages/model/detail/components/ModelDataCard.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
TableContainer,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
IconButton,
|
||||
Flex,
|
||||
Button,
|
||||
useDisclosure,
|
||||
Textarea,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem
|
||||
} from '@chakra-ui/react';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { ModelDataSchema } from '@/types/mongoSchema';
|
||||
import { ModelDataStatusMap } from '@/constants/model';
|
||||
import { usePagination } from '@/hooks/usePagination';
|
||||
import {
|
||||
getModelDataList,
|
||||
delOneModelData,
|
||||
putModelDataById,
|
||||
getModelSplitDataList
|
||||
} from '@/api/model';
|
||||
import { DeleteIcon, RepeatIcon } from '@chakra-ui/icons';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
const InputModel = dynamic(() => import('./InputDataModal'));
|
||||
const SelectModel = dynamic(() => import('./SelectFileModal'));
|
||||
|
||||
const ModelDataCard = ({ model }: { model: ModelSchema }) => {
|
||||
const { toast } = useToast();
|
||||
const { Loading } = useLoading();
|
||||
|
||||
const {
|
||||
data: modelDataList,
|
||||
isLoading,
|
||||
Pagination,
|
||||
total,
|
||||
getData,
|
||||
pageNum
|
||||
} = usePagination<ModelDataSchema>({
|
||||
api: getModelDataList,
|
||||
pageSize: 10,
|
||||
params: {
|
||||
modelId: model._id
|
||||
}
|
||||
});
|
||||
|
||||
const updateAnswer = useCallback(
|
||||
async (dataId: string, text: string) => {
|
||||
await putModelDataById({
|
||||
dataId,
|
||||
text
|
||||
});
|
||||
toast({
|
||||
title: '修改回答成功',
|
||||
status: 'success'
|
||||
});
|
||||
},
|
||||
[toast]
|
||||
);
|
||||
|
||||
const {
|
||||
isOpen: isOpenInputModal,
|
||||
onOpen: onOpenInputModal,
|
||||
onClose: onCloseInputModal
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenSelectModal,
|
||||
onOpen: onOpenSelectModal,
|
||||
onClose: onCloseSelectModal
|
||||
} = useDisclosure();
|
||||
|
||||
const { data, refetch } = useQuery(['getModelSplitDataList'], () =>
|
||||
getModelSplitDataList(model._id)
|
||||
);
|
||||
|
||||
const refetchData = useCallback(
|
||||
(num = 1) => {
|
||||
getData(num);
|
||||
refetch();
|
||||
},
|
||||
[getData, refetch]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'} flex={1}>
|
||||
模型数据: {total}组{' '}
|
||||
<Box as={'span'} fontSize={'sm'}>
|
||||
(测试版本)
|
||||
</Box>
|
||||
</Box>
|
||||
<IconButton
|
||||
icon={<RepeatIcon />}
|
||||
aria-label={'refresh'}
|
||||
variant={'outline'}
|
||||
mr={4}
|
||||
onClick={() => refetchData(pageNum)}
|
||||
/>
|
||||
<Menu>
|
||||
<MenuButton as={Button}>导入</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem onClick={onOpenInputModal}>手动输入</MenuItem>
|
||||
<MenuItem onClick={onOpenSelectModal}>文件导入</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Flex>
|
||||
{data && data.length > 0 && <Box fontSize={'xs'}>{data.length}条数据正在拆分中...</Box>}
|
||||
<Box mt={4}>
|
||||
<TableContainer h={'600px'} overflowY={'auto'}>
|
||||
<Table variant={'simple'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Question</Th>
|
||||
<Th>Text</Th>
|
||||
<Th>Status</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{modelDataList.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
<Td w={'350px'}>
|
||||
{item.q.map((item, i) => (
|
||||
<Box
|
||||
key={item.id}
|
||||
fontSize={'xs'}
|
||||
w={'100%'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
_notLast={{ mb: 1 }}
|
||||
>
|
||||
Q{i + 1}:{' '}
|
||||
<Box as={'span'} userSelect={'all'}>
|
||||
{item.text}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Td>
|
||||
<Td minW={'200px'}>
|
||||
<Textarea
|
||||
w={'100%'}
|
||||
h={'100%'}
|
||||
defaultValue={item.text}
|
||||
fontSize={'xs'}
|
||||
resize={'both'}
|
||||
onBlur={(e) => {
|
||||
const oldVal = modelDataList.find((data) => item._id === data._id)?.text;
|
||||
if (oldVal !== e.target.value) {
|
||||
updateAnswer(item._id, e.target.value);
|
||||
}
|
||||
}}
|
||||
></Textarea>
|
||||
</Td>
|
||||
<Td w={'100px'}>{ModelDataStatusMap[item.status]}</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
variant={'outline'}
|
||||
colorScheme={'gray'}
|
||||
aria-label={'delete'}
|
||||
size={'sm'}
|
||||
onClick={async () => {
|
||||
await delOneModelData(item._id);
|
||||
refetchData(pageNum);
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Box mt={2} textAlign={'end'}>
|
||||
<Pagination />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
{isOpenInputModal && (
|
||||
<InputModel modelId={model._id} onClose={onCloseInputModal} onSuccess={refetchData} />
|
||||
)}
|
||||
{isOpenSelectModal && (
|
||||
<SelectModel modelId={model._id} onClose={onCloseSelectModal} onSuccess={refetchData} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelDataCard;
|
||||
@@ -11,13 +11,28 @@ import {
|
||||
SliderFilledTrack,
|
||||
SliderThumb,
|
||||
SliderMark,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
Button
|
||||
} from '@chakra-ui/react';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { modelList } from '@/constants/model';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
|
||||
const ModelEditForm = ({ formHooks }: { formHooks: UseFormReturn<ModelSchema> }) => {
|
||||
const ModelEditForm = ({
|
||||
formHooks,
|
||||
canTrain,
|
||||
handleDelModel
|
||||
}: {
|
||||
formHooks: UseFormReturn<ModelSchema>;
|
||||
canTrain: boolean;
|
||||
handleDelModel: () => void;
|
||||
}) => {
|
||||
const { openConfirm, ConfirmChild } = useConfirm({
|
||||
content: '确认删除该模型?'
|
||||
});
|
||||
const { register, setValue, getValues } = formHooks;
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
@@ -29,7 +44,7 @@ const ModelEditForm = ({ formHooks }: { formHooks: UseFormReturn<ModelSchema> })
|
||||
</Flex>
|
||||
<FormControl mt={4}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 50px'} w={0}>
|
||||
<Box flex={'0 0 80px'} w={0}>
|
||||
名称:
|
||||
</Box>
|
||||
<Input
|
||||
@@ -39,7 +54,36 @@ const ModelEditForm = ({ formHooks }: { formHooks: UseFormReturn<ModelSchema> })
|
||||
></Input>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<FormControl mt={4}>
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Box flex={'0 0 80px'} w={0}>
|
||||
底层模型:
|
||||
</Box>
|
||||
<Box>{getValues('service.modelName')}</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Box flex={'0 0 80px'} w={0}>
|
||||
价格:
|
||||
</Box>
|
||||
<Box>
|
||||
{formatPrice(
|
||||
modelList.find((item) => item.model === getValues('service.modelName'))?.price || 0,
|
||||
1000
|
||||
)}
|
||||
元/1K tokens(包括上下文和回答)
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>删除:</Box>
|
||||
<Button
|
||||
colorScheme={'gray'}
|
||||
variant={'outline'}
|
||||
size={'sm'}
|
||||
onClick={openConfirm(handleDelModel)}
|
||||
>
|
||||
删除模型
|
||||
</Button>
|
||||
</Flex>
|
||||
{/* <FormControl mt={4}>
|
||||
<Box mb={1}>介绍:</Box>
|
||||
<Textarea
|
||||
rows={5}
|
||||
@@ -47,7 +91,7 @@ const ModelEditForm = ({ formHooks }: { formHooks: UseFormReturn<ModelSchema> })
|
||||
{...register('intro')}
|
||||
placeholder={'模型的介绍,仅做展示,不影响模型的效果'}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormControl> */}
|
||||
</Card>
|
||||
<Card p={4}>
|
||||
<Box fontWeight={'bold'}>模型效果</Box>
|
||||
@@ -94,18 +138,27 @@ const ModelEditForm = ({ formHooks }: { formHooks: UseFormReturn<ModelSchema> })
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<Box mt={4}>
|
||||
<Box mb={1}>系统提示词</Box>
|
||||
<Textarea
|
||||
rows={6}
|
||||
maxLength={500}
|
||||
{...register('systemPrompt')}
|
||||
placeholder={
|
||||
'模型默认的 prompt 词,通过调整该内容,可以生成一个限定范围的模型。\n\n注意,改功能会影响对话的整体朝向!'
|
||||
}
|
||||
/>
|
||||
{canTrain ? (
|
||||
<Box fontWeight={'bold'}>
|
||||
训练的模型会自动根据知识库内容回答,无法设置系统prompt。注意:
|
||||
使用该模型,在对话时需要消耗等多的 tokens
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Box mb={1}>系统提示词</Box>
|
||||
<Textarea
|
||||
rows={6}
|
||||
maxLength={-1}
|
||||
{...register('systemPrompt')}
|
||||
placeholder={
|
||||
'模型默认的 prompt 词,通过调整该内容,可以生成一个限定范围的模型。\n\n注意,改功能会影响对话的整体朝向!'
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Card>
|
||||
<Card p={4}>
|
||||
{/* <Card p={4}>
|
||||
<Box fontWeight={'bold'}>安全策略</Box>
|
||||
<FormControl mt={2}>
|
||||
<Flex alignItems={'center'}>
|
||||
@@ -201,7 +254,8 @@ const ModelEditForm = ({ formHooks }: { formHooks: UseFormReturn<ModelSchema> })
|
||||
<Box ml={3}>次</Box>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
</Card>
|
||||
</Card> */}
|
||||
<ConfirmChild />
|
||||
</>
|
||||
);
|
||||
};
|
||||
155
src/pages/model/detail/components/SelectFileModal.tsx
Normal file
155
src/pages/model/detail/components/SelectFileModal.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody
|
||||
} from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useSelectFile } from '@/hooks/useSelectFile';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { encode } from 'gpt-token-utils';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { readTxtContent, readPdfContent, readDocContent } from '@/utils/tools';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { postModelDataFileText } from '@/api/model';
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
|
||||
|
||||
const fileExtension = '.txt,.doc,.docx,.pdf,.md';
|
||||
|
||||
const SelectFileModal = ({
|
||||
onClose,
|
||||
onSuccess,
|
||||
modelId
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
modelId: string;
|
||||
}) => {
|
||||
const [selecting, setSelecting] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const { File, onOpen } = useSelectFile({ fileType: fileExtension, multiple: true });
|
||||
const [fileText, setFileText] = useState('');
|
||||
const { openConfirm, ConfirmChild } = useConfirm({
|
||||
content: '确认导入该文件,需要一定时间进行拆解,该任务无法终止!如果余额不足,任务讲被终止。'
|
||||
});
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
async (e: File[]) => {
|
||||
setSelecting(true);
|
||||
try {
|
||||
const fileTexts = (
|
||||
await Promise.all(
|
||||
e.map((file) => {
|
||||
// @ts-ignore
|
||||
const extension = file?.name?.split('.').pop().toLowerCase();
|
||||
switch (extension) {
|
||||
case 'txt':
|
||||
case 'md':
|
||||
return readTxtContent(file);
|
||||
case 'pdf':
|
||||
return readPdfContent(file);
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return readDocContent(file);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
.join('\n')
|
||||
.replace(/\n+/g, '\n');
|
||||
setFileText(fileTexts);
|
||||
console.log(encode(fileTexts));
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
toast({
|
||||
title: typeof error === 'string' ? error : '解析文件失败',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
setSelecting(false);
|
||||
},
|
||||
[setSelecting, toast]
|
||||
);
|
||||
|
||||
const { mutate, isLoading } = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!fileText) return;
|
||||
await postModelDataFileText(modelId, fileText);
|
||||
toast({
|
||||
title: '导入数据成功,需要一段拆解和训练',
|
||||
status: 'success'
|
||||
});
|
||||
onClose();
|
||||
onSuccess();
|
||||
},
|
||||
onError() {
|
||||
toast({
|
||||
title: '导入文件失败',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxW={'min(900px, 90vw)'} position={'relative'}>
|
||||
<ModalHeader>文件导入</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
|
||||
<ModalBody>
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
p={2}
|
||||
h={'100%'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Button isLoading={selecting} onClick={onOpen}>
|
||||
选择文件
|
||||
</Button>
|
||||
<Box mt={2}>支持 {fileExtension} 文件. 会先对文本进行拆分,需要时间较长。</Box>
|
||||
<Box mt={2}>
|
||||
一共 {fileText.length} 个字,{encode(fileText).length} 个tokens
|
||||
</Box>
|
||||
<Box
|
||||
h={'300px'}
|
||||
w={'100%'}
|
||||
overflow={'auto'}
|
||||
p={2}
|
||||
backgroundColor={'blackAlpha.50'}
|
||||
whiteSpace={'pre'}
|
||||
fontSize={'xs'}
|
||||
>
|
||||
{fileText}
|
||||
</Box>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
|
||||
<Flex px={6} pt={2} pb={4}>
|
||||
<Box flex={1}></Box>
|
||||
<Button variant={'outline'} mr={3} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button isLoading={isLoading} isDisabled={fileText === ''} onClick={openConfirm(mutate)}>
|
||||
确认导入
|
||||
</Button>
|
||||
</Flex>
|
||||
</ModalContent>
|
||||
<ConfirmChild />
|
||||
<File onSelect={onSelectFile} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectFileModal;
|
||||
@@ -1,47 +1,35 @@
|
||||
import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
getModelById,
|
||||
delModelById,
|
||||
postTrainModel,
|
||||
putModelTrainingStatus,
|
||||
putModelById
|
||||
} from '@/api/model';
|
||||
import { getModelById, delModelById, putModelTrainingStatus, putModelById } from '@/api/model';
|
||||
import { getChatSiteId } from '@/api/chat';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { Card, Box, Flex, Button, Tag, Grid } from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { formatModelStatus, ModelStatusEnum, ModelList, defaultModel } from '@/constants/model';
|
||||
import { formatModelStatus, ModelStatusEnum, modelList, defaultModel } from '@/constants/model';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
import ModelEditForm from './components/ModelEditForm';
|
||||
import Icon from '@/components/Iconfont';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const Training = dynamic(() => import('./components/Training'));
|
||||
const ModelDataCard = dynamic(() => import('./components/ModelDataCard'));
|
||||
|
||||
const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { isPc, media } = useScreen();
|
||||
const { setLoading } = useGlobalStore();
|
||||
const { openConfirm, ConfirmChild } = useConfirm({
|
||||
content: '确认删除该模型?'
|
||||
});
|
||||
const SelectFileDom = useRef<HTMLInputElement>(null);
|
||||
|
||||
// const SelectFileDom = useRef<HTMLInputElement>(null);
|
||||
const [model, setModel] = useState<ModelSchema>(defaultModel);
|
||||
const formHooks = useForm<ModelSchema>({
|
||||
defaultValues: model
|
||||
});
|
||||
|
||||
const canTrain = useMemo(() => {
|
||||
const openai = ModelList[model.service.company].find(
|
||||
(item) => item.model === model?.service.modelName
|
||||
);
|
||||
return openai && openai.trainName;
|
||||
const openai = modelList.find((item) => item.model === model?.service.modelName);
|
||||
return !!(openai && openai.trainName);
|
||||
}, [model]);
|
||||
|
||||
/* 加载模型数据 */
|
||||
@@ -93,34 +81,34 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
}, [setLoading, model, router]);
|
||||
|
||||
/* 上传数据集,触发微调 */
|
||||
const startTraining = useCallback(
|
||||
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!modelId || !e.target.files || e.target.files?.length === 0) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const file = e.target.files[0];
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
await postTrainModel(modelId, formData);
|
||||
// const startTraining = useCallback(
|
||||
// async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// if (!modelId || !e.target.files || e.target.files?.length === 0) return;
|
||||
// setLoading(true);
|
||||
// try {
|
||||
// const file = e.target.files[0];
|
||||
// const formData = new FormData();
|
||||
// formData.append('file', file);
|
||||
// await postTrainModel(modelId, formData);
|
||||
|
||||
toast({
|
||||
title: '开始训练,大约需要 30 分钟',
|
||||
status: 'success'
|
||||
});
|
||||
// toast({
|
||||
// title: '开始训练...',
|
||||
// status: 'success'
|
||||
// });
|
||||
|
||||
// 重新获取模型
|
||||
loadModel();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: typeof err === 'string' ? err : '文件格式错误',
|
||||
status: 'error'
|
||||
});
|
||||
console.log('error->', err);
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
[setLoading, loadModel, modelId, toast]
|
||||
);
|
||||
// // 重新获取模型
|
||||
// loadModel();
|
||||
// } catch (err: any) {
|
||||
// toast({
|
||||
// title: err?.message || '上传文件失败',
|
||||
// status: 'error'
|
||||
// });
|
||||
// console.log('error->', err);
|
||||
// }
|
||||
// setLoading(false);
|
||||
// },
|
||||
// [setLoading, loadModel, modelId, toast]
|
||||
// );
|
||||
|
||||
/* 点击更新模型状态 */
|
||||
const handleClickUpdateStatus = useCallback(async () => {
|
||||
@@ -128,7 +116,12 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await putModelTrainingStatus(model._id);
|
||||
const res = await putModelTrainingStatus(model._id);
|
||||
typeof res === 'string' &&
|
||||
toast({
|
||||
title: res,
|
||||
status: 'info'
|
||||
});
|
||||
loadModel();
|
||||
} catch (error: any) {
|
||||
console.log('error->', error);
|
||||
@@ -197,7 +190,7 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
return () => {
|
||||
window.onbeforeunload = null;
|
||||
};
|
||||
}, []);
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -247,72 +240,33 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
)}
|
||||
</Card>
|
||||
<Grid mt={5} gridTemplateColumns={media('1fr 1fr', '1fr')} gridGap={5}>
|
||||
<ModelEditForm formHooks={formHooks} />
|
||||
<Card p={4}>{!!model && <Training model={model} />}</Card>
|
||||
<Card p={4}>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'}>
|
||||
神奇操作
|
||||
</Box>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>模型微调:</Box>
|
||||
<Button
|
||||
size={'sm'}
|
||||
onClick={() => {
|
||||
SelectFileDom.current?.click();
|
||||
}}
|
||||
title={!canTrain ? '' : '模型不支持微调'}
|
||||
isDisabled={!canTrain}
|
||||
>
|
||||
上传微调数据集
|
||||
</Button>
|
||||
<Flex
|
||||
as={'a'}
|
||||
href="/TrainingTemplate.jsonl"
|
||||
download
|
||||
ml={5}
|
||||
cursor={'pointer'}
|
||||
alignItems={'center'}
|
||||
color={'blue.500'}
|
||||
>
|
||||
<Icon name={'icon-yunxiazai'} color={'#3182ce'} />
|
||||
下载模板
|
||||
</Flex>
|
||||
</Flex>
|
||||
{/* 提示 */}
|
||||
<Box mt={3} py={3} color={'blackAlpha.500'}>
|
||||
<Box as={'li'} lineHeight={1.9}>
|
||||
每行包括一个 prompt 和一个 completion
|
||||
</Box>
|
||||
<Box as={'li'} lineHeight={1.9}>
|
||||
prompt 必须以 \n\n###\n\n 结尾,且尽量保障每个 prompt
|
||||
内容不都是同一个标点结尾,可以加一个空格打断相同性,
|
||||
</Box>
|
||||
<Box as={'li'} lineHeight={1.9}>
|
||||
completion 开头必须有一个空格,末尾必须以 ### 结尾,同样的不要都是同一个标点结尾。
|
||||
</Box>
|
||||
</Box>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>删除模型:</Box>
|
||||
<Button
|
||||
colorScheme={'red'}
|
||||
size={'sm'}
|
||||
onClick={() => {
|
||||
openConfirm(() => {
|
||||
handleDelModel();
|
||||
});
|
||||
}}
|
||||
>
|
||||
删除模型
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
<ModelEditForm formHooks={formHooks} handleDelModel={handleDelModel} canTrain={canTrain} />
|
||||
|
||||
{/* {canTrain && (
|
||||
<Card p={4}>
|
||||
<Training model={model} />
|
||||
</Card>
|
||||
)} */}
|
||||
{canTrain && model._id && (
|
||||
<Card
|
||||
p={4}
|
||||
{...media(
|
||||
{
|
||||
gridColumnStart: 1,
|
||||
gridColumnEnd: 3
|
||||
},
|
||||
{}
|
||||
)}
|
||||
>
|
||||
<ModelDataCard model={model} />
|
||||
</Card>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{/* 文件选择 */}
|
||||
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
|
||||
{/* <Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
|
||||
<input ref={SelectFileDom} type="file" accept=".jsonl" onChange={startTraining} />
|
||||
</Box>
|
||||
<ConfirmChild />
|
||||
</Box> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user