Compare commits

..

31 Commits

Author SHA1 Message Date
archer
84daf85393 fix: base url 2023-06-18 22:38:55 +08:00
archer
6c62d80a4c fix: refresh page 2023-06-18 22:19:49 +08:00
archer
ff2043c0fb feat: maxToken setting 2023-06-18 21:23:36 +08:00
archer
ee9afa310a feat: openapi v2 chat 2023-06-18 21:06:07 +08:00
archer
2b93ae2d00 fix: time conf 2023-06-17 21:53:04 +08:00
archer
00c93a63cd perf: queue link 2023-06-17 21:27:44 +08:00
archer
61447c60ac feat: new app page 2023-06-17 17:31:38 +08:00
archer
df2fda6176 feat: auth key 2023-06-16 00:26:11 +08:00
archer
bc2504832f fix: nextjs version 2023-06-16 00:03:52 +08:00
archer
33ffd9d7dd loading 2023-06-15 22:36:09 +08:00
archer
80578a08c8 perf: app store 2023-06-15 22:17:54 +08:00
archer
2463e11cb9 feat: date picker 2023-06-15 21:44:31 +08:00
archer
4cbe4ebdc3 perf: image 2023-06-15 20:06:56 +08:00
archer
bb36e637e0 perf: code 2023-06-15 17:32:35 +08:00
archer
6f9e929298 perf: code 2023-06-15 17:32:12 +08:00
archer
bf1592d2c6 feat: admin set share 2023-06-15 00:21:27 +08:00
archer
c6259fca78 perf: export source 2023-06-14 23:14:26 +08:00
archer
cf3eb3b7b5 perf: upload img 2023-06-14 22:45:47 +08:00
archer
7c52cec0ea perf: binary avatar 2023-06-14 22:26:11 +08:00
archer
7c159d8aba fix: markdown 2023-06-14 20:58:11 +08:00
archer
07f8e18c10 fix: gpt35 4k 2023-06-14 20:54:34 +08:00
archer
e4aeee7be3 perf: token count 2023-06-14 20:02:43 +08:00
archer
8036ed6143 perf: qa 2023-06-14 14:33:26 +08:00
archer
85e6a0f38d fix: token limit 2023-06-14 10:01:00 +08:00
archer
dab70378bb feat: gpt35-16k 2023-06-14 09:45:49 +08:00
archer
0a0febd2e6 perf: admin 2023-06-14 00:24:50 +08:00
archer
391332c8dd perf: ssr 2023-06-13 20:07:32 +08:00
archer
89e7c1abca perf: admin 2023-06-13 11:49:26 +08:00
archer
fc3c360985 fix: context menu 2023-06-13 10:52:44 +08:00
archer
006ba3b877 fix: mermaid 2023-06-12 23:17:48 +08:00
archer
5a534aa630 perf: del loading 2023-06-12 22:12:29 +08:00
126 changed files with 3625 additions and 2277 deletions

View File

@@ -42,6 +42,12 @@ Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接
- [FastGpt V3.4 更新集合](https://www.bilibili.com/video/BV1Lo4y147Qh/?vd_source=92041a1a395f852f9d89158eaa3f61b4)
- [FastGpt 知识库演示](https://www.bilibili.com/video/BV1Wo4y1p7i1/)
## Powered by
- [TuShan 5 分钟搭建后台管理系统](https://github.com/msgbyte/tushan)
- [Laf 3 分钟快速接入三方应用](https://github.com/labring/laf)
- [Sealos 快速部署集群应用](https://github.com/labring/sealos)
## 🌟 Star History
[![Star History Chart](https://api.star-history.com/svg?repos=c121914yu/FastGPT&type=Date)](https://star-history.com/#c121914yu/FastGPT&Date)

View File

@@ -25,7 +25,7 @@
"react-admin": "^4.11.0",
"react-dom": "^18.2.0",
"react-i18next": "^12.3.1",
"tushan": "^0.2.22"
"tushan": "^0.2.23"
},
"devDependencies": {
"@types/jsonexport": "^3.0.2",

137
admin/pnpm-lock.yaml generated
View File

@@ -1,8 +1,4 @@
lockfileVersion: '6.1'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
lockfileVersion: '6.0'
dependencies:
'@arco-design/web-react':
@@ -46,10 +42,10 @@ dependencies:
version: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0)
react-i18next:
specifier: ^12.3.1
version: registry.npmmirror.com/react-i18next@12.3.1(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0)
version: registry.npmmirror.com/react-i18next@12.3.1(react-dom@18.2.0)(react@18.2.0)
tushan:
specifier: ^0.2.22
version: registry.npmmirror.com/tushan@0.2.22(history@5.3.0)(prop-types@15.8.1)(react-hook-form@7.44.3)
specifier: ^0.2.23
version: registry.npmmirror.com/tushan@0.2.23
devDependencies:
'@types/jsonexport':
@@ -167,10 +163,10 @@ packages:
'@babel/helpers': registry.npmmirror.com/@babel/helpers@7.22.5
'@babel/parser': registry.npmmirror.com/@babel/parser@7.22.5
'@babel/template': registry.npmmirror.com/@babel/template@7.22.5
'@babel/traverse': registry.npmmirror.com/@babel/traverse@7.22.5(supports-color@5.5.0)
'@babel/traverse': registry.npmmirror.com/@babel/traverse@7.22.5
'@babel/types': registry.npmmirror.com/@babel/types@7.22.5
convert-source-map: registry.npmmirror.com/convert-source-map@1.9.0
debug: registry.npmmirror.com/debug@4.3.4(supports-color@5.5.0)
debug: registry.npmmirror.com/debug@4.3.4
gensync: registry.npmmirror.com/gensync@1.0.0-beta.2
json5: registry.npmmirror.com/json5@2.2.3
semver: registry.npmmirror.com/semver@6.3.0
@@ -258,7 +254,7 @@ packages:
'@babel/helper-split-export-declaration': registry.npmmirror.com/@babel/helper-split-export-declaration@7.22.5
'@babel/helper-validator-identifier': registry.npmmirror.com/@babel/helper-validator-identifier@7.22.5
'@babel/template': registry.npmmirror.com/@babel/template@7.22.5
'@babel/traverse': registry.npmmirror.com/@babel/traverse@7.22.5(supports-color@5.5.0)
'@babel/traverse': registry.npmmirror.com/@babel/traverse@7.22.5
'@babel/types': registry.npmmirror.com/@babel/types@7.22.5
transitivePeerDependencies:
- supports-color
@@ -314,7 +310,7 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
'@babel/template': registry.npmmirror.com/@babel/template@7.22.5
'@babel/traverse': registry.npmmirror.com/@babel/traverse@7.22.5(supports-color@5.5.0)
'@babel/traverse': registry.npmmirror.com/@babel/traverse@7.22.5
'@babel/types': registry.npmmirror.com/@babel/types@7.22.5
transitivePeerDependencies:
- supports-color
@@ -384,6 +380,26 @@ packages:
'@babel/parser': registry.npmmirror.com/@babel/parser@7.22.5
'@babel/types': registry.npmmirror.com/@babel/types@7.22.5
registry.npmmirror.com/@babel/traverse@7.22.5:
resolution: {integrity: sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@babel/traverse/-/traverse-7.22.5.tgz}
name: '@babel/traverse'
version: 7.22.5
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': registry.npmmirror.com/@babel/code-frame@7.22.5
'@babel/generator': registry.npmmirror.com/@babel/generator@7.22.5
'@babel/helper-environment-visitor': registry.npmmirror.com/@babel/helper-environment-visitor@7.22.5
'@babel/helper-function-name': registry.npmmirror.com/@babel/helper-function-name@7.22.5
'@babel/helper-hoist-variables': registry.npmmirror.com/@babel/helper-hoist-variables@7.22.5
'@babel/helper-split-export-declaration': registry.npmmirror.com/@babel/helper-split-export-declaration@7.22.5
'@babel/parser': registry.npmmirror.com/@babel/parser@7.22.5
'@babel/types': registry.npmmirror.com/@babel/types@7.22.5
debug: registry.npmmirror.com/debug@4.3.4
globals: registry.npmmirror.com/globals@11.12.0
transitivePeerDependencies:
- supports-color
dev: true
registry.npmmirror.com/@babel/traverse@7.22.5(supports-color@5.5.0):
resolution: {integrity: sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@babel/traverse/-/traverse-7.22.5.tgz}
id: registry.npmmirror.com/@babel/traverse/7.22.5
@@ -403,6 +419,7 @@ packages:
globals: registry.npmmirror.com/globals@11.12.0
transitivePeerDependencies:
- supports-color
dev: false
registry.npmmirror.com/@babel/types@7.22.5:
resolution: {integrity: sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@babel/types/-/types-7.22.5.tgz}
@@ -2099,6 +2116,19 @@ packages:
supports-color: registry.npmmirror.com/supports-color@5.5.0
dev: false
registry.npmmirror.com/debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz}
name: debug
version: 4.3.4
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: registry.npmmirror.com/ms@2.1.2
registry.npmmirror.com/debug@4.3.4(supports-color@5.5.0):
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz}
id: registry.npmmirror.com/debug/4.3.4
@@ -2113,6 +2143,7 @@ packages:
dependencies:
ms: registry.npmmirror.com/ms@2.1.2
supports-color: registry.npmmirror.com/supports-color@5.5.0
dev: false
registry.npmmirror.com/decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz}
@@ -3459,7 +3490,7 @@ packages:
version: 5.0.0
engines: {node: '>=14.0.0'}
dependencies:
debug: registry.npmmirror.com/debug@4.3.4(supports-color@5.5.0)
debug: registry.npmmirror.com/debug@4.3.4
transitivePeerDependencies:
- supports-color
dev: false
@@ -3947,14 +3978,45 @@ packages:
- react-native
dev: false
registry.npmmirror.com/ra-data-json-server@4.11.1(history@5.3.0)(react-dom@18.2.0)(react-hook-form@7.44.3)(react-router-dom@6.12.1)(react-router@6.12.1)(react@18.2.0):
registry.npmmirror.com/ra-core@4.11.1(react-dom@18.2.0)(react-router-dom@6.12.1)(react-router@6.12.1)(react@18.2.0):
resolution: {integrity: sha512-nqVe++/BvGJpxsfz1HRZbAtoualhbx9UHAYT6n1IekuW5TZ0s86Zj5fRPS4lw2r12a3VR+rsACW3d0zexzIyXg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ra-core/-/ra-core-4.11.1.tgz}
id: registry.npmmirror.com/ra-core/4.11.1
name: ra-core
version: 4.11.1
peerDependencies:
history: ^5.1.0
react: ^16.9.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.9.0 || ^17.0.0 || ^18.0.0
react-hook-form: ^7.43.9
react-router: ^6.1.0
react-router-dom: ^6.1.0
dependencies:
clsx: registry.npmmirror.com/clsx@1.2.1
date-fns: registry.npmmirror.com/date-fns@2.30.0
eventemitter3: registry.npmmirror.com/eventemitter3@4.0.7
inflection: registry.npmmirror.com/inflection@1.12.0
jsonexport: registry.npmmirror.com/jsonexport@3.2.0
lodash: registry.npmmirror.com/lodash@4.17.21
prop-types: registry.npmmirror.com/prop-types@15.8.1
query-string: registry.npmmirror.com/query-string@7.1.3
react: registry.npmmirror.com/react@18.2.0
react-dom: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0)
react-is: registry.npmmirror.com/react-is@17.0.2
react-query: registry.npmmirror.com/react-query@3.39.3(react-dom@18.2.0)(react@18.2.0)
react-router: registry.npmmirror.com/react-router@6.12.1(react@18.2.0)
react-router-dom: registry.npmmirror.com/react-router-dom@6.12.1(react-dom@18.2.0)(react@18.2.0)
transitivePeerDependencies:
- react-native
dev: false
registry.npmmirror.com/ra-data-json-server@4.11.1(react-dom@18.2.0)(react-router-dom@6.12.1)(react-router@6.12.1)(react@18.2.0):
resolution: {integrity: sha512-EE+1Sl2uJfTAhuJPVOPbelkB3JvmSFw0aN45kOpzMcDm8IdWrzMl5I5qHqB7/qV/UrAgBDs0uK0nqg9b6Im6Bw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ra-data-json-server/-/ra-data-json-server-4.11.1.tgz}
id: registry.npmmirror.com/ra-data-json-server/4.11.1
name: ra-data-json-server
version: 4.11.1
dependencies:
query-string: registry.npmmirror.com/query-string@7.1.3
ra-core: registry.npmmirror.com/ra-core@4.11.1(history@5.3.0)(react-dom@18.2.0)(react-hook-form@7.44.3)(react-router-dom@6.12.1)(react-router@6.12.1)(react@18.2.0)
ra-core: registry.npmmirror.com/ra-core@4.11.1(react-dom@18.2.0)(react-router-dom@6.12.1)(react-router@6.12.1)(react@18.2.0)
transitivePeerDependencies:
- history
- react
@@ -4230,6 +4292,28 @@ packages:
react-dom: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0)
dev: false
registry.npmmirror.com/react-i18next@12.3.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-5v8E2XjZDFzK7K87eSwC7AJcAkcLt5xYZ4+yTPDAW1i7C93oOY1dnr4BaQM7un4Hm+GmghuiPvevWwlca5PwDA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/react-i18next/-/react-i18next-12.3.1.tgz}
id: registry.npmmirror.com/react-i18next/12.3.1
name: react-i18next
version: 12.3.1
peerDependencies:
i18next: '>= 19.0.0'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@babel/runtime': registry.npmmirror.com/@babel/runtime@7.22.5
html-parse-stringify: registry.npmmirror.com/html-parse-stringify@3.0.1
react: registry.npmmirror.com/react@18.2.0
react-dom: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0)
dev: false
registry.npmmirror.com/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz}
name: react-is
@@ -4356,7 +4440,7 @@ packages:
react: registry.npmmirror.com/react@18.2.0
dev: false
registry.npmmirror.com/react-smooth@2.0.3(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0):
registry.npmmirror.com/react-smooth@2.0.3(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-yl4y3XiMorss7ayF5QnBiSprig0+qFHui8uh7Hgg46QX5O+aRMRKlfGGNGLHno35JkQSvSYY8eCWkBfHfrSHfg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/react-smooth/-/react-smooth-2.0.3.tgz}
id: registry.npmmirror.com/react-smooth/2.0.3
name: react-smooth
@@ -4367,7 +4451,6 @@ packages:
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
fast-equals: registry.npmmirror.com/fast-equals@5.0.1
prop-types: registry.npmmirror.com/prop-types@15.8.1
react: registry.npmmirror.com/react@18.2.0
react-dom: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0)
react-transition-group: registry.npmmirror.com/react-transition-group@2.9.0(react-dom@18.2.0)(react@18.2.0)
@@ -4458,7 +4541,7 @@ packages:
decimal.js-light: registry.npmmirror.com/decimal.js-light@2.5.1
dev: false
registry.npmmirror.com/recharts@2.6.2(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0):
registry.npmmirror.com/recharts@2.6.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-dVhNfgI21LlF+4AesO3mj+i+9YdAAjoGaDWIctUgH/G2iy14YVtb/DSUeic77xr19rbKCiq+pQGfeg2kJQDHig==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/recharts/-/recharts-2.6.2.tgz}
id: registry.npmmirror.com/recharts/2.6.2
name: recharts
@@ -4472,12 +4555,11 @@ packages:
classnames: registry.npmmirror.com/classnames@2.3.2
eventemitter3: registry.npmmirror.com/eventemitter3@4.0.7
lodash: registry.npmmirror.com/lodash@4.17.21
prop-types: registry.npmmirror.com/prop-types@15.8.1
react: registry.npmmirror.com/react@18.2.0
react-dom: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0)
react-is: registry.npmmirror.com/react-is@16.13.1
react-resize-detector: registry.npmmirror.com/react-resize-detector@8.1.0(react-dom@18.2.0)(react@18.2.0)
react-smooth: registry.npmmirror.com/react-smooth@2.0.3(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0)
react-smooth: registry.npmmirror.com/react-smooth@2.0.3(react-dom@18.2.0)(react@18.2.0)
recharts-scale: registry.npmmirror.com/recharts-scale@0.4.5
reduce-css-calc: registry.npmmirror.com/reduce-css-calc@2.1.8
victory-vendor: registry.npmmirror.com/victory-vendor@36.6.10
@@ -5096,11 +5178,10 @@ packages:
version: 2.5.3
dev: false
registry.npmmirror.com/tushan@0.2.22(history@5.3.0)(prop-types@15.8.1)(react-hook-form@7.44.3):
resolution: {integrity: sha512-b+FOFKZduo6GjTS+AfUym4ipP8vmtQFVLETEmhyLO7/awcSXhxQsU3UBWVIbanLOXGOuvkRrACi1Iuk35iXydw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/tushan/-/tushan-0.2.22.tgz}
id: registry.npmmirror.com/tushan/0.2.22
registry.npmmirror.com/tushan@0.2.23:
resolution: {integrity: sha512-1qPuAyaJbw14Hqn298aGvPhlF/qIo9ZgKp/0RDB5ZQ9OzcATxaoug9znTQGJd8aec5xM07T6lW6mjsFHpUh+tA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/tushan/-/tushan-0.2.23.tgz}
name: tushan
version: 0.2.22
version: 0.2.23
dependencies:
'@arco-design/web-react': registry.npmmirror.com/@arco-design/web-react@2.49.1(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)
'@tanstack/react-query': registry.npmmirror.com/@tanstack/react-query@4.29.12(react-dom@18.2.0)(react@18.2.0)
@@ -5123,7 +5204,7 @@ packages:
lodash-es: registry.npmmirror.com/lodash-es@4.17.21
postcss: registry.npmmirror.com/postcss@8.4.24
qs: registry.npmmirror.com/qs@6.11.2
ra-data-json-server: registry.npmmirror.com/ra-data-json-server@4.11.1(history@5.3.0)(react-dom@18.2.0)(react-hook-form@7.44.3)(react-router-dom@6.12.1)(react-router@6.12.1)(react@18.2.0)
ra-data-json-server: registry.npmmirror.com/ra-data-json-server@4.11.1(react-dom@18.2.0)(react-router-dom@6.12.1)(react-router@6.12.1)(react@18.2.0)
react: registry.npmmirror.com/react@18.2.0
react-dom: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0)
react-helmet: registry.npmmirror.com/react-helmet@6.1.0(react@18.2.0)
@@ -5132,7 +5213,7 @@ packages:
react-json-view: registry.npmmirror.com/react-json-view@1.21.3(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)
react-router: registry.npmmirror.com/react-router@6.12.1(react@18.2.0)
react-router-dom: registry.npmmirror.com/react-router-dom@6.12.1(react-dom@18.2.0)(react@18.2.0)
recharts: registry.npmmirror.com/recharts@2.6.2(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0)
recharts: registry.npmmirror.com/recharts@2.6.2(react-dom@18.2.0)(react@18.2.0)
styled-components: registry.npmmirror.com/styled-components@5.3.11(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)
tailwindcss: registry.npmmirror.com/tailwindcss@3.3.2
url-regex: registry.npmmirror.com/url-regex@5.0.0
@@ -5572,3 +5653,7 @@ packages:
react: registry.npmmirror.com/react@18.2.0
use-sync-external-store: registry.npmmirror.com/use-sync-external-store@1.2.0(react@18.2.0)
dev: false
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false

View File

@@ -15,6 +15,15 @@ useAppRoute(app);
useKbRoute(app);
useSystemRoute(app);
app.get('/*', (req, res) => {
res.sendFile(new URL('dist/index.html', import.meta.url).pathname);
});
app.use((err, req, res, next) => {
res.sendFile(new URL('dist/index.html', import.meta.url).pathname);
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);

View File

@@ -1,4 +1,4 @@
import { User, Model, Kb } from '../schema.js';
import { Model, Kb } from '../schema.js';
import { auth } from './system.js';
export const useAppRoute = (app) => {
@@ -8,18 +8,19 @@ export const useAppRoute = (app) => {
const start = parseInt(req.query._start) || 0;
const end = parseInt(req.query._end) || 20;
const order = req.query._order === 'DESC' ? -1 : 1;
const sort = req.query._sort || '_id';
const userId = req.query.userId || '';
const sort = req.query._sort;
const name = req.query.name || '';
const id = req.query.id || '';
const where = {
...(userId ? { userId: userId } : {}),
name
...(name && { name: { $regex: name, $options: 'i' } }),
...(id && { _id: id })
};
const modelsRaw = await Model.find()
const modelsRaw = await Model.find(where)
.skip(start)
.limit(end - start)
.sort({ [sort]: order });
.sort({ [sort]: order, 'share.isShare': -1, 'share.collection': -1 });
const models = [];
@@ -37,15 +38,19 @@ export const useAppRoute = (app) => {
id: model._id.toString(),
userId: model.userId,
name: model.name,
intro: model.intro,
model: model.chat?.chatModel,
relatedKbs: kbNames, // 将relatedKbs的id转换为相应的Kb名称
searchMode: model.chat?.searchMode,
systemPrompt: model.chat?.systemPrompt || '',
temperature: model.chat?.temperature
temperature: model.chat?.temperature || 0,
'share.topNum': model.share?.topNum || 0,
'share.isShare': model.share?.isShare || false,
'share.collection': model.share?.collection || 0
};
models.push(orderedModel);
}
const totalCount = await Model.countDocuments();
const totalCount = await Model.countDocuments(where);
res.header('Access-Control-Expose-Headers', 'X-Total-Count');
res.header('X-Total-Count', totalCount);
res.json(models);
@@ -54,4 +59,29 @@ export const useAppRoute = (app) => {
res.status(500).json({ error: 'Error fetching models', details: err.message });
}
});
// 修改 app 信息
app.put('/models/:id', auth(), async (req, res) => {
try {
const _id = req.params.id;
let {
share: { isShare, topNum },
intro
} = req.body;
await Model.findByIdAndUpdate(_id, {
$set: {
intro: intro,
'share.topNum': Number(topNum),
'share.isShare': isShare === 'true' || isShare === true
}
});
res.json({});
} catch (err) {
console.log(`Error updating user: ${err}`);
res.status(500).json({ error: 'Error updating user' });
}
});
};

View File

@@ -10,7 +10,21 @@ export const useKbRoute = (app) => {
const order = req.query._order === 'DESC' ? -1 : 1;
const sort = req.query._sort || '_id';
const tag = req.query.tag || '';
const where = { tags: { $elemMatch: { $regex: tag, $options: 'i' } } };
const name = req.query.name || '';
const where = {
...(name
? {
name: { $regex: name, $options: 'i' }
}
: {}),
...(tag
? {
tags: { $elemMatch: { $regex: tag, $options: 'i' } }
}
: {})
};
console.log(where);
const kbsRaw = await Kb.find(where)
.skip(start)

View File

@@ -110,8 +110,7 @@ export const auth = () => {
try {
const authorization = req.headers.authorization;
if (!authorization) {
res.status(401).end('not found authorization in headers');
return;
return next(new Error("unAuthorization"))
}
const token = authorization.slice('Bearer '.length);

View File

@@ -9,6 +9,52 @@ const hashPassword = (psw) => {
};
export const useUserRoute = (app) => {
// 统计近 30 天注册用户数量
app.get('/users/data', auth(), async (req, res) => {
try {
const day = 60;
let startCount = await User.countDocuments({
createTime: { $lt: new Date(Date.now() - day * 24 * 60 * 60 * 1000) }
});
const usersRaw = await User.aggregate([
{ $match: { createTime: { $gte: new Date(Date.now() - day * 24 * 60 * 60 * 1000) } } },
{
$group: {
_id: {
year: { $year: '$createTime' },
month: { $month: '$createTime' },
day: { $dayOfMonth: '$createTime' }
},
count: { $sum: 1 }
}
},
{
$project: {
_id: 0,
date: { $dateFromParts: { year: '$_id.year', month: '$_id.month', day: '$_id.day' } },
count: 1
}
},
{ $sort: { date: 1 } }
]);
const countResult = usersRaw.map((item) => {
const increaseRate = `${((item.count / startCount) * 100).toFixed(2)}%`;
startCount += item.count;
return {
date: item.date,
count: startCount,
increase: item.count,
increaseRate
};
});
res.json(countResult);
} catch (err) {
console.log(`Error fetching users: ${err}`);
res.status(500).json({ error: 'Error fetching users' });
}
});
// 获取用户列表
app.get('/users', auth(), async (req, res) => {
try {

View File

@@ -61,14 +61,15 @@ const modelSchema = new mongoose.Schema({
name: String,
avatar: String,
status: String,
intro: String,
chat: {
relatedKbs: [mongoose.Schema.Types.ObjectId],
searchMode: String,
systemPrompt: String,
temperature: Number,
chatModel: String
},
share: {
topNum: Number,
isShare: Boolean,
isShareDetail: Boolean,
intro: String,

View File

@@ -9,6 +9,7 @@ import {
import { authProvider } from './auth';
import { userFields, payFields, kbFields, ModelFields, SystemFields } from './fields';
import { Dashboard } from './Dashboard';
import { IconUser, IconApps, IconBook, IconStamp } from 'tushan/icon';
const authStorageKey = 'tushan:auth';
@@ -40,6 +41,7 @@ function App() {
<Resource
name="users"
label="用户信息"
icon={<IconUser />}
list={
<ListTable
filter={[
@@ -52,10 +54,29 @@ function App() {
/>
}
/>
<Resource
name="models"
icon={<IconApps />}
label="应用"
list={
<ListTable
filter={[
createTextField('id', {
label: 'id'
}),
createTextField('name', {
label: 'name'
})
]}
fields={ModelFields}
action={{ detail: true, edit: true }}
/>
}
/>
<Resource
name="pays"
label="支付记录"
icon={<IconStamp />}
list={
<ListTable
filter={[
@@ -71,9 +92,13 @@ function App() {
<Resource
name="kbs"
label="知识库"
icon={<IconBook />}
list={
<ListTable
filter={[
createTextField('name', {
label: 'name'
}),
createTextField('tag', {
label: 'tag'
})
@@ -83,11 +108,7 @@ function App() {
/>
}
/>
<Resource
name="models"
label="应用"
list={<ListTable fields={ModelFields} action={{ detail: true }} />}
/>
<Resource
name="system"
label="系统"

View File

@@ -2,22 +2,36 @@ import { Card, Link, Space, Grid, Divider, Typography } from '@arco-design/web-r
import { IconApps, IconUser, IconUserGroup } from 'tushan/icon';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
AreaChart,
Area
} from 'tushan/chart';
import dayjs from 'dayjs';
const authStorageKey = 'tushan:auth';
type UsersChartDataType = { count: number; date: string; increase: number; increaseRate: string };
export const Dashboard: React.FC = React.memo(() => {
const [userCount, setUserCount] = useState(0); //用户数量
const [kbCount, setkbCount] = useState(0);
const [modelCount, setmodelCount] = useState(0);
useEffect(() => {
const fetchCounts = async () => {
const baseUrl = import.meta.env.VITE_PUBLIC_SERVER_URL;
const { token } = JSON.parse(window.localStorage.getItem(authStorageKey) ?? '{}');
const [usersData, setUsersData] = useState<UsersChartDataType[]>([]);
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
};
useEffect(() => {
const baseUrl = import.meta.env.VITE_PUBLIC_SERVER_URL;
const { token } = JSON.parse(window.localStorage.getItem(authStorageKey) ?? '{}');
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
};
const fetchCounts = async () => {
const userResponse = await fetch(`${baseUrl}/users?_end=1`, {
headers
});
@@ -31,7 +45,6 @@ export const Dashboard: React.FC = React.memo(() => {
const userTotalCount = userResponse.headers.get('X-Total-Count');
const kbTotalCount = kbResponse.headers.get('X-Total-Count');
const modelTotalCount = modelResponse.headers.get('X-Total-Count');
console.log(userTotalCount);
if (userTotalCount) {
setUserCount(Number(userTotalCount));
@@ -43,8 +56,20 @@ export const Dashboard: React.FC = React.memo(() => {
setmodelCount(Number(modelTotalCount));
}
};
const fetchUserData = async () => {
const userResponse: UsersChartDataType[] = await fetch(`${baseUrl}/users/data`, {
headers
}).then((res) => res.json());
setUsersData(
userResponse.map((item) => ({
...item,
date: dayjs(item.date).format('MM/DD')
}))
);
};
fetchCounts();
fetchUserData();
}, []);
return (
@@ -71,11 +96,12 @@ export const Dashboard: React.FC = React.memo(() => {
<Divider type="vertical" style={{ height: 40 }} />
<Grid.Col flex={1} style={{ paddingLeft: '1rem' }}>
<DataItem icon={<IconApps />} title={'AI模型'} count={modelCount} />
<DataItem icon={<IconApps />} title={'应用'} count={modelCount} />
</Grid.Col>
</Grid.Row>
<Divider />
<UserChart data={usersData} />
</Card>
</Space>
</div>
@@ -84,38 +110,31 @@ export const Dashboard: React.FC = React.memo(() => {
});
Dashboard.displayName = 'Dashboard';
const DashboardItem: React.FC<
React.PropsWithChildren<{
title: string;
href?: string;
}>
> = React.memo((props) => {
const { t } = useTranslation();
const DashboardItem = React.memo(
(props: { title: string; href?: string; children: React.ReactNode }) => {
const { t } = useTranslation();
return (
<Card
title={props.title}
extra={
props.href && (
<Link target="_blank" href={props.href}>
{t('tushan.dashboard.more')}
</Link>
)
}
bordered={false}
style={{ overflow: 'hidden' }}
>
{props.children}
</Card>
);
});
return (
<Card
title={props.title}
extra={
props.href && (
<Link target="_blank" href={props.href}>
{t('tushan.dashboard.more')}
</Link>
)
}
bordered={false}
style={{ overflow: 'hidden' }}
>
{props.children}
</Card>
);
}
);
DashboardItem.displayName = 'DashboardItem';
const DataItem: React.FC<{
icon: React.ReactElement;
title: string;
count: number;
}> = React.memo((props) => {
const DataItem = React.memo((props: { icon: React.ReactElement; title: string; count: number }) => {
return (
<Space>
<div
@@ -141,3 +160,65 @@ const DataItem: React.FC<{
);
});
DataItem.displayName = 'DataItem';
const CustomTooltip = ({ active, payload }: any) => {
const data = payload?.[0]?.payload as UsersChartDataType;
if (active && data) {
return (
<div
style={{
background: 'white',
padding: '5px 8px',
borderRadius: '8px',
boxShadow: '2px 2px 5px rgba(0,0,0,0.2)'
}}
>
<p className="label">
count: <strong>{data.count}</strong>
</p>
<p className="label">
increase: <strong>{data.increase}</strong>
</p>
<p className="label">
increaseRate: <strong>{data.increaseRate}</strong>
</p>
</div>
);
}
return null;
};
const UserChart = ({ data }: { data: UsersChartDataType[] }) => {
return (
<ResponsiveContainer width="100%" height={320}>
<AreaChart
width={730}
height={250}
data={data}
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
>
<defs>
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#8884d8" stopOpacity={0.8} />
<stop offset="95%" stopColor="#8884d8" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorPv" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#82ca9d" stopOpacity={0.8} />
<stop offset="95%" stopColor="#82ca9d" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis dataKey="date" />
<YAxis />
<CartesianGrid strokeDasharray="3 3" />
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="count"
stroke="#82ca9d"
fillOpacity={1}
fill="url(#colorPv)"
/>
</AreaChart>
</ResponsiveContainer>
);
};

View File

@@ -2,7 +2,7 @@ import { createTextField, createNumberField } from 'tushan';
export const userFields = [
createTextField('id', { label: 'ID' }),
createTextField('username', { label: '用户名' }),
createTextField('username', { label: '用户名', edit: { hidden: true } }),
createNumberField('balance', { label: '余额', list: { sort: true } }),
createTextField('createTime', { label: 'Create Time', list: { sort: true } }),
createTextField('password', { label: '密码', list: { hidden: true } })
@@ -19,24 +19,29 @@ export const payFields = [
export const kbFields = [
createTextField('id', { label: 'ID' }),
createTextField('userId', { label: '所属用户' }),
createTextField('userId', { label: '所属用户', edit: { hidden: true } }),
createTextField('name', { label: '知识库' }),
createTextField('tags', { label: 'Tags' })
];
export const ModelFields = [
createTextField('id', { label: 'ID' }),
createTextField('userId', { label: '所属用户' }),
createTextField('userId', { label: '所属用户', list: { hidden: true }, edit: { hidden: true } }),
createTextField('name', { label: '名字' }),
createTextField('relatedKbs', { label: '引用的知识库' }),
createTextField('searchMode', { label: '搜索模式' }),
createTextField('model', { label: '模型', edit: { hidden: true } }),
createTextField('share.collection', { label: '收藏数', list: { sort: true } }),
createTextField('share.topNum', { label: '置顶等级', list: { sort: true } }),
createTextField('share.isShare', { label: '是否分享(true,false)' }),
createTextField('intro', { label: '介绍', list: { width: 400 } }),
createTextField('relatedKbs', { label: '引用的知识库', list: { hidden: true } }),
createTextField('temperature', { label: '温度' }),
createTextField('systemPrompt', {
label: '提示词',
list: {
width: 400
width: 400,
hidden: true
}
}),
createTextField('temperature', { label: '温度' })
})
];
export const SystemFields = [

View File

@@ -1,4 +1,5 @@
# 运行端口,如果不是 3000 口运行,需要改成其他的。注意:不是改了这个变量就会变成其他端口,而是因为改成其他端口,才用这个变量。
DB_MAX_LINK=15 # database max link
PORT=3000
# 代理
# AXIOS_PROXY_HOST=127.0.0.1

View File

@@ -15,7 +15,7 @@
"@chakra-ui/icons": "^2.0.17",
"@chakra-ui/react": "^2.7.0",
"@chakra-ui/system": "^2.5.8",
"@dqbd/tiktoken": "^1.0.6",
"@dqbd/tiktoken": "^1.0.7",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@next/font": "13.1.6",
@@ -24,6 +24,7 @@
"axios": "^1.3.3",
"cookie": "^0.5.0",
"crypto": "^1.0.1",
"date-fns": "^2.30.0",
"dayjs": "^1.11.7",
"eventsource-parser": "^0.1.0",
"formidable": "^2.1.1",
@@ -41,16 +42,16 @@
"nextjs-cors": "^2.1.2",
"nodemailer": "^6.9.1",
"nprogress": "^0.2.0",
"openai": "^3.2.1",
"openai": "^3.3.0",
"papaparse": "^5.4.1",
"pg": "^8.10.0",
"react": "18.2.0",
"react-day-picker": "^8.7.1",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.1",
"react-markdown": "^8.0.5",
"react-syntax-highlighter": "^15.5.0",
"rehype-katex": "^6.0.2",
"remark-breaks": "^3.0.3",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"request-ip": "^3.3.0",

72
client/pnpm-lock.yaml generated
View File

@@ -24,8 +24,8 @@ dependencies:
specifier: ^2.5.8
version: registry.npmmirror.com/@chakra-ui/system@2.5.8(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(react@18.2.0)
'@dqbd/tiktoken':
specifier: ^1.0.6
version: registry.npmmirror.com/@dqbd/tiktoken@1.0.6
specifier: ^1.0.7
version: registry.npmmirror.com/@dqbd/tiktoken@1.0.7
'@emotion/react':
specifier: ^11.10.6
version: registry.npmmirror.com/@emotion/react@11.10.6(@types/react@18.0.28)(react@18.2.0)
@@ -50,6 +50,9 @@ dependencies:
crypto:
specifier: ^1.0.1
version: registry.npmmirror.com/crypto@1.0.1
date-fns:
specifier: ^2.30.0
version: registry.npmmirror.com/date-fns@2.30.0
dayjs:
specifier: ^1.11.7
version: registry.npmmirror.com/dayjs@1.11.7
@@ -102,8 +105,8 @@ dependencies:
specifier: ^0.2.0
version: registry.npmmirror.com/nprogress@0.2.0
openai:
specifier: ^3.2.1
version: registry.npmmirror.com/openai@3.2.1
specifier: ^3.3.0
version: registry.npmmirror.com/openai@3.3.0
papaparse:
specifier: ^5.4.1
version: registry.npmmirror.com/papaparse@5.4.1
@@ -113,6 +116,9 @@ dependencies:
react:
specifier: 18.2.0
version: registry.npmmirror.com/react@18.2.0
react-day-picker:
specifier: ^8.7.1
version: registry.npmmirror.com/react-day-picker@8.7.1(date-fns@2.30.0)(react@18.2.0)
react-dom:
specifier: 18.2.0
version: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0)
@@ -128,9 +134,6 @@ dependencies:
rehype-katex:
specifier: ^6.0.2
version: registry.npmmirror.com/rehype-katex@6.0.2
remark-breaks:
specifier: ^3.0.3
version: registry.npmmirror.com/remark-breaks@3.0.3
remark-gfm:
specifier: ^3.0.1
version: registry.npmmirror.com/remark-gfm@3.0.1
@@ -4264,10 +4267,10 @@ packages:
react: registry.npmmirror.com/react@18.2.0
dev: false
registry.npmmirror.com/@dqbd/tiktoken@1.0.6:
resolution: {integrity: sha512-umSdeZTy/SbPPKVuZKV/XKyFPmXSN145CcM3iHjBbmhlohBJg7vaDp4cPCW+xNlWL6L2U1sp7T2BD+di2sUKdA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@dqbd/tiktoken/-/tiktoken-1.0.6.tgz}
registry.npmmirror.com/@dqbd/tiktoken@1.0.7:
resolution: {integrity: sha512-bhR5k5W+8GLzysjk8zTMVygQZsgvf7W1F0IlL4ZQ5ugjo5rCyiwGM5d8DYriXspytfu98tv59niang3/T+FoDw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@dqbd/tiktoken/-/tiktoken-1.0.7.tgz}
name: '@dqbd/tiktoken'
version: 1.0.6
version: 1.0.7
dev: false
registry.npmmirror.com/@emotion/babel-plugin@11.11.0:
@@ -6900,6 +6903,15 @@ packages:
engines: {node: '>= 6'}
dev: false
registry.npmmirror.com/date-fns@2.30.0:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/date-fns/-/date-fns-2.30.0.tgz}
name: date-fns
version: 2.30.0
engines: {node: '>=0.11'}
dependencies:
'@babel/runtime': registry.npmmirror.com/@babel/runtime@7.22.5
dev: false
registry.npmmirror.com/dayjs@1.11.7:
resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/dayjs/-/dayjs-1.11.7.tgz}
name: dayjs
@@ -8233,6 +8245,7 @@ packages:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz}
name: graceful-fs
version: 4.2.11
requiresBuild: true
registry.npmmirror.com/grapheme-splitter@1.0.4:
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz}
@@ -9401,15 +9414,6 @@ packages:
mdast-util-to-markdown: registry.npmmirror.com/mdast-util-to-markdown@1.5.0
dev: false
registry.npmmirror.com/mdast-util-newline-to-break@1.0.0:
resolution: {integrity: sha512-491LcYv3gbGhhCrLoeALncQmega2xPh+m3gbsIhVsOX4sw85+ShLFPvPyibxc1Swx/6GtzxgVodq+cGa/47ULg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/mdast-util-newline-to-break/-/mdast-util-newline-to-break-1.0.0.tgz}
name: mdast-util-newline-to-break
version: 1.0.0
dependencies:
'@types/mdast': registry.npmmirror.com/@types/mdast@3.0.11
mdast-util-find-and-replace: registry.npmmirror.com/mdast-util-find-and-replace@2.2.2
dev: false
registry.npmmirror.com/mdast-util-phrasing@3.0.1:
resolution: {integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz}
name: mdast-util-phrasing
@@ -10245,10 +10249,10 @@ packages:
is-wsl: registry.npmmirror.com/is-wsl@2.2.0
dev: true
registry.npmmirror.com/openai@3.2.1:
resolution: {integrity: sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/openai/-/openai-3.2.1.tgz}
registry.npmmirror.com/openai@3.3.0:
resolution: {integrity: sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/openai/-/openai-3.3.0.tgz}
name: openai
version: 3.2.1
version: 3.3.0
dependencies:
axios: registry.npmmirror.com/axios@0.26.1
form-data: registry.npmmirror.com/form-data@4.0.0
@@ -10727,6 +10731,19 @@ packages:
react: registry.npmmirror.com/react@18.2.0
dev: false
registry.npmmirror.com/react-day-picker@8.7.1(date-fns@2.30.0)(react@18.2.0):
resolution: {integrity: sha512-Gv426AW8b151CZfh3aP5RUGztLwHB/EyJgWZ5iMgtzbFBkjHfG6Y66CIQFMWGLnYjsQ9DYSJRmJ5S0Pg5HWKjA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/react-day-picker/-/react-day-picker-8.7.1.tgz}
id: registry.npmmirror.com/react-day-picker/8.7.1
name: react-day-picker
version: 8.7.1
peerDependencies:
date-fns: ^2.28.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
date-fns: registry.npmmirror.com/date-fns@2.30.0
react: registry.npmmirror.com/react@18.2.0
dev: false
registry.npmmirror.com/react-dom@18.2.0(react@18.2.0):
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/react-dom/-/react-dom-18.2.0.tgz}
id: registry.npmmirror.com/react-dom/18.2.0
@@ -11052,16 +11069,6 @@ packages:
unified: registry.npmmirror.com/unified@10.1.2
dev: false
registry.npmmirror.com/remark-breaks@3.0.3:
resolution: {integrity: sha512-C7VkvcUp1TPUc2eAYzsPdaUh8Xj4FSbQnYA5A9f80diApLZscTDeG7efiWP65W8hV2sEy3JuGVU0i6qr5D8Hug==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/remark-breaks/-/remark-breaks-3.0.3.tgz}
name: remark-breaks
version: 3.0.3
dependencies:
'@types/mdast': registry.npmmirror.com/@types/mdast@3.0.11
mdast-util-newline-to-break: registry.npmmirror.com/mdast-util-newline-to-break@1.0.0
unified: registry.npmmirror.com/unified@10.1.2
dev: false
registry.npmmirror.com/remark-gfm@3.0.1:
resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/remark-gfm/-/remark-gfm-3.0.1.tgz}
name: remark-gfm
@@ -11413,6 +11420,7 @@ packages:
name: source-map
version: 0.6.1
engines: {node: '>=0.10.0'}
requiresBuild: true
registry.npmmirror.com/space-separated-tokens@1.1.5:
resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz}

View File

@@ -1,26 +1,18 @@
### 常见问题
**Git 地址**
[项目地址,完全开源,随便用。](https://github.com/c121914yu/FastGPT)
**问题文档**
[先看文档,再提问](https://kjqvjse66l.feishu.cn/docx/HtrgdT0pkonP4kxGx8qcu6XDnGh)
**删除和复制**
电脑端:聊天内容右侧有复制和删除的图标。
移动端:点击对话头像,可以选择复制或删除该条内容。
**Git 地址**: [项目地址,完全开源,随便用。](https://github.com/c121914yu/FastGPT)
**问题文档**: [先看文档,再提问](https://kjqvjse66l.feishu.cn/docx/HtrgdT0pkonP4kxGx8qcu6XDnGh)
**价格表**
如果使用了自己的 Api Key网页上 openai 模型聊天不会计费。可以在账号页,看到详细账单。
| 计费项 | 价格: 元/ 1K tokens包含上下文|
| --- | --- |
| 知识库 - 索引 | 0.001 |
| chatgpt - 对话 | 0.025 |
| chatgpt - 对话 | 0.022 |
| chatgpt16K - 对话 | 0.025 |
| gpt4 - 对话 | 0.5 |
| 文件拆分 | 0.025 |
**其他问题**
请 WX 联系: YNyiqi
| 交流群 | 小助手 |
| ----------------------- | -------------------- |
| ![](https://otnvvf-imgs.oss.laf.run/wxqun300.jpg) | ![](https://otnvvf-imgs.oss.laf.run/wx300.jpg) |

View File

@@ -19,7 +19,8 @@ FastGpt 项目完全开源,可随意私有化部署,去除平台风险忧虑
| 计费项 | 价格: 元/ 1K tokens包含上下文|
| --- | --- |
| 知识库 - 索引 | 0.001 |
| chatgpt - 对话 | 0.025 |
| chatgpt - 对话 | 0.022 |
| chatgpt16K - 对话 | 0.025 |
| gpt4 - 对话 | 0.5 |
| 文件拆分 | 0.025 |

View File

@@ -1,5 +1,8 @@
### Fast GPT V3.8.4
### Fast GPT V3.8.8
1. 新增 - mermaid 导图兼容,可以在应用市场 'mermaid 导图' 进行体验。
2. 优化 - 部分 UI 和账号页
2. 优化 - 知识库搜索速度
1. 新增 - V2 版 OpenAPI可以在任意第三方套壳 ChatGpt 项目中直接使用 FastGpt 的应用,注意!是直接,不需要改任何代码。具体参考[API 文档中《在第三方应用中使用 FastGpt》](https://kjqvjse66l.feishu.cn/docx/DmLedTWtUoNGX8xui9ocdUEjnNh)
2. 新增 - 应用配置最大回复长度
3. 新增 - 更多的知识库配置项:相似度、最大搜索数量、自定义空搜索结果回复。
4. 新增 - 知识库搜索测试,方便调试。
5. 优化 - 知识库提示词位置,拥有更强的引导。
6. 优化 - 应用编辑页面。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -1,67 +1,104 @@
import { GUIDE_PROMPT_HEADER, NEW_CHATID_HEADER, QUOTE_LEN_HEADER } from '@/constants/chat';
import { Props, ChatResponseType } from '@/pages/api/openapi/v1/chat/completions';
import { sseResponseEventEnum } from '@/constants/chat';
import { getErrText } from '@/utils/tools';
interface StreamFetchProps {
url: string;
data: any;
data: Props;
onMessage: (text: string) => void;
abortSignal: AbortController;
}
export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchProps) =>
new Promise<{
responseText: string;
newChatId: string;
systemPrompt: string;
quoteLen: number;
}>(async (resolve, reject) => {
export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps) =>
new Promise<ChatResponseType & { responseText: string }>(async (resolve, reject) => {
try {
const res = await fetch(url, {
const response = await window.fetch('/api/openapi/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
signal: abortSignal.signal
signal: abortSignal.signal,
body: JSON.stringify({
...data,
stream: true
})
});
const reader = res.body?.getReader();
if (!reader) return;
const decoder = new TextDecoder();
if (!response?.body) {
throw new Error('Request Error');
}
const newChatId = decodeURIComponent(res.headers.get(NEW_CHATID_HEADER) || '');
const systemPrompt = decodeURIComponent(res.headers.get(GUIDE_PROMPT_HEADER) || '').trim();
const quoteLen = res.headers.get(QUOTE_LEN_HEADER)
? Number(res.headers.get(QUOTE_LEN_HEADER))
: 0;
const reader = response.body?.getReader();
const decoder = new TextDecoder('utf-8');
// response data
let responseText = '';
let newChatId = '';
let quoteLen = 0;
const read = async () => {
try {
const { done, value } = await reader?.read();
const { done, value } = await reader.read();
if (done) {
if (res.status === 200) {
resolve({ responseText, newChatId, quoteLen, systemPrompt });
if (response.status === 200) {
return resolve({
responseText,
newChatId,
quoteLen
});
} else {
const parseError = JSON.parse(responseText);
reject(parseError?.message || '请求异常');
return reject('响应过程出现异常~');
}
return;
}
const text = decoder.decode(value);
responseText += text;
onMessage(text);
const chunk = decoder.decode(value);
const chunkLines = chunk.split('\n\n').filter((item) => item);
const chunkResponse = chunkLines.map((item) => {
const splitEvent = item.split('\n');
if (splitEvent.length === 2) {
return {
event: splitEvent[0].replace('event: ', ''),
data: splitEvent[1].replace('data: ', '')
};
}
return {
event: '',
data: splitEvent[0].replace('data: ', '')
};
});
chunkResponse.forEach((item) => {
// parse json data
const data = (() => {
try {
return JSON.parse(item.data);
} catch (error) {
return item.data;
}
})();
if (item.event === sseResponseEventEnum.answer && data !== '[DONE]') {
const answer: string = data?.choices[0].delta.content || '';
onMessage(answer);
responseText += answer;
} else if (item.event === sseResponseEventEnum.chatResponse) {
const chatResponse = data as ChatResponseType;
newChatId = chatResponse.newChatId;
quoteLen = chatResponse.quoteLen || 0;
}
});
read();
} catch (err: any) {
if (err?.message === 'The user aborted a request.') {
return resolve({ responseText, newChatId, quoteLen, systemPrompt });
return resolve({
responseText,
newChatId,
quoteLen
});
}
reject(typeof err === 'string' ? err : err?.message || '请求异常');
reject(getErrText(err, '请求异常'));
}
};
read();
} catch (err: any) {
console.log(err, '====');
reject(typeof err === 'string' ? err : err?.message || '请求异常');
reject(getErrText(err, '请求异常'));
}
});

View File

@@ -42,7 +42,7 @@ export const getKbDataList = (data: GetKbDataListProps) =>
* 获取导出数据(不分页)
*/
export const getExportDataList = (kbId: string) =>
GET<[string, string][]>(
GET<[string, string, string][]>(
`/plugins/kb/data/exportModelData`,
{ kbId },
{

View File

@@ -4,6 +4,7 @@ import type { ChatItemType } from '@/types/chat';
export interface InitChatResponse {
chatId: string;
modelId: string;
systemPrompt?: string;
model: {
name: string;
avatar: string;

View File

@@ -5,3 +5,5 @@ import type { InitDateResponse } from '@/pages/api/system/getInitData';
export const getInitData = () => GET<InitDateResponse>('/system/getInitData');
export const getSystemModelList = () => GET<ChatModelItemType[]>('/system/getModels');
export const uploadImg = (base64Img: string) => POST<string>('/system/uploadImage', { base64Img });

View File

@@ -66,7 +66,7 @@ export const loginOut = () => GET('/user/loginout');
export const putUserInfo = (data: UserUpdateParams) => PUT('/user/update', data);
export const getUserBills = (data: RequestPaging) =>
GET<PagingData<UserBillType>>(`/user/getBill?${Obj2Query(data)}`);
POST<PagingData<UserBillType>>(`/user/getBill`, data);
export const getPayOrders = () => GET<PaySchema[]>(`/user/getPayOrders`);

View File

@@ -0,0 +1,160 @@
import React, { useState } from 'react';
import {
Box,
Button,
Modal,
ModalOverlay,
ModalContent,
Flex,
ModalFooter,
ModalBody,
ModalCloseButton,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
IconButton
} from '@chakra-ui/react';
import { getOpenApiKeys, createAOpenApiKey, delOpenApiById } from '@/api/openapi';
import { useQuery, useMutation } from '@tanstack/react-query';
import { useLoading } from '@/hooks/useLoading';
import dayjs from 'dayjs';
import { AddIcon, DeleteIcon } from '@chakra-ui/icons';
import { getErrText, useCopyData } from '@/utils/tools';
import { useToast } from '@/hooks/useToast';
import MyIcon from '../Icon';
const APIKeyModal = ({ onClose }: { onClose: () => void }) => {
const { Loading } = useLoading();
const { toast } = useToast();
const {
data: apiKeys = [],
isLoading: isGetting,
refetch
} = useQuery(['getOpenApiKeys'], getOpenApiKeys);
const [apiKey, setApiKey] = useState('');
const { copyData } = useCopyData();
const { mutate: onclickCreateApiKey, isLoading: isCreating } = useMutation({
mutationFn: () => createAOpenApiKey(),
onSuccess(res) {
setApiKey(res);
refetch();
},
onError(err) {
toast({
status: 'warning',
title: getErrText(err)
});
}
});
const { mutate: onclickRemove, isLoading: isDeleting } = useMutation({
mutationFn: async (id: string) => delOpenApiById(id),
onSuccess() {
refetch();
}
});
return (
<Modal isOpen onClose={onClose}>
<ModalOverlay />
<ModalContent w={'600px'} maxW={'90vw'} position={'relative'}>
<Box py={3} px={5}>
<Box fontWeight={'bold'} fontSize={'2xl'}>
API
</Box>
<Box fontSize={'sm'} color={'myGray.600'}>
API 使~
</Box>
</Box>
<ModalCloseButton />
<ModalBody minH={'300px'} maxH={['70vh', '500px']} overflow={'overlay'}>
<TableContainer mt={2} position={'relative'}>
<Table>
<Thead>
<Tr>
<Th>Api Key</Th>
<Th></Th>
<Th>使</Th>
<Th />
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{apiKeys.map(({ id, apiKey, createTime, lastUsedTime }) => (
<Tr key={id}>
<Td>{apiKey}</Td>
<Td>{dayjs(createTime).format('YYYY/MM/DD HH:mm:ss')}</Td>
<Td>
{lastUsedTime
? dayjs(lastUsedTime).format('YYYY/MM/DD HH:mm:ss')
: '没有使用过'}
</Td>
<Td>
<IconButton
icon={<DeleteIcon />}
size={'xs'}
aria-label={'delete'}
variant={'base'}
colorScheme={'gray'}
onClick={() => onclickRemove(id)}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</ModalBody>
<ModalFooter>
<Button
variant="base"
leftIcon={<AddIcon color={'myGray.600'} fontSize={'sm'} />}
onClick={() => onclickCreateApiKey()}
>
</Button>
</ModalFooter>
<Loading loading={isGetting || isCreating || isDeleting} fixed={false} />
</ModalContent>
<Modal isOpen={!!apiKey} onClose={() => setApiKey('')}>
<ModalOverlay />
<ModalContent w={'400px'} maxW={'90vw'}>
<Box py={3} px={5}>
<Box fontWeight={'bold'} fontSize={'2xl'}>
API
</Box>
<Box fontSize={'sm'} color={'myGray.600'}>
~
</Box>
</Box>
<ModalCloseButton />
<ModalBody>
<Flex
bg={'myGray.100'}
px={3}
py={2}
cursor={'pointer'}
onClick={() => copyData(apiKey)}
>
<Box flex={1}>{apiKey}</Box>
<MyIcon name={'copy'} w={'16px'}></MyIcon>
</Flex>
</ModalBody>
<ModalFooter>
<Button variant="base" onClick={() => setApiKey('')}>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Modal>
);
};
export default APIKeyModal;

View File

@@ -0,0 +1,4 @@
.datePicker {
--rdp-background-color: #d6e8ff;
--rdp-accent-color: #0000ff;
}

View File

@@ -0,0 +1,121 @@
import React, { useState, useMemo, useRef } from 'react';
import { Box, Card, Flex, useTheme, useOutsideClick, Button } from '@chakra-ui/react';
import { addDays, format } from 'date-fns';
import { type DateRange, DayPicker } from 'react-day-picker';
import MyIcon from '../Icon';
import 'react-day-picker/dist/style.css';
import styles from './index.module.scss';
import zhCN from 'date-fns/locale/zh-CN';
const DateRangePicker = ({
onChange,
onSuccess,
position = 'bottom',
defaultDate = {
from: addDays(new Date(), -30),
to: new Date()
}
}: {
onChange?: (date: DateRange) => void;
onSuccess?: (date: DateRange) => void;
position?: 'bottom' | 'top';
defaultDate?: DateRange;
}) => {
const theme = useTheme();
const OutRangeRef = useRef(null);
const [range, setRange] = useState<DateRange | undefined>(defaultDate);
const [showSelected, setShowSelected] = useState(false);
const formatSelected = useMemo(() => {
if (range?.from && range.to) {
return `${format(range.from, 'y-MM-dd')} ~ ${format(range.to, 'y-MM-dd')}`;
}
return `${format(new Date(), 'y-MM-dd')} ~ ${format(new Date(), 'y-MM-dd')}`;
}, [range]);
useOutsideClick({
ref: OutRangeRef,
handler: () => {
setShowSelected(false);
}
});
return (
<Box position={'relative'} ref={OutRangeRef}>
<Flex
border={theme.borders.base}
px={3}
py={1}
borderRadius={'sm'}
cursor={'pointer'}
bg={'myWhite.600'}
fontSize={'sm'}
onClick={() => setShowSelected(true)}
>
<Box>{formatSelected}</Box>
<MyIcon ml={2} name={'date'} w={'16px'} color={'myGray.600'} />
</Flex>
{showSelected && (
<Card
position={'absolute'}
zIndex={1}
{...(position === 'top'
? {
bottom: '40px'
}
: {})}
>
<DayPicker
locale={zhCN}
id="test"
mode="range"
className={styles.datePicker}
defaultMonth={defaultDate.to}
selected={range}
disabled={[
{ from: new Date(2022, 3, 1), to: addDays(new Date(), -90) },
{ from: addDays(new Date(), 1), to: new Date(2099, 1, 1) }
]}
onSelect={(date) => {
if (date?.from === undefined) {
date = {
from: range?.from,
to: range?.from
};
}
if (date?.to === undefined) {
date.to = date.from;
}
setRange(date);
onChange && onChange(date);
}}
footer={
<Flex justifyContent={'flex-end'}>
<Button
variant={'outline'}
size={'sm'}
mr={2}
onClick={() => setShowSelected(false)}
>
</Button>
<Button
size={'sm'}
onClick={() => {
onSuccess && onSuccess(range || defaultDate);
setShowSelected(false);
}}
>
</Button>
</Flex>
}
/>
</Card>
)}
</Box>
);
};
export default DateRangePicker;
export type DateRangeType = DateRange;

View 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="1686969412308" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3481" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M517.864056 487.834624c-56.774051-54.213739-58.850339-144.187937-4.6366-200.960964 54.212716-56.773028 144.187937-58.849316 200.960964-4.6366 56.775074 54.213739 58.850339 144.186913 4.6366 200.960964C664.613328 539.972075 574.639131 542.048363 517.864056 487.834624zM687.194626 452.994118c37.533848-39.308261 36.09508-101.596909-3.210112-139.128711-39.304168-37.531801-101.593839-36.094056-139.127687 3.211135-37.532825 39.307238-36.093033 101.593839 3.212158 139.125641C587.374176 493.736031 649.660778 492.302379 687.194626 452.994118zM479.104287 670.917406l-101.495602 106.289792c26.206872 25.024953 27.167756 66.540486 2.14178 92.749404-25.028023 26.209942-66.543555 27.16571-92.750427 2.140757l-58.361199 53.027727c0 0-68.750827 11.100826-100.379175-19.101033-31.630395-30.205952-37.865399-112.721271-37.865399-112.721271l246.37427-258.302951c-63.173808-117.608581-47.24707-267.162736 49.939389-368.939747 36.517705-38.242999 80.346933-65.156976 127.165238-81.040734l1.084705 46.269813c-35.443233 14.07967-68.566632 35.596729-96.618525 64.973804-80.271208 84.064604-96.099708 205.865671-49.433876 305.083393l23.075555 39.163975L146.090774 798.015106c0 0 0.593518 49.77873 17.242709 65.677838 14.888082 14.216793 61.832254 9.828856 61.832254 9.828856l60.407812-63.260789 31.631418 30.203906c8.741082 8.346085 22.570042 8.030907 30.91715-0.711198 8.347109-8.742105 8.026814-22.571065-0.713244-30.91715l-31.632441-30.207999 156.456355-163.846672 39.009456 22.481014c101.259218 42.039465 222.201731 20.61041 302.474986-63.453171 104.251366-109.178585 100.260471-282.211477-8.91709-386.464889-33.591049-32.075533-73.260537-53.829999-115.093295-65.49262l-1.030469-45.153386c53.197596 12.471033 103.945397 38.547944 146.323577 79.015611 126.645398 120.931257 131.277906 321.649698 10.344602 448.296119C748.158093 705.787588 599.500355 728.598106 479.104287 670.917406z" p-id="3482"></path></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View 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="1686832863390" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4120" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M782.84 188.75h-43.15v-60.46c0-16.57-13.43-30-30-30s-30 13.43-30 30v60.46H371.88v-60.46c0-16.57-13.43-30-30-30s-30 13.43-30 30v60.46H250.5c-66.17 0-120 53.83-120 120v494.47c0 66.17 53.83 120 120 120h532.33c66.17 0 120-53.83 120-120V308.75c0.01-66.17-53.82-120-119.99-120z m-532.34 60h61.37v133.63c0 16.57 13.43 30 30 30s30-13.43 30-30V248.75h307.81v133.63c0 16.57 13.43 30 30 30s30-13.43 30-30V248.75h43.15c33.08 0 60 26.92 60 60V649.5H190.5V308.75c0-33.08 26.92-60 60-60z m532.34 614.47H250.5c-33.08 0-60-26.92-60-60V709.5h652.33v93.72c0.01 33.08-26.91 60-59.99 60z" p-id="4121"></path></svg>

After

Width:  |  Height:  |  Size: 924 B

View File

@@ -1 +0,0 @@
<?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="1683254594671" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1491" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M46.95735957 106.20989621h930.08528086v158.0067668H46.95735957zM46.95735957 353.99323467v608.68515424h930.08528086V353.99323467H46.95735957z m346.5375657 418.35882335L328.85579413 835.19565715l-165.18889183-172.37101684 165.18889183-172.37101686 64.63913114 62.84359914-105.93635373 109.52741772 105.93635373 109.52741771z m127.48273175 62.84359913l-86.18550917-23.34190854 87.98104116-330.37778366 86.1855077 23.34191003L520.97765702 835.19565715z m193.91739489 0l-64.63913114-62.84359913 105.93635372-109.52741771-105.93635372-109.52741772 64.63913114-62.84359914 165.18889182 172.37101686-165.18889182 172.37101684z" p-id="1492"></path></svg>

Before

Width:  |  Height:  |  Size: 976 B

View File

@@ -6,7 +6,6 @@ const map = {
model: require('./icons/model.svg').default,
copy: require('./icons/copy.svg').default,
chatSend: require('./icons/chatSend.svg').default,
develop: require('./icons/develop.svg').default,
user: require('./icons/user.svg').default,
delete: require('./icons/delete.svg').default,
withdraw: require('./icons/withdraw.svg').default,
@@ -33,7 +32,9 @@ const map = {
export: require('./icons/export.svg').default,
text: require('./icons/text.svg').default,
history: require('./icons/history.svg').default,
kbTest: require('./icons/kbTest.svg').default
kbTest: require('./icons/kbTest.svg').default,
date: require('./icons/date.svg').default,
apikey: require('./icons/apikey.svg').default
};
export type IconName = keyof typeof map;

View File

@@ -1,23 +0,0 @@
type TIconfont = {
name: string;
color?: string;
width?: number | string;
height?: number | string;
className?: string;
};
function Iconfont({ name, color = 'inherit', width = 16, height = 16, className = '' }: TIconfont) {
const style = {
fill: color,
width,
height
};
return (
<svg className={`icon ${className}`} aria-hidden="true" style={style}>
<use xlinkHref={`#${name}`}></use>
</svg>
);
}
export default Iconfont;

View File

@@ -44,12 +44,6 @@ const Navbar = ({ unread }: { unread: number }) => {
link: '/model/share',
activeLink: ['/model/share']
},
{
label: '开发',
icon: 'develop',
link: '/openapi',
activeLink: ['/openapi']
},
{
label: '账号',
icon: 'user',

View File

@@ -14,7 +14,7 @@ const Loading = ({ fixed = true }: { fixed?: boolean }) => {
alignItems={'center'}
justifyContent={'center'}
>
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="myBlue.500" size="xl" />
<Spinner thickness="4px" speed="0.65s" emptyColor="myGray.100" color="myBlue.600" size="xl" />
</Flex>
);
};

View File

@@ -0,0 +1,39 @@
import React, { useState } from 'react';
import { Image, Skeleton } from '@chakra-ui/react';
const MdImage = ({ src }: { src: string }) => {
const [isLoading, setIsLoading] = useState(true);
const [succeed, setSucceed] = useState(false);
return (
<Skeleton
minH="100px"
isLoaded={!isLoading}
fadeDuration={2}
display={'flex'}
justifyContent={'center'}
my={1}
>
<Image
display={'inline-block'}
borderRadius={'md'}
src={src}
alt={''}
fallbackSrc={'/imgs/errImg.png'}
fallbackStrategy={'onError'}
cursor={succeed ? 'pointer' : 'default'}
loading="eager"
onLoad={() => {
setIsLoading(false);
setSucceed(true);
}}
onError={() => setIsLoading(false)}
onClick={() => {
if (!succeed) return;
window.open(src, '_blank');
}}
/>
</Skeleton>
);
};
export default React.memo(MdImage);

View File

@@ -1,17 +1,24 @@
import React, { memo } from 'react';
import { Box } from '@chakra-ui/react';
const Loading = () => {
const Loading = ({ text }: { text?: string }) => {
return (
<Box
minW={'100px'}
w={'100%'}
h={'80px'}
backgroundImage={'url("/imgs/loading.gif")'}
backgroundSize={'contain'}
backgroundRepeat={'no-repeat'}
backgroundPosition={'center'}
/>
<Box>
<Box
minW={'100px'}
w={'100%'}
h={'80px'}
backgroundImage={'url("/imgs/loading.gif")'}
backgroundSize={'contain'}
backgroundRepeat={'no-repeat'}
backgroundPosition={'center'}
/>
{text && (
<Box mt={1} textAlign={'center'} fontSize={'sm'} color={'myGray.600'}>
{text}
</Box>
)}
</Box>
);
};

View File

@@ -24,17 +24,41 @@ mermaidAPI.initialize({
const MermaidBlock = ({ code }: { code: string }) => {
const dom = useRef<HTMLDivElement>(null);
const [svg, setSvg] = useState('');
const [errorSvgCode, setErrorSvgCode] = useState('');
useEffect(() => {
try {
const formatCode = code.replace(//g, ':');
mermaidAPI.render(`mermaid-${Date.now()}`, formatCode, (svgCode: string) => {
(async () => {
const punctuationMap: Record<string, string> = {
'': ',',
'': ';',
'。': '.',
'': ':',
'': '!',
'': '?',
'“': '"',
'”': '"',
'': "'",
'': "'",
'【': '[',
'】': ']',
'': '(',
'': ')',
'《': '<',
'》': '>',
'、': ','
};
const formatCode = code.replace(
/([,;。:!?“”‘’【】()《》、])/g,
(match) => punctuationMap[match]
);
try {
const svgCode = await mermaidAPI.render(`mermaid-${Date.now()}`, formatCode);
setSvg(svgCode);
});
} catch (error) {
console.log(error);
}
} catch (error) {
setErrorSvgCode(formatCode);
console.log(error);
}
})();
}, [code]);
const onclickExport = useCallback(() => {

View File

@@ -1,9 +1,8 @@
import React, { memo, useMemo, useEffect } from 'react';
import React, { memo, useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import { formatLinkText } from '@/utils/tools';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import remarkBreaks from 'remark-breaks';
import rehypeKatex from 'rehype-katex';
import 'katex/dist/katex.min.css';
@@ -11,6 +10,7 @@ import styles from './index.module.scss';
import CodeLight from './codeLight';
import Loading from './Loading';
import MermaidCodeBlock from './MermaidCodeBlock';
import MdImage from './Image';
const Markdown = ({
source,
@@ -30,15 +30,22 @@ const Markdown = ({
className={`markdown ${styles.markdown}
${isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''}
`}
remarkPlugins={[remarkGfm, remarkMath, remarkBreaks]}
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
pre: 'div',
img({ src = '' }) {
return isChatting ? <Loading text="图片加载中..." /> : <MdImage src={src} />;
},
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
if (match?.[1] === 'mermaid') {
return isChatting ? <Loading /> : <MermaidCodeBlock code={String(children)} />;
return isChatting ? (
<Loading text="导图加载中..." />
) : (
<MermaidCodeBlock code={String(children)} />
);
}
return (

View File

@@ -25,6 +25,7 @@ const Radio = ({ list, value, onChange, ...props }: Props) => {
mr: 1,
borderRadius: '16px',
transition: '0.2s',
boxSizing: 'border-box',
...(value === item.value
? {
border: '5px solid',

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { Menu, MenuButton, MenuList, MenuItem, Button, useDisclosure } from '@chakra-ui/react';
import type { ButtonProps } from '@chakra-ui/react';
import { ChevronDownIcon } from '@chakra-ui/icons';
interface Props extends ButtonProps {
value?: string;
placeholder?: string;
list: {
label: string;
id: string;
}[];
onchange?: (val: string) => void;
}
const MySelect = ({ placeholder, value, width = 'auto', list, onchange, ...props }: Props) => {
const menuItemStyles = {
borderRadius: 'sm',
py: 2,
display: 'flex',
alignItems: 'center',
_hover: {
backgroundColor: 'myWhite.600'
}
};
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<Menu autoSelect={false} onOpen={onOpen} onClose={onClose}>
<MenuButton as={'span'}>
<Button
width={width}
px={3}
variant={'base'}
display={'flex'}
alignItems={'center'}
justifyContent={'space-between'}
{...(isOpen
? {
boxShadow: '0px 0px 4px #A8DBFF',
borderColor: 'myBlue.600'
}
: {})}
{...props}
>
{list.find((item) => item.id === value)?.label || placeholder}
<ChevronDownIcon />
</Button>
</MenuButton>
<MenuList
minW={
Array.isArray(width) ? width.map((item) => `${item} !important`) : `${width} !important`
}
p={'6px'}
border={'1px solid #fff'}
boxShadow={'0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);'}
zIndex={99}
>
{list.map((item) => (
<MenuItem
key={item.id}
{...menuItemStyles}
{...(value === item.id
? {
color: 'myBlue.600'
}
: {})}
onClick={() => {
if (onchange && value !== item.id) {
onchange(item.id);
}
}}
>
{item.label}
</MenuItem>
))}
</MenuList>
</Menu>
);
};
export default MySelect;

View File

@@ -9,28 +9,30 @@ import {
} from '@chakra-ui/react';
const MySlider = ({
markList,
markList = [],
setVal,
activeVal,
max = 100,
min = 0,
step = 1
step = 1,
width = '100%'
}: {
markList: {
markList?: {
label: string | number;
value: number;
}[];
activeVal?: number;
activeVal: number;
setVal: (index: number) => void;
max?: number;
min?: number;
step?: number;
width?: string | string[] | number | number[];
}) => {
const startEndPointStyle = {
content: '""',
borderRadius: '10px',
width: '10px',
height: '10px',
borderRadius: '6px',
width: '6px',
height: '6px',
backgroundColor: '#ffffff',
border: '2px solid #D7DBE2',
position: 'absolute',
@@ -44,37 +46,62 @@ const MySlider = ({
}, [activeVal, markList]);
return (
<Slider max={max} min={min} step={step} size={'lg'} value={value} onChange={setVal}>
{markList.map((item, i) => (
<Slider
max={max}
min={min}
step={step}
size={'lg'}
value={activeVal}
width={width}
onChange={setVal}
>
{markList?.map((item, i) => (
<SliderMark
key={item.value}
value={i}
mt={3}
value={item.value}
fontSize={'sm'}
mt={3}
whiteSpace={'nowrap'}
transform={'translateX(-50%)'}
{...(activeVal === item.value ? { color: 'myBlue.500', fontWeight: 'bold' } : {})}
color={'myGray.600'}
>
<Box px={3} cursor={'pointer'}>
{item.label}
</Box>
</SliderMark>
))}
<SliderMark
value={activeVal}
textAlign="center"
bg="myBlue.600"
color="white"
px={1}
minW={'18px'}
w={'auto'}
h={'18px'}
borderRadius={'18px'}
fontSize={'xs'}
transform={'translate(-50%, -170%)'}
boxSizing={'border-box'}
>
{activeVal}
</SliderMark>
<SliderTrack
bg={'#EAEDF3'}
overflow={'visible'}
h={'4px'}
_before={{
...startEndPointStyle,
left: '-5px'
left: '-3px'
}}
_after={{
...startEndPointStyle,
right: '-5px'
right: '-3px'
}}
>
<SliderFilledTrack />
<SliderFilledTrack bg={'myBlue.600'} />
</SliderTrack>
<SliderThumb border={'2.5px solid'} borderColor={'myBlue.500'}></SliderThumb>
<SliderThumb border={'3px solid'} borderColor={'myBlue.600'}></SliderThumb>
</Slider>
);
};

View File

@@ -24,13 +24,13 @@ const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => {
return {
fontSize: 'md',
outP: '4px',
inlineP: 2
inlineP: 1
};
case 'lg':
return {
fontSize: 'lg',
outP: '5px',
inlineP: 3
inlineP: 2
};
}
}, [size]);

File diff suppressed because one or more lines are too long

View File

@@ -8,14 +8,12 @@ export type EmbeddingModelType = 'text-embedding-ada-002';
export enum OpenAiChatEnum {
'GPT35' = 'gpt-3.5-turbo',
'GPT3516k' = 'gpt-3.5-turbo-16k',
'GPT4' = 'gpt-4',
'GPT432k' = 'gpt-4-32k'
}
export enum ClaudeEnum {
'Claude' = 'Claude'
}
export type ChatModelType = `${OpenAiChatEnum}` | `${ClaudeEnum}`;
export type ChatModelType = `${OpenAiChatEnum}`;
export type ChatModelItemType = {
chatModel: ChatModelType;
@@ -29,9 +27,17 @@ export type ChatModelItemType = {
export const ChatModelMap = {
[OpenAiChatEnum.GPT35]: {
chatModel: OpenAiChatEnum.GPT35,
name: 'ChatGpt',
contextMaxToken: 4096,
systemMaxToken: 2700,
name: 'Gpt35-4k',
contextMaxToken: 4000,
systemMaxToken: 2400,
maxTemperature: 1.2,
price: 2.2
},
[OpenAiChatEnum.GPT3516k]: {
chatModel: OpenAiChatEnum.GPT3516k,
name: 'Gpt35-16k',
contextMaxToken: 16000,
systemMaxToken: 8000,
maxTemperature: 1.2,
price: 2.5
},
@@ -50,14 +56,6 @@ export const ChatModelMap = {
systemMaxToken: 8000,
maxTemperature: 1.2,
price: 90
},
[ClaudeEnum.Claude]: {
chatModel: ClaudeEnum.Claude,
name: 'Claude(免费体验)',
contextMaxToken: 9000,
systemMaxToken: 2700,
maxTemperature: 1,
price: 0
}
};
@@ -71,78 +69,26 @@ export const getChatModelList = async () => {
return list;
};
export enum ModelStatusEnum {
running = 'running',
training = 'training',
pending = 'pending',
closed = 'closed'
}
export const formatModelStatus = {
[ModelStatusEnum.running]: {
colorTheme: 'green',
text: '运行中'
},
[ModelStatusEnum.training]: {
colorTheme: 'blue',
text: '训练中'
},
[ModelStatusEnum.pending]: {
colorTheme: 'gray',
text: '加载中'
},
[ModelStatusEnum.closed]: {
colorTheme: 'red',
text: '已关闭'
}
};
/* 知识库搜索时的配置 */
// 搜索方式
export enum appVectorSearchModeEnum {
hightSimilarity = 'hightSimilarity', // 高相似度+禁止回复
lowSimilarity = 'lowSimilarity', // 低相似度
noContext = 'noContex' // 高相似度+无上下文回复
}
export const ModelVectorSearchModeMap: Record<
`${appVectorSearchModeEnum}`,
{
text: string;
similarity: number;
}
> = {
[appVectorSearchModeEnum.hightSimilarity]: {
text: '高相似度, 无匹配时拒绝回复',
similarity: 0.8
},
[appVectorSearchModeEnum.noContext]: {
text: '高相似度,无匹配时直接回复',
similarity: 0.8
},
[appVectorSearchModeEnum.lowSimilarity]: {
text: '低相似度匹配',
similarity: 0.3
}
};
export const defaultModel: ModelSchema = {
_id: 'modelId',
userId: 'userId',
name: '模型名称',
avatar: '/icon/logo.png',
status: ModelStatusEnum.pending,
intro: '',
updateTime: Date.now(),
chat: {
relatedKbs: [],
searchMode: appVectorSearchModeEnum.hightSimilarity,
searchSimilarity: 0.2,
searchLimit: 5,
searchEmptyText: '',
systemPrompt: '',
temperature: 0,
maxToken: 4000,
chatModel: OpenAiChatEnum.GPT35
},
share: {
isShare: false,
isShareDetail: false,
intro: '',
collection: 0
}
};

View File

@@ -11,15 +11,11 @@ const { definePartsStyle: switchPart, defineMultiStyleConfig: switchMultiStyle }
createMultiStyleConfigHelpers(switchAnatomy.keys);
const { definePartsStyle: selectPart, defineMultiStyleConfig: selectMultiStyle } =
createMultiStyleConfigHelpers(selectAnatomy.keys);
const { definePartsStyle: checkboxPart, defineMultiStyleConfig: checkboxMultiStyle } =
createMultiStyleConfigHelpers(checkboxAnatomy.keys);
// modal 弹窗
const ModalTheme = defineMultiStyleConfig({
baseStyle: definePartsStyle({
dialog: {
width: '90%'
}
dialog: {}
})
});
@@ -41,7 +37,7 @@ const Button = defineStyleConfig({
},
sm: {
fontSize: 'sm',
px: 3,
px: 4,
py: 0,
fontWeight: 'normal',
height: '26px',
@@ -69,6 +65,7 @@ const Button = defineStyleConfig({
backgroundImage:
'linear-gradient(to bottom right, #2152d9 0%,#3370ff 40%, #4e83fd 100%) !important',
color: 'white',
border: 'none',
_hover: {
filter: 'brightness(115%)'
},

View File

@@ -76,6 +76,20 @@ export const usePagination = <T = any,>({
mutate(+e.target.value);
}
}}
onKeyDown={(e) => {
// @ts-ignore
const val = +e.target.value;
if (val && e.keyCode === 13) {
if (val === pageNum) return;
if (val >= maxPage) {
mutate(maxPage);
} else if (val < 1) {
mutate(1);
} else {
mutate(val);
}
}
}}
/>
<Box mx={2}>/</Box>
{maxPage}

View File

@@ -8,9 +8,9 @@ import { theme } from '@/constants/theme';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import NProgress from 'nprogress'; //nprogress module
import Router from 'next/router';
import 'nprogress/nprogress.css';
import '../styles/reset.scss';
import { useGlobalStore } from '@/store/global';
import 'nprogress/nprogress.css';
import '@/styles/reset.scss';
//Binding events.
Router.events.on('routeChangeStart', () => NProgress.start());
@@ -28,7 +28,7 @@ const queryClient = new QueryClient({
}
});
export default function App({ Component, pageProps }: AppProps) {
function App({ Component, pageProps }: AppProps) {
const {
loadInitData,
initData: { googleVerKey }
@@ -49,14 +49,20 @@ export default function App({ Component, pageProps }: AppProps) {
/>
<link rel="icon" href="/favicon.ico" />
</Head>
<Script src="/js/qrcode.min.js" strategy="lazyOnload"></Script>
<Script src="/js/pdf.js" strategy="lazyOnload"></Script>
<Script src="/js/html2pdf.bundle.min.js" strategy="lazyOnload"></Script>
<Script src="/js/qrcode.min.js" strategy="afterInteractive"></Script>
<Script src="/js/pdf.js" strategy="afterInteractive"></Script>
<Script src="/js/html2pdf.bundle.min.js" strategy="afterInteractive"></Script>
{googleVerKey && (
<Script
src={`https://www.recaptcha.net/recaptcha/api.js?render=${googleVerKey}`}
strategy="lazyOnload"
></Script>
<>
<Script
src={`https://www.recaptcha.net/recaptcha/api.js?render=${googleVerKey}`}
strategy="afterInteractive"
></Script>
<Script
src={`https://www.google.com/recaptcha/api.js?render=${googleVerKey}`}
strategy="afterInteractive"
></Script>
</>
)}
<Script src="/js/particles.js"></Script>
<QueryClientProvider client={queryClient}>
@@ -72,6 +78,5 @@ export default function App({ Component, pageProps }: AppProps) {
);
}
// export function reportWebVitals(metric: NextWebVitalsMetric) {
// console.log(metric);
// }
// @ts-ignore
export default App;

View File

@@ -1,12 +1,10 @@
function Error({ errStr }: { errStr: string }) {
return <p>{errStr}</p>;
function Error() {
return (
<p>
safari chrome
</p>
);
}
Error.getInitialProps = ({ res, err }: { res: any; err: any }) => {
console.log(err);
return {
errStr: `部分系统不兼容,导致页面崩溃。如果可以,请联系作者,反馈下具体操作和页面。大部分是 苹果 的 safari 浏览器导致,可以尝试更换 chrome 浏览器。`
};
};
export default Error;

View File

@@ -1,195 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { authChat } from '@/service/utils/auth';
import { modelServiceToolMap } from '@/service/utils/chat';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
import { resStreamResponse } from '@/service/utils/chat';
import { appKbSearch } from '../openapi/kb/appKbSearch';
import { ChatRoleEnum, QUOTE_LEN_HEADER, GUIDE_PROMPT_HEADER } from '@/constants/chat';
import { BillTypeEnum } from '@/constants/user';
import { sensitiveCheck } from '../openapi/text/sensitiveCheck';
import { NEW_CHATID_HEADER } from '@/constants/chat';
import { saveChat } from './saveChat';
import { Types } from 'mongoose';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
res.on('close', () => {
res.end();
});
res.on('error', () => {
console.log('error: ', 'request error');
res.end();
});
try {
const { chatId, prompt, modelId } = req.body as {
prompt: [ChatItemType, ChatItemType];
modelId: string;
chatId?: string;
};
if (!modelId || !prompt) {
throw new Error('缺少参数');
}
await connectToDatabase();
let startTime = Date.now();
const { model, showModelDetail, content, userOpenAiKey, systemAuthKey, userId } =
await authChat({
modelId,
chatId,
req
});
const modelConstantsData = ChatModelMap[model.chat.chatModel];
// 读取对话内容
const prompts = [...content, prompt[0]];
const {
code = 200,
systemPrompts = [],
quote = [],
guidePrompt = ''
} = await (async () => {
// 使用了知识库搜索
if (model.chat.relatedKbs?.length > 0) {
const { code, searchPrompts, rawSearch, guidePrompt } = await appKbSearch({
model,
userId,
fixedQuote: content[content.length - 1]?.quote || [],
prompt: prompt[0],
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity
});
return {
code,
quote: rawSearch,
systemPrompts: searchPrompts,
guidePrompt
};
}
if (model.chat.systemPrompt) {
return {
guidePrompt: model.chat.systemPrompt,
systemPrompts: [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
]
};
}
return {};
})();
// get conversationId. create a newId if it is null
const conversationId = chatId || String(new Types.ObjectId());
!chatId && res.setHeader(NEW_CHATID_HEADER, conversationId);
if (showModelDetail) {
guidePrompt && res.setHeader(GUIDE_PROMPT_HEADER, encodeURIComponent(guidePrompt));
res.setHeader(QUOTE_LEN_HEADER, quote.length);
}
// search result is empty
if (code === 201) {
const response = systemPrompts[0]?.value;
await saveChat({
chatId,
newChatId: conversationId,
modelId,
prompts: [
prompt[0],
{
...prompt[1],
quote: [],
value: response
}
],
userId
});
return res.end(response);
}
prompts.unshift(...systemPrompts);
// content check
await sensitiveCheck({
input: [...systemPrompts, prompt[0]].map((item) => item.value).join('')
});
// 计算温度
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
2
);
// 发出 chat 请求
const { streamResponse, responseMessages } = await modelServiceToolMap[
model.chat.chatModel
].chatCompletion({
apiKey: userOpenAiKey || systemAuthKey,
temperature: +temperature,
messages: prompts,
stream: true,
res,
chatId: conversationId
});
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
if (res.closed) return res.end();
try {
const { totalTokens, finishMessages, responseContent } = await resStreamResponse({
model: model.chat.chatModel,
res,
chatResponse: streamResponse,
prompts: responseMessages
});
// save chat
await saveChat({
chatId,
newChatId: conversationId,
modelId,
prompts: [
prompt[0],
{
...prompt[1],
value: responseContent,
quote: showModelDetail ? quote : [],
systemPrompt: showModelDetail ? guidePrompt : ''
}
],
userId
});
res.end();
// 只有使用平台的 key 才计费
pushChatBill({
isPay: !userOpenAiKey,
chatModel: model.chat.chatModel,
userId,
chatId: conversationId,
textLen: finishMessages.map((item) => item.value).join('').length,
tokens: totalTokens,
type: BillTypeEnum.chat
});
} catch (error) {
res.end();
console.log('error结束', error);
}
} catch (err: any) {
res.status(500);
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -6,7 +6,6 @@ import { authUser } from '@/service/utils/auth';
import { ChatItemType } from '@/types/chat';
import { authModel } from '@/service/utils/auth';
import mongoose from 'mongoose';
import { ModelStatusEnum } from '@/constants/model';
import type { ModelSchema } from '@/types/mongoSchema';
/* 初始化我的聊天框,需要身份验证 */
@@ -21,32 +20,32 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase();
let model: ModelSchema;
// 没有 modelId 时直接获取用户的第一个id
if (!modelId) {
const myModel = await Model.findOne({ userId });
if (!myModel) {
const { _id } = await Model.create({
name: '应用1',
userId,
status: ModelStatusEnum.running
});
model = (await Model.findById(_id)) as ModelSchema;
const model = await (async () => {
if (!modelId) {
const myModel = await Model.findOne({ userId });
if (!myModel) {
const { _id } = await Model.create({
name: '应用1',
userId
});
return (await Model.findById(_id)) as ModelSchema;
} else {
return myModel;
}
} else {
model = myModel;
// 校验使用权限
const authRes = await authModel({
modelId,
userId,
authUser: false,
authOwner: false
});
return authRes.model;
}
modelId = model._id;
} else {
// 校验使用权限
const authRes = await authModel({
modelId,
userId,
authUser: false,
authOwner: false
});
model = authRes.model;
}
})();
modelId = modelId || model._id;
// 历史记录
let history: ChatItemType[] = [];
@@ -88,6 +87,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
]);
}
const isOwner = String(model.userId) === userId;
jsonRes<InitChatResponse>(res, {
data: {
chatId: chatId || '',
@@ -95,10 +96,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
model: {
name: model.name,
avatar: model.avatar,
intro: model.share.intro,
canUse: model.share.isShare || String(model.userId) === userId
intro: model.intro,
canUse: model.share.isShare || isOwner
},
chatModel: model.chat.chatModel,
systemPrompt: isOwner ? model.chat.systemPrompt : '',
history
}
});

View File

@@ -4,10 +4,9 @@ import { ChatItemType } from '@/types/chat';
import { connectToDatabase, Chat, Model } from '@/service/mongo';
import { authModel } from '@/service/utils/auth';
import { authUser } from '@/service/utils/auth';
import mongoose from 'mongoose';
import { Types } from 'mongoose';
type Props = {
newChatId?: string;
chatId?: string;
modelId: string;
prompts: [ChatItemType, ChatItemType];
@@ -16,7 +15,7 @@ type Props = {
/* 聊天内容存存储 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { chatId, modelId, prompts, newChatId } = req.body as Props;
const { chatId, modelId, prompts } = req.body as Props;
if (!prompts) {
throw new Error('缺少参数');
@@ -24,16 +23,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { userId } = await authUser({ req, authToken: true });
const nId = await saveChat({
const response = await saveChat({
chatId,
modelId,
prompts,
newChatId,
userId
});
jsonRes(res, {
data: nId
data: response
});
} catch (err) {
jsonRes(res, {
@@ -44,58 +42,54 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
export async function saveChat({
chatId,
newChatId,
chatId,
modelId,
prompts,
userId
}: Props & { userId: string }) {
}: Props & { newChatId?: Types.ObjectId; userId: string }) {
await connectToDatabase();
const { model } = await authModel({ modelId, userId, authOwner: false });
const content = prompts.map((item) => ({
_id: item._id ? new mongoose.Types.ObjectId(item._id) : undefined,
_id: item._id,
obj: item.obj,
value: item.value,
systemPrompt: item.systemPrompt,
systemPrompt: item.systemPrompt || '',
quote: item.quote || []
}));
const [id] = await Promise.all([
...(chatId // update chat
? [
Chat.findByIdAndUpdate(chatId, {
$push: {
content: {
$each: content
}
},
title: content[0].value.slice(0, 20),
latestChat: content[1].value,
updateTime: new Date()
}).then(() => '')
]
: [
Chat.create({
_id: newChatId ? new mongoose.Types.ObjectId(newChatId) : undefined,
userId,
modelId,
content,
title: content[0].value.slice(0, 20),
latestChat: content[1].value
}).then((res) => res._id)
]),
// update model
...(String(model.userId) === userId
? [
Model.findByIdAndUpdate(modelId, {
updateTime: new Date()
})
]
: [])
]);
if (String(model.userId) === userId) {
Model.findByIdAndUpdate(modelId, {
updateTime: new Date()
});
}
const response = await (chatId
? Chat.findByIdAndUpdate(chatId, {
$push: {
content: {
$each: content
}
},
title: content[0].value.slice(0, 20),
latestChat: content[1].value,
updateTime: new Date()
}).then(() => ({
newChatId: ''
}))
: Chat.create({
_id: newChatId,
userId,
modelId,
content,
title: content[0].value.slice(0, 20),
latestChat: content[1].value
}).then((res) => ({
newChatId: String(res._id)
})));
return {
id
...response
};
}

View File

@@ -1,140 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { authShareChat } from '@/service/utils/auth';
import { modelServiceToolMap } from '@/service/utils/chat';
import { ChatItemSimpleType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model';
import { pushChatBill, updateShareChatBill } from '@/service/events/pushBill';
import { resStreamResponse } from '@/service/utils/chat';
import { ChatRoleEnum } from '@/constants/chat';
import { BillTypeEnum } from '@/constants/user';
import { sensitiveCheck } from '../../openapi/text/sensitiveCheck';
import { appKbSearch } from '../../openapi/kb/appKbSearch';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
res.on('error', () => {
console.log('error: ', 'request error');
res.end();
});
try {
const { shareId, password, historyId, prompts } = req.body as {
prompts: ChatItemSimpleType[];
password: string;
shareId: string;
historyId: string;
};
if (!historyId || !prompts) {
throw new Error('分享链接无效');
}
await connectToDatabase();
let startTime = Date.now();
const { model, userOpenAiKey, systemAuthKey, userId } = await authShareChat({
shareId,
password
});
const modelConstantsData = ChatModelMap[model.chat.chatModel];
const { code = 200, systemPrompts = [] } = await (async () => {
// 使用了知识库搜索
if (model.chat.relatedKbs?.length > 0) {
const { code, searchPrompts } = await appKbSearch({
model,
userId,
fixedQuote: [],
prompt: prompts[prompts.length - 1],
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity
});
return {
code,
systemPrompts: searchPrompts
};
}
if (model.chat.systemPrompt) {
return {
systemPrompts: [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
]
};
}
return {};
})();
// search result is empty
if (code === 201) {
return res.send(systemPrompts[0]?.value);
}
prompts.unshift(...systemPrompts);
// content check
await sensitiveCheck({
input: [...systemPrompts, prompts[prompts.length - 1]].map((item) => item.value).join('')
});
// 计算温度
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
2
);
// 发出请求
const { streamResponse, responseMessages } = await modelServiceToolMap[
model.chat.chatModel
].chatCompletion({
apiKey: userOpenAiKey || systemAuthKey,
temperature: +temperature,
messages: prompts,
stream: true,
res,
chatId: historyId
});
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
if (res.closed) return res.end();
try {
const { totalTokens, finishMessages } = await resStreamResponse({
model: model.chat.chatModel,
res,
chatResponse: streamResponse,
prompts: responseMessages
});
res.end();
/* bill */
pushChatBill({
isPay: !userOpenAiKey,
chatModel: model.chat.chatModel,
userId,
textLen: finishMessages.map((item) => item.value).join('').length,
tokens: totalTokens,
type: BillTypeEnum.chat
});
updateShareChatBill({
shareId,
tokens: totalTokens
});
} catch (error) {
res.end();
console.log('error结束', error);
}
} catch (err: any) {
res.status(500);
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -50,7 +50,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
model: {
name: model.name,
avatar: model.avatar,
intro: model.share.intro
intro: model.intro
},
chatModel: model.chat.chatModel
}

View File

@@ -3,7 +3,6 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { ModelStatusEnum } from '@/constants/model';
import { Model } from '@/service/models/model';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -32,8 +31,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 创建模型
const response = await Model.create({
name,
userId,
status: ModelStatusEnum.running
userId
});
jsonRes(res, {

View File

@@ -31,7 +31,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
$and: [
{ 'share.isShare': true },
{
$or: [{ name: { $regex: regex } }, { 'share.intro': { $regex: regex } }]
$or: [{ name: { $regex: regex } }, { intro: { $regex: regex } }]
}
]
};
@@ -66,6 +66,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
avatar: { $ifNull: ['$avatar', '/icon/logo.png'] },
name: 1,
userId: 1,
intro: 1,
share: 1,
isCollection: {
$cond: {
@@ -77,7 +78,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
}
},
{
$sort: { 'share.collection': -1 }
$sort: { 'share.topNum': -1, 'share.collection': -1 }
},
{
$skip: (pageNum - 1) * pageSize

View File

@@ -9,10 +9,10 @@ import { authModel } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { name, avatar, chat, share } = req.body as ModelUpdateParams;
const { name, avatar, chat, share, intro } = req.body as ModelUpdateParams;
const { modelId } = req.query as { modelId: string };
if (!name || !chat || !modelId) {
if (!modelId) {
throw new Error('参数错误');
}
@@ -35,10 +35,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
{
name,
avatar,
intro,
chat,
'share.isShare': share.isShare,
'share.isShareDetail': share.isShareDetail,
'share.intro': share.intro
...(share && {
'share.isShare': share.isShare,
'share.isShareDetail': share.isShareDetail
})
}
);

View File

@@ -2,16 +2,13 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { authUser, authModel, getApiKey } from '@/service/utils/auth';
import { modelServiceToolMap, resStreamResponse } from '@/service/utils/chat';
import { ChatItemSimpleType } from '@/types/chat';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model';
import { ChatModelMap } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
import { ChatRoleEnum } from '@/constants/chat';
import { withNextCors } from '@/service/utils/tools';
import { BillTypeEnum } from '@/constants/user';
import { sensitiveCheck } from '../../openapi/text/sensitiveCheck';
import { NEW_CHATID_HEADER } from '@/constants/chat';
import { Types } from 'mongoose';
import { appKbSearch } from '../kb/appKbSearch';
/* 发送提示词 */
@@ -32,7 +29,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
isStream = true
} = req.body as {
chatId?: string;
prompts: ChatItemSimpleType[];
prompts: ChatItemType[];
modelId: string;
isStream: boolean;
};
@@ -66,67 +63,60 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
});
const modelConstantsData = ChatModelMap[model.chat.chatModel];
const prompt = prompts[prompts.length - 1];
let systemPrompts: {
obj: ChatRoleEnum;
value: string;
}[] = [];
const { userSystemPrompt = [], quotePrompt = [] } = await (async () => {
// 使用了知识库搜索
if (model.chat.relatedKbs?.length > 0) {
const { userSystemPrompt, quotePrompt } = await appKbSearch({
model,
userId,
fixedQuote: [],
prompt: prompt,
similarity: model.chat.searchSimilarity,
limit: model.chat.searchLimit
});
// 使用了知识库搜索
if (model.chat.relatedKbs?.length > 0) {
const { code, searchPrompts } = await appKbSearch({
model,
userId,
fixedQuote: [],
prompt: prompts[prompts.length - 1],
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity
});
// search result is empty
if (code === 201) {
return isStream
? res.send(searchPrompts[0]?.value)
: jsonRes(res, {
data: searchPrompts[0]?.value,
message: searchPrompts[0]?.value
});
return {
userSystemPrompt: userSystemPrompt ? [userSystemPrompt] : [],
quotePrompt: [quotePrompt]
};
}
if (model.chat.systemPrompt) {
return {
userSystemPrompt: [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
]
};
}
return {};
})();
systemPrompts = searchPrompts;
} else if (model.chat.systemPrompt) {
systemPrompts = [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
];
// search result is empty
if (model.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && model.chat.searchEmptyText) {
const response = model.chat.searchEmptyText;
return res.end(response);
}
prompts.unshift(...systemPrompts);
// content check
await sensitiveCheck({
input: [...systemPrompts, prompts[prompts.length - 1]].map((item) => item.value).join('')
});
// 读取对话内容
const completePrompts = [...quotePrompt, ...prompts.slice(0, -1), ...userSystemPrompt, prompt];
// 计算温度
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
2
);
// get conversationId. create a newId if it is null
const conversationId = chatId || String(new Types.ObjectId());
!chatId && res?.setHeader(NEW_CHATID_HEADER, conversationId);
// 发出请求
const { streamResponse, responseMessages, responseText, totalTokens } =
await modelServiceToolMap[model.chat.chatModel].chatCompletion({
apiKey,
temperature: +temperature,
messages: prompts,
messages: completePrompts,
stream: isStream,
res,
chatId: conversationId
res
});
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);

View File

@@ -18,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
({ _id, apiKey, createTime, lastUsedTime }) => {
return {
id: _id,
apiKey: `${apiKey.substring(0, 2)}******${apiKey.substring(apiKey.length - 2)}`,
apiKey: `******${apiKey.substring(apiKey.length - 4)}`,
createTime,
lastUsedTime
};

View File

@@ -3,9 +3,8 @@ import { jsonRes } from '@/service/response';
import { authUser } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
import { withNextCors } from '@/service/utils/tools';
import type { ChatItemSimpleType } from '@/types/chat';
import type { ChatItemType } from '@/types/chat';
import type { ModelSchema } from '@/types/mongoSchema';
import { appVectorSearchModeEnum } from '@/constants/model';
import { authModel } from '@/service/utils/auth';
import { ChatModelMap } from '@/constants/model';
import { ChatRoleEnum } from '@/constants/chat';
@@ -19,18 +18,21 @@ export type QuoteItemType = {
source?: string;
};
type Props = {
prompts: ChatItemSimpleType[];
prompts: ChatItemType[];
similarity: number;
limit: number;
appId: string;
};
type Response = {
code: 200 | 201;
rawSearch: QuoteItemType[];
guidePrompt: string;
searchPrompts: {
userSystemPrompt: {
obj: ChatRoleEnum;
value: string;
}[];
};
quotePrompt: {
obj: ChatRoleEnum;
value: string;
};
};
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -41,7 +43,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
throw new Error('userId is empty');
}
const { prompts, similarity, appId } = req.body as Props;
const { prompts, similarity, limit, appId } = req.body as Props;
if (!similarity || !Array.isArray(prompts) || !appId) {
throw new Error('params is error');
@@ -58,7 +60,8 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
userId,
fixedQuote: [],
prompt: prompts[prompts.length - 1],
similarity
similarity,
limit
});
jsonRes<Response>(res, {
@@ -76,15 +79,17 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
export async function appKbSearch({
model,
userId,
fixedQuote,
fixedQuote = [],
prompt,
similarity
similarity = 0.8,
limit = 5
}: {
model: ModelSchema;
userId: string;
fixedQuote: QuoteItemType[];
prompt: ChatItemSimpleType;
fixedQuote?: QuoteItemType[];
prompt: ChatItemType;
similarity: number;
limit: number;
}): Promise<Response> {
const modelConstantsData = ChatModelMap[model.chat.chatModel];
@@ -103,7 +108,7 @@ export async function appKbSearch({
.map((item) => `'${item}'`)
.join(',')}) AND vector <#> '[${promptVector[0]}]' < -${similarity} order by vector <#> '[${
promptVector[0]
}]' limit 15;
}]' limit ${limit};
COMMIT;`
);
@@ -115,7 +120,7 @@ export async function appKbSearch({
...searchRes.slice(0, 3),
...fixedQuote.slice(0, 2),
...searchRes.slice(3),
...fixedQuote.slice(2, 5)
...fixedQuote.slice(2, Math.floor(fixedQuote.length * 0.4))
].filter((item) => {
if (idSet.has(item.id)) {
return false;
@@ -125,86 +130,44 @@ export async function appKbSearch({
});
// 计算固定提示词的 token 数量
const guidePrompt = model.chat.systemPrompt // user system prompt
const userSystemPrompt = model.chat.systemPrompt // user system prompt
? {
obj: ChatRoleEnum.System,
obj: ChatRoleEnum.Human,
value: model.chat.systemPrompt
}
: model.chat.searchMode === appVectorSearchModeEnum.noContext
? {
obj: ChatRoleEnum.System,
value: `知识库是关于"${model.name}"的内容,根据知识库内容回答问题.`
}
: {
obj: ChatRoleEnum.System,
value: `玩一个问答游戏,规则为:
1.你完全忘记你已有的知识
2.你只回答关于"${model.name}"的问题
3.你只从知识库中选择内容进行回答
4.如果问题不在知识库中,你会回答:"我不知道。"
请务必遵守规则`
obj: ChatRoleEnum.Human,
value: `知识库是关于 ${model.name} 的内容,参考知识库回答问题。与 "${model.name}" 无关内容,直接回复: "我不知道"。`
};
const fixedSystemTokens = modelToolMap[model.chat.chatModel].countTokens({
messages: [guidePrompt]
messages: [userSystemPrompt]
});
// filter part quote by maxToken
const sliceResult = modelToolMap[model.chat.chatModel]
.tokenSlice({
maxToken: modelConstantsData.systemMaxToken - fixedSystemTokens,
messages: filterSearch.map((item) => ({
messages: filterSearch.map((item, i) => ({
obj: ChatRoleEnum.System,
value: `${item.q}\n${item.a}`
value: `${i + 1}: [${item.q}\n${item.a}]`
}))
})
.map((item) => item.value);
.map((item) => item.value)
.join('\n')
.trim();
// slice filterSearch
const rawSearch = filterSearch.slice(0, sliceResult.length);
// system prompt
const systemPrompt = sliceResult.join('\n').trim();
/* 高相似度+不回复 */
if (!systemPrompt && model.chat.searchMode === appVectorSearchModeEnum.hightSimilarity) {
return {
code: 201,
rawSearch: [],
guidePrompt: '',
searchPrompts: [
{
obj: ChatRoleEnum.System,
value: '对不起,你的问题不在知识库中。'
}
]
};
}
/* 高相似度+无上下文,不添加额外知识,仅用系统提示词 */
if (!systemPrompt && model.chat.searchMode === appVectorSearchModeEnum.noContext) {
return {
code: 200,
rawSearch: [],
guidePrompt: model.chat.systemPrompt || '',
searchPrompts: model.chat.systemPrompt
? [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
]
: []
};
}
const quoteText = sliceResult ? `知识库:\n${sliceResult}` : '';
return {
code: 200,
rawSearch,
guidePrompt: guidePrompt.value || '',
searchPrompts: [
{
obj: ChatRoleEnum.System,
value: `知识库:<${systemPrompt}>`
},
guidePrompt
]
userSystemPrompt,
quotePrompt: {
obj: ChatRoleEnum.System,
value: quoteText
}
};
}

View File

@@ -8,6 +8,7 @@ import { TrainingModeEnum } from '@/constants/plugin';
import { startQueue } from '@/service/utils/tools';
import { PgClient } from '@/service/pg';
import { modelToolMap } from '@/utils/plugin';
import { OpenAiChatEnum } from '@/constants/model';
type DateItemType = { a: string; q: string; source?: string };
@@ -23,8 +24,8 @@ export type Response = {
};
const modeMaxToken = {
[TrainingModeEnum.index]: 700,
[TrainingModeEnum.qa]: 3300
[TrainingModeEnum.index]: 6000,
[TrainingModeEnum.qa]: 10000
};
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -75,14 +76,17 @@ export async function pushDataToKb({
data.forEach((item) => {
const text = item.q + item.a;
// count token
const token = modelToolMap['gpt-3.5-turbo'].countTokens({
messages: [{ obj: 'System', value: item.q }]
});
if (mode === TrainingModeEnum.qa) {
// count token
const token = modelToolMap[OpenAiChatEnum.GPT3516k].countTokens({
messages: [{ obj: 'System', value: item.q }]
});
if (token > modeMaxToken[TrainingModeEnum.qa]) {
return;
}
}
if (mode === TrainingModeEnum.qa && token > modeMaxToken[TrainingModeEnum.qa]) {
console.log('q is too long');
} else if (!set.has(text)) {
if (!set.has(text)) {
filterData.push(item);
set.add(text);
}

View File

@@ -7,6 +7,7 @@ import { embeddingModel } from '@/constants/model';
import { axiosConfig } from '@/service/utils/tools';
import { pushGenerateVectorBill } from '@/service/events/pushBill';
import { ApiKeyType } from '@/service/utils/auth';
import { OpenAiChatEnum } from '@/constants/model';
type Props = {
input: string[];
@@ -42,7 +43,7 @@ export async function openaiEmbedding({
type = 'chat'
}: { userId: string; mustPay?: boolean } & Props) {
const { userOpenAiKey, systemAuthKey } = await getApiKey({
model: 'gpt-3.5-turbo',
model: OpenAiChatEnum.GPT35,
userId,
mustPay,
type

View File

@@ -4,7 +4,7 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase, OpenApi } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890');
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@@ -14,11 +14,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const count = await OpenApi.find({ userId }).countDocuments();
if (count >= 5) {
throw new Error('最多 5 组API Key');
if (count >= 10) {
throw new Error('最多 10 API 秘钥');
}
const apiKey = `${userId}-${nanoid()}`;
const apiKey = `fastgpt-${nanoid()}`;
await OpenApi.create({
userId,

View File

@@ -2,17 +2,18 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authUser } from '@/service/utils/auth';
import type { ChatItemSimpleType } from '@/types/chat';
import type { ChatItemType } from '@/types/chat';
import { countOpenAIToken } from '@/utils/plugin/openai';
import { OpenAiChatEnum } from '@/constants/model';
type ModelType = 'gpt-3.5-turbo' | 'gpt-4' | 'gpt-4-32k';
type ModelType = `${OpenAiChatEnum}`;
type Props = {
messages: ChatItemSimpleType[];
messages: ChatItemType[];
model: ModelType;
maxLen: number;
};
type Response = ChatItemSimpleType[];
type Response = ChatItemType[];
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@@ -44,11 +45,11 @@ export function gpt_chatItemTokenSlice({
model,
maxToken
}: {
messages: ChatItemSimpleType[];
messages: ChatItemType[];
model: ModelType;
maxToken: number;
}) {
let result: ChatItemSimpleType[] = [];
let result: ChatItemType[] = [];
for (let i = 0; i < messages.length; i++) {
const msgs = [...result, messages[i]];

View File

@@ -0,0 +1,312 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { authUser, authModel, getApiKey, authShareChat } from '@/service/utils/auth';
import { modelServiceToolMap, V2_StreamResponse } from '@/service/utils/chat';
import { jsonRes } from '@/service/response';
import { ChatModelMap } from '@/constants/model';
import { pushChatBill, updateShareChatBill } from '@/service/events/pushBill';
import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
import { withNextCors } from '@/service/utils/tools';
import { BillTypeEnum } from '@/constants/user';
import { appKbSearch } from '../../../openapi/kb/appKbSearch';
import type { CreateChatCompletionRequest } from 'openai';
import { gptMessage2ChatType, textAdaptGptResponse } from '@/utils/adapt';
import { getChatHistory } from './getHistory';
import { saveChat } from '@/pages/api/chat/saveChat';
import { sseResponse } from '@/service/utils/tools';
import { getErrText } from '@/utils/tools';
import { type ChatCompletionRequestMessage } from 'openai';
import { Types } from 'mongoose';
export type MessageItemType = ChatCompletionRequestMessage & { _id?: string };
type FastGptWebChatProps = {
chatId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history
appId?: string;
};
type FastGptShareChatProps = {
password?: string;
shareId?: string;
};
export type Props = CreateChatCompletionRequest &
FastGptWebChatProps &
FastGptShareChatProps & {
messages: MessageItemType[];
};
export type ChatResponseType = {
newChatId: string;
quoteLen?: number;
};
/* 发送提示词 */
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) {
res.on('close', () => {
res.end();
});
res.on('error', () => {
console.log('error: ', 'request error');
res.end();
});
let { chatId, appId, shareId, password = '', stream = false, messages = [] } = req.body as Props;
let step = 0;
try {
if (!messages) {
throw new Error('Prams Error');
}
if (!Array.isArray(messages)) {
throw new Error('messages is not array');
}
await connectToDatabase();
let startTime = Date.now();
/* user auth */
const {
userId,
appId: authAppid,
authType
} = await (shareId
? authShareChat({
shareId,
password
})
: authUser({ req }));
appId = appId ? appId : authAppid;
if (!appId) {
throw new Error('appId is empty');
}
// auth app permission
const { model, showModelDetail } = await authModel({
userId,
modelId: appId,
authOwner: false,
reserveDetail: true
});
const showAppDetail = !shareId && showModelDetail;
/* get api key */
const { systemAuthKey: apiKey, userOpenAiKey } = await getApiKey({
model: model.chat.chatModel,
userId,
mustPay: authType !== 'token'
});
// get history
const { history } = await getChatHistory({ chatId, userId });
const prompts = history.concat(gptMessage2ChatType(messages));
// adapt fastgpt web
if (prompts[prompts.length - 1].obj === 'AI') {
prompts.pop();
}
// user question
const prompt = prompts[prompts.length - 1];
const {
rawSearch = [],
userSystemPrompt = [],
quotePrompt = []
} = await (async () => {
// 使用了知识库搜索
if (model.chat.relatedKbs?.length > 0) {
const { rawSearch, userSystemPrompt, quotePrompt } = await appKbSearch({
model,
userId,
fixedQuote: history[history.length - 1]?.quote,
prompt,
similarity: model.chat.searchSimilarity,
limit: model.chat.searchLimit
});
return {
rawSearch,
userSystemPrompt: userSystemPrompt ? [userSystemPrompt] : [],
quotePrompt: [quotePrompt]
};
}
if (model.chat.systemPrompt) {
return {
userSystemPrompt: [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
]
};
}
return {};
})();
// search result is empty
if (model.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && model.chat.searchEmptyText) {
const response = model.chat.searchEmptyText;
if (stream) {
sseResponse({
res,
event: sseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: response,
model: model.chat.chatModel,
finish_reason: 'stop'
})
});
return res.end();
} else {
return res.json({
id: chatId || '',
model: model.chat.chatModel,
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
choices: [
{ message: [{ role: 'assistant', content: response }], finish_reason: 'stop', index: 0 }
]
});
}
}
// api messages. [quote,context,systemPrompt,question]
const completePrompts = [...quotePrompt, ...prompts.slice(0, -1), ...userSystemPrompt, prompt];
// chat temperature
const modelConstantsData = ChatModelMap[model.chat.chatModel];
// FastGpt temperature range: 1~10
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
2
);
// start model api. responseText and totalTokens: valid only if stream = false
const { streamResponse, responseMessages, responseText, totalTokens } =
await modelServiceToolMap[model.chat.chatModel].chatCompletion({
apiKey: userOpenAiKey || apiKey,
temperature: +temperature,
maxToken: model.chat.maxToken,
messages: completePrompts,
stream,
res
});
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
if (res.closed) return res.end();
// create a chatId
const newChatId = chatId === '' ? new Types.ObjectId() : undefined;
// response answer
const {
textLen = 0,
answer = responseText,
tokens = totalTokens
} = await (async () => {
if (stream) {
// 创建响应流
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Transfer-Encoding', 'chunked');
res.setHeader('X-Accel-Buffering', 'no');
res.setHeader('Cache-Control', 'no-cache, no-transform');
step = 1;
try {
// response newChatId and quota
sseResponse({
res,
event: sseResponseEventEnum.chatResponse,
data: JSON.stringify({
newChatId,
quoteLen: rawSearch.length
})
});
// response answer
const { finishMessages, totalTokens, responseContent } = await V2_StreamResponse({
model: model.chat.chatModel,
res,
chatResponse: streamResponse,
prompts: responseMessages
});
return {
answer: responseContent,
textLen: finishMessages.map((item) => item.value).join('').length,
tokens: totalTokens
};
} catch (error) {
console.log('stream response error', error);
return {};
}
} else {
return {
textLen: responseMessages.map((item) => item.value).join('').length
};
}
})();
// save chat history
if (typeof chatId === 'string') {
await saveChat({
newChatId,
chatId,
modelId: appId,
prompts: [
prompt,
{
_id: messages[messages.length - 1]._id,
obj: ChatRoleEnum.AI,
value: answer,
...(showAppDetail
? {
quote: rawSearch,
systemPrompt: userSystemPrompt?.[0]?.value
}
: {})
}
],
userId
});
}
// close response
if (stream) {
res.end();
} else {
res.json({
...(showAppDetail
? {
rawSearch
}
: {}),
newChatId,
id: chatId || '',
model: model.chat.chatModel,
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: tokens },
choices: [
{ message: [{ role: 'assistant', content: answer }], finish_reason: 'stop', index: 0 }
]
});
}
pushChatBill({
isPay: !userOpenAiKey,
chatModel: model.chat.chatModel,
userId,
textLen,
tokens,
type: authType === 'apikey' ? BillTypeEnum.openapiChat : BillTypeEnum.chat
});
shareId &&
updateShareChatBill({
shareId,
tokens
});
} catch (err: any) {
res.status(500);
if (step === 1) {
res.end(getErrText(err, 'Stream response error'));
} else {
jsonRes(res, {
code: 500,
error: err
});
}
}
});

View File

@@ -0,0 +1,66 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authUser } from '@/service/utils/auth';
import { connectToDatabase, Chat } from '@/service/mongo';
import { Types } from 'mongoose';
import type { ChatItemType } from '@/types/chat';
export type Props = {
chatId?: string;
limit?: number;
};
export type Response = { history: ChatItemType[] };
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { userId } = await authUser({ req });
const { chatId, limit } = req.body as Props;
jsonRes<Response>(res, {
data: await getChatHistory({
chatId,
userId,
limit
})
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
export async function getChatHistory({
chatId,
userId,
limit = 50
}: Props & { userId: string }): Promise<Response> {
if (!chatId) {
return { history: [] };
}
const history = await Chat.aggregate([
{ $match: { _id: new Types.ObjectId(chatId), userId: new Types.ObjectId(userId) } },
{
$project: {
content: {
$slice: ['$content', -limit] // 返回 content 数组的最后50个元素
}
}
},
{ $unwind: '$content' },
{
$project: {
_id: '$content._id',
obj: '$content.obj',
value: '$content.value',
quote: '$content.quote'
}
}
]);
return { history };
}

View File

@@ -42,16 +42,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
where: [['kb_id', kbId], 'AND', ['user_id', userId]]
});
// 从 pg 中获取所有数据
const pgData = await PgClient.select<{ q: string; a: string }>('modelData', {
const pgData = await PgClient.select<{ q: string; a: string; source: string }>('modelData', {
where: [['kb_id', kbId], 'AND', ['user_id', userId]],
fields: ['q', 'a'],
fields: ['q', 'a', 'source'],
order: [{ field: 'id', mode: 'DESC' }],
limit: count
});
const data: [string, string][] = pgData.rows.map((item) => [
const data: [string, string, string][] = pgData.rows.map((item) => [
item.q.replace(/\n/g, '\\n'),
item.a.replace(/\n/g, '\\n')
item.a.replace(/\n/g, '\\n'),
item.source
]);
// update export time

View File

@@ -8,6 +8,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const chatModelList: ChatModelItemType[] = [];
if (global.systemEnv.openAIKeys) {
chatModelList.push(ChatModelMap[OpenAiChatEnum.GPT3516k]);
chatModelList.push(ChatModelMap[OpenAiChatEnum.GPT35]);
}
if (global.systemEnv.gpt4Key) {

View File

@@ -0,0 +1,25 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Image } from '@/service/mongo';
// get the models available to the system
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { id } = req.query;
const data = await Image.findById(id);
if (!data) {
throw new Error('no image');
}
res.setHeader('Content-Type', 'image/jpeg');
res.send(data.binary);
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
}

View File

@@ -0,0 +1,37 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Image } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
type Props = { base64Img: string };
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
const { base64Img } = req.body as Props;
const data = await uploadImg({
userId,
base64Img
});
jsonRes(res, { data });
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
}
export async function uploadImg({ base64Img, userId }: Props & { userId: string }) {
const base64Data = base64Img.split(',')[1];
const { _id } = await Image.create({
userId,
binary: Buffer.from(base64Data, 'base64')
});
return `/api/system/img/${_id}`;
}

View File

@@ -12,7 +12,7 @@ import { startQueue } from '@/service/utils/tools';
/* 校验支付结果 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
let { payId } = req.query as { payId: string };
const { payId } = req.query as { payId: string };
const { userId } = await authUser({ req, authToken: true });
@@ -34,10 +34,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
throw new Error('找不到用户');
}
// 获取邀请者
let inviter: UserModelSchema | null = null;
if (user.inviterId) {
inviter = await User.findById(user.inviterId);
}
const inviter = await (async () => {
if (user.inviterId) {
return User.findById(user.inviterId, '_id promotion');
}
return null;
})();
const payRes = await getPayResult(payOrder.orderId);
@@ -73,28 +75,35 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
amount: (payOrder.price / PRICE_SCALE) * inviter.promotion.rate * 0.01
});
}
jsonRes(res, {
unlockTask(userId);
return jsonRes(res, {
data: '支付成功'
});
unlockTask(userId);
}
} catch (error) {
await Pay.findByIdAndUpdate(payId, {
status: 'NOTPAY'
});
console.log(error);
// roll back status
try {
await Pay.findByIdAndUpdate(payId, {
status: 'NOTPAY'
});
} catch (error) {}
}
} else if (payRes.trade_state === 'CLOSED' || diffInHours > 24) {
return jsonRes(res, {
code: 500,
data: '更新订单失败,请重试'
});
}
if (payRes.trade_state === 'CLOSED' || diffInHours > 24) {
// 订单已关闭
await Pay.findByIdAndUpdate(payId, {
status: 'CLOSED'
});
jsonRes(res, {
return jsonRes(res, {
data: '订单已过期'
});
} else {
throw new Error(payRes?.trade_state_desc || '订单无效');
}
throw new Error(payRes?.trade_state_desc || '订单无效');
} catch (err) {
// console.log(err);
jsonRes(res, {

View File

@@ -4,23 +4,32 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase, Bill } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { adaptBill } from '@/utils/adapt';
import { addDays } from 'date-fns';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
let { pageNum = 1, pageSize = 10 } = req.query as {
pageNum: string;
pageSize: string;
const {
pageNum = 1,
pageSize = 10,
dateStart = addDays(new Date(), -7),
dateEnd = new Date()
} = req.body as {
pageNum: number;
pageSize: number;
dateStart: Date;
dateEnd: Date;
};
pageNum = +pageNum;
pageSize = +pageSize;
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const where = {
userId
userId,
time: {
$gte: new Date(dateStart).setHours(0, 0, 0, 0),
$lte: new Date(dateEnd).setHours(23, 59, 59, 999)
}
};
// get bill record and total by record

View File

@@ -41,6 +41,7 @@ const PcSliderBar = ({
chatId: string;
};
const ContextMenuRef = useRef(null);
const onclickContext = useRef(false);
const theme = useTheme();
const { isPc } = useGlobalStore();
@@ -68,10 +69,16 @@ const PcSliderBar = ({
// close contextMenu
useOutsideClick({
ref: ContextMenuRef,
handler: () =>
handler: () => {
setTimeout(() => {
setContextMenuData(undefined);
}, 10)
if (contextMenuData && !onclickContext.current) {
setContextMenuData(undefined);
}
}, 10);
setTimeout(() => {
onclickContext.current = false;
}, 10);
}
});
const onclickContextMenu = useCallback(
@@ -80,9 +87,10 @@ const PcSliderBar = ({
if (!isPc) return;
onclickContext.current = true;
setContextMenuData({
left: e.clientX + 15,
top: e.clientY + 10,
left: e.clientX,
top: e.clientY,
history
});
},

View File

@@ -38,9 +38,6 @@ const ModelList = ({ models, modelId }: { models: ModelListItemType[]; modelId:
<Box className="textEllipsis" color={'myGray.1000'}>
{item.name}
</Box>
<Box className="textEllipsis" color={'myGray.400'} fontSize={'sm'}>
{item.systemPrompt || '这个 应用 没有设置提示词~'}
</Box>
</Box>
</Flex>
</Box>

View File

@@ -43,35 +43,6 @@ const QuoteModal = ({
isLoading
} = useQuery(['getHistoryQuote'], () => getHistoryQuote({ historyId, chatId }));
/**
* click edit, get new kbDataItem
*/
const onclickEdit = useCallback(
async (item: QuoteItemType) => {
try {
setIsLoading(true);
const data = (await getKbDataItemById(item.id)) as QuoteItemType;
if (!data) {
throw new Error('该数据已被删除');
}
setEditDataItem({
dataId: data.id,
q: data.q,
a: data.a
});
} catch (err) {
toast({
status: 'warning',
title: getErrText(err)
});
}
setIsLoading(false);
},
[setIsLoading, toast]
);
/**
* update kbData, update mongo status and reload quotes
*/
@@ -98,6 +69,36 @@ const QuoteModal = ({
[chatId, historyId, refetch, setIsLoading, toast]
);
/**
* click edit, get new kbDataItem
*/
const onclickEdit = useCallback(
async (item: QuoteItemType) => {
try {
setIsLoading(true);
const data = (await getKbDataItemById(item.id)) as QuoteItemType;
if (!data) {
updateQuoteStatus(item.id, '已删除');
throw new Error('该数据已被删除');
}
setEditDataItem({
dataId: data.id,
q: data.q,
a: data.a
});
} catch (err) {
toast({
status: 'warning',
title: getErrText(err)
});
}
setIsLoading(false);
},
[setIsLoading, toast, updateQuoteStatus]
);
return (
<>
<Modal isOpen={true} onClose={onClose}>

View File

@@ -39,6 +39,7 @@ const PcSliderBar = ({
const { isPc } = useGlobalStore();
const ContextMenuRef = useRef(null);
const onclickContext = useRef(false);
const [contextMenuData, setContextMenuData] = useState<{
left: number;
@@ -51,10 +52,16 @@ const PcSliderBar = ({
// close contextMenu
useOutsideClick({
ref: ContextMenuRef,
handler: () =>
handler: () => {
setTimeout(() => {
setContextMenuData(undefined);
})
if (contextMenuData && !onclickContext.current) {
setContextMenuData(undefined);
}
}, 10);
setTimeout(() => {
onclickContext.current = false;
}, 10);
}
});
const onclickContextMenu = useCallback(
@@ -62,10 +69,11 @@ const PcSliderBar = ({
e.preventDefault(); // 阻止默认右键菜单
if (!isPc) return;
onclickContext.current = true;
setContextMenuData({
left: e.clientX + 15,
top: e.clientY + 10,
left: e.clientX,
top: e.clientY,
history
});
},

View File

@@ -59,6 +59,7 @@ const History = dynamic(() => import('./components/History'), {
});
import styles from './index.module.scss';
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
const textareaMinH = '22px';
@@ -78,7 +79,6 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
const [showHistoryQuote, setShowHistoryQuote] = useState<string>();
const [showSystemPrompt, setShowSystemPrompt] = useState('');
const [messageContextMenuData, setMessageContextMenuData] = useState<{
// message messageContextMenuData
left: number;
top: number;
message: ChatSiteItemType;
@@ -171,19 +171,15 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
controller.current = abortSignal;
isLeavePage.current = false;
const prompt: ChatItemType[] = prompts.map((item) => ({
_id: item._id,
obj: item.obj,
value: item.value
}));
const messages = adaptChatItem_openAI({ messages: prompts, reserveId: true });
// 流请求,获取数据
const { newChatId, quoteLen, systemPrompt } = await streamFetch({
url: '/api/chat/chat',
const { newChatId, quoteLen } = await streamFetch({
data: {
prompt,
messages,
chatId,
modelId
appId: modelId,
model: ''
},
onMessage: (text: string) => {
setChatData((state) => ({
@@ -223,7 +219,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
...item,
status: 'finish',
quoteLen,
systemPrompt
systemPrompt: chatData.systemPrompt
};
})
}));
@@ -238,6 +234,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
[
chatId,
modelId,
chatData.systemPrompt,
setChatData,
loadHistory,
loadMyModels,
@@ -329,8 +326,8 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
// 删除一句话
const delChatRecord = useCallback(
async (index: number, historyId: string) => {
if (!messageContextMenuData) return;
async (index: number, historyId?: string) => {
if (!messageContextMenuData || !historyId) return;
setIsLoading(true);
try {

View File

@@ -56,6 +56,7 @@ const ShareHistory = dynamic(() => import('./components/ShareHistory'), {
});
import styles from './index.module.scss';
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
const textareaMinH = '22px';
@@ -170,19 +171,15 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
controller.current = abortSignal;
isLeavePage.current = false;
const formatPrompts = prompts.map((item) => ({
obj: item.obj,
value: item.value
}));
const messages = adaptChatItem_openAI({ messages: prompts, reserveId: true });
// 流请求,获取数据
const { responseText } = await streamFetch({
url: '/api/chat/shareChat/chat',
data: {
prompts: formatPrompts.slice(-shareChatData.maxContext - 1, -1),
messages: messages.slice(-shareChatData.maxContext - 1, -1),
password,
shareId,
historyId
model: ''
},
onMessage: (text: string) => {
setShareChatData((state) => ({
@@ -226,7 +223,7 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
setShareChatHistory({
historyId,
shareId,
title: formatPrompts[formatPrompts.length - 2].value,
title: prompts[prompts.length - 2].value,
latestChat: responseText,
chats: responseHistory
});
@@ -235,7 +232,7 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
{
type: 'shareChatFinish',
data: {
question: formatPrompts[0].value,
question: prompts[prompts.length - 2].value,
answer: responseText
}
},

View File

@@ -163,7 +163,7 @@ const Home = () => {
position={'absolute'}
userSelect={'none'}
>
<Image src="/icon/logo.png" w={['70px', '120px']} h={['70px', '120px']} alt={''}></Image>
<Image src="/icon/logo2.png" w={['70px', '120px']} h={['70px', '120px']} alt={''}></Image>
<Box
className={styles.textlg}
fontWeight={'bold'}

View File

@@ -21,7 +21,7 @@ import {
delOneKbDataByDataId,
getTrainingData
} from '@/api/plugins/kb';
import { DeleteIcon } from '@chakra-ui/icons';
import { DeleteIcon, RepeatIcon } from '@chakra-ui/icons';
import { fileDownload } from '@/utils/file';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useToast } from '@/hooks/useToast';
@@ -29,6 +29,7 @@ import Papa from 'papaparse';
import dynamic from 'next/dynamic';
import InputModal, { FormData as InputDataType } from './InputDataModal';
import { debounce } from 'lodash';
import { getErrText } from '@/utils/tools';
const SelectFileModal = dynamic(() => import('./SelectFileModal'));
const SelectCsvModal = dynamic(() => import('./SelectCsvModal'));
@@ -37,6 +38,7 @@ const DataCard = ({ kbId }: { kbId: string }) => {
const lastSearch = useRef('');
const [searchText, setSearchText] = useState('');
const { toast } = useToast();
const [isDeleting, setIsDeleting] = useState(false);
const {
data: kbDataList,
@@ -93,7 +95,7 @@ const DataCard = ({ kbId }: { kbId: string }) => {
onSuccess(res) {
try {
const text = Papa.unparse({
fields: ['question', 'answer'],
fields: ['question', 'answer', 'source'],
data: res
});
fileDownload({
@@ -144,6 +146,18 @@ const DataCard = ({ kbId }: { kbId: string }) => {
: {total}
</Box>
<Box>
<IconButton
icon={<RepeatIcon />}
aria-label={'refresh'}
variant={'base'}
isLoading={isLoading}
mr={[2, 4]}
size={'sm'}
onClick={() => {
refetchData(pageNum);
getTrainingData({ kbId, init: true });
}}
/>
<Button
variant={'base'}
mr={2}
@@ -219,7 +233,7 @@ const DataCard = ({ kbId }: { kbId: string }) => {
pt={3}
userSelect={'none'}
boxShadow={'none'}
_hover={{ boxShadow: 'lg', '& .delete': { display: 'block' } }}
_hover={{ boxShadow: 'lg', '& .delete': { display: 'flex' } }}
border={'1px solid '}
borderColor={'myGray.200'}
onClick={() =>
@@ -249,19 +263,28 @@ const DataCard = ({ kbId }: { kbId: string }) => {
</Box>
<IconButton
className="delete"
display={['block', 'none']}
display={['flex', 'none']}
icon={<DeleteIcon />}
variant={'base'}
colorScheme={'gray'}
aria-label={'delete'}
size={'xs'}
borderRadius={'md'}
lineHeight={1}
_hover={{ color: 'red.600' }}
isLoading={isDeleting}
onClick={async (e) => {
e.stopPropagation();
await delOneKbDataByDataId(item.id);
refetchData(pageNum);
try {
setIsDeleting(true);
await delOneKbDataByDataId(item.id);
refetchData(pageNum);
} catch (error) {
toast({
title: getErrText(error),
status: 'error'
});
}
setIsDeleting(false);
}}
/>
</Flex>

View File

@@ -36,10 +36,11 @@ const Detail = ({ kbId }: { kbId: string }) => {
});
const { reset } = form;
useQuery([kbId, myKbList], () => getKbDetail(kbId), {
useQuery([kbId], () => getKbDetail(kbId), {
onSuccess(res) {
kbId && setLastKbId(kbId);
if (res) {
setCurrentTab(TabEnum.data);
reset(res);
BasicInfo.current?.initInput?.(res.tags);
}

View File

@@ -114,12 +114,14 @@ const Info = (
const file = e[0];
if (!file) return;
try {
const base64 = await compressImg({
const src = await compressImg({
file,
maxW: 100,
maxH: 100
});
setValue('avatar', base64);
setValue('avatar', src);
setRefresh((state) => !state);
} catch (err: any) {
toast({

View File

@@ -29,14 +29,14 @@ const fileExtension = '.txt,.doc,.docx,.pdf,.md';
const modeMap = {
[TrainingModeEnum.qa]: {
maxLen: 2600,
slideLen: 700,
price: ChatModelMap[OpenAiChatEnum.GPT35].price,
maxLen: 8000,
slideLen: 3000,
price: ChatModelMap[OpenAiChatEnum.GPT3516k].price,
isPrompt: true
},
[TrainingModeEnum.index]: {
maxLen: 700,
slideLen: 300,
maxLen: 1000,
slideLen: 500,
price: embeddingPrice,
isPrompt: false
}

View File

@@ -158,8 +158,7 @@ const Test = () => {
'repeat(1,1fr)',
'repeat(1,1fr)',
'repeat(1,1fr)',
'repeat(2,1fr)',
'repeat(3,1fr)'
'repeat(2,1fr)'
]}
gridGap={4}
>

View File

@@ -9,9 +9,14 @@ import { useToast } from '@/hooks/useToast';
import { useQuery } from '@tanstack/react-query';
import { useUserStore } from '@/store/user';
import { MyModelsTypeEnum } from '@/constants/user';
import dynamic from 'next/dynamic';
import Avatar from '@/components/Avatar';
import Tabs from '@/components/Tabs';
const Avatar = dynamic(() => import('@/components/Avatar'), {
ssr: true
});
const Tabs = dynamic(() => import('@/components/Tabs'), {
ssr: true
});
const ModelList = ({ modelId }: { modelId: string }) => {
const [currentTab, setCurrentTab] = useState(MyModelsTypeEnum.my);
@@ -78,7 +83,7 @@ const ModelList = ({ modelId }: { modelId: string }) => {
<Flex flex={1} mr={2} position={'relative'} alignItems={'center'}>
<Input
h={'32px'}
placeholder="搜索 AI 应用"
placeholder="根据名字和介绍搜索 AI 应用"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
@@ -106,7 +111,7 @@ const ModelList = ({ modelId }: { modelId: string }) => {
/>
</Tooltip>
</Flex>
<Flex mb={3} userSelect={'none'}>
<Flex userSelect={'none'}>
<Box flex={1}></Box>
<Tabs
w={'130px'}
@@ -124,7 +129,7 @@ const ModelList = ({ modelId }: { modelId: string }) => {
<Flex
key={item._id}
position={'relative'}
alignItems={['flex-start', 'center']}
alignItems={'center'}
p={3}
mb={[2, 0]}
cursor={'pointer'}
@@ -149,9 +154,6 @@ const ModelList = ({ modelId }: { modelId: string }) => {
<Box className="textEllipsis" color={'myGray.1000'}>
{item.name}
</Box>
<Box className="textEllipsis" color={'myGray.400'} fontSize={'sm'}>
{item.systemPrompt || '这个 应用 没有设置提示词~'}
</Box>
</Box>
</Flex>
))}

View File

@@ -0,0 +1,85 @@
import React, { useEffect, useState } from 'react';
import { Box, Divider, Flex, useTheme, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useCopyData } from '@/utils/tools';
import dynamic from 'next/dynamic';
import MyIcon from '@/components/Icon';
const APIKeyModal = dynamic(() => import('@/components/APIKeyModal'), {
ssr: true
});
const API = ({ modelId }: { modelId: string }) => {
const theme = useTheme();
const { copyData } = useCopyData();
const [baseUrl, setBaseUrl] = useState('https://fastgpt.run/api/openapi');
const {
isOpen: isOpenAPIModal,
onOpen: onOpenAPIModal,
onClose: onCloseAPIModal
} = useDisclosure();
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
setBaseUrl(`${location.origin}/api/openapi`);
}, []);
return (
<Flex flexDirection={'column'} h={'100%'}>
<Box display={['none', 'flex']} px={5} alignItems={'center'}>
<Box flex={1}>
AppId:
<Box
as={'span'}
ml={2}
fontWeight={'bold'}
cursor={'pointer'}
onClick={() => copyData(modelId, '已复制 AppId')}
>
{modelId}
</Box>
</Box>
<Flex
bg={'myWhite.600'}
py={2}
px={4}
borderRadius={'md'}
cursor={'pointer'}
onClick={() => copyData(baseUrl, '已复制 API 地址')}
>
<Box border={theme.borders.md} px={2} borderRadius={'md'} fontSize={'sm'}>
API服务器
</Box>
<Box ml={2} color={'myGray.900'} fontSize={['sm', 'md']}>
{baseUrl}
</Box>
</Flex>
<Button
ml={3}
leftIcon={<MyIcon name={'apikey'} w={'16px'} color={''} />}
variant={'base'}
onClick={onOpenAPIModal}
>
API
</Button>
</Box>
<Divider mt={3} />
<Box flex={1}>
<Skeleton h="100%" isLoaded={isLoaded} fadeDuration={2}>
<iframe
style={{
width: '100%',
height: '100%'
}}
src="https://kjqvjse66l.feishu.cn/docx/DmLedTWtUoNGX8xui9ocdUEjnNh"
frameBorder="0"
onLoad={() => setIsLoaded(true)}
onError={() => setIsLoaded(true)}
/>
</Skeleton>
</Box>
{isOpenAPIModal && <APIKeyModal onClose={onCloseAPIModal} />}
</Flex>
);
};
export default API;

View File

@@ -0,0 +1,394 @@
import React, { useState, useCallback } from 'react';
import { useRouter } from 'next/router';
import {
Card,
Flex,
Box,
Button,
useDisclosure,
Modal,
ModalOverlay,
ModalContent,
ModalBody,
ModalHeader,
ModalFooter,
ModalCloseButton,
Grid,
useTheme,
IconButton,
Tooltip,
Textarea
} from '@chakra-ui/react';
import { useUserStore } from '@/store/user';
import { useQuery } from '@tanstack/react-query';
import Avatar from '@/components/Avatar';
import { AddIcon, DeleteIcon, QuestionOutlineIcon } from '@chakra-ui/icons';
import { putModelById } from '@/api/model';
import { useToast } from '@/hooks/useToast';
import { useLoading } from '@/hooks/useLoading';
import { useForm } from 'react-hook-form';
import MyIcon from '@/components/Icon';
import MySlider from '@/components/Slider';
const Kb = ({ modelId }: { modelId: string }) => {
const theme = useTheme();
const router = useRouter();
const { toast } = useToast();
const { modelDetail, loadKbList, loadModelDetail } = useUserStore();
const { Loading, setIsLoading } = useLoading();
const [selectedIdList, setSelectedIdList] = useState<string[]>([]);
const [refresh, setRefresh] = useState(false);
const { register, reset, getValues, setValue } = useForm({
defaultValues: {
searchSimilarity: modelDetail.chat.searchSimilarity,
searchLimit: modelDetail.chat.searchLimit,
searchEmptyText: modelDetail.chat.searchEmptyText
}
});
const {
isOpen: isOpenKbSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const {
isOpen: isOpenEditParams,
onOpen: onOpenEditParams,
onClose: onCloseEditParams
} = useDisclosure();
const onchangeKb = useCallback(
async (
data: {
relatedKbs?: string[];
searchSimilarity?: number;
searchLimit?: number;
searchEmptyText?: string;
} = {}
) => {
setIsLoading(true);
try {
await putModelById(modelId, {
chat: {
...modelDetail.chat,
...data
}
});
loadModelDetail(modelId, true);
} catch (err: any) {
toast({
title: err?.message || '更新失败',
status: 'error'
});
}
setIsLoading(false);
},
[setIsLoading, modelId, modelDetail.chat, loadModelDetail, toast]
);
// init kb select list
const { isLoading, data: kbList = [] } = useQuery(['loadKbList'], () => loadKbList());
return (
<Box position={'relative'} px={5} minH={'50vh'}>
<Box fontWeight={'bold'}>({modelDetail.chat?.relatedKbs.length})</Box>
{(() => {
const kbs =
modelDetail.chat?.relatedKbs
?.map((id) => kbList.find((kb) => kb._id === id))
.filter((item) => item) || [];
return (
<Grid
mt={2}
gridTemplateColumns={[
'repeat(1,1fr)',
'repeat(2,1fr)',
'repeat(3,1fr)',
'repeat(4,1fr)'
]}
gridGap={[3, 4]}
>
<Card
p={3}
border={theme.borders.base}
boxShadow={'sm'}
cursor={'pointer'}
bg={'myGray.100'}
_hover={{
bg: 'white',
color: 'myBlue.800'
}}
onClick={() => {
reset({
searchSimilarity: modelDetail.chat.searchSimilarity,
searchLimit: modelDetail.chat.searchLimit,
searchEmptyText: modelDetail.chat.searchEmptyText
});
onOpenEditParams();
}}
>
<Flex alignItems={'center'} h={'38px'} fontWeight={'bold'}>
<IconButton
mr={2}
size={'sm'}
borderRadius={'lg'}
icon={<MyIcon name={'edit'} w={'14px'} color={'myGray.600'} />}
aria-label={''}
variant={'base'}
/>
</Flex>
<Flex mt={3} h={'30px'} color={'myGray.600'} fontSize={'sm'}>
: {modelDetail.chat.searchSimilarity}, :{' '}
{modelDetail.chat.searchLimit}, :{' '}
{modelDetail.chat.searchEmptyText !== '' ? 'true' : 'false'}
</Flex>
</Card>
<Card
p={3}
border={theme.borders.base}
boxShadow={'sm'}
cursor={'pointer'}
bg={'myGray.100'}
_hover={{
bg: 'white',
color: 'myBlue.800'
}}
onClick={() => {
setSelectedIdList(
modelDetail.chat?.relatedKbs ? [...modelDetail.chat?.relatedKbs] : []
);
onOpenKbSelect();
}}
>
<Flex alignItems={'center'} h={'38px'} fontWeight={'bold'}>
<IconButton
mr={2}
size={'sm'}
borderRadius={'lg'}
icon={<AddIcon />}
aria-label={''}
variant={'base'}
/>
</Flex>
<Flex mt={3} h={'30px'} color={'myGray.600'} fontSize={'sm'}>
AI
</Flex>
</Card>
{kbs.map((item) =>
item ? (
<Card
key={item._id}
p={3}
border={theme.borders.base}
boxShadow={'sm'}
_hover={{
boxShadow: 'lg',
'& .detailBtn': {
display: 'block'
},
'& .delete': {
display: 'block'
}
}}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['26px', '32px', '38px']}></Avatar>
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
{item.name}
</Box>
</Flex>
<Flex mt={3} alignItems={'flex-end'} justifyContent={'flex-end'} h={'30px'}>
<Button
mr={3}
className="detailBtn"
display={['flex', 'none']}
variant={'base'}
size={'sm'}
onClick={() => router.push(`/kb?kbId=${item._id}`)}
>
</Button>
<IconButton
className="delete"
display={['flex', 'none']}
icon={<DeleteIcon />}
variant={'outline'}
aria-label={'delete'}
size={'sm'}
_hover={{ color: 'red.600' }}
onClick={() => {
const ids = modelDetail.chat?.relatedKbs
? [...modelDetail.chat.relatedKbs]
: [];
const i = ids.findIndex((id) => id === item._id);
ids.splice(i, 1);
onchangeKb({ relatedKbs: ids });
}}
/>
</Flex>
</Card>
) : null
)}
</Grid>
);
})()}
{/* select kb modal */}
<Modal isOpen={isOpenKbSelect} onClose={onCloseKbSelect}>
<ModalOverlay />
<ModalContent
display={'flex'}
flexDirection={'column'}
w={'800px'}
maxW={'90vw'}
h={['90vh', 'auto']}
>
<ModalHeader>({selectedIdList.length})</ModalHeader>
<ModalCloseButton />
<ModalBody
flex={['1 0 0', '0 0 auto']}
maxH={'80vh'}
overflowY={'auto'}
display={'grid'}
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
gridGap={3}
>
{kbList.map((item) => (
<Card
key={item._id}
p={3}
border={theme.borders.base}
boxShadow={'sm'}
h={'80px'}
cursor={'pointer'}
order={modelDetail.chat?.relatedKbs?.includes(item._id) ? 0 : 1}
_hover={{
boxShadow: 'md'
}}
{...(selectedIdList.includes(item._id)
? {
bg: 'myBlue.300'
}
: {})}
onClick={() => {
let ids = [...selectedIdList];
if (!selectedIdList.includes(item._id)) {
ids = ids.concat(item._id);
} else {
const i = ids.findIndex((id) => id === item._id);
ids.splice(i, 1);
}
ids = ids.filter((id) => kbList.find((item) => item._id === id));
setSelectedIdList(ids);
}}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['24px', '28px', '32px']}></Avatar>
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
{item.name}
</Box>
</Flex>
</Card>
))}
</ModalBody>
<ModalFooter>
<Button
onClick={() => {
onCloseKbSelect();
onchangeKb({ relatedKbs: selectedIdList });
}}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* edit mode */}
<Modal isOpen={isOpenEditParams} onClose={onCloseEditParams}>
<ModalOverlay />
<ModalContent display={'flex'} flexDirection={'column'} w={'600px'} maxW={'90vw'}>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody>
<Flex pt={3} pb={5}>
<Box flex={'0 0 100px'}>
<Tooltip label={'高相似度推荐0.8及以上。'}>
<QuestionOutlineIcon ml={1} />
</Tooltip>
</Box>
<MySlider
markList={[
{ label: '0', value: 0 },
{ label: '1', value: 1 }
]}
min={0}
max={1}
step={0.01}
activeVal={getValues('searchSimilarity')}
setVal={(val) => {
setValue('searchSimilarity', val);
setRefresh(!refresh);
}}
/>
</Flex>
<Flex py={8}>
<Box flex={'0 0 100px'}></Box>
<Box flex={1}>
<MySlider
markList={[
{ label: '1', value: 1 },
{ label: '20', value: 20 }
]}
min={1}
max={20}
activeVal={getValues('searchLimit')}
setVal={(val) => {
setValue('searchLimit', val);
setRefresh(!refresh);
}}
/>
</Box>
</Flex>
<Flex pt={3}>
<Box flex={'0 0 100px'}></Box>
<Box flex={1}>
<Textarea
rows={5}
maxLength={500}
placeholder={
'若填写该内容,没有搜索到对应内容时,将直接回复填写的内容。\n为了连贯上下文FastGpt 会取部分上一个聊天的搜索记录作为补充,因此在连续对话时,该功能可能会失效。'
}
{...register('searchEmptyText')}
></Textarea>
</Box>
</Flex>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onCloseEditParams}>
</Button>
<Button
onClick={() => {
onCloseEditParams();
onchangeKb({
searchSimilarity: getValues('searchSimilarity'),
searchLimit: getValues('searchLimit'),
searchEmptyText: getValues('searchEmptyText')
});
}}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
<Loading loading={isLoading} fixed={false} />
</Box>
);
};
export default Kb;

View File

@@ -1,622 +0,0 @@
import React, { useState, useCallback } from 'react';
import {
Box,
Card,
Flex,
FormControl,
Input,
Textarea,
Slider,
SliderTrack,
SliderFilledTrack,
SliderThumb,
SliderMark,
Tooltip,
Button,
Select,
Switch,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
useDisclosure,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Checkbox
} from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { useForm, UseFormReturn } from 'react-hook-form';
import { ChatModelMap, ModelVectorSearchModeMap, getChatModelList } from '@/constants/model';
import { formatPrice } from '@/utils/user';
import { useConfirm } from '@/hooks/useConfirm';
import { useSelectFile } from '@/hooks/useSelectFile';
import { useToast } from '@/hooks/useToast';
import { compressImg } from '@/utils/file';
import { useQuery } from '@tanstack/react-query';
import { getShareChatList, createShareChat, delShareChatById } from '@/api/chat';
import { useRouter } from 'next/router';
import { defaultShareChat } from '@/constants/model';
import type { ShareChatEditType } from '@/types/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { formatTimeToChatTime, useCopyData, getErrText } from '@/utils/tools';
import MyIcon from '@/components/Icon';
import { useGlobalStore } from '@/store/global';
import { useUserStore } from '@/store/user';
import Avatar from '@/components/Avatar';
const ModelEditForm = ({
formHooks,
isOwner,
canRead,
handleDelModel
}: {
formHooks: UseFormReturn<ModelSchema>;
isOwner: boolean;
canRead: boolean;
handleDelModel: () => void;
}) => {
const router = useRouter();
const { modelId } = router.query as { modelId: string };
const [refresh, setRefresh] = useState(false);
const { toast } = useToast();
const { setLoading } = useGlobalStore();
const { loadKbList } = useUserStore();
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认删除该应用?'
});
const { copyData } = useCopyData();
const { register, setValue, getValues } = formHooks;
const {
register: registerShareChat,
getValues: getShareChatValues,
setValue: setShareChatValues,
handleSubmit: submitShareChat,
reset: resetShareChat
} = useForm({
defaultValues: defaultShareChat
});
const {
isOpen: isOpenCreateShareChat,
onOpen: onOpenCreateShareChat,
onClose: onCloseCreateShareChat
} = useDisclosure();
const {
isOpen: isOpenKbSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const base64 = await compressImg({
file,
maxW: 100,
maxH: 100
});
setValue('avatar', base64);
setRefresh((state) => !state);
} catch (err: any) {
toast({
title: getErrText(err, '头像选择异常'),
status: 'warning'
});
}
},
[setValue, toast]
);
const { data: chatModelList = [] } = useQuery(['initChatModelList'], getChatModelList);
const { data: shareChatList = [], refetch: refetchShareChatList } = useQuery(
['initShareChatList', modelId],
() => getShareChatList(modelId)
);
const onclickCreateShareChat = useCallback(
async (e: ShareChatEditType) => {
try {
setLoading(true);
const id = await createShareChat({
...e,
modelId
});
onCloseCreateShareChat();
refetchShareChatList();
const url = `你可以与 ${getValues('name')} 进行对话。
对话地址为:${location.origin}/chat/share?shareId=${id}
${e.password ? `密码为: ${e.password}` : ''}`;
copyData(url, '已复制分享地址');
resetShareChat(defaultShareChat);
} catch (err) {
toast({
title: getErrText(err, '创建分享链接异常'),
status: 'warning'
});
console.log(err);
}
setLoading(false);
},
[
copyData,
getValues,
modelId,
onCloseCreateShareChat,
refetchShareChatList,
resetShareChat,
setLoading,
toast
]
);
// format share used token
const formatTokens = (tokens: number) => {
if (tokens < 10000) return tokens;
return `${(tokens / 10000).toFixed(2)}`;
};
// init kb select list
const { data: kbList = [] } = useQuery(['loadKbList'], () => loadKbList());
const RenderSelectedKbList = useCallback(() => {
const kbs = getValues('chat.relatedKbs')?.map((id) => kbList.find((kb) => kb._id === id)) || [];
return (
<>
{kbs.map((item) =>
item ? (
<Card
key={item._id}
p={3}
mt={3}
cursor={'pointer'}
onClick={() => router.push(`/kb?kbId=${item._id}`)}
>
<Flex alignItems={'center'}>
<Avatar src={item.avatar} w={'20px'} h={'20px'}></Avatar>
<Box ml={3} fontWeight={'bold'}>
{item.name}
</Box>
</Flex>
</Card>
) : null
)}
</>
);
}, [getValues, kbList, router]);
return (
<>
{/* basic info */}
<Card p={4}>
<Box fontWeight={'bold'}></Box>
<Flex alignItems={'center'} mt={4}>
<Box flex={'0 0 80px'} w={0}>
modelId
</Box>
<Box userSelect={'all'}>{getValues('_id')}</Box>
</Flex>
<Flex mt={4} alignItems={'center'}>
<Box flex={'0 0 80px'} w={0}>
</Box>
<Avatar
src={getValues('avatar')}
w={['28px', '36px']}
h={['28px', '36px']}
cursor={isOwner ? 'pointer' : 'default'}
title={'点击切换头像'}
onClick={() => isOwner && onOpenSelectFile()}
/>
</Flex>
<FormControl mt={4}>
<Flex alignItems={'center'}>
<Box flex={'0 0 80px'} w={0}>
</Box>
<Input
isDisabled={!isOwner}
{...register('name', {
required: '展示名称不能为空'
})}
></Input>
</Flex>
</FormControl>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
</Box>
<Select
isDisabled={!isOwner}
{...register('chat.chatModel', {
onChange() {
setRefresh((state) => !state);
}
})}
>
{chatModelList.map((item) => (
<option key={item.chatModel} value={item.chatModel}>
{item.name}
</option>
))}
</Select>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
</Box>
<Box>
{formatPrice(ChatModelMap[getValues('chat.chatModel')]?.price, 1000)}
/1K tokens()
</Box>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
</Box>
<Box>{getValues('share.collection')}</Box>
</Flex>
{isOwner && (
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 80px'}></Box>
<Button
colorScheme={'gray'}
variant={'base'}
size={'sm'}
onClick={openConfirm(handleDelModel)}
>
</Button>
</Flex>
)}
</Card>
{/* model effect */}
{canRead && (
<Card p={4}>
<Box fontWeight={'bold'}></Box>
<FormControl mt={4}>
<Flex alignItems={'center'}>
<Box flex={'0 0 80px'} w={0}>
<Box as={'span'} mr={2}>
</Box>
<Tooltip label={'温度越高,模型的发散能力越强;温度越低,内容越严谨。'}>
<QuestionOutlineIcon />
</Tooltip>
</Box>
<Slider
aria-label="slider-ex-1"
min={0}
max={10}
step={1}
value={getValues('chat.temperature')}
isDisabled={!isOwner}
onChange={(e) => {
setValue('chat.temperature', e);
setRefresh(!refresh);
}}
>
<SliderMark
value={getValues('chat.temperature')}
textAlign="center"
bg="myBlue.600"
color="white"
w={'18px'}
h={'18px'}
borderRadius={'100px'}
fontSize={'xs'}
transform={'translate(-50%, -200%)'}
>
{getValues('chat.temperature')}
</SliderMark>
<SliderTrack>
<SliderFilledTrack bg={'myBlue.700'} />
</SliderTrack>
<SliderThumb />
</Slider>
</Flex>
</FormControl>
{getValues('chat.relatedKbs')?.length > 0 && (
<Flex mt={4} alignItems={'center'}>
<Box mr={4} whiteSpace={'nowrap'}>
&emsp;
</Box>
<Select
isDisabled={!isOwner}
{...register('chat.searchMode', {
required: '搜索模式不能为空'
})}
>
{Object.entries(ModelVectorSearchModeMap).map(([key, { text }]) => (
<option key={key} value={key}>
{text}
</option>
))}
</Select>
</Flex>
)}
<Box mt={4}>
<Box mb={1}></Box>
<Textarea
rows={8}
maxLength={-1}
isDisabled={!isOwner}
placeholder={
'模型默认的 prompt 词,通过调整该内容,可以引导模型聊天方向。\n\n如果使用了知识库搜索没有填写该内容时系统会自动补充提示词如果填写了内容则以填写的内容为准。'
}
{...register('chat.systemPrompt')}
/>
</Box>
</Card>
)}
{isOwner && (
<>
{/* model share setting */}
<Card p={4}>
<Box fontWeight={'bold'}></Box>
<Box>
<Flex mt={5} alignItems={'center'}>
<Box mr={1} fontSize={['sm', 'md']}>
:
</Box>
<Tooltip label="开启模型分享后,你的模型将会出现在共享市场,可供 FastGpt 所有用户使用。用户使用时不会消耗你的 tokens而是消耗使用者的 tokens。">
<QuestionOutlineIcon mr={3} />
</Tooltip>
<Switch
isChecked={getValues('share.isShare')}
onChange={() => {
setValue('share.isShare', !getValues('share.isShare'));
setRefresh(!refresh);
}}
/>
</Flex>
<Box mt={5}>
<Box></Box>
<Textarea
mt={1}
rows={6}
maxLength={150}
{...register('share.intro')}
placeholder={'介绍模型的功能、场景等吸引更多人来使用最多150字。'}
/>
</Box>
</Box>
</Card>
<Card p={4}>
<Flex justifyContent={'space-between'}>
<Box fontWeight={'bold'}></Box>
<Button size={'sm'} variant={'base'} colorScheme={'myBlue'} onClick={onOpenKbSelect}>
</Button>
</Flex>
<RenderSelectedKbList />
</Card>
</>
)}
{/* shareChat */}
<Card p={4} gridColumnStart={1} gridColumnEnd={[2, 3]}>
<Flex justifyContent={'space-between'}>
<Box fontWeight={'bold'}>
<Tooltip label="可以直接分享该模型给其他用户去进行对话对方无需登录即可直接进行对话。注意这个功能会消耗你账号的tokens。请保管好链接和密码。">
<QuestionOutlineIcon ml={1} />
</Tooltip>
Beta
</Box>
<Button
size={'sm'}
variant={'base'}
colorScheme={'myBlue'}
{...(shareChatList.length >= 10
? {
isDisabled: true,
title: '最多创建10组'
}
: {})}
onClick={onOpenCreateShareChat}
>
</Button>
</Flex>
<TableContainer mt={1} minH={'100px'}>
<Table variant={'simple'} w={'100%'}>
<Thead>
<Tr>
<Th></Th>
<Th></Th>
<Th></Th>
<Th>tokens消耗</Th>
<Th>使</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{shareChatList.map((item) => (
<Tr key={item._id}>
<Td>{item.name}</Td>
<Td>{item.password === '1' ? '已开启' : '未使用'}</Td>
<Td>{item.maxContext}</Td>
<Td>{formatTokens(item.tokens)}</Td>
<Td>{item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'}</Td>
<Td>
<Flex>
<MyIcon
mr={3}
name="copy"
w={'14px'}
cursor={'pointer'}
_hover={{ color: 'myBlue.600' }}
onClick={() => {
const url = `${location.origin}/chat/share?shareId=${item._id}`;
copyData(url, '已复制分享地址');
}}
/>
<MyIcon
name="delete"
w={'14px'}
cursor={'pointer'}
_hover={{ color: 'red' }}
onClick={async () => {
setLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
} catch (error) {
console.log(error);
}
setLoading(false);
}}
/>
</Flex>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Card>
{/* create shareChat modal */}
<Modal isOpen={isOpenCreateShareChat} onClose={onCloseCreateShareChat}>
<ModalOverlay />
<ModalContent>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody>
<FormControl>
<Flex alignItems={'center'}>
<Box flex={'0 0 60px'} w={0}>
:
</Box>
<Input
placeholder="记录名字,仅用于展示"
maxLength={20}
{...registerShareChat('name', {
required: '记录名称不能为空'
})}
/>
</Flex>
</FormControl>
<FormControl mt={4}>
<Flex alignItems={'center'}>
<Box flex={'0 0 60px'} w={0}>
:
</Box>
<Input placeholder={'不设置密码,可直接访问'} {...registerShareChat('password')} />
</Flex>
<Box fontSize={'xs'} ml={'60px'}>
</Box>
</FormControl>
<FormControl mt={9}>
<Flex alignItems={'center'}>
<Box flex={'0 0 120px'} w={0}>
</Box>
<Slider
aria-label="slider-ex-1"
min={1}
max={20}
step={1}
value={getShareChatValues('maxContext')}
isDisabled={!isOwner}
onChange={(e) => {
setShareChatValues('maxContext', e);
setRefresh(!refresh);
}}
>
<SliderMark
value={getShareChatValues('maxContext')}
textAlign="center"
bg="myBlue.600"
color="white"
w={'18px'}
h={'18px'}
borderRadius={'100px'}
fontSize={'xs'}
transform={'translate(-50%, -200%)'}
>
{getShareChatValues('maxContext')}
</SliderMark>
<SliderTrack>
<SliderFilledTrack bg={'myBlue.700'} />
</SliderTrack>
<SliderThumb />
</Slider>
</Flex>
</FormControl>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onCloseCreateShareChat}>
</Button>
<Button onClick={submitShareChat(onclickCreateShareChat)}></Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* select kb modal */}
<Modal isOpen={isOpenKbSelect} onClose={onCloseKbSelect}>
<ModalOverlay />
<ModalContent>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody>
{kbList.map((item) => (
<Card key={item._id} p={3} mb={3}>
<Checkbox
isChecked={getValues('chat.relatedKbs')?.includes(item._id)}
onChange={(e) => {
const ids = getValues('chat.relatedKbs');
// toggle to true
if (e.target.checked) {
setValue('chat.relatedKbs', ids.concat(item._id));
} else {
const i = ids.findIndex((id) => id === item._id);
ids.splice(i, 1);
setValue('chat.relatedKbs', ids);
}
setRefresh(!refresh);
}}
>
<Flex alignItems={'center'}>
<Avatar src={item.avatar} w={'20px'} h={'20px'} />
<Box ml={3} fontWeight={'bold'}>
{item.name}
</Box>
</Flex>
</Checkbox>
</Card>
))}
</ModalBody>
<ModalFooter>
<Button onClick={onCloseKbSelect}>,</Button>
</ModalFooter>
</ModalContent>
</Modal>
<File onSelect={onSelectFile} />
<ConfirmChild />
</>
);
};
export default ModelEditForm;

View File

@@ -0,0 +1,366 @@
import React, { useCallback, useState, useMemo } from 'react';
import { Box, Flex, Button, FormControl, Input, Textarea, Divider } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import { useRouter } from 'next/router';
import { useUserStore } from '@/store/user';
import { useToast } from '@/hooks/useToast';
import { useLoading } from '@/hooks/useLoading';
import { delModelById, putModelById } from '@/api/model';
import { useSelectFile } from '@/hooks/useSelectFile';
import { compressImg } from '@/utils/file';
import { getErrText } from '@/utils/tools';
import { useConfirm } from '@/hooks/useConfirm';
import { ChatModelMap, getChatModelList } from '@/constants/model';
import { formatPrice } from '@/utils/user';
import type { ModelSchema } from '@/types/mongoSchema';
import Avatar from '@/components/Avatar';
import MySelect from '@/components/Select';
import MySlider from '@/components/Slider';
const Settings = ({ modelId }: { modelId: string }) => {
const { toast } = useToast();
const router = useRouter();
const { Loading, setIsLoading } = useLoading();
const { userInfo, modelDetail, loadModelDetail, refreshModel, setLastModelId } = useUserStore();
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认删除该应用?'
});
const [btnLoading, setBtnLoading] = useState(false);
const [refresh, setRefresh] = useState(false);
const {
register,
setValue,
getValues,
formState: { errors },
reset,
handleSubmit
} = useForm({
defaultValues: modelDetail
});
const isOwner = useMemo(
() => modelDetail.userId === userInfo?._id,
[modelDetail.userId, userInfo?._id]
);
const tokenLimit = useMemo(() => {
const max = ChatModelMap[getValues('chat.chatModel')]?.contextMaxToken || 4000;
if (max < getValues('chat.maxToken')) {
setValue('chat.maxToken', max);
}
return max;
}, [getValues, setValue, refresh]);
// 提交保存模型修改
const saveSubmitSuccess = useCallback(
async (data: ModelSchema) => {
setBtnLoading(true);
try {
await putModelById(data._id, {
name: data.name,
avatar: data.avatar,
intro: data.intro,
chat: data.chat,
share: data.share
});
refreshModel.updateModelDetail(data);
} catch (err: any) {
toast({
title: err?.message || '更新失败',
status: 'error'
});
}
setBtnLoading(false);
},
[refreshModel, toast]
);
// 提交保存表单失败
const saveSubmitError = useCallback(() => {
// deep search message
const deepSearch = (obj: any): string => {
if (!obj) return '提交表单错误';
if (!!obj.message) {
return obj.message;
}
return deepSearch(Object.values(obj)[0]);
};
toast({
title: deepSearch(errors),
status: 'error',
duration: 4000,
isClosable: true
});
}, [errors, toast]);
const saveUpdateModel = useCallback(
() => handleSubmit(saveSubmitSuccess, saveSubmitError)(),
[handleSubmit, saveSubmitError, saveSubmitSuccess]
);
/* 点击删除 */
const handleDelModel = useCallback(async () => {
if (!modelDetail) return;
setIsLoading(true);
try {
await delModelById(modelDetail._id);
toast({
title: '删除成功',
status: 'success'
});
refreshModel.removeModelDetail(modelDetail._id);
router.replace('/model');
} catch (err: any) {
toast({
title: err?.message || '删除失败',
status: 'error'
});
}
setIsLoading(false);
}, [modelDetail, setIsLoading, toast, refreshModel, router]);
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const src = await compressImg({
file,
maxW: 100,
maxH: 100
});
setValue('avatar', src);
setRefresh((state) => !state);
} catch (err: any) {
toast({
title: getErrText(err, '头像选择异常'),
status: 'warning'
});
}
},
[setValue, toast]
);
// load model data
const { isLoading } = useQuery([modelId], () => loadModelDetail(modelId, true), {
onSuccess(res) {
res && reset(res);
modelId && setLastModelId(modelId);
setRefresh(!refresh);
},
onError(err: any) {
toast({
title: err?.message || '获取应用异常',
status: 'error'
});
setLastModelId('');
refreshModel.freshMyModels();
router.replace('/model');
}
});
const { data: chatModelList = [] } = useQuery(['initChatModelList'], getChatModelList);
return (
<Box
pb={3}
px={[5, '25px', '50px']}
fontSize={['sm', 'lg']}
maxW={['auto', '800px']}
position={'relative'}
>
<Flex alignItems={'center'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<Avatar
src={getValues('avatar')}
w={['32px', '40px']}
h={['32px', '40px']}
cursor={isOwner ? 'pointer' : 'default'}
title={'点击切换头像'}
onClick={() => isOwner && onOpenSelectFile()}
/>
</Flex>
<FormControl mt={5}>
<Flex alignItems={'center'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<Input
isDisabled={!isOwner}
{...register('name', {
required: '展示名称不能为空'
})}
></Input>
</Flex>
</FormControl>
<Flex mt={5} alignItems={'flex-start'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<Textarea
rows={5}
maxLength={500}
placeholder={'给你的 AI 应用一个介绍'}
{...register('intro')}
></Textarea>
</Flex>
<Divider mt={5} />
<Flex alignItems={'center'} mt={5}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<MySelect
width={['200px', '240px']}
value={getValues('chat.chatModel')}
list={chatModelList.map((item) => ({
id: item.chatModel,
label: item.name
}))}
onchange={(val: any) => {
setValue('chat.chatModel', val);
setRefresh(!refresh);
}}
/>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<Box fontSize={['sm', 'md']}>
{formatPrice(ChatModelMap[getValues('chat.chatModel')]?.price, 1000)}
/1K tokens()
</Box>
</Flex>
<Flex alignItems={'center'} my={10}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<Box flex={1} ml={'10px'}>
<MySlider
markList={[
{ label: '严谨', value: 0 },
{ label: '发散', value: 10 }
]}
width={['100%', '260px']}
min={0}
max={10}
activeVal={getValues('chat.temperature')}
setVal={(val) => {
setValue('chat.temperature', val);
setRefresh(!refresh);
}}
/>
</Box>
</Flex>
<Flex alignItems={'center'} mt={12} mb={10}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<Box flex={1} ml={'10px'}>
<MySlider
markList={[
{ label: '100', value: 100 },
{ label: `${tokenLimit}`, value: tokenLimit }
]}
width={['100%', '260px']}
min={100}
max={tokenLimit}
step={50}
activeVal={getValues('chat.maxToken')}
setVal={(val) => {
setValue('chat.maxToken', val);
setRefresh(!refresh);
}}
/>
</Box>
</Flex>
<Flex mt={10} alignItems={'flex-start'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<Textarea
rows={8}
placeholder={
'模型默认的 prompt 词,通过调整该内容,可以引导模型聊天方向。\n\n如果使用了知识库搜索没有填写该内容时系统会自动补充提示词如果填写了内容则以填写的内容为准。'
}
{...register('chat.systemPrompt')}
></Textarea>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}></Box>
<Button
mr={3}
w={'120px'}
size={['sm', 'md']}
isLoading={btnLoading}
isDisabled={!isOwner}
onClick={async () => {
try {
await saveUpdateModel();
toast({
title: '更新成功',
status: 'success'
});
} catch (error) {
console.log(error);
error;
}
}}
>
{isOwner ? '保存' : '仅读,无法修改'}
</Button>
<Button
mr={3}
w={'100px'}
size={['sm', 'md']}
variant={'base'}
color={'myBlue.600'}
borderColor={'myBlue.600'}
isLoading={btnLoading}
onClick={async () => {
try {
router.prefetch('/chat');
await saveUpdateModel();
} catch (error) {}
router.push(`/chat?modelId=${modelId}`);
}}
>
</Button>
{isOwner && (
<Button
colorScheme={'gray'}
variant={'base'}
size={['sm', 'md']}
isLoading={btnLoading}
_hover={{ color: 'red.600' }}
onClick={openConfirm(handleDelModel)}
>
</Button>
)}
</Flex>
<File onSelect={onSelectFile} />
<ConfirmChild />
<Loading loading={isLoading} fixed={false} />
</Box>
);
};
export default Settings;

View File

@@ -0,0 +1,281 @@
import React, { useCallback, useState } from 'react';
import {
Flex,
Box,
Tooltip,
Button,
TableContainer,
Table,
Thead,
Tr,
Th,
Td,
Tbody,
useDisclosure,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
FormControl,
Slider,
SliderTrack,
SliderFilledTrack,
SliderThumb,
SliderMark,
Input
} from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import MyIcon from '@/components/Icon';
import { useToast } from '@/hooks/useToast';
import { useLoading } from '@/hooks/useLoading';
import { useQuery } from '@tanstack/react-query';
import { getShareChatList, delShareChatById, createShareChat } from '@/api/chat';
import { formatTimeToChatTime, useCopyData, getErrText } from '@/utils/tools';
import { useForm } from 'react-hook-form';
import { defaultShareChat } from '@/constants/model';
import type { ShareChatEditType } from '@/types/model';
const Share = ({ modelId }: { modelId: string }) => {
const { toast } = useToast();
const { Loading, setIsLoading } = useLoading();
const { copyData } = useCopyData();
const {
isOpen: isOpenCreateShareChat,
onOpen: onOpenCreateShareChat,
onClose: onCloseCreateShareChat
} = useDisclosure();
const {
register: registerShareChat,
getValues: getShareChatValues,
setValue: setShareChatValues,
handleSubmit: submitShareChat,
reset: resetShareChat
} = useForm({
defaultValues: defaultShareChat
});
const [refresh, setRefresh] = useState(false);
const {
isFetching,
data: shareChatList = [],
refetch: refetchShareChatList
} = useQuery(['initShareChatList', modelId], () => getShareChatList(modelId));
const onclickCreateShareChat = useCallback(
async (e: ShareChatEditType) => {
try {
setIsLoading(true);
const id = await createShareChat({
...e,
modelId
});
onCloseCreateShareChat();
refetchShareChatList();
const url = `对话地址为:${location.origin}/chat/share?shareId=${id}
${e.password ? `密码为: ${e.password}` : ''}`;
copyData(url, '已复制分享地址');
resetShareChat(defaultShareChat);
} catch (err) {
toast({
title: getErrText(err, '创建分享链接异常'),
status: 'warning'
});
console.log(err);
}
setIsLoading(false);
},
[
copyData,
modelId,
onCloseCreateShareChat,
refetchShareChatList,
resetShareChat,
setIsLoading,
toast
]
);
// format share used token
const formatTokens = (tokens: number) => {
if (tokens < 10000) return tokens;
return `${(tokens / 10000).toFixed(2)}`;
};
return (
<Box position={'relative'} px={5} minH={'50vh'}>
<Flex justifyContent={'space-between'}>
<Box fontWeight={'bold'}>
<Tooltip label="可以直接分享该模型给其他用户去进行对话对方无需登录即可直接进行对话。注意这个功能会消耗你账号的tokens。请保管好链接和密码。">
<QuestionOutlineIcon ml={1} />
</Tooltip>
</Box>
<Button
variant={'base'}
colorScheme={'myBlue'}
size={['sm', 'md']}
{...(shareChatList.length >= 10
? {
isDisabled: true,
title: '最多创建10组'
}
: {})}
onClick={onOpenCreateShareChat}
>
</Button>
</Flex>
<TableContainer mt={3}>
<Table variant={'simple'} w={'100%'} overflowX={'auto'}>
<Thead>
<Tr>
<Th></Th>
<Th></Th>
<Th></Th>
<Th>tokens消耗</Th>
<Th>使</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{shareChatList.map((item) => (
<Tr key={item._id}>
<Td>{item.name}</Td>
<Td>{item.password === '1' ? '已开启' : '未使用'}</Td>
<Td>{item.maxContext}</Td>
<Td>{formatTokens(item.tokens)}</Td>
<Td>{item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'}</Td>
<Td>
<Flex>
<MyIcon
mr={3}
name="copy"
w={'14px'}
cursor={'pointer'}
_hover={{ color: 'myBlue.600' }}
onClick={() => {
const url = `${location.origin}/chat/share?shareId=${item._id}`;
copyData(url, '已复制分享地址');
}}
/>
<MyIcon
name="delete"
w={'14px'}
cursor={'pointer'}
_hover={{ color: 'red' }}
onClick={async () => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
} catch (error) {
console.log(error);
}
setIsLoading(false);
}}
/>
</Flex>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{shareChatList.length === 0 && !isFetching && (
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} pt={'10vh'}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
</Box>
</Flex>
)}
{/* create shareChat modal */}
<Modal isOpen={isOpenCreateShareChat} onClose={onCloseCreateShareChat}>
<ModalOverlay />
<ModalContent>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody>
<FormControl>
<Flex alignItems={'center'}>
<Box flex={'0 0 60px'} w={0}>
:
</Box>
<Input
placeholder="记录名字,仅用于展示"
maxLength={20}
{...registerShareChat('name', {
required: '记录名称不能为空'
})}
/>
</Flex>
</FormControl>
<FormControl mt={4}>
<Flex alignItems={'center'}>
<Box flex={'0 0 60px'} w={0}>
:
</Box>
<Input placeholder={'不设置密码,可直接访问'} {...registerShareChat('password')} />
</Flex>
<Box fontSize={'xs'} ml={'60px'} color={'myGray.600'}>
</Box>
</FormControl>
<FormControl mt={9}>
<Flex alignItems={'center'}>
<Box flex={'0 0 120px'} w={0}>
</Box>
<Slider
aria-label="slider-ex-1"
min={1}
max={20}
step={1}
value={getShareChatValues('maxContext')}
onChange={(e) => {
setShareChatValues('maxContext', e);
setRefresh(!refresh);
}}
>
<SliderMark
value={getShareChatValues('maxContext')}
textAlign="center"
bg="myBlue.600"
color="white"
w={'18px'}
h={'18px'}
borderRadius={'100px'}
fontSize={'xs'}
transform={'translate(-50%, -200%)'}
>
{getShareChatValues('maxContext')}
</SliderMark>
<SliderTrack>
<SliderFilledTrack bg={'myBlue.700'} />
</SliderTrack>
<SliderThumb />
</Slider>
</Flex>
</FormControl>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onCloseCreateShareChat}>
</Button>
<Button onClick={submitShareChat(onclickCreateShareChat)}></Button>
</ModalFooter>
</ModalContent>
</Modal>
<Loading loading={isFetching} fixed={false} />
</Box>
);
};
export default Share;

View File

@@ -1,125 +1,41 @@
import React, { useCallback, useState, useMemo, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import { useRouter } from 'next/router';
import { delModelById, putModelById } from '@/api/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { Card, Box, Flex, Button, Grid } from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useForm } from 'react-hook-form';
import { useQuery } from '@tanstack/react-query';
import { Box, Flex } from '@chakra-ui/react';
import { useUserStore } from '@/store/user';
import { useLoading } from '@/hooks/useLoading';
import ModelEditForm from './components/ModelEditForm';
import { useGlobalStore } from '@/store/global';
import dynamic from 'next/dynamic';
import Tabs from '@/components/Tabs';
const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
const { toast } = useToast();
import Settings from './components/Settings';
const Kb = dynamic(() => import('./components/Kb'), {
ssr: true
});
const Share = dynamic(() => import('./components/Share'), {
ssr: true
});
const API = dynamic(() => import('./components/API'), {
ssr: true
});
enum TabEnum {
'settings' = 'settings',
'kb' = 'kb',
'share' = 'share',
'API' = 'API'
}
const ModelDetail = ({ modelId }: { modelId: string }) => {
const router = useRouter();
const { userInfo, modelDetail, loadModelDetail, refreshModel, setLastModelId } = useUserStore();
const { Loading, setIsLoading } = useLoading();
const [btnLoading, setBtnLoading] = useState(false);
const formHooks = useForm({
defaultValues: modelDetail
});
// load model data
const { isLoading } = useQuery([modelId], () => loadModelDetail(modelId), {
onSuccess(res) {
res && formHooks.reset(res);
modelId && setLastModelId(modelId);
},
onError(err: any) {
toast({
title: err?.message || '获取应用异常',
status: 'error'
});
setLastModelId('');
refreshModel.freshMyModels();
router.replace('/model');
}
});
const { isPc } = useGlobalStore();
const { modelDetail, userInfo } = useUserStore();
const [currentTab, setCurrentTab] = useState<`${TabEnum}`>(TabEnum.settings);
const isOwner = useMemo(
() => modelDetail.userId === userInfo?._id,
[modelDetail.userId, userInfo?._id]
);
const canRead = useMemo(
() => isOwner || isLoading || modelDetail.share.isShareDetail,
[isLoading, isOwner, modelDetail.share.isShareDetail]
);
/* 点击删除 */
const handleDelModel = useCallback(async () => {
if (!modelDetail) return;
setIsLoading(true);
try {
await delModelById(modelDetail._id);
toast({
title: '删除成功',
status: 'success'
});
refreshModel.removeModelDetail(modelDetail._id);
router.replace('/model');
} catch (err: any) {
toast({
title: err?.message || '删除失败',
status: 'error'
});
}
setIsLoading(false);
}, [modelDetail, setIsLoading, toast, refreshModel, router]);
/* 点前往聊天预览页 */
const handlePreviewChat = useCallback(async () => {
router.push(`/chat?modelId=${modelId}`);
}, [router, modelId]);
// 提交保存模型修改
const saveSubmitSuccess = useCallback(
async (data: ModelSchema) => {
setBtnLoading(true);
try {
await putModelById(data._id, {
name: data.name,
avatar: data.avatar || '/icon/logo.png',
chat: data.chat,
share: data.share
});
refreshModel.updateModelDetail(data);
} catch (err: any) {
toast({
title: err?.message || '更新失败',
status: 'error'
});
}
setBtnLoading(false);
},
[refreshModel, toast]
);
// 提交保存表单失败
const saveSubmitError = useCallback(() => {
// deep search message
const deepSearch = (obj: any): string => {
if (!obj) return '提交表单错误';
if (!!obj.message) {
return obj.message;
}
return deepSearch(Object.values(obj)[0]);
};
toast({
title: deepSearch(formHooks.formState.errors),
status: 'error',
duration: 4000,
isClosable: true
});
}, [formHooks.formState.errors, toast]);
const saveUpdateModel = useCallback(
() => formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)(),
[formHooks, saveSubmitError, saveSubmitSuccess]
);
useEffect(() => {
window.onbeforeunload = (e) => {
e.preventDefault();
@@ -131,86 +47,54 @@ const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
};
}, [router]);
useEffect(() => {
setCurrentTab(TabEnum.settings);
}, [modelId]);
return (
<Box h={'100%'} p={5} overflow={'overlay'} position={'relative'}>
<Flex
flexDirection={'column'}
h={'100%'}
maxW={'100vw'}
pt={4}
overflow={'overlay'}
position={'relative'}
bg={'white'}
>
{/* 头部 */}
<Card px={6} py={3}>
{isPc ? (
<Flex alignItems={'center'}>
<Box fontSize={'xl'} fontWeight={'bold'}>
{modelDetail.name}
</Box>
<Box flex={1} />
<Button variant={'base'} onClick={handlePreviewChat}>
</Button>
{isOwner && (
<Button
isLoading={btnLoading}
ml={4}
onClick={async () => {
try {
await saveUpdateModel();
toast({
title: '更新成功',
status: 'success'
});
} catch (error) {
console.log(error);
error;
}
}}
>
</Button>
)}
</Flex>
) : (
<>
<Flex alignItems={'center'}>
<Box as={'h3'} fontSize={'xl'} fontWeight={'bold'} flex={1}>
{modelDetail.name}
</Box>
</Flex>
<Box mt={4} textAlign={'right'}>
<Button variant={'base'} size={'sm'} onClick={handlePreviewChat}>
</Button>
{isOwner && (
<Button
ml={4}
size={'sm'}
isLoading={btnLoading}
onClick={async () => {
try {
await saveUpdateModel();
toast({
title: '更新成功',
status: 'success'
});
} catch (error) {
console.log(error);
error;
}
}}
>
</Button>
)}
</Box>
</>
)}
</Card>
<Grid mt={5} gridTemplateColumns={['1fr', '1fr 1fr']} gridGap={5}>
<ModelEditForm
formHooks={formHooks}
handleDelModel={handleDelModel}
isOwner={isOwner}
canRead={canRead}
<Box textAlign={['center', 'left']} px={5} mb={4}>
<Box className="textlg" display={['block', 'none']} fontSize={'3xl'} fontWeight={'bold'}>
{modelDetail.name}
</Box>
<Tabs
mx={['auto', '0']}
mt={2}
w={['300px', '360px']}
list={[
{ label: '配置', id: TabEnum.settings },
...(isOwner ? [{ label: '知识库', id: TabEnum.kb }] : []),
{ label: '分享', id: TabEnum.share },
{ label: 'API', id: TabEnum.API },
{ label: '立即对话', id: 'startChat' }
]}
size={isPc ? 'md' : 'sm'}
activeId={currentTab}
onChange={(e: any) => {
if (e === 'startChat') {
router.push(`/chat?modelId=${modelId}`);
} else {
setCurrentTab(e);
}
}}
/>
</Grid>
<Loading loading={isLoading} fixed={false} />
</Box>
</Box>
<Box flex={1}>
{currentTab === TabEnum.settings && <Settings modelId={modelId} />}
{currentTab === TabEnum.kb && <Kb modelId={modelId} />}
{currentTab === TabEnum.API && <API modelId={modelId} />}
{currentTab === TabEnum.share && <Share modelId={modelId} />}
</Box>
</Flex>
);
};

View File

@@ -10,7 +10,7 @@ import SideBar from '@/components/SideBar';
const ModelDetail = dynamic(() => import('./components/detail/index'), {
loading: () => <Loading fixed={false} />,
ssr: false
ssr: true
});
const Model = ({ modelId }: { modelId: string }) => {
@@ -34,7 +34,7 @@ const Model = ({ modelId }: { modelId: string }) => {
</SideBar>
)}
<Box flex={1} h={'100%'} position={'relative'}>
{modelId && <ModelDetail modelId={modelId} isPc={isPc} />}
{modelId && <ModelDetail modelId={modelId} />}
</Box>
</Flex>
);

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Box, Flex, Button, Tooltip } from '@chakra-ui/react';
import { Box, Flex, Button, Tooltip, Card } from '@chakra-ui/react';
import type { ShareModelItem } from '@/types/model';
import { useRouter } from 'next/router';
import MyIcon from '@/components/Icon';
@@ -18,14 +18,20 @@ const ShareModelList = ({
return (
<>
{models.map((model) => (
<Flex
<Card
key={model._id}
display={'flex'}
w={'100%'}
flexDirection={'column'}
key={model._id}
p={4}
border={'1px solid'}
borderColor={'gray.200'}
borderRadius={'md'}
border={'1px solid '}
userSelect={'none'}
boxShadow={'none'}
borderColor={'myGray.200'}
_hover={{
boxShadow: 'lg'
}}
>
<Flex alignItems={'center'}>
<Avatar
@@ -38,7 +44,7 @@ const ShareModelList = ({
{model.name}
</Box>
</Flex>
<Tooltip label={model.share.intro}>
<Tooltip label={model.intro}>
<Box
className={styles.intro}
flex={1}
@@ -47,7 +53,7 @@ const ShareModelList = ({
wordBreak={'break-all'}
color={'blackAlpha.600'}
>
{model.share.intro || '这个 应用 还没有介绍~'}
{model.intro || '这个应用还没有介绍~'}
</Box>
</Tooltip>
@@ -76,7 +82,7 @@ const ShareModelList = ({
</Button>
</Box>
</Flex>
</Flex>
</Card>
))}
</>
);

View File

@@ -6,6 +6,7 @@ import { usePagination } from '@/hooks/usePagination';
import type { ShareModelItem } from '@/types/model';
import { useUserStore } from '@/store/user';
import ShareModelList from './components/list';
import styles from './index.module.scss';
const modelList = () => {
const { Loading } = useLoading();
@@ -42,51 +43,49 @@ const modelList = () => {
);
return (
<Box py={[5, 10]} px={'5vw'}>
<Card px={6} py={3}>
<Box display={['block', 'flex']} alignItems={'center'} justifyContent={'space-between'}>
<Box fontWeight={'bold'} flex={1} fontSize={'xl'}>
</Box>
<Box mt={[2, 0]} textAlign={'right'}>
<Input
w={['200px', '250px']}
size={'sm'}
value={searchText}
placeholder="搜索应用,回车确认"
onChange={(e) => setSearchText(e.target.value)}
onBlur={() => {
if (searchText === lastSearch.current) return;
<Box px={[5, 10]} py={[4, 6]} position={'relative'} minH={'109vh'}>
<Flex alignItems={'center'} mb={2}>
<Box className={'textlg'} fontWeight={'bold'} fontSize={'3xl'}>
AI
</Box>
{/* <Box mt={[2, 0]} textAlign={'right'}>
<Input
w={['200px', '250px']}
size={'sm'}
value={searchText}
placeholder="搜索应用,回车确认"
onChange={(e) => setSearchText(e.target.value)}
onBlur={() => {
if (searchText === lastSearch.current) return;
getData(1);
lastSearch.current = searchText;
}}
onKeyDown={(e) => {
if (searchText === lastSearch.current) return;
if (e.key === 'Enter') {
getData(1);
lastSearch.current = searchText;
}}
onKeyDown={(e) => {
if (searchText === lastSearch.current) return;
if (e.key === 'Enter') {
getData(1);
lastSearch.current = searchText;
}
}}
/>
</Box>
</Box>
<Grid
templateColumns={[
'repeat(1,1fr)',
'repeat(2,1fr)',
'repeat(3,1fr)',
'repeat(4,1fr)',
'repeat(5,1fr)'
]}
gridGap={4}
mt={4}
>
<ShareModelList models={models} onclickCollection={onclickCollection} />
</Grid>
<Flex mt={4} justifyContent={'flex-end'}>
<Pagination />
</Flex>
</Card>
}
}}
/>
</Box> */}
</Flex>
<Grid
templateColumns={[
'repeat(1,1fr)',
'repeat(2,1fr)',
'repeat(3,1fr)',
'repeat(4,1fr)',
'repeat(5,1fr)'
]}
gridGap={4}
mt={4}
>
<ShareModelList models={models} onclickCollection={onclickCollection} />
</Grid>
<Flex mt={4} justifyContent={'center'}>
<Pagination />
</Flex>
<Loading loading={isLoading} />
</Box>

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { Table, Thead, Tbody, Tr, Th, Td, TableContainer, Flex, Box } from '@chakra-ui/react';
import { BillTypeMap } from '@/constants/user';
import { getUserBills } from '@/api/user';
@@ -7,18 +7,29 @@ import { usePagination } from '@/hooks/usePagination';
import { useLoading } from '@/hooks/useLoading';
import dayjs from 'dayjs';
import MyIcon from '@/components/Icon';
import DateRangePicker, { type DateRangeType } from '@/components/DateRangePicker';
import { addDays } from 'date-fns';
const BillTable = () => {
const { Loading } = useLoading();
const [dateRange, setDateRange] = useState<DateRangeType>({
from: addDays(new Date(), -7),
to: new Date()
});
const {
data: bills,
isLoading,
Pagination,
pageSize,
total
total,
getData
} = usePagination<UserBillType>({
api: getUserBills
api: getUserBills,
params: {
dateStart: dateRange.from,
dateEnd: dateRange.to
}
});
return (
@@ -48,8 +59,6 @@ const BillTable = () => {
))}
</Tbody>
</Table>
<Loading loading={isLoading} fixed={false} />
</TableContainer>
{!isLoading && bills.length === 0 && (
@@ -62,9 +71,18 @@ const BillTable = () => {
)}
{total > pageSize && (
<Flex w={'100%'} mt={4} justifyContent={'flex-end'}>
<Pagination />
<DateRangePicker
defaultDate={dateRange}
position="top"
onChange={setDateRange}
onSuccess={() => getData(1)}
/>
<Box ml={2}>
<Pagination />
</Box>
</Flex>
)}
<Loading loading={isLoading} fixed={false} />
</>
);
};

View File

@@ -58,7 +58,7 @@ const PayModal = ({ onClose }: { onClose: () => void }) => {
},
{
enabled: !!payId,
refetchInterval: 2000,
refetchInterval: 3000,
onSuccess(res) {
if (!res) return;
toast({
@@ -114,7 +114,8 @@ const PayModal = ({ onClose }: { onClose: () => void }) => {
| 计费项 | 价格: 元/ 1K tokens(包含上下文)|
| --- | --- |
| 知识库 - 索引 | 0.001 |
| chatgpt - 对话 | 0.025 |
| chatgpt - 对话 | 0.022 |
| chatgpt16K - 对话 | 0.025 |
| gpt4 - 对话 | 0.5 |
| 文件拆分 | 0.025 |`}
/>

View File

@@ -44,8 +44,6 @@ const OpenApi = () => {
))}
</Tbody>
</Table>
<Loading loading={isLoading} fixed={false} />
</TableContainer>
{!isLoading && promotionRecords.length === 0 && (
@@ -61,6 +59,7 @@ const OpenApi = () => {
<Pagination />
</Flex>
)}
<Loading loading={isLoading} fixed={false} />
</>
);
};

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