feat: v4
This commit is contained in:
parent
4c54e1821b
commit
9bdd5f522d
@ -49,6 +49,7 @@
|
|||||||
"react-hook-form": "^7.43.1",
|
"react-hook-form": "^7.43.1",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
|
"reactflow": "^11.7.4",
|
||||||
"rehype-katex": "^6.0.2",
|
"rehype-katex": "^6.0.2",
|
||||||
"remark-breaks": "^3.0.3",
|
"remark-breaks": "^3.0.3",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
|
|||||||
397
client/pnpm-lock.yaml
generated
397
client/pnpm-lock.yaml
generated
@ -125,6 +125,9 @@ dependencies:
|
|||||||
react-syntax-highlighter:
|
react-syntax-highlighter:
|
||||||
specifier: ^15.5.0
|
specifier: ^15.5.0
|
||||||
version: registry.npmmirror.com/react-syntax-highlighter@15.5.0(react@18.2.0)
|
version: registry.npmmirror.com/react-syntax-highlighter@15.5.0(react@18.2.0)
|
||||||
|
reactflow:
|
||||||
|
specifier: ^11.7.4
|
||||||
|
version: registry.npmmirror.com/reactflow@11.7.4(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0)
|
||||||
rehype-katex:
|
rehype-katex:
|
||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: registry.npmmirror.com/rehype-katex@6.0.2
|
version: registry.npmmirror.com/rehype-katex@6.0.2
|
||||||
@ -4924,6 +4927,126 @@ packages:
|
|||||||
version: 2.11.8
|
version: 2.11.8
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@reactflow/background@11.2.4(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-SYQbCRCU0GuxT/40Tm7ZK+l5wByGnNJSLtZhbL9C/Hl7JhsJXV3UGXr0vrlhVZUBEtkWA7XhZM/5S9XEA5XSFA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@reactflow/background/-/background-11.2.4.tgz}
|
||||||
|
id: registry.npmmirror.com/@reactflow/background/11.2.4
|
||||||
|
name: '@reactflow/background'
|
||||||
|
version: 11.2.4
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17'
|
||||||
|
react-dom: '>=17'
|
||||||
|
dependencies:
|
||||||
|
'@reactflow/core': registry.npmmirror.com/@reactflow/core@11.7.4(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
classcat: registry.npmmirror.com/classcat@5.0.4
|
||||||
|
react: registry.npmmirror.com/react@18.2.0
|
||||||
|
react-dom: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0)
|
||||||
|
zustand: registry.npmmirror.com/zustand@4.3.5(immer@9.0.19)(react@18.2.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- immer
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@reactflow/controls@11.1.15(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-//33XfBYu8vQ6brfmlZwKrDoh+8hh93xO2d88XiqfIbrPEEb32SYjsb9mS9VuHKNlSIW+eB27fBA1Gt00mEj5w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@reactflow/controls/-/controls-11.1.15.tgz}
|
||||||
|
id: registry.npmmirror.com/@reactflow/controls/11.1.15
|
||||||
|
name: '@reactflow/controls'
|
||||||
|
version: 11.1.15
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17'
|
||||||
|
react-dom: '>=17'
|
||||||
|
dependencies:
|
||||||
|
'@reactflow/core': registry.npmmirror.com/@reactflow/core@11.7.4(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
classcat: registry.npmmirror.com/classcat@5.0.4
|
||||||
|
react: registry.npmmirror.com/react@18.2.0
|
||||||
|
react-dom: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0)
|
||||||
|
zustand: registry.npmmirror.com/zustand@4.3.5(immer@9.0.19)(react@18.2.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- immer
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@reactflow/core@11.7.4(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-nt0T8ERp8TE7YCDQViaoEY9lb0StDPrWHVx3zBjhStFYET3wc88t8QRasZdf99xRTmyNtI3U3M40M5EBLNUpMw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@reactflow/core/-/core-11.7.4.tgz}
|
||||||
|
id: registry.npmmirror.com/@reactflow/core/11.7.4
|
||||||
|
name: '@reactflow/core'
|
||||||
|
version: 11.7.4
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17'
|
||||||
|
react-dom: '>=17'
|
||||||
|
dependencies:
|
||||||
|
'@types/d3': registry.npmmirror.com/@types/d3@7.4.0
|
||||||
|
'@types/d3-drag': registry.npmmirror.com/@types/d3-drag@3.0.2
|
||||||
|
'@types/d3-selection': registry.npmmirror.com/@types/d3-selection@3.0.5
|
||||||
|
'@types/d3-zoom': registry.npmmirror.com/@types/d3-zoom@3.0.3
|
||||||
|
classcat: registry.npmmirror.com/classcat@5.0.4
|
||||||
|
d3-drag: registry.npmmirror.com/d3-drag@3.0.0
|
||||||
|
d3-selection: registry.npmmirror.com/d3-selection@3.0.0
|
||||||
|
d3-zoom: registry.npmmirror.com/d3-zoom@3.0.0
|
||||||
|
react: registry.npmmirror.com/react@18.2.0
|
||||||
|
react-dom: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0)
|
||||||
|
zustand: registry.npmmirror.com/zustand@4.3.5(immer@9.0.19)(react@18.2.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- immer
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@reactflow/minimap@11.5.4(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-1tDBj2zX2gxu2oHU6qvH5RGNrOWRfRxu8369KhDotuuBN5yJrGXJzWIKikwhzjsNsQJYOB+B0cS44yWAfwSwzw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@reactflow/minimap/-/minimap-11.5.4.tgz}
|
||||||
|
id: registry.npmmirror.com/@reactflow/minimap/11.5.4
|
||||||
|
name: '@reactflow/minimap'
|
||||||
|
version: 11.5.4
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17'
|
||||||
|
react-dom: '>=17'
|
||||||
|
dependencies:
|
||||||
|
'@reactflow/core': registry.npmmirror.com/@reactflow/core@11.7.4(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@types/d3-selection': registry.npmmirror.com/@types/d3-selection@3.0.5
|
||||||
|
'@types/d3-zoom': registry.npmmirror.com/@types/d3-zoom@3.0.3
|
||||||
|
classcat: registry.npmmirror.com/classcat@5.0.4
|
||||||
|
d3-selection: registry.npmmirror.com/d3-selection@3.0.0
|
||||||
|
d3-zoom: registry.npmmirror.com/d3-zoom@3.0.0
|
||||||
|
react: registry.npmmirror.com/react@18.2.0
|
||||||
|
react-dom: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0)
|
||||||
|
zustand: registry.npmmirror.com/zustand@4.3.5(immer@9.0.19)(react@18.2.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- immer
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@reactflow/node-resizer@2.1.1(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-5Q+IBmZfpp/bYsw3+KRVJB1nUbj6W3XAp5ycx4uNWH+K98vbssymyQsW0vvKkIhxEPg6tkiMzO4UWRWvwBwt1g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@reactflow/node-resizer/-/node-resizer-2.1.1.tgz}
|
||||||
|
id: registry.npmmirror.com/@reactflow/node-resizer/2.1.1
|
||||||
|
name: '@reactflow/node-resizer'
|
||||||
|
version: 2.1.1
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17'
|
||||||
|
react-dom: '>=17'
|
||||||
|
dependencies:
|
||||||
|
'@reactflow/core': registry.npmmirror.com/@reactflow/core@11.7.4(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
classcat: registry.npmmirror.com/classcat@5.0.4
|
||||||
|
d3-drag: registry.npmmirror.com/d3-drag@3.0.0
|
||||||
|
d3-selection: registry.npmmirror.com/d3-selection@3.0.0
|
||||||
|
react: registry.npmmirror.com/react@18.2.0
|
||||||
|
react-dom: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0)
|
||||||
|
zustand: registry.npmmirror.com/zustand@4.3.5(immer@9.0.19)(react@18.2.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- immer
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@reactflow/node-toolbar@1.2.3(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-uFQy9xpog92s0G1wsPLniwV9nyH4i/MmL7QoMsWdnKaOi7XMhd8SJcCzUdHC3imR21HltsuQITff/XQ51ApMbg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@reactflow/node-toolbar/-/node-toolbar-1.2.3.tgz}
|
||||||
|
id: registry.npmmirror.com/@reactflow/node-toolbar/1.2.3
|
||||||
|
name: '@reactflow/node-toolbar'
|
||||||
|
version: 1.2.3
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17'
|
||||||
|
react-dom: '>=17'
|
||||||
|
dependencies:
|
||||||
|
'@reactflow/core': registry.npmmirror.com/@reactflow/core@11.7.4(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
classcat: registry.npmmirror.com/classcat@5.0.4
|
||||||
|
react: registry.npmmirror.com/react@18.2.0
|
||||||
|
react-dom: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0)
|
||||||
|
zustand: registry.npmmirror.com/zustand@4.3.5(immer@9.0.19)(react@18.2.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- immer
|
||||||
|
dev: false
|
||||||
|
|
||||||
registry.npmmirror.com/@rushstack/eslint-patch@1.3.1:
|
registry.npmmirror.com/@rushstack/eslint-patch@1.3.1:
|
||||||
resolution: {integrity: sha512-RkmuBcqiNioeeBKbgzMlOdreUkJfYaSjwgx9XDgGGpjvWgyaxWvDmZVSN9CS6LjEASadhgPv2BcFp+SeouWXXA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@rushstack/eslint-patch/-/eslint-patch-1.3.1.tgz}
|
resolution: {integrity: sha512-RkmuBcqiNioeeBKbgzMlOdreUkJfYaSjwgx9XDgGGpjvWgyaxWvDmZVSN9CS6LjEASadhgPv2BcFp+SeouWXXA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@rushstack/eslint-patch/-/eslint-patch-1.3.1.tgz}
|
||||||
name: '@rushstack/eslint-patch'
|
name: '@rushstack/eslint-patch'
|
||||||
@ -5198,6 +5321,247 @@ packages:
|
|||||||
version: 0.5.1
|
version: 0.5.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-array@3.0.5:
|
||||||
|
resolution: {integrity: sha512-Qk7fpJ6qFp+26VeQ47WY0mkwXaiq8+76RJcncDEfMc2ocRzXLO67bLFRNI4OX1aGBoPzsM5Y2T+/m1pldOgD+A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.0.5.tgz}
|
||||||
|
name: '@types/d3-array'
|
||||||
|
version: 3.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-axis@3.0.2:
|
||||||
|
resolution: {integrity: sha512-uGC7DBh0TZrU/LY43Fd8Qr+2ja1FKmH07q2FoZFHo1eYl8aj87GhfVoY1saJVJiq24rp1+wpI6BvQJMKgQm8oA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-axis/-/d3-axis-3.0.2.tgz}
|
||||||
|
name: '@types/d3-axis'
|
||||||
|
version: 3.0.2
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-selection': registry.npmmirror.com/@types/d3-selection@3.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-brush@3.0.2:
|
||||||
|
resolution: {integrity: sha512-2TEm8KzUG3N7z0TrSKPmbxByBx54M+S9lHoP2J55QuLU0VSQ9mE96EJSAOVNEqd1bbynMjeTS9VHmz8/bSw8rA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-brush/-/d3-brush-3.0.2.tgz}
|
||||||
|
name: '@types/d3-brush'
|
||||||
|
version: 3.0.2
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-selection': registry.npmmirror.com/@types/d3-selection@3.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-chord@3.0.2:
|
||||||
|
resolution: {integrity: sha512-abT/iLHD3sGZwqMTX1TYCMEulr+wBd0SzyOQnjYNLp7sngdOHYtNkMRI5v3w5thoN+BWtlHVDx2Osvq6fxhZWw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-chord/-/d3-chord-3.0.2.tgz}
|
||||||
|
name: '@types/d3-chord'
|
||||||
|
version: 3.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-color@3.1.0:
|
||||||
|
resolution: {integrity: sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.0.tgz}
|
||||||
|
name: '@types/d3-color'
|
||||||
|
version: 3.1.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-contour@3.0.2:
|
||||||
|
resolution: {integrity: sha512-k6/bGDoAGJZnZWaKzeB+9glgXCYGvh6YlluxzBREiVo8f/X2vpTEdgPy9DN7Z2i42PZOZ4JDhVdlTSTSkLDPlQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-contour/-/d3-contour-3.0.2.tgz}
|
||||||
|
name: '@types/d3-contour'
|
||||||
|
version: 3.0.2
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-array': registry.npmmirror.com/@types/d3-array@3.0.5
|
||||||
|
'@types/geojson': registry.npmmirror.com/@types/geojson@7946.0.10
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-delaunay@6.0.1:
|
||||||
|
resolution: {integrity: sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz}
|
||||||
|
name: '@types/d3-delaunay'
|
||||||
|
version: 6.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-dispatch@3.0.2:
|
||||||
|
resolution: {integrity: sha512-rxN6sHUXEZYCKV05MEh4z4WpPSqIw+aP7n9ZN6WYAAvZoEAghEK1WeVZMZcHRBwyaKflU43PCUAJNjFxCzPDjg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.2.tgz}
|
||||||
|
name: '@types/d3-dispatch'
|
||||||
|
version: 3.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-drag@3.0.2:
|
||||||
|
resolution: {integrity: sha512-qmODKEDvyKWVHcWWCOVcuVcOwikLVsyc4q4EBJMREsoQnR2Qoc2cZQUyFUPgO9q4S3qdSqJKBsuefv+h0Qy+tw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.2.tgz}
|
||||||
|
name: '@types/d3-drag'
|
||||||
|
version: 3.0.2
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-selection': registry.npmmirror.com/@types/d3-selection@3.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-dsv@3.0.1:
|
||||||
|
resolution: {integrity: sha512-76pBHCMTvPLt44wFOieouXcGXWOF0AJCceUvaFkxSZEu4VDUdv93JfpMa6VGNFs01FHfuP4a5Ou68eRG1KBfTw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-dsv/-/d3-dsv-3.0.1.tgz}
|
||||||
|
name: '@types/d3-dsv'
|
||||||
|
version: 3.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-ease@3.0.0:
|
||||||
|
resolution: {integrity: sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.0.tgz}
|
||||||
|
name: '@types/d3-ease'
|
||||||
|
version: 3.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-fetch@3.0.2:
|
||||||
|
resolution: {integrity: sha512-gllwYWozWfbep16N9fByNBDTkJW/SyhH6SGRlXloR7WdtAaBui4plTP+gbUgiEot7vGw/ZZop1yDZlgXXSuzjA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-fetch/-/d3-fetch-3.0.2.tgz}
|
||||||
|
name: '@types/d3-fetch'
|
||||||
|
version: 3.0.2
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-dsv': registry.npmmirror.com/@types/d3-dsv@3.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-force@3.0.4:
|
||||||
|
resolution: {integrity: sha512-q7xbVLrWcXvSBBEoadowIUJ7sRpS1yvgMWnzHJggFy5cUZBq2HZL5k/pBSm0GdYWS1vs5/EDwMjSKF55PDY4Aw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-force/-/d3-force-3.0.4.tgz}
|
||||||
|
name: '@types/d3-force'
|
||||||
|
version: 3.0.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-format@3.0.1:
|
||||||
|
resolution: {integrity: sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-format/-/d3-format-3.0.1.tgz}
|
||||||
|
name: '@types/d3-format'
|
||||||
|
version: 3.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-geo@3.0.3:
|
||||||
|
resolution: {integrity: sha512-bK9uZJS3vuDCNeeXQ4z3u0E7OeJZXjUgzFdSOtNtMCJCLvDtWDwfpRVWlyt3y8EvRzI0ccOu9xlMVirawolSCw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-geo/-/d3-geo-3.0.3.tgz}
|
||||||
|
name: '@types/d3-geo'
|
||||||
|
version: 3.0.3
|
||||||
|
dependencies:
|
||||||
|
'@types/geojson': registry.npmmirror.com/@types/geojson@7946.0.10
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-hierarchy@3.1.2:
|
||||||
|
resolution: {integrity: sha512-9hjRTVoZjRFR6xo8igAJyNXQyPX6Aq++Nhb5ebrUF414dv4jr2MitM2fWiOY475wa3Za7TOS2Gh9fmqEhLTt0A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz}
|
||||||
|
name: '@types/d3-hierarchy'
|
||||||
|
version: 3.1.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-interpolate@3.0.1:
|
||||||
|
resolution: {integrity: sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz}
|
||||||
|
name: '@types/d3-interpolate'
|
||||||
|
version: 3.0.1
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-color': registry.npmmirror.com/@types/d3-color@3.1.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-path@3.0.0:
|
||||||
|
resolution: {integrity: sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.0.0.tgz}
|
||||||
|
name: '@types/d3-path'
|
||||||
|
version: 3.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-polygon@3.0.0:
|
||||||
|
resolution: {integrity: sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-polygon/-/d3-polygon-3.0.0.tgz}
|
||||||
|
name: '@types/d3-polygon'
|
||||||
|
version: 3.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-quadtree@3.0.2:
|
||||||
|
resolution: {integrity: sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz}
|
||||||
|
name: '@types/d3-quadtree'
|
||||||
|
version: 3.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-random@3.0.1:
|
||||||
|
resolution: {integrity: sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-random/-/d3-random-3.0.1.tgz}
|
||||||
|
name: '@types/d3-random'
|
||||||
|
version: 3.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-scale-chromatic@3.0.0:
|
||||||
|
resolution: {integrity: sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz}
|
||||||
|
name: '@types/d3-scale-chromatic'
|
||||||
|
version: 3.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-scale@4.0.3:
|
||||||
|
resolution: {integrity: sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.3.tgz}
|
||||||
|
name: '@types/d3-scale'
|
||||||
|
version: 4.0.3
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-time': registry.npmmirror.com/@types/d3-time@3.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-selection@3.0.5:
|
||||||
|
resolution: {integrity: sha512-xCB0z3Hi8eFIqyja3vW8iV01+OHGYR2di/+e+AiOcXIOrY82lcvWW8Ke1DYE/EUVMsBl4Db9RppSBS3X1U6J0w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.5.tgz}
|
||||||
|
name: '@types/d3-selection'
|
||||||
|
version: 3.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-shape@3.1.1:
|
||||||
|
resolution: {integrity: sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.1.tgz}
|
||||||
|
name: '@types/d3-shape'
|
||||||
|
version: 3.1.1
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-path': registry.npmmirror.com/@types/d3-path@3.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-time-format@4.0.0:
|
||||||
|
resolution: {integrity: sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-time-format/-/d3-time-format-4.0.0.tgz}
|
||||||
|
name: '@types/d3-time-format'
|
||||||
|
version: 4.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-time@3.0.0:
|
||||||
|
resolution: {integrity: sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.0.tgz}
|
||||||
|
name: '@types/d3-time'
|
||||||
|
version: 3.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-timer@3.0.0:
|
||||||
|
resolution: {integrity: sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.0.tgz}
|
||||||
|
name: '@types/d3-timer'
|
||||||
|
version: 3.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-transition@3.0.3:
|
||||||
|
resolution: {integrity: sha512-/S90Od8Id1wgQNvIA8iFv9jRhCiZcGhPd2qX0bKF/PS+y0W5CrXKgIiELd2CvG1mlQrWK/qlYh3VxicqG1ZvgA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-transition/-/d3-transition-3.0.3.tgz}
|
||||||
|
name: '@types/d3-transition'
|
||||||
|
version: 3.0.3
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-selection': registry.npmmirror.com/@types/d3-selection@3.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3-zoom@3.0.3:
|
||||||
|
resolution: {integrity: sha512-OWk1yYIIWcZ07+igN6BeoG6rqhnJ/pYe+R1qWFM2DtW49zsoSjgb9G5xB0ZXA8hh2jAzey1XuRmMSoXdKw8MDA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.3.tgz}
|
||||||
|
name: '@types/d3-zoom'
|
||||||
|
version: 3.0.3
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-interpolate': registry.npmmirror.com/@types/d3-interpolate@3.0.1
|
||||||
|
'@types/d3-selection': registry.npmmirror.com/@types/d3-selection@3.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/d3@7.4.0:
|
||||||
|
resolution: {integrity: sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/d3/-/d3-7.4.0.tgz}
|
||||||
|
name: '@types/d3'
|
||||||
|
version: 7.4.0
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-array': registry.npmmirror.com/@types/d3-array@3.0.5
|
||||||
|
'@types/d3-axis': registry.npmmirror.com/@types/d3-axis@3.0.2
|
||||||
|
'@types/d3-brush': registry.npmmirror.com/@types/d3-brush@3.0.2
|
||||||
|
'@types/d3-chord': registry.npmmirror.com/@types/d3-chord@3.0.2
|
||||||
|
'@types/d3-color': registry.npmmirror.com/@types/d3-color@3.1.0
|
||||||
|
'@types/d3-contour': registry.npmmirror.com/@types/d3-contour@3.0.2
|
||||||
|
'@types/d3-delaunay': registry.npmmirror.com/@types/d3-delaunay@6.0.1
|
||||||
|
'@types/d3-dispatch': registry.npmmirror.com/@types/d3-dispatch@3.0.2
|
||||||
|
'@types/d3-drag': registry.npmmirror.com/@types/d3-drag@3.0.2
|
||||||
|
'@types/d3-dsv': registry.npmmirror.com/@types/d3-dsv@3.0.1
|
||||||
|
'@types/d3-ease': registry.npmmirror.com/@types/d3-ease@3.0.0
|
||||||
|
'@types/d3-fetch': registry.npmmirror.com/@types/d3-fetch@3.0.2
|
||||||
|
'@types/d3-force': registry.npmmirror.com/@types/d3-force@3.0.4
|
||||||
|
'@types/d3-format': registry.npmmirror.com/@types/d3-format@3.0.1
|
||||||
|
'@types/d3-geo': registry.npmmirror.com/@types/d3-geo@3.0.3
|
||||||
|
'@types/d3-hierarchy': registry.npmmirror.com/@types/d3-hierarchy@3.1.2
|
||||||
|
'@types/d3-interpolate': registry.npmmirror.com/@types/d3-interpolate@3.0.1
|
||||||
|
'@types/d3-path': registry.npmmirror.com/@types/d3-path@3.0.0
|
||||||
|
'@types/d3-polygon': registry.npmmirror.com/@types/d3-polygon@3.0.0
|
||||||
|
'@types/d3-quadtree': registry.npmmirror.com/@types/d3-quadtree@3.0.2
|
||||||
|
'@types/d3-random': registry.npmmirror.com/@types/d3-random@3.0.1
|
||||||
|
'@types/d3-scale': registry.npmmirror.com/@types/d3-scale@4.0.3
|
||||||
|
'@types/d3-scale-chromatic': registry.npmmirror.com/@types/d3-scale-chromatic@3.0.0
|
||||||
|
'@types/d3-selection': registry.npmmirror.com/@types/d3-selection@3.0.5
|
||||||
|
'@types/d3-shape': registry.npmmirror.com/@types/d3-shape@3.1.1
|
||||||
|
'@types/d3-time': registry.npmmirror.com/@types/d3-time@3.0.0
|
||||||
|
'@types/d3-time-format': registry.npmmirror.com/@types/d3-time-format@4.0.0
|
||||||
|
'@types/d3-timer': registry.npmmirror.com/@types/d3-timer@3.0.0
|
||||||
|
'@types/d3-transition': registry.npmmirror.com/@types/d3-transition@3.0.3
|
||||||
|
'@types/d3-zoom': registry.npmmirror.com/@types/d3-zoom@3.0.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
registry.npmmirror.com/@types/debug@4.1.8:
|
registry.npmmirror.com/@types/debug@4.1.8:
|
||||||
resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/debug/-/debug-4.1.8.tgz}
|
resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/debug/-/debug-4.1.8.tgz}
|
||||||
name: '@types/debug'
|
name: '@types/debug'
|
||||||
@ -5214,6 +5578,12 @@ packages:
|
|||||||
'@types/node': registry.npmmirror.com/@types/node@18.14.0
|
'@types/node': registry.npmmirror.com/@types/node@18.14.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
registry.npmmirror.com/@types/geojson@7946.0.10:
|
||||||
|
resolution: {integrity: sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.10.tgz}
|
||||||
|
name: '@types/geojson'
|
||||||
|
version: 7946.0.10
|
||||||
|
dev: false
|
||||||
|
|
||||||
registry.npmmirror.com/@types/hast@2.3.4:
|
registry.npmmirror.com/@types/hast@2.3.4:
|
||||||
resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/hast/-/hast-2.3.4.tgz}
|
resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/hast/-/hast-2.3.4.tgz}
|
||||||
name: '@types/hast'
|
name: '@types/hast'
|
||||||
@ -6045,6 +6415,12 @@ packages:
|
|||||||
fsevents: registry.npmmirror.com/fsevents@2.3.2
|
fsevents: registry.npmmirror.com/fsevents@2.3.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/classcat@5.0.4:
|
||||||
|
resolution: {integrity: sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/classcat/-/classcat-5.0.4.tgz}
|
||||||
|
name: classcat
|
||||||
|
version: 5.0.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
registry.npmmirror.com/client-only@0.0.1:
|
registry.npmmirror.com/client-only@0.0.1:
|
||||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz}
|
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz}
|
||||||
name: client-only
|
name: client-only
|
||||||
@ -10700,6 +11076,27 @@ packages:
|
|||||||
loose-envify: registry.npmmirror.com/loose-envify@1.4.0
|
loose-envify: registry.npmmirror.com/loose-envify@1.4.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/reactflow@11.7.4(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-QI6+oc1Ft6oFeLSdHlp+SmgymbI5Tm49wj5JyE84O4A54yN/ImfYaBhLit9Cmfzxn9Tz6tDqmGMGbk4bdtB8/w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/reactflow/-/reactflow-11.7.4.tgz}
|
||||||
|
id: registry.npmmirror.com/reactflow/11.7.4
|
||||||
|
name: reactflow
|
||||||
|
version: 11.7.4
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17'
|
||||||
|
react-dom: '>=17'
|
||||||
|
dependencies:
|
||||||
|
'@reactflow/background': registry.npmmirror.com/@reactflow/background@11.2.4(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@reactflow/controls': registry.npmmirror.com/@reactflow/controls@11.1.15(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@reactflow/core': registry.npmmirror.com/@reactflow/core@11.7.4(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@reactflow/minimap': registry.npmmirror.com/@reactflow/minimap@11.5.4(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@reactflow/node-resizer': registry.npmmirror.com/@reactflow/node-resizer@2.1.1(immer@9.0.19)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@reactflow/node-toolbar': registry.npmmirror.com/@reactflow/node-toolbar@1.2.3(immer@9.0.19)(react-dom@18.2.0)(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)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- immer
|
||||||
|
dev: false
|
||||||
|
|
||||||
registry.npmmirror.com/readable-stream@1.1.14:
|
registry.npmmirror.com/readable-stream@1.1.14:
|
||||||
resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/readable-stream/-/readable-stream-1.1.14.tgz}
|
resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/readable-stream/-/readable-stream-1.1.14.tgz}
|
||||||
name: readable-stream
|
name: readable-stream
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { GET, POST, DELETE, PUT } from './request';
|
import { GET, POST, DELETE, PUT } from './request';
|
||||||
import type { ModelSchema } from '@/types/mongoSchema';
|
import type { AppSchema } from '@/types/mongoSchema';
|
||||||
import type { ModelUpdateParams } from '@/types/model';
|
import type { ModelUpdateParams } from '@/types/model';
|
||||||
import { RequestPaging } from '../types/index';
|
import { RequestPaging } from '../types/index';
|
||||||
import type { ModelListResponse } from './response/model';
|
import type { ModelListResponse } from './response/model';
|
||||||
@ -22,13 +22,13 @@ export const delModelById = (id: string) => DELETE(`/model/del?modelId=${id}`);
|
|||||||
/**
|
/**
|
||||||
* 根据 ID 获取模型
|
* 根据 ID 获取模型
|
||||||
*/
|
*/
|
||||||
export const getModelById = (id: string) => GET<ModelSchema>(`/model/detail?modelId=${id}`);
|
export const getModelById = (id: string) => GET<AppSchema>(`/model/detail?modelId=${id}`);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据 ID 更新模型
|
* 根据 ID 更新模型
|
||||||
*/
|
*/
|
||||||
export const putModelById = (id: string, data: ModelUpdateParams) =>
|
export const putAppById = (id: string, data: ModelUpdateParams) =>
|
||||||
PUT(`/model/update?modelId=${id}`, data);
|
PUT(`/model/update?appId=${id}`, data);
|
||||||
|
|
||||||
/* 共享市场 */
|
/* 共享市场 */
|
||||||
/**
|
/**
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Props, ChatResponseType } from '@/pages/api/openapi/v1/chat/completions';
|
import { Props } from '@/pages/api/openapi/v1/chat/completions';
|
||||||
import { sseResponseEventEnum } from '@/constants/chat';
|
import { sseResponseEventEnum } from '@/constants/chat';
|
||||||
import { getErrText } from '@/utils/tools';
|
import { getErrText } from '@/utils/tools';
|
||||||
import { parseStreamChunk } from '@/utils/adapt';
|
import { parseStreamChunk } from '@/utils/adapt';
|
||||||
@ -9,10 +9,10 @@ interface StreamFetchProps {
|
|||||||
abortSignal: AbortController;
|
abortSignal: AbortController;
|
||||||
}
|
}
|
||||||
export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps) =>
|
export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps) =>
|
||||||
new Promise<ChatResponseType & { responseText: string; errMsg: string }>(
|
new Promise<{ responseText: string; errMsg: string; newChatId: string | null }>(
|
||||||
async (resolve, reject) => {
|
async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const response = await window.fetch('/api/openapi/v1/chat/test', {
|
const response = await window.fetch('/api/openapi/v1/chat/completions2', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@ -37,9 +37,8 @@ export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps)
|
|||||||
|
|
||||||
// response data
|
// response data
|
||||||
let responseText = '';
|
let responseText = '';
|
||||||
let newChatId = '';
|
|
||||||
let quoteLen = 0;
|
|
||||||
let errMsg = '';
|
let errMsg = '';
|
||||||
|
const newChatId = response.headers.get('newChatId');
|
||||||
|
|
||||||
const read = async () => {
|
const read = async () => {
|
||||||
try {
|
try {
|
||||||
@ -48,9 +47,8 @@ export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps)
|
|||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return resolve({
|
return resolve({
|
||||||
responseText,
|
responseText,
|
||||||
newChatId,
|
errMsg,
|
||||||
quoteLen,
|
newChatId
|
||||||
errMsg
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return reject('响应过程出现异常~');
|
return reject('响应过程出现异常~');
|
||||||
@ -72,11 +70,6 @@ export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps)
|
|||||||
const answer: string = data?.choices?.[0].delta.content || '';
|
const answer: string = data?.choices?.[0].delta.content || '';
|
||||||
onMessage(answer);
|
onMessage(answer);
|
||||||
responseText += answer;
|
responseText += answer;
|
||||||
} else if (item.event === sseResponseEventEnum.chatResponse) {
|
|
||||||
const chatResponse = data as ChatResponseType;
|
|
||||||
newChatId =
|
|
||||||
chatResponse.newChatId !== undefined ? chatResponse.newChatId : newChatId;
|
|
||||||
quoteLen = chatResponse.quoteLen !== undefined ? chatResponse.quoteLen : quoteLen;
|
|
||||||
} else if (item.event === sseResponseEventEnum.error) {
|
} else if (item.event === sseResponseEventEnum.error) {
|
||||||
errMsg = getErrText(data, '流响应错误');
|
errMsg = getErrText(data, '流响应错误');
|
||||||
}
|
}
|
||||||
@ -86,9 +79,8 @@ export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps)
|
|||||||
if (err?.message === 'The user aborted a request.') {
|
if (err?.message === 'The user aborted a request.') {
|
||||||
return resolve({
|
return resolve({
|
||||||
responseText,
|
responseText,
|
||||||
newChatId,
|
errMsg,
|
||||||
quoteLen,
|
newChatId
|
||||||
errMsg
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
reject(getErrText(err, '请求异常'));
|
reject(getErrText(err, '请求异常'));
|
||||||
|
|||||||
6
client/src/api/response/chat.d.ts
vendored
6
client/src/api/response/chat.d.ts
vendored
@ -1,4 +1,4 @@
|
|||||||
import type { ChatPopulate, ModelSchema } from '@/types/mongoSchema';
|
import type { ChatPopulate, AppSchema } from '@/types/mongoSchema';
|
||||||
import type { ChatItemType } from '@/types/chat';
|
import type { ChatItemType } from '@/types/chat';
|
||||||
|
|
||||||
export interface InitChatResponse {
|
export interface InitChatResponse {
|
||||||
@ -12,7 +12,7 @@ export interface InitChatResponse {
|
|||||||
intro: string;
|
intro: string;
|
||||||
canUse: boolean;
|
canUse: boolean;
|
||||||
};
|
};
|
||||||
chatModel: ModelSchema['chat']['chatModel']; // 对话模型名
|
chatModel: AppSchema['chat']['chatModel']; // 对话模型名
|
||||||
history: ChatItemType[];
|
history: ChatItemType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,5 +24,5 @@ export interface InitShareChatResponse {
|
|||||||
avatar: string;
|
avatar: string;
|
||||||
intro: string;
|
intro: string;
|
||||||
};
|
};
|
||||||
chatModel: ModelSchema['chat']['chatModel']; // 对话模型名
|
chatModel: AppSchema['chat']['chatModel']; // 对话模型名
|
||||||
}
|
}
|
||||||
|
|||||||
4
client/src/api/response/model.d.ts
vendored
4
client/src/api/response/model.d.ts
vendored
@ -1,6 +1,6 @@
|
|||||||
import { ModelListItemType } from '@/types/model';
|
import { ModelListItemType } from '@/types/model';
|
||||||
|
|
||||||
export type ModelListResponse = {
|
export type ModelListResponse = {
|
||||||
myModels: ModelListItemType[];
|
myApps: ModelListItemType[];
|
||||||
myCollectionModels: ModelListItemType[];
|
myCollectionApps: ModelListItemType[];
|
||||||
};
|
};
|
||||||
|
|||||||
1
client/src/components/Icon/icons/save.svg
Normal file
1
client/src/components/Icon/icons/save.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1688217440856" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2309" xmlns:xlink="http://www.w3.org/1999/xlink" ><path d="M819.823 83.694H206.991c-67.703 0-122.588 54.885-122.588 122.588v612.833c0 67.703 54.885 122.588 122.588 122.588h612.833c67.703 0 122.588-54.885 122.588-122.588V206.282c-0.001-67.703-54.885-122.588-122.589-122.588z m-124.435 63.313v241.142H331.772V147.007h363.616z m185.787 672.274c0.027 33.765-27.323 61.158-61.088 61.185H207.133c-16.389 0-31.864-6.297-43.454-17.887s-18.039-26.91-18.039-43.298v-612.94c0.061-33.923 27.57-61.395 61.493-61.41h61.327v245.294c-0.05 33.771 27.286 61.187 61.057 61.237h367.888c33.853 0 61.299-27.387 61.299-61.237V144.931h61.206c33.872 0.036 61.301 27.524 61.265 61.396V819.281z" fill="" p-id="2310"></path><path d="M574.817 329.936c17.483 0 31.656-14.173 31.656-31.656v-61.292c0-17.483-14.173-31.656-31.656-31.656s-31.656 14.173-31.656 31.656v61.292c0 17.483 14.173 31.656 31.656 31.656z" fill="" p-id="2311"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -34,7 +34,8 @@ const map = {
|
|||||||
history: require('./icons/history.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,
|
date: require('./icons/date.svg').default,
|
||||||
apikey: require('./icons/apikey.svg').default
|
apikey: require('./icons/apikey.svg').default,
|
||||||
|
save: require('./icons/save.svg').default
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IconName = keyof typeof map;
|
export type IconName = keyof typeof map;
|
||||||
|
|||||||
@ -14,7 +14,8 @@ import { getUnreadCount } from '@/api/user';
|
|||||||
const pcUnShowLayoutRoute: Record<string, boolean> = {
|
const pcUnShowLayoutRoute: Record<string, boolean> = {
|
||||||
'/': true,
|
'/': true,
|
||||||
'/login': true,
|
'/login': true,
|
||||||
'/chat/share': true
|
'/chat/share': true,
|
||||||
|
'/app/edit': true
|
||||||
};
|
};
|
||||||
const phoneUnShowLayoutRoute: Record<string, boolean> = {
|
const phoneUnShowLayoutRoute: Record<string, boolean> = {
|
||||||
'/': true,
|
'/': true,
|
||||||
@ -60,10 +61,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Box h={'100%'} bg={'#F3F4F5E0'}>
|
||||||
h={'100%'}
|
|
||||||
bgGradient={'linear(to-t,rgba(173, 206, 255, 0.05) 0%, rgba(173, 206, 255, 0.12) 100%)'}
|
|
||||||
>
|
|
||||||
<Box h={'100%'} display={['none', 'block']}>
|
<Box h={'100%'} display={['none', 'block']}>
|
||||||
{pcUnShowLayoutRoute[router.pathname] ? (
|
{pcUnShowLayoutRoute[router.pathname] ? (
|
||||||
<Auth>{children}</Auth>
|
<Auth>{children}</Auth>
|
||||||
|
|||||||
@ -29,6 +29,12 @@ const Navbar = ({ unread }: { unread: number }) => {
|
|||||||
{
|
{
|
||||||
label: '应用',
|
label: '应用',
|
||||||
icon: 'model',
|
icon: 'model',
|
||||||
|
link: `/app/list`,
|
||||||
|
activeLink: ['/app/list']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '旧应用',
|
||||||
|
icon: 'model',
|
||||||
link: `/model?modelId=${lastModelId}`,
|
link: `/model?modelId=${lastModelId}`,
|
||||||
activeLink: ['/model']
|
activeLink: ['/model']
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,14 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { Menu, MenuButton, MenuList, MenuItem, Button, useDisclosure } from '@chakra-ui/react';
|
import {
|
||||||
|
Menu,
|
||||||
|
Box,
|
||||||
|
MenuList,
|
||||||
|
MenuItem,
|
||||||
|
MenuButton,
|
||||||
|
Button,
|
||||||
|
useDisclosure,
|
||||||
|
useOutsideClick
|
||||||
|
} from '@chakra-ui/react';
|
||||||
import type { ButtonProps } from '@chakra-ui/react';
|
import type { ButtonProps } from '@chakra-ui/react';
|
||||||
import { ChevronDownIcon } from '@chakra-ui/icons';
|
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||||
interface Props extends ButtonProps {
|
interface Props extends ButtonProps {
|
||||||
@ -7,13 +16,14 @@ interface Props extends ButtonProps {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
list: {
|
list: {
|
||||||
label: string;
|
label: string;
|
||||||
id: string;
|
value: string;
|
||||||
}[];
|
}[];
|
||||||
onchange?: (val: string) => void;
|
onchange?: (val: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MySelect = ({ placeholder, value, width = 'auto', list, onchange, ...props }: Props) => {
|
const MySelect = ({ placeholder, value, width = 'auto', list, onchange, ...props }: Props) => {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const SelectRef = useRef(null);
|
||||||
const menuItemStyles = {
|
const menuItemStyles = {
|
||||||
borderRadius: 'sm',
|
borderRadius: 'sm',
|
||||||
py: 2,
|
py: 2,
|
||||||
@ -25,9 +35,16 @@ const MySelect = ({ placeholder, value, width = 'auto', list, onchange, ...props
|
|||||||
};
|
};
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
|
||||||
|
useOutsideClick({
|
||||||
|
ref: SelectRef,
|
||||||
|
handler: () => {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu autoSelect={false} onOpen={onOpen} onClose={onClose}>
|
<Menu autoSelect={false} isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
||||||
<MenuButton style={{ width: '100%', position: 'relative' }} as={'span'}>
|
<Box ref={SelectRef} position={'relative'} onClick={() => (isOpen ? onClose() : onOpen())}>
|
||||||
<Button
|
<Button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
width={width}
|
width={width}
|
||||||
@ -36,6 +53,9 @@ const MySelect = ({ placeholder, value, width = 'auto', list, onchange, ...props
|
|||||||
display={'flex'}
|
display={'flex'}
|
||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
justifyContent={'space-between'}
|
justifyContent={'space-between'}
|
||||||
|
_active={{
|
||||||
|
transform: ''
|
||||||
|
}}
|
||||||
{...(isOpen
|
{...(isOpen
|
||||||
? {
|
? {
|
||||||
boxShadow: '0px 0px 4px #A8DBFF',
|
boxShadow: '0px 0px 4px #A8DBFF',
|
||||||
@ -44,44 +64,48 @@ const MySelect = ({ placeholder, value, width = 'auto', list, onchange, ...props
|
|||||||
: {})}
|
: {})}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{list.find((item) => item.id === value)?.label || placeholder}
|
{list.find((item) => item.value === value)?.label || placeholder}
|
||||||
<ChevronDownIcon />
|
<ChevronDownIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</MenuButton>
|
|
||||||
<MenuList
|
<MenuList
|
||||||
minW={(() => {
|
minW={(() => {
|
||||||
const w = ref.current?.clientWidth;
|
const w = ref.current?.clientWidth;
|
||||||
if (w) {
|
if (w) {
|
||||||
return `${w}px !important`;
|
return `${w}px !important`;
|
||||||
|
}
|
||||||
|
return 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);'
|
||||||
}
|
}
|
||||||
return Array.isArray(width)
|
zIndex={99}
|
||||||
? width.map((item) => `${item} !important`)
|
transform={'translateY(35px) !important'}
|
||||||
: `${width} !important`;
|
>
|
||||||
})()}
|
{list.map((item) => (
|
||||||
p={'6px'}
|
<MenuItem
|
||||||
border={'1px solid #fff'}
|
key={item.value}
|
||||||
boxShadow={'0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);'}
|
{...menuItemStyles}
|
||||||
zIndex={99}
|
{...(value === item.value
|
||||||
>
|
? {
|
||||||
{list.map((item) => (
|
color: 'myBlue.600'
|
||||||
<MenuItem
|
}
|
||||||
key={item.id}
|
: {})}
|
||||||
{...menuItemStyles}
|
onClick={() => {
|
||||||
{...(value === item.id
|
if (onchange && value !== item.value) {
|
||||||
? {
|
onchange(item.value);
|
||||||
color: 'myBlue.600'
|
|
||||||
}
|
}
|
||||||
: {})}
|
}}
|
||||||
onClick={() => {
|
>
|
||||||
if (onchange && value !== item.id) {
|
{item.label}
|
||||||
onchange(item.id);
|
</MenuItem>
|
||||||
}
|
))}
|
||||||
}}
|
</MenuList>
|
||||||
>
|
</Box>
|
||||||
{item.label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,8 +10,8 @@ import {
|
|||||||
|
|
||||||
const MySlider = ({
|
const MySlider = ({
|
||||||
markList = [],
|
markList = [],
|
||||||
setVal,
|
onChange,
|
||||||
activeVal,
|
value,
|
||||||
max = 100,
|
max = 100,
|
||||||
min = 0,
|
min = 0,
|
||||||
step = 1,
|
step = 1,
|
||||||
@ -21,8 +21,8 @@ const MySlider = ({
|
|||||||
label: string | number;
|
label: string | number;
|
||||||
value: number;
|
value: number;
|
||||||
}[];
|
}[];
|
||||||
activeVal: number;
|
value: number;
|
||||||
setVal: (index: number) => void;
|
onChange?: (index: number) => void;
|
||||||
max?: number;
|
max?: number;
|
||||||
min?: number;
|
min?: number;
|
||||||
step?: number;
|
step?: number;
|
||||||
@ -40,10 +40,6 @@ const MySlider = ({
|
|||||||
top: 0,
|
top: 0,
|
||||||
transform: 'translateY(-3px)'
|
transform: 'translateY(-3px)'
|
||||||
};
|
};
|
||||||
const value = useMemo(() => {
|
|
||||||
const index = markList.findIndex((item) => item.value === activeVal);
|
|
||||||
return index > -1 ? index : 0;
|
|
||||||
}, [activeVal, markList]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slider
|
<Slider
|
||||||
@ -51,9 +47,9 @@ const MySlider = ({
|
|||||||
min={min}
|
min={min}
|
||||||
step={step}
|
step={step}
|
||||||
size={'lg'}
|
size={'lg'}
|
||||||
value={activeVal}
|
value={value}
|
||||||
width={width}
|
width={width}
|
||||||
onChange={setVal}
|
onChange={onChange}
|
||||||
>
|
>
|
||||||
{markList?.map((item, i) => (
|
{markList?.map((item, i) => (
|
||||||
<SliderMark
|
<SliderMark
|
||||||
@ -71,7 +67,7 @@ const MySlider = ({
|
|||||||
</SliderMark>
|
</SliderMark>
|
||||||
))}
|
))}
|
||||||
<SliderMark
|
<SliderMark
|
||||||
value={activeVal}
|
value={value}
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
bg="myBlue.600"
|
bg="myBlue.600"
|
||||||
color="white"
|
color="white"
|
||||||
@ -84,7 +80,7 @@ const MySlider = ({
|
|||||||
transform={'translate(-50%, -170%)'}
|
transform={'translate(-50%, -170%)'}
|
||||||
boxSizing={'border-box'}
|
boxSizing={'border-box'}
|
||||||
>
|
>
|
||||||
{activeVal}
|
{value}
|
||||||
</SliderMark>
|
</SliderMark>
|
||||||
<SliderTrack
|
<SliderTrack
|
||||||
bg={'#EAEDF3'}
|
bg={'#EAEDF3'}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
12
client/src/constants/data.ts
Normal file
12
client/src/constants/data.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export enum ChatModelEnum {
|
||||||
|
'GPT35' = 'gpt-3.5-turbo',
|
||||||
|
'GPT3516k' = 'gpt-3.5-turbo-16k',
|
||||||
|
'GPT4' = 'gpt-4',
|
||||||
|
'GPT432k' = 'gpt-4-32k'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chatModelList = [
|
||||||
|
{ label: 'Gpt35-16k', value: ChatModelEnum.GPT3516k },
|
||||||
|
{ label: 'Gpt35-4k', value: ChatModelEnum.GPT35 },
|
||||||
|
{ label: 'Gpt4-8k', value: ChatModelEnum.GPT4 }
|
||||||
|
];
|
||||||
311
client/src/constants/flow/ModuleTemplate.ts
Normal file
311
client/src/constants/flow/ModuleTemplate.ts
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
import { AppModuleItemTypeEnum, SystemInputEnum } from '../app';
|
||||||
|
import { FlowModuleTypeEnum, FlowInputItemTypeEnum, FlowOutputItemTypeEnum } from './index';
|
||||||
|
import type { AppModuleTemplateItemType } from '@/types/app';
|
||||||
|
import { chatModelList } from '../data';
|
||||||
|
import {
|
||||||
|
Input_Template_History,
|
||||||
|
Input_Template_TFSwitch,
|
||||||
|
Input_Template_UserChatInput
|
||||||
|
} from './inputTemplate';
|
||||||
|
|
||||||
|
export const UserInputModule: AppModuleTemplateItemType = {
|
||||||
|
logo: '',
|
||||||
|
name: '用户问题',
|
||||||
|
intro: '用户输入的内容。该模块通常作为应用的入口,用户在发送消息后会首先执行该模块。',
|
||||||
|
type: AppModuleItemTypeEnum.initInput,
|
||||||
|
flowType: FlowModuleTypeEnum.questionInputNode,
|
||||||
|
url: '/openapi/modules/init/userChatInput',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
key: SystemInputEnum.userChatInput,
|
||||||
|
type: FlowInputItemTypeEnum.systemInput,
|
||||||
|
label: '用户问题'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
key: SystemInputEnum.userChatInput,
|
||||||
|
label: '用户问题',
|
||||||
|
type: FlowOutputItemTypeEnum.source,
|
||||||
|
targets: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
export const HistoryModule: AppModuleTemplateItemType = {
|
||||||
|
logo: '',
|
||||||
|
name: '聊天记录',
|
||||||
|
intro: '用户输入的内容。该模块通常作为应用的入口,用户在发送消息后会首先执行该模块。',
|
||||||
|
type: AppModuleItemTypeEnum.initInput,
|
||||||
|
flowType: FlowModuleTypeEnum.historyNode,
|
||||||
|
url: '/openapi/modules/init/history',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
key: 'maxContext',
|
||||||
|
type: FlowInputItemTypeEnum.numberInput,
|
||||||
|
label: '最长记录数',
|
||||||
|
value: 4,
|
||||||
|
min: 0,
|
||||||
|
max: 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SystemInputEnum.history,
|
||||||
|
type: FlowInputItemTypeEnum.hidden,
|
||||||
|
label: '聊天记录'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
key: SystemInputEnum.history,
|
||||||
|
label: '聊天记录',
|
||||||
|
type: FlowOutputItemTypeEnum.source,
|
||||||
|
targets: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChatModule: AppModuleTemplateItemType = {
|
||||||
|
logo: '',
|
||||||
|
name: 'AI 对话',
|
||||||
|
intro: 'OpenAI GPT 大模型对话。',
|
||||||
|
flowType: FlowModuleTypeEnum.chatNode,
|
||||||
|
type: AppModuleItemTypeEnum.http,
|
||||||
|
url: '/openapi/modules/chat/gpt',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
key: 'model',
|
||||||
|
type: FlowInputItemTypeEnum.select,
|
||||||
|
label: '对话模型',
|
||||||
|
value: chatModelList[0].value,
|
||||||
|
list: chatModelList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'temperature',
|
||||||
|
type: FlowInputItemTypeEnum.slider,
|
||||||
|
label: '温度',
|
||||||
|
value: 0,
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
markList: [
|
||||||
|
{ label: '严谨', value: 0 },
|
||||||
|
{ label: '发散', value: 10 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'maxToken',
|
||||||
|
type: FlowInputItemTypeEnum.slider,
|
||||||
|
label: '回复上限',
|
||||||
|
value: 3000,
|
||||||
|
min: 0,
|
||||||
|
max: 4000,
|
||||||
|
step: 50,
|
||||||
|
markList: [
|
||||||
|
{ label: '0', value: 0 },
|
||||||
|
{ label: '4000', value: 4000 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'systemPrompt',
|
||||||
|
type: FlowInputItemTypeEnum.textarea,
|
||||||
|
label: '系统提示词',
|
||||||
|
description:
|
||||||
|
'模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。',
|
||||||
|
placeholder:
|
||||||
|
'模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。',
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'limitPrompt',
|
||||||
|
type: FlowInputItemTypeEnum.textarea,
|
||||||
|
label: '限定词',
|
||||||
|
description:
|
||||||
|
'限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"',
|
||||||
|
placeholder:
|
||||||
|
'限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"',
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
|
Input_Template_TFSwitch,
|
||||||
|
{
|
||||||
|
key: 'quotePrompt',
|
||||||
|
type: FlowInputItemTypeEnum.target,
|
||||||
|
label: '引用内容'
|
||||||
|
},
|
||||||
|
Input_Template_History,
|
||||||
|
Input_Template_UserChatInput
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
key: 'answer',
|
||||||
|
label: '模型回复',
|
||||||
|
description: '直接响应,无需配置',
|
||||||
|
type: FlowOutputItemTypeEnum.hidden,
|
||||||
|
targets: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KBSearchModule: AppModuleTemplateItemType = {
|
||||||
|
logo: '',
|
||||||
|
name: '知识库搜索',
|
||||||
|
intro: '去知识库中搜索对应的答案。可作为 AI 对话引用参考。',
|
||||||
|
flowType: FlowModuleTypeEnum.kbSearchNode,
|
||||||
|
type: AppModuleItemTypeEnum.http,
|
||||||
|
url: '/openapi/modules/kb/search',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
key: 'kb_ids',
|
||||||
|
type: FlowInputItemTypeEnum.custom,
|
||||||
|
label: '关联的知识库',
|
||||||
|
value: [],
|
||||||
|
list: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'similarity',
|
||||||
|
type: FlowInputItemTypeEnum.slider,
|
||||||
|
label: '相似度',
|
||||||
|
value: 0.8,
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.01,
|
||||||
|
markList: [
|
||||||
|
{ label: '0', value: 0 },
|
||||||
|
{ label: '1', value: 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'limit',
|
||||||
|
type: FlowInputItemTypeEnum.slider,
|
||||||
|
label: '单次搜索上限',
|
||||||
|
value: 5,
|
||||||
|
min: 1,
|
||||||
|
max: 20,
|
||||||
|
step: 1,
|
||||||
|
markList: [
|
||||||
|
{ label: '1', value: 1 },
|
||||||
|
{ label: '20', value: 20 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
Input_Template_TFSwitch,
|
||||||
|
Input_Template_UserChatInput
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
key: 'rawSearch',
|
||||||
|
label: '源搜索数据',
|
||||||
|
type: FlowOutputItemTypeEnum.hidden,
|
||||||
|
response: true,
|
||||||
|
targets: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'isEmpty',
|
||||||
|
label: '搜索结果为空',
|
||||||
|
type: FlowOutputItemTypeEnum.source,
|
||||||
|
targets: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'quotePrompt',
|
||||||
|
label: '引用内容',
|
||||||
|
description: '搜索结果为空时不触发',
|
||||||
|
type: FlowOutputItemTypeEnum.source,
|
||||||
|
targets: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AnswerModule: AppModuleTemplateItemType = {
|
||||||
|
logo: '',
|
||||||
|
name: '指定回复',
|
||||||
|
intro: '该模块可以直接回复一段指定的内容。常用于引导、提示。',
|
||||||
|
type: AppModuleItemTypeEnum.answer,
|
||||||
|
flowType: FlowModuleTypeEnum.answerNode,
|
||||||
|
inputs: [
|
||||||
|
Input_Template_TFSwitch,
|
||||||
|
{
|
||||||
|
key: 'answerText',
|
||||||
|
value: '',
|
||||||
|
type: FlowInputItemTypeEnum.input,
|
||||||
|
label: '回复的内容'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
outputs: []
|
||||||
|
};
|
||||||
|
export const TFSwitchModule: AppModuleTemplateItemType = {
|
||||||
|
logo: '',
|
||||||
|
name: 'TF开关',
|
||||||
|
intro: '可以判断输入的内容为 True 或者 False,从而执行不同操作。',
|
||||||
|
type: AppModuleItemTypeEnum.switch,
|
||||||
|
flowType: FlowModuleTypeEnum.tfSwitchNode,
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
key: SystemInputEnum.switch,
|
||||||
|
type: FlowInputItemTypeEnum.target,
|
||||||
|
label: '输入'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
key: 'true',
|
||||||
|
label: 'True',
|
||||||
|
type: FlowOutputItemTypeEnum.source,
|
||||||
|
targets: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'false',
|
||||||
|
label: 'False',
|
||||||
|
type: FlowOutputItemTypeEnum.source,
|
||||||
|
targets: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClassifyQuestionModule: AppModuleTemplateItemType = {
|
||||||
|
logo: '',
|
||||||
|
name: '意图识别',
|
||||||
|
intro: '可以判断用户问题属于哪方面问题,从而执行不同的操作。',
|
||||||
|
type: AppModuleItemTypeEnum.switch,
|
||||||
|
flowType: FlowModuleTypeEnum.tfSwitchNode,
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
key: SystemInputEnum.switch,
|
||||||
|
type: FlowInputItemTypeEnum.target,
|
||||||
|
label: '输入'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
key: 'true',
|
||||||
|
label: 'True',
|
||||||
|
type: FlowOutputItemTypeEnum.source,
|
||||||
|
targets: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'false',
|
||||||
|
label: 'False',
|
||||||
|
type: FlowOutputItemTypeEnum.source,
|
||||||
|
targets: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModuleTemplates = [
|
||||||
|
{
|
||||||
|
label: '输入模块',
|
||||||
|
list: [UserInputModule, HistoryModule]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '对话模块',
|
||||||
|
list: [ChatModule]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '知识库模块',
|
||||||
|
list: [KBSearchModule]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '工具',
|
||||||
|
list: [AnswerModule, TFSwitchModule]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Agent',
|
||||||
|
list: [ClassifyQuestionModule]
|
||||||
|
}
|
||||||
|
];
|
||||||
5
client/src/constants/flow/defaultModule.ts
Normal file
5
client/src/constants/flow/defaultModule.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
export const nodeDefaultStyle: CSSProperties = {
|
||||||
|
border: '1px solid #DEE0E2'
|
||||||
|
};
|
||||||
36
client/src/constants/flow/index.ts
Normal file
36
client/src/constants/flow/index.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
export enum FlowInputItemTypeEnum {
|
||||||
|
systemInput = 'systemInput', // history, userChatInput
|
||||||
|
input = 'input',
|
||||||
|
textarea = 'textarea',
|
||||||
|
numberInput = 'numberInput',
|
||||||
|
select = 'select',
|
||||||
|
slider = 'slider',
|
||||||
|
custom = 'custom',
|
||||||
|
target = 'target',
|
||||||
|
none = 'none',
|
||||||
|
hidden = 'hidden'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FlowOutputItemTypeEnum {
|
||||||
|
answer = 'answer',
|
||||||
|
source = 'source',
|
||||||
|
none = 'none',
|
||||||
|
hidden = 'hidden'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FlowModuleTypeEnum {
|
||||||
|
questionInputNode = 'questionInput',
|
||||||
|
historyNode = 'historyNode',
|
||||||
|
chatNode = 'chatNode',
|
||||||
|
kbSearchNode = 'kbSearchNode',
|
||||||
|
tfSwitchNode = 'tfSwitchNode',
|
||||||
|
answerNode = 'answerNode'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const edgeOptions = {
|
||||||
|
style: {
|
||||||
|
strokeWidth: 1,
|
||||||
|
stroke: '#5A646Es'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const connectionLineStyle = { strokeWidth: 1, stroke: '#5A646Es' };
|
||||||
21
client/src/constants/flow/inputTemplate.ts
Normal file
21
client/src/constants/flow/inputTemplate.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { FlowInputItemType } from '@/types/flow';
|
||||||
|
import { SystemInputEnum } from '../app';
|
||||||
|
import { FlowInputItemTypeEnum } from './index';
|
||||||
|
|
||||||
|
export const Input_Template_TFSwitch: FlowInputItemType = {
|
||||||
|
key: SystemInputEnum.switch,
|
||||||
|
type: FlowInputItemTypeEnum.target,
|
||||||
|
label: '触发器'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Input_Template_History: FlowInputItemType = {
|
||||||
|
key: SystemInputEnum.history,
|
||||||
|
type: FlowInputItemTypeEnum.target,
|
||||||
|
label: '聊天记录'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Input_Template_UserChatInput: FlowInputItemType = {
|
||||||
|
key: SystemInputEnum.userChatInput,
|
||||||
|
type: FlowInputItemTypeEnum.target,
|
||||||
|
label: '用户问题'
|
||||||
|
};
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import type { ShareChatEditType } from '@/types/model';
|
import type { ShareChatEditType } from '@/types/model';
|
||||||
import type { ModelSchema } from '@/types/mongoSchema';
|
import type { AppSchema } from '@/types/mongoSchema';
|
||||||
|
|
||||||
export const embeddingModel = 'text-embedding-ada-002';
|
export const embeddingModel = 'text-embedding-ada-002';
|
||||||
export const embeddingPrice = 0.1;
|
export const embeddingPrice = 0.1;
|
||||||
@ -64,8 +64,8 @@ export const chatModelList: ChatModelItemType[] = [
|
|||||||
ChatModelMap[OpenAiChatEnum.GPT4]
|
ChatModelMap[OpenAiChatEnum.GPT4]
|
||||||
];
|
];
|
||||||
|
|
||||||
export const defaultModel: ModelSchema = {
|
export const defaultApp: AppSchema = {
|
||||||
_id: 'modelId',
|
_id: '',
|
||||||
userId: 'userId',
|
userId: 'userId',
|
||||||
name: '模型名称',
|
name: '模型名称',
|
||||||
avatar: '/icon/logo.png',
|
avatar: '/icon/logo.png',
|
||||||
@ -86,7 +86,8 @@ export const defaultModel: ModelSchema = {
|
|||||||
isShare: false,
|
isShare: false,
|
||||||
isShareDetail: false,
|
isShareDetail: false,
|
||||||
collection: 0
|
collection: 0
|
||||||
}
|
},
|
||||||
|
modules: []
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultShareChat: ShareChatEditType = {
|
export const defaultShareChat: ShareChatEditType = {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { extendTheme, defineStyleConfig, ComponentStyleConfig } from '@chakra-ui/react';
|
import { extendTheme, defineStyleConfig, ComponentStyleConfig } from '@chakra-ui/react';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { modalAnatomy, switchAnatomy, selectAnatomy, checkboxAnatomy } from '@chakra-ui/anatomy';
|
import { modalAnatomy, switchAnatomy, selectAnatomy, numberInputAnatomy } from '@chakra-ui/anatomy';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
|
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
|
||||||
|
|
||||||
@ -11,6 +11,8 @@ const { definePartsStyle: switchPart, defineMultiStyleConfig: switchMultiStyle }
|
|||||||
createMultiStyleConfigHelpers(switchAnatomy.keys);
|
createMultiStyleConfigHelpers(switchAnatomy.keys);
|
||||||
const { definePartsStyle: selectPart, defineMultiStyleConfig: selectMultiStyle } =
|
const { definePartsStyle: selectPart, defineMultiStyleConfig: selectMultiStyle } =
|
||||||
createMultiStyleConfigHelpers(selectAnatomy.keys);
|
createMultiStyleConfigHelpers(selectAnatomy.keys);
|
||||||
|
const { definePartsStyle: numInputPart, defineMultiStyleConfig: numInputMultiStyle } =
|
||||||
|
createMultiStyleConfigHelpers(numberInputAnatomy.keys);
|
||||||
|
|
||||||
// modal 弹窗
|
// modal 弹窗
|
||||||
const ModalTheme = defineMultiStyleConfig({
|
const ModalTheme = defineMultiStyleConfig({
|
||||||
@ -122,6 +124,39 @@ const Input: ComponentStyleConfig = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const NumberInput = numInputMultiStyle({
|
||||||
|
variants: {
|
||||||
|
outline: numInputPart({
|
||||||
|
field: {
|
||||||
|
bg: 'myWhite.300',
|
||||||
|
border: '1px solid',
|
||||||
|
borderRadius: 'base',
|
||||||
|
borderColor: 'myGray.200',
|
||||||
|
_focus: {
|
||||||
|
borderColor: 'myBlue.600 !important',
|
||||||
|
boxShadow: '0px 0px 4px #A8DBFF !important',
|
||||||
|
bg: 'transparent'
|
||||||
|
},
|
||||||
|
_disabled: {
|
||||||
|
color: 'myGray.400 !important',
|
||||||
|
bg: 'myWhite.300 !important'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stepper: {
|
||||||
|
bg: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
color: 'myGray.600',
|
||||||
|
_active: {
|
||||||
|
color: 'myBlue.600'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
defaultProps: {
|
||||||
|
variant: 'outline'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const Textarea: ComponentStyleConfig = {
|
const Textarea: ComponentStyleConfig = {
|
||||||
variants: {
|
variants: {
|
||||||
outline: {
|
outline: {
|
||||||
@ -260,6 +295,7 @@ export const theme = extendTheme({
|
|||||||
Input,
|
Input,
|
||||||
Textarea,
|
Textarea,
|
||||||
Switch,
|
Switch,
|
||||||
Select
|
Select,
|
||||||
|
NumberInput
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import type { UseMutationOptions } from '@tanstack/react-query';
|
import type { UseMutationOptions } from '@tanstack/react-query';
|
||||||
|
import { getErrText } from '@/utils/tools';
|
||||||
|
|
||||||
interface Props extends UseMutationOptions<any, any, any, any> {
|
interface Props extends UseMutationOptions<any, any, any, any> {
|
||||||
successToast?: string;
|
successToast?: string;
|
||||||
@ -23,7 +24,7 @@ export const useRequest = ({ successToast, errorToast, onSuccess, onError, ...pr
|
|||||||
onError?.(err, variables, context);
|
onError?.(err, variables, context);
|
||||||
errorToast &&
|
errorToast &&
|
||||||
toast({
|
toast({
|
||||||
title: typeof err === 'string' ? err : err?.message || errorToast,
|
title: getErrText(err, errorToast),
|
||||||
status: 'error'
|
status: 'error'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import { connectToDatabase, Chat, Model } from '@/service/mongo';
|
|||||||
import type { InitChatResponse } from '@/api/response/chat';
|
import type { InitChatResponse } from '@/api/response/chat';
|
||||||
import { authUser } from '@/service/utils/auth';
|
import { authUser } from '@/service/utils/auth';
|
||||||
import { ChatItemType } from '@/types/chat';
|
import { ChatItemType } from '@/types/chat';
|
||||||
import { authModel } from '@/service/utils/auth';
|
import { authApp } from '@/service/utils/auth';
|
||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import type { ModelSchema } from '@/types/mongoSchema';
|
import type { AppSchema } from '@/types/mongoSchema';
|
||||||
|
|
||||||
/* 初始化我的聊天框,需要身份验证 */
|
/* 初始化我的聊天框,需要身份验证 */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
@ -21,7 +21,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
|
|
||||||
// 没有 modelId 时,直接获取用户的第一个id
|
// 没有 modelId 时,直接获取用户的第一个id
|
||||||
const model = await (async () => {
|
const app = await (async () => {
|
||||||
if (!modelId) {
|
if (!modelId) {
|
||||||
const myModel = await Model.findOne({ userId });
|
const myModel = await Model.findOne({ userId });
|
||||||
if (!myModel) {
|
if (!myModel) {
|
||||||
@ -29,23 +29,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
name: '应用1',
|
name: '应用1',
|
||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
return (await Model.findById(_id)) as ModelSchema;
|
return (await Model.findById(_id)) as AppSchema;
|
||||||
} else {
|
} else {
|
||||||
return myModel;
|
return myModel;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 校验使用权限
|
// 校验使用权限
|
||||||
const authRes = await authModel({
|
const authRes = await authApp({
|
||||||
modelId,
|
appId: modelId,
|
||||||
userId,
|
userId,
|
||||||
authUser: false,
|
authUser: false,
|
||||||
authOwner: false
|
authOwner: false
|
||||||
});
|
});
|
||||||
return authRes.model;
|
return authRes.app;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
modelId = modelId || model._id;
|
modelId = modelId || app._id;
|
||||||
|
|
||||||
// 历史记录
|
// 历史记录
|
||||||
let history: ChatItemType[] = [];
|
let history: ChatItemType[] = [];
|
||||||
@ -87,21 +87,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOwner = String(model.userId) === userId;
|
const isOwner = String(app.userId) === userId;
|
||||||
|
|
||||||
jsonRes<InitChatResponse>(res, {
|
jsonRes<InitChatResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
chatId: chatId || '',
|
chatId: chatId || '',
|
||||||
modelId: modelId,
|
modelId: modelId,
|
||||||
model: {
|
model: {
|
||||||
name: model.name,
|
name: app.name,
|
||||||
avatar: model.avatar,
|
avatar: app.avatar,
|
||||||
intro: model.intro,
|
intro: app.intro,
|
||||||
canUse: model.share.isShare || isOwner
|
canUse: app.share.isShare || isOwner
|
||||||
},
|
},
|
||||||
chatModel: model.chat.chatModel,
|
chatModel: app.chat.chatModel,
|
||||||
systemPrompt: isOwner ? model.chat.systemPrompt : '',
|
systemPrompt: isOwner ? app.chat.systemPrompt : '',
|
||||||
limitPrompt: isOwner ? model.chat.limitPrompt : '',
|
limitPrompt: isOwner ? app.chat.limitPrompt : '',
|
||||||
history
|
history
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { jsonRes } from '@/service/response';
|
import { jsonRes } from '@/service/response';
|
||||||
import { ChatItemType } from '@/types/chat';
|
import { ChatItemType } from '@/types/chat';
|
||||||
import { connectToDatabase, Chat, Model } from '@/service/mongo';
|
import { connectToDatabase, Chat, Model } from '@/service/mongo';
|
||||||
import { authModel } from '@/service/utils/auth';
|
import { authApp } from '@/service/utils/auth';
|
||||||
import { authUser } from '@/service/utils/auth';
|
import { authUser } from '@/service/utils/auth';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ export async function saveChat({
|
|||||||
userId
|
userId
|
||||||
}: Props & { newChatId?: Types.ObjectId; userId: string }): Promise<{ newChatId: string }> {
|
}: Props & { newChatId?: Types.ObjectId; userId: string }): Promise<{ newChatId: string }> {
|
||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
const { model } = await authModel({ modelId, userId, authOwner: false });
|
const { app } = await authApp({ appId: modelId, userId, authOwner: false });
|
||||||
|
|
||||||
const content = prompts.map((item) => ({
|
const content = prompts.map((item) => ({
|
||||||
_id: item._id,
|
_id: item._id,
|
||||||
@ -59,7 +59,7 @@ export async function saveChat({
|
|||||||
quote: item.quote || []
|
quote: item.quote || []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (String(model.userId) === userId) {
|
if (String(app.userId) === userId) {
|
||||||
await Model.findByIdAndUpdate(modelId, {
|
await Model.findByIdAndUpdate(modelId, {
|
||||||
updateTime: new Date()
|
updateTime: new Date()
|
||||||
});
|
});
|
||||||
@ -93,8 +93,8 @@ export async function saveChat({
|
|||||||
newChatId: String(res._id)
|
newChatId: String(res._id)
|
||||||
}))
|
}))
|
||||||
]),
|
]),
|
||||||
// update model
|
// update app
|
||||||
...(String(model.userId) === userId
|
...(String(app.userId) === userId
|
||||||
? [
|
? [
|
||||||
Model.findByIdAndUpdate(modelId, {
|
Model.findByIdAndUpdate(modelId, {
|
||||||
updateTime: new Date()
|
updateTime: new Date()
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { jsonRes } from '@/service/response';
|
import { jsonRes } from '@/service/response';
|
||||||
import { connectToDatabase, ShareChat } from '@/service/mongo';
|
import { connectToDatabase, ShareChat } from '@/service/mongo';
|
||||||
import { authModel, authUser } from '@/service/utils/auth';
|
import { authApp, authUser } from '@/service/utils/auth';
|
||||||
import type { ShareChatEditType } from '@/types/model';
|
import type { ShareChatEditType } from '@/types/model';
|
||||||
|
|
||||||
/* create a shareChat */
|
/* create a shareChat */
|
||||||
@ -14,8 +14,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
|
|
||||||
const { userId } = await authUser({ req, authToken: true });
|
const { userId } = await authUser({ req, authToken: true });
|
||||||
await authModel({
|
await authApp({
|
||||||
modelId,
|
appId: modelId,
|
||||||
userId,
|
userId,
|
||||||
authOwner: false
|
authOwner: false
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { jsonRes } from '@/service/response';
|
import { jsonRes } from '@/service/response';
|
||||||
import { connectToDatabase, ShareChat, User } from '@/service/mongo';
|
import { connectToDatabase, ShareChat, User } from '@/service/mongo';
|
||||||
import type { InitShareChatResponse } from '@/api/response/chat';
|
import type { InitShareChatResponse } from '@/api/response/chat';
|
||||||
import { authModel } from '@/service/utils/auth';
|
import { authApp } from '@/service/utils/auth';
|
||||||
import { hashPassword } from '@/service/utils/tools';
|
import { hashPassword } from '@/service/utils/tools';
|
||||||
import { HUMAN_ICON } from '@/constants/chat';
|
import { HUMAN_ICON } from '@/constants/chat';
|
||||||
|
|
||||||
@ -35,8 +35,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 校验使用权限
|
// 校验使用权限
|
||||||
const { model } = await authModel({
|
const { app } = await authApp({
|
||||||
modelId: shareChat.modelId,
|
appId: shareChat.modelId,
|
||||||
userId: String(shareChat.userId),
|
userId: String(shareChat.userId),
|
||||||
authOwner: false
|
authOwner: false
|
||||||
});
|
});
|
||||||
@ -48,11 +48,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
maxContext: shareChat.maxContext,
|
maxContext: shareChat.maxContext,
|
||||||
userAvatar: user?.avatar || HUMAN_ICON,
|
userAvatar: user?.avatar || HUMAN_ICON,
|
||||||
model: {
|
model: {
|
||||||
name: model.name,
|
name: app.name,
|
||||||
avatar: model.avatar,
|
avatar: app.avatar,
|
||||||
intro: model.intro
|
intro: app.intro
|
||||||
},
|
},
|
||||||
chatModel: model.chat.chatModel
|
chatModel: app.chat.chatModel
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { jsonRes } from '@/service/response';
|
import { jsonRes } from '@/service/response';
|
||||||
import { Chat, Model, connectToDatabase, Collection, ShareChat } from '@/service/mongo';
|
import { Chat, Model, connectToDatabase, Collection, ShareChat } from '@/service/mongo';
|
||||||
import { authUser } from '@/service/utils/auth';
|
import { authUser } from '@/service/utils/auth';
|
||||||
import { authModel } from '@/service/utils/auth';
|
import { authApp } from '@/service/utils/auth';
|
||||||
|
|
||||||
/* 获取我的模型 */
|
/* 获取我的模型 */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
@ -19,8 +19,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
|
|
||||||
// 验证是否是该用户的 model
|
// 验证是否是该用户的 model
|
||||||
await authModel({
|
await authApp({
|
||||||
modelId,
|
appId: modelId,
|
||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { jsonRes } from '@/service/response';
|
import { jsonRes } from '@/service/response';
|
||||||
import { connectToDatabase } from '@/service/mongo';
|
import { connectToDatabase } from '@/service/mongo';
|
||||||
import { authUser } from '@/service/utils/auth';
|
import { authUser } from '@/service/utils/auth';
|
||||||
import { authModel } from '@/service/utils/auth';
|
import { authApp } from '@/service/utils/auth';
|
||||||
|
|
||||||
/* 获取我的模型 */
|
/* 获取我的模型 */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
@ -18,14 +18,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
|
|
||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
|
|
||||||
const { model } = await authModel({
|
const { app } = await authApp({
|
||||||
modelId,
|
appId: modelId,
|
||||||
userId,
|
userId,
|
||||||
authOwner: false
|
authOwner: false
|
||||||
});
|
});
|
||||||
|
|
||||||
jsonRes(res, {
|
jsonRes(res, {
|
||||||
data: model
|
data: app
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonRes(res, {
|
jsonRes(res, {
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
|
|
||||||
// 根据 userId 获取模型信息
|
// 根据 userId 获取模型信息
|
||||||
const [myModels, myCollections] = await Promise.all([
|
const [myApps, myCollections] = await Promise.all([
|
||||||
Model.find(
|
Model.find(
|
||||||
{
|
{
|
||||||
userId
|
userId
|
||||||
@ -33,20 +33,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
|
|
||||||
jsonRes<ModelListResponse>(res, {
|
jsonRes<ModelListResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
myModels: myModels.map((item) => ({
|
myApps: myApps.map((item) => ({
|
||||||
_id: item._id,
|
_id: item._id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
avatar: item.avatar,
|
avatar: item.avatar,
|
||||||
intro: item.intro
|
intro: item.intro
|
||||||
})),
|
})),
|
||||||
myCollectionModels: myCollections
|
myCollectionApps: myCollections
|
||||||
.map((item: any) => ({
|
.map((item: any) => ({
|
||||||
_id: item.modelId?._id,
|
_id: item.modelId?._id,
|
||||||
name: item.modelId?.name,
|
name: item.modelId?.name,
|
||||||
avatar: item.modelId?.avatar,
|
avatar: item.modelId?.avatar,
|
||||||
intro: item.modelId?.intro
|
intro: item.modelId?.intro
|
||||||
}))
|
}))
|
||||||
.filter((item) => !myModels.find((model) => String(model._id) === String(item._id))) // 去重
|
.filter((item) => !myApps.find((model) => String(model._id) === String(item._id))) // 去重
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -4,16 +4,16 @@ import { connectToDatabase } from '@/service/mongo';
|
|||||||
import { authUser } from '@/service/utils/auth';
|
import { authUser } from '@/service/utils/auth';
|
||||||
import { Model } from '@/service/models/model';
|
import { Model } from '@/service/models/model';
|
||||||
import type { ModelUpdateParams } from '@/types/model';
|
import type { ModelUpdateParams } from '@/types/model';
|
||||||
import { authModel } from '@/service/utils/auth';
|
import { authApp } from '@/service/utils/auth';
|
||||||
|
|
||||||
/* 获取我的模型 */
|
/* 获取我的模型 */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
try {
|
try {
|
||||||
const { name, avatar, chat, share, intro } = req.body as ModelUpdateParams;
|
const { name, avatar, chat, share, intro, modules } = req.body as ModelUpdateParams;
|
||||||
const { modelId } = req.query as { modelId: string };
|
const { appId } = req.query as { appId: string };
|
||||||
|
|
||||||
if (!modelId) {
|
if (!appId) {
|
||||||
throw new Error('参数错误');
|
throw new Error('appId is empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 凭证校验
|
// 凭证校验
|
||||||
@ -21,15 +21,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
|
|
||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
|
|
||||||
await authModel({
|
await authApp({
|
||||||
modelId,
|
appId,
|
||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新模型
|
// 更新模型
|
||||||
await Model.updateOne(
|
await Model.updateOne(
|
||||||
{
|
{
|
||||||
_id: modelId,
|
_id: appId,
|
||||||
userId
|
userId
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -40,7 +40,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
...(share && {
|
...(share && {
|
||||||
'share.isShare': share.isShare,
|
'share.isShare': share.isShare,
|
||||||
'share.isShareDetail': share.isShareDetail
|
'share.isShareDetail': share.isShareDetail
|
||||||
})
|
}),
|
||||||
|
...(modules && { modules })
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { connectToDatabase } from '@/service/mongo';
|
import { connectToDatabase } from '@/service/mongo';
|
||||||
import { authUser, authModel, getApiKey } from '@/service/utils/auth';
|
import { authUser, authApp, getApiKey } from '@/service/utils/auth';
|
||||||
import { modelServiceToolMap, resStreamResponse } from '@/service/utils/chat';
|
import { modelServiceToolMap, resStreamResponse } from '@/service/utils/chat';
|
||||||
import { ChatItemType } from '@/types/chat';
|
import { ChatItemType } from '@/types/chat';
|
||||||
import { jsonRes } from '@/service/response';
|
import { jsonRes } from '@/service/response';
|
||||||
@ -50,19 +50,19 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
/* 凭证校验 */
|
/* 凭证校验 */
|
||||||
const { userId } = await authUser({ req });
|
const { userId } = await authUser({ req });
|
||||||
|
|
||||||
const { model } = await authModel({
|
const { app } = await authApp({
|
||||||
userId,
|
userId,
|
||||||
modelId
|
appId: modelId
|
||||||
});
|
});
|
||||||
|
|
||||||
/* get api key */
|
/* get api key */
|
||||||
const { systemAuthKey: apiKey } = await getApiKey({
|
const { systemAuthKey: apiKey } = await getApiKey({
|
||||||
model: model.chat.chatModel,
|
model: app.chat.chatModel,
|
||||||
userId,
|
userId,
|
||||||
mustPay: true
|
mustPay: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const modelConstantsData = ChatModelMap[model.chat.chatModel];
|
const modelConstantsData = ChatModelMap[app.chat.chatModel];
|
||||||
const prompt = prompts[prompts.length - 1];
|
const prompt = prompts[prompts.length - 1];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -71,14 +71,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
quotePrompt = []
|
quotePrompt = []
|
||||||
} = await (async () => {
|
} = await (async () => {
|
||||||
// 使用了知识库搜索
|
// 使用了知识库搜索
|
||||||
if (model.chat.relatedKbs?.length > 0) {
|
if (app.chat.relatedKbs?.length > 0) {
|
||||||
const { quotePrompt, userSystemPrompt, userLimitPrompt } = await appKbSearch({
|
const { quotePrompt, userSystemPrompt, userLimitPrompt } = await appKbSearch({
|
||||||
model,
|
model: app,
|
||||||
userId,
|
userId,
|
||||||
fixedQuote: [],
|
fixedQuote: [],
|
||||||
prompt: prompt,
|
prompt: prompt,
|
||||||
similarity: model.chat.searchSimilarity,
|
similarity: app.chat.searchSimilarity,
|
||||||
limit: model.chat.searchLimit
|
limit: app.chat.searchLimit
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -88,19 +88,19 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
userSystemPrompt: model.chat.systemPrompt
|
userSystemPrompt: app.chat.systemPrompt
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
obj: ChatRoleEnum.System,
|
obj: ChatRoleEnum.System,
|
||||||
value: model.chat.systemPrompt
|
value: app.chat.systemPrompt
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
userLimitPrompt: model.chat.limitPrompt
|
userLimitPrompt: app.chat.limitPrompt
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
obj: ChatRoleEnum.Human,
|
obj: ChatRoleEnum.Human,
|
||||||
value: model.chat.limitPrompt
|
value: app.chat.limitPrompt
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: []
|
: []
|
||||||
@ -108,8 +108,8 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
// search result is empty
|
// search result is empty
|
||||||
if (model.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && model.chat.searchEmptyText) {
|
if (app.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && app.chat.searchEmptyText) {
|
||||||
const response = model.chat.searchEmptyText;
|
const response = app.chat.searchEmptyText;
|
||||||
return res.end(response);
|
return res.end(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,14 +123,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
];
|
];
|
||||||
|
|
||||||
// 计算温度
|
// 计算温度
|
||||||
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
|
const temperature = (modelConstantsData.maxTemperature * (app.chat.temperature / 10)).toFixed(
|
||||||
2
|
2
|
||||||
);
|
);
|
||||||
|
|
||||||
// 发出请求
|
// 发出请求
|
||||||
const { streamResponse, responseMessages, responseText, totalTokens } =
|
const { streamResponse, responseMessages, responseText, totalTokens } =
|
||||||
await modelServiceToolMap.chatCompletion({
|
await modelServiceToolMap.chatCompletion({
|
||||||
model: model.chat.chatModel,
|
model: app.chat.chatModel,
|
||||||
apiKey,
|
apiKey,
|
||||||
temperature: +temperature,
|
temperature: +temperature,
|
||||||
messages: completePrompts,
|
messages: completePrompts,
|
||||||
@ -146,7 +146,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
if (isStream) {
|
if (isStream) {
|
||||||
try {
|
try {
|
||||||
const { finishMessages, totalTokens } = await resStreamResponse({
|
const { finishMessages, totalTokens } = await resStreamResponse({
|
||||||
model: model.chat.chatModel,
|
model: app.chat.chatModel,
|
||||||
res,
|
res,
|
||||||
chatResponse: streamResponse,
|
chatResponse: streamResponse,
|
||||||
prompts: responseMessages
|
prompts: responseMessages
|
||||||
@ -173,7 +173,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
|
|
||||||
pushChatBill({
|
pushChatBill({
|
||||||
isPay: true,
|
isPay: true,
|
||||||
chatModel: model.chat.chatModel,
|
chatModel: app.chat.chatModel,
|
||||||
userId,
|
userId,
|
||||||
textLen,
|
textLen,
|
||||||
tokens,
|
tokens,
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import { authUser } from '@/service/utils/auth';
|
|||||||
import { PgClient } from '@/service/pg';
|
import { PgClient } from '@/service/pg';
|
||||||
import { withNextCors } from '@/service/utils/tools';
|
import { withNextCors } from '@/service/utils/tools';
|
||||||
import type { ChatItemType } from '@/types/chat';
|
import type { ChatItemType } from '@/types/chat';
|
||||||
import type { ModelSchema } from '@/types/mongoSchema';
|
import type { AppSchema } from '@/types/mongoSchema';
|
||||||
import { authModel } from '@/service/utils/auth';
|
import { authApp } from '@/service/utils/auth';
|
||||||
import { ChatModelMap } from '@/constants/model';
|
import { ChatModelMap } from '@/constants/model';
|
||||||
import { ChatRoleEnum } from '@/constants/chat';
|
import { ChatRoleEnum } from '@/constants/chat';
|
||||||
import { openaiEmbedding } from '../plugin/openaiEmbedding';
|
import { openaiEmbedding } from '../plugin/openaiEmbedding';
|
||||||
@ -54,13 +54,13 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
}
|
}
|
||||||
|
|
||||||
// auth model
|
// auth model
|
||||||
const { model } = await authModel({
|
const { app } = await authApp({
|
||||||
modelId: appId,
|
appId,
|
||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await appKbSearch({
|
const result = await appKbSearch({
|
||||||
model,
|
model: app,
|
||||||
userId,
|
userId,
|
||||||
fixedQuote: [],
|
fixedQuote: [],
|
||||||
prompt: prompts[prompts.length - 1],
|
prompt: prompts[prompts.length - 1],
|
||||||
@ -88,7 +88,7 @@ export async function appKbSearch({
|
|||||||
similarity = 0.8,
|
similarity = 0.8,
|
||||||
limit = 5
|
limit = 5
|
||||||
}: {
|
}: {
|
||||||
model: ModelSchema;
|
model: AppSchema;
|
||||||
userId: string;
|
userId: string;
|
||||||
fixedQuote?: QuoteItemType[];
|
fixedQuote?: QuoteItemType[];
|
||||||
prompt: ChatItemType;
|
prompt: ChatItemType;
|
||||||
|
|||||||
@ -106,7 +106,7 @@ export async function classifyQuestion({
|
|||||||
if (!arg.type) {
|
if (!arg.type) {
|
||||||
throw new Error('');
|
throw new Error('');
|
||||||
}
|
}
|
||||||
console.log(adaptMessages, arg.type);
|
console.log(arg.type);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[arg.type]: 1
|
[arg.type]: 1
|
||||||
|
|||||||
@ -5,9 +5,8 @@ import { sseResponse } from '@/service/utils/tools';
|
|||||||
import { ChatModelMap, OpenAiChatEnum } from '@/constants/model';
|
import { ChatModelMap, OpenAiChatEnum } from '@/constants/model';
|
||||||
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
|
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
|
||||||
import { modelToolMap } from '@/utils/plugin';
|
import { modelToolMap } from '@/utils/plugin';
|
||||||
import { ChatCompletionType, ChatContextFilter } from '@/service/utils/chat/index';
|
import { ChatContextFilter } from '@/service/utils/chat/index';
|
||||||
import type { ChatItemType } from '@/types/chat';
|
import type { ChatItemType } from '@/types/chat';
|
||||||
import { getSystemOpenAiKey } from '@/service/utils/auth';
|
|
||||||
import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
|
import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
|
||||||
import { parseStreamChunk, textAdaptGptResponse } from '@/utils/adapt';
|
import { parseStreamChunk, textAdaptGptResponse } from '@/utils/adapt';
|
||||||
import { getOpenAIApi, axiosConfig } from '@/service/ai/openai';
|
import { getOpenAIApi, axiosConfig } from '@/service/ai/openai';
|
||||||
@ -23,7 +22,7 @@ export type Props = {
|
|||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
limitPrompt?: string;
|
limitPrompt?: string;
|
||||||
};
|
};
|
||||||
export type Response = { history: ChatItemType[] };
|
export type Response = { answer: string };
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
@ -89,7 +88,7 @@ export async function chatCompletion({
|
|||||||
userChatInput,
|
userChatInput,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
limitPrompt
|
limitPrompt
|
||||||
}: Props & { res: NextApiResponse }) {
|
}: Props & { res: NextApiResponse }): Promise<Response> {
|
||||||
const messages: ChatItemType[] = [
|
const messages: ChatItemType[] = [
|
||||||
...(quotePrompt
|
...(quotePrompt
|
||||||
? [
|
? [
|
||||||
@ -131,7 +130,6 @@ export async function chatCompletion({
|
|||||||
|
|
||||||
const adaptMessages = adaptChatItem_openAI({ messages: filterMessages, reserveId: false });
|
const adaptMessages = adaptChatItem_openAI({ messages: filterMessages, reserveId: false });
|
||||||
const chatAPI = getOpenAIApi();
|
const chatAPI = getOpenAIApi();
|
||||||
console.log(adaptMessages);
|
|
||||||
|
|
||||||
/* count response max token */
|
/* count response max token */
|
||||||
const promptsToken = modelToolMap[model].countTokens({
|
const promptsToken = modelToolMap[model].countTokens({
|
||||||
@ -156,37 +154,35 @@ export async function chatCompletion({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const { answer, totalTokens } = await (async () => {
|
const { answer } = await (async () => {
|
||||||
if (stream) {
|
if (stream) {
|
||||||
// sse response
|
// sse response
|
||||||
const { answer } = await streamResponse({ res, response });
|
const { answer } = await streamResponse({ res, response });
|
||||||
// count tokens
|
// count tokens
|
||||||
const finishMessages = filterMessages.concat({
|
// const finishMessages = filterMessages.concat({
|
||||||
obj: ChatRoleEnum.AI,
|
// obj: ChatRoleEnum.AI,
|
||||||
value: answer
|
// value: answer
|
||||||
});
|
// });
|
||||||
|
|
||||||
const totalTokens = modelToolMap[model].countTokens({
|
// const totalTokens = modelToolMap[model].countTokens({
|
||||||
messages: finishMessages
|
// messages: finishMessages
|
||||||
});
|
// });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
answer,
|
answer
|
||||||
totalTokens
|
// totalTokens
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const answer = stream ? '' : response.data.choices?.[0].message?.content || '';
|
const answer = stream ? '' : response.data.choices?.[0].message?.content || '';
|
||||||
const totalTokens = stream ? 0 : response.data.usage?.total_tokens || 0;
|
// const totalTokens = stream ? 0 : response.data.usage?.total_tokens || 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
answer,
|
answer
|
||||||
totalTokens
|
// totalTokens
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// count price
|
|
||||||
const unitPrice = ChatModelMap[model]?.price || 3;
|
|
||||||
return {
|
return {
|
||||||
answer
|
answer
|
||||||
};
|
};
|
||||||
|
|||||||
20
client/src/pages/api/openapi/modules/init/history.tsx
Normal file
20
client/src/pages/api/openapi/modules/init/history.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { SystemInputEnum } from '@/constants/app';
|
||||||
|
import { ChatItemType } from '@/types/chat';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
maxContext: number;
|
||||||
|
[SystemInputEnum.history]: ChatItemType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { maxContext = 5, history } = req.body as Props;
|
||||||
|
|
||||||
|
jsonRes(res, {
|
||||||
|
data: {
|
||||||
|
history: history.slice(-maxContext)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
17
client/src/pages/api/openapi/modules/init/userChatInput.tsx
Normal file
17
client/src/pages/api/openapi/modules/init/userChatInput.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { SystemInputEnum } from '@/constants/app';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
[SystemInputEnum.userChatInput]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { userChatInput } = req.body as Props;
|
||||||
|
jsonRes(res, {
|
||||||
|
data: {
|
||||||
|
userChatInput
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -25,7 +25,7 @@ type Props = {
|
|||||||
type Response = {
|
type Response = {
|
||||||
rawSearch: QuoteItemType[];
|
rawSearch: QuoteItemType[];
|
||||||
isEmpty?: boolean;
|
isEmpty?: boolean;
|
||||||
quotePrompt: string;
|
quotePrompt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
@ -43,7 +43,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
throw new Error('params is error');
|
throw new Error('params is error');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await appKbSearch({
|
const result = await kbSearch({
|
||||||
kb_ids,
|
kb_ids,
|
||||||
history,
|
history,
|
||||||
similarity,
|
similarity,
|
||||||
@ -64,7 +64,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function appKbSearch({
|
export async function kbSearch({
|
||||||
kb_ids = [],
|
kb_ids = [],
|
||||||
history = [],
|
history = [],
|
||||||
similarity = 0.8,
|
similarity = 0.8,
|
||||||
@ -108,8 +108,8 @@ export async function appKbSearch({
|
|||||||
const rawSearch = searchRes.slice(0, sliceResult.length);
|
const rawSearch = searchRes.slice(0, sliceResult.length);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isEmpty: rawSearch.length === 0,
|
isEmpty: rawSearch.length === 0 ? true : undefined,
|
||||||
rawSearch,
|
rawSearch,
|
||||||
quotePrompt: sliceResult ? `知识库:\n${sliceResult}` : ''
|
quotePrompt: sliceResult ? `知识库:\n${sliceResult}` : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
export type Props = {
|
|
||||||
url: string;
|
|
||||||
body: Record<string, any>;
|
|
||||||
};
|
|
||||||
@ -96,7 +96,7 @@ export async function openaiEmbedding_system({ input }: Props) {
|
|||||||
input
|
input
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timeout: 60000,
|
timeout: 20000,
|
||||||
...axiosConfig(apiKey)
|
...axiosConfig(apiKey)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { connectToDatabase } from '@/service/mongo';
|
import { connectToDatabase } from '@/service/mongo';
|
||||||
import { authUser, authModel, getApiKey, authShareChat } from '@/service/utils/auth';
|
import { authUser, authApp, getApiKey, authShareChat } from '@/service/utils/auth';
|
||||||
import { modelServiceToolMap, V2_StreamResponse } from '@/service/utils/chat';
|
import { modelServiceToolMap, V2_StreamResponse } from '@/service/utils/chat';
|
||||||
import { jsonRes } from '@/service/response';
|
import { jsonRes } from '@/service/response';
|
||||||
import { ChatModelMap } from '@/constants/model';
|
import { ChatModelMap } from '@/constants/model';
|
||||||
@ -79,9 +79,9 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
}
|
}
|
||||||
|
|
||||||
// auth app permission
|
// auth app permission
|
||||||
const { model, showModelDetail } = await authModel({
|
const { app, showModelDetail } = await authApp({
|
||||||
userId,
|
userId,
|
||||||
modelId: appId,
|
appId,
|
||||||
authOwner: false,
|
authOwner: false,
|
||||||
reserveDetail: true
|
reserveDetail: true
|
||||||
});
|
});
|
||||||
@ -90,7 +90,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
|
|
||||||
/* get api key */
|
/* get api key */
|
||||||
const { systemAuthKey: apiKey, userOpenAiKey } = await getApiKey({
|
const { systemAuthKey: apiKey, userOpenAiKey } = await getApiKey({
|
||||||
model: model.chat.chatModel,
|
model: app.chat.chatModel,
|
||||||
userId,
|
userId,
|
||||||
mustPay: authType !== 'token'
|
mustPay: authType !== 'token'
|
||||||
});
|
});
|
||||||
@ -112,14 +112,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
quotePrompt = []
|
quotePrompt = []
|
||||||
} = await (async () => {
|
} = await (async () => {
|
||||||
// 使用了知识库搜索
|
// 使用了知识库搜索
|
||||||
if (model.chat.relatedKbs?.length > 0) {
|
if (app.chat.relatedKbs?.length > 0) {
|
||||||
const { rawSearch, quotePrompt, userSystemPrompt, userLimitPrompt } = await appKbSearch({
|
const { rawSearch, quotePrompt, userSystemPrompt, userLimitPrompt } = await appKbSearch({
|
||||||
model,
|
model: app,
|
||||||
userId,
|
userId,
|
||||||
fixedQuote: history[history.length - 1]?.quote,
|
fixedQuote: history[history.length - 1]?.quote,
|
||||||
prompt,
|
prompt,
|
||||||
similarity: model.chat.searchSimilarity,
|
similarity: app.chat.searchSimilarity,
|
||||||
limit: model.chat.searchLimit
|
limit: app.chat.searchLimit
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -130,19 +130,19 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
userSystemPrompt: model.chat.systemPrompt
|
userSystemPrompt: app.chat.systemPrompt
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
obj: ChatRoleEnum.System,
|
obj: ChatRoleEnum.System,
|
||||||
value: model.chat.systemPrompt
|
value: app.chat.systemPrompt
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
userLimitPrompt: model.chat.limitPrompt
|
userLimitPrompt: app.chat.limitPrompt
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
obj: ChatRoleEnum.Human,
|
obj: ChatRoleEnum.Human,
|
||||||
value: model.chat.limitPrompt
|
value: app.chat.limitPrompt
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: []
|
: []
|
||||||
@ -150,15 +150,15 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
// search result is empty
|
// search result is empty
|
||||||
if (model.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && model.chat.searchEmptyText) {
|
if (app.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && app.chat.searchEmptyText) {
|
||||||
const response = model.chat.searchEmptyText;
|
const response = app.chat.searchEmptyText;
|
||||||
if (stream) {
|
if (stream) {
|
||||||
sseResponse({
|
sseResponse({
|
||||||
res,
|
res,
|
||||||
event: sseResponseEventEnum.answer,
|
event: sseResponseEventEnum.answer,
|
||||||
data: textAdaptGptResponse({
|
data: textAdaptGptResponse({
|
||||||
text: response,
|
text: response,
|
||||||
model: model.chat.chatModel,
|
model: app.chat.chatModel,
|
||||||
finish_reason: 'stop'
|
finish_reason: 'stop'
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@ -166,9 +166,9 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
} else {
|
} else {
|
||||||
return res.json({
|
return res.json({
|
||||||
id: chatId || '',
|
id: chatId || '',
|
||||||
model: model.chat.chatModel,
|
|
||||||
object: 'chat.completion',
|
object: 'chat.completion',
|
||||||
created: 1688608930,
|
created: 1688608930,
|
||||||
|
model: app.chat.chatModel,
|
||||||
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
||||||
choices: [
|
choices: [
|
||||||
{ message: { role: 'assistant', content: response }, finish_reason: 'stop', index: 0 }
|
{ message: { role: 'assistant', content: response }, finish_reason: 'stop', index: 0 }
|
||||||
@ -186,9 +186,9 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
prompt
|
prompt
|
||||||
];
|
];
|
||||||
// chat temperature
|
// chat temperature
|
||||||
const modelConstantsData = ChatModelMap[model.chat.chatModel];
|
const modelConstantsData = ChatModelMap[app.chat.chatModel];
|
||||||
// FastGpt temperature range: 1~10
|
// FastGpt temperature range: 1~10
|
||||||
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
|
const temperature = (modelConstantsData.maxTemperature * (app.chat.temperature / 10)).toFixed(
|
||||||
2
|
2
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -196,13 +196,13 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
input: `${userSystemPrompt[0]?.value}\n${userLimitPrompt[0]?.value}\n${prompt.value}`
|
input: `${userSystemPrompt[0]?.value}\n${userLimitPrompt[0]?.value}\n${prompt.value}`
|
||||||
});
|
});
|
||||||
|
|
||||||
// start model api. responseText and totalTokens: valid only if stream = false
|
// start app api. responseText and totalTokens: valid only if stream = false
|
||||||
const { streamResponse, responseMessages, responseText, totalTokens } =
|
const { streamResponse, responseMessages, responseText, totalTokens } =
|
||||||
await modelServiceToolMap.chatCompletion({
|
await modelServiceToolMap.chatCompletion({
|
||||||
model: model.chat.chatModel,
|
model: app.chat.chatModel,
|
||||||
apiKey: userOpenAiKey || apiKey,
|
apiKey: userOpenAiKey || apiKey,
|
||||||
temperature: +temperature,
|
temperature: +temperature,
|
||||||
maxToken: model.chat.maxToken,
|
maxToken: app.chat.maxToken,
|
||||||
messages: completePrompts,
|
messages: completePrompts,
|
||||||
stream,
|
stream,
|
||||||
res
|
res
|
||||||
@ -242,7 +242,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
});
|
});
|
||||||
// response answer
|
// response answer
|
||||||
const { finishMessages, totalTokens, responseContent } = await V2_StreamResponse({
|
const { finishMessages, totalTokens, responseContent } = await V2_StreamResponse({
|
||||||
model: model.chat.chatModel,
|
model: app.chat.chatModel,
|
||||||
res,
|
res,
|
||||||
chatResponse: streamResponse,
|
chatResponse: streamResponse,
|
||||||
prompts: responseMessages
|
prompts: responseMessages
|
||||||
@ -300,7 +300,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
id: chatId || '',
|
id: chatId || '',
|
||||||
object: 'chat.completion',
|
object: 'chat.completion',
|
||||||
created: 1688608930,
|
created: 1688608930,
|
||||||
model: model.chat.chatModel,
|
model: app.chat.chatModel,
|
||||||
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: tokens },
|
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: tokens },
|
||||||
choices: [
|
choices: [
|
||||||
{ message: { role: 'assistant', content: answer }, finish_reason: 'stop', index: 0 }
|
{ message: { role: 'assistant', content: answer }, finish_reason: 'stop', index: 0 }
|
||||||
@ -310,7 +310,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
|
|
||||||
pushChatBill({
|
pushChatBill({
|
||||||
isPay: !userOpenAiKey,
|
isPay: !userOpenAiKey,
|
||||||
chatModel: model.chat.chatModel,
|
chatModel: app.chat.chatModel,
|
||||||
userId,
|
userId,
|
||||||
textLen,
|
textLen,
|
||||||
tokens,
|
tokens,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { connectToDatabase } from '@/service/mongo';
|
import { connectToDatabase } from '@/service/mongo';
|
||||||
import { authUser, authModel, getApiKey, authShareChat } from '@/service/utils/auth';
|
import { authUser, authApp, getApiKey, authShareChat } from '@/service/utils/auth';
|
||||||
import { sseErrRes, jsonRes } from '@/service/response';
|
import { sseErrRes, jsonRes } from '@/service/response';
|
||||||
import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
|
import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
|
||||||
import { withNextCors } from '@/service/utils/tools';
|
import { withNextCors } from '@/service/utils/tools';
|
||||||
@ -13,14 +13,14 @@ import { type ChatCompletionRequestMessage } from 'openai';
|
|||||||
import {
|
import {
|
||||||
kbChatAppDemo,
|
kbChatAppDemo,
|
||||||
chatAppDemo,
|
chatAppDemo,
|
||||||
lafClassifyQuestionDemo,
|
|
||||||
classifyQuestionDemo,
|
|
||||||
SpecificInputEnum,
|
SpecificInputEnum,
|
||||||
AppModuleItemTypeEnum
|
AppModuleItemTypeEnum
|
||||||
} from '@/constants/app';
|
} from '@/constants/app';
|
||||||
import { Types } from 'mongoose';
|
import { model, Types } from 'mongoose';
|
||||||
import { moduleFetch } from '@/service/api/request';
|
import { moduleFetch } from '@/service/api/request';
|
||||||
import { AppModuleItemType } from '@/types/app';
|
import { AppModuleItemType, RunningModuleItemType } from '@/types/app';
|
||||||
|
import { FlowInputItemTypeEnum, FlowOutputItemTypeEnum } from '@/constants/flow';
|
||||||
|
import { SystemInputEnum } from '@/constants/app';
|
||||||
|
|
||||||
export type MessageItemType = ChatCompletionRequestMessage & { _id?: string };
|
export type MessageItemType = ChatCompletionRequestMessage & { _id?: string };
|
||||||
type FastGptWebChatProps = {
|
type FastGptWebChatProps = {
|
||||||
@ -82,8 +82,15 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
throw new Error('appId is empty');
|
throw new Error('appId is empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
// get history
|
// auth app, get history
|
||||||
const { history } = await getChatHistory({ chatId, userId });
|
const [{ app }, { history }] = await Promise.all([
|
||||||
|
authApp({
|
||||||
|
appId,
|
||||||
|
userId
|
||||||
|
}),
|
||||||
|
getChatHistory({ chatId, userId })
|
||||||
|
]);
|
||||||
|
|
||||||
const prompts = history.concat(gptMessage2ChatType(messages));
|
const prompts = history.concat(gptMessage2ChatType(messages));
|
||||||
if (prompts[prompts.length - 1].obj === 'AI') {
|
if (prompts[prompts.length - 1].obj === 'AI') {
|
||||||
prompts.pop();
|
prompts.pop();
|
||||||
@ -95,12 +102,15 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
throw new Error('Question is empty');
|
throw new Error('Question is empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* start process */
|
const newChatId = chatId === '' ? new Types.ObjectId() : undefined;
|
||||||
const modules = JSON.parse(JSON.stringify(classifyQuestionDemo.modules));
|
if (stream && newChatId) {
|
||||||
|
res.setHeader('newChatId', String(newChatId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* start process */
|
||||||
const { responseData, answerText } = await dispatchModules({
|
const { responseData, answerText } = await dispatchModules({
|
||||||
res,
|
res,
|
||||||
modules,
|
modules: app.modules,
|
||||||
params: {
|
params: {
|
||||||
history: prompts,
|
history: prompts,
|
||||||
userChatInput: prompt.value
|
userChatInput: prompt.value
|
||||||
@ -110,8 +120,9 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
|
|
||||||
// save chat
|
// save chat
|
||||||
if (typeof chatId === 'string') {
|
if (typeof chatId === 'string') {
|
||||||
const { newChatId } = await saveChat({
|
await saveChat({
|
||||||
chatId,
|
chatId,
|
||||||
|
newChatId,
|
||||||
modelId: appId,
|
modelId: appId,
|
||||||
prompts: [
|
prompts: [
|
||||||
prompt,
|
prompt,
|
||||||
@ -124,19 +135,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
],
|
],
|
||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newChatId) {
|
|
||||||
sseResponse({
|
|
||||||
res,
|
|
||||||
event: sseResponseEventEnum.chatResponse,
|
|
||||||
data: JSON.stringify({
|
|
||||||
newChatId
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stream) {
|
if (stream) {
|
||||||
|
sseResponse({
|
||||||
|
res,
|
||||||
|
event: sseResponseEventEnum.answer,
|
||||||
|
data: '[DONE]'
|
||||||
|
});
|
||||||
sseResponse({
|
sseResponse({
|
||||||
res,
|
res,
|
||||||
event: sseResponseEventEnum.appStreamResponse,
|
event: sseResponseEventEnum.appStreamResponse,
|
||||||
@ -145,7 +151,10 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
res.end();
|
res.end();
|
||||||
} else {
|
} else {
|
||||||
res.json({
|
res.json({
|
||||||
data: responseData,
|
data: {
|
||||||
|
newChatId,
|
||||||
|
...responseData
|
||||||
|
},
|
||||||
id: chatId || '',
|
id: chatId || '',
|
||||||
model: '',
|
model: '',
|
||||||
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
||||||
@ -183,6 +192,7 @@ async function dispatchModules({
|
|||||||
params?: Record<string, any>;
|
params?: Record<string, any>;
|
||||||
stream?: boolean;
|
stream?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const runningModules = loadModules(modules);
|
||||||
let storeData: Record<string, any> = {};
|
let storeData: Record<string, any> = {};
|
||||||
let responseData: Record<string, any> = {};
|
let responseData: Record<string, any> = {};
|
||||||
let answerText = '';
|
let answerText = '';
|
||||||
@ -212,7 +222,10 @@ async function dispatchModules({
|
|||||||
...data
|
...data
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function moduleInput(module: AppModuleItemType, data: Record<string, any> = {}): Promise<any> {
|
function moduleInput(
|
||||||
|
module: RunningModuleItemType,
|
||||||
|
data: Record<string, any> = {}
|
||||||
|
): Promise<any> {
|
||||||
const checkInputFinish = () => {
|
const checkInputFinish = () => {
|
||||||
return !module.inputs.find((item: any) => item.value === undefined);
|
return !module.inputs.find((item: any) => item.value === undefined);
|
||||||
};
|
};
|
||||||
@ -222,50 +235,58 @@ async function dispatchModules({
|
|||||||
module.inputs[index].value = value;
|
module.inputs[index].value = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const set = new Set();
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
Object.entries(data).map(([key, val]: any) => {
|
Object.entries(data).map(([key, val]: any) => {
|
||||||
updateInputValue(key, val);
|
updateInputValue(key, val);
|
||||||
if (checkInputFinish()) {
|
|
||||||
|
if (!set.has(module.moduleId) && checkInputFinish()) {
|
||||||
|
set.add(module.moduleId);
|
||||||
return moduleRun(module);
|
return moduleRun(module);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function moduleOutput(module: AppModuleItemType, result: Record<string, any> = {}): Promise<any> {
|
function moduleOutput(
|
||||||
|
module: RunningModuleItemType,
|
||||||
|
result: Record<string, any> = {}
|
||||||
|
): Promise<any> {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
module.outputs.map((item) => {
|
module.outputs.map((outputItem) => {
|
||||||
if (result[item.key] === undefined) return;
|
if (result[outputItem.key] === undefined) return;
|
||||||
/* update output value */
|
/* update output value */
|
||||||
item.value = result[item.key];
|
outputItem.value = result[outputItem.key];
|
||||||
|
|
||||||
pushStore({
|
pushStore({
|
||||||
isResponse: item.response,
|
isResponse: outputItem.response,
|
||||||
answer: item.answer ? item.value : '',
|
answer: outputItem.answer ? outputItem.value : '',
|
||||||
data: {
|
data: {
|
||||||
[item.key]: item.value
|
[outputItem.key]: outputItem.value
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* update target */
|
/* update target */
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
item.targets.map((target: any) => {
|
outputItem.targets.map((target: any) => {
|
||||||
// find module
|
// find module
|
||||||
const targetModule = modules.find((item) => item.moduleId === target.moduleId);
|
const targetModule = runningModules.find((item) => item.moduleId === target.moduleId);
|
||||||
if (!targetModule) return;
|
if (!targetModule) return;
|
||||||
return moduleInput(targetModule, { [target.key]: item.value });
|
return moduleInput(targetModule, { [target.key]: outputItem.value });
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
async function moduleRun(module: AppModuleItemType): Promise<any> {
|
async function moduleRun(module: RunningModuleItemType): Promise<any> {
|
||||||
|
if (res.closed) return Promise.resolve();
|
||||||
console.log('run=========', module.type, module.url);
|
console.log('run=========', module.type, module.url);
|
||||||
|
|
||||||
if (module.type === AppModuleItemTypeEnum.answer) {
|
if (module.type === AppModuleItemTypeEnum.answer) {
|
||||||
pushStore({
|
pushStore({
|
||||||
answer: module.inputs[0].value
|
answer: module.inputs.find((item) => item.key === SpecificInputEnum.answerText)?.value || ''
|
||||||
});
|
});
|
||||||
return AnswerResponse({
|
return StreamAnswer({
|
||||||
res,
|
res,
|
||||||
stream,
|
stream,
|
||||||
text: module.inputs.find((item) => item.key === SpecificInputEnum.answerText)?.value
|
text: module.inputs.find((item) => item.key === SpecificInputEnum.answerText)?.value
|
||||||
@ -276,16 +297,19 @@ async function dispatchModules({
|
|||||||
return moduleOutput(module, switchResponse(module));
|
return moduleOutput(module, switchResponse(module));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (module.type === AppModuleItemTypeEnum.http && module.url) {
|
if (
|
||||||
|
(module.type === AppModuleItemTypeEnum.http ||
|
||||||
|
module.type === AppModuleItemTypeEnum.initInput) &&
|
||||||
|
module.url
|
||||||
|
) {
|
||||||
// get fetch params
|
// get fetch params
|
||||||
const inputParams: Record<string, any> = {};
|
const params: Record<string, any> = {};
|
||||||
module.inputs.forEach((item: any) => {
|
module.inputs.forEach((item: any) => {
|
||||||
inputParams[item.key] = item.value;
|
params[item.key] = item.value;
|
||||||
});
|
});
|
||||||
const data = {
|
const data = {
|
||||||
stream,
|
stream,
|
||||||
...module.body,
|
...params
|
||||||
...inputParams
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// response data
|
// response data
|
||||||
@ -299,8 +323,12 @@ async function dispatchModules({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从填充 params 开始进入递归
|
// start process width initInput
|
||||||
await Promise.all(modules.map((module) => moduleInput(module, params)));
|
const initModules = runningModules.filter(
|
||||||
|
(item) => item.type === AppModuleItemTypeEnum.initInput
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(initModules.map((module) => moduleInput(module, params)));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
responseData,
|
responseData,
|
||||||
@ -308,7 +336,29 @@ async function dispatchModules({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function AnswerResponse({
|
function loadModules(modules: AppModuleItemType[]): RunningModuleItemType[] {
|
||||||
|
return modules.map((module) => {
|
||||||
|
return {
|
||||||
|
moduleId: module.moduleId,
|
||||||
|
type: module.type,
|
||||||
|
url: module.url,
|
||||||
|
inputs: module.inputs
|
||||||
|
.filter((item) => item.type !== FlowInputItemTypeEnum.target || item.connected) // filter unconnected target input
|
||||||
|
.map((item) => ({
|
||||||
|
key: item.key,
|
||||||
|
value: item.value
|
||||||
|
})),
|
||||||
|
outputs: module.outputs.map((item) => ({
|
||||||
|
key: item.key,
|
||||||
|
answer: item.type === FlowOutputItemTypeEnum.answer,
|
||||||
|
response: item.response,
|
||||||
|
value: undefined,
|
||||||
|
targets: item.targets
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function StreamAnswer({
|
||||||
res,
|
res,
|
||||||
stream = false,
|
stream = false,
|
||||||
text = ''
|
text = ''
|
||||||
@ -322,13 +372,13 @@ function AnswerResponse({
|
|||||||
res,
|
res,
|
||||||
event: sseResponseEventEnum.answer,
|
event: sseResponseEventEnum.answer,
|
||||||
data: textAdaptGptResponse({
|
data: textAdaptGptResponse({
|
||||||
text
|
text: text.replace(/\\n/g, '\n')
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
function switchResponse(module: any) {
|
function switchResponse(module: RunningModuleItemType) {
|
||||||
const val = module?.inputs?.[0]?.value;
|
const val = module?.inputs?.[0]?.value;
|
||||||
|
|
||||||
if (val) {
|
if (val) {
|
||||||
85
client/src/pages/app/detail/components/API.tsx
Normal file
85
client/src/pages/app/detail/components/API.tsx
Normal 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: false
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
394
client/src/pages/app/detail/components/Kb.tsx
Normal file
394
client/src/pages/app/detail/components/Kb.tsx
Normal 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 { putAppById } from '@/api/app';
|
||||||
|
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 { appDetail, loadKbList, loadAppDetail } = useUserStore();
|
||||||
|
const { Loading, setIsLoading } = useLoading();
|
||||||
|
const [selectedIdList, setSelectedIdList] = useState<string[]>([]);
|
||||||
|
const [refresh, setRefresh] = useState(false);
|
||||||
|
const { register, reset, getValues, setValue } = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
searchSimilarity: appDetail.chat.searchSimilarity,
|
||||||
|
searchLimit: appDetail.chat.searchLimit,
|
||||||
|
searchEmptyText: appDetail.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 putAppById(modelId, {
|
||||||
|
chat: {
|
||||||
|
...appDetail.chat,
|
||||||
|
...data
|
||||||
|
}
|
||||||
|
});
|
||||||
|
loadAppDetail(modelId, true);
|
||||||
|
} catch (err: any) {
|
||||||
|
toast({
|
||||||
|
title: err?.message || '更新失败',
|
||||||
|
status: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
},
|
||||||
|
[setIsLoading, modelId, appDetail.chat, loadAppDetail, toast]
|
||||||
|
);
|
||||||
|
|
||||||
|
// init kb select list
|
||||||
|
const { isLoading, data: kbList = [] } = useQuery(['loadKbList'], () => loadKbList());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box position={'relative'} px={5} minH={'50vh'}>
|
||||||
|
<Box fontWeight={'bold'}>关联的知识库({appDetail.chat?.relatedKbs.length})</Box>
|
||||||
|
{(() => {
|
||||||
|
const kbs =
|
||||||
|
appDetail.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: appDetail.chat.searchSimilarity,
|
||||||
|
searchLimit: appDetail.chat.searchLimit,
|
||||||
|
searchEmptyText: appDetail.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'}>
|
||||||
|
相似度: {appDetail.chat.searchSimilarity}, 单次搜索数量:{' '}
|
||||||
|
{appDetail.chat.searchLimit}, 空搜索时拒绝回复:{' '}
|
||||||
|
{appDetail.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(
|
||||||
|
appDetail.chat?.relatedKbs ? [...appDetail.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 = appDetail.chat?.relatedKbs
|
||||||
|
? [...appDetail.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={appDetail.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}
|
||||||
|
value={getValues('searchSimilarity')}
|
||||||
|
onChange={(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}
|
||||||
|
value={getValues('searchLimit')}
|
||||||
|
onChange={(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;
|
||||||
390
client/src/pages/app/detail/components/Settings.tsx
Normal file
390
client/src/pages/app/detail/components/Settings.tsx
Normal file
@ -0,0 +1,390 @@
|
|||||||
|
import React, { useCallback, useState, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Textarea,
|
||||||
|
Divider,
|
||||||
|
Tooltip
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||||
|
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, putAppById } from '@/api/app';
|
||||||
|
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 { AppSchema } from '@/types/mongoSchema';
|
||||||
|
|
||||||
|
import Avatar from '@/components/Avatar';
|
||||||
|
import MySelect from '@/components/Select';
|
||||||
|
import MySlider from '@/components/Slider';
|
||||||
|
|
||||||
|
const systemPromptTip =
|
||||||
|
'模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。';
|
||||||
|
const limitPromptTip =
|
||||||
|
'限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"';
|
||||||
|
|
||||||
|
const Settings = ({ modelId }: { modelId: string }) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
const { Loading, setIsLoading } = useLoading();
|
||||||
|
const { userInfo, appDetail, myApps, loadAppDetail, 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: appDetail
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOwner = useMemo(
|
||||||
|
() => appDetail.userId === userInfo?._id,
|
||||||
|
[appDetail.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: AppSchema) => {
|
||||||
|
setBtnLoading(true);
|
||||||
|
try {
|
||||||
|
await putAppById(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 (!appDetail) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await delModelById(appDetail._id);
|
||||||
|
toast({
|
||||||
|
title: '删除成功',
|
||||||
|
status: 'success'
|
||||||
|
});
|
||||||
|
refreshModel.removeModelDetail(appDetail._id);
|
||||||
|
router.replace(`/model?modelId=${myApps[1]?._id}`);
|
||||||
|
} catch (err: any) {
|
||||||
|
toast({
|
||||||
|
title: err?.message || '删除失败',
|
||||||
|
status: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
}, [appDetail, setIsLoading, toast, refreshModel, router, myApps]);
|
||||||
|
|
||||||
|
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], () => loadAppDetail(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={4}
|
||||||
|
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={['90%', '280px']}
|
||||||
|
value={getValues('chat.chatModel')}
|
||||||
|
list={chatModelList.map((item) => ({
|
||||||
|
value: item.chatModel,
|
||||||
|
label: `${item.name} (${formatPrice(
|
||||||
|
ChatModelMap[item.chatModel]?.price,
|
||||||
|
1000
|
||||||
|
)} 元/1k tokens)`
|
||||||
|
}))}
|
||||||
|
onchange={(val: any) => {
|
||||||
|
setValue('chat.chatModel', val);
|
||||||
|
setRefresh(!refresh);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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={['90%', '260px']}
|
||||||
|
min={0}
|
||||||
|
max={10}
|
||||||
|
value={getValues('chat.temperature')}
|
||||||
|
onChange={(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={['90%', '260px']}
|
||||||
|
min={100}
|
||||||
|
max={tokenLimit}
|
||||||
|
step={50}
|
||||||
|
value={getValues('chat.maxToken')}
|
||||||
|
onChange={(val) => {
|
||||||
|
setValue('chat.maxToken', val);
|
||||||
|
setRefresh(!refresh);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
<Flex mt={10} alignItems={'flex-start'}>
|
||||||
|
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
||||||
|
提示词
|
||||||
|
<Tooltip label={systemPromptTip}>
|
||||||
|
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
<Textarea
|
||||||
|
rows={8}
|
||||||
|
placeholder={systemPromptTip}
|
||||||
|
{...register('chat.systemPrompt')}
|
||||||
|
></Textarea>
|
||||||
|
</Flex>
|
||||||
|
<Flex mt={5} alignItems={'flex-start'}>
|
||||||
|
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
||||||
|
限定词
|
||||||
|
<Tooltip label={limitPromptTip}>
|
||||||
|
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
<Textarea
|
||||||
|
rows={5}
|
||||||
|
placeholder={limitPromptTip}
|
||||||
|
{...register('chat.limitPrompt')}
|
||||||
|
></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;
|
||||||
281
client/src/pages/app/detail/components/Share.tsx
Normal file
281
client/src/pages/app/detail/components/Share.tsx
Normal 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;
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { NodeProps } from 'reactflow';
|
||||||
|
import NodeCard from './modules/NodeCard';
|
||||||
|
import { FlowModuleItemType } from '@/types/flow';
|
||||||
|
import Divider from './modules/Divider';
|
||||||
|
import Container from './modules/Container';
|
||||||
|
import RenderInput from './render/RenderInput';
|
||||||
|
|
||||||
|
const NodeAnswer = ({
|
||||||
|
data: { moduleId, inputs, outputs, onChangeNode, ...props }
|
||||||
|
}: NodeProps<FlowModuleItemType>) => {
|
||||||
|
return (
|
||||||
|
<NodeCard
|
||||||
|
minW={'400px'}
|
||||||
|
logo={'/icon/logo.png'}
|
||||||
|
name={'SSE 响应'}
|
||||||
|
moduleId={moduleId}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Divider text="Input" />
|
||||||
|
<Container>
|
||||||
|
<RenderInput moduleId={moduleId} onChangeNode={onChangeNode} flowInputList={inputs} />
|
||||||
|
</Container>
|
||||||
|
</NodeCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default React.memo(NodeAnswer);
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { NodeProps } from 'reactflow';
|
||||||
|
import NodeCard from './modules/NodeCard';
|
||||||
|
import { FlowModuleItemType } from '@/types/flow';
|
||||||
|
import Divider from './modules/Divider';
|
||||||
|
import Container from './modules/Container';
|
||||||
|
import RenderInput from './render/RenderInput';
|
||||||
|
import RenderOutput from './render/RenderOutput';
|
||||||
|
import { FlowOutputItemTypeEnum } from '@/constants/flow';
|
||||||
|
|
||||||
|
const NodeChat = ({
|
||||||
|
data: { moduleId, inputs, outputs, onChangeNode, ...props }
|
||||||
|
}: NodeProps<FlowModuleItemType>) => {
|
||||||
|
const outputsLen = useMemo(
|
||||||
|
() => outputs.filter((item) => item.type !== FlowOutputItemTypeEnum.hidden).length,
|
||||||
|
[outputs]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<NodeCard minW={'400px'} logo={'/icon/logo.png'} name={'对话'} moduleId={moduleId} {...props}>
|
||||||
|
<Divider text="Input" />
|
||||||
|
<Container>
|
||||||
|
<RenderInput moduleId={moduleId} onChangeNode={onChangeNode} flowInputList={inputs} />
|
||||||
|
</Container>
|
||||||
|
{outputsLen > 0 && (
|
||||||
|
<>
|
||||||
|
<Divider text="Output" />
|
||||||
|
<Container>
|
||||||
|
<RenderOutput flowOutputList={outputs} />
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</NodeCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default React.memo(NodeChat);
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { NodeProps } from 'reactflow';
|
||||||
|
import NodeCard from './modules/NodeCard';
|
||||||
|
import { FlowModuleItemType } from '@/types/flow';
|
||||||
|
import Divider from './modules/Divider';
|
||||||
|
import Container from './modules/Container';
|
||||||
|
import RenderInput from './render/RenderInput';
|
||||||
|
import RenderOutput from './render/RenderOutput';
|
||||||
|
|
||||||
|
const NodeHistory = ({
|
||||||
|
data: { inputs, outputs, onChangeNode, ...props }
|
||||||
|
}: NodeProps<FlowModuleItemType>) => {
|
||||||
|
return (
|
||||||
|
<NodeCard minW={'300px'} {...props}>
|
||||||
|
<Divider text="Input" />
|
||||||
|
<Container>
|
||||||
|
<RenderInput moduleId={props.moduleId} onChangeNode={onChangeNode} flowInputList={inputs} />
|
||||||
|
</Container>
|
||||||
|
<Divider text="Output" />
|
||||||
|
<Container>
|
||||||
|
<RenderOutput flowOutputList={outputs} />
|
||||||
|
</Container>
|
||||||
|
</NodeCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default React.memo(NodeHistory);
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { NodeProps } from 'reactflow';
|
||||||
|
import NodeCard from './modules/NodeCard';
|
||||||
|
import { FlowModuleItemType } from '@/types/flow';
|
||||||
|
import Divider from './modules/Divider';
|
||||||
|
import Container from './modules/Container';
|
||||||
|
import RenderInput from './render/RenderInput';
|
||||||
|
import RenderOutput from './render/RenderOutput';
|
||||||
|
import KBSelect from './Plugins/KBSelect';
|
||||||
|
|
||||||
|
const NodeKbSearch = ({
|
||||||
|
data: { moduleId, inputs, outputs, onChangeNode, ...props }
|
||||||
|
}: NodeProps<FlowModuleItemType>) => {
|
||||||
|
return (
|
||||||
|
<NodeCard
|
||||||
|
minW={'400px'}
|
||||||
|
logo={'/icon/logo.png'}
|
||||||
|
name={'知识库搜索'}
|
||||||
|
moduleId={moduleId}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Divider text="Input" />
|
||||||
|
<Container>
|
||||||
|
<RenderInput
|
||||||
|
moduleId={moduleId}
|
||||||
|
onChangeNode={onChangeNode}
|
||||||
|
flowInputList={inputs}
|
||||||
|
CustomComponent={{
|
||||||
|
kb_ids: ({ key, value, onChangeNode }) => (
|
||||||
|
<KBSelect
|
||||||
|
relatedKbs={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChangeNode({
|
||||||
|
moduleId,
|
||||||
|
key,
|
||||||
|
value: e
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
<Divider text="Output" />
|
||||||
|
<Container>
|
||||||
|
<RenderOutput flowOutputList={outputs} />
|
||||||
|
</Container>
|
||||||
|
</NodeCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default React.memo(NodeKbSearch);
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Handle, Position, NodeProps } from 'reactflow';
|
||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
import NodeCard from './modules/NodeCard';
|
||||||
|
import { FlowModuleItemType } from '@/types/flow';
|
||||||
|
import Container from './modules/Container';
|
||||||
|
import { SystemInputEnum } from '@/constants/app';
|
||||||
|
|
||||||
|
const QuestionInputNode = ({
|
||||||
|
data: { inputs, outputs, onChangeNode, ...props }
|
||||||
|
}: NodeProps<FlowModuleItemType>) => {
|
||||||
|
return (
|
||||||
|
<NodeCard minW={'220px'} {...props}>
|
||||||
|
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'} textAlign={'end'}>
|
||||||
|
<Box>用户问题</Box>
|
||||||
|
<Handle
|
||||||
|
style={{
|
||||||
|
bottom: '0',
|
||||||
|
right: '0',
|
||||||
|
transform: 'translate(50%,-5px)',
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
background: '#9CA2A8'
|
||||||
|
}}
|
||||||
|
id={SystemInputEnum.userChatInput}
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
</NodeCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default React.memo(QuestionInputNode);
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Handle, Position, NodeProps } from 'reactflow';
|
||||||
|
import { Flex, Box } from '@chakra-ui/react';
|
||||||
|
import NodeCard from './modules/NodeCard';
|
||||||
|
import { SystemInputEnum } from '@/constants/app';
|
||||||
|
import { FlowModuleItemType } from '@/types/flow';
|
||||||
|
import Divider from './modules/Divider';
|
||||||
|
import Container from './modules/Container';
|
||||||
|
import Label from './modules/Label';
|
||||||
|
|
||||||
|
const NodeTFSwitch = ({ data: { inputs, outputs, ...props } }: NodeProps<FlowModuleItemType>) => {
|
||||||
|
return (
|
||||||
|
<NodeCard minW={'220px'} logo={'/icon/logo.png'} name={'TF 开关'} {...props}>
|
||||||
|
<Divider text="输入输出" />
|
||||||
|
<Container h={'100px'} py={0} px={0} display={'flex'} alignItems={'center'}>
|
||||||
|
<Box flex={1} pl={'12px'}>
|
||||||
|
<Label
|
||||||
|
required
|
||||||
|
description="接收到 false、0、null、undefined或空字符串时,执行 False,反之执行 True"
|
||||||
|
>
|
||||||
|
输入
|
||||||
|
</Label>
|
||||||
|
<Handle
|
||||||
|
style={{
|
||||||
|
top: '50%',
|
||||||
|
left: '0',
|
||||||
|
transform: 'translate(-50%,-50%)',
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
background: '#9CA2A8'
|
||||||
|
}}
|
||||||
|
id={SystemInputEnum.switch}
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
onConnect={(params) => console.log('input onConnect', params)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box flex={1} pr={'12px'}>
|
||||||
|
<Flex alignItems={'center'} justifyContent={'flex-end'} mb={'26px'} position={'relative'}>
|
||||||
|
<Label>True</Label>
|
||||||
|
<Handle
|
||||||
|
style={{
|
||||||
|
top: '0',
|
||||||
|
right: '-12px',
|
||||||
|
transform: 'translate(50%,5px)',
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
background: '#9CA2A8'
|
||||||
|
}}
|
||||||
|
id={'true'}
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
onConnect={(params) => console.log('handle onConnect', params)}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Flex alignItems={'center'} justifyContent={'flex-end'} position={'relative'}>
|
||||||
|
<Label>False</Label>
|
||||||
|
<Handle
|
||||||
|
style={{
|
||||||
|
bottom: '0',
|
||||||
|
right: '-12px',
|
||||||
|
transform: 'translate(50%,-5px)',
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
background: '#9CA2A8'
|
||||||
|
}}
|
||||||
|
id={'false'}
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
onConnect={(params) => console.log('handle onConnect', params)}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</NodeCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default React.memo(NodeTFSwitch);
|
||||||
@ -0,0 +1,143 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Flex,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalBody,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
ModalCloseButton,
|
||||||
|
useTheme,
|
||||||
|
useDisclosure,
|
||||||
|
Grid
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useUserStore } from '@/store/user';
|
||||||
|
import Avatar from '@/components/Avatar';
|
||||||
|
|
||||||
|
const KBSelect = ({
|
||||||
|
relatedKbs = [],
|
||||||
|
onChange
|
||||||
|
}: {
|
||||||
|
relatedKbs: string[];
|
||||||
|
onChange: (e: string[]) => void;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { myKbList, loadKbList } = useUserStore();
|
||||||
|
const [selectedIdList, setSelectedIdList] = useState<string[]>(relatedKbs);
|
||||||
|
const {
|
||||||
|
isOpen: isOpenKbSelect,
|
||||||
|
onOpen: onOpenKbSelect,
|
||||||
|
onClose: onCloseKbSelect
|
||||||
|
} = useDisclosure();
|
||||||
|
|
||||||
|
const showKbList = useMemo(
|
||||||
|
() => myKbList.filter((item) => relatedKbs.includes(item._id)),
|
||||||
|
[myKbList, relatedKbs]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadKbList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid gridTemplateColumns={'1fr 1fr'} gridGap={4}>
|
||||||
|
<Button h={'36px'} onClick={onOpenKbSelect}>
|
||||||
|
选择知识库
|
||||||
|
</Button>
|
||||||
|
{showKbList.map((item) => (
|
||||||
|
<Flex
|
||||||
|
key={item._id}
|
||||||
|
alignItems={'center'}
|
||||||
|
h={'36px'}
|
||||||
|
border={theme.borders.base}
|
||||||
|
px={2}
|
||||||
|
borderRadius={'md'}
|
||||||
|
>
|
||||||
|
<Avatar src={item.avatar} w={'24px'}></Avatar>
|
||||||
|
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
|
||||||
|
{item.name}
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
<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}
|
||||||
|
>
|
||||||
|
{myKbList.map((item) => (
|
||||||
|
<Card
|
||||||
|
key={item._id}
|
||||||
|
p={3}
|
||||||
|
border={theme.borders.base}
|
||||||
|
boxShadow={'sm'}
|
||||||
|
h={'80px'}
|
||||||
|
cursor={'pointer'}
|
||||||
|
order={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) => myKbList.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();
|
||||||
|
onChange(selectedIdList);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
完成
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KBSelect;
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
|
import { ModuleTemplates } from '@/constants/flow/ModuleTemplate';
|
||||||
|
import type { AppModuleTemplateItemType } from '@/types/app';
|
||||||
|
import type { XYPosition } from 'reactflow';
|
||||||
|
import Avatar from '@/components/Avatar';
|
||||||
|
|
||||||
|
const ModuleStoreList = ({
|
||||||
|
isOpen,
|
||||||
|
onAddNode
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onAddNode: (e: { template: AppModuleTemplateItemType; position: XYPosition }) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
flexDirection={'column'}
|
||||||
|
position={'absolute'}
|
||||||
|
top={'65px'}
|
||||||
|
left={0}
|
||||||
|
h={isOpen ? '90%' : '0'}
|
||||||
|
w={isOpen ? '360px' : '0'}
|
||||||
|
bg={'white'}
|
||||||
|
zIndex={1}
|
||||||
|
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||||
|
borderRadius={'20px'}
|
||||||
|
overflow={'hidden'}
|
||||||
|
transition={'.2s ease'}
|
||||||
|
px={'15px'}
|
||||||
|
userSelect={'none'}
|
||||||
|
>
|
||||||
|
<Box w={'330px'} py={4} fontSize={'xl'} fontWeight={'bold'}>
|
||||||
|
添加模块
|
||||||
|
</Box>
|
||||||
|
<Box w={'330px'} flex={'1 0 0'} overflow={'overlay'}>
|
||||||
|
{ModuleTemplates.map((item) =>
|
||||||
|
item.list.map((item) => (
|
||||||
|
<Flex
|
||||||
|
key={item.name}
|
||||||
|
alignItems={'center'}
|
||||||
|
p={5}
|
||||||
|
cursor={'pointer'}
|
||||||
|
_hover={{ bg: 'myWhite.600' }}
|
||||||
|
borderRadius={'md'}
|
||||||
|
draggable
|
||||||
|
onDragEnd={(e) => {
|
||||||
|
if (e.clientX < 400) return;
|
||||||
|
onAddNode({
|
||||||
|
template: item,
|
||||||
|
position: { x: e.clientX, y: e.clientY }
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar src={item.logo} w={'34px'} />
|
||||||
|
<Box ml={5} flex={'1 0 0'}>
|
||||||
|
<Box color={'black'}>{item.name}</Box>
|
||||||
|
<Box color={'myGray.500'} fontSize={'sm'}>
|
||||||
|
{item.intro}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModuleStoreList;
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath } from 'reactflow';
|
||||||
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
import MyIcon from '@/components/Icon';
|
||||||
|
|
||||||
|
export default function ButtonEdge({
|
||||||
|
id,
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
sourcePosition,
|
||||||
|
targetPosition,
|
||||||
|
style = {},
|
||||||
|
markerEnd,
|
||||||
|
data
|
||||||
|
}: EdgeProps<{
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
}>) {
|
||||||
|
const [edgePath, labelX, labelY] = getBezierPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
sourcePosition,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
targetPosition
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />
|
||||||
|
<EdgeLabelRenderer>
|
||||||
|
<Flex
|
||||||
|
alignItems={'center'}
|
||||||
|
justifyContent={'center'}
|
||||||
|
position={'absolute'}
|
||||||
|
transform={`translate(-50%, -50%) translate(${labelX}px,${labelY}px)`}
|
||||||
|
pointerEvents={'all'}
|
||||||
|
w={'20px'}
|
||||||
|
h={'20px'}
|
||||||
|
bg={'white'}
|
||||||
|
borderRadius={'20px'}
|
||||||
|
color={'black'}
|
||||||
|
cursor={'pointer'}
|
||||||
|
border={'1px solid #fff'}
|
||||||
|
_hover={{
|
||||||
|
boxShadow: '0 0 6px 2px rgba(0, 0, 0, 0.08)'
|
||||||
|
}}
|
||||||
|
onClick={() => data?.onDelete(id)}
|
||||||
|
>
|
||||||
|
<MyIcon name="closeSolid" w={'100%'} color={'myGray.600'}></MyIcon>
|
||||||
|
</Flex>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
import { BoxProps } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const Container = ({ children, ...props }: BoxProps) => {
|
||||||
|
return (
|
||||||
|
<Box px={4} py={3} position={'relative'} {...props}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Container;
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, useTheme } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const Divider = ({ text }: { text: 'Body' | 'Input' | 'Output' | string }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
textAlign={'center'}
|
||||||
|
bg={'#f8f8f8'}
|
||||||
|
py={2}
|
||||||
|
borderTop={theme.borders.base}
|
||||||
|
borderBottom={theme.borders.base}
|
||||||
|
fontSize={'lg'}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Divider;
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Tooltip } from '@chakra-ui/react';
|
||||||
|
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||||
|
|
||||||
|
const Label = ({
|
||||||
|
required = false,
|
||||||
|
children,
|
||||||
|
description
|
||||||
|
}: {
|
||||||
|
required?: boolean;
|
||||||
|
children: React.ReactNode | string;
|
||||||
|
description?: string;
|
||||||
|
}) => (
|
||||||
|
<Box as={'label'} display={'inline-block'} position={'relative'}>
|
||||||
|
{children}
|
||||||
|
{required && (
|
||||||
|
<Box position={'absolute'} top={'-2px'} right={'-10px'} color={'red.500'} fontWeight={'bold'}>
|
||||||
|
*
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<Tooltip label={description}>
|
||||||
|
<QuestionOutlineIcon display={['none', 'inline']} fontSize={'12px'} mb={1} ml={1} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Label;
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Flex, useTheme } from '@chakra-ui/react';
|
||||||
|
import MyIcon from '@/components/Icon';
|
||||||
|
import Avatar from '@/components/Avatar';
|
||||||
|
import type { FlowModuleItemType } from '@/types/flow';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode | React.ReactNode[] | string;
|
||||||
|
logo?: string;
|
||||||
|
name?: string;
|
||||||
|
intro?: string;
|
||||||
|
minW?: string | number;
|
||||||
|
moduleId: string;
|
||||||
|
onDelNode: FlowModuleItemType['onDelNode'];
|
||||||
|
};
|
||||||
|
|
||||||
|
const NodeCard = ({
|
||||||
|
children,
|
||||||
|
logo = '/icon/logo.png',
|
||||||
|
name = '未知模块',
|
||||||
|
minW = '300px',
|
||||||
|
onDelNode,
|
||||||
|
moduleId
|
||||||
|
}: Props) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box minW={minW} bg={'white'} border={theme.borders.md} borderRadius={'md'} boxShadow={'sm'}>
|
||||||
|
<Flex className="custom-drag-handle" px={4} py={3} alignItems={'center'}>
|
||||||
|
<Avatar src={logo} borderRadius={'md'} w={'30px'} h={'30px'} />
|
||||||
|
<Box ml={3} flex={1} fontSize={'lg'} color={'myGray.600'}>
|
||||||
|
{name}
|
||||||
|
</Box>
|
||||||
|
<MyIcon
|
||||||
|
className={'nodrag'}
|
||||||
|
name="delete"
|
||||||
|
cursor={'pointer'}
|
||||||
|
color={'myGray.600'}
|
||||||
|
w={'16px'}
|
||||||
|
_hover={{ color: 'red.600' }}
|
||||||
|
onClick={() => onDelNode(moduleId)}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(NodeCard);
|
||||||
@ -0,0 +1,182 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { FlowInputItemType, FlowModuleItemType } from '@/types/flow';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Textarea,
|
||||||
|
Input,
|
||||||
|
Tooltip,
|
||||||
|
NumberInput,
|
||||||
|
NumberInputField,
|
||||||
|
NumberInputStepper,
|
||||||
|
NumberIncrementStepper,
|
||||||
|
NumberDecrementStepper
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { FlowInputItemTypeEnum } from '@/constants/flow';
|
||||||
|
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||||
|
import { Handle, Position } from 'reactflow';
|
||||||
|
import MySelect from '@/components/Select';
|
||||||
|
import MySlider from '@/components/Slider';
|
||||||
|
|
||||||
|
const Label = ({
|
||||||
|
required = false,
|
||||||
|
children,
|
||||||
|
description
|
||||||
|
}: {
|
||||||
|
required?: boolean;
|
||||||
|
children: React.ReactNode | string;
|
||||||
|
description?: string;
|
||||||
|
}) => (
|
||||||
|
<Box as={'label'} display={'inline-block'} position={'relative'}>
|
||||||
|
{children}
|
||||||
|
{required && (
|
||||||
|
<Box position={'absolute'} top={'-2px'} right={'-10px'} color={'red.500'} fontWeight={'bold'}>
|
||||||
|
*
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<Tooltip label={description}>
|
||||||
|
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const RenderBody = ({
|
||||||
|
flowInputList,
|
||||||
|
moduleId,
|
||||||
|
CustomComponent = {},
|
||||||
|
onChangeNode
|
||||||
|
}: {
|
||||||
|
flowInputList: FlowInputItemType[];
|
||||||
|
moduleId: string;
|
||||||
|
CustomComponent?: Record<
|
||||||
|
string,
|
||||||
|
(e: {
|
||||||
|
key: string;
|
||||||
|
value: any;
|
||||||
|
onChangeNode: FlowModuleItemType['onChangeNode'];
|
||||||
|
}) => React.ReactNode
|
||||||
|
>;
|
||||||
|
onChangeNode: FlowModuleItemType['onChangeNode'];
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{flowInputList.map(
|
||||||
|
(item) =>
|
||||||
|
item.type !== FlowInputItemTypeEnum.hidden && (
|
||||||
|
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
|
||||||
|
<Label required={item.required} description={item.description}>
|
||||||
|
{item.label}
|
||||||
|
</Label>
|
||||||
|
<Box mt={2} className={'nodrag'}>
|
||||||
|
{item.type === FlowInputItemTypeEnum.numberInput && (
|
||||||
|
<NumberInput
|
||||||
|
defaultValue={item.value}
|
||||||
|
min={item.min}
|
||||||
|
max={item.max}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChangeNode({
|
||||||
|
moduleId,
|
||||||
|
key: item.key,
|
||||||
|
value: Number(e)
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NumberInputField />
|
||||||
|
<NumberInputStepper>
|
||||||
|
<NumberIncrementStepper />
|
||||||
|
<NumberDecrementStepper />
|
||||||
|
</NumberInputStepper>
|
||||||
|
</NumberInput>
|
||||||
|
)}
|
||||||
|
{item.type === FlowInputItemTypeEnum.input && (
|
||||||
|
<Input
|
||||||
|
placeholder={item.placeholder}
|
||||||
|
defaultValue={item.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChangeNode({
|
||||||
|
moduleId,
|
||||||
|
key: item.key,
|
||||||
|
value: e.target.value
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.type === FlowInputItemTypeEnum.textarea && (
|
||||||
|
<Textarea
|
||||||
|
rows={5}
|
||||||
|
placeholder={item.placeholder}
|
||||||
|
resize={'both'}
|
||||||
|
defaultValue={item.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChangeNode({
|
||||||
|
moduleId,
|
||||||
|
key: item.key,
|
||||||
|
value: e.target.value
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.type === FlowInputItemTypeEnum.select && (
|
||||||
|
<MySelect
|
||||||
|
width={'100%'}
|
||||||
|
value={item.value}
|
||||||
|
list={item.list || []}
|
||||||
|
onchange={(e) => {
|
||||||
|
onChangeNode({
|
||||||
|
moduleId,
|
||||||
|
key: item.key,
|
||||||
|
value: e
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.type === FlowInputItemTypeEnum.slider && (
|
||||||
|
<Box pt={5} pb={4} px={2}>
|
||||||
|
<MySlider
|
||||||
|
markList={item.markList}
|
||||||
|
width={'100%'}
|
||||||
|
min={item.min || 0}
|
||||||
|
max={item.max}
|
||||||
|
step={item.step || 1}
|
||||||
|
value={item.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChangeNode({
|
||||||
|
moduleId,
|
||||||
|
key: item.key,
|
||||||
|
value: e
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{item.type === FlowInputItemTypeEnum.custom && CustomComponent[item.key] && (
|
||||||
|
<>
|
||||||
|
{CustomComponent[item.key]({ key: item.key, value: item.value, onChangeNode })}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{item.type === FlowInputItemTypeEnum.target && (
|
||||||
|
<Handle
|
||||||
|
style={{
|
||||||
|
top: '50%',
|
||||||
|
left: '-14px',
|
||||||
|
transform: 'translate(-50%,-50%)',
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
background: '#9CA2A8'
|
||||||
|
}}
|
||||||
|
id={item.key}
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
onConnect={(params) => console.log('handle onConnect', params)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(RenderBody);
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { FlowOutputItemType } from '@/types/flow';
|
||||||
|
import { Box, Tooltip, Flex } from '@chakra-ui/react';
|
||||||
|
import { FlowOutputItemTypeEnum } from '@/constants/flow';
|
||||||
|
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||||
|
import { Handle, Position } from 'reactflow';
|
||||||
|
|
||||||
|
const Label = ({
|
||||||
|
children,
|
||||||
|
description
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode | string;
|
||||||
|
description?: string;
|
||||||
|
}) => (
|
||||||
|
<Flex as={'label'} justifyContent={'right'} alignItems={'center'} position={'relative'}>
|
||||||
|
{description && (
|
||||||
|
<Tooltip label={description}>
|
||||||
|
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
const RenderBody = ({ flowOutputList }: { flowOutputList: FlowOutputItemType[] }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{flowOutputList.map(
|
||||||
|
(item) =>
|
||||||
|
item.type !== FlowOutputItemTypeEnum.hidden && (
|
||||||
|
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
|
||||||
|
<Label description={item.description}>{item.label}</Label>
|
||||||
|
<Box mt={FlowOutputItemTypeEnum.answer ? 0 : 2} className={'nodrag'}>
|
||||||
|
{item.type === FlowOutputItemTypeEnum.source && (
|
||||||
|
<Handle
|
||||||
|
style={{
|
||||||
|
top: '50%',
|
||||||
|
right: '-14px',
|
||||||
|
transform: 'translate(50%,-50%)',
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
background: '#9CA2A8'
|
||||||
|
}}
|
||||||
|
type="source"
|
||||||
|
id={item.key}
|
||||||
|
position={Position.Right}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(RenderBody);
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
.panel {
|
||||||
|
.react-flow__panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
312
client/src/pages/app/detail/components/edit/index.tsx
Normal file
312
client/src/pages/app/detail/components/edit/index.tsx
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import ReactFlow, {
|
||||||
|
Background,
|
||||||
|
Controls,
|
||||||
|
ReactFlowProvider,
|
||||||
|
addEdge,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
XYPosition,
|
||||||
|
Connection
|
||||||
|
} from 'reactflow';
|
||||||
|
import { Box, Flex, IconButton, useTheme, useDisclosure } from '@chakra-ui/react';
|
||||||
|
import { SmallCloseIcon } from '@chakra-ui/icons';
|
||||||
|
import { edgeOptions, connectionLineStyle, FlowModuleTypeEnum } from '@/constants/flow';
|
||||||
|
import { appModule2FlowNode, appModule2FlowEdge } from '@/utils/adapt';
|
||||||
|
import {
|
||||||
|
FlowModuleItemType,
|
||||||
|
FlowOutputTargetItemType,
|
||||||
|
type FlowModuleItemChangeProps
|
||||||
|
} from '@/types/flow';
|
||||||
|
import { customAlphabet } from 'nanoid';
|
||||||
|
import { putAppById } from '@/api/app';
|
||||||
|
import { useRequest } from '@/hooks/useRequest';
|
||||||
|
import type { AppSchema } from '@/types/mongoSchema';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
import MyIcon from '@/components/Icon';
|
||||||
|
import ButtonEdge from './components/modules/ButtonEdge';
|
||||||
|
const NodeChat = dynamic(() => import('./components/NodeChat'), {
|
||||||
|
ssr: false
|
||||||
|
});
|
||||||
|
const NodeKbSearch = dynamic(() => import('./components/NodeKbSearch'), {
|
||||||
|
ssr: false
|
||||||
|
});
|
||||||
|
const NodeHistory = dynamic(() => import('./components/NodeHistory'), {
|
||||||
|
ssr: false
|
||||||
|
});
|
||||||
|
const NodeTFSwitch = dynamic(() => import('./components/NodeTFSwitch'), {
|
||||||
|
ssr: false
|
||||||
|
});
|
||||||
|
const NodeAnswer = dynamic(() => import('./components/NodeAnswer'), {
|
||||||
|
ssr: false
|
||||||
|
});
|
||||||
|
const NodeQuestionInput = dynamic(() => import('./components/NodeQuestionInput'), {
|
||||||
|
ssr: false
|
||||||
|
});
|
||||||
|
const TemplateList = dynamic(() => import('./components/TemplateList'), {
|
||||||
|
ssr: false
|
||||||
|
});
|
||||||
|
|
||||||
|
import 'reactflow/dist/style.css';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
import { AppModuleTemplateItemType } from '@/types/app';
|
||||||
|
|
||||||
|
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
[FlowModuleTypeEnum.questionInputNode]: NodeQuestionInput,
|
||||||
|
[FlowModuleTypeEnum.historyNode]: NodeHistory,
|
||||||
|
[FlowModuleTypeEnum.chatNode]: NodeChat,
|
||||||
|
[FlowModuleTypeEnum.kbSearchNode]: NodeKbSearch,
|
||||||
|
[FlowModuleTypeEnum.tfSwitchNode]: NodeTFSwitch,
|
||||||
|
[FlowModuleTypeEnum.answerNode]: NodeAnswer
|
||||||
|
};
|
||||||
|
const edgeTypes = {
|
||||||
|
buttonedge: ButtonEdge
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppEdit = ({ app, onBack }: { app: AppSchema; onBack: () => void }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||||
|
const {
|
||||||
|
isOpen: isOpenTemplate,
|
||||||
|
onOpen: onOpenTemplate,
|
||||||
|
onClose: onCloseTemplate
|
||||||
|
} = useDisclosure();
|
||||||
|
|
||||||
|
const onChangeNode = useCallback(
|
||||||
|
({ moduleId, key, value, valueKey = 'value' }: FlowModuleItemChangeProps) => {
|
||||||
|
setNodes((nodes) =>
|
||||||
|
nodes.map((node) => {
|
||||||
|
if (node.id !== moduleId) return node;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
inputs: node.data.inputs.map((item) => {
|
||||||
|
if (item.key === key) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
[valueKey]: value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[setNodes]
|
||||||
|
);
|
||||||
|
const onDelNode = useCallback(
|
||||||
|
(nodeId: string) => {
|
||||||
|
setNodes((state) => state.filter((item) => item.id !== nodeId));
|
||||||
|
setEdges((state) => state.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
|
||||||
|
},
|
||||||
|
[setEdges, setNodes]
|
||||||
|
);
|
||||||
|
const onAddNode = useCallback(
|
||||||
|
({ template, position }: { template: AppModuleTemplateItemType; position: XYPosition }) => {
|
||||||
|
setNodes((state) =>
|
||||||
|
state.concat(
|
||||||
|
appModule2FlowNode({
|
||||||
|
item: {
|
||||||
|
...template,
|
||||||
|
position,
|
||||||
|
moduleId: nanoid()
|
||||||
|
},
|
||||||
|
onChangeNode,
|
||||||
|
onDelNode
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[onChangeNode, onDelNode, setNodes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDelConnect = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
setEdges((state) => state.filter((item) => item.id !== id));
|
||||||
|
},
|
||||||
|
[setEdges]
|
||||||
|
);
|
||||||
|
const onConnect = useCallback(
|
||||||
|
({ connect }: { connect: Connection }) => {
|
||||||
|
setEdges((state) =>
|
||||||
|
addEdge(
|
||||||
|
{
|
||||||
|
...connect,
|
||||||
|
type: 'buttonedge',
|
||||||
|
animated: true,
|
||||||
|
data: {
|
||||||
|
onDelete: onDelConnect
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[onDelConnect, setEdges]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutate: onclickSave, isLoading } = useRequest({
|
||||||
|
mutationFn: () => {
|
||||||
|
const modules = nodes.map((item) => ({
|
||||||
|
...item.data,
|
||||||
|
position: item.position,
|
||||||
|
onChangeNode: undefined,
|
||||||
|
onDelNode: undefined,
|
||||||
|
outputs: item.data.outputs.map((output) => ({
|
||||||
|
...output,
|
||||||
|
targets: [] as FlowOutputTargetItemType[]
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
// update inputs and outputs
|
||||||
|
modules.forEach((module) => {
|
||||||
|
module.inputs.forEach((input) => {
|
||||||
|
input.connected = !!edges.find(
|
||||||
|
(edge) => edge.target === module.moduleId && edge.targetHandle === input.key
|
||||||
|
);
|
||||||
|
});
|
||||||
|
module.outputs.forEach((output) => {
|
||||||
|
output.targets = edges
|
||||||
|
.filter(
|
||||||
|
(edge) =>
|
||||||
|
edge.source === module.moduleId &&
|
||||||
|
edge.sourceHandle === output.key &&
|
||||||
|
edge.targetHandle
|
||||||
|
)
|
||||||
|
.map((edge) => ({
|
||||||
|
moduleId: edge.target,
|
||||||
|
key: edge.targetHandle || ''
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return putAppById(app._id, {
|
||||||
|
modules
|
||||||
|
});
|
||||||
|
},
|
||||||
|
successToast: '保存配置成功',
|
||||||
|
errorToast: '保存配置异常'
|
||||||
|
});
|
||||||
|
|
||||||
|
const initData = useCallback(
|
||||||
|
(app: AppSchema) => {
|
||||||
|
console.log('init');
|
||||||
|
|
||||||
|
const edges = appModule2FlowEdge({
|
||||||
|
modules: app.modules,
|
||||||
|
onDelete: onDelConnect
|
||||||
|
});
|
||||||
|
setEdges(edges);
|
||||||
|
|
||||||
|
setNodes(
|
||||||
|
app.modules.map((item) =>
|
||||||
|
appModule2FlowNode({
|
||||||
|
item,
|
||||||
|
onChangeNode,
|
||||||
|
onDelNode
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[onDelConnect, setEdges, setNodes, onChangeNode, onDelNode]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initData(JSON.parse(JSON.stringify(app)));
|
||||||
|
}, [app]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex h={'100%'} flexDirection={'column'} bg={'#fff'}>
|
||||||
|
<Flex py={3} px={5} borderBottom={theme.borders.base} alignItems={'center'}>
|
||||||
|
<IconButton
|
||||||
|
size={'sm'}
|
||||||
|
icon={<MyIcon name={'back'} w={'14px'} />}
|
||||||
|
w={'28px'}
|
||||||
|
h={'28px'}
|
||||||
|
borderRadius={'md'}
|
||||||
|
borderColor={'myGray.300'}
|
||||||
|
variant={'base'}
|
||||||
|
aria-label={''}
|
||||||
|
onClick={onBack}
|
||||||
|
/>
|
||||||
|
<Box ml={5} fontSize={'xl'} flex={1}>
|
||||||
|
{app.name}
|
||||||
|
</Box>
|
||||||
|
<IconButton
|
||||||
|
icon={<MyIcon name={'save'} w={'16px'} />}
|
||||||
|
borderRadius={'lg'}
|
||||||
|
isLoading={isLoading}
|
||||||
|
aria-label={'save'}
|
||||||
|
onClick={onclickSave}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Box
|
||||||
|
flex={'1 0 0'}
|
||||||
|
w={'100%'}
|
||||||
|
h={0}
|
||||||
|
position={'relative'}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
position={'absolute'}
|
||||||
|
top={5}
|
||||||
|
left={5}
|
||||||
|
w={'38px'}
|
||||||
|
h={'38px'}
|
||||||
|
borderRadius={'50%'}
|
||||||
|
icon={<SmallCloseIcon fontSize={'26px'} />}
|
||||||
|
transform={isOpenTemplate ? '' : 'rotate(135deg)'}
|
||||||
|
transition={'0.2s ease'}
|
||||||
|
aria-label={''}
|
||||||
|
zIndex={1}
|
||||||
|
boxShadow={'1px 1px 6px #4e83fd'}
|
||||||
|
onClick={() => (isOpenTemplate ? onCloseTemplate() : onOpenTemplate())}
|
||||||
|
/>
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<ReactFlow
|
||||||
|
className={styles.panel}
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
minZoom={0.4}
|
||||||
|
maxZoom={1.5}
|
||||||
|
fitView
|
||||||
|
defaultEdgeOptions={edgeOptions}
|
||||||
|
connectionLineStyle={connectionLineStyle}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
edgeTypes={edgeTypes}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={(connect) => {
|
||||||
|
connect.sourceHandle &&
|
||||||
|
connect.targetHandle &&
|
||||||
|
onConnect({
|
||||||
|
connect
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Background />
|
||||||
|
<Controls
|
||||||
|
position={'bottom-center'}
|
||||||
|
style={{ display: 'flex' }}
|
||||||
|
showInteractive={false}
|
||||||
|
/>
|
||||||
|
</ReactFlow>
|
||||||
|
</ReactFlowProvider>
|
||||||
|
<TemplateList isOpen={isOpenTemplate} onAddNode={onAddNode} />
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppEdit;
|
||||||
127
client/src/pages/app/detail/index.tsx
Normal file
127
client/src/pages/app/detail/index.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
|
import { useUserStore } from '@/store/user';
|
||||||
|
import { useGlobalStore } from '@/store/global';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import Tabs from '@/components/Tabs';
|
||||||
|
|
||||||
|
import Settings from './components/Settings';
|
||||||
|
import { defaultApp } from '@/constants/model';
|
||||||
|
|
||||||
|
const EditApp = dynamic(() => import('./components/edit'), {
|
||||||
|
ssr: false
|
||||||
|
});
|
||||||
|
const Share = dynamic(() => import('./components/Share'), {
|
||||||
|
ssr: false
|
||||||
|
});
|
||||||
|
const API = dynamic(() => import('./components/API'), {
|
||||||
|
ssr: false
|
||||||
|
});
|
||||||
|
|
||||||
|
enum TabEnum {
|
||||||
|
'settings' = 'settings',
|
||||||
|
'edit' = 'edit',
|
||||||
|
'share' = 'share',
|
||||||
|
'API' = 'API'
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { appId } = router.query as { appId: string };
|
||||||
|
const { isPc } = useGlobalStore();
|
||||||
|
const { appDetail = defaultApp, loadAppDetail, userInfo } = useUserStore();
|
||||||
|
|
||||||
|
const isOwner = useMemo(
|
||||||
|
() => appDetail.userId === userInfo?._id,
|
||||||
|
[appDetail.userId, userInfo?._id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setCurrentTab = useCallback(
|
||||||
|
(tab: `${TabEnum}`) => {
|
||||||
|
router.replace({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: {
|
||||||
|
appId,
|
||||||
|
currentTab: tab
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[appId, router]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.onbeforeunload = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = '内容已修改,确认离开页面吗?';
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.onbeforeunload = null;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadAppDetail(appId);
|
||||||
|
}, [appId, loadAppDetail]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
flexDirection={'column'}
|
||||||
|
h={'100%'}
|
||||||
|
maxW={'100vw'}
|
||||||
|
pt={4}
|
||||||
|
overflow={'overlay'}
|
||||||
|
position={'relative'}
|
||||||
|
bg={'white'}
|
||||||
|
>
|
||||||
|
{/* 头部 */}
|
||||||
|
<Box textAlign={['center', 'left']} px={5} mb={4}>
|
||||||
|
<Box className="textlg" display={['block', 'none']} fontSize={'3xl'} fontWeight={'bold'}>
|
||||||
|
{appDetail.name}
|
||||||
|
</Box>
|
||||||
|
<Tabs
|
||||||
|
mx={['auto', '0']}
|
||||||
|
mt={2}
|
||||||
|
w={['300px', '360px']}
|
||||||
|
list={[
|
||||||
|
{ label: '配置', id: TabEnum.settings },
|
||||||
|
...(isOwner ? [{ label: '编排', id: TabEnum.edit }] : []),
|
||||||
|
{ 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=${appId}`);
|
||||||
|
} else {
|
||||||
|
setCurrentTab(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box flex={1}>
|
||||||
|
{currentTab === TabEnum.settings && <Settings modelId={appId} />}
|
||||||
|
{currentTab === TabEnum.edit && (
|
||||||
|
<Box position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
|
||||||
|
<EditApp app={appDetail} onBack={() => setCurrentTab(TabEnum.settings)} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{currentTab === TabEnum.API && <API modelId={appId} />}
|
||||||
|
{currentTab === TabEnum.share && <Share modelId={appId} />}
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getServerSideProps(context: any) {
|
||||||
|
const currentTab = context?.query?.currentTab || TabEnum.settings;
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: { currentTab }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppDetail;
|
||||||
26
client/src/pages/app/list/index.tsx
Normal file
26
client/src/pages/app/list/index.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, useTheme } from '@chakra-ui/react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
|
const MyApps = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const router = useRouter();
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box
|
||||||
|
className="textlg"
|
||||||
|
borderBottom={theme.borders.base}
|
||||||
|
letterSpacing={1}
|
||||||
|
py={3}
|
||||||
|
px={5}
|
||||||
|
fontSize={'24px'}
|
||||||
|
fontWeight={'bold'}
|
||||||
|
onClick={() => router.push(`/app/detail?appId=642adec15f01d67d4613efdb`)}
|
||||||
|
>
|
||||||
|
我的应用
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MyApps;
|
||||||
@ -54,11 +54,8 @@ const PcSliderBar = ({
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { history, loadHistory } = useChatStore();
|
const { history, loadHistory } = useChatStore();
|
||||||
const { myModels, myCollectionModels, loadMyModels } = useUserStore();
|
const { myApps, myCollectionApps, loadMyModels } = useUserStore();
|
||||||
const models = useMemo(
|
const models = useMemo(() => [...myApps, ...myCollectionApps], [myCollectionApps, myApps]);
|
||||||
() => [...myModels, ...myCollectionModels],
|
|
||||||
[myCollectionModels, myModels]
|
|
||||||
);
|
|
||||||
|
|
||||||
// custom title edit
|
// custom title edit
|
||||||
const { onOpenModal, EditModal: EditTitleModal } = useEditInfo({
|
const { onOpenModal, EditModal: EditTitleModal } = useEditInfo({
|
||||||
|
|||||||
@ -35,13 +35,10 @@ const PhoneSliderBar = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [currentTab, setCurrentTab] = useState(TabEnum.app);
|
const [currentTab, setCurrentTab] = useState(TabEnum.app);
|
||||||
const { myModels, myCollectionModels, loadMyModels } = useUserStore();
|
const { myApps, myCollectionApps, loadMyModels } = useUserStore();
|
||||||
const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure();
|
const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure();
|
||||||
|
|
||||||
const models = useMemo(
|
const models = useMemo(() => [...myApps, ...myCollectionApps], [myCollectionApps, myApps]);
|
||||||
() => [...myModels, ...myCollectionModels],
|
|
||||||
[myCollectionModels, myModels]
|
|
||||||
);
|
|
||||||
useQuery(['loadModels'], () => loadMyModels(false));
|
useQuery(['loadModels'], () => loadMyModels(false));
|
||||||
|
|
||||||
const { history, loadHistory } = useChatStore();
|
const { history, loadHistory } = useChatStore();
|
||||||
|
|||||||
@ -175,7 +175,7 @@ const Chat = () => {
|
|||||||
const messages = adaptChatItem_openAI({ messages: prompts, reserveId: true });
|
const messages = adaptChatItem_openAI({ messages: prompts, reserveId: true });
|
||||||
|
|
||||||
// 流请求,获取数据
|
// 流请求,获取数据
|
||||||
const { newChatId, quoteLen, errMsg } = await streamFetch({
|
const { newChatId, errMsg } = await streamFetch({
|
||||||
data: {
|
data: {
|
||||||
messages,
|
messages,
|
||||||
chatId,
|
chatId,
|
||||||
@ -203,6 +203,7 @@ const Chat = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// save chat
|
||||||
if (newChatId) {
|
if (newChatId) {
|
||||||
setForbidLoadChatData(true);
|
setForbidLoadChatData(true);
|
||||||
router.replace(`/chat?modelId=${modelId}&chatId=${newChatId}`);
|
router.replace(`/chat?modelId=${modelId}&chatId=${newChatId}`);
|
||||||
@ -219,7 +220,7 @@ const Chat = () => {
|
|||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
status: 'finish',
|
status: 'finish',
|
||||||
quoteLen,
|
quoteLen: 0,
|
||||||
systemPrompt: `${chatData.systemPrompt}${`${
|
systemPrompt: `${chatData.systemPrompt}${`${
|
||||||
chatData.limitPrompt ? `\n\n${chatData.limitPrompt}` : ''
|
chatData.limitPrompt ? `\n\n${chatData.limitPrompt}` : ''
|
||||||
}`}`
|
}`}`
|
||||||
|
|||||||
@ -270,8 +270,8 @@ const SelectFileModal = ({
|
|||||||
min={300}
|
min={300}
|
||||||
max={1000}
|
max={1000}
|
||||||
step={50}
|
step={50}
|
||||||
activeVal={modeMap[TrainingModeEnum.index].maxLen}
|
value={modeMap[TrainingModeEnum.index].maxLen}
|
||||||
setVal={(val) => {
|
onChange={(val) => {
|
||||||
setModeMap((state) => ({
|
setModeMap((state) => ({
|
||||||
...state,
|
...state,
|
||||||
[TrainingModeEnum.index]: {
|
[TrainingModeEnum.index]: {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { useSendCode } from '@/hooks/useSendCode';
|
|||||||
import type { ResLogin } from '@/api/response/user';
|
import type { ResLogin } from '@/api/response/user';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { postCreateModel } from '@/api/model';
|
import { postCreateModel } from '@/api/app';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
loginSuccess: (e: ResLogin) => void;
|
loginSuccess: (e: ResLogin) => void;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Box, Flex, Input, IconButton, Tooltip, useTheme } from '@chakra-ui/reac
|
|||||||
import { AddIcon } from '@chakra-ui/icons';
|
import { AddIcon } from '@chakra-ui/icons';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import MyIcon from '@/components/Icon';
|
import MyIcon from '@/components/Icon';
|
||||||
import { postCreateModel } from '@/api/model';
|
import { postCreateModel } from '@/api/app';
|
||||||
import { useLoading } from '@/hooks/useLoading';
|
import { useLoading } from '@/hooks/useLoading';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
@ -25,7 +25,7 @@ const ModelList = ({ modelId }: { modelId: string }) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { Loading, setIsLoading } = useLoading();
|
const { Loading, setIsLoading } = useLoading();
|
||||||
const { myModels, myCollectionModels, loadMyModels, refreshModel } = useUserStore();
|
const { myApps, myCollectionApps, loadMyModels, refreshModel } = useUserStore();
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
|
|
||||||
/* 加载模型 */
|
/* 加载模型 */
|
||||||
@ -35,7 +35,7 @@ const ModelList = ({ modelId }: { modelId: string }) => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const id = await postCreateModel({
|
const id = await postCreateModel({
|
||||||
name: `AI应用${myModels.length + 1}`
|
name: `AI应用${myApps.length + 1}`
|
||||||
});
|
});
|
||||||
toast({
|
toast({
|
||||||
title: '创建成功',
|
title: '创建成功',
|
||||||
@ -50,23 +50,23 @@ const ModelList = ({ modelId }: { modelId: string }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}, [myModels.length, refreshModel, router, setIsLoading, toast]);
|
}, [myApps.length, refreshModel, router, setIsLoading, toast]);
|
||||||
|
|
||||||
const currentModels = useMemo(() => {
|
const currentModels = useMemo(() => {
|
||||||
const map = {
|
const map = {
|
||||||
[MyModelsTypeEnum.my]: {
|
[MyModelsTypeEnum.my]: {
|
||||||
list: myModels.filter((item) => new RegExp(searchText, 'ig').test(item.name + item.intro)),
|
list: myApps.filter((item) => new RegExp(searchText, 'ig').test(item.name + item.intro)),
|
||||||
emptyText: '还没有 AI 应用~\n快来创建一个吧'
|
emptyText: '还没有 AI 应用~\n快来创建一个吧'
|
||||||
},
|
},
|
||||||
[MyModelsTypeEnum.collection]: {
|
[MyModelsTypeEnum.collection]: {
|
||||||
list: myCollectionModels.filter((item) =>
|
list: myCollectionApps.filter((item) =>
|
||||||
new RegExp(searchText, 'ig').test(item.name + item.intro)
|
new RegExp(searchText, 'ig').test(item.name + item.intro)
|
||||||
),
|
),
|
||||||
emptyText: '收藏的 AI 应用为空~\n快去市场找一个吧'
|
emptyText: '收藏的 AI 应用为空~\n快去市场找一个吧'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return map[currentTab];
|
return map[currentTab];
|
||||||
}, [currentTab, myCollectionModels, myModels, searchText]);
|
}, [currentTab, myCollectionApps, myApps, searchText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import { useUserStore } from '@/store/user';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import Avatar from '@/components/Avatar';
|
import Avatar from '@/components/Avatar';
|
||||||
import { AddIcon, DeleteIcon, QuestionOutlineIcon } from '@chakra-ui/icons';
|
import { AddIcon, DeleteIcon, QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||||
import { putModelById } from '@/api/model';
|
import { putAppById } from '@/api/app';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { useLoading } from '@/hooks/useLoading';
|
import { useLoading } from '@/hooks/useLoading';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
@ -34,15 +34,15 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { modelDetail, loadKbList, loadModelDetail } = useUserStore();
|
const { appDetail, loadKbList, loadAppDetail } = useUserStore();
|
||||||
const { Loading, setIsLoading } = useLoading();
|
const { Loading, setIsLoading } = useLoading();
|
||||||
const [selectedIdList, setSelectedIdList] = useState<string[]>([]);
|
const [selectedIdList, setSelectedIdList] = useState<string[]>([]);
|
||||||
const [refresh, setRefresh] = useState(false);
|
const [refresh, setRefresh] = useState(false);
|
||||||
const { register, reset, getValues, setValue } = useForm({
|
const { register, reset, getValues, setValue } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
searchSimilarity: modelDetail.chat.searchSimilarity,
|
searchSimilarity: appDetail.chat.searchSimilarity,
|
||||||
searchLimit: modelDetail.chat.searchLimit,
|
searchLimit: appDetail.chat.searchLimit,
|
||||||
searchEmptyText: modelDetail.chat.searchEmptyText
|
searchEmptyText: appDetail.chat.searchEmptyText
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -68,13 +68,13 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
|||||||
) => {
|
) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await putModelById(modelId, {
|
await putAppById(modelId, {
|
||||||
chat: {
|
chat: {
|
||||||
...modelDetail.chat,
|
...appDetail.chat,
|
||||||
...data
|
...data
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
loadModelDetail(modelId, true);
|
loadAppDetail(modelId, true);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast({
|
toast({
|
||||||
title: err?.message || '更新失败',
|
title: err?.message || '更新失败',
|
||||||
@ -83,7 +83,7 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
|||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
},
|
},
|
||||||
[setIsLoading, modelId, modelDetail.chat, loadModelDetail, toast]
|
[setIsLoading, modelId, appDetail.chat, loadAppDetail, toast]
|
||||||
);
|
);
|
||||||
|
|
||||||
// init kb select list
|
// init kb select list
|
||||||
@ -91,10 +91,10 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box position={'relative'} px={5} minH={'50vh'}>
|
<Box position={'relative'} px={5} minH={'50vh'}>
|
||||||
<Box fontWeight={'bold'}>关联的知识库({modelDetail.chat?.relatedKbs.length})</Box>
|
<Box fontWeight={'bold'}>关联的知识库({appDetail.chat?.relatedKbs.length})</Box>
|
||||||
{(() => {
|
{(() => {
|
||||||
const kbs =
|
const kbs =
|
||||||
modelDetail.chat?.relatedKbs
|
appDetail.chat?.relatedKbs
|
||||||
?.map((id) => kbList.find((kb) => kb._id === id))
|
?.map((id) => kbList.find((kb) => kb._id === id))
|
||||||
.filter((item) => item) || [];
|
.filter((item) => item) || [];
|
||||||
return (
|
return (
|
||||||
@ -120,9 +120,9 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
|||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
reset({
|
reset({
|
||||||
searchSimilarity: modelDetail.chat.searchSimilarity,
|
searchSimilarity: appDetail.chat.searchSimilarity,
|
||||||
searchLimit: modelDetail.chat.searchLimit,
|
searchLimit: appDetail.chat.searchLimit,
|
||||||
searchEmptyText: modelDetail.chat.searchEmptyText
|
searchEmptyText: appDetail.chat.searchEmptyText
|
||||||
});
|
});
|
||||||
onOpenEditParams();
|
onOpenEditParams();
|
||||||
}}
|
}}
|
||||||
@ -139,9 +139,9 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
|||||||
调整搜索参数
|
调整搜索参数
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex mt={3} h={'30px'} color={'myGray.600'} fontSize={'sm'}>
|
<Flex mt={3} h={'30px'} color={'myGray.600'} fontSize={'sm'}>
|
||||||
相似度: {modelDetail.chat.searchSimilarity}, 单次搜索数量:{' '}
|
相似度: {appDetail.chat.searchSimilarity}, 单次搜索数量:{' '}
|
||||||
{modelDetail.chat.searchLimit}, 空搜索时拒绝回复:{' '}
|
{appDetail.chat.searchLimit}, 空搜索时拒绝回复:{' '}
|
||||||
{modelDetail.chat.searchEmptyText !== '' ? 'true' : 'false'}
|
{appDetail.chat.searchEmptyText !== '' ? 'true' : 'false'}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
<Card
|
<Card
|
||||||
@ -156,7 +156,7 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
|||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedIdList(
|
setSelectedIdList(
|
||||||
modelDetail.chat?.relatedKbs ? [...modelDetail.chat?.relatedKbs] : []
|
appDetail.chat?.relatedKbs ? [...appDetail.chat?.relatedKbs] : []
|
||||||
);
|
);
|
||||||
onOpenKbSelect();
|
onOpenKbSelect();
|
||||||
}}
|
}}
|
||||||
@ -219,8 +219,8 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
|||||||
size={'sm'}
|
size={'sm'}
|
||||||
_hover={{ color: 'red.600' }}
|
_hover={{ color: 'red.600' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const ids = modelDetail.chat?.relatedKbs
|
const ids = appDetail.chat?.relatedKbs
|
||||||
? [...modelDetail.chat.relatedKbs]
|
? [...appDetail.chat.relatedKbs]
|
||||||
: [];
|
: [];
|
||||||
const i = ids.findIndex((id) => id === item._id);
|
const i = ids.findIndex((id) => id === item._id);
|
||||||
ids.splice(i, 1);
|
ids.splice(i, 1);
|
||||||
@ -262,7 +262,7 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
|||||||
boxShadow={'sm'}
|
boxShadow={'sm'}
|
||||||
h={'80px'}
|
h={'80px'}
|
||||||
cursor={'pointer'}
|
cursor={'pointer'}
|
||||||
order={modelDetail.chat?.relatedKbs?.includes(item._id) ? 0 : 1}
|
order={appDetail.chat?.relatedKbs?.includes(item._id) ? 0 : 1}
|
||||||
_hover={{
|
_hover={{
|
||||||
boxShadow: 'md'
|
boxShadow: 'md'
|
||||||
}}
|
}}
|
||||||
@ -328,8 +328,8 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
|||||||
min={0}
|
min={0}
|
||||||
max={1}
|
max={1}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
activeVal={getValues('searchSimilarity')}
|
value={getValues('searchSimilarity')}
|
||||||
setVal={(val) => {
|
onChange={(val) => {
|
||||||
setValue('searchSimilarity', val);
|
setValue('searchSimilarity', val);
|
||||||
setRefresh(!refresh);
|
setRefresh(!refresh);
|
||||||
}}
|
}}
|
||||||
@ -345,8 +345,8 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
|||||||
]}
|
]}
|
||||||
min={1}
|
min={1}
|
||||||
max={20}
|
max={20}
|
||||||
activeVal={getValues('searchLimit')}
|
value={getValues('searchLimit')}
|
||||||
setVal={(val) => {
|
onChange={(val) => {
|
||||||
setValue('searchLimit', val);
|
setValue('searchLimit', val);
|
||||||
setRefresh(!refresh);
|
setRefresh(!refresh);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import { useRouter } from 'next/router';
|
|||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { useLoading } from '@/hooks/useLoading';
|
import { useLoading } from '@/hooks/useLoading';
|
||||||
import { delModelById, putModelById } from '@/api/model';
|
import { delModelById, putAppById } from '@/api/app';
|
||||||
import { useSelectFile } from '@/hooks/useSelectFile';
|
import { useSelectFile } from '@/hooks/useSelectFile';
|
||||||
import { compressImg } from '@/utils/file';
|
import { compressImg } from '@/utils/file';
|
||||||
import { getErrText } from '@/utils/tools';
|
import { getErrText } from '@/utils/tools';
|
||||||
@ -24,7 +24,7 @@ import { useConfirm } from '@/hooks/useConfirm';
|
|||||||
import { ChatModelMap, chatModelList } from '@/constants/model';
|
import { ChatModelMap, chatModelList } from '@/constants/model';
|
||||||
import { formatPrice } from '@/utils/user';
|
import { formatPrice } from '@/utils/user';
|
||||||
|
|
||||||
import type { ModelSchema } from '@/types/mongoSchema';
|
import type { AppSchema } from '@/types/mongoSchema';
|
||||||
|
|
||||||
import Avatar from '@/components/Avatar';
|
import Avatar from '@/components/Avatar';
|
||||||
import MySelect from '@/components/Select';
|
import MySelect from '@/components/Select';
|
||||||
@ -39,7 +39,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { Loading, setIsLoading } = useLoading();
|
const { Loading, setIsLoading } = useLoading();
|
||||||
const { userInfo, modelDetail, myModels, loadModelDetail, refreshModel, setLastModelId } =
|
const { userInfo, appDetail, myApps, loadAppDetail, refreshModel, setLastModelId } =
|
||||||
useUserStore();
|
useUserStore();
|
||||||
const { File, onOpen: onOpenSelectFile } = useSelectFile({
|
const { File, onOpen: onOpenSelectFile } = useSelectFile({
|
||||||
fileType: '.jpg,.png',
|
fileType: '.jpg,.png',
|
||||||
@ -60,12 +60,12 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
|||||||
reset,
|
reset,
|
||||||
handleSubmit
|
handleSubmit
|
||||||
} = useForm({
|
} = useForm({
|
||||||
defaultValues: modelDetail
|
defaultValues: appDetail
|
||||||
});
|
});
|
||||||
|
|
||||||
const isOwner = useMemo(
|
const isOwner = useMemo(
|
||||||
() => modelDetail.userId === userInfo?._id,
|
() => appDetail.userId === userInfo?._id,
|
||||||
[modelDetail.userId, userInfo?._id]
|
[appDetail.userId, userInfo?._id]
|
||||||
);
|
);
|
||||||
const tokenLimit = useMemo(() => {
|
const tokenLimit = useMemo(() => {
|
||||||
const max = ChatModelMap[getValues('chat.chatModel')]?.contextMaxToken || 4000;
|
const max = ChatModelMap[getValues('chat.chatModel')]?.contextMaxToken || 4000;
|
||||||
@ -79,10 +79,10 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
|||||||
|
|
||||||
// 提交保存模型修改
|
// 提交保存模型修改
|
||||||
const saveSubmitSuccess = useCallback(
|
const saveSubmitSuccess = useCallback(
|
||||||
async (data: ModelSchema) => {
|
async (data: AppSchema) => {
|
||||||
setBtnLoading(true);
|
setBtnLoading(true);
|
||||||
try {
|
try {
|
||||||
await putModelById(data._id, {
|
await putAppById(data._id, {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
avatar: data.avatar,
|
avatar: data.avatar,
|
||||||
intro: data.intro,
|
intro: data.intro,
|
||||||
@ -126,16 +126,16 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
|||||||
|
|
||||||
/* 点击删除 */
|
/* 点击删除 */
|
||||||
const handleDelModel = useCallback(async () => {
|
const handleDelModel = useCallback(async () => {
|
||||||
if (!modelDetail) return;
|
if (!appDetail) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await delModelById(modelDetail._id);
|
await delModelById(appDetail._id);
|
||||||
toast({
|
toast({
|
||||||
title: '删除成功',
|
title: '删除成功',
|
||||||
status: 'success'
|
status: 'success'
|
||||||
});
|
});
|
||||||
refreshModel.removeModelDetail(modelDetail._id);
|
refreshModel.removeModelDetail(appDetail._id);
|
||||||
router.replace(`/model?modelId=${myModels[1]?._id}`);
|
router.replace(`/model?modelId=${myApps[1]?._id}`);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast({
|
toast({
|
||||||
title: err?.message || '删除失败',
|
title: err?.message || '删除失败',
|
||||||
@ -143,7 +143,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}, [modelDetail, setIsLoading, toast, refreshModel, router, myModels]);
|
}, [appDetail, setIsLoading, toast, refreshModel, router, myApps]);
|
||||||
|
|
||||||
const onSelectFile = useCallback(
|
const onSelectFile = useCallback(
|
||||||
async (e: File[]) => {
|
async (e: File[]) => {
|
||||||
@ -168,7 +168,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// load model data
|
// load model data
|
||||||
const { isLoading } = useQuery([modelId], () => loadModelDetail(modelId, true), {
|
const { isLoading } = useQuery([modelId], () => loadAppDetail(modelId, true), {
|
||||||
onSuccess(res) {
|
onSuccess(res) {
|
||||||
res && reset(res);
|
res && reset(res);
|
||||||
modelId && setLastModelId(modelId);
|
modelId && setLastModelId(modelId);
|
||||||
@ -241,7 +241,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
|||||||
width={['100%', '300px']}
|
width={['100%', '300px']}
|
||||||
value={getValues('chat.chatModel')}
|
value={getValues('chat.chatModel')}
|
||||||
list={chatModelList.map((item) => ({
|
list={chatModelList.map((item) => ({
|
||||||
id: item.chatModel,
|
value: item.chatModel,
|
||||||
label: `${item.name} (${formatPrice(
|
label: `${item.name} (${formatPrice(
|
||||||
ChatModelMap[item.chatModel]?.price,
|
ChatModelMap[item.chatModel]?.price,
|
||||||
1000
|
1000
|
||||||
@ -266,8 +266,8 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
|||||||
width={['95%', '280px']}
|
width={['95%', '280px']}
|
||||||
min={0}
|
min={0}
|
||||||
max={10}
|
max={10}
|
||||||
activeVal={getValues('chat.temperature')}
|
value={getValues('chat.temperature')}
|
||||||
setVal={(val) => {
|
onChange={(val) => {
|
||||||
setValue('chat.temperature', val);
|
setValue('chat.temperature', val);
|
||||||
setRefresh(!refresh);
|
setRefresh(!refresh);
|
||||||
}}
|
}}
|
||||||
@ -288,8 +288,8 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
|||||||
min={100}
|
min={100}
|
||||||
max={tokenLimit}
|
max={tokenLimit}
|
||||||
step={50}
|
step={50}
|
||||||
activeVal={getValues('chat.maxToken')}
|
value={getValues('chat.maxToken')}
|
||||||
setVal={(val) => {
|
onChange={(val) => {
|
||||||
setValue('chat.maxToken', val);
|
setValue('chat.maxToken', val);
|
||||||
setRefresh(!refresh);
|
setRefresh(!refresh);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import dynamic from 'next/dynamic';
|
|||||||
import Tabs from '@/components/Tabs';
|
import Tabs from '@/components/Tabs';
|
||||||
|
|
||||||
import Settings from './components/Settings';
|
import Settings from './components/Settings';
|
||||||
import { defaultModel } from '@/constants/model';
|
import { defaultApp } from '@/constants/model';
|
||||||
|
|
||||||
const Kb = dynamic(() => import('./components/Kb'), {
|
const Kb = dynamic(() => import('./components/Kb'), {
|
||||||
ssr: false
|
ssr: false
|
||||||
@ -29,12 +29,12 @@ enum TabEnum {
|
|||||||
const ModelDetail = ({ modelId }: { modelId: string }) => {
|
const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isPc } = useGlobalStore();
|
const { isPc } = useGlobalStore();
|
||||||
const { modelDetail = defaultModel, userInfo } = useUserStore();
|
const { appDetail = defaultApp, userInfo } = useUserStore();
|
||||||
const [currentTab, setCurrentTab] = useState<`${TabEnum}`>(TabEnum.settings);
|
const [currentTab, setCurrentTab] = useState<`${TabEnum}`>(TabEnum.settings);
|
||||||
|
|
||||||
const isOwner = useMemo(
|
const isOwner = useMemo(
|
||||||
() => modelDetail.userId === userInfo?._id,
|
() => appDetail.userId === userInfo?._id,
|
||||||
[modelDetail.userId, userInfo?._id]
|
[appDetail.userId, userInfo?._id]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -65,7 +65,7 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
|
|||||||
{/* 头部 */}
|
{/* 头部 */}
|
||||||
<Box textAlign={['center', 'left']} px={5} mb={4}>
|
<Box textAlign={['center', 'left']} px={5} mb={4}>
|
||||||
<Box className="textlg" display={['block', 'none']} fontSize={'3xl'} fontWeight={'bold'}>
|
<Box className="textlg" display={['block', 'none']} fontSize={'3xl'} fontWeight={'bold'}>
|
||||||
{modelDetail.name}
|
{appDetail.name}
|
||||||
</Box>
|
</Box>
|
||||||
<Tabs
|
<Tabs
|
||||||
mx={['auto', '0']}
|
mx={['auto', '0']}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useRef, useCallback } from 'react';
|
import React, { useState, useRef, useCallback } from 'react';
|
||||||
import { Box, Flex, Card, Grid, Input } from '@chakra-ui/react';
|
import { Box, Flex, Card, Grid, Input } from '@chakra-ui/react';
|
||||||
import { useLoading } from '@/hooks/useLoading';
|
import { useLoading } from '@/hooks/useLoading';
|
||||||
import { getShareModelList, triggerModelCollection } from '@/api/model';
|
import { getShareModelList, triggerModelCollection } from '@/api/app';
|
||||||
import { usePagination } from '@/hooks/usePagination';
|
import { usePagination } from '@/hooks/usePagination';
|
||||||
import type { ShareModelItem } from '@/types/model';
|
import type { ShareModelItem } from '@/types/model';
|
||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
|
|||||||
@ -12,6 +12,7 @@ interface Props {
|
|||||||
export const moduleFetch = ({ url, data, res }: Props) =>
|
export const moduleFetch = ({ url, data, res }: Props) =>
|
||||||
new Promise<Record<string, any>>(async (resolve, reject) => {
|
new Promise<Record<string, any>>(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
|
const abortSignal = new AbortController();
|
||||||
const baseUrl = `http://localhost:3000/api`;
|
const baseUrl = `http://localhost:3000/api`;
|
||||||
const requestUrl = url.startsWith('/') ? `${baseUrl}${url}` : url;
|
const requestUrl = url.startsWith('/') ? `${baseUrl}${url}` : url;
|
||||||
const response = await fetch(requestUrl, {
|
const response = await fetch(requestUrl, {
|
||||||
@ -19,9 +20,15 @@ export const moduleFetch = ({ url, data, res }: Props) =>
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data),
|
||||||
|
signal: abortSignal.signal
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response.status >= 300 || response.status < 200) {
|
||||||
|
const err = await response.json();
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
if (!response?.body) {
|
if (!response?.body) {
|
||||||
throw new Error('Request Error');
|
throw new Error('Request Error');
|
||||||
}
|
}
|
||||||
@ -34,14 +41,19 @@ export const moduleFetch = ({ url, data, res }: Props) =>
|
|||||||
|
|
||||||
const reader = response.body?.getReader();
|
const reader = response.body?.getReader();
|
||||||
|
|
||||||
let chatResponse = {};
|
let chatResponse: Record<string, any> = {};
|
||||||
|
|
||||||
const read = async () => {
|
const read = async () => {
|
||||||
try {
|
try {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
if (done) {
|
if (done) {
|
||||||
return resolve(chatResponse);
|
return resolve(chatResponse);
|
||||||
|
} else if (res.closed) {
|
||||||
|
resolve(chatResponse);
|
||||||
|
abortSignal.abort();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunkResponse = parseStreamChunk(value);
|
const chunkResponse = parseStreamChunk(value);
|
||||||
|
|
||||||
chunkResponse.forEach((item) => {
|
chunkResponse.forEach((item) => {
|
||||||
@ -53,12 +65,25 @@ export const moduleFetch = ({ url, data, res }: Props) =>
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
if (item.event === sseResponseEventEnum.moduleFetchResponse) {
|
if (!res.closed && item.event === sseResponseEventEnum.moduleFetchResponse) {
|
||||||
chatResponse = {
|
chatResponse = {
|
||||||
...chatResponse,
|
...chatResponse,
|
||||||
...data
|
...data
|
||||||
};
|
};
|
||||||
} else if (item.event === sseResponseEventEnum.answer && data?.choices?.[0]?.delta) {
|
} else if (
|
||||||
|
!res.closed &&
|
||||||
|
item.event === sseResponseEventEnum.answer &&
|
||||||
|
data?.choices?.[0]?.delta
|
||||||
|
) {
|
||||||
|
// save answer
|
||||||
|
const answer: string = data?.choices?.[0].delta.content || '';
|
||||||
|
if (answer) {
|
||||||
|
chatResponse = {
|
||||||
|
...chatResponse,
|
||||||
|
answer: chatResponse.answer ? chatResponse.answer + answer : answer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
sseResponse({
|
sseResponse({
|
||||||
res,
|
res,
|
||||||
event: sseResponseEventEnum.answer,
|
event: sseResponseEventEnum.answer,
|
||||||
@ -68,6 +93,9 @@ export const moduleFetch = ({ url, data, res }: Props) =>
|
|||||||
});
|
});
|
||||||
read();
|
read();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
if (err?.message === 'The operation was aborted.') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
reject(getErrText(err, '请求异常'));
|
reject(getErrText(err, '请求异常'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { Schema, model, models, Model as MongoModel } from 'mongoose';
|
import { Schema, model, models, Model as MongoModel } from 'mongoose';
|
||||||
import { ModelSchema as ModelType } from '@/types/mongoSchema';
|
import { AppSchema as ModelType } from '@/types/mongoSchema';
|
||||||
import { ChatModelMap, OpenAiChatEnum } from '@/constants/model';
|
import { ChatModelMap, OpenAiChatEnum } from '@/constants/model';
|
||||||
|
|
||||||
const ModelSchema = new Schema({
|
const AppSchema = new Schema({
|
||||||
userId: {
|
userId: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'user',
|
ref: 'user',
|
||||||
@ -91,14 +91,18 @@ const ModelSchema = new Schema({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
modules: {
|
||||||
|
type: Array,
|
||||||
|
default: []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ModelSchema.index({ updateTime: -1 });
|
AppSchema.index({ updateTime: -1 });
|
||||||
ModelSchema.index({ 'share.collection': -1 });
|
AppSchema.index({ 'share.collection': -1 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Model: MongoModel<ModelType> = models['model'] || model('model', ModelSchema);
|
export const Model: MongoModel<ModelType> = models['model'] || model('model', AppSchema);
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export const jsonRes = <T = any>(
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.status(code).json({
|
||||||
code,
|
code,
|
||||||
statusText: '',
|
statusText: '',
|
||||||
message: msg,
|
message: msg,
|
||||||
@ -92,7 +92,7 @@ export const sseErrRes = (res: NextApiResponse, error: any) => {
|
|||||||
} else if (openaiError[error?.response?.statusText]) {
|
} else if (openaiError[error?.response?.statusText]) {
|
||||||
msg = openaiError[error.response.statusText];
|
msg = openaiError[error.response.statusText];
|
||||||
}
|
}
|
||||||
console.log(error);
|
console.log('sse error', error);
|
||||||
|
|
||||||
sseResponse({
|
sseResponse({
|
||||||
res,
|
res,
|
||||||
|
|||||||
@ -2,10 +2,10 @@ import type { NextApiRequest } from 'next';
|
|||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import Cookie from 'cookie';
|
import Cookie from 'cookie';
|
||||||
import { Chat, Model, OpenApi, User, ShareChat, KB } from '../mongo';
|
import { Chat, Model, OpenApi, User, ShareChat, KB } from '../mongo';
|
||||||
import type { ModelSchema } from '@/types/mongoSchema';
|
import type { AppSchema } from '@/types/mongoSchema';
|
||||||
import type { ChatItemType } from '@/types/chat';
|
import type { ChatItemType } from '@/types/chat';
|
||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { defaultModel } from '@/constants/model';
|
import { defaultApp } from '@/constants/model';
|
||||||
import { formatPrice } from '@/utils/user';
|
import { formatPrice } from '@/utils/user';
|
||||||
import { ERROR_ENUM } from '../errorCode';
|
import { ERROR_ENUM } from '../errorCode';
|
||||||
import { ChatModelType, OpenAiChatEnum } from '@/constants/model';
|
import { ChatModelType, OpenAiChatEnum } from '@/constants/model';
|
||||||
@ -204,22 +204,22 @@ export const getApiKey = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 模型使用权校验
|
// 模型使用权校验
|
||||||
export const authModel = async ({
|
export const authApp = async ({
|
||||||
modelId,
|
appId,
|
||||||
userId,
|
userId,
|
||||||
authUser = true,
|
authUser = true,
|
||||||
authOwner = true,
|
authOwner = true,
|
||||||
reserveDetail = false
|
reserveDetail = false
|
||||||
}: {
|
}: {
|
||||||
modelId: string;
|
appId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
authUser?: boolean;
|
authUser?: boolean;
|
||||||
authOwner?: boolean;
|
authOwner?: boolean;
|
||||||
reserveDetail?: boolean; // focus reserve detail
|
reserveDetail?: boolean; // focus reserve detail
|
||||||
}) => {
|
}) => {
|
||||||
// 获取 model 数据
|
// 获取 model 数据
|
||||||
const model = await Model.findById<ModelSchema>(modelId);
|
const app = await Model.findById<AppSchema>(appId);
|
||||||
if (!model) {
|
if (!app) {
|
||||||
return Promise.reject('模型不存在');
|
return Promise.reject('模型不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,21 +228,21 @@ export const authModel = async ({
|
|||||||
1. authOwner=true or authUser = true , just owner can use
|
1. authOwner=true or authUser = true , just owner can use
|
||||||
2. authUser = false and share, anyone can use
|
2. authUser = false and share, anyone can use
|
||||||
*/
|
*/
|
||||||
if (authOwner || (authUser && !model.share.isShare)) {
|
if (authOwner || (authUser && !app.share.isShare)) {
|
||||||
if (userId !== String(model.userId)) return Promise.reject(ERROR_ENUM.unAuthModel);
|
if (userId !== String(app.userId)) return Promise.reject(ERROR_ENUM.unAuthModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// do not share detail info
|
// do not share detail info
|
||||||
if (!reserveDetail && !model.share.isShareDetail && userId !== String(model.userId)) {
|
if (!reserveDetail && !app.share.isShareDetail && userId !== String(app.userId)) {
|
||||||
model.chat = {
|
app.chat = {
|
||||||
...defaultModel.chat,
|
...defaultApp.chat,
|
||||||
chatModel: model.chat.chatModel
|
chatModel: app.chat.chatModel
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
model,
|
app,
|
||||||
showModelDetail: userId === String(model.userId)
|
showModelDetail: userId === String(app.userId)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -270,9 +270,9 @@ export const authChat = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { userId } = await authUser({ req, authToken: true });
|
const { userId } = await authUser({ req, authToken: true });
|
||||||
|
|
||||||
// 获取 model 数据
|
// 获取 app 数据
|
||||||
const { model, showModelDetail } = await authModel({
|
const { app, showModelDetail } = await authApp({
|
||||||
modelId,
|
appId: modelId,
|
||||||
userId,
|
userId,
|
||||||
authOwner: false,
|
authOwner: false,
|
||||||
reserveDetail: true
|
reserveDetail: true
|
||||||
@ -304,7 +304,7 @@ export const authChat = async ({
|
|||||||
}
|
}
|
||||||
// 获取 user 的 apiKey
|
// 获取 user 的 apiKey
|
||||||
const { userOpenAiKey, systemAuthKey } = await getApiKey({
|
const { userOpenAiKey, systemAuthKey } = await getApiKey({
|
||||||
model: model.chat.chatModel,
|
model: app.chat.chatModel,
|
||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -313,7 +313,7 @@ export const authChat = async ({
|
|||||||
systemAuthKey,
|
systemAuthKey,
|
||||||
content,
|
content,
|
||||||
userId,
|
userId,
|
||||||
model,
|
model: app,
|
||||||
showModelDetail
|
showModelDetail
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,15 +2,15 @@ import { create } from 'zustand';
|
|||||||
import { devtools, persist } from 'zustand/middleware';
|
import { devtools, persist } from 'zustand/middleware';
|
||||||
import { immer } from 'zustand/middleware/immer';
|
import { immer } from 'zustand/middleware/immer';
|
||||||
import type { UserType, UserUpdateParams } from '@/types/user';
|
import type { UserType, UserUpdateParams } from '@/types/user';
|
||||||
import { getMyModels, getModelById } from '@/api/model';
|
import { getMyModels, getModelById } from '@/api/app';
|
||||||
import { formatPrice } from '@/utils/user';
|
import { formatPrice } from '@/utils/user';
|
||||||
import { getTokenLogin } from '@/api/user';
|
import { getTokenLogin } from '@/api/user';
|
||||||
import { defaultModel } from '@/constants/model';
|
import { defaultApp } from '@/constants/model';
|
||||||
import { ModelListItemType } from '@/types/model';
|
import { ModelListItemType } from '@/types/model';
|
||||||
import { KbItemType } from '@/types/plugin';
|
import { KbItemType } from '@/types/plugin';
|
||||||
import { getKbList, getKbById } from '@/api/plugins/kb';
|
import { getKbList, getKbById } from '@/api/plugins/kb';
|
||||||
import { defaultKbDetail } from '@/constants/kb';
|
import { defaultKbDetail } from '@/constants/kb';
|
||||||
import type { ModelSchema } from '@/types/mongoSchema';
|
import type { AppSchema } from '@/types/mongoSchema';
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
userInfo: UserType | null;
|
userInfo: UserType | null;
|
||||||
@ -20,14 +20,14 @@ type State = {
|
|||||||
// model
|
// model
|
||||||
lastModelId: string;
|
lastModelId: string;
|
||||||
setLastModelId: (id: string) => void;
|
setLastModelId: (id: string) => void;
|
||||||
myModels: ModelListItemType[];
|
myApps: ModelListItemType[];
|
||||||
myCollectionModels: ModelListItemType[];
|
myCollectionApps: ModelListItemType[];
|
||||||
loadMyModels: (init?: boolean) => Promise<null>;
|
loadMyModels: (init?: boolean) => Promise<null>;
|
||||||
modelDetail: ModelSchema;
|
appDetail: AppSchema;
|
||||||
loadModelDetail: (id: string, init?: boolean) => Promise<ModelSchema>;
|
loadAppDetail: (id: string, init?: boolean) => Promise<AppSchema>;
|
||||||
refreshModel: {
|
refreshModel: {
|
||||||
freshMyModels(): void;
|
freshMyModels(): void;
|
||||||
updateModelDetail(model: ModelSchema): void;
|
updateModelDetail(model: AppSchema): void;
|
||||||
removeModelDetail(modelId: string): void;
|
removeModelDetail(modelId: string): void;
|
||||||
};
|
};
|
||||||
// kb
|
// kb
|
||||||
@ -74,24 +74,24 @@ export const useUserStore = create<State>()(
|
|||||||
state.lastModelId = id;
|
state.lastModelId = id;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
myModels: [],
|
myApps: [],
|
||||||
myCollectionModels: [],
|
myCollectionApps: [],
|
||||||
async loadMyModels(init = false) {
|
async loadMyModels(init = false) {
|
||||||
if (get().myModels.length > 0 && !init) return null;
|
if (get().myApps.length > 0 && !init) return null;
|
||||||
const res = await getMyModels();
|
const res = await getMyModels();
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.myModels = res.myModels;
|
state.myApps = res.myApps;
|
||||||
state.myCollectionModels = res.myCollectionModels;
|
state.myCollectionApps = res.myCollectionApps;
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
modelDetail: defaultModel,
|
appDetail: defaultApp,
|
||||||
async loadModelDetail(id: string, init = false) {
|
async loadAppDetail(id: string, init = false) {
|
||||||
if (id === get().modelDetail._id && !init) return get().modelDetail;
|
if (id === get().appDetail._id && !init) return get().appDetail;
|
||||||
|
|
||||||
const res = await getModelById(id);
|
const res = await getModelById(id);
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.modelDetail = res;
|
state.appDetail = res;
|
||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
@ -99,16 +99,16 @@ export const useUserStore = create<State>()(
|
|||||||
freshMyModels() {
|
freshMyModels() {
|
||||||
get().loadMyModels(true);
|
get().loadMyModels(true);
|
||||||
},
|
},
|
||||||
updateModelDetail(model: ModelSchema) {
|
updateModelDetail(model: AppSchema) {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.modelDetail = model;
|
state.appDetail = model;
|
||||||
});
|
});
|
||||||
get().loadMyModels(true);
|
get().loadMyModels(true);
|
||||||
},
|
},
|
||||||
removeModelDetail(modelId: string) {
|
removeModelDetail(modelId: string) {
|
||||||
if (modelId === get().modelDetail._id) {
|
if (modelId === get().appDetail._id) {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.modelDetail = defaultModel;
|
state.appDetail = defaultApp;
|
||||||
state.lastModelId = '';
|
state.lastModelId = '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
3
client/src/styles/reactflow.scss
Normal file
3
client/src/styles/reactflow.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.react-flow__panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@ -83,6 +83,9 @@ textarea::placeholder {
|
|||||||
.grecaptcha-badge {
|
.grecaptcha-badge {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
.react-flow__panel.react-flow__attribution {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
.textlg {
|
.textlg {
|
||||||
background: linear-gradient(to bottom right, #1237b3 0%, #3370ff 40%, #4e83fd 80%, #85b1ff 100%);
|
background: linear-gradient(to bottom right, #1237b3 0%, #3370ff 40%, #4e83fd 80%, #85b1ff 100%);
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
|
|||||||
79
client/src/types/app.d.ts
vendored
79
client/src/types/app.d.ts
vendored
@ -1,41 +1,7 @@
|
|||||||
|
import { FlowModuleTypeEnum } from '@/constants/flow';
|
||||||
|
import { XYPosition } from 'reactflow';
|
||||||
import { AppModuleItemTypeEnum, ModulesInputItemTypeEnum } from '../constants/app';
|
import { AppModuleItemTypeEnum, ModulesInputItemTypeEnum } from '../constants/app';
|
||||||
|
import type { FlowInputItemType, FlowOutputItemType } from './flow';
|
||||||
/* input item */
|
|
||||||
export type ModuleItemCommonType = {
|
|
||||||
key: string; // 字段名
|
|
||||||
formType: `${ModuleInputItemTypeEnum}`;
|
|
||||||
label: string;
|
|
||||||
description?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
max?: number;
|
|
||||||
min?: number;
|
|
||||||
default?: any;
|
|
||||||
enum?: { label: string; value: any }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ModuleItemOutputItemType = {
|
|
||||||
key: string;
|
|
||||||
targets: { moduleId: string; key: string }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ModuleItemType = {
|
|
||||||
moduleId: string;
|
|
||||||
avatar: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
url: string;
|
|
||||||
body: ModuleItemCommonType[];
|
|
||||||
inputs: ModuleItemCommonType[];
|
|
||||||
outputs: ModuleItemOutputItemType[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/* input item */
|
|
||||||
type FormItemCommonType = {
|
|
||||||
key: string; // 字段名
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
formType: `${ModulesInputItemTypeEnum}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* agent */
|
/* agent */
|
||||||
/* question classify */
|
/* question classify */
|
||||||
@ -45,25 +11,44 @@ export type ClassifyQuestionAgentItemType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* app module */
|
/* app module */
|
||||||
export type AppModuleItemType = {
|
export type AppModuleTemplateItemType = {
|
||||||
|
logo?: string;
|
||||||
|
name?: string;
|
||||||
|
intro?: string;
|
||||||
|
|
||||||
|
flowType: `${FlowModuleTypeEnum}`;
|
||||||
|
type: `${AppModuleItemTypeEnum}`;
|
||||||
|
url?: string;
|
||||||
|
inputs: FlowInputItemType[];
|
||||||
|
outputs: FlowOutputItemType[];
|
||||||
|
};
|
||||||
|
export type AppModuleItemType = AppModuleTemplateItemType & {
|
||||||
|
moduleId: string;
|
||||||
|
position?: XYPosition;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AppItemType = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
modules: AppModuleItemType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RunningModuleItemType = {
|
||||||
moduleId: string;
|
moduleId: string;
|
||||||
type: `${AppModuleItemTypeEnum}`;
|
type: `${AppModuleItemTypeEnum}`;
|
||||||
url?: string;
|
url?: string;
|
||||||
body: Record<string, any>;
|
inputs: {
|
||||||
inputs: { key: string; value: any }[];
|
|
||||||
outputs: {
|
|
||||||
key: string;
|
key: string;
|
||||||
value?: any;
|
value?: any;
|
||||||
|
}[];
|
||||||
|
outputs: {
|
||||||
|
key: string;
|
||||||
|
answer?: boolean;
|
||||||
response?: boolean;
|
response?: boolean;
|
||||||
answer?: boolean; // json response
|
value?: any;
|
||||||
targets: {
|
targets: {
|
||||||
moduleId: string;
|
moduleId: string;
|
||||||
key: string;
|
key: string;
|
||||||
}[];
|
}[];
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AppItemType = {
|
|
||||||
id: string;
|
|
||||||
modules: AppModuleItemType[];
|
|
||||||
};
|
|
||||||
|
|||||||
48
client/src/types/flow.d.ts
vendored
Normal file
48
client/src/types/flow.d.ts
vendored
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
FlowBodyItemTypeEnum,
|
||||||
|
FlowInputItemTypeEnum,
|
||||||
|
FlowOutputItemTypeEnum
|
||||||
|
} from '@/constants/flow';
|
||||||
|
import { Connection } from 'reactflow';
|
||||||
|
import type { AppModuleItemType } from './app';
|
||||||
|
|
||||||
|
export type FlowInputItemType = {
|
||||||
|
key: string; // 字段名
|
||||||
|
value?: any;
|
||||||
|
type: `${FlowInputItemTypeEnum}`;
|
||||||
|
label: string;
|
||||||
|
connected?: boolean;
|
||||||
|
description?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
max?: number;
|
||||||
|
min?: number;
|
||||||
|
step?: number;
|
||||||
|
required?: boolean;
|
||||||
|
list?: { label: string; value: any }[];
|
||||||
|
markList?: { label: string; value: any }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FlowOutputTargetItemType = {
|
||||||
|
moduleId: string;
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
export type FlowOutputItemType = {
|
||||||
|
key: string; // 字段名
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
type: `${FlowOutputItemTypeEnum}`;
|
||||||
|
response?: boolean;
|
||||||
|
targets: FlowOutputTargetItemType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FlowModuleItemChangeProps = {
|
||||||
|
moduleId: string;
|
||||||
|
key: string;
|
||||||
|
value: any;
|
||||||
|
valueKey?: keyof FlowInputItemType & keyof FlowBodyItemType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FlowModuleItemType = AppModuleItemType & {
|
||||||
|
onChangeNode: (e: FlowModuleItemChangeProps) => void;
|
||||||
|
onDelNode: (id: string) => void;
|
||||||
|
};
|
||||||
9
client/src/types/model.d.ts
vendored
9
client/src/types/model.d.ts
vendored
@ -1,4 +1,4 @@
|
|||||||
import type { ModelSchema, kbSchema } from './mongoSchema';
|
import type { AppSchema, kbSchema } from './mongoSchema';
|
||||||
import { ChatModelType } from '@/constants/model';
|
import { ChatModelType } from '@/constants/model';
|
||||||
|
|
||||||
export type ModelListItemType = {
|
export type ModelListItemType = {
|
||||||
@ -12,8 +12,9 @@ export interface ModelUpdateParams {
|
|||||||
name?: string;
|
name?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
intro?: string;
|
intro?: string;
|
||||||
chat?: ModelSchema['chat'];
|
chat?: AppSchema['chat'];
|
||||||
share?: ModelSchema['share'];
|
share?: AppSchema['share'];
|
||||||
|
modules?: AppSchema['modules'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShareModelItem {
|
export interface ShareModelItem {
|
||||||
@ -22,7 +23,7 @@ export interface ShareModelItem {
|
|||||||
name: string;
|
name: string;
|
||||||
intro: string;
|
intro: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
share: ModelSchema['share'];
|
share: AppSchema['share'];
|
||||||
isCollection: boolean;
|
isCollection: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
client/src/types/mongoSchema.d.ts
vendored
8
client/src/types/mongoSchema.d.ts
vendored
@ -3,6 +3,7 @@ import { ModelNameEnum, ChatModelType, EmbeddingModelType } from '@/constants/mo
|
|||||||
import type { DataType } from './data';
|
import type { DataType } from './data';
|
||||||
import { BillTypeEnum, InformTypeEnum } from '@/constants/user';
|
import { BillTypeEnum, InformTypeEnum } from '@/constants/user';
|
||||||
import { TrainingModeEnum } from '@/constants/plugin';
|
import { TrainingModeEnum } from '@/constants/plugin';
|
||||||
|
import type { AppModuleItemType } from './app';
|
||||||
|
|
||||||
export interface UserModelSchema {
|
export interface UserModelSchema {
|
||||||
_id: string;
|
_id: string;
|
||||||
@ -30,7 +31,7 @@ export interface AuthCodeSchema {
|
|||||||
expiredTime: number;
|
expiredTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelSchema {
|
export interface AppSchema {
|
||||||
_id: string;
|
_id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -53,9 +54,10 @@ export interface ModelSchema {
|
|||||||
isShareDetail: boolean;
|
isShareDetail: boolean;
|
||||||
collection: number;
|
collection: number;
|
||||||
};
|
};
|
||||||
|
modules: AppModuleItemType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelPopulate extends ModelSchema {
|
export interface ModelPopulate extends AppSchema {
|
||||||
userId: UserModelSchema;
|
userId: UserModelSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +95,7 @@ export interface ChatSchema {
|
|||||||
}
|
}
|
||||||
export interface ChatPopulate extends ChatSchema {
|
export interface ChatPopulate extends ChatSchema {
|
||||||
userId: UserModelSchema;
|
userId: UserModelSchema;
|
||||||
modelId: ModelSchema;
|
modelId: AppSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BillSchema {
|
export interface BillSchema {
|
||||||
|
|||||||
@ -6,6 +6,12 @@ import { ChatCompletionRequestMessageRoleEnum } from 'openai';
|
|||||||
import { ChatRoleEnum } from '@/constants/chat';
|
import { ChatRoleEnum } from '@/constants/chat';
|
||||||
import type { MessageItemType } from '@/pages/api/openapi/v1/chat/completions';
|
import type { MessageItemType } from '@/pages/api/openapi/v1/chat/completions';
|
||||||
import { ChatModelMap, OpenAiChatEnum } from '@/constants/model';
|
import { ChatModelMap, OpenAiChatEnum } from '@/constants/model';
|
||||||
|
import type { AppModuleItemType } from '@/types/app';
|
||||||
|
import type { FlowModuleItemType } from '@/types/flow';
|
||||||
|
import type { Edge, Node } from 'reactflow';
|
||||||
|
import { connectionLineStyle } from '@/constants/flow';
|
||||||
|
import { customAlphabet } from 'nanoid';
|
||||||
|
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||||
|
|
||||||
export const adaptBill = (bill: BillSchema): UserBillType => {
|
export const adaptBill = (bill: BillSchema): UserBillType => {
|
||||||
return {
|
return {
|
||||||
@ -75,3 +81,53 @@ export const parseStreamChunk = (value: BufferSource) => {
|
|||||||
|
|
||||||
return chunkResponse;
|
return chunkResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const appModule2FlowNode = ({
|
||||||
|
item,
|
||||||
|
onChangeNode,
|
||||||
|
onDelNode
|
||||||
|
}: {
|
||||||
|
item: AppModuleItemType;
|
||||||
|
onChangeNode: FlowModuleItemType['onChangeNode'];
|
||||||
|
onDelNode: FlowModuleItemType['onDelNode'];
|
||||||
|
}): Node<FlowModuleItemType> => {
|
||||||
|
return {
|
||||||
|
id: item.moduleId,
|
||||||
|
type: item.flowType,
|
||||||
|
data: {
|
||||||
|
...item,
|
||||||
|
onChangeNode,
|
||||||
|
onDelNode
|
||||||
|
},
|
||||||
|
position: item.position || { x: 0, y: 0 }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export const appModule2FlowEdge = ({
|
||||||
|
modules,
|
||||||
|
onDelete
|
||||||
|
}: {
|
||||||
|
modules: AppModuleItemType[];
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
}) => {
|
||||||
|
console.log(modules);
|
||||||
|
|
||||||
|
const edges: Edge[] = [];
|
||||||
|
modules.forEach((module) =>
|
||||||
|
module.outputs.forEach((output) =>
|
||||||
|
output.targets.forEach((target) => {
|
||||||
|
edges.push({
|
||||||
|
style: connectionLineStyle,
|
||||||
|
source: module.moduleId,
|
||||||
|
target: target.moduleId,
|
||||||
|
sourceHandle: output.key,
|
||||||
|
targetHandle: target.key,
|
||||||
|
id: nanoid(),
|
||||||
|
animated: true,
|
||||||
|
type: 'buttonedge',
|
||||||
|
data: { onDelete }
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return edges;
|
||||||
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user