Commit ff7b0ded authored by shirlyn.guo's avatar shirlyn.guo 👌🏻

Merge branch 'master' of https://gitlab.gsstcloud.com/poc/poc-fe into shirlyn

parents 4363bd5a 4cc157d3
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/> />
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_4711453_f5muspehl1h.css" /> <link rel="stylesheet" href="//at.alicdn.com/t/c/font_4711453_i03chzm1n0e.css" />
<title>Model Link</title> <title>Model Link</title>
</head> </head>
......
...@@ -22,12 +22,14 @@ ...@@ -22,12 +22,14 @@
"@unocss/reset": "^0.61.9", "@unocss/reset": "^0.61.9",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^10.11.1",
"axios": "^1.7.7", "axios": "^1.7.7",
"bowser": "^2.11.0",
"clipboardy": "^4.0.0", "clipboardy": "^4.0.0",
"cropperjs": "^1.6.2", "cropperjs": "^1.6.2",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dompurify": "^3.2.0", "dompurify": "^3.2.0",
"github-markdown-css": "^5.7.0", "github-markdown-css": "^5.7.0",
"highlight.js": "^11.10.0", "highlight.js": "^11.10.0",
"howler": "^2.2.4",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"marked": "^15.0.0", "marked": "^15.0.0",
"marked-highlight": "^2.2.1", "marked-highlight": "^2.2.1",
...@@ -46,6 +48,7 @@ ...@@ -46,6 +48,7 @@
"@commitlint/config-conventional": "^19.5.0", "@commitlint/config-conventional": "^19.5.0",
"@commitlint/types": "^19.5.0", "@commitlint/types": "^19.5.0",
"@intlify/unplugin-vue-i18n": "^4.0.0", "@intlify/unplugin-vue-i18n": "^4.0.0",
"@types/howler": "^2.2.12",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^20.16.5", "@types/node": "^20.16.5",
"@types/spark-md5": "^3.0.4", "@types/spark-md5": "^3.0.4",
......
...@@ -26,6 +26,9 @@ importers: ...@@ -26,6 +26,9 @@ importers:
axios: axios:
specifier: ^1.7.7 specifier: ^1.7.7
version: 1.7.7 version: 1.7.7
bowser:
specifier: ^2.11.0
version: 2.11.0
clipboardy: clipboardy:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
...@@ -44,6 +47,9 @@ importers: ...@@ -44,6 +47,9 @@ importers:
highlight.js: highlight.js:
specifier: ^11.10.0 specifier: ^11.10.0
version: 11.10.0 version: 11.10.0
howler:
specifier: ^2.2.4
version: 2.2.4
lodash-es: lodash-es:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
...@@ -93,6 +99,9 @@ importers: ...@@ -93,6 +99,9 @@ importers:
'@intlify/unplugin-vue-i18n': '@intlify/unplugin-vue-i18n':
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0(rollup@4.21.3)(vue-i18n@9.14.0(vue@3.5.12(typescript@5.6.2)))(webpack-sources@3.2.3) version: 4.0.0(rollup@4.21.3)(vue-i18n@9.14.0(vue@3.5.12(typescript@5.6.2)))(webpack-sources@3.2.3)
'@types/howler':
specifier: ^2.2.12
version: 2.2.12
'@types/lodash-es': '@types/lodash-es':
specifier: ^4.17.12 specifier: ^4.17.12
version: 4.17.12 version: 4.17.12
...@@ -107,10 +116,10 @@ importers: ...@@ -107,10 +116,10 @@ importers:
version: 13.12.2 version: 13.12.2
'@typescript-eslint/parser': '@typescript-eslint/parser':
specifier: ^7.18.0 specifier: ^7.18.0
version: 7.18.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) version: 7.18.0(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)
'@unocss/eslint-config': '@unocss/eslint-config':
specifier: ^0.61.9 specifier: ^0.61.9
version: 0.61.9(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) version: 0.61.9(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^4.6.2 specifier: ^4.6.2
version: 4.6.2(vite@5.4.6(@types/node@20.16.5)(sass@1.79.1))(vue@3.5.12(typescript@5.6.2)) version: 4.6.2(vite@5.4.6(@types/node@20.16.5)(sass@1.79.1))(vue@3.5.12(typescript@5.6.2))
...@@ -122,16 +131,16 @@ importers: ...@@ -122,16 +131,16 @@ importers:
version: 10.4.20(postcss@8.4.47) version: 10.4.20(postcss@8.4.47)
eslint: eslint:
specifier: ^9.10.0 specifier: ^9.10.0
version: 9.10.0(jiti@1.21.6) version: 9.10.0(jiti@2.0.0-beta.3)
eslint-config-prettier: eslint-config-prettier:
specifier: ^9.1.0 specifier: ^9.1.0
version: 9.1.0(eslint@9.10.0(jiti@1.21.6)) version: 9.1.0(eslint@9.10.0(jiti@2.0.0-beta.3))
eslint-plugin-prettier: eslint-plugin-prettier:
specifier: ^5.2.1 specifier: ^5.2.1
version: 5.2.1(eslint-config-prettier@9.1.0(eslint@9.10.0(jiti@1.21.6)))(eslint@9.10.0(jiti@1.21.6))(prettier@3.3.3) version: 5.2.1(eslint-config-prettier@9.1.0(eslint@9.10.0(jiti@2.0.0-beta.3)))(eslint@9.10.0(jiti@2.0.0-beta.3))(prettier@3.3.3)
eslint-plugin-vue: eslint-plugin-vue:
specifier: ^9.28.0 specifier: ^9.28.0
version: 9.28.0(eslint@9.10.0(jiti@1.21.6)) version: 9.28.0(eslint@9.10.0(jiti@2.0.0-beta.3))
globals: globals:
specifier: ^15.9.0 specifier: ^15.9.0
version: 15.9.0 version: 15.9.0
...@@ -188,7 +197,7 @@ importers: ...@@ -188,7 +197,7 @@ importers:
version: 5.6.2 version: 5.6.2
typescript-eslint: typescript-eslint:
specifier: ^7.18.0 specifier: ^7.18.0
version: 7.18.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) version: 7.18.0(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)
unocss: unocss:
specifier: ^0.61.9 specifier: ^0.61.9
version: 0.61.9(postcss@8.4.47)(rollup@4.21.3)(vite@5.4.6(@types/node@20.16.5)(sass@1.79.1)) version: 0.61.9(postcss@8.4.47)(rollup@4.21.3)(vite@5.4.6(@types/node@20.16.5)(sass@1.79.1))
...@@ -203,13 +212,13 @@ importers: ...@@ -203,13 +212,13 @@ importers:
version: 5.4.6(@types/node@20.16.5)(sass@1.79.1) version: 5.4.6(@types/node@20.16.5)(sass@1.79.1)
vite-plugin-checker: vite-plugin-checker:
specifier: ^0.7.2 specifier: ^0.7.2
version: 0.7.2(eslint@9.10.0(jiti@1.21.6))(optionator@0.9.4)(stylelint@16.9.0(typescript@5.6.2))(typescript@5.6.2)(vite@5.4.6(@types/node@20.16.5)(sass@1.79.1))(vue-tsc@2.0.29(typescript@5.6.2)) version: 0.7.2(eslint@9.10.0(jiti@2.0.0-beta.3))(meow@13.2.0)(optionator@0.9.4)(stylelint@16.9.0(typescript@5.6.2))(typescript@5.6.2)(vite@5.4.6(@types/node@20.16.5)(sass@1.79.1))(vue-tsc@2.0.29(typescript@5.6.2))
vite-svg-loader: vite-svg-loader:
specifier: ^5.1.0 specifier: ^5.1.0
version: 5.1.0(vue@3.5.12(typescript@5.6.2)) version: 5.1.0(vue@3.5.12(typescript@5.6.2))
vue-eslint-parser: vue-eslint-parser:
specifier: ^9.4.3 specifier: ^9.4.3
version: 9.4.3(eslint@9.10.0(jiti@1.21.6)) version: 9.4.3(eslint@9.10.0(jiti@2.0.0-beta.3))
vue-tsc: vue-tsc:
specifier: ^2.0.29 specifier: ^2.0.29
version: 2.0.29(typescript@5.6.2) version: 2.0.29(typescript@5.6.2)
...@@ -1003,6 +1012,9 @@ packages: ...@@ -1003,6 +1012,9 @@ packages:
'@types/estree@1.0.5': '@types/estree@1.0.5':
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
'@types/howler@2.2.12':
resolution: {integrity: sha512-hy769UICzOSdK0Kn1FBk4gN+lswcj1EKRkmiDtMkUGvFfYJzgaDXmVXkSShS2m89ERAatGIPnTUlp2HhfkVo5g==}
'@types/katex@0.16.7': '@types/katex@0.16.7':
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
...@@ -1377,6 +1389,9 @@ packages: ...@@ -1377,6 +1389,9 @@ packages:
boolbase@1.0.0: boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
bowser@2.11.0:
resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==}
brace-expansion@1.1.11: brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
...@@ -1983,6 +1998,9 @@ packages: ...@@ -1983,6 +1998,9 @@ packages:
resolution: {integrity: sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==} resolution: {integrity: sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
howler@2.2.4:
resolution: {integrity: sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==}
html-tags@3.3.1: html-tags@3.3.1:
resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
...@@ -3762,9 +3780,9 @@ snapshots: ...@@ -3762,9 +3780,9 @@ snapshots:
'@esbuild/win32-x64@0.23.1': '@esbuild/win32-x64@0.23.1':
optional: true optional: true
'@eslint-community/eslint-utils@4.4.0(eslint@9.10.0(jiti@1.21.6))': '@eslint-community/eslint-utils@4.4.0(eslint@9.10.0(jiti@2.0.0-beta.3))':
dependencies: dependencies:
eslint: 9.10.0(jiti@1.21.6) eslint: 9.10.0(jiti@2.0.0-beta.3)
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.11.1': {} '@eslint-community/regexpp@4.11.1': {}
...@@ -3974,6 +3992,8 @@ snapshots: ...@@ -3974,6 +3992,8 @@ snapshots:
'@types/estree@1.0.5': {} '@types/estree@1.0.5': {}
'@types/howler@2.2.12': {}
'@types/katex@0.16.7': {} '@types/katex@0.16.7': {}
'@types/lodash-es@4.17.12': '@types/lodash-es@4.17.12':
...@@ -3992,15 +4012,15 @@ snapshots: ...@@ -3992,15 +4012,15 @@ snapshots:
'@types/web-bluetooth@0.0.20': {} '@types/web-bluetooth@0.0.20': {}
'@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)': '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2))(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)':
dependencies: dependencies:
'@eslint-community/regexpp': 4.11.1 '@eslint-community/regexpp': 4.11.1
'@typescript-eslint/parser': 7.18.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) '@typescript-eslint/parser': 7.18.0(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)
'@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/scope-manager': 7.18.0
'@typescript-eslint/type-utils': 7.18.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) '@typescript-eslint/type-utils': 7.18.0(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)
'@typescript-eslint/utils': 7.18.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) '@typescript-eslint/utils': 7.18.0(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)
'@typescript-eslint/visitor-keys': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0
eslint: 9.10.0(jiti@1.21.6) eslint: 9.10.0(jiti@2.0.0-beta.3)
graphemer: 1.4.0 graphemer: 1.4.0
ignore: 5.3.2 ignore: 5.3.2
natural-compare: 1.4.0 natural-compare: 1.4.0
...@@ -4010,14 +4030,14 @@ snapshots: ...@@ -4010,14 +4030,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/parser@7.18.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)': '@typescript-eslint/parser@7.18.0(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)':
dependencies: dependencies:
'@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/scope-manager': 7.18.0
'@typescript-eslint/types': 7.18.0 '@typescript-eslint/types': 7.18.0
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.2) '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.2)
'@typescript-eslint/visitor-keys': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0
debug: 4.3.7 debug: 4.3.7
eslint: 9.10.0(jiti@1.21.6) eslint: 9.10.0(jiti@2.0.0-beta.3)
optionalDependencies: optionalDependencies:
typescript: 5.6.2 typescript: 5.6.2
transitivePeerDependencies: transitivePeerDependencies:
...@@ -4028,12 +4048,12 @@ snapshots: ...@@ -4028,12 +4048,12 @@ snapshots:
'@typescript-eslint/types': 7.18.0 '@typescript-eslint/types': 7.18.0
'@typescript-eslint/visitor-keys': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0
'@typescript-eslint/type-utils@7.18.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)': '@typescript-eslint/type-utils@7.18.0(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)':
dependencies: dependencies:
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.2) '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.2)
'@typescript-eslint/utils': 7.18.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) '@typescript-eslint/utils': 7.18.0(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)
debug: 4.3.7 debug: 4.3.7
eslint: 9.10.0(jiti@1.21.6) eslint: 9.10.0(jiti@2.0.0-beta.3)
ts-api-utils: 1.3.0(typescript@5.6.2) ts-api-utils: 1.3.0(typescript@5.6.2)
optionalDependencies: optionalDependencies:
typescript: 5.6.2 typescript: 5.6.2
...@@ -4057,13 +4077,13 @@ snapshots: ...@@ -4057,13 +4077,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/utils@7.18.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)': '@typescript-eslint/utils@7.18.0(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)':
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@9.10.0(jiti@1.21.6)) '@eslint-community/eslint-utils': 4.4.0(eslint@9.10.0(jiti@2.0.0-beta.3))
'@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/scope-manager': 7.18.0
'@typescript-eslint/types': 7.18.0 '@typescript-eslint/types': 7.18.0
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.2) '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.2)
eslint: 9.10.0(jiti@1.21.6) eslint: 9.10.0(jiti@2.0.0-beta.3)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
- typescript - typescript
...@@ -4112,17 +4132,17 @@ snapshots: ...@@ -4112,17 +4132,17 @@ snapshots:
'@unocss/core@0.61.9': {} '@unocss/core@0.61.9': {}
'@unocss/eslint-config@0.61.9(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)': '@unocss/eslint-config@0.61.9(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)':
dependencies: dependencies:
'@unocss/eslint-plugin': 0.61.9(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) '@unocss/eslint-plugin': 0.61.9(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)
transitivePeerDependencies: transitivePeerDependencies:
- eslint - eslint
- supports-color - supports-color
- typescript - typescript
'@unocss/eslint-plugin@0.61.9(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)': '@unocss/eslint-plugin@0.61.9(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)':
dependencies: dependencies:
'@typescript-eslint/utils': 7.18.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) '@typescript-eslint/utils': 7.18.0(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)
'@unocss/config': 0.61.9 '@unocss/config': 0.61.9
'@unocss/core': 0.61.9 '@unocss/core': 0.61.9
magic-string: 0.30.11 magic-string: 0.30.11
...@@ -4525,6 +4545,8 @@ snapshots: ...@@ -4525,6 +4545,8 @@ snapshots:
boolbase@1.0.0: {} boolbase@1.0.0: {}
bowser@2.11.0: {}
brace-expansion@1.1.11: brace-expansion@1.1.11:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
...@@ -4866,29 +4888,29 @@ snapshots: ...@@ -4866,29 +4888,29 @@ snapshots:
optionalDependencies: optionalDependencies:
source-map: 0.6.1 source-map: 0.6.1
eslint-config-prettier@9.1.0(eslint@9.10.0(jiti@1.21.6)): eslint-config-prettier@9.1.0(eslint@9.10.0(jiti@2.0.0-beta.3)):
dependencies: dependencies:
eslint: 9.10.0(jiti@1.21.6) eslint: 9.10.0(jiti@2.0.0-beta.3)
eslint-plugin-prettier@5.2.1(eslint-config-prettier@9.1.0(eslint@9.10.0(jiti@1.21.6)))(eslint@9.10.0(jiti@1.21.6))(prettier@3.3.3): eslint-plugin-prettier@5.2.1(eslint-config-prettier@9.1.0(eslint@9.10.0(jiti@2.0.0-beta.3)))(eslint@9.10.0(jiti@2.0.0-beta.3))(prettier@3.3.3):
dependencies: dependencies:
eslint: 9.10.0(jiti@1.21.6) eslint: 9.10.0(jiti@2.0.0-beta.3)
prettier: 3.3.3 prettier: 3.3.3
prettier-linter-helpers: 1.0.0 prettier-linter-helpers: 1.0.0
synckit: 0.9.1 synckit: 0.9.1
optionalDependencies: optionalDependencies:
eslint-config-prettier: 9.1.0(eslint@9.10.0(jiti@1.21.6)) eslint-config-prettier: 9.1.0(eslint@9.10.0(jiti@2.0.0-beta.3))
eslint-plugin-vue@9.28.0(eslint@9.10.0(jiti@1.21.6)): eslint-plugin-vue@9.28.0(eslint@9.10.0(jiti@2.0.0-beta.3)):
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@9.10.0(jiti@1.21.6)) '@eslint-community/eslint-utils': 4.4.0(eslint@9.10.0(jiti@2.0.0-beta.3))
eslint: 9.10.0(jiti@1.21.6) eslint: 9.10.0(jiti@2.0.0-beta.3)
globals: 13.24.0 globals: 13.24.0
natural-compare: 1.4.0 natural-compare: 1.4.0
nth-check: 2.1.1 nth-check: 2.1.1
postcss-selector-parser: 6.1.2 postcss-selector-parser: 6.1.2
semver: 7.6.3 semver: 7.6.3
vue-eslint-parser: 9.4.3(eslint@9.10.0(jiti@1.21.6)) vue-eslint-parser: 9.4.3(eslint@9.10.0(jiti@2.0.0-beta.3))
xml-name-validator: 4.0.0 xml-name-validator: 4.0.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
...@@ -4907,9 +4929,9 @@ snapshots: ...@@ -4907,9 +4929,9 @@ snapshots:
eslint-visitor-keys@4.0.0: {} eslint-visitor-keys@4.0.0: {}
eslint@9.10.0(jiti@1.21.6): eslint@9.10.0(jiti@2.0.0-beta.3):
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@9.10.0(jiti@1.21.6)) '@eslint-community/eslint-utils': 4.4.0(eslint@9.10.0(jiti@2.0.0-beta.3))
'@eslint-community/regexpp': 4.11.1 '@eslint-community/regexpp': 4.11.1
'@eslint/config-array': 0.18.0 '@eslint/config-array': 0.18.0
'@eslint/eslintrc': 3.1.0 '@eslint/eslintrc': 3.1.0
...@@ -4944,7 +4966,7 @@ snapshots: ...@@ -4944,7 +4966,7 @@ snapshots:
strip-ansi: 6.0.1 strip-ansi: 6.0.1
text-table: 0.2.0 text-table: 0.2.0
optionalDependencies: optionalDependencies:
jiti: 1.21.6 jiti: 2.0.0-beta.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
...@@ -5159,6 +5181,8 @@ snapshots: ...@@ -5159,6 +5181,8 @@ snapshots:
highlight.js@11.10.0: {} highlight.js@11.10.0: {}
howler@2.2.4: {}
html-tags@3.3.1: {} html-tags@3.3.1: {}
htmlparser2@8.0.2: htmlparser2@8.0.2:
...@@ -6015,12 +6039,12 @@ snapshots: ...@@ -6015,12 +6039,12 @@ snapshots:
type-fest@4.26.1: {} type-fest@4.26.1: {}
typescript-eslint@7.18.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2): typescript-eslint@7.18.0(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2):
dependencies: dependencies:
'@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2))(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)
'@typescript-eslint/parser': 7.18.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) '@typescript-eslint/parser': 7.18.0(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)
'@typescript-eslint/utils': 7.18.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) '@typescript-eslint/utils': 7.18.0(eslint@9.10.0(jiti@2.0.0-beta.3))(typescript@5.6.2)
eslint: 9.10.0(jiti@1.21.6) eslint: 9.10.0(jiti@2.0.0-beta.3)
optionalDependencies: optionalDependencies:
typescript: 5.6.2 typescript: 5.6.2
transitivePeerDependencies: transitivePeerDependencies:
...@@ -6154,7 +6178,7 @@ snapshots: ...@@ -6154,7 +6178,7 @@ snapshots:
evtd: 0.2.4 evtd: 0.2.4
vue: 3.5.12(typescript@5.6.2) vue: 3.5.12(typescript@5.6.2)
vite-plugin-checker@0.7.2(eslint@9.10.0(jiti@1.21.6))(optionator@0.9.4)(stylelint@16.9.0(typescript@5.6.2))(typescript@5.6.2)(vite@5.4.6(@types/node@20.16.5)(sass@1.79.1))(vue-tsc@2.0.29(typescript@5.6.2)): vite-plugin-checker@0.7.2(eslint@9.10.0(jiti@2.0.0-beta.3))(meow@13.2.0)(optionator@0.9.4)(stylelint@16.9.0(typescript@5.6.2))(typescript@5.6.2)(vite@5.4.6(@types/node@20.16.5)(sass@1.79.1))(vue-tsc@2.0.29(typescript@5.6.2)):
dependencies: dependencies:
'@babel/code-frame': 7.24.7 '@babel/code-frame': 7.24.7
ansi-escapes: 4.3.2 ansi-escapes: 4.3.2
...@@ -6172,7 +6196,8 @@ snapshots: ...@@ -6172,7 +6196,8 @@ snapshots:
vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-textdocument: 1.0.12
vscode-uri: 3.0.8 vscode-uri: 3.0.8
optionalDependencies: optionalDependencies:
eslint: 9.10.0(jiti@1.21.6) eslint: 9.10.0(jiti@2.0.0-beta.3)
meow: 13.2.0
optionator: 0.9.4 optionator: 0.9.4
stylelint: 16.9.0(typescript@5.6.2) stylelint: 16.9.0(typescript@5.6.2)
typescript: 5.6.2 typescript: 5.6.2
...@@ -6225,10 +6250,10 @@ snapshots: ...@@ -6225,10 +6250,10 @@ snapshots:
dependencies: dependencies:
vue: 3.5.12(typescript@5.6.2) vue: 3.5.12(typescript@5.6.2)
vue-eslint-parser@9.4.3(eslint@9.10.0(jiti@1.21.6)): vue-eslint-parser@9.4.3(eslint@9.10.0(jiti@2.0.0-beta.3)):
dependencies: dependencies:
debug: 4.3.7 debug: 4.3.7
eslint: 9.10.0(jiti@1.21.6) eslint: 9.10.0(jiti@2.0.0-beta.3)
eslint-scope: 7.2.2 eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
espree: 9.6.1 espree: 9.6.1
......
...@@ -170,3 +170,20 @@ export function fetchRemoveSalePublishApplication<T>(agentPublishId: number) { ...@@ -170,3 +170,20 @@ export function fetchRemoveSalePublishApplication<T>(agentPublishId: number) {
export function fetchGetApplicationMallInfo<T>(agentId: string) { export function fetchGetApplicationMallInfo<T>(agentId: string) {
return request.post<T>(`/bizAgentApplicationMallRest/getMallInfoByAgentId.json?agentId=${agentId}`) return request.post<T>(`/bizAgentApplicationMallRest/getMallInfoByAgentId.json?agentId=${agentId}`)
} }
/**
* @query agentId 应用Id
* @returns 获取用户自动播放配置
*/
export function fetchGetAutoPlayByAgentId<T>(agentId: string) {
return request.post<T>(`/agentApplicationRest/autoPlayByAgentId.json?agentId=${agentId}`)
}
/**
* @query agentId 应用Id
* @query autoPlay 是否自动播放
* @returns 设置用户自动播放配置
*/
export function fetchUpdateAutoPlay<T>(agentId: string, autoPlay: 'Y' | 'N') {
return request.post<T>(`/agentApplicationRest/enableAutoPlay.json?agentId=${agentId}&autoPlay=${autoPlay}`)
}
import { request } from '@/utils/request' import { request } from '@/utils/request'
import type { AxiosProgressEvent } from 'axios'
export function fetchAgentApplicationSelectList<T>() { export function fetchAgentApplicationSelectList<T>() {
return request.post<T>('/agentApplicationRest/getDefaultList.json', { return request.post<T>('/agentApplicationRest/getDefaultList.json', {
...@@ -28,3 +29,17 @@ export function fetchMessageRecordList<T>(dialogueId: string) { ...@@ -28,3 +29,17 @@ export function fetchMessageRecordList<T>(dialogueId: string) {
timeout: 12000, timeout: 12000,
}) })
} }
export function fetchFileUpload<T>(
formData: FormData,
config: { onUploadProgress: (progressEvent?: AxiosProgressEvent) => void; signal?: AbortSignal } = {
onUploadProgress: () => {},
},
) {
return request.post<T>(`/bosRest/upload.json`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 0,
onUploadProgress: config.onUploadProgress,
signal: config.signal,
})
}
import { request } from '@/utils/request'
/**
* @returns 获取音色列表
*/
export function fetchGetTimbreList<T>() {
return request.post<T>('/bizVoiceTimbreRest/getTimbreList.json')
}
/**
* @query timbreId 音色Id
* @returns 获取音色详情
*/
export function fetchGetTimbreInfoDetail<T>(timbreId: string) {
return request.post<T>(`/bizVoiceTimbreRest/getTimbreInfo.json?timbreId=${timbreId}`)
}
<?xml version="1.0" encoding="UTF-8"?>
<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>黑色暂停</title>
<defs>
<rect id="path-1" x="0" y="0" width="34" height="34"></rect>
</defs>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="对话-进入" transform="translate(-181.000000, -310.000000)">
<g id="编组-2" transform="translate(181.000000, 310.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="矩形"></g>
<path d="M17.0006063,1.5 C21.2734934,1.5 25.1479946,3.23913189 27.9545032,6.04584067 C30.7609677,8.85250533 32.5,12.7272396 32.5,17.0006063 C32.5,21.2738531 30.7610549,25.1482139 27.9547057,27.9545631 C25.1481717,30.7610971 21.2735836,32.5 17.0006063,32.5 C12.7272396,32.5 8.85250534,30.7609677 6.04584068,27.9545032 C3.23913189,25.1479946 1.5,21.2734933 1.5,17.0006063 C1.5,12.7275994 3.23921912,8.8527246 6.04604319,6.04590054 C8.85268248,3.23926124 12.72733,1.5 17.0006063,1.5 L17.0006063,1.5 Z" id="路径" stroke="#333333" stroke-width="3" fill-rule="nonzero" mask="url(#mask-2)"></path>
<rect id="矩形" fill="#000DFF" mask="url(#mask-2)" x="11.3333333" y="11.3333333" width="3.23809524" height="11.3333333" rx="1.61904762"></rect>
<rect id="矩形备份" fill="#000DFF" mask="url(#mask-2)" x="19.4285714" y="11.3333333" width="3.23809524" height="11.3333333" rx="1.61904762"></rect>
</g>
</g>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>播放</title>
<defs>
<rect id="path-1" x="0" y="0" width="34" height="34"></rect>
</defs>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="对话-按住语音-提示语" transform="translate(-181.000000, -310.000000)">
<g id="播放" transform="translate(181.000000, 310.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="矩形"></g>
<g mask="url(#mask-2)" fill-rule="nonzero">
<path d="M17.0005306,1.5 C21.2734482,1.5 25.1478582,3.23921344 27.9543229,6.04585326 C30.7609106,8.85261617 32.5,12.7274291 32.5,17.0005306 C32.5,21.2735273 30.7609869,25.1480133 27.9545001,27.9545001 C25.1480133,30.7609869 21.2735273,32.5 17.0005306,32.5 C12.7269453,32.5 8.85210629,30.7611966 6.04545533,27.9547208 C3.23892745,25.1483681 1.5,21.273932 1.5,17.0005306 C1.5,12.7270243 3.23900379,8.85226132 6.04563255,6.04563255 C8.85226132,3.23900379 12.7270243,1.5 17.0005306,1.5 Z" id="路径" stroke="#CCCCCC" stroke-width="3"></path>
<path d="M13.6855279,9.59377047 C14.4484879,9.59377047 15.1658188,9.88982862 15.2443432,9.92378513 L15.2443432,9.92378513 L15.3812303,9.99488155 L24.6725758,15.6783496 L24.8073406,15.9213507 C25.5108767,17.1883524 24.8275023,18.3131612 24.1950626,18.7609625 L24.1950626,18.7609625 L24.1048657,18.8193252 L14.9196343,24.02528 C14.471833,24.2544864 14.0654162,24.3606004 13.6727943,24.3606004 L13.6727943,24.3606004 L13.4721536,24.3516533 C12.36468,24.2516108 11.7569642,23.3358336 11.5685528,22.7816235 L11.5685528,22.7816235 L11.5112512,22.6150245 L11.5112512,11.827471 C11.5112512,10.4713336 12.3654693,9.59377047 13.6855279,9.59377047 Z M13.64096,11.7171125 C13.6367155,11.7447021 13.6345932,11.781842 13.6345932,11.8274711 L13.6345932,11.8274711 L13.6345932,22.1884461 C13.6515714,22.21073 13.6674885,22.2287694 13.6802222,22.2393808 C13.7046285,22.2372585 13.7799694,22.2266471 13.9136731,22.1587341 L13.9136731,22.1587341 L22.8856153,17.071627 L14.3551075,11.8539996 C14.2044256,11.7988203 13.837271,11.7065011 13.64096,11.7171125 Z" id="形状结合" stroke="#000DFF" stroke-width="0.5" fill="#000DFF"></path>
</g>
</g>
</g>
</g>
</svg>
...@@ -7,3 +7,12 @@ export const INDEX_URLS: Record<'DEV' | 'PROD', string> = { ...@@ -7,3 +7,12 @@ export const INDEX_URLS: Record<'DEV' | 'PROD', string> = {
DEV: 'https://poc-sit.gsstcloud.com/fe/', DEV: 'https://poc-sit.gsstcloud.com/fe/',
PROD: 'https://model-link.gsstcloud.com/fe/', PROD: 'https://model-link.gsstcloud.com/fe/',
} }
const ENV = import.meta.env.VITE_APP_ENV
export const Domain_Name: Record<'DEV' | 'PROD', string> = {
DEV: 'poc-sit.gsstcloud.com',
PROD: 'model-link.gsstcloud.com',
}
export const TEXTTOSPEECH_WS_URL = `wss://${Domain_Name[ENV || 'DEV']}/websocket/textToSpeechTC.ws`
...@@ -91,6 +91,21 @@ common_module: ...@@ -91,6 +91,21 @@ common_module:
bind: 'Bind' bind: 'Bind'
sms: 'Short message' sms: 'Short message'
verificationCode: 'Verification code' verificationCode: 'Verification code'
role: 'Role'
mandarin: 'Mandarin'
cantonese: 'Cantonese'
english: 'English'
voice: 'Voice'
sound: 'Sound'
voice_auto_play: 'Voice auto play'
start_playing: 'play'
stop_playing: 'stop'
unplayable: 'unplayable'
unplayable_tip: 'The voice setting do not match the model output language'
response_error: 'Response error'
agent_exception: 'Agent exception, please try again later!'
equity: 'Equity'
file_size_limit: 'The file size cannot exceed {size}'
dialogue_module: dialogue_module:
continue_question_message: 'You can keep asking questions' continue_question_message: 'You can keep asking questions'
...@@ -103,6 +118,8 @@ common_module: ...@@ -103,6 +118,8 @@ common_module:
cancel_associate_file_tip: 'No longer answer around this file' cancel_associate_file_tip: 'No longer answer around this file'
upload_file_limit: 'Only a single file can be uploaded in PDF, DOC, DOCX, MD, TXT format, up to 10MB' upload_file_limit: 'Only a single file can be uploaded in PDF, DOC, DOCX, MD, TXT format, up to 10MB'
overwrite_file_tip: 'The newly uploaded file overwrites the original file, whether to continue uploading' overwrite_file_tip: 'The newly uploaded file overwrites the original file, whether to continue uploading'
stop_playing_and_then_operate: 'When the audio is playing, stop playing and then perform the operation'
do_not_operate_until_the_reply_is_complete: 'Do not operate until the reply is complete'
data_table_module: data_table_module:
action: 'Controls' action: 'Controls'
...@@ -281,6 +298,10 @@ personal_space_module: ...@@ -281,6 +298,10 @@ personal_space_module:
memory_fragment_delete_row_tip_content: 'After data deletion, it cannot be revoked. Are you sure you want to delete it?' memory_fragment_delete_row_tip_content: 'After data deletion, it cannot be revoked. Are you sure you want to delete it?'
add_knowledge_successfully: 'Data set {0} was added successfully' add_knowledge_successfully: 'Data set {0} was added successfully'
remove_knowledge_successfully: 'Data set {0} was removed successfully' remove_knowledge_successfully: 'Data set {0} was removed successfully'
setting_voice: 'Setting voice'
setting_voice_message: 'You can set the language and tone. If the selected language is inconsistent with the model language, the speech cannot be played'
setting_voice_desc: 'You can customize the voice and timbre for voice broadcast, and you can set only one at a time.'
currently_only_one_voice_can_be_set: 'Currently, you can set only one voice'
memory_variable_modal: memory_variable_modal:
edit_memory_variable: 'Edit memory variable' edit_memory_variable: 'Edit memory variable'
...@@ -462,3 +483,9 @@ personal_settings_module: ...@@ -462,3 +483,9 @@ personal_settings_module:
please_enter_the_correct_verification_code: 'Please enter the correct verification code' please_enter_the_correct_verification_code: 'Please enter the correct verification code'
binding_successful: 'Binding successful' binding_successful: 'Binding successful'
obtaining_the_verification_code: 'Obtaining the verification code' obtaining_the_verification_code: 'Obtaining the verification code'
upload_file: 'Upload file'
file_type_restriction_doc: 'Only.pdf,.txt, .md, .doc, and.docx file types are supported'
the_file_type_cannot_be_uploaded: 'The file type cannot be uploaded'
equity_Module:
file_empty_tip: 'The file content cannot be empty'
...@@ -90,6 +90,21 @@ common_module: ...@@ -90,6 +90,21 @@ common_module:
bind: '绑定' bind: '绑定'
sms: '短信' sms: '短信'
verificationCode: '验证码' verificationCode: '验证码'
role: '角色'
mandarin: '普通话'
cantonese: '粤语'
english: '英语'
voice: '语音'
sound: '声音'
voice_auto_play: '语音自动播放'
start_playing: '开始播放'
stop_playing: '停止播放'
unplayable: '不可播放'
unplayable_tip: '语音设置与模型输出语言不匹配'
response_error: '响应错误'
agent_exception: '应用异常,请稍后重试!'
equity: '权益'
file_size_limit: '文件大小不能超过 {size}'
dialogue_module: dialogue_module:
continue_question_message: '你可以继续提问' continue_question_message: '你可以继续提问'
...@@ -102,6 +117,8 @@ common_module: ...@@ -102,6 +117,8 @@ common_module:
cancel_associate_file_tip: '不再围绕这个文件回答' cancel_associate_file_tip: '不再围绕这个文件回答'
upload_file_limit: '仅支持上传单个文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB' upload_file_limit: '仅支持上传单个文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB'
overwrite_file_tip: '新上传的文件会覆盖原有文件,是否继续上传' overwrite_file_tip: '新上传的文件会覆盖原有文件,是否继续上传'
stop_playing_and_then_operate: '音频播放中,请停止播放后再操作'
do_not_operate_until_the_reply_is_complete: '回复完成后再操作'
data_table_module: data_table_module:
action: '操作' action: '操作'
...@@ -279,6 +296,10 @@ personal_space_module: ...@@ -279,6 +296,10 @@ personal_space_module:
memory_fragment_delete_row_tip_content: '数据删除后不可撤销,确定要删除吗?' memory_fragment_delete_row_tip_content: '数据删除后不可撤销,确定要删除吗?'
add_knowledge_successfully: '数据集 {0} 添加成功' add_knowledge_successfully: '数据集 {0} 添加成功'
remove_knowledge_successfully: '数据集 {0} 移除成功' remove_knowledge_successfully: '数据集 {0} 移除成功'
setting_voice: '设置语音'
setting_voice_message: '你可以设置语言和音色,若所选语言与模型语言不一致,则无法播放语音'
setting_voice_desc: '您可自定义语音及音色,用于语音播报,且每次仅可设置一种。'
currently_only_one_voice_can_be_set: '当前仅可设置一种声音'
memory_variable_modal: memory_variable_modal:
edit_memory_variable: '编辑记忆变量' edit_memory_variable: '编辑记忆变量'
...@@ -460,3 +481,9 @@ personal_settings_module: ...@@ -460,3 +481,9 @@ personal_settings_module:
please_enter_the_correct_verification_code: '请输入正确验证码' please_enter_the_correct_verification_code: '请输入正确验证码'
binding_successful: '绑定成功' binding_successful: '绑定成功'
obtaining_the_verification_code: '获取验证码方式' obtaining_the_verification_code: '获取验证码方式'
upload_file: '上传文件'
file_type_restriction_doc: '只支持.pdf, .txt,.md,.doc,.docx文件类型'
the_file_type_cannot_be_uploaded: '暂不支持上传该文件类型'
equity_Module:
file_empty_tip: '文件内容不能为空'
...@@ -90,6 +90,21 @@ common_module: ...@@ -90,6 +90,21 @@ common_module:
bind: '綁定' bind: '綁定'
sms: '短信' sms: '短信'
verificationCode: '驗證碼' verificationCode: '驗證碼'
role: '角色'
mandarin: '普通話'
cantonese: '粵語'
english: '英語'
voice: '語音'
sound: '聲音'
voice_auto_play: '語音自動播放'
start_playing: '開始播放'
stop_playing: '停止播放'
unplayable: '不可播放'
unplayable_tip: '語音設置與模型輸出語言不匹配'
response_error: '響應錯誤'
agent_exception: '應用異常,請稍後重試!'
equity: '权益'
file_size_limit: '文件大小不能超過 {size}'
dialogue_module: dialogue_module:
continue_question_message: '你可以繼續提問' continue_question_message: '你可以繼續提問'
...@@ -102,6 +117,8 @@ common_module: ...@@ -102,6 +117,8 @@ common_module:
cancel_associate_file_tip: '不再圍繞這個文件回答' cancel_associate_file_tip: '不再圍繞這個文件回答'
upload_file_limit: '僅支持上傳單個文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB' upload_file_limit: '僅支持上傳單個文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB'
overwrite_file_tip: '新上傳的文件會覆蓋原有文件,是否繼續上傳' overwrite_file_tip: '新上傳的文件會覆蓋原有文件,是否繼續上傳'
stop_playing_and_then_operate: '音頻播放中,請停止播放後再操作'
do_not_operate_until_the_reply_is_complete: '回覆完成後再操作'
data_table_module: data_table_module:
action: '操作' action: '操作'
...@@ -279,6 +296,10 @@ personal_space_module: ...@@ -279,6 +296,10 @@ personal_space_module:
memory_fragment_delete_row_tip_content: '數據删除後不可撤銷,確定要删除嗎?' memory_fragment_delete_row_tip_content: '數據删除後不可撤銷,確定要删除嗎?'
add_knowledge_successfully: '數據集 {0} 添加成功' add_knowledge_successfully: '數據集 {0} 添加成功'
remove_knowledge_successfully: '數據集 {0} 移除成功' remove_knowledge_successfully: '數據集 {0} 移除成功'
setting_voice: '設置語音'
setting_voice_message: '你可以設置語言和音色,若所選語言與模型語言不一致,則無法播放語音'
setting_voice_desc: '您可自定義語音及音色,用於語音播報,且每次僅可設置一種。'
currently_only_one_voice_can_be_set: '當前僅可設置一種聲音'
memory_variable_modal: memory_variable_modal:
edit_memory_variable: '編輯記憶變數' edit_memory_variable: '編輯記憶變數'
...@@ -460,3 +481,9 @@ personal_settings_module: ...@@ -460,3 +481,9 @@ personal_settings_module:
please_enter_the_correct_verification_code: '請輸入正確驗證碼' please_enter_the_correct_verification_code: '請輸入正確驗證碼'
binding_successful: '綁定成功' binding_successful: '綁定成功'
obtaining_the_verification_code: '獲取驗證碼方式' obtaining_the_verification_code: '獲取驗證碼方式'
upload_file: '上傳文件'
file_type_restriction_doc: '只支持.pdf, .txt,.md,.doc,.docx文件類型'
the_file_type_cannot_be_uploaded: '暫不支持上傳該文件類型'
equity_Module:
file_empty_tip: '文件內容不能為空'
...@@ -32,6 +32,10 @@ export function defaultPersonalAppConfigState(): PersonalAppConfigState { ...@@ -32,6 +32,10 @@ export function defaultPersonalAppConfigState(): PersonalAppConfigState {
communicationTurn: 3, communicationTurn: 3,
temperature: 0.5, temperature: 0.5,
}, },
voiceConfig: {
defaultOpen: 'Y',
timbreId: '',
},
modifiedTime: new Date(), modifiedTime: new Date(),
createdTime: '', createdTime: '',
isCollect: '', isCollect: '',
......
...@@ -40,6 +40,11 @@ export const useSystemLanguageStore = defineStore('system-language-store', { ...@@ -40,6 +40,11 @@ export const useSystemLanguageStore = defineStore('system-language-store', {
languageOptions: defaultLanguageOptions, languageOptions: defaultLanguageOptions,
}), }),
getters: {
currentLanguage(state): I18n.LangType {
return state.currentLanguageInfo.key
},
},
actions: { actions: {
updateCurrentLanguageInfo(key: I18n.LangType) { updateCurrentLanguageInfo(key: I18n.LangType) {
if (this.currentLanguageInfo.key === key) return '' if (this.currentLanguageInfo.key === key) return ''
......
...@@ -36,6 +36,10 @@ export interface PersonalAppConfigState { ...@@ -36,6 +36,10 @@ export interface PersonalAppConfigState {
communicationTurn: number //参考对话轮次 0-100 communicationTurn: number //参考对话轮次 0-100
temperature: number //多样性 0-1.00 temperature: number //多样性 0-1.00
} }
voiceConfig: {
defaultOpen: 'Y' | 'N' //是否默认开启 Y-开启 N-关闭
timbreId: string //音色ID
}
popularity?: number popularity?: number
modifiedTime: Date modifiedTime: Date
createdTime: string createdTime: string
......
import Bowser from 'bowser'
export function validBrowser() {
const browser = Bowser.getParser(window.navigator.userAgent)
return browser.satisfies({
android: {
chrome: '> 80',
},
})
}
import i18n from '@/locales'
const { t } = i18n.global
export default class WebSocketCtr {
private socket: WebSocket | null = null
private readonly url: string
public isDisconnect: boolean = true
constructor(url: string) {
this.url = url
}
private initSocket(callBack?: () => void) {
const socket = new WebSocket(this.url)
socket.onopen = () => {
this.isDisconnect = false
this.onConnect()
callBack && callBack()
}
socket.onmessage = (event: any) => {
let data: any = { final: false }
try {
data = JSON.parse(event.data)
} catch (err) {
window.$message.error(t('common_module.response_error'))
this.onMessageError()
}
if (data.code && data.code !== '0') {
if (this.socket) {
this.socket.close()
this.socket = null
}
this.onMessageError()
return
}
this.onMessage(data)
}
socket.onerror = (event) => {
this.onError(event)
}
socket.onclose = () => {
this.isDisconnect = true
this.onDisconnect()
}
this.socket = socket
}
connect(callBack?: () => void) {
if (this.socket && [0, 1].includes(this.socket.readyState)) {
callBack && callBack()
return
}
this.initSocket(callBack)
}
send(content: { [k: string]: any }) {
this.socket && this.socket.send(JSON.stringify(content))
}
disconnect() {
if (this.socket) {
this.socket.close()
this.socket = null
}
this.onDisconnect()
}
/* call back */
onConnect() {}
onMessage(_message: any) {}
onMessageError() {}
onError(_event?: any) {}
onDisconnect() {}
}
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, ref, toValue } from 'vue' import { computed, nextTick, ref, shallowRef, toValue, useTemplateRef } from 'vue'
import type { AgentApplicationRecordItem, MessageItemInterface } from '../types' import type { AgentApplicationRecordItem, MessageItemInterface } from '../types'
import { fetchAgentApplicationSelectList } from '@/apis/home-agent' import { fetchAgentApplicationSelectList, fetchFileUpload } from '@/apis/home-agent'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import fetchEventStreamSource from '../utils/fetch-event-stream-source' import fetchEventStreamSource from '../utils/fetch-event-stream-source'
import { throttle } from 'lodash-es' import { throttle } from 'lodash-es'
...@@ -31,13 +31,24 @@ const currentFetchEventSourceController = defineModel<AbortController | null>('c ...@@ -31,13 +31,24 @@ const currentFetchEventSourceController = defineModel<AbortController | null>('c
const { t } = useI18n() const { t } = useI18n()
const inputFileRef = useTemplateRef<HTMLInputElement | null>('inputFileRef')
let fileUploadController = shallowRef<AbortController | null>(null)
const isShowApplicationSelectMenu = ref(false) const isShowApplicationSelectMenu = ref(false)
const agentApplicationSelectList = ref<AgentApplicationRecordItem[]>([]) const agentApplicationSelectList = ref<AgentApplicationRecordItem[]>([])
const currentLatestMessageItemKeyMap = ref(new Map<'assistant' | 'user', string>()) const currentLatestMessageItemKeyMap = ref(new Map<'assistant' | 'user', string>())
const currentInputFileInfo = ref({
url: '',
fileName: '',
percentage: 5,
uploading: false,
})
const isQuestionSubmitBtnDisabled = computed(() => { const isQuestionSubmitBtnDisabled = computed(() => {
return questionContent.value.trim().length === 0 || isAgentResponding.value return questionContent.value.trim().length === 0 || isAgentResponding.value || currentInputFileInfo.value.uploading
}) })
;(function () { ;(function () {
...@@ -150,6 +161,7 @@ function questionSubmit() { ...@@ -150,6 +161,7 @@ function questionSubmit() {
dialogsId: props.currentSessionId, //会话ID dialogsId: props.currentSessionId, //会话ID
agentId: currentAgentApplication.value.agentId, //应用ID agentId: currentAgentApplication.value.agentId, //应用ID
input: questionContent.value.trim(), //提问文本 input: questionContent.value.trim(), //提问文本
fileUrls: currentInputFileInfo.value.url ? [currentInputFileInfo.value.url] : [],
}, },
{ {
onmessage: (message) => { onmessage: (message) => {
...@@ -177,7 +189,9 @@ function questionSubmit() { ...@@ -177,7 +189,9 @@ function questionSubmit() {
emit('historyRecordListUpdate') emit('historyRecordListUpdate')
}) })
}, },
onerror: () => { onerror: (err) => {
err.message && window.$message.error(err.message)
emit('deleteMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!) emit('deleteMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!)
}, },
}, },
...@@ -190,11 +204,85 @@ function handleQuestionSubmitEnter(event: KeyboardEvent) { ...@@ -190,11 +204,85 @@ function handleQuestionSubmitEnter(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) { if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault() event.preventDefault()
if (questionContent.value.trim().length > 0) { if (questionContent.value.trim().length > 0 && !isAgentResponding.value && !currentInputFileInfo.value.uploading) {
questionSubmit() questionSubmit()
} }
} }
} }
function handleFileUploadPopup() {
inputFileRef.value && inputFileRef.value.click()
}
function handleFileUpload(e: Event) {
const target = e.target as HTMLInputElement
const file = target.files && target.files[0]
if (file) {
if (!['.pdf', '.txt', '.md', '.doc', '.docx'].some((fileType) => file.name.endsWith(fileType))) {
window.$message.warning(t('personal_settings_module.the_file_type_cannot_be_uploaded'))
return
}
if (file.size === 0) {
window.$message.warning(t('equity_Module.file_empty_tip'))
return
} else if (file.size > 1024 * 1024 * 10) {
window.$message.warning(t('common_module.file_size_limit', { size: '10MB' }))
return
}
currentInputFileInfo.value = {
fileName: file.name,
url: URL.createObjectURL(file),
percentage: 5,
uploading: true,
}
fileUploadController.value && fileUploadController.value.abort()
fileUploadController.value = new AbortController()
const formData = new FormData()
formData.append('file', file)
fetchFileUpload<string>(formData, {
onUploadProgress: (progressEvent) => {
const percentage = Number.parseInt((progressEvent?.progress || 0).toFixed(2)) * 100
currentInputFileInfo.value.percentage = percentage < 5 ? 5 : percentage
if (progressEvent?.progress === 1) {
currentInputFileInfo.value.uploading = false
}
},
signal: fileUploadController.value.signal,
}).then((res) => {
currentInputFileInfo.value.url = res.data
})
}
}
function handleFileUploadCancel() {
currentInputFileInfo.value = {
url: '',
fileName: '',
percentage: 0,
uploading: false,
}
inputFileRef.value && (inputFileRef.value.value = '')
if (fileUploadController.value) {
fileUploadController.value.abort()
fileUploadController.value = null
}
}
function handleFileUploadReplace() {
handleFileUploadPopup()
}
</script> </script>
<template> <template>
...@@ -281,12 +369,77 @@ function handleQuestionSubmitEnter(event: KeyboardEvent) { ...@@ -281,12 +369,77 @@ function handleQuestionSubmitEnter(event: KeyboardEvent) {
</Transition> </Transition>
</div> </div>
<n-button class="application-select-btn !h-[34px] !rounded-[10px] !p-0" @click="handleCreateNewSession"> <div class="flex items-center">
<div class="box-border flex w-full items-center justify-between px-[12px]"> <Transition name="file-upload" mode="out-in">
<i class="iconfont icon-session mr-[5px] text-[14px]"></i> <div v-if="!currentInputFileInfo.fileName">
<span class="text-[14px]">{{ t('home_module.starting_a_new_session') }}</span> <n-popover trigger="hover">
</div> <template #trigger>
</n-button> <n-button
class="application-select-btn !mr-[14px] !h-[34px] !rounded-[10px] !p-0"
@click="handleFileUploadPopup"
>
<div class="box-border flex w-full items-center justify-between px-[12px]">
<i class="iconfont icon-upload mr-[5px] text-[14px]"></i>
<span class="text-[14px]">{{ t('personal_settings_module.upload_file') }}</span>
</div>
</n-button>
</template>
<span>{{ t('personal_settings_module.file_type_restriction_doc') }}</span>
</n-popover>
</div>
<div v-else class="relative !mr-[14px] flex !h-[34px] items-center rounded-[10px] bg-[#ECEFFF] px-[14px]">
<div
class="mr-[5px] h-[18px] w-[18px] bg-[url('https://gsst-poe-sit.gz.bcebos.com/icon/doc.svg')] bg-contain bg-no-repeat"
></div>
<div class="w-[260px] text-[14px]">
<n-ellipsis :tooltip="{ width: 400 }">
{{ currentInputFileInfo.fileName }}
</n-ellipsis>
</div>
<div class="ml-[10px]">
<i
class="iconfont icon-huanyihuan hover:text-theme-color mr-[10px] cursor-pointer text-[14px] transition"
@click="handleFileUploadReplace"
></i>
<i
class="iconfont icon-close hover:text-theme-color cursor-pointer text-[14px] transition"
@click="handleFileUploadCancel"
></i>
</div>
<div v-show="currentInputFileInfo.uploading" class="absolute bottom-[1px] left-[40px] w-[86%]">
<n-progress
type="line"
:percentage="currentInputFileInfo.percentage"
:show-indicator="false"
processing
:fill-border-radius="0"
:border-radius="0"
:height="3"
rail-color="#DCDCDC"
/>
</div>
</div>
</Transition>
<n-button class="application-select-btn !h-[34px] !rounded-[10px] !p-0" @click="handleCreateNewSession">
<div class="box-border flex w-full items-center justify-between px-[12px]">
<i class="iconfont icon-session mr-[5px] text-[14px]"></i>
<span class="text-[14px]">{{ t('home_module.starting_a_new_session') }}</span>
</div>
</n-button>
<input
ref="inputFileRef"
type="file"
class="hidden"
accept=".pdf,.txt,.md,.doc,.docx"
@change="handleFileUpload"
/>
</div>
</div> </div>
<div> <div>
...@@ -340,4 +493,16 @@ function handleQuestionSubmitEnter(event: KeyboardEvent) { ...@@ -340,4 +493,16 @@ function handleQuestionSubmitEnter(event: KeyboardEvent) {
opacity: 0; opacity: 0;
scale: 0.8; scale: 0.8;
} }
.file-upload-enter-active,
.file-upload-leave-active {
transition-timing-function: ease-in-out;
transition-duration: 0.2s;
transition-property: opacity;
}
.file-upload-enter-from,
.file-upload-leave-to {
opacity: 0;
}
</style> </style>
...@@ -166,7 +166,7 @@ defineExpose({ ...@@ -166,7 +166,7 @@ defineExpose({
<template> <template>
<Transition name="history-menu"> <Transition name="history-menu">
<div v-show="isShowHistoryMenu" class="absolute bottom-0 right-[24px] top-0 z-10 py-[24px]"> <div v-show="isShowHistoryMenu" class="absolute bottom-0 right-[24px] top-0 z-10 py-[24px]">
<div class="box-border h-full w-[249px] rounded-[10px] bg-[#ECEFFF] py-[15px] pl-[10px]"> <div class="box-border h-full w-[269px] rounded-[10px] bg-[#ECEFFF] py-[15px] pl-[10px]">
<n-scrollbar> <n-scrollbar>
<div class="pr-[10px]"> <div class="pr-[10px]">
<!-- <div class="select-none"> <!-- <div class="select-none">
...@@ -263,7 +263,7 @@ defineExpose({ ...@@ -263,7 +263,7 @@ defineExpose({
</Transition> </Transition>
<button <button
class="flex-center absolute right-[278px] top-1/2 z-10 h-[24px] w-[24px] rounded-full bg-[#ECEFFF] transition-[right] duration-300 ease-in-out" class="flex-center absolute right-[298px] top-1/2 z-10 h-[24px] w-[24px] rounded-full bg-[#ECEFFF] transition-[right] duration-300 ease-in-out"
:class="[isShowHistoryMenu ? [] : ['!right-[5px]']]" :class="[isShowHistoryMenu ? [] : ['!right-[5px]']]"
@click="handleShowHistoryMenuSwitch" @click="handleShowHistoryMenuSwitch"
> >
......
...@@ -57,9 +57,9 @@ const currentFetchEventSourceController = ref<AbortController | null>(null) ...@@ -57,9 +57,9 @@ const currentFetchEventSourceController = ref<AbortController | null>(null)
// }) // })
const homeContainerWidthWatchDebounce = debounce((newWidth) => { const homeContainerWidthWatchDebounce = debounce((newWidth) => {
if (newWidth <= 1060) { if (newWidth <= 1120) {
isShowHistoryMenu.value = false isShowHistoryMenu.value = false
} else if (newWidth >= 1300) { } else if (newWidth >= 1320) {
isShowHistoryMenu.value = true isShowHistoryMenu.value = true
} }
}, 300) }, 300)
...@@ -201,7 +201,7 @@ function onGetMessageRecordList(recordId: string) { ...@@ -201,7 +201,7 @@ function onGetMessageRecordList(recordId: string) {
<div ref="homeContainerRef" class="relative h-full min-h-[650px] w-full"> <div ref="homeContainerRef" class="relative h-full min-h-[650px] w-full">
<div <div
class="bg-px-home-home_bg-png relative h-full w-full bg-contain bg-center bg-no-repeat pr-0 transition-[padding] duration-300 ease-in-out" class="bg-px-home-home_bg-png relative h-full w-full bg-contain bg-center bg-no-repeat pr-0 transition-[padding] duration-300 ease-in-out"
:class="{ '!pr-[273px]': isShowHistoryMenu }" :class="{ '!pr-[293px]': isShowHistoryMenu }"
> >
<div class="mx-auto flex h-full w-[750px] flex-col px-[5px] py-[40px]"> <div class="mx-auto flex h-full w-[750px] flex-col px-[5px] py-[40px]">
<AgentAbout <AgentAbout
......
...@@ -50,6 +50,9 @@ export default function fetchEventStreamSource( ...@@ -50,6 +50,9 @@ export default function fetchEventStreamSource(
data.message && options.onmessage && options.onmessage(data.message) data.message && options.onmessage && options.onmessage(data.message)
} else { } else {
options.onerror && options.onerror(new Error(data.message)) options.onerror && options.onerror(new Error(data.message))
controller.abort()
options.onclose && options.onclose()
} }
} catch (error) { } catch (error) {
options.onerror && options.onerror(error as Error) options.onerror && options.onerror(error as Error)
......
...@@ -424,16 +424,13 @@ function handleEmailCodeGain() { ...@@ -424,16 +424,13 @@ function handleEmailCodeGain() {
<div class="ml-[6px] mr-[10px] h-[18px] w-[1px] bg-[#868686]"></div> <div class="ml-[6px] mr-[10px] h-[18px] w-[1px] bg-[#868686]"></div>
<div class="text-end"> <div class="text-end">
<n-button <button
v-show="!isShowCountdown" v-show="!isShowCountdown"
class="!text-[11px]" class="cursor-pointer text-[11px] text-[#333] transition active:text-[#6A6A6A]"
type="tertiary"
size="small"
@click="handleSMSCodeGain" @click="handleSMSCodeGain"
> >
{{ t('login_module.get_verification_code') }} {{ t('login_module.get_verification_code') }}
</n-button> </button>
<div v-show="isShowCountdown" class="inline-block w-[50px] text-center"> <div v-show="isShowCountdown" class="inline-block w-[50px] text-center">
<n-countdown <n-countdown
ref="countdownRef" ref="countdownRef"
...@@ -498,15 +495,13 @@ function handleEmailCodeGain() { ...@@ -498,15 +495,13 @@ function handleEmailCodeGain() {
<div class="ml-[6px] mr-[10px] h-[18px] w-[1px] bg-[#868686]"></div> <div class="ml-[6px] mr-[10px] h-[18px] w-[1px] bg-[#868686]"></div>
<div class="text-end"> <div class="text-end">
<n-button <button
v-show="!isShowCountdown" v-show="!isShowCountdown"
class="!text-[11px]" class="cursor-pointer text-[11px] text-[#333] transition active:text-[#6A6A6A]"
type="tertiary"
size="small"
@click="handleEmailCodeGain" @click="handleEmailCodeGain"
> >
{{ t('login_module.get_verification_code') }} {{ t('login_module.get_verification_code') }}
</n-button> </button>
<div v-show="isShowCountdown" class="inline-block w-[50px] text-center"> <div v-show="isShowCountdown" class="inline-block w-[50px] text-center">
<n-countdown <n-countdown
......
<script setup lang="ts"> <script setup lang="ts">
import { fetchEmailCode, fetchUserInfoUpdate, fetchVerifyCode } from '@/apis/user'
import { useUserStore } from '@/store/modules/user'
import { ss } from '@/utils/storage'
import type { CountdownInst, FormInst, FormItemRule } from 'naive-ui' import type { CountdownInst, FormInst, FormItemRule } from 'naive-ui'
import isEmail from 'validator/es/lib/isEmail'
import { onMounted, ref, shallowReadonly, useTemplateRef } from 'vue' import { onMounted, ref, shallowReadonly, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/store/modules/user'
import isEmail from 'validator/es/lib/isEmail'
import { fetchEmailCode, fetchUserInfoUpdate, fetchVerifyCode } from '@/apis/user'
import { ss } from '@/utils/storage'
const { t } = useI18n() const { t } = useI18n()
const userStore = useUserStore() const userStore = useUserStore()
const emailInfoFormRef = useTemplateRef<FormInst>('emailInfoFormRef') const emailInfoFormRef = useTemplateRef<FormInst>('emailInfoFormRef')
const countdownRef = useTemplateRef<CountdownInst>('countdownRef') const countdownRef = useTemplateRef<CountdownInst>('countdownRef')
...@@ -145,6 +146,7 @@ function handleSMSCodeGain() { ...@@ -145,6 +146,7 @@ function handleSMSCodeGain() {
:bordered="false" :bordered="false"
size="medium" size="medium"
closable closable
content-style="margin-top: 20px;"
@close="() => (isShowMailboxBindingModal = false)" @close="() => (isShowMailboxBindingModal = false)"
> >
<n-form <n-form
...@@ -153,16 +155,13 @@ function handleSMSCodeGain() { ...@@ -153,16 +155,13 @@ function handleSMSCodeGain() {
label-width="auto" label-width="auto"
:model="emailInfoForm" :model="emailInfoForm"
:rules="emailInfoFormRules" :rules="emailInfoFormRules"
size="large"
> >
<n-form-item :label="t('common_module.email')" path="email"> <n-form-item :label="t('common_module.email')" path="email">
<n-input v-model:value="emailInfoForm.email" :placeholder="t('login_module.please_enter_your_email_address')"> <n-input v-model:value="emailInfoForm.email" :placeholder="t('login_module.please_enter_your_email_address')">
<template #suffix> <template #suffix>
<span class="mx-[10px] inline-block h-[50%] w-[2px] bg-[#e0e0e6]"></span> <span class="mx-[10px] inline-block h-[50%] w-[2px] bg-[#e0e0e6]"></span>
<span <span v-show="!isShowCountdown" class="cursor-pointer text-[12px] text-[#333]" @click="handleSMSCodeGain">
v-show="!isShowCountdown"
class="text-theme-color cursor-pointer text-[12px]"
@click="handleSMSCodeGain"
>
{{ t('login_module.get_verification_code') }} {{ t('login_module.get_verification_code') }}
</span> </span>
...@@ -190,10 +189,16 @@ function handleSMSCodeGain() { ...@@ -190,10 +189,16 @@ function handleSMSCodeGain() {
<template #footer> <template #footer>
<div class="text-end"> <div class="text-end">
<n-space justify="end"> <n-space justify="end">
<n-button @click="() => (isShowMailboxBindingModal = false)"> <n-button class="!h-[34px] !w-[96px]" round @click="() => (isShowMailboxBindingModal = false)">
{{ t('common_module.cancel_btn_text') }} {{ t('common_module.cancel_btn_text') }}
</n-button> </n-button>
<n-button type="primary" :loading="mailboxBindingSubmitBtnLoading" @click="handleMailboxBindingSubmit"> <n-button
class="!h-[34px] !w-[96px]"
color="#6F77FF"
round
:loading="mailboxBindingSubmitBtnLoading"
@click="handleMailboxBindingSubmit"
>
{{ t('common_module.confirm_btn_text') }} {{ t('common_module.confirm_btn_text') }}
</n-button> </n-button>
</n-space> </n-space>
......
<script setup lang="ts"> <script setup lang="ts">
import { fetchEmailCode, fetchSMSCode, fetchUserPasswordUpdate, fetchVerifyCode } from '@/apis/user' import { fetchEmailCode, fetchSMSCode, fetchUserPasswordUpdate, fetchVerifyCode } from '@/apis/user'
import { useSystemLanguageStore } from '@/store/modules/system-language'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { ss } from '@/utils/storage' import { ss } from '@/utils/storage'
import type { CountdownInst, FormInst, FormItemRule } from 'naive-ui' import type { CountdownInst, FormInst, FormItemRule } from 'naive-ui'
...@@ -8,9 +9,11 @@ import { onMounted, ref, shallowReadonly, useTemplateRef, watchEffect } from 'vu ...@@ -8,9 +9,11 @@ import { onMounted, ref, shallowReadonly, useTemplateRef, watchEffect } from 'vu
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { t } = useI18n() const { t } = useI18n()
const userStore = useUserStore()
const systemLanguageStore = useSystemLanguageStore()
const passwordInfoFormRef = useTemplateRef<FormInst>('passwordInfoFormRef') const passwordInfoFormRef = useTemplateRef<FormInst>('passwordInfoFormRef')
const countdownRef = useTemplateRef<CountdownInst>('countdownRef') const countdownRef = useTemplateRef<CountdownInst>('countdownRef')
const userStore = useUserStore()
const isShowPasswordChangeModal = defineModel<boolean>('isShowPasswordChangeModal', { default: false }) const isShowPasswordChangeModal = defineModel<boolean>('isShowPasswordChangeModal', { default: false })
...@@ -51,7 +54,7 @@ const passwordFormRules = shallowReadonly({ ...@@ -51,7 +54,7 @@ const passwordFormRules = shallowReadonly({
validator: (_rule: FormItemRule, value: string) => { validator: (_rule: FormItemRule, value: string) => {
if (!value) { if (!value) {
return new Error(t('personal_settings_module.please_enter_your_new_password')) return new Error(t('personal_settings_module.please_enter_your_new_password'))
} else if (value.length <= 6) { } else if (value.length < 6) {
return new Error(t('personal_settings_module.the_password_contains_a_maximum_of_6_characters')) return new Error(t('personal_settings_module.the_password_contains_a_maximum_of_6_characters'))
} }
...@@ -186,11 +189,12 @@ function handleSMSCodeGain() { ...@@ -186,11 +189,12 @@ function handleSMSCodeGain() {
<template> <template>
<n-modal v-model:show="isShowPasswordChangeModal" :mask-closable="false" :on-after-leave="onModalAfterLeave"> <n-modal v-model:show="isShowPasswordChangeModal" :mask-closable="false" :on-after-leave="onModalAfterLeave">
<n-card <n-card
class="!w-[600px]" :class="systemLanguageStore.currentLanguage === 'en' ? '!w-[700px]' : '!w-[600px]'"
:title="t('personal_settings_module.password_change')" :title="t('personal_settings_module.password_change')"
:bordered="false" :bordered="false"
size="medium" size="medium"
closable closable
content-style="margin-top: 20px;"
@close="() => (isShowPasswordChangeModal = false)" @close="() => (isShowPasswordChangeModal = false)"
> >
<n-form <n-form
...@@ -199,6 +203,7 @@ function handleSMSCodeGain() { ...@@ -199,6 +203,7 @@ function handleSMSCodeGain() {
label-width="auto" label-width="auto"
:model="passwordInfoForm" :model="passwordInfoForm"
:rules="passwordFormRules" :rules="passwordFormRules"
size="large"
> >
<n-form-item :label="t('personal_settings_module.obtaining_the_verification_code')"> <n-form-item :label="t('personal_settings_module.obtaining_the_verification_code')">
<div> <div>
...@@ -265,10 +270,16 @@ function handleSMSCodeGain() { ...@@ -265,10 +270,16 @@ function handleSMSCodeGain() {
<template #footer> <template #footer>
<div class="text-end"> <div class="text-end">
<n-space justify="end"> <n-space justify="end">
<n-button @click="() => (isShowPasswordChangeModal = false)"> <n-button class="!h-[34px] !w-[96px]" round @click="() => (isShowPasswordChangeModal = false)">
{{ t('common_module.cancel_btn_text') }} {{ t('common_module.cancel_btn_text') }}
</n-button> </n-button>
<n-button type="primary" :loading="passwordChangeSubmitBtnLoading" @click="handlePasswordChangeSubmit"> <n-button
class="!h-[34px] !w-[96px]"
color="#6F77FF"
round
:loading="passwordChangeSubmitBtnLoading"
@click="handlePasswordChangeSubmit"
>
{{ t('common_module.confirm_btn_text') }} {{ t('common_module.confirm_btn_text') }}
</n-button> </n-button>
</n-space> </n-space>
......
...@@ -175,13 +175,15 @@ function handleUserInfoFormItemEditUpdate(key: keyof typeof userInfoFormItemEdit ...@@ -175,13 +175,15 @@ function handleUserInfoFormItemEditUpdate(key: keyof typeof userInfoFormItemEdit
</h4> </h4>
<div v-if="userInfoFormItemEdit.nickName" class="flex"> <div v-if="userInfoFormItemEdit.nickName" class="flex">
<div class="flex w-[220px] items-center"> <div class="flex w-[330px] items-center">
<n-input <n-input
ref="inputRefs" ref="inputRefs"
v-model:value="userInfoForm.nickName" v-model:value="userInfoForm.nickName"
:placeholder="t('personal_settings_module.please_enter_the_account_nickname')" :placeholder="t('personal_settings_module.please_enter_the_account_nickname')"
type="text" type="text"
size="small" size="small"
maxlength="20"
show-count
/> />
</div> </div>
......
<script setup lang="ts">
import AgentSetting from './agent-setting/agent-setting.vue'
import AgentPreview from './agent-preview/agent-preview.vue'
</script>
<template>
<div class="flex h-full w-full flex-1">
<AgentSetting />
<AgentPreview />
</div>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { inject, onMounted, onUnmounted, ref } from 'vue' import { computed, inject, onMounted, onUnmounted, ref, shallowRef } from 'vue'
import Preamble from './preamble.vue' import Preamble from './components/preamble.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Emitter } from 'mitt' import { Emitter } from 'mitt'
import MessageList from './message-list.vue' import { Howl } from 'howler'
import FooterInput from './footer-input.vue' import { ValueOf } from 'type-fest'
import MessageList from './components/message-list.vue'
import FooterInput from './components/footer-input.vue'
import MemoryPreviewModal from './components/memory-preview-modal.vue'
import { fetchCreateContinueQuestions } from '@/apis/agent-application' import { fetchCreateContinueQuestions } from '@/apis/agent-application'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config' import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { useSystemLanguageStore } from '@/store/modules/system-language'
import { Brain, Down } from '@icon-park/vue-next' import { Brain, Down } from '@icon-park/vue-next'
import MemoryPreviewModal from './memory-preview-modal.vue' import { validBrowser } from '@/utils/browser-detection'
const { t } = useI18n() const { t } = useI18n()
const router = useRouter() const router = useRouter()
const personalAppConfigStore = usePersonalAppConfigStore() const personalAppConfigStore = usePersonalAppConfigStore()
const systemLanguageStore = useSystemLanguageStore()
const emitter = inject<Emitter<MittEvents>>('emitter') const emitter = inject<Emitter<MittEvents>>('emitter')
const messageListRef = ref<InstanceType<typeof MessageList> | null>(null) const messageListRef = ref<InstanceType<typeof MessageList> | null>(null)
const footerInputRef = ref<InstanceType<typeof FooterInput> | null>(null) const footerInputRef = ref<InstanceType<typeof FooterInput> | null>(null)
const messageList = ref<ConversationMessageItem[]>([]) const messageList = ref(new Map<string, ConversationMessageItem>())
const continuousQuestionStatus = ref<'default' | 'close'>(personalAppConfigStore.commConfig.continuousQuestionStatus) const continuousQuestionStatus = ref<'default' | 'close'>(personalAppConfigStore.commConfig.continuousQuestionStatus)
const continuousQuestionList = ref<string[]>([]) const continuousQuestionList = ref<string[]>([])
const isShowMemoryPreviewModal = ref(false) const isShowMemoryPreviewModal = ref(false)
const selectedMemoryTabName = ref('memoryVariable') const selectedMemoryTabName = ref('memoryVariable')
const answerAudioAutoPlay = ref(personalAppConfigStore.voiceConfig.defaultOpen === 'Y')
const answerAudioPlaying = ref(false) // 语音播放中
const currentPlayMessageItem = ref<ConversationMessageItem | null>(null)
const currentPlayAudioFragmentSerialNo = ref(0)
const currentSoundCtl = shallowRef<Howl | null>(null)
const isSoundCtlCreated = ref(false)
const isEnLanguage = computed(() => {
return systemLanguageStore.currentLanguageInfo.key === 'en'
})
onMounted(() => { onMounted(() => {
emitter?.on('resetAgent', () => { emitter?.on('resetAgent', () => {
handleAudioPause()
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
messageList.value = [] messageList.value.clear()
}) })
}) })
onUnmounted(() => { onUnmounted(() => {
emitter?.off('resetAgent') emitter?.off('resetAgent')
handleAudioPause()
footerInputRef.value?.blockMessageResponse()
messageList.value.clear()
}) })
function handleAddMessageItem(messageItem: ConversationMessageItem) { function handleAddMessageItem(messageId: string, messageItem: ConversationMessageItem) {
messageList.value.push(messageItem) messageList.value.set(messageId, messageItem)
} }
function handleUpdateSpecifyMessageItem(messageItemIndex: number, newObj: Partial<ConversationMessageItem>) { function handleUpdateSpecifyMessageItem(messageId: string, newMessageItem: Partial<ConversationMessageItem>) {
if (messageList.value[messageItemIndex]) { const currentMessageItemInfo = messageList.value.get(messageId)
Object.entries(newObj).forEach(([k, v]) => {
;(messageList.value[messageItemIndex] as any)[k as keyof typeof newObj] = v if (currentMessageItemInfo) {
const updatePropertyLength = Object.keys(newMessageItem).length
if (updatePropertyLength > 4) {
messageList.value.set(messageId, Object.assign({}, currentMessageItemInfo, newMessageItem))
return
}
Object.entries<ValueOf<typeof newMessageItem>>(newMessageItem).forEach(([key, value]) => {
if (Object.prototype.hasOwnProperty.call(currentMessageItemInfo, key)) {
;(currentMessageItemInfo as any)[key as keyof ConversationMessageItem] = value
}
}) })
} }
} }
function handleDeleteLastMessageItem() { function handleDeleteMessageItem(messageId: string) {
messageList.value.pop() messageList.value.delete(messageId)
} }
function handleUpdatePageScroll() { function handleUpdatePageScroll() {
...@@ -68,8 +98,10 @@ function handleClearAllMessage() { ...@@ -68,8 +98,10 @@ function handleClearAllMessage() {
t('common_module.dialogue_module.clear_message_dialog_title'), t('common_module.dialogue_module.clear_message_dialog_title'),
) )
.then(() => { .then(() => {
handleAudioPause()
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
messageList.value = [] messageList.value.clear()
answerAudioPlaying.value = false
window.$message.success(t('common_module.clear_success_message')) window.$message.success(t('common_module.clear_success_message'))
}) })
} }
...@@ -88,6 +120,10 @@ function handleUpdateContinueQuestionStatus(status: 'default' | 'close') { ...@@ -88,6 +120,10 @@ function handleUpdateContinueQuestionStatus(status: 'default' | 'close') {
continuousQuestionList.value = [] continuousQuestionList.value = []
} }
function handleUpdateAudioAutoPlaying(isAutoPlaying: boolean) {
personalAppConfigStore.voiceConfig.defaultOpen = isAutoPlaying ? 'Y' : 'N'
}
function handleTurnMultiModelDialogue() { function handleTurnMultiModelDialogue() {
if (!personalAppConfigStore.baseInfo.agentId) return if (!personalAppConfigStore.baseInfo.agentId) return
...@@ -103,6 +139,94 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) { ...@@ -103,6 +139,94 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) {
selectedMemoryTabName.value = MemoryTabName selectedMemoryTabName.value = MemoryTabName
isShowMemoryPreviewModal.value = true isShowMemoryPreviewModal.value = true
} }
function howlSoundFactory(url: string) {
const soundCtl = new Howl({
src: [url],
format: ['mpeg'],
html5: !validBrowser(),
preload: true,
autoplay: true,
onplay: () => {
answerAudioPlaying.value = true
currentSoundCtl.value = soundCtl
if (currentPlayMessageItem.value) {
currentPlayMessageItem.value.isVoiceLoading = false
currentPlayMessageItem.value.isVoicePlaying = true
}
},
onend: () => {
soundCtl.unload()
currentPlayAudioFragmentSerialNo.value += 1
if (
currentPlayMessageItem.value &&
currentPlayAudioFragmentSerialNo.value > currentPlayMessageItem.value.voiceFragmentUrlList.length - 1
) {
currentPlayMessageItem.value.isVoicePlaying = false
answerAudioPlaying.value = false
}
let audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value]
if (audioFragmentUrl) {
howlSoundFactory(audioFragmentUrl)
} else if (
(currentPlayMessageItem.value?.voiceFragmentUrlList || []).length > currentPlayAudioFragmentSerialNo.value
) {
let timerId: NodeJS.Timeout | null = null
const audioFragmentCheck = () => {
audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value]
if (audioFragmentUrl) {
timerId && clearInterval(timerId)
howlSoundFactory(audioFragmentUrl)
}
}
timerId = setInterval(audioFragmentCheck, 600)
}
},
})
return soundCtl
}
function handleAudioPlay(currentMessageItem: ConversationMessageItem, specificPlayUrl?: string) {
handleAudioPause()
currentPlayMessageItem.value = currentMessageItem
currentPlayAudioFragmentSerialNo.value = 0
const audioUrl = specificPlayUrl || currentMessageItem.voiceFragmentUrlList[0]
howlSoundFactory(audioUrl)
isSoundCtlCreated.value = true
}
function handleAudioPause(isClearMessageList = false) {
if (currentSoundCtl.value) {
currentSoundCtl.value.pause()
currentSoundCtl.value.unload()
currentSoundCtl.value = null
isSoundCtlCreated.value = false
}
currentPlayMessageItem.value && (currentPlayMessageItem.value.isVoicePlaying = false)
answerAudioPlaying.value = false
if (isClearMessageList) {
messageList.value.clear()
footerInputRef.value?.blockMessageResponse()
}
}
</script> </script>
<template> <template>
...@@ -113,7 +237,23 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) { ...@@ -113,7 +237,23 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) {
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.preview') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.preview') }}
</p> </p>
<div class="flex items-center"> <div class="flex items-center" :class="isEnLanguage ? 'gap-1.5 2xl:gap-4' : 'gap-4'">
<n-popover placement="bottom" trigger="hover" :show-arrow="false">
<template #trigger>
<div v-show="personalAppConfigStore.voiceConfig.timbreId" class="text-font-color cursor-pointer">
<i class="iconfont icon-yuyin mr-1 text-sm" />
<span>{{ t('common_module.voice') }}</span>
</div>
</template>
<div class="flex items-center gap-2.5">
<span>{{ t('common_module.voice_auto_play') }}</span>
<n-switch v-model:value="answerAudioAutoPlay" size="small" @update:value="handleUpdateAudioAutoPlaying">
<template #checked> {{ t('common_module.open') }} </template>
<template #unchecked> {{ t('common_module.close') }} </template>
</n-switch>
</div>
</n-popover>
<div <div
:class=" :class="
personalAppConfigStore.baseInfo.agentId personalAppConfigStore.baseInfo.agentId
...@@ -135,7 +275,7 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) { ...@@ -135,7 +275,7 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) {
> >
<n-popover placement="bottom" trigger="hover" class="p-[4px]!" :show-arrow="false"> <n-popover placement="bottom" trigger="hover" class="p-[4px]!" :show-arrow="false">
<template #trigger> <template #trigger>
<div class="flex items-center justify-center pl-5 text-[14px]"> <div class="flex items-center justify-center text-[14px]">
<Brain theme="outline" size="15" fill="#333" /> <Brain theme="outline" size="15" fill="#333" />
<div class="mx-[4px]"> <div class="mx-[4px]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.memory') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.memory') }}
...@@ -166,16 +306,18 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) { ...@@ -166,16 +306,18 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) {
</div> </div>
<div class="flex w-full flex-1 overflow-hidden"> <div class="flex w-full flex-1 overflow-hidden">
<div v-show="messageList.length === 0" class="flex w-full"> <div v-show="messageList.size === 0" class="flex w-full">
<Preamble /> <Preamble />
</div> </div>
<div v-show="messageList.length > 0" class="w-full"> <div v-show="messageList.size > 0" class="w-full">
<MessageList <MessageList
ref="messageListRef" ref="messageListRef"
:message-list="messageList" :message-list="messageList"
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:continuous-question-list="continuousQuestionList" :continuous-question-list="continuousQuestionList"
@audio-play="handleAudioPlay"
@audio-pause="handleAudioPause"
/> />
</div> </div>
</div> </div>
...@@ -184,13 +326,17 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) { ...@@ -184,13 +326,17 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) {
ref="footerInputRef" ref="footerInputRef"
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:message-list="messageList" :message-list="messageList"
:answer-audio-auto-play="answerAudioAutoPlay"
:answer-audio-playing="answerAudioPlaying"
@add-message-item="handleAddMessageItem" @add-message-item="handleAddMessageItem"
@update-specify-message-item="handleUpdateSpecifyMessageItem" @update-specify-message-item="handleUpdateSpecifyMessageItem"
@delete-last-message-item="handleDeleteLastMessageItem" @delete-message-item="handleDeleteMessageItem"
@update-page-scroll="handleUpdatePageScroll" @update-page-scroll="handleUpdatePageScroll"
@clear-all-message="handleClearAllMessage" @clear-all-message="handleClearAllMessage"
@create-continue-questions="handleCreateContinueQuestions" @create-continue-questions="handleCreateContinueQuestions"
@update-continuous-question-status="handleUpdateContinueQuestionStatus" @update-continuous-question-status="handleUpdateContinueQuestionStatus"
@audio-play="handleAudioPlay"
@audio-pause="handleAudioPause"
/> />
<MemoryPreviewModal v-model="isShowMemoryPreviewModal" :data="selectedMemoryTabName" /> <MemoryPreviewModal v-model="isShowMemoryPreviewModal" :data="selectedMemoryTabName" />
......
...@@ -2,15 +2,20 @@ ...@@ -2,15 +2,20 @@
import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue' import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
import { Emitter } from 'mitt' import { Emitter } from 'mitt'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { nanoid } from 'nanoid'
import OverwriteMessageTipModal from './overwrite-message-tip-modal.vue' import OverwriteMessageTipModal from './overwrite-message-tip-modal.vue'
import { fetchCustomEventSource } from '@/composables/useEventSource' import { fetchCustomEventSource } from '@/composables/useEventSource'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config' import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { UploadStatus } from '@/enums/upload-status' import { UploadStatus } from '@/enums/upload-status'
import { useDialogueFile } from '@/composables/useDialogueFile' import { useDialogueFile } from '@/composables/useDialogueFile'
import { TEXTTOSPEECH_WS_URL } from '@/config/base-url'
import WebSocketCtr from '@/utils/web-socket-ctr'
interface Props { interface Props {
messageList: ConversationMessageItem[] messageList: Map<string, ConversationMessageItem>
continuousQuestionStatus: 'default' | 'close' continuousQuestionStatus: 'default' | 'close'
answerAudioAutoPlay: boolean
answerAudioPlaying: boolean
} }
const { t } = useI18n() const { t } = useI18n()
...@@ -18,13 +23,15 @@ const { t } = useI18n() ...@@ -18,13 +23,15 @@ const { t } = useI18n()
const props = defineProps<Props>() const props = defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
addMessageItem: [value: ConversationMessageItem] addMessageItem: [messageId: string, value: ConversationMessageItem]
updateSpecifyMessageItem: [messageItemIndex: number, newObj: Partial<ConversationMessageItem>] updateSpecifyMessageItem: [messageId: string, newObj: Partial<ConversationMessageItem>]
deleteLastMessageItem: [] deleteMessageItem: [messageId: string]
updatePageScroll: [] updatePageScroll: []
clearAllMessage: [] clearAllMessage: []
createContinueQuestions: [value: string] createContinueQuestions: [value: string]
updateContinuousQuestionStatus: [value: 'default' | 'close'] updateContinuousQuestionStatus: [value: 'default' | 'close']
audioPlay: [messageItem: ConversationMessageItem, requestId?: string]
audioPause: []
}>() }>()
const personalAppConfigStore = usePersonalAppConfigStore() const personalAppConfigStore = usePersonalAppConfigStore()
...@@ -37,6 +44,14 @@ const emitter = inject<Emitter<MittEvents>>('emitter') ...@@ -37,6 +44,14 @@ const emitter = inject<Emitter<MittEvents>>('emitter')
const inputMessageContent = ref('') const inputMessageContent = ref('')
const isAnswerResponseWait = ref(false) const isAnswerResponseWait = ref(false)
const currentReplyContentSentenceExtractIndex = ref(0)
const sentenceFragmentSerialNo = ref(0)
const sentenceExtractCheckEnabled = ref(false)
const assistantFullAnswerContent = ref('')
const currentAgentTimberId = ref('')
const sentenceSpeechException = ref(false)
const messageAudioLoading = ref(false)
const currentLatestMessageItemKeyMap = ref(new Map<'assistant' | 'user', string>())
let controller: AbortController | null = null let controller: AbortController | null = null
...@@ -49,7 +64,7 @@ const isCreateContinueQuestions = computed(() => { ...@@ -49,7 +64,7 @@ const isCreateContinueQuestions = computed(() => {
}) })
const isAllowClearMessage = computed(() => { const isAllowClearMessage = computed(() => {
return props.messageList.length > 0 return props.messageList.size > 0
}) })
const isSendBtnDisabled = computed(() => { const isSendBtnDisabled = computed(() => {
...@@ -57,7 +72,10 @@ const isSendBtnDisabled = computed(() => { ...@@ -57,7 +72,10 @@ const isSendBtnDisabled = computed(() => {
}) })
const isInputMessageDisabled = computed(() => { const isInputMessageDisabled = computed(() => {
return uploadFileList.value.some((fileItem) => fileItem.status !== UploadStatus.FINISHED) return (
uploadFileList.value.some((fileItem) => fileItem.status !== UploadStatus.FINISHED) ||
!personalAppConfigStore.baseInfo.agentId
)
}) })
const isEnableDocumentParse = computed(() => { const isEnableDocumentParse = computed(() => {
...@@ -91,7 +109,7 @@ onMounted(() => { ...@@ -91,7 +109,7 @@ onMounted(() => {
}) })
}) })
function messageItemFactory() { function messageItemFactory(): ConversationMessageItem {
return { return {
timestamp: Date.now(), timestamp: Date.now(),
role: 'user', role: 'user',
...@@ -99,11 +117,32 @@ function messageItemFactory() { ...@@ -99,11 +117,32 @@ function messageItemFactory() {
isEmptyContent: false, isEmptyContent: false,
isTextContentLoading: false, isTextContentLoading: false,
isAnswerResponseLoading: false, isAnswerResponseLoading: false,
} as const isVoiceLoading: false,
isVoicePlaying: false,
voiceFragmentUrlList: [],
isVoiceEnabled: false,
}
} }
function handleMessageSend() { function handleMessageSend() {
if (!inputMessageContent.value.trim() || isAnswerResponseWait.value || isInputMessageDisabled.value) return '' if (!inputMessageContent.value.trim() || isInputMessageDisabled.value) {
return
}
if (isAnswerResponseWait.value || messageAudioLoading.value) {
window.$message.warning(t('common_module.dialogue_module.do_not_operate_until_the_reply_is_complete'))
return
}
if (props.answerAudioPlaying) {
window.$message.warning(t('common_module.dialogue_module.stop_playing_and_then_operate'))
return
}
const latestUserMessageKey = nanoid()
const latestAssistantMessageKey = nanoid()
currentLatestMessageItemKeyMap.value.set('user', latestUserMessageKey)
currentLatestMessageItemKeyMap.value.set('assistant', latestAssistantMessageKey)
const messages: { const messages: {
content: { content: {
...@@ -117,7 +156,7 @@ function handleMessageSend() { ...@@ -117,7 +156,7 @@ function handleMessageSend() {
}[] = [] }[] = []
emit('updateContinuousQuestionStatus', personalAppConfigStore.commConfig.continuousQuestionStatus) emit('updateContinuousQuestionStatus', personalAppConfigStore.commConfig.continuousQuestionStatus)
emit('addMessageItem', { ...messageItemFactory(), textContent: inputMessageContent.value }) emit('addMessageItem', latestUserMessageKey, { ...messageItemFactory(), textContent: inputMessageContent.value })
emit('updatePageScroll') emit('updatePageScroll')
props.messageList.forEach((messageItem) => { props.messageList.forEach((messageItem) => {
...@@ -138,15 +177,26 @@ function handleMessageSend() { ...@@ -138,15 +177,26 @@ function handleMessageSend() {
inputMessageContent.value = '' inputMessageContent.value = ''
isAnswerResponseWait.value = true isAnswerResponseWait.value = true
emit('addMessageItem', { currentReplyContentSentenceExtractIndex.value = 0
sentenceFragmentSerialNo.value = 0
sentenceExtractCheckEnabled.value = false
assistantFullAnswerContent.value = ''
currentAgentTimberId.value = personalAppConfigStore.voiceConfig.timbreId
sentenceSpeechException.value = false
messageAudioLoading.value = false
const isVoiceEnabled = !!personalAppConfigStore.voiceConfig.timbreId
emit('addMessageItem', latestAssistantMessageKey, {
...messageItemFactory(), ...messageItemFactory(),
role: 'assistant', role: 'assistant',
isTextContentLoading: true, isTextContentLoading: true,
isAnswerResponseLoading: true, isAnswerResponseLoading: true,
isVoiceLoading: true,
isVoiceEnabled,
}) })
emit('updatePageScroll') emit('updatePageScroll')
const currentMessageIndex = props.messageList.length - 1
let replyTextContent = '' let replyTextContent = ''
controller = new AbortController() controller = new AbortController()
...@@ -161,11 +211,12 @@ function handleMessageSend() { ...@@ -161,11 +211,12 @@ function handleMessageSend() {
controller, controller,
onMessage: (data: any) => { onMessage: (data: any) => {
if (data === '[DONE]') { if (data === '[DONE]') {
emit('updateSpecifyMessageItem', currentMessageIndex, { emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
isEmptyContent: !replyTextContent, isEmptyContent: !replyTextContent,
isTextContentLoading: false, isTextContentLoading: false,
isAnswerResponseLoading: false, isAnswerResponseLoading: false,
}) })
isCreateContinueQuestions.value && emit('createContinueQuestions', replyTextContent) isCreateContinueQuestions.value && emit('createContinueQuestions', replyTextContent)
emit('updatePageScroll') emit('updatePageScroll')
blockMessageResponse() blockMessageResponse()
...@@ -175,7 +226,18 @@ function handleMessageSend() { ...@@ -175,7 +226,18 @@ function handleMessageSend() {
if (data) { if (data) {
replyTextContent += data replyTextContent += data
emit('updateSpecifyMessageItem', currentMessageIndex, { assistantFullAnswerContent.value = (assistantFullAnswerContent.value + data).replace(
/\^\[[\d\\[\]-]+?\]\^/g,
'',
)
if (!sentenceExtractCheckEnabled.value && isVoiceEnabled) {
sentenceExtract(latestAssistantMessageKey)
sentenceExtractCheckEnabled.value = true
messageAudioLoading.value = true
}
emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
textContent: replyTextContent, textContent: replyTextContent,
isTextContentLoading: false, isTextContentLoading: false,
}) })
...@@ -195,12 +257,13 @@ function handleMessageSend() { ...@@ -195,12 +257,13 @@ function handleMessageSend() {
} }
function errorMessageResponse() { function errorMessageResponse() {
emit('updateSpecifyMessageItem', props.messageList.length - 1, { emit('updateSpecifyMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!, {
isTextContentLoading: false, isTextContentLoading: false,
textContent: '', textContent: '',
}) })
emit('deleteLastMessageItem') emit('deleteMessageItem', currentLatestMessageItemKeyMap.value.get('user')!)
emit('deleteLastMessageItem') emit('deleteMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!)
emit('audioPause')
blockMessageResponse() blockMessageResponse()
} }
...@@ -226,6 +289,108 @@ function handleSelectFile(cb: () => void) { ...@@ -226,6 +289,108 @@ function handleSelectFile(cb: () => void) {
cb() cb()
} }
function sentenceExtract(messageId: string) {
const symbolRegExp = /[。!?;.!?;]/g
let sentenceDraft = assistantFullAnswerContent.value
.replace(/\s{5,}/gi, '')
.slice(currentReplyContentSentenceExtractIndex.value)
let matchResult = symbolRegExp.exec(sentenceDraft)
function matchExtract() {
const article = assistantFullAnswerContent.value.replace(/\s{5,}/gi, '')
if (matchResult && matchResult.index && matchResult.index > 60) {
sentenceDraft = article.slice(
currentReplyContentSentenceExtractIndex.value,
currentReplyContentSentenceExtractIndex.value + matchResult.index + matchResult['0'].length,
)
currentReplyContentSentenceExtractIndex.value += sentenceDraft.length
ttsSocketSendText(sentenceDraft, sentenceFragmentSerialNo.value, messageId)
sentenceFragmentSerialNo.value += 1
if (article.length - currentReplyContentSentenceExtractIndex.value > 60) {
sentenceDraft = article.slice(currentReplyContentSentenceExtractIndex.value)
matchResult = symbolRegExp.exec(sentenceDraft)
matchExtract()
} else {
setTimeout(() => sentenceExtract(messageId), 600)
}
} else if (!isAnswerResponseWait.value) {
/* 延时避免最后回复内容没有更新全 */
setTimeout(() => {
sentenceDraft = article.slice(currentReplyContentSentenceExtractIndex.value)
ttsSocketSendText(sentenceDraft, sentenceFragmentSerialNo.value, messageId)
sentenceFragmentSerialNo.value += 1
}, 700)
} else {
sentenceDraft = assistantFullAnswerContent.value
.replace(/\s{5,}/gi, '')
.slice(currentReplyContentSentenceExtractIndex.value)
matchResult = symbolRegExp.exec(sentenceDraft)
setTimeout(() => matchExtract(), 500)
}
}
if (matchResult) matchExtract()
else setTimeout(() => sentenceExtract(messageId), 600)
}
function ttsSocketSendText(text: string, audioUrlSerialNo: number, messageId: string) {
if (sentenceSpeechException.value) {
return
}
const ttsSocketCtl = new WebSocketCtr(TEXTTOSPEECH_WS_URL)
ttsSocketCtl.onMessage = (data: { audio: string; replyVoiceUrl: string; final: boolean }) => {
if (data.replyVoiceUrl) {
if (props.messageList.get(messageId)?.voiceFragmentUrlList) {
const voiceFragmentUrlListDraft = [...props.messageList.get(messageId)!.voiceFragmentUrlList]
voiceFragmentUrlListDraft[audioUrlSerialNo] = data.replyVoiceUrl
messageAudioLoading.value = false
emit('updateSpecifyMessageItem', messageId, { voiceFragmentUrlList: voiceFragmentUrlListDraft })
if (props.answerAudioAutoPlay && audioUrlSerialNo === 0 && voiceFragmentUrlListDraft[audioUrlSerialNo]) {
emit('audioPlay', props.messageList.get(messageId)!)
}
if (!props.answerAudioAutoPlay) {
emit('updateSpecifyMessageItem', messageId, { isVoiceLoading: false })
}
}
}
}
ttsSocketCtl.onMessageError = () => {
emit('updateSpecifyMessageItem', messageId, { isVoiceLoading: false, voiceFragmentUrlList: [] })
props.messageList.get(messageId)?.isVoicePlaying && emit('audioPause')
sentenceSpeechException.value = true
messageAudioLoading.value = false
window.$message.error(t('common_module.unplayable_tip'))
}
const content = (text || '').replace(/\^\[[\d\\[\]-]+?\]\^/g, '')
if (content && currentAgentTimberId.value) {
ttsSocketCtl.connect(() => {
ttsSocketCtl.send({
codec: 'wav',
sampleRate: 16000,
speed: 0,
voiceType: currentAgentTimberId.value,
volume: 0,
content,
})
})
}
}
defineExpose({ defineExpose({
blockMessageResponse, blockMessageResponse,
}) })
...@@ -291,7 +456,13 @@ defineExpose({ ...@@ -291,7 +456,13 @@ defineExpose({
<div <div
class="bg-px-send-png absolute bottom-2 right-[20px] h-[24px] w-[24px]" class="bg-px-send-png absolute bottom-2 right-[20px] h-[24px] w-[24px]"
:class=" :class="
isSendBtnDisabled || isAnswerResponseWait || isInputMessageDisabled ? 'opacity-60' : 'cursor-pointer' isSendBtnDisabled ||
isAnswerResponseWait ||
isInputMessageDisabled ||
answerAudioPlaying ||
messageAudioLoading
? 'opacity-60'
: 'cursor-pointer'
" "
@click="handleMessageSend" @click="handleMessageSend"
/> />
......
...@@ -3,7 +3,7 @@ import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config' ...@@ -3,7 +3,7 @@ import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { Close, Delete } from '@icon-park/vue-next' import { Close, Delete } from '@icon-park/vue-next'
import { DataTableColumns } from 'naive-ui' import { DataTableColumns } from 'naive-ui'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { MemoryVariableForm } from './memory-variable-modal.vue' import { MemoryVariableForm } from '../../components/memory-variable-modal.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { import {
fetchDeleteAllLongMemory, fetchDeleteAllLongMemory,
......
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import CustomLoading from './custom-loading.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import { useUserStore } from '@/store/modules/user'
interface Props {
role: 'user' | 'assistant'
messageItem: ConversationMessageItem
}
const props = defineProps<Props>()
const emit = defineEmits<{
audioPlay: []
audioPause: []
}>()
const { t } = useI18n()
const userStore = useUserStore()
const personalAppConfigStore = usePersonalAppConfigStore()
const useAvatar = computed(() => {
return userStore.userInfo.avatarUrl || 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/1725952917468.png'
})
const assistantAvatar = computed(() => {
return personalAppConfigStore.baseInfo.agentAvatar
})
const isShowAudioControl = computed(() => {
return props.role === 'assistant' && !props.messageItem.isVoiceLoading
})
const isPlayableAudio = computed(() => {
return isShowAudioControl.value && !!props.messageItem.voiceFragmentUrlList.length
})
const isShowVoiceLoading = computed(() => {
return props.role === 'assistant' && props.messageItem.isVoiceLoading && props.messageItem.isVoiceEnabled
})
function handleAudioControl() {
if (!isPlayableAudio.value) {
return
}
if (props.messageItem.isVoicePlaying) {
emit('audioPause')
} else {
emit('audioPlay')
}
}
</script>
<template>
<div class="mb-5 flex last:mb-0">
<NImage
:src="role === 'user' ? useAvatar : assistantAvatar"
preview-disabled
:width="32"
:height="32"
object-fit="cover"
class="mr-2 mt-1.5 h-8 w-8 flex-shrink-0 rounded-full"
/>
<div class="flex flex-col items-start">
<div
class="min-w-[80px] max-w-full flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
:class="role === 'user' ? 'bg-[#4b87ff] text-white' : 'bg-white text-[#333]'"
>
<div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4">
<CustomLoading />
</div>
<div v-else>
<p class="break-all">
<MarkdownRender
:raw-text-content="
messageItem.isEmptyContent
? t('common_module.dialogue_module.empty_message_content')
: messageItem.textContent
"
:color="role === 'user' ? '#fff' : '#192338'"
/>
</p>
<div v-show="role === 'assistant' && messageItem.isAnswerResponseLoading" class="mb-[5px] mt-4 px-4">
<CustomLoading />
</div>
</div>
</div>
<div
v-show="isShowAudioControl"
class="text-font-color flex items-center gap-0.5"
:class="isPlayableAudio ? 'hover:text-theme-color cursor-pointer hover:opacity-80' : 'cursor-not-allowed'"
@click="handleAudioControl"
>
<i v-if="!messageItem.isVoicePlaying" class="iconfont icon-play text-[24px]" />
<div v-else class="mx-1.5 my-3 h-[12px] w-[12px] bg-[url(@/assets/images/playing.gif)] bg-[length:100%_100%]" />
<span
v-show="isPlayableAudio"
class="text-[12px]"
:class="messageItem.isVoicePlaying ? 'text-theme-color' : ''"
>
{{ messageItem.isVoicePlaying ? t('common_module.stop_playing') : t('common_module.start_playing') }}
</span>
<n-popover style="max-width: 310px">
<template #trigger>
<span v-show="!isPlayableAudio" class="text-[12px]"> {{ t('common_module.unplayable') }} </span>
</template>
{{ t('common_module.unplayable_tip') }}
</n-popover>
</div>
<div v-if="isShowVoiceLoading" class="py-3.5 pl-6">
<CustomLoading />
</div>
</div>
</div>
</template>
...@@ -5,20 +5,25 @@ import ContinueQuestion from './continue-question.vue' ...@@ -5,20 +5,25 @@ import ContinueQuestion from './continue-question.vue'
import { useScroll } from '@/composables/useScroll' import { useScroll } from '@/composables/useScroll'
interface Props { interface Props {
messageList: ConversationMessageItem[] messageList: Map<string, ConversationMessageItem>
continuousQuestionStatus: 'default' | 'close' continuousQuestionStatus: 'default' | 'close'
continuousQuestionList: string[] continuousQuestionList: string[]
} }
const props = defineProps<Props>() const props = defineProps<Props>()
defineEmits<{
audioPlay: [messageItem: ConversationMessageItem]
audioPause: []
}>()
const { scrollRef, scrollToBottom } = useScroll() const { scrollRef, scrollToBottom } = useScroll()
const isShowContinueQuestion = computed(() => { const isShowContinueQuestion = computed(() => {
return ( return (
props.continuousQuestionStatus === 'default' && props.continuousQuestionStatus === 'default' &&
props.messageList.length > 1 && props.messageList.size > 1 &&
!props.messageList[props.messageList.length - 1].isAnswerResponseLoading !Array.from(props.messageList.entries()).pop()?.[1].isAnswerResponseLoading
) )
}) })
...@@ -31,10 +36,12 @@ defineExpose({ ...@@ -31,10 +36,12 @@ defineExpose({
<main ref="scrollRef" class="h-full overflow-y-auto px-5"> <main ref="scrollRef" class="h-full overflow-y-auto px-5">
<div> <div>
<MessageItem <MessageItem
v-for="messageItem in messageList" v-for="[key, messageItem] in messageList"
:key="messageItem.timestamp" :key="key"
:role="messageItem.role" :role="messageItem.role"
:message-item="messageItem" :message-item="messageItem"
@audio-play="() => $emit('audioPlay', messageItem)"
@audio-pause="() => $emit('audioPause')"
/> />
</div> </div>
......
...@@ -8,8 +8,8 @@ import { useThrottleFn } from '@vueuse/core' ...@@ -8,8 +8,8 @@ import { useThrottleFn } from '@vueuse/core'
import CustomIcon from '@/components/custom-icon/custom-icon.vue' import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { Help, People, RightOne } from '@icon-park/vue-next' import { Help, People, RightOne } from '@icon-park/vue-next'
import UploadImage from '@/components/upload-image/upload-image.vue' import UploadImage from '@/components/upload-image/upload-image.vue'
import AutoConfigModal from './auto-config-modal.vue' import AutoConfigModal from './components/auto-config-modal.vue'
import OptimizeSystemModal from './optimize-system-modal.vue' import OptimizeSystemModal from './components/optimize-system-modal.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config' import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { import {
...@@ -20,10 +20,11 @@ import { ...@@ -20,10 +20,11 @@ import {
fetchSaveAgentApplication, fetchSaveAgentApplication,
} from '@/apis/agent-application' } from '@/apis/agent-application'
import { fetchCustomEventSource } from '@/composables/useEventSource' import { fetchCustomEventSource } from '@/composables/useEventSource'
import AgentModelSetting from './agent-model-setting.vue' import AgentModelSetting from './components/agent-model-setting.vue'
import AgentAssociatedKnowledge from './agent-associated-knowledge.vue' import AgentAssociatedKnowledge from './components/agent-associated-knowledge.vue'
import AgentMemorySetting from './agent-memory-setting.vue' import AgentMemorySetting from './components/agent-memory-setting.vue'
import AgentDialogueSetting from './agent-dialogue-setting.vue' import AgentDialogueSetting from './components/agent-dialogue-setting.vue'
import AgentRoleSetting from './components/agent-role-setting.vue'
const { t } = useI18n() const { t } = useI18n()
...@@ -74,10 +75,10 @@ watch( ...@@ -74,10 +75,10 @@ watch(
() => personalAppConfigStore.$state, () => personalAppConfigStore.$state,
() => { () => {
if (!baseInfo.value.agentId) { if (!baseInfo.value.agentId) {
handleUpdatePersonalAppId() handleCreatePersonalAgent()
} }
}, },
{ deep: true, once: true }, { deep: true },
) )
watch( watch(
...@@ -114,7 +115,7 @@ const handleSavePersonalAppConfig = useThrottleFn( ...@@ -114,7 +115,7 @@ const handleSavePersonalAppConfig = useThrottleFn(
true, true,
) )
// 保存应用配置 // 更新保存应用配置
async function handleSaveAgentApplication() { async function handleSaveAgentApplication() {
if (!baseInfo.value.agentTitle) { if (!baseInfo.value.agentTitle) {
return return
...@@ -123,8 +124,8 @@ async function handleSaveAgentApplication() { ...@@ -123,8 +124,8 @@ async function handleSaveAgentApplication() {
await fetchSaveAgentApplication<PersonalAppConfigState>(personalAppConfigStore.$state) await fetchSaveAgentApplication<PersonalAppConfigState>(personalAppConfigStore.$state)
} }
// 更新保存应用ID // 新建应用
async function handleUpdatePersonalAppId() { async function handleCreatePersonalAgent() {
if (!baseInfo.value.agentTitle) { if (!baseInfo.value.agentTitle) {
return return
} }
...@@ -250,10 +251,14 @@ async function handleSettingAgent(autoConfigInputValue: string) { ...@@ -250,10 +251,14 @@ async function handleSettingAgent(autoConfigInputValue: string) {
handleAIGeneratePreamble(), handleAIGeneratePreamble(),
handleAIGenerateFeaturedQuestions(), handleAIGenerateFeaturedQuestions(),
handleAIGenerateAgentSystem(), handleAIGenerateAgentSystem(),
]).finally(() => { ])
isFullScreenLoading.value = false .finally(() => {
emitter?.emit('resetAgent') isFullScreenLoading.value = false
}) emitter?.emit('resetAgent')
})
.catch(() => {
handleStopGenerate()
})
}) })
.catch(() => { .catch(() => {
isFullScreenLoading.value = false isFullScreenLoading.value = false
...@@ -558,29 +563,33 @@ function handleStopGenerate() { ...@@ -558,29 +563,33 @@ function handleStopGenerate() {
</div> </div>
</div> </div>
<div class="h-full flex-1 overflow-auto border-r border-[#e8e9eb] py-4"> <div class="flex h-full flex-1 flex-col overflow-auto border-r border-[#e8e9eb] py-4">
<div class="flex h-6 items-center px-5 leading-6"> <div class="mb-1 flex h-6 items-center px-5 leading-6">
<CustomIcon icon="streamline:decent-work-and-economic-growth-solid" class="mr-1.5 text-base" /> <CustomIcon icon="streamline:decent-work-and-economic-growth-solid" class="mr-1.5 text-base" />
<span> <span>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.ability_expand') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.ability_expand') }}
</span> </span>
</div> </div>
<AgentAssociatedKnowledge v-model:knowledge-config="knowledgeConfig" /> <div class="flex-1 overflow-auto">
<AgentAssociatedKnowledge v-model:knowledge-config="knowledgeConfig" />
<AgentMemorySetting
v-model:variable-structure="commConfig.variableStructure" <AgentMemorySetting
v-model:is-long-memory="commConfig.isLongMemory" v-model:variable-structure="commConfig.variableStructure"
/> v-model:is-long-memory="commConfig.isLongMemory"
/>
<AgentDialogueSetting
v-model:comm-config-expanded-names="commConfigExpandedNames" <AgentDialogueSetting
v-model:comm-config="commConfig" v-model:comm-config-expanded-names="commConfigExpandedNames"
v-model:generate-featured-questions-loading="generateFeaturedQuestionsLoading" v-model:comm-config="commConfig"
v-model:generate-preamble-loading="generatePreambleLoading" v-model:generate-featured-questions-loading="generateFeaturedQuestionsLoading"
@generate-preamble="handleAIGeneratePreamble" v-model:generate-preamble-loading="generatePreambleLoading"
@generate-featured-questions="handleAIGenerateFeaturedQuestions" @generate-preamble="handleAIGeneratePreamble"
/> @generate-featured-questions="handleAIGenerateFeaturedQuestions"
/>
<AgentRoleSetting />
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n' ...@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n'
import { Plus, RightOne } from '@icon-park/vue-next' import { Plus, RightOne } from '@icon-park/vue-next'
import { fetchGetKnowledgeListByKdIds } from '@/apis/knowledge' import { fetchGetKnowledgeListByKdIds } from '@/apis/knowledge'
import AssociatedKnowledgeModal from './associated-knowledge-modal.vue' import AssociatedKnowledgeModal from './associated-knowledge-modal.vue'
import { KnowledgeItem } from '../../personal-knowledge/types' import { KnowledgeItem } from '@/views/personal-space/personal-knowledge/types'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
const { t } = useI18n() const { t } = useI18n()
...@@ -92,7 +92,7 @@ function handleUpdateDocumentParsing(value: boolean) { ...@@ -92,7 +92,7 @@ function handleUpdateDocumentParsing(value: boolean) {
<template> <template>
<section class="border-b border-[#e8e9eb] px-5"> <section class="border-b border-[#e8e9eb] px-5">
<div class="pt-4"> <div class="pt-4">
<h2 class="my-3 text-[#84868c]"> <h2 class="mb-3 text-[#84868c]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.knowledge') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.knowledge') }}
</h2> </h2>
......
...@@ -61,7 +61,7 @@ function handleAIGenerateFeaturedQuestions() { ...@@ -61,7 +61,7 @@ function handleAIGenerateFeaturedQuestions() {
<template> <template>
<section class="border-b border-[#e8e9eb] px-5"> <section class="border-b border-[#e8e9eb] px-5">
<div class="pt-4"> <div class="pt-4">
<h2 class="my-3 text-[#84868c]"> <h2 class="mb-3 text-[#84868c]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.dialogue') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.dialogue') }}
</h2> </h2>
<NCollapse <NCollapse
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Plus, Help, MoreOne, Edit, Copy, ReduceOne, RightOne } from '@icon-park/vue-next' import { Plus, Help, MoreOne, Edit, Copy, ReduceOne, RightOne } from '@icon-park/vue-next'
import MemoryVariableModal, { MemoryVariableForm } from './memory-variable-modal.vue' import MemoryVariableModal, { MemoryVariableForm } from '../../components/memory-variable-modal.vue'
import { VariableStructureItem } from '@/store/types/personal-app-config' import { VariableStructureItem } from '@/store/types/personal-app-config'
import { copyToClip } from '@/utils/copy' import { copyToClip } from '@/utils/copy'
...@@ -69,7 +69,7 @@ function handleChangeMemoryFragmentState(value: boolean) { ...@@ -69,7 +69,7 @@ function handleChangeMemoryFragmentState(value: boolean) {
<template> <template>
<section class="border-b border-[#e8e9eb] px-5"> <section class="border-b border-[#e8e9eb] px-5">
<div class="pt-4"> <div class="pt-4">
<h2 class="my-3 text-[#84868c]"> <h2 class="mb-3 text-[#84868c]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.memory') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.memory') }}
</h2> </h2>
<NCollapse <NCollapse
......
...@@ -4,11 +4,14 @@ import { SelectOption } from 'naive-ui' ...@@ -4,11 +4,14 @@ import { SelectOption } from 'naive-ui'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Help, Down } from '@icon-park/vue-next' import { Help, Down } from '@icon-park/vue-next'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { useSystemLanguageStore } from '@/store/modules/system-language'
import { DiversityModeItem, diversityModeList } from '@/data/agent-setting-data' import { DiversityModeItem, diversityModeList } from '@/data/agent-setting-data'
import { fetchGetLargeModelInfo, fetchGetLargeModelList } from '@/apis/agent-application' import { fetchGetLargeModelInfo, fetchGetLargeModelList } from '@/apis/agent-application'
const { t } = useI18n() const { t } = useI18n()
const systemLanguageStore = useSystemLanguageStore()
let modalListOptions = reactive<SelectOption[]>([]) let modalListOptions = reactive<SelectOption[]>([])
let modalListRenderLabel: (option: SelectOption) => VNodeChild let modalListRenderLabel: (option: SelectOption) => VNodeChild
...@@ -17,6 +20,13 @@ const commModelConfig = defineModel<PersonalAppConfigState['commModelConfig']>(' ...@@ -17,6 +20,13 @@ const commModelConfig = defineModel<PersonalAppConfigState['commModelConfig']>('
const currentLargeModelIcon = ref('') const currentLargeModelIcon = ref('')
const currentDiversityMode = ref('balance') const currentDiversityMode = ref('balance')
const modelSettingWidth = ref(systemLanguageStore.currentLanguageInfo.key === 'en' ? '508px' : '420px')
const sliderLabelWidth = ref(systemLanguageStore.currentLanguageInfo.key === 'en' ? '158px' : '105px')
const isEnLanguage = computed(() => {
return systemLanguageStore.currentLanguageInfo.key === 'en'
})
const isDisabledCommModelConfig = computed(() => { const isDisabledCommModelConfig = computed(() => {
return currentDiversityMode.value !== 'custom' return currentDiversityMode.value !== 'custom'
}) })
...@@ -107,7 +117,7 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) { ...@@ -107,7 +117,7 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
<template> <template>
<div> <div>
<NPopover placement="bottom" trigger="click" style="width: 420px"> <NPopover placement="bottom" trigger="click" :style="{ width: modelSettingWidth }">
<template #trigger> <template #trigger>
<div <div
class="hover:border-theme-color flex cursor-pointer items-center justify-between rounded-md border border-[#d4d6d9] px-3 py-[7px]" class="hover:border-theme-color flex cursor-pointer items-center justify-between rounded-md border border-[#d4d6d9] px-3 py-[7px]"
...@@ -119,11 +129,14 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) { ...@@ -119,11 +129,14 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
<Down theme="outline" size="16" fill="#333" class="ml-1.5 text-base outline-none" /> <Down theme="outline" size="16" fill="#333" class="ml-1.5 text-base outline-none" />
</div> </div>
</template> </template>
<div class="mb-2 mt-[6px] flex items-center"> <div class="flex items-center" :class="isEnLanguage ? 'my-[18px] justify-between' : 'mb-2 mt-1.5 justify-start'">
<span class="font-500 mr-3 text-sm text-[#151b26]"> <span class="font-500 mr-3 text-sm text-[#151b26]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.question_answer_model') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.question_answer_model') }}
</span> </span>
<span class="rounded bg-[#f2f5f9] px-1 text-xs text-[#5c5f66]"> <span
class="rounded-theme bg-[#f2f5f9] text-xs text-[#5c5f66]"
:class="isEnLanguage ? 'px-[13px] py-2' : 'px-1'"
>
{{ {{
t('personal_space_module.agent_module.agent_setting_module.agent_config_module.question_answer_model_desc') t('personal_space_module.agent_module.agent_setting_module.agent_config_module.question_answer_model_desc')
}} }}
...@@ -141,7 +154,7 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) { ...@@ -141,7 +154,7 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
<span> <span>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.generate_diversity') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.generate_diversity') }}
</span> </span>
<ul class="rounded-theme mt-2 grid grid-cols-4 overflow-hidden"> <ul class="rounded-theme grid grid-cols-4 overflow-hidden" :class="isEnLanguage ? 'mt-3' : 'mt-2'">
<li <li
v-for="(diversityModeItem, index) in diversityModeList" v-for="(diversityModeItem, index) in diversityModeList"
:key="index" :key="index"
...@@ -159,8 +172,8 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) { ...@@ -159,8 +172,8 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
</div> </div>
<div class="mt-4 text-xs"> <div class="mt-4 text-xs">
<div class="mb-2.5 flex h-[34px] items-center justify-between"> <div class="flex h-[34px] items-center justify-between" :class="isEnLanguage ? 'mb-3.5' : 'mb-2.5'">
<div class="flex w-[105px] items-center"> <div class="flex items-center" :style="{ width: sliderLabelWidth }">
<span> <span>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.topP') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.topP') }}
</span> </span>
...@@ -202,12 +215,12 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) { ...@@ -202,12 +215,12 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
:max="1" :max="1"
:disabled="isDisabledCommModelConfig" :disabled="isDisabledCommModelConfig"
size="small" size="small"
class="w-[90px]! text-xs!" class="common-model-config-input-number w-[90px]!"
/> />
</div> </div>
<div class="mb-2.5 flex h-[34px] items-center justify-between"> <div class="flex h-[34px] items-center justify-between" :class="isEnLanguage ? 'mb-3.5' : 'mb-2.5'">
<div class="flex w-[105px] items-center"> <div class="flex items-center" :style="{ width: sliderLabelWidth }">
<span> <span>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.temperature') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.temperature') }}
</span> </span>
...@@ -251,12 +264,12 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) { ...@@ -251,12 +264,12 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
:max="1" :max="1"
:disabled="isDisabledCommModelConfig" :disabled="isDisabledCommModelConfig"
size="small" size="small"
class="w-[90px]! text-xs!" class="common-model-config-input-number w-[90px]!"
/> />
</div> </div>
<div class="mb-2.5 flex h-[34px] items-center justify-between"> <div class="flex h-[34px] items-center justify-between" :class="isEnLanguage ? 'mb-3.5' : 'mb-2.5'">
<div class="flex w-[105px] items-center"> <div class="flex items-center" :style="{ width: sliderLabelWidth }">
<span> <span>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.communication_turn') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.communication_turn') }}
</span> </span>
...@@ -296,7 +309,7 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) { ...@@ -296,7 +309,7 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
:min="0" :min="0"
:max="100" :max="100"
size="small" size="small"
class="w-[90px]!" class="common-model-config-input-number w-[90px]!"
placeholder="" placeholder=""
/> />
</div> </div>
...@@ -312,4 +325,10 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) { ...@@ -312,4 +325,10 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
border-radius: 6px; border-radius: 6px;
} }
} }
.common-model-config-input-number {
:deep(.n-input__input) {
font-size: 12px;
}
}
</style> </style>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import { Plus, RightOne, MoreOne, Edit, ReduceOne } from '@icon-park/vue-next'
import TimbreSettingModal from './timbre-setting-modal.vue'
import { TimbreLanguageInfoItem } from '../../types'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { fetchGetTimbreInfoDetail } from '@/apis/timber'
const { t } = useI18n()
const personalAppConfigStore = usePersonalAppConfigStore()
const { voiceConfig } = storeToRefs(personalAppConfigStore)
const roleConfigExpandedNames = ref<string[]>([])
const isShowTimbreSettingModal = ref(false)
const timbreInfoDetail = ref<TimbreLanguageInfoItem>()
let timbreInfo = reactive<TimbreLanguageInfoItem>({ language: 0, matchLang: '', timbreInfo: [] })
const timberFullName = computed(() => {
const languageOptions = [
{ label: 'common_module.mandarin', value: 'zh-CN' },
{ label: 'common_module.cantonese', value: 'zh-HK' },
{ label: 'common_module.english', value: 'en' },
]
const currentLanguage = languageOptions.find((item) => item.value === timbreInfoDetail.value?.matchLang)
return `${timbreInfoDetail.value?.timbreInfo?.[0]?.timbreName || '--'}(${t(currentLanguage?.label || 'common_module.sound')})`
})
const isHasTimbreId = computed(() => !!voiceConfig.value.timbreId)
onMounted(() => {
voiceConfig.value.timbreId && handleGetTimberInfoDetail()
})
async function handleGetTimberInfoDetail() {
const res = await fetchGetTimbreInfoDetail<TimbreLanguageInfoItem>(voiceConfig.value.timbreId)
if (res.code === 0) {
timbreInfoDetail.value = res.data
roleConfigExpandedNames.value = ['timbre']
}
}
function handleUpdateRoleConfigExpandedNames(expandedNames: string[]) {
roleConfigExpandedNames.value = expandedNames
}
function handleShowAssociatedTimbreModel() {
if (voiceConfig.value.timbreId) {
return
}
isShowTimbreSettingModal.value = true
timbreInfo = { language: 0, matchLang: '', timbreInfo: [] }
}
function handleUpdateTimbreId(timberId: string) {
isShowTimbreSettingModal.value = false
voiceConfig.value.timbreId = timberId
voiceConfig.value.defaultOpen = 'Y'
handleGetTimberInfoDetail()
}
function handleRemoveTimbreId() {
voiceConfig.value.timbreId = ''
}
function handleEditAssociatedTimbreModel() {
isShowTimbreSettingModal.value = true
timbreInfo = timbreInfoDetail.value!
}
</script>
<template>
<section class="border-b border-[#e8e9eb] px-5">
<div class="pt-4">
<h2 class="mb-3 text-[#84868c]">{{ t('common_module.role') }}</h2>
<NCollapse
:expanded-names="roleConfigExpandedNames"
:trigger-areas="['main', 'arrow']"
@update:expanded-names="handleUpdateRoleConfigExpandedNames"
>
<template #arrow>
<RightOne theme="filled" size="17" fill="#333" :stroke-width="3" />
</template>
<NCollapseItem :title="t('common_module.voice')" name="timbre" class="my-[13px]!">
<template #header-extra>
<NTooltip trigger="hover">
<template #trigger>
<Plus
theme="outline"
size="22"
:stroke-width="3"
class="text-theme-color"
:class="isHasTimbreId ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="handleShowAssociatedTimbreModel"
/>
</template>
{{
isHasTimbreId
? t(
'personal_space_module.agent_module.agent_setting_module.agent_config_module.currently_only_one_voice_can_be_set',
)
: t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_voice')
}}
</NTooltip>
</template>
<span v-show="!voiceConfig.timbreId" class="text-xs text-[#84868c]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_voice_desc') }}
</span>
<div class="flex flex-1 flex-wrap items-center gap-[12px] overflow-hidden">
<div
v-show="voiceConfig.timbreId"
class="font-400 line-height-[20px] flex cursor-pointer items-center rounded-[4px] bg-[#f2f5f9] py-[2px] pl-[8px] text-[12px] hover:bg-[#e3e8f0]"
@click="handleEditAssociatedTimbreModel"
>
<div class="max-w-[205px] truncate text-[#151b26]">{{ timberFullName }}</div>
<n-popover placement="bottom" trigger="hover" :show-arrow="false" class="p-[4px]!">
<template #trigger>
<MoreOne theme="outline" size="14" fill="#333" :stroke-width="3" class="mr-[4px] mt-[2px]" />
</template>
<div class="text-[12px]">
<div
class="flex h-[30px] w-[90px] cursor-pointer items-center justify-start px-[8px] py-[5px] hover:rounded-[4px] hover:bg-[#f2f5f9]"
@click="handleEditAssociatedTimbreModel"
>
<Edit theme="outline" size="16" fill="#333" :stroke-width="3" /><span class="ml-[4px]">
{{
t(
'personal_space_module.agent_module.agent_setting_module.agent_config_module.memory_variable_action_edit',
)
}}
</span>
</div>
<n-space>
<div
class="flex h-[30px] w-[90px] cursor-pointer items-center justify-start px-[8px] py-[5px] hover:rounded-[4px] hover:bg-[#f2f5f9]"
@click="handleRemoveTimbreId"
>
<ReduceOne theme="outline" size="16" fill="#333" :stroke-width="3" />
<span class="ml-[4px]">
{{ t('common_module.delete') }}
</span>
</div>
</n-space>
</div>
</n-popover>
</div>
</div>
</NCollapseItem>
</NCollapse>
</div>
</section>
<TimbreSettingModal
v-model:is-show-modal="isShowTimbreSettingModal"
:btn-loading="false"
:modal-title="t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_voice')"
:timbre-info="timbreInfo"
@confirm="handleUpdateTimbreId"
/>
</template>
...@@ -7,10 +7,11 @@ import { usePagination } from '@/composables/usePagination.ts' ...@@ -7,10 +7,11 @@ import { usePagination } from '@/composables/usePagination.ts'
import CustomModal from '@/components/custom-modal/custom-modal.vue' import CustomModal from '@/components/custom-modal/custom-modal.vue'
import { fetchCreateKnowledge, fetchGetKnowledgeList } from '@/apis/knowledge' import { fetchCreateKnowledge, fetchGetKnowledgeList } from '@/apis/knowledge'
import { KnowledgeDocumentItem, KnowledgeItem } from '@/views/personal-space/personal-knowledge/types' import { KnowledgeDocumentItem, KnowledgeItem } from '@/views/personal-space/personal-knowledge/types'
import { TrainStatus } from '@/views/personal-space/personal-knowledge/types.d'
import { formatDateTime } from '@/utils/date-formatter' import { formatDateTime } from '@/utils/date-formatter'
import CreateKnowledgeModal, { import CreateKnowledgeModal, {
KnowledgeFormDataInterface, KnowledgeFormDataInterface,
} from '../../personal-knowledge/components/create-knowledge-modal.vue' } from '@/views/personal-space/personal-knowledge/components/create-knowledge-modal.vue'
interface Props { interface Props {
isShowModal: boolean isShowModal: boolean
...@@ -95,7 +96,7 @@ async function handleGetKnowledgeList() { ...@@ -95,7 +96,7 @@ async function handleGetKnowledgeList() {
paginationData.pageSize = 999999 paginationData.pageSize = 999999
knowledgeListLoading.value = true knowledgeListLoading.value = true
const res = await fetchGetKnowledgeList<KnowledgeItem[]>('', searchKnowledgeInputValue.value, { const res = await fetchGetKnowledgeList<KnowledgeItem[]>(TrainStatus.COMPLETE, searchKnowledgeInputValue.value, {
pagingInfo: paginationData, pagingInfo: paginationData,
}) })
...@@ -171,6 +172,11 @@ async function handleCreateKnowledgeNextStep(createKnowledgeData: KnowledgeFormD ...@@ -171,6 +172,11 @@ async function handleCreateKnowledgeNextStep(createKnowledgeData: KnowledgeFormD
<template #content> <template #content>
<div class="flex w-full justify-end px-3"> <div class="flex w-full justify-end px-3">
<div class="gap-4.5 flex items-center"> <div class="gap-4.5 flex items-center">
<i
class="iconfont icon-shuaxin text-gray-font-color hover:text-font-color cursor-pointer"
@click="handleGetKnowledgeList"
/>
<NInput <NInput
v-model:value="searchKnowledgeInputValue" v-model:value="searchKnowledgeInputValue"
:placeholder="t('personal_space_module.knowledge_module.search_knowledge_placeholder')" :placeholder="t('personal_space_module.knowledge_module.search_knowledge_placeholder')"
......
<script setup lang="ts">
import { computed, h, readonly, ref, shallowRef, watch } from 'vue'
import { SelectOption, SelectRenderTag } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { Howl } from 'howler'
import { useSystemLanguageStore } from '@/store/modules/system-language'
import CustomModal from '@/components/custom-modal/custom-modal.vue'
import { fetchGetTimbreList } from '@/apis/timber'
import { TimbreLanguageInfoItem, TimbreInfoItem } from '../../types'
import { validBrowser } from '@/utils/browser-detection'
interface Props {
modalTitle: string
isShowModal: boolean
btnLoading: boolean
timbreInfo?: TimbreLanguageInfoItem
}
interface Emits {
(e: 'update:isShowModal', value: boolean): void
(e: 'confirm', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const { t } = useI18n()
const systemLanguageStore = useSystemLanguageStore()
const languageInfoList = ref<TimbreLanguageInfoItem[]>([])
const timbreInfoList = ref<TimbreInfoItem[]>([])
const timbreOptionList = ref<SelectOption[]>([])
const selectedLanguage = ref('')
const selectedTimberId = ref('')
const timberUrl = ref('')
const isPlaySound = ref(false)
const requestDataLoading = ref(false)
const currentSoundCtl = shallowRef<Howl | null>(null)
let timbreRenderTag: SelectRenderTag
const languageOptions = readonly([
{ label: () => h('span', {}, t('common_module.mandarin')), value: 'zh-CN' },
{ label: () => h('span', {}, t('common_module.cantonese')), value: 'zh-HK' },
{ label: () => h('span', {}, t('common_module.english')), value: 'en' },
])
const showModal = computed({
get() {
return props.isShowModal
},
set(value: boolean) {
handleAudioPause()
emit('update:isShowModal', value)
},
})
const isHasTimberUrl = computed(() => {
return !!timberUrl.value
})
const confirmBtnDisabled = computed(() => {
return requestDataLoading.value || !selectedTimberId.value
})
watch(
() => showModal.value,
(value) => {
value && handleGetTimberList()
},
)
async function handleGetTimberList() {
requestDataLoading.value = true
selectedLanguage.value = props.timbreInfo?.matchLang || systemLanguageStore.currentLanguageInfo.key
const res = await fetchGetTimbreList<TimbreLanguageInfoItem[]>()
if (res.code === 0) {
requestDataLoading.value = false
languageInfoList.value = res.data
timbreInfoList.value =
languageInfoList.value.find((item) => item.matchLang === selectedLanguage.value)?.timbreInfo || []
selectedTimberId.value = props.timbreInfo?.timbreInfo?.[0]?.timbreId || timbreInfoList.value?.[0]?.timbreId || ''
timberUrl.value = props.timbreInfo?.timbreInfo?.[0]?.voiceUrl || timbreInfoList.value?.[0]?.voiceUrl || ''
handleRenderTimbreOption()
}
}
function handleRenderTimbreOption() {
timbreOptionList.value = timbreInfoList.value.map((item) => {
return {
label: () =>
h('div', { class: 'flex items-baseline justify-between w-[180px]' }, [
h('span', { class: 'max-w-[160px] line-clamp-1 whitespace-normal' }, { default: () => item.timbreName }),
h('i', {
class: {
'iconfont icon-playing ': item.isPlaying,
'iconfont icon-stop text-[14px]': !item.isPlaying,
'text-theme-color text-[14px]': true,
},
onClick: (e) => {
e.stopPropagation()
timbreInfoList.value.forEach((mockItem) => {
if (mockItem.timbreId !== item.timbreId) {
mockItem.isPlaying = false
}
})
isPlaySound.value = false
item.isPlaying = !item.isPlaying
if (item.isPlaying) {
handleAudioPlay(item.voiceUrl)
}
},
}),
]),
value: item.timbreId,
nickName: item.timbreName,
timberUrl: item.voiceUrl,
}
})
timbreRenderTag = ({ option }) => {
return h('span', {}, { default: () => option.nickName as string })
}
}
function handleUpdateTimberLanguage(value: string) {
timbreInfoList.value = languageInfoList.value.find((item) => item.matchLang === value)?.timbreInfo || []
selectedTimberId.value = timbreInfoList.value?.[0].timbreId || ''
timberUrl.value = timbreInfoList.value?.[0].voiceUrl || ''
isPlaySound.value = false
handleAudioPause()
handleRenderTimbreOption()
}
function handleUpdateTimber(_value: string, option: SelectOption) {
timberUrl.value = option.timberUrl as string
}
function handleClickTimber() {
isPlaySound.value = !isPlaySound.value
if (isPlaySound.value) {
timbreInfoList.value.forEach((item) => (item.isPlaying = false))
handleAudioPlay(timberUrl.value)
} else {
handleAudioPause()
}
}
function handleUpdateTimberId() {
selectedTimberId.value && emit('confirm', selectedTimberId.value)
}
function howlSoundFactory(url: string) {
const soundCtl = new Howl({
src: [url],
format: ['mpeg'],
html5: !validBrowser(),
preload: true,
autoplay: true,
onplay: () => {
currentSoundCtl.value = soundCtl
},
onend: () => {
soundCtl.unload()
timbreInfoList.value.forEach((timbreItem) => (timbreItem.isPlaying = false))
isPlaySound.value = false
},
})
return soundCtl
}
function handleAudioPlay(audioUrl: string) {
if (currentSoundCtl.value) {
currentSoundCtl.value.pause()
currentSoundCtl.value.unload()
currentSoundCtl.value = null
}
howlSoundFactory(audioUrl)
}
function handleAudioPause() {
if (currentSoundCtl.value) {
currentSoundCtl.value.pause()
currentSoundCtl.value.unload()
currentSoundCtl.value = null
}
timbreInfoList.value.forEach((timbreItem) => (timbreItem.isPlaying = false))
isPlaySound.value = false
}
</script>
<template>
<CustomModal
v-model:is-show="showModal"
:title="modalTitle"
:btn-loading="btnLoading"
:btn-disabled="confirmBtnDisabled"
:width="515"
:height="378"
@confirm="handleUpdateTimberId"
>
<template #content>
<div class="text-gray-font-color mb-2">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_voice_message') }}
</div>
<div v-show="!requestDataLoading" class="flex items-center gap-3">
<n-select
v-model:value="selectedLanguage"
class="w-[211px]!"
:options="languageOptions"
@update:value="handleUpdateTimberLanguage"
/>
<n-select
v-model:value="selectedTimberId"
class="w-[211px]!"
:options="timbreOptionList"
:render-tag="timbreRenderTag"
:show-checkmark="false"
@update:value="handleUpdateTimber"
/>
<div
class="flex h-5 w-5 items-center justify-center rounded-full bg-[#eff0ff]"
:class="isHasTimberUrl ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'"
>
<i
class="iconfont text-theme-color text-[13px] leading-[13px]"
:class="isPlaySound ? 'icon-playing' : 'icon-stop'"
@click="handleClickTimber"
/>
</div>
</div>
<div v-show="requestDataLoading" class="flex items-center gap-3">
<n-skeleton height="32px" />
<n-skeleton height="32px" />
<n-skeleton height="20px" width="20px" circle class="flex-shrink-0" />
</div>
</template>
</CustomModal>
</template>
export interface TimbreInfoItem {
timbreId: string
timbreName: string
voiceUrl: string
isPlaying: boolean
}
export interface TimbreLanguageInfoItem {
language: number
matchLang: string
timbreInfo: TimbreInfoItem[]
}
...@@ -2,14 +2,12 @@ ...@@ -2,14 +2,12 @@
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Computer, PreviewOpen, AllApplication, SettingOne } from '@icon-park/vue-next' import { Computer, PreviewOpen, AllApplication, SettingOne } from '@icon-park/vue-next'
import useTableScrollY from '@/composables/useTableScrollY' import useTableScrollY from '@/composables/useTableScrollY'
import { copyToClip } from '@/utils/copy' import { copyToClip } from '@/utils/copy'
import { formatDateTime } from '@/utils/date-formatter' import { formatDateTime } from '@/utils/date-formatter'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import SaleApplicationsConfigurationModal from '../../personal-app/sale-applications-configuration-modal.vue' import SaleApplicationsConfigurationModal from '@/views/personal-space/personal-app/sale-applications-configuration-modal.vue'
import { import {
fetchGetApplicationInfo, fetchGetApplicationInfo,
fetchGetApplicationMallInfo, fetchGetApplicationMallInfo,
...@@ -82,8 +80,7 @@ function handleClickChannelPublishTableAction(actionType: string) { ...@@ -82,8 +80,7 @@ function handleClickChannelPublishTableAction(actionType: string) {
} }
function handleAccessPage() { function handleAccessPage() {
const channelUrl = `${window.location.origin}/fe/share/web_source/${router.currentRoute.value.params.agentId}` router.push({ name: 'ShareWebApplication', params: { agentId: router.currentRoute.value.params.agentId } })
location.href = channelUrl
} }
function handleCopyShareLink() { function handleCopyShareLink() {
......
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import CustomLoading from './custom-loading.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import { useUserStore } from '@/store/modules/user'
interface Props {
role: 'user' | 'assistant'
messageItem: ConversationMessageItem
}
const { t } = useI18n()
const userStore = useUserStore()
defineProps<Props>()
const personalAppConfigStore = usePersonalAppConfigStore()
const useAvatar = computed(() => {
return userStore.userInfo.avatarUrl || 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/1725952917468.png'
})
const assistantAvatar = computed(() => {
return personalAppConfigStore.baseInfo.agentAvatar
})
</script>
<template>
<div class="mb-5 flex last:mb-0">
<NImage
:src="role === 'user' ? useAvatar : assistantAvatar"
preview-disabled
:width="32"
:height="32"
object-fit="cover"
class="mr-2 mt-1.5 h-8 w-8 rounded-full"
/>
<div
class="min-w-[80px] max-w-[calc(100%-32px-12px)] flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
:class="role === 'user' ? 'bg-[#4b87ff] text-white' : 'bg-white text-[#333]'"
>
<div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4">
<CustomLoading />
</div>
<div v-else>
<p class="break-all">
<MarkdownRender
:raw-text-content="
messageItem.isEmptyContent
? t('common_module.dialogue_module.empty_message_content')
: messageItem.textContent
"
:color="role === 'user' ? '#fff' : '#192338'"
/>
</p>
<div v-show="role === 'assistant' && messageItem.isAnswerResponseLoading" class="mb-[5px] mt-4 px-4">
<CustomLoading />
</div>
</div>
</div>
</div>
</template>
...@@ -4,9 +4,8 @@ import { useRouter } from 'vue-router' ...@@ -4,9 +4,8 @@ import { useRouter } from 'vue-router'
import { Emitter } from 'mitt' import { Emitter } from 'mitt'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import PageNarBar from './components/page-narbar.vue' import PageNarBar from './components/page-narbar.vue'
import AppSetting from './components/app-setting.vue' import AgentConfig from './components/agent-config/agent-config.vue'
import AppPreview from './components/app-preview.vue' import AgentPublish from './components/agent-publish.vue'
import AppPublish from './components/app-publish.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config' import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { fetchGetDebugApplicationInfo } from '@/apis/agent-application' import { fetchGetDebugApplicationInfo } from '@/apis/agent-application'
...@@ -79,13 +78,11 @@ function handleChangeAgentAppTabKey(currentTabKey: string) { ...@@ -79,13 +78,11 @@ function handleChangeAgentAppTabKey(currentTabKey: string) {
<div class="h-content flex w-full flex-1"> <div class="h-content flex w-full flex-1">
<div v-if="currentAgentAppTabKey === 'config'" class="flex h-full w-full flex-1"> <div v-if="currentAgentAppTabKey === 'config'" class="flex h-full w-full flex-1">
<AppSetting /> <AgentConfig />
<AppPreview />
</div> </div>
<div v-if="currentAgentAppTabKey === 'publish'" class="flex h-full w-full flex-1"> <div v-if="currentAgentAppTabKey === 'publish'" class="flex h-full w-full flex-1">
<AppPublish /> <AgentPublish />
</div> </div>
</div> </div>
......
...@@ -2,18 +2,25 @@ ...@@ -2,18 +2,25 @@
import { computed, inject, onMounted, onUnmounted, ref } from 'vue' import { computed, inject, onMounted, onUnmounted, ref } from 'vue'
import { Emitter } from 'mitt' import { Emitter } from 'mitt'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { nanoid } from 'nanoid'
import { fetchCustomEventSource } from '@/composables/useEventSource' import { fetchCustomEventSource } from '@/composables/useEventSource'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { UploadStatus } from '@/enums/upload-status' import { UploadStatus } from '@/enums/upload-status'
import { useDialogueFile } from '@/composables/useDialogueFile' import { useDialogueFile } from '@/composables/useDialogueFile'
import { useLayoutConfig } from '@/composables/useLayoutConfig' import { useLayoutConfig } from '@/composables/useLayoutConfig'
import { TEXTTOSPEECH_WS_URL } from '@/config/base-url'
import WebSocketCtr from '@/utils/web-socket-ctr'
interface Props { interface Props {
agentId: string agentId: string
dialogsId: string dialogsId: string
messageList: ConversationMessageItem[] messageList: Map<string, ConversationMessageItem>
continuousQuestionStatus: 'default' | 'close' continuousQuestionStatus: 'default' | 'close'
isEnableDocumentParse: boolean isEnableDocumentParse: boolean
isEnableVoice: boolean
answerAudioAutoPlay: boolean
answerAudioPlaying: boolean
timbreId: string
} }
const { t } = useI18n() const { t } = useI18n()
...@@ -21,14 +28,16 @@ const { t } = useI18n() ...@@ -21,14 +28,16 @@ const { t } = useI18n()
const props = defineProps<Props>() const props = defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
addMessageItem: [value: ConversationMessageItem] addMessageItem: [messageId: string, value: ConversationMessageItem]
updateSpecifyMessageItem: [messageItemIndex: number, newObj: Partial<ConversationMessageItem>] updateSpecifyMessageItem: [messageId: string, newObj: Partial<ConversationMessageItem>]
deleteLastMessageItem: [] deleteMessageItem: [messageId: string]
updatePageScroll: [] updatePageScroll: []
clearAllMessage: [] clearAllMessage: []
toLogin: [] toLogin: []
createContinueQuestions: [value: string] createContinueQuestions: [value: string]
resetContinueQuestionList: [] resetContinueQuestionList: []
audioPlay: [messageItem: ConversationMessageItem, requestId?: string]
audioPause: []
}>() }>()
const { isMobile } = useLayoutConfig() const { isMobile } = useLayoutConfig()
...@@ -40,8 +49,14 @@ const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = us ...@@ -40,8 +49,14 @@ const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = us
const emitter = inject<Emitter<MittEvents>>('emitter') const emitter = inject<Emitter<MittEvents>>('emitter')
const inputMessageContent = ref('') const inputMessageContent = ref('')
const isAnswerResponseWait = ref(false) const isAnswerResponseWait = ref(false)
const currentReplyContentSentenceExtractIndex = ref(0)
const sentenceFragmentSerialNo = ref(0)
const sentenceExtractCheckEnabled = ref(false)
const assistantFullAnswerContent = ref('')
const sentenceSpeechException = ref(false)
const messageAudioLoading = ref(false)
const currentLatestMessageItemKeyMap = ref(new Map<'assistant' | 'user', string>())
let controller: AbortController | null = null let controller: AbortController | null = null
...@@ -50,7 +65,7 @@ const isLogin = computed(() => { ...@@ -50,7 +65,7 @@ const isLogin = computed(() => {
}) })
const isAllowClearMessage = computed(() => { const isAllowClearMessage = computed(() => {
return props.messageList.length > 0 return props.messageList.size > 0
}) })
const isSendBtnDisabled = computed(() => { const isSendBtnDisabled = computed(() => {
...@@ -94,7 +109,7 @@ onUnmounted(() => { ...@@ -94,7 +109,7 @@ onUnmounted(() => {
emitter?.off('selectQuestion') emitter?.off('selectQuestion')
}) })
function messageItemFactory() { function messageItemFactory(): ConversationMessageItem {
return { return {
timestamp: Date.now(), timestamp: Date.now(),
role: 'user', role: 'user',
...@@ -102,7 +117,10 @@ function messageItemFactory() { ...@@ -102,7 +117,10 @@ function messageItemFactory() {
isEmptyContent: false, isEmptyContent: false,
isTextContentLoading: false, isTextContentLoading: false,
isAnswerResponseLoading: false, isAnswerResponseLoading: false,
} as const isVoiceLoading: false,
isVoicePlaying: false,
voiceFragmentUrlList: [],
}
} }
function handleMessageSend() { function handleMessageSend() {
...@@ -111,25 +129,49 @@ function handleMessageSend() { ...@@ -111,25 +129,49 @@ function handleMessageSend() {
return return
} }
if (!inputMessageContent.value.trim() || isAnswerResponseWait.value || isInputMessageDisabled.value) return '' if (!inputMessageContent.value.trim() || isInputMessageDisabled.value) {
return
}
if (isAnswerResponseWait.value || messageAudioLoading.value) {
window.$message.warning(t('common_module.dialogue_module.do_not_operate_until_the_reply_is_complete'))
return
}
if (props.answerAudioPlaying) {
window.$message.warning(t('common_module.dialogue_module.stop_playing_and_then_operate'))
return
}
const latestUserMessageKey = nanoid()
const latestAssistantMessageKey = nanoid()
currentLatestMessageItemKeyMap.value.set('user', latestUserMessageKey)
currentLatestMessageItemKeyMap.value.set('assistant', latestAssistantMessageKey)
emit('resetContinueQuestionList') emit('resetContinueQuestionList')
emit('addMessageItem', { ...messageItemFactory(), textContent: inputMessageContent.value }) emit('addMessageItem', latestUserMessageKey, { ...messageItemFactory(), textContent: inputMessageContent.value })
emit('updatePageScroll') emit('updatePageScroll')
emit('addMessageItem', { emit('addMessageItem', latestAssistantMessageKey, {
...messageItemFactory(), ...messageItemFactory(),
role: 'assistant', role: 'assistant',
isTextContentLoading: true, isTextContentLoading: true,
isAnswerResponseLoading: true, isAnswerResponseLoading: true,
isVoiceLoading: true,
}) })
emit('updatePageScroll') emit('updatePageScroll')
const input = inputMessageContent.value const input = inputMessageContent.value
const currentMessageIndex = props.messageList.length - 1
let replyTextContent = '' let replyTextContent = ''
isAnswerResponseWait.value = true isAnswerResponseWait.value = true
inputMessageContent.value = '' inputMessageContent.value = ''
currentReplyContentSentenceExtractIndex.value = 0
sentenceFragmentSerialNo.value = 0
sentenceExtractCheckEnabled.value = false
assistantFullAnswerContent.value = ''
sentenceSpeechException.value = false
messageAudioLoading.value = false
controller = new AbortController() controller = new AbortController()
...@@ -144,11 +186,12 @@ function handleMessageSend() { ...@@ -144,11 +186,12 @@ function handleMessageSend() {
controller, controller,
onMessage: (data: any) => { onMessage: (data: any) => {
if (data === '[DONE]') { if (data === '[DONE]') {
emit('updateSpecifyMessageItem', currentMessageIndex, { emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
isEmptyContent: !replyTextContent, isEmptyContent: !replyTextContent,
isTextContentLoading: false, isTextContentLoading: false,
isAnswerResponseLoading: false, isAnswerResponseLoading: false,
}) })
isCreateContinueQuestions.value && emit('createContinueQuestions', replyTextContent) isCreateContinueQuestions.value && emit('createContinueQuestions', replyTextContent)
emit('updatePageScroll') emit('updatePageScroll')
blockMessageResponse() blockMessageResponse()
...@@ -158,7 +201,18 @@ function handleMessageSend() { ...@@ -158,7 +201,18 @@ function handleMessageSend() {
if (data) { if (data) {
replyTextContent += data replyTextContent += data
emit('updateSpecifyMessageItem', currentMessageIndex, { assistantFullAnswerContent.value = (assistantFullAnswerContent.value + data).replace(
/\^\[[\d\\[\]-]+?\]\^/g,
'',
)
if (!sentenceExtractCheckEnabled.value && props.isEnableVoice) {
sentenceExtract(latestAssistantMessageKey)
sentenceExtractCheckEnabled.value = true
messageAudioLoading.value = true
}
emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
textContent: replyTextContent, textContent: replyTextContent,
isTextContentLoading: false, isTextContentLoading: false,
}) })
...@@ -178,12 +232,13 @@ function handleMessageSend() { ...@@ -178,12 +232,13 @@ function handleMessageSend() {
} }
function errorMessageResponse() { function errorMessageResponse() {
emit('updateSpecifyMessageItem', props.messageList.length - 1, { emit('updateSpecifyMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!, {
isTextContentLoading: false, isTextContentLoading: false,
textContent: '', textContent: '',
}) })
emit('deleteLastMessageItem') emit('deleteMessageItem', currentLatestMessageItemKeyMap.value.get('user')!)
emit('deleteLastMessageItem') emit('deleteMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!)
emit('audioPause')
blockMessageResponse() blockMessageResponse()
} }
...@@ -213,6 +268,108 @@ function handleSelectFile(cb: () => void) { ...@@ -213,6 +268,108 @@ function handleSelectFile(cb: () => void) {
cb() cb()
} }
function sentenceExtract(messageId: string) {
const symbolRegExp = /[。!?;.!?;]/g
let sentenceDraft = assistantFullAnswerContent.value
.replace(/\s{5,}/gi, '')
.slice(currentReplyContentSentenceExtractIndex.value)
let matchResult = symbolRegExp.exec(sentenceDraft)
function matchExtract() {
const article = assistantFullAnswerContent.value.replace(/\s{5,}/gi, '')
if (matchResult && matchResult.index && matchResult.index > 60) {
sentenceDraft = article.slice(
currentReplyContentSentenceExtractIndex.value,
currentReplyContentSentenceExtractIndex.value + matchResult.index + matchResult['0'].length,
)
currentReplyContentSentenceExtractIndex.value += sentenceDraft.length
ttsSocketSendText(sentenceDraft, sentenceFragmentSerialNo.value, messageId)
sentenceFragmentSerialNo.value += 1
if (article.length - currentReplyContentSentenceExtractIndex.value > 60) {
sentenceDraft = article.slice(currentReplyContentSentenceExtractIndex.value)
matchResult = symbolRegExp.exec(sentenceDraft)
matchExtract()
} else {
setTimeout(() => sentenceExtract(messageId), 600)
}
} else if (!isAnswerResponseWait.value) {
/* 延时避免最后回复内容没有更新全 */
setTimeout(() => {
sentenceDraft = article.slice(currentReplyContentSentenceExtractIndex.value)
ttsSocketSendText(sentenceDraft, sentenceFragmentSerialNo.value, messageId)
sentenceFragmentSerialNo.value += 1
}, 700)
} else {
sentenceDraft = assistantFullAnswerContent.value
.replace(/\s{5,}/gi, '')
.slice(currentReplyContentSentenceExtractIndex.value)
matchResult = symbolRegExp.exec(sentenceDraft)
setTimeout(() => matchExtract(), 500)
}
}
if (matchResult) matchExtract()
else setTimeout(() => sentenceExtract(messageId), 600)
}
function ttsSocketSendText(text: string, audioUrlSerialNo: number, messageId: string) {
if (sentenceSpeechException.value) {
return
}
const ttsSocketCtl = new WebSocketCtr(TEXTTOSPEECH_WS_URL)
ttsSocketCtl.onMessage = (data: { audio: string; replyVoiceUrl: string; final: boolean }) => {
if (data.replyVoiceUrl) {
if (props.messageList.get(messageId)?.voiceFragmentUrlList) {
const voiceFragmentUrlListDraft = [...props.messageList.get(messageId)!.voiceFragmentUrlList]
voiceFragmentUrlListDraft[audioUrlSerialNo] = data.replyVoiceUrl
messageAudioLoading.value = false
emit('updateSpecifyMessageItem', messageId, { voiceFragmentUrlList: voiceFragmentUrlListDraft })
if (props.answerAudioAutoPlay && audioUrlSerialNo === 0 && voiceFragmentUrlListDraft[audioUrlSerialNo]) {
emit('audioPlay', props.messageList.get(messageId)!)
}
if (!props.answerAudioAutoPlay) {
emit('updateSpecifyMessageItem', messageId, { isVoiceLoading: false })
}
}
}
}
ttsSocketCtl.onMessageError = () => {
emit('updateSpecifyMessageItem', messageId, { isVoiceLoading: false, voiceFragmentUrlList: [] })
props.messageList.get(messageId)?.isVoicePlaying && emit('audioPause')
sentenceSpeechException.value = true
messageAudioLoading.value = false
window.$message.error(t('common_module.unplayable_tip'))
}
const content = (text || '').replace(/\^\[[\d\\[\]-]+?\]\^/g, '')
if (content && props.timbreId) {
ttsSocketCtl.connect(() => {
ttsSocketCtl.send({
codec: 'wav',
sampleRate: 16000,
speed: 0,
voiceType: props.timbreId,
volume: 0,
content,
})
})
}
}
defineExpose({ defineExpose({
blockMessageResponse, blockMessageResponse,
}) })
...@@ -280,7 +437,12 @@ defineExpose({ ...@@ -280,7 +437,12 @@ defineExpose({
<div <div
class="bg-px-send-png absolute bottom-2 right-[20px] h-[24px] w-[24px]" class="bg-px-send-png absolute bottom-2 right-[20px] h-[24px] w-[24px]"
:class=" :class="
isSendBtnDisabled || isAnswerResponseWait || !isLogin || isInputMessageDisabled isSendBtnDisabled ||
isAnswerResponseWait ||
!isLogin ||
isInputMessageDisabled ||
answerAudioPlaying ||
messageAudioLoading
? 'opacity-60' ? 'opacity-60'
: 'cursor-pointer' : 'cursor-pointer'
" "
...@@ -336,8 +498,9 @@ defineExpose({ ...@@ -336,8 +498,9 @@ defineExpose({
? 'hover:text-theme-color text-font-color cursor-pointer' ? 'hover:text-theme-color text-font-color cursor-pointer'
: 'cursor-not-allowed text-[#b8babf]' : 'cursor-not-allowed text-[#b8babf]'
" "
@click="handleClearAllMessage"
> >
<i class="iconfont icon-clear text-base leading-none" @click="handleClearAllMessage" /> <i class="iconfont icon-clear text-base leading-none" />
</div> </div>
</template> </template>
<span class="text-xs"> {{ t('common_module.dialogue_module.clear_message_popover_message') }}</span> <span class="text-xs"> {{ t('common_module.dialogue_module.clear_message_popover_message') }}</span>
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import CustomLoading from './custom-loading.vue' import CustomLoading from './custom-loading.vue'
import MusicWavesLoading from './music-waves-loading.vue'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue' import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { useLayoutConfig } from '@/composables/useLayoutConfig' import { useLayoutConfig } from '@/composables/useLayoutConfig'
...@@ -17,6 +18,11 @@ const { t } = useI18n() ...@@ -17,6 +18,11 @@ const { t } = useI18n()
const props = defineProps<Props>() const props = defineProps<Props>()
const emit = defineEmits<{
audioPlay: []
audioPause: []
}>()
const userStore = useUserStore() const userStore = useUserStore()
const { isMobile } = useLayoutConfig() const { isMobile } = useLayoutConfig()
...@@ -31,6 +37,34 @@ const assistantAvatar = computed(() => { ...@@ -31,6 +37,34 @@ const assistantAvatar = computed(() => {
'https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp' 'https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp'
) )
}) })
const timbreEnabled = computed(() => {
return !!props.agentApplicationConfig.voiceConfig.timbreId
})
const isShowAudioControl = computed(() => {
return props.role === 'assistant' && !props.messageItem.isVoiceLoading && timbreEnabled.value
})
const isPlayableAudio = computed(() => {
return isShowAudioControl.value && !!props.messageItem.voiceFragmentUrlList.length
})
const isShowWebVoiceLoading = computed(() => {
return props.role === 'assistant' && !isMobile.value && props.messageItem.isVoiceLoading && timbreEnabled.value
})
function handleAudioControl() {
if (!isPlayableAudio.value) {
return
}
if (props.messageItem.isVoicePlaying) {
emit('audioPause')
} else {
emit('audioPlay')
}
}
</script> </script>
<template> <template>
...@@ -48,36 +82,88 @@ const assistantAvatar = computed(() => { ...@@ -48,36 +82,88 @@ const assistantAvatar = computed(() => {
:width="32" :width="32"
:height="32" :height="32"
object-fit="cover" object-fit="cover"
class="mr-2 mt-1.5 h-8 w-8 rounded-full" class="mr-2 mt-1.5 h-8 w-8 flex-shrink-0 rounded-full"
/> />
<div <div class="flex w-full flex-col" :class="isMobile && role === 'user' ? 'items-end' : 'items-start'">
class="min-w-[80px] flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]" <div
:class="[ class="min-w-[80px] flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
role === 'user' ? 'bg-theme-color text-white' : 'bg-white text-[#333]', :class="[
isMobile ? 'max-w-[calc(100%-20px)]' : 'max-w-[calc(100%-32px-12px)]', role === 'user' ? 'bg-theme-color text-white' : 'bg-white text-[#333]',
]" isMobile ? 'max-w-[calc(100%-20px)]' : 'max-w-full',
> ]"
<div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4"> >
<CustomLoading /> <div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4">
</div> <CustomLoading />
</div>
<div v-else> <div v-else>
<p class="break-all"> <p class="break-all">
<MarkdownRender <MarkdownRender
:raw-text-content=" :raw-text-content="
messageItem.isEmptyContent messageItem.isEmptyContent
? t('common_module.dialogue_module.empty_message_content') ? t('common_module.dialogue_module.empty_message_content')
: messageItem.textContent : messageItem.textContent
"
:color="role === 'user' ? '#fff' : '#192338'"
/>
</p>
<div
v-show="
role === 'assistant' && (messageItem.isAnswerResponseLoading || (isMobile && messageItem.isVoiceLoading))
" "
:color="role === 'user' ? '#fff' : '#192338'" class="mb-[5px] mt-4 px-4"
>
<CustomLoading />
</div>
</div>
<div v-show="isShowAudioControl && isMobile" class="mt-[13px] flex items-center gap-2">
<div
class="h-[18px] w-[18px] cursor-pointer"
:class="messageItem.isVoicePlaying ? 'bg-svg-pause' : 'bg-svg-play'"
@click="handleAudioControl"
/> />
</p>
<div v-show="role === 'assistant' && messageItem.isAnswerResponseLoading" class="mb-[5px] mt-4 px-4"> <MusicWavesLoading v-show="messageItem.isVoicePlaying && isPlayableAudio" bar-bg-color="#333" />
<CustomLoading />
<n-popover style="max-width: 310px">
<template #trigger>
<span v-show="!isPlayableAudio" class="text-[12px]"> {{ t('common_module.unplayable') }} </span>
</template>
{{ t('common_module.unplayable_tip') }}
</n-popover>
</div> </div>
</div> </div>
<div
v-show="isShowAudioControl && !isMobile"
class="text-font-color flex items-center gap-0.5"
:class="isPlayableAudio ? 'hover:text-theme-color cursor-pointer hover:opacity-80' : 'cursor-not-allowed'"
@click="handleAudioControl"
>
<i v-if="!messageItem.isVoicePlaying" class="iconfont icon-play text-[24px]" />
<div v-else class="mx-1.5 my-3 h-[12px] w-[12px] bg-[url(@/assets/images/playing.gif)] bg-[length:100%_100%]" />
<span
v-show="isPlayableAudio"
class="text-[12px]"
:class="messageItem.isVoicePlaying ? 'text-theme-color' : ''"
>
{{ messageItem.isVoicePlaying ? t('common_module.stop_playing') : t('common_module.start_playing') }}
</span>
<n-popover style="max-width: 310px">
<template #trigger>
<span v-show="!isPlayableAudio" class="text-[12px]"> {{ t('common_module.unplayable') }} </span>
</template>
{{ t('common_module.unplayable_tip') }}
</n-popover>
</div>
<div v-if="isShowWebVoiceLoading" class="py-3.5 pl-5">
<CustomLoading />
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -6,7 +6,7 @@ import { PersonalAppConfigState } from '@/store/types/personal-app-config' ...@@ -6,7 +6,7 @@ import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { computed } from 'vue' import { computed } from 'vue'
interface Props { interface Props {
messageList: ConversationMessageItem[] messageList: Map<string, ConversationMessageItem>
agentApplicationConfig: PersonalAppConfigState agentApplicationConfig: PersonalAppConfigState
continuousQuestionStatus: 'default' | 'close' continuousQuestionStatus: 'default' | 'close'
continuousQuestionList: string[] continuousQuestionList: string[]
...@@ -14,13 +14,18 @@ interface Props { ...@@ -14,13 +14,18 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
defineEmits<{
audioPlay: [messageItem: ConversationMessageItem]
audioPause: []
}>()
const { scrollRef, scrollToBottom } = useScroll() const { scrollRef, scrollToBottom } = useScroll()
const isShowContinueQuestion = computed(() => { const isShowContinueQuestion = computed(() => {
return ( return (
props.continuousQuestionStatus === 'default' && props.continuousQuestionStatus === 'default' &&
props.messageList.length > 1 && props.messageList.size > 1 &&
!props.messageList[props.messageList.length - 1].isAnswerResponseLoading !Array.from(props.messageList.entries()).pop()?.[1].isAnswerResponseLoading
) )
}) })
...@@ -33,11 +38,13 @@ defineExpose({ ...@@ -33,11 +38,13 @@ defineExpose({
<main ref="scrollRef" class="h-full overflow-y-auto px-5"> <main ref="scrollRef" class="h-full overflow-y-auto px-5">
<div> <div>
<MessageItem <MessageItem
v-for="messageItem in messageList" v-for="[key, messageItem] in messageList"
:key="messageItem.timestamp" :key="key"
:role="messageItem.role" :role="messageItem.role"
:message-item="messageItem" :message-item="messageItem"
:agent-application-config="agentApplicationConfig" :agent-application-config="agentApplicationConfig"
@audio-play="() => $emit('audioPlay', messageItem)"
@audio-pause="() => $emit('audioPause')"
/> />
</div> </div>
......
...@@ -38,7 +38,7 @@ function handleToLogin() { ...@@ -38,7 +38,7 @@ function handleToLogin() {
<NButton <NButton
v-show="isLogin" v-show="isLogin"
type="primary" type="primary"
class="rounded-md! h-[32px]! text-xs! w-[80px]!" class="rounded-md! h-[32px]! text-xs! min-w-[80px]!"
@click="handleToCreateApplication" @click="handleToCreateApplication"
> >
{{ t('common_module.create_agent_btn_text') }} {{ t('common_module.create_agent_btn_text') }}
......
<script setup lang="ts">
interface Props {
barBgColor?: string
height?: string
}
withDefaults(defineProps<Props>(), {
barBgColor: '#363636',
height: '12px',
})
</script>
<template>
<div class="music">
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
</div>
</template>
<style lang="scss" scoped>
.music {
display: flex;
align-items: center;
justify-content: space-between;
width: 42px;
height: v-bind(height);
.bar {
width: 2px;
/* stylelint-disable-next-line value-keyword-case */
background: v-bind(barBgColor);
border-radius: 50px;
animation: loader 1.5s ease-in-out infinite;
&:nth-child(1) {
/* background: purple; */
animation-delay: 1s;
}
&:nth-child(2) {
/* background: crimson; */
animation-delay: 0.8s;
}
&:nth-child(3) {
/* background: deeppink; */
animation-delay: 0.6s;
}
&:nth-child(4) {
/* background: orange; */
animation-delay: 0.4s;
}
&:nth-child(5) {
/* background: gold; */
animation-delay: 0.2s;
}
&:nth-child(6) {
/* background: gold; */
animation-delay: 0.2s;
}
&:nth-child(7) {
/* background: gold; */
animation-delay: 0.4s;
}
&:nth-child(8) {
/* background: deeppink; */
animation-delay: 0.6s;
}
&:nth-child(9) {
/* background: crimson; */
animation-delay: 0.8s;
}
&:nth-child(10) {
/* background: purple; */
animation-delay: 1s;
}
}
}
@keyframes loader {
0% {
height: 4px;
}
50% {
height: v-bind(height);
}
100% {
height: 4px;
}
}
</style>
...@@ -55,8 +55,8 @@ function handleToLogin() { ...@@ -55,8 +55,8 @@ function handleToLogin() {
<span class="mb-1 line-clamp-1 max-w-[200px] break-all">{{ agentApplicationConfig.baseInfo.agentTitle }}</span> <span class="mb-1 line-clamp-1 max-w-[200px] break-all">{{ agentApplicationConfig.baseInfo.agentTitle }}</span>
<div class="flex items-center text-xs text-[#84868c]"> <div class="flex items-center text-xs text-[#84868c]">
<img v-show="isLogin" :src="agentMemberInfo.avatarUrl" class="mr-2 h-5 w-5 rounded-full" /> <img v-show="isLogin" :src="agentMemberInfo.avatarUrl" class="mr-2 h-5 w-5 rounded-full" />
<n-ellipsis v-show="isLogin" class="max-w-[120px]! mr-4"> <n-ellipsis class="max-w-[120px]! mr-4">
<span class="select-none">{{ agentMemberInfo.nickName }}</span> <span v-show="isLogin" class="select-none">{{ agentMemberInfo.nickName }}</span>
</n-ellipsis> </n-ellipsis>
<span> <span>
{{ t('common_module.publish_time_in') }} {{ t('common_module.publish_time_in') }}
......
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref, shallowRef } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Howl } from 'howler'
import { ValueOf } from 'type-fest'
import PageHeader from './components/mobile-page-header.vue' import PageHeader from './components/mobile-page-header.vue'
import Preamble from './components/preamble.vue' import Preamble from './components/preamble.vue'
import MessageList from './components/message-list.vue' import MessageList from './components/message-list.vue'
...@@ -9,8 +11,15 @@ import FooterInput from './components/footer-input.vue' ...@@ -9,8 +11,15 @@ import FooterInput from './components/footer-input.vue'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { defaultPersonalAppConfigState } from '@/store/modules/personal-app-config' import { defaultPersonalAppConfigState } from '@/store/modules/personal-app-config'
import { fetchCreateContinueQuestions, fetchCreateDialogues, fetchGetApplicationInfo } from '@/apis/agent-application' import {
fetchCreateContinueQuestions,
fetchCreateDialogues,
fetchGetApplicationInfo,
fetchGetAutoPlayByAgentId,
fetchUpdateAutoPlay,
} from '@/apis/agent-application'
import { useLayoutConfig } from '@/composables/useLayoutConfig' import { useLayoutConfig } from '@/composables/useLayoutConfig'
import { validBrowser } from '@/utils/browser-detection'
const { t } = useI18n() const { t } = useI18n()
...@@ -29,15 +38,25 @@ const agentApplicationConfig = ref<PersonalAppConfigState>(defaultPersonalAppCon ...@@ -29,15 +38,25 @@ const agentApplicationConfig = ref<PersonalAppConfigState>(defaultPersonalAppCon
const messageListRef = ref<InstanceType<typeof MessageList> | null>(null) const messageListRef = ref<InstanceType<typeof MessageList> | null>(null)
const footerInputRef = ref<InstanceType<typeof FooterInput> | null>(null) const footerInputRef = ref<InstanceType<typeof FooterInput> | null>(null)
const messageList = ref<ConversationMessageItem[]>([]) const messageList = ref(new Map<string, ConversationMessageItem>())
const continuousQuestionStatus = ref<'default' | 'close'>('default') const continuousQuestionStatus = ref<'default' | 'close'>('default')
const continueQuestionList = ref<string[]>([]) const continueQuestionList = ref<string[]>([])
const answerAudioAutoPlay = ref(false) // 语音是否自动播放
const answerAudioPlaying = ref(false) // 语音播放中
const currentPlayMessageItem = ref<ConversationMessageItem | null>(null)
const currentPlayAudioFragmentSerialNo = ref(0)
const currentSoundCtl = shallowRef<Howl | null>(null)
const isSoundCtlCreated = ref(false)
const isEnableDocumentParse = computed(() => { const isEnableDocumentParse = computed(() => {
return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y' return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y'
}) })
const isEnableVoice = computed(() => {
return !!agentApplicationConfig.value.voiceConfig.timbreId
})
onMounted(async () => { onMounted(async () => {
if (router.currentRoute.value.params.agentId) { if (router.currentRoute.value.params.agentId) {
agentId.value = router.currentRoute.value.params.agentId as string agentId.value = router.currentRoute.value.params.agentId as string
...@@ -53,7 +72,10 @@ onMounted(async () => { ...@@ -53,7 +72,10 @@ onMounted(async () => {
if (agentId.value) { if (agentId.value) {
fullScreenLoading.value = true fullScreenLoading.value = true
await handleGetApplicationDetail() await handleGetApplicationDetail()
userStore.isLogin && (await handleCreateDialogues()) if (userStore.isLogin) {
await handleCreateDialogues()
await handleGetAutoPlayByAgentId()
}
fullScreenLoading.value = false fullScreenLoading.value = false
return return
} }
...@@ -61,6 +83,11 @@ onMounted(async () => { ...@@ -61,6 +83,11 @@ onMounted(async () => {
router.replace({ name: 'Home' }) router.replace({ name: 'Home' })
}) })
onUnmounted(() => {
handleAudioPause()
footerInputRef.value?.blockMessageResponse()
})
async function handleGetApplicationDetail() { async function handleGetApplicationDetail() {
fetchGetApplicationInfo<PersonalAppConfigState>(agentId.value) fetchGetApplicationInfo<PersonalAppConfigState>(agentId.value)
.then((res) => { .then((res) => {
...@@ -81,6 +108,14 @@ async function handleCreateDialogues() { ...@@ -81,6 +108,14 @@ async function handleCreateDialogues() {
} }
} }
async function handleGetAutoPlayByAgentId() {
const res = await fetchGetAutoPlayByAgentId<'Y' | 'N'>(agentId.value)
if (res.code === 0) {
answerAudioAutoPlay.value = res.data === 'Y'
}
}
function handleToLoginPage() { function handleToLoginPage() {
router.push({ router.push({
name: 'Login', name: 'Login',
...@@ -96,20 +131,36 @@ function handleCreateApplicationPage() { ...@@ -96,20 +131,36 @@ function handleCreateApplicationPage() {
}) })
} }
function handleAddMessageItem(messageItem: ConversationMessageItem) { async function handleUpdateAutoPlaying(isAutoPlaying: boolean) {
messageList.value.push(messageItem) const autoplay = isAutoPlaying ? 'Y' : 'N'
await fetchUpdateAutoPlay(agentId.value, autoplay)
} }
function handleUpdateSpecifyMessageItem(messageItemIndex: number, newObj: Partial<ConversationMessageItem>) { function handleAddMessageItem(messageId: string, messageItem: ConversationMessageItem) {
if (messageList.value[messageItemIndex]) { messageList.value.set(messageId, messageItem)
Object.entries(newObj).forEach(([k, v]) => { }
;(messageList.value[messageItemIndex] as any)[k as keyof typeof newObj] = v
function handleUpdateSpecifyMessageItem(messageId: string, newMessageItem: Partial<ConversationMessageItem>) {
const currentMessageItemInfo = messageList.value.get(messageId)
if (currentMessageItemInfo) {
const updatePropertyLength = Object.keys(newMessageItem).length
if (updatePropertyLength > 4) {
messageList.value.set(messageId, Object.assign({}, currentMessageItemInfo, newMessageItem))
return
}
Object.entries<ValueOf<typeof newMessageItem>>(newMessageItem).forEach(([key, value]) => {
if (Object.prototype.hasOwnProperty.call(currentMessageItemInfo, key)) {
;(currentMessageItemInfo as any)[key as keyof ConversationMessageItem] = value
}
}) })
} }
} }
function handleDeleteLastMessageItem() { function handleDeleteMessageItem(messageId: string) {
messageList.value.pop() messageList.value.delete(messageId)
} }
function handleUpdatePageScroll() { function handleUpdatePageScroll() {
...@@ -123,8 +174,10 @@ function handleClearAllMessage() { ...@@ -123,8 +174,10 @@ function handleClearAllMessage() {
t('common_module.dialogue_module.clear_message_dialog_title'), t('common_module.dialogue_module.clear_message_dialog_title'),
) )
.then(() => { .then(() => {
handleAudioPause()
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
messageList.value = [] messageList.value.clear()
answerAudioPlaying.value = false
window.$message.success(t('common_module.clear_success_message')) window.$message.success(t('common_module.clear_success_message'))
}) })
} }
...@@ -141,6 +194,94 @@ async function handleCreateContinueQuestions(replyTextContent: string) { ...@@ -141,6 +194,94 @@ async function handleCreateContinueQuestions(replyTextContent: string) {
function handleResetContinueQuestionList() { function handleResetContinueQuestionList() {
continueQuestionList.value = [] continueQuestionList.value = []
} }
function howlSoundFactory(url: string) {
const soundCtl = new Howl({
src: [url],
format: ['mpeg'],
html5: !validBrowser(),
preload: true,
autoplay: true,
onplay: () => {
answerAudioPlaying.value = true
currentSoundCtl.value = soundCtl
if (currentPlayMessageItem.value) {
currentPlayMessageItem.value.isVoiceLoading = false
currentPlayMessageItem.value.isVoicePlaying = true
}
},
onend: () => {
soundCtl.unload()
currentPlayAudioFragmentSerialNo.value += 1
if (
currentPlayMessageItem.value &&
currentPlayAudioFragmentSerialNo.value > currentPlayMessageItem.value.voiceFragmentUrlList.length - 1
) {
currentPlayMessageItem.value.isVoicePlaying = false
answerAudioPlaying.value = false
}
let audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value]
if (audioFragmentUrl) {
howlSoundFactory(audioFragmentUrl)
} else if (
(currentPlayMessageItem.value?.voiceFragmentUrlList || []).length > currentPlayAudioFragmentSerialNo.value
) {
let timerId: NodeJS.Timeout | null = null
const audioFragmentCheck = () => {
audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value]
if (audioFragmentUrl) {
timerId && clearInterval(timerId)
howlSoundFactory(audioFragmentUrl)
}
}
timerId = setInterval(audioFragmentCheck, 600)
}
},
})
return soundCtl
}
function handleAudioPlay(currentMessageItem: ConversationMessageItem, specificPlayUrl?: string) {
handleAudioPause()
currentPlayMessageItem.value = currentMessageItem
currentPlayAudioFragmentSerialNo.value = 0
const audioUrl = specificPlayUrl || currentMessageItem.voiceFragmentUrlList[0]
howlSoundFactory(audioUrl)
isSoundCtlCreated.value = true
}
function handleAudioPause(isClearMessageList = false) {
if (currentSoundCtl.value) {
currentSoundCtl.value.pause()
currentSoundCtl.value.unload()
currentSoundCtl.value = null
isSoundCtlCreated.value = false
}
currentPlayMessageItem.value && (currentPlayMessageItem.value.isVoicePlaying = false)
answerAudioPlaying.value = false
if (isClearMessageList) {
messageList.value.clear()
footerInputRef.value?.blockMessageResponse()
}
}
</script> </script>
<template> <template>
...@@ -152,11 +293,21 @@ function handleResetContinueQuestionList() { ...@@ -152,11 +293,21 @@ function handleResetContinueQuestionList() {
/> />
<div class="flex h-[calc(100%-48px)] w-full flex-col bg-[#f2f5f9]"> <div class="flex h-[calc(100%-48px)] w-full flex-col bg-[#f2f5f9]">
<div v-if="messageList.length === 0" class="w-full flex-1 overflow-auto px-4"> <div class="mt-5 flex select-none justify-end px-4">
<div v-show="isEnableVoice" class="flex items-center gap-2">
<span>{{ t('common_module.voice_auto_play') }}</span>
<n-switch v-model:value="answerAudioAutoPlay" size="small" @update:value="handleUpdateAutoPlaying">
<template #checked> {{ t('common_module.open') }} </template>
<template #unchecked> {{ t('common_module.close') }} </template>
</n-switch>
</div>
</div>
<div v-if="messageList.size === 0" class="w-full flex-1 overflow-auto px-4">
<Preamble :agent-application-config="agentApplicationConfig" /> <Preamble :agent-application-config="agentApplicationConfig" />
</div> </div>
<div v-if="messageList.length > 0" class="flex w-full flex-1 flex-col overflow-hidden pt-5"> <div v-if="messageList.size > 0" class="flex w-full flex-1 flex-col overflow-hidden pt-5">
<div class="flex-1 overflow-auto"> <div class="flex-1 overflow-auto">
<MessageList <MessageList
ref="messageListRef" ref="messageListRef"
...@@ -164,6 +315,8 @@ function handleResetContinueQuestionList() { ...@@ -164,6 +315,8 @@ function handleResetContinueQuestionList() {
:message-list="messageList" :message-list="messageList"
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:continuous-question-list="continueQuestionList" :continuous-question-list="continueQuestionList"
@audio-play="handleAudioPlay"
@audio-pause="handleAudioPause"
/> />
</div> </div>
</div> </div>
...@@ -176,14 +329,20 @@ function handleResetContinueQuestionList() { ...@@ -176,14 +329,20 @@ function handleResetContinueQuestionList() {
:agent-id="agentApplicationConfig.baseInfo.agentId" :agent-id="agentApplicationConfig.baseInfo.agentId"
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:is-enable-document-parse="isEnableDocumentParse" :is-enable-document-parse="isEnableDocumentParse"
:is-enable-voice="isEnableVoice"
:answer-audio-auto-play="answerAudioAutoPlay"
:answer-audio-playing="answerAudioPlaying"
:timbre-id="agentApplicationConfig.voiceConfig.timbreId"
@add-message-item="handleAddMessageItem" @add-message-item="handleAddMessageItem"
@update-specify-message-item="handleUpdateSpecifyMessageItem" @update-specify-message-item="handleUpdateSpecifyMessageItem"
@delete-last-message-item="handleDeleteLastMessageItem" @delete-message-item="handleDeleteMessageItem"
@update-page-scroll="handleUpdatePageScroll" @update-page-scroll="handleUpdatePageScroll"
@clear-all-message="handleClearAllMessage" @clear-all-message="handleClearAllMessage"
@to-login="handleToLoginPage" @to-login="handleToLoginPage"
@create-continue-questions="handleCreateContinueQuestions" @create-continue-questions="handleCreateContinueQuestions"
@reset-continue-question-list="handleResetContinueQuestionList" @reset-continue-question-list="handleResetContinueQuestionList"
@audio-play="handleAudioPlay"
@audio-pause="handleAudioPause"
/> />
</div> </div>
</div> </div>
......
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref, shallowRef } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Howl } from 'howler'
import { ValueOf } from 'type-fest'
import PageHeader from './components/web-page-header.vue' import PageHeader from './components/web-page-header.vue'
import Preamble from './components/preamble.vue' import Preamble from './components/preamble.vue'
import MessageList from './components/message-list.vue' import MessageList from './components/message-list.vue'
import FooterInput from './components/footer-input.vue' import FooterInput from './components/footer-input.vue'
import { fetchCreateContinueQuestions, fetchCreateDialogues, fetchGetApplicationInfo } from '@/apis/agent-application' import {
fetchCreateContinueQuestions,
fetchCreateDialogues,
fetchGetApplicationInfo,
fetchGetAutoPlayByAgentId,
fetchUpdateAutoPlay,
} from '@/apis/agent-application'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { defaultPersonalAppConfigState } from '@/store/modules/personal-app-config' import { defaultPersonalAppConfigState } from '@/store/modules/personal-app-config'
import { createDefaultUserInfoFactory, useUserStore } from '@/store/modules/user' import { createDefaultUserInfoFactory, useUserStore } from '@/store/modules/user'
import { useLayoutConfig } from '@/composables/useLayoutConfig' import { useLayoutConfig } from '@/composables/useLayoutConfig'
import { fetchGetMemberInfoById } from '@/apis/user' import { fetchGetMemberInfoById } from '@/apis/user'
import { UserInfo } from '@/store/types/user' import { UserInfo } from '@/store/types/user'
import { validBrowser } from '@/utils/browser-detection'
const { t } = useI18n() const { t } = useI18n()
...@@ -32,15 +41,25 @@ const agentApplicationConfig = ref<PersonalAppConfigState>(defaultPersonalAppCon ...@@ -32,15 +41,25 @@ const agentApplicationConfig = ref<PersonalAppConfigState>(defaultPersonalAppCon
const messageListRef = ref<InstanceType<typeof MessageList> | null>(null) const messageListRef = ref<InstanceType<typeof MessageList> | null>(null)
const footerInputRef = ref<InstanceType<typeof FooterInput> | null>(null) const footerInputRef = ref<InstanceType<typeof FooterInput> | null>(null)
const messageList = ref<ConversationMessageItem[]>([]) const messageList = ref(new Map<string, ConversationMessageItem>())
const continuousQuestionStatus = ref<'default' | 'close'>('default') const continuousQuestionStatus = ref<'default' | 'close'>('default')
const continueQuestionList = ref<string[]>([]) const continueQuestionList = ref<string[]>([])
const answerAudioAutoPlay = ref(false)
const answerAudioPlaying = ref(false) // 语音播放中
const currentPlayMessageItem = ref<ConversationMessageItem | null>(null)
const currentPlayAudioFragmentSerialNo = ref(0)
const currentSoundCtl = shallowRef<Howl | null>(null)
const isSoundCtlCreated = ref(false)
const isEnableDocumentParse = computed(() => { const isEnableDocumentParse = computed(() => {
return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y' return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y'
}) })
const isEnableVoice = computed(() => {
return !!agentApplicationConfig.value.voiceConfig.timbreId
})
onMounted(async () => { onMounted(async () => {
if (router.currentRoute.value.params.agentId) { if (router.currentRoute.value.params.agentId) {
agentId.value = router.currentRoute.value.params.agentId as string agentId.value = router.currentRoute.value.params.agentId as string
...@@ -56,7 +75,11 @@ onMounted(async () => { ...@@ -56,7 +75,11 @@ onMounted(async () => {
if (agentId.value) { if (agentId.value) {
fullScreenLoading.value = true fullScreenLoading.value = true
await handleGetApplicationDetail() await handleGetApplicationDetail()
userStore.isLogin && (await handleCreateDialogues()) if (userStore.isLogin) {
await handleCreateDialogues()
await handleGetAutoPlayByAgentId()
}
fullScreenLoading.value = false fullScreenLoading.value = false
return return
} }
...@@ -64,6 +87,11 @@ onMounted(async () => { ...@@ -64,6 +87,11 @@ onMounted(async () => {
router.replace({ name: 'Home' }) router.replace({ name: 'Home' })
}) })
onUnmounted(() => {
handleAudioPause()
footerInputRef.value?.blockMessageResponse()
})
async function handleGetApplicationDetail() { async function handleGetApplicationDetail() {
fetchGetApplicationInfo<PersonalAppConfigState>(agentId.value) fetchGetApplicationInfo<PersonalAppConfigState>(agentId.value)
.then((res) => { .then((res) => {
...@@ -93,6 +121,14 @@ async function handleCreateDialogues() { ...@@ -93,6 +121,14 @@ async function handleCreateDialogues() {
} }
} }
async function handleGetAutoPlayByAgentId() {
const res = await fetchGetAutoPlayByAgentId<'Y' | 'N'>(agentId.value)
if (res.code === 0) {
answerAudioAutoPlay.value = res.data === 'Y'
}
}
function handleBack() { function handleBack() {
if (!history.state.back) { if (!history.state.back) {
router.replace({ router.replace({
...@@ -116,20 +152,36 @@ function handleCreateApplicationPage() { ...@@ -116,20 +152,36 @@ function handleCreateApplicationPage() {
}) })
} }
function handleAddMessageItem(messageItem: ConversationMessageItem) { async function handleUpdateAutoPlaying(isAutoPlaying: boolean) {
messageList.value.push(messageItem) const autoplay = isAutoPlaying ? 'Y' : 'N'
await fetchUpdateAutoPlay(agentId.value, autoplay)
}
function handleAddMessageItem(messageId: string, messageItem: ConversationMessageItem) {
messageList.value.set(messageId, messageItem)
} }
function handleUpdateSpecifyMessageItem(messageItemIndex: number, newObj: Partial<ConversationMessageItem>) { function handleUpdateSpecifyMessageItem(messageId: string, newMessageItem: Partial<ConversationMessageItem>) {
if (messageList.value[messageItemIndex]) { const currentMessageItemInfo = messageList.value.get(messageId)
Object.entries(newObj).forEach(([k, v]) => {
;(messageList.value[messageItemIndex] as any)[k as keyof typeof newObj] = v if (currentMessageItemInfo) {
const updatePropertyLength = Object.keys(newMessageItem).length
if (updatePropertyLength > 4) {
messageList.value.set(messageId, Object.assign({}, currentMessageItemInfo, newMessageItem))
return
}
Object.entries<ValueOf<typeof newMessageItem>>(newMessageItem).forEach(([key, value]) => {
if (Object.prototype.hasOwnProperty.call(currentMessageItemInfo, key)) {
;(currentMessageItemInfo as any)[key as keyof ConversationMessageItem] = value
}
}) })
} }
} }
function handleDeleteLastMessageItem() { function handleDeleteMessageItem(messageId: string) {
messageList.value.pop() messageList.value.delete(messageId)
} }
function handleUpdatePageScroll() { function handleUpdatePageScroll() {
...@@ -143,8 +195,10 @@ function handleClearAllMessage() { ...@@ -143,8 +195,10 @@ function handleClearAllMessage() {
t('common_module.dialogue_module.clear_message_dialog_title'), t('common_module.dialogue_module.clear_message_dialog_title'),
) )
.then(() => { .then(() => {
handleAudioPause()
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
messageList.value = [] messageList.value.clear()
answerAudioPlaying.value = false
window.$message.success(t('common_module.clear_success_message')) window.$message.success(t('common_module.clear_success_message'))
}) })
} }
...@@ -161,6 +215,94 @@ async function handleCreateContinueQuestions(replyTextContent: string) { ...@@ -161,6 +215,94 @@ async function handleCreateContinueQuestions(replyTextContent: string) {
function handleResetContinueQuestionList() { function handleResetContinueQuestionList() {
continueQuestionList.value = [] continueQuestionList.value = []
} }
function howlSoundFactory(url: string) {
const soundCtl = new Howl({
src: [url],
format: ['mpeg'],
html5: !validBrowser(),
preload: true,
autoplay: true,
onplay: () => {
answerAudioPlaying.value = true
currentSoundCtl.value = soundCtl
if (currentPlayMessageItem.value) {
currentPlayMessageItem.value.isVoiceLoading = false
currentPlayMessageItem.value.isVoicePlaying = true
}
},
onend: () => {
soundCtl.unload()
currentPlayAudioFragmentSerialNo.value += 1
if (
currentPlayMessageItem.value &&
currentPlayAudioFragmentSerialNo.value > currentPlayMessageItem.value.voiceFragmentUrlList.length - 1
) {
currentPlayMessageItem.value.isVoicePlaying = false
answerAudioPlaying.value = false
}
let audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value]
if (audioFragmentUrl) {
howlSoundFactory(audioFragmentUrl)
} else if (
(currentPlayMessageItem.value?.voiceFragmentUrlList || []).length > currentPlayAudioFragmentSerialNo.value
) {
let timerId: NodeJS.Timeout | null = null
const audioFragmentCheck = () => {
audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value]
if (audioFragmentUrl) {
timerId && clearInterval(timerId)
howlSoundFactory(audioFragmentUrl)
}
}
timerId = setInterval(audioFragmentCheck, 600)
}
},
})
return soundCtl
}
function handleAudioPlay(currentMessageItem: ConversationMessageItem, specificPlayUrl?: string) {
handleAudioPause()
currentPlayMessageItem.value = currentMessageItem
currentPlayAudioFragmentSerialNo.value = 0
const audioUrl = specificPlayUrl || currentMessageItem.voiceFragmentUrlList[0]
howlSoundFactory(audioUrl)
isSoundCtlCreated.value = true
}
function handleAudioPause(isClearMessageList = false) {
if (currentSoundCtl.value) {
currentSoundCtl.value.pause()
currentSoundCtl.value.unload()
currentSoundCtl.value = null
isSoundCtlCreated.value = false
}
currentPlayMessageItem.value && (currentPlayMessageItem.value.isVoicePlaying = false)
answerAudioPlaying.value = false
if (isClearMessageList) {
messageList.value.clear()
footerInputRef.value?.blockMessageResponse()
}
}
</script> </script>
<template> <template>
...@@ -175,33 +317,29 @@ function handleResetContinueQuestionList() { ...@@ -175,33 +317,29 @@ function handleResetContinueQuestionList() {
/> />
<div class="h-[calc(100%-68px)] w-full bg-[#f2f5f9]"> <div class="h-[calc(100%-68px)] w-full bg-[#f2f5f9]">
<div class="mx-auto flex h-full w-[1000px] flex-col overflow-hidden"> <div class="relative mx-auto flex h-full w-[1000px] flex-col overflow-hidden">
<div v-if="messageList.length === 0" class="w-full flex-1 overflow-auto px-5"> <div v-show="isEnableVoice" class="absolute right-10 top-7 flex select-none items-center gap-2">
<span>{{ t('common_module.voice_auto_play') }}</span>
<n-switch v-model:value="answerAudioAutoPlay" size="small" @update:value="handleUpdateAutoPlaying">
<template #checked> {{ t('common_module.open') }} </template>
<template #unchecked> {{ t('common_module.close') }} </template>
</n-switch>
</div>
<div v-if="messageList.size === 0" class="w-full flex-1 overflow-auto px-5">
<Preamble :agent-application-config="agentApplicationConfig" /> <Preamble :agent-application-config="agentApplicationConfig" />
</div> </div>
<div v-if="messageList.length > 0" class="flex w-full flex-1 flex-col overflow-hidden"> <div v-if="messageList.size > 0" class="flex w-full flex-1 flex-col overflow-hidden">
<!-- <div class="my-5 ml-16 mr-5 border-b border-[#e8e9eb] py-6"> <div class="mt-20 flex-1 overflow-auto">
<div class="flex items-start">
<img :src="agentApplicationConfig.baseInfo.agentAvatar" class="h-14 w-14 rounded-xl" />
<p class="font-500 ml-4 line-clamp-1 h-8 max-w-[90%] break-all text-2xl leading-8 text-[#151b26]">
{{ agentApplicationConfig.baseInfo.agentTitle }}
</p>
</div>
<div
v-show="agentApplicationConfig.commConfig.preamble"
class="mt-3 rounded-xl border border-[#e8e9eb] bg-white px-4 py-3 shadow-[0_2px_2px_#0000000a]"
>
{{ agentApplicationConfig.commConfig.preamble }}
</div>
</div> -->
<div class="mt-16 flex-1 overflow-auto">
<MessageList <MessageList
ref="messageListRef" ref="messageListRef"
:agent-application-config="agentApplicationConfig" :agent-application-config="agentApplicationConfig"
:message-list="messageList" :message-list="messageList"
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:continuous-question-list="continueQuestionList" :continuous-question-list="continueQuestionList"
@audio-play="handleAudioPlay"
@audio-pause="handleAudioPause"
/> />
</div> </div>
</div> </div>
...@@ -214,14 +352,20 @@ function handleResetContinueQuestionList() { ...@@ -214,14 +352,20 @@ function handleResetContinueQuestionList() {
:agent-id="agentApplicationConfig.baseInfo.agentId" :agent-id="agentApplicationConfig.baseInfo.agentId"
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:is-enable-document-parse="isEnableDocumentParse" :is-enable-document-parse="isEnableDocumentParse"
:is-enable-voice="isEnableVoice"
:answer-audio-auto-play="answerAudioAutoPlay"
:answer-audio-playing="answerAudioPlaying"
:timbre-id="agentApplicationConfig.voiceConfig.timbreId"
@add-message-item="handleAddMessageItem" @add-message-item="handleAddMessageItem"
@update-specify-message-item="handleUpdateSpecifyMessageItem" @update-specify-message-item="handleUpdateSpecifyMessageItem"
@delete-last-message-item="handleDeleteLastMessageItem" @delete-message-item="handleDeleteMessageItem"
@update-page-scroll="handleUpdatePageScroll" @update-page-scroll="handleUpdatePageScroll"
@clear-all-message="handleClearAllMessage" @clear-all-message="handleClearAllMessage"
@to-login="handleToLoginPage" @to-login="handleToLoginPage"
@create-continue-questions="handleCreateContinueQuestions" @create-continue-questions="handleCreateContinueQuestions"
@reset-continue-question-list="handleResetContinueQuestionList" @reset-continue-question-list="handleResetContinueQuestionList"
@audio-play="handleAudioPlay"
@audio-pause="handleAudioPause"
/> />
</div> </div>
</div> </div>
......
...@@ -5,4 +5,8 @@ declare interface ConversationMessageItem { ...@@ -5,4 +5,8 @@ declare interface ConversationMessageItem {
isEmptyContent: boolean isEmptyContent: boolean
isTextContentLoading: boolean isTextContentLoading: boolean
isAnswerResponseLoading: boolean isAnswerResponseLoading: boolean
isVoiceLoading: boolean
isVoicePlaying: boolean
voiceFragmentUrlList: string[]
isVoiceEnabled?: boolean
} }
...@@ -91,6 +91,21 @@ declare namespace I18n { ...@@ -91,6 +91,21 @@ declare namespace I18n {
bind: string bind: string
sms: string sms: string
verificationCode: string verificationCode: string
role: string
mandarin: string
cantonese: string
english: string
voice: string
sound: string
voice_auto_play: string
start_playing: string
stop_playing: string
unplayable: string
unplayable_tip: string
response_error: string
agent_exception: string
equity: string
file_size_limit: string
dialogue_module: { dialogue_module: {
continue_question_message: string continue_question_message: string
...@@ -103,6 +118,8 @@ declare namespace I18n { ...@@ -103,6 +118,8 @@ declare namespace I18n {
cancel_associate_file_tip: string cancel_associate_file_tip: string
upload_file_limit: string upload_file_limit: string
overwrite_file_tip: string overwrite_file_tip: string
stop_playing_and_then_operate: string
do_not_operate_until_the_reply_is_complete: string
} }
data_table_module: { data_table_module: {
...@@ -279,6 +296,10 @@ declare namespace I18n { ...@@ -279,6 +296,10 @@ declare namespace I18n {
memory_fragment_delete_row_tip_content: string memory_fragment_delete_row_tip_content: string
add_knowledge_successfully: string add_knowledge_successfully: string
remove_knowledge_successfully: string remove_knowledge_successfully: string
setting_voice: string
setting_voice_message: string
setting_voice_desc: string
currently_only_one_voice_can_be_set: string
memory_variable_modal: { memory_variable_modal: {
edit_memory_variable: string edit_memory_variable: string
...@@ -476,6 +497,13 @@ declare namespace I18n { ...@@ -476,6 +497,13 @@ declare namespace I18n {
please_enter_the_correct_verification_code: string please_enter_the_correct_verification_code: string
binding_successful: string binding_successful: string
obtaining_the_verification_code: string obtaining_the_verification_code: string
upload_file: string
file_type_restriction_doc: string
the_file_type_cannot_be_uploaded: string
}
equity_Module: {
file_empty_tip: string
} }
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment