Commit d6e6f419 authored by nick zheng's avatar nick zheng

feat: agent应用插件内容返回显示

parent 026511a5
...@@ -46,6 +46,7 @@ ...@@ -46,6 +46,7 @@
"vant": "^4.9.18", "vant": "^4.9.18",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^9.14.0", "vue-i18n": "^9.14.0",
"vue-json-viewer": "^3.0.4",
"vue-router": "^4.4.5" "vue-router": "^4.4.5"
}, },
"devDependencies": { "devDependencies": {
......
...@@ -98,6 +98,9 @@ importers: ...@@ -98,6 +98,9 @@ importers:
vue-i18n: vue-i18n:
specifier: ^9.14.0 specifier: ^9.14.0
version: 9.14.0(vue@3.5.13(typescript@5.6.2)) version: 9.14.0(vue@3.5.13(typescript@5.6.2))
vue-json-viewer:
specifier: ^3.0.4
version: 3.0.4(vue@3.5.13(typescript@5.6.2))
vue-router: vue-router:
specifier: ^4.4.5 specifier: ^4.4.5
version: 4.4.5(vue@3.5.13(typescript@5.6.2)) version: 4.4.5(vue@3.5.13(typescript@5.6.2))
...@@ -251,7 +254,7 @@ importers: ...@@ -251,7 +254,7 @@ importers:
version: 5.4.6(@types/node@20.16.5)(sass@1.79.1)(terser@5.39.0) version: 5.4.6(@types/node@20.16.5)(sass@1.79.1)(terser@5.39.0)
vite-plugin-checker: vite-plugin-checker:
specifier: ^0.7.2 specifier: ^0.7.2
version: 0.7.2(eslint@9.10.0(jiti@2.4.2))(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)(terser@5.39.0))(vue-tsc@2.0.29(typescript@5.6.2)) version: 0.7.2(eslint@9.10.0(jiti@2.4.2))(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)(terser@5.39.0))(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.13(typescript@5.6.2)) version: 5.1.0(vue@3.5.13(typescript@5.6.2))
...@@ -1568,6 +1571,9 @@ packages: ...@@ -1568,6 +1571,9 @@ packages:
resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
engines: {node: '>=18'} engines: {node: '>=18'}
clipboard@2.0.11:
resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==}
clipboardy@4.0.0: clipboardy@4.0.0:
resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==}
engines: {node: '>=18'} engines: {node: '>=18'}
...@@ -1756,6 +1762,9 @@ packages: ...@@ -1756,6 +1762,9 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
delegate@3.2.0:
resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==}
destr@2.0.3: destr@2.0.3:
resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==}
...@@ -2128,6 +2137,9 @@ packages: ...@@ -2128,6 +2137,9 @@ packages:
globjoin@0.1.4: globjoin@0.1.4:
resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
good-listener@1.2.2:
resolution: {integrity: sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==}
gopd@1.2.0: gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
...@@ -2956,6 +2968,9 @@ packages: ...@@ -2956,6 +2968,9 @@ packages:
seemly@0.3.8: seemly@0.3.8:
resolution: {integrity: sha512-MW8Qs6vbzo0pHmDpFSYPna+lwpZ6Zk1ancbajw/7E8TKtHdV+1DfZZD+kKJEhG/cAoB/i+LiT+5msZOqj0DwRA==} resolution: {integrity: sha512-MW8Qs6vbzo0pHmDpFSYPna+lwpZ6Zk1ancbajw/7E8TKtHdV+1DfZZD+kKJEhG/cAoB/i+LiT+5msZOqj0DwRA==}
select@1.1.2:
resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==}
semver@6.3.1: semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true hasBin: true
...@@ -3190,6 +3205,9 @@ packages: ...@@ -3190,6 +3205,9 @@ packages:
through@2.3.8: through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
tiny-emitter@2.1.0:
resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
tiny-invariant@1.3.3: tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
...@@ -3486,6 +3504,11 @@ packages: ...@@ -3486,6 +3504,11 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.0.0 vue: ^3.0.0
vue-json-viewer@3.0.4:
resolution: {integrity: sha512-pnC080rTub6YjccthVSNQod2z9Sl5IUUq46srXtn6rxwhW8QM4rlYn+CTSLFKXWfw+N3xv77Cioxw7B4XUKIbQ==}
peerDependencies:
vue: ^3.2.2
vue-router@4.4.5: vue-router@4.4.5:
resolution: {integrity: sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==} resolution: {integrity: sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==}
peerDependencies: peerDependencies:
...@@ -5000,6 +5023,12 @@ snapshots: ...@@ -5000,6 +5023,12 @@ snapshots:
slice-ansi: 5.0.0 slice-ansi: 5.0.0
string-width: 7.2.0 string-width: 7.2.0
clipboard@2.0.11:
dependencies:
good-listener: 1.2.2
select: 1.1.2
tiny-emitter: 2.1.0
clipboardy@4.0.0: clipboardy@4.0.0:
dependencies: dependencies:
execa: 8.0.1 execa: 8.0.1
...@@ -5165,6 +5194,8 @@ snapshots: ...@@ -5165,6 +5194,8 @@ snapshots:
delayed-stream@1.0.0: {} delayed-stream@1.0.0: {}
delegate@3.2.0: {}
destr@2.0.3: {} destr@2.0.3: {}
dir-glob@3.0.1: dir-glob@3.0.1:
...@@ -5596,6 +5627,10 @@ snapshots: ...@@ -5596,6 +5627,10 @@ snapshots:
globjoin@0.1.4: {} globjoin@0.1.4: {}
good-listener@1.2.2:
dependencies:
delegate: 3.2.0
gopd@1.2.0: {} gopd@1.2.0: {}
graceful-fs@4.2.11: {} graceful-fs@4.2.11: {}
...@@ -6286,6 +6321,8 @@ snapshots: ...@@ -6286,6 +6321,8 @@ snapshots:
seemly@0.3.8: {} seemly@0.3.8: {}
select@1.1.2: {}
semver@6.3.1: {} semver@6.3.1: {}
semver@7.6.3: {} semver@7.6.3: {}
...@@ -6565,6 +6602,8 @@ snapshots: ...@@ -6565,6 +6602,8 @@ snapshots:
through@2.3.8: {} through@2.3.8: {}
tiny-emitter@2.1.0: {}
tiny-invariant@1.3.3: {} tiny-invariant@1.3.3: {}
tinyexec@0.3.0: {} tinyexec@0.3.0: {}
...@@ -6766,7 +6805,7 @@ snapshots: ...@@ -6766,7 +6805,7 @@ snapshots:
evtd: 0.2.4 evtd: 0.2.4
vue: 3.5.13(typescript@5.6.2) vue: 3.5.13(typescript@5.6.2)
vite-plugin-checker@0.7.2(eslint@9.10.0(jiti@2.4.2))(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)(terser@5.39.0))(vue-tsc@2.0.29(typescript@5.6.2)): vite-plugin-checker@0.7.2(eslint@9.10.0(jiti@2.4.2))(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)(terser@5.39.0))(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
...@@ -6785,6 +6824,7 @@ snapshots: ...@@ -6785,6 +6824,7 @@ snapshots:
vscode-uri: 3.0.8 vscode-uri: 3.0.8
optionalDependencies: optionalDependencies:
eslint: 9.10.0(jiti@2.4.2) eslint: 9.10.0(jiti@2.4.2)
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
...@@ -6858,6 +6898,11 @@ snapshots: ...@@ -6858,6 +6898,11 @@ snapshots:
'@vue/devtools-api': 6.6.4 '@vue/devtools-api': 6.6.4
vue: 3.5.13(typescript@5.6.2) vue: 3.5.13(typescript@5.6.2)
vue-json-viewer@3.0.4(vue@3.5.13(typescript@5.6.2)):
dependencies:
clipboard: 2.0.11
vue: 3.5.13(typescript@5.6.2)
vue-router@4.4.5(vue@3.5.13(typescript@5.6.2)): vue-router@4.4.5(vue@3.5.13(typescript@5.6.2)):
dependencies: dependencies:
'@vue/devtools-api': 6.6.4 '@vue/devtools-api': 6.6.4
......
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { CheckOne } from '@icon-park/vue-next'
import JsonViewer from 'vue-json-viewer'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import { useLayoutConfig } from '@/composables/useLayoutConfig'
interface Props {
displayFormat: 'json' | 'markdown' | 'none'
name: string
arguments: string
content: string
pluginLoading: boolean
}
const props = defineProps<Props>()
const { isMobile } = useLayoutConfig()
const { t } = useI18n()
const isFolding = ref(true)
const parsedPluginArguments = computed(() => {
try {
if (props.arguments) {
return JSON.parse(props.arguments)
}
return {}
} catch (error) {
return { error: 'Invalid JSON' }
}
})
const parsedPluginContent = computed(() => {
try {
if (props.content) {
return JSON.parse(props.content)
}
return {}
} catch (error) {
return { error: 'Invalid JSON' }
}
})
const parsedMarkdownPluginContent = computed(() => {
try {
if (props.content) {
return JSON.parse(props.content)
}
return ''
} catch (error) {
return props.content
}
})
</script>
<template>
<div
class="flex w-full flex-col bg-[#f3f5f9]"
:class="
isMobile ? 'rounded-[2.66667vw] px-[3.2vw] py-[2.66667vw] text-[3.2vw]' : 'rounded-[10px] px-[12px] py-[10px]'
"
>
<div class="flex flex-1 items-center justify-between gap-[10px] overflow-hidden">
<div class="flex flex-1 items-center gap-[6px] overflow-hidden">
<div
v-show="pluginLoading"
class="bg-px-plugin_loading-gif bg-contain bg-center bg-no-repeat"
:class="isMobile ? 'h-[3.73333vw] w-[3.73333vw]' : 'h-[14px] w-[14px]'"
/>
<CheckOne v-show="!pluginLoading" theme="outline" size="16" fill="#40bd23" />
<n-ellipsis class="text-gray-font-color w-full leading-5" :line-clamp="1" :tooltip="{ with: 'trigger' }">
{{
pluginLoading
? t('common_module.plugin_in_progress', { pluginName: name })
: t('common_module.plugin_executed_successfully', {
pluginName: name,
})
}}
</n-ellipsis>
</div>
<span
v-show="!pluginLoading"
class="iconfont icon-left text-font-color transform cursor-pointer text-[12px] duration-200 ease-in-out hover:text-[#777ef9]"
:class="isFolding ? 'rotate-[270deg]' : 'rotate-[90deg]'"
@click="isFolding = !isFolding"
/>
</div>
<div class="flex transform flex-col duration-200 ease-in-out" :class="isFolding ? 'h-0 overflow-hidden' : 'h-fit'">
<!-- json格式 -->
<div v-if="displayFormat === 'json'">
<div class="mt-[10px]">
<span
class="rounded-theme inline-block bg-[#E8EAFF] text-[#0072FF]"
:class="
isMobile
? 'h-[7.2vw] px-[3.2vw] text-[2.93333vw] leading-[7.2vw]'
: 'h-[29px] px-[12px] text-[12px] leading-[29px]'
"
>
{{ t('common_module.input_parameter') }}
</span>
<JsonViewer :value="parsedPluginArguments" :expand-depth="3" />
</div>
<div class="mt-[10px]">
<span
class="rounded-theme inline-block bg-[#E8EAFF] text-[#0072FF]"
:class="
isMobile
? 'h-[7.2vw] px-[3.2vw] text-[2.93333vw] leading-[7.2vw]'
: 'h-[29px] px-[12px] text-[12px] leading-[29px]'
"
>
{{ t('common_module.output_parameter') }}
</span>
<JsonViewer :value="parsedPluginContent" :expand-depth="3" />
</div>
</div>
<!-- markdown格式 -->
<div v-if="displayFormat === 'markdown'">
<div class="mt-[10px]">
<MarkdownRender :raw-text-content="parsedMarkdownPluginContent" :font-size="isMobile ? '3.2vw' : '14px'" />
</div>
</div>
<!-- none格式 -->
<div v-if="displayFormat === 'none'">
<span>{{ content }}</span>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
:deep(.markdown-render-container .markdown-render-inner) {
table {
color: #fff !important;
}
}
:deep(.jv-container.jv-light) {
font-family: 'Microsoft YaHei UI';
font-size: 14px;
.jv-code.open {
padding-bottom: 0;
}
.jv-code {
padding: 10px 0 0;
background-color: #f3f5f9;
}
.jv-item.jv-string {
color: #999;
}
.jv-item.jv-number {
color: #0072ff;
}
.jv-key {
color: #000;
}
}
</style>
...@@ -12,7 +12,8 @@ interface FileInfoItem { ...@@ -12,7 +12,8 @@ interface FileInfoItem {
status: 'pending' | 'uploading' | 'finished' | 'removed' | 'error' status: 'pending' | 'uploading' | 'finished' | 'removed' | 'error'
url: string url: string
percentage: number percentage: number
type?: string type: string
format: 'document' | 'image'
} }
const { t } = i18n.global const { t } = i18n.global
...@@ -20,11 +21,12 @@ export function useDialogueFile() { ...@@ -20,11 +21,12 @@ export function useDialogueFile() {
const uploadFileList = ref<FileInfoItem[]>([]) const uploadFileList = ref<FileInfoItem[]>([])
function handleLimitUpload(data: { file: UploadFileInfo }) { function handleLimitUpload(data: { file: UploadFileInfo }) {
const allowTypeList = ['md', 'doc', 'docx', 'pdf', 'txt'] const documentTypeList = ['md', 'doc', 'docx', 'pdf', 'txt'] // 文件类型
const imageTypeList = ['png', 'jpeg', 'jpg', 'webp'] // 图片类型
const fileType = (data.file.file && data.file.file?.name.split('.')?.pop()?.toLowerCase()) || '' const fileType = (data.file.file && data.file.file?.name.split('.')?.pop()?.toLowerCase()) || ''
if (fileType && !allowTypeList.includes(fileType)) { if (fileType && !documentTypeList.includes(fileType) && !imageTypeList.includes(fileType)) {
window.$message.error( window.$message.error(
t('personal_space_module.knowledge_module.upload_document_module.upload_format_error_message'), t('personal_space_module.knowledge_module.upload_document_module.upload_format_error_message'),
) )
...@@ -44,6 +46,7 @@ export function useDialogueFile() { ...@@ -44,6 +46,7 @@ export function useDialogueFile() {
type: fileType, type: fileType,
percentage: 0, percentage: 0,
url: '', url: '',
format: documentTypeList.includes(fileType) ? ('document' as const) : ('image' as const),
}) })
uploadFileList.value = [] uploadFileList.value = []
...@@ -64,6 +67,7 @@ export function useDialogueFile() { ...@@ -64,6 +67,7 @@ export function useDialogueFile() {
url: '', url: '',
percentage: 0, percentage: 0,
type: fileType, type: fileType,
format: documentTypeList.includes(fileType) ? ('document' as const) : ('image' as const),
}) })
uploadFileList.value = [] uploadFileList.value = []
...@@ -78,14 +82,18 @@ export function useDialogueFile() { ...@@ -78,14 +82,18 @@ export function useDialogueFile() {
const formData = new FormData() const formData = new FormData()
formData.append('file', file.file.file) formData.append('file', file.file.file)
const fileFormat = file.file?.name.split('.')?.pop()?.toLowerCase()
const documentTypeList = ['md', 'doc', 'docx', 'pdf', 'txt']
const fileData = reactive({ const fileData = reactive({
id: file.file.id, id: file.file.id,
name: file.file.name, name: file.file.name,
status: UploadStatus.UPLOADING, status: UploadStatus.UPLOADING,
size: file.file?.file?.size || 0, size: file.file?.file?.size || 0,
type: file.file?.name.split('.')?.pop()?.toLowerCase(), type: fileFormat,
percentage: 0, percentage: 0,
url: '', url: '',
format: documentTypeList.includes(fileFormat) ? ('document' as const) : ('image' as const),
}) })
if (uploadFileList.value.length <= 1) { if (uploadFileList.value.length <= 1) {
......
...@@ -8,7 +8,7 @@ import { languageKeyTransform } from '@/utils/language-key-transform' ...@@ -8,7 +8,7 @@ import { languageKeyTransform } from '@/utils/language-key-transform'
interface ResponseData { interface ResponseData {
message: string message: string
reasoningContent: string reasoningContent: string
function: { name: string } function: { displayFormat: 'json' | 'markdown' | 'none'; name: string; result: string; arguments: string }
knowledgeContentResult: KnowledgeContentResultItem[] knowledgeContentResult: KnowledgeContentResultItem[]
dbChainResult: DBChainResultItem[] dbChainResult: DBChainResultItem[]
} }
......
...@@ -161,6 +161,8 @@ common_module: ...@@ -161,6 +161,8 @@ common_module:
add_database_successfully: 'Database set {0} was added successfully' add_database_successfully: 'Database set {0} was added successfully'
remove_database_successfully: 'Database set {0} was removed successfully' remove_database_successfully: 'Database set {0} was removed successfully'
database_QA_executed_successfully: 'Database Q&A executed successfully' database_QA_executed_successfully: 'Database Q&A executed successfully'
input_parameter: 'Input parameter'
output_parameter: 'Output parameter'
dialogue_module: dialogue_module:
continue_question_message: 'You can keep asking questions' continue_question_message: 'You can keep asking questions'
...@@ -171,7 +173,7 @@ common_module: ...@@ -171,7 +173,7 @@ common_module:
clear_message_dialog_title: 'Are you sure you want to clear the conversation?' clear_message_dialog_title: 'Are you sure you want to clear the conversation?'
clear_message_dialog_content: 'Clearing the conversation will clear all the current conversation content. Are you sure to clear the conversation?' clear_message_dialog_content: 'Clearing the conversation will clear all the current conversation content. Are you sure to clear the conversation?'
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, PNG, JPG, JPEG, WEBP 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' 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' do_not_operate_until_the_reply_is_complete: 'Do not operate until the reply is complete'
...@@ -386,7 +388,7 @@ personal_space_module: ...@@ -386,7 +388,7 @@ personal_space_module:
only_one_database_can_be_added: 'Only one database can be added' only_one_database_can_be_added: 'Only one database can be added'
associated_database_desc: 'You can upload local spreadsheet data or connect to a business database to build your dataset. When users ask numerical questions, the application can query, compute, and analyze the data to provide answers. Note: The app supports association with only 1 database at most' associated_database_desc: 'You can upload local spreadsheet data or connect to a business database to build your dataset. When users ask numerical questions, the application can query, compute, and analyze the data to provide answers. Note: The app supports association with only 1 database at most'
upload_file: 'Upload file' upload_file: 'Upload file'
upload_file_desc: 'Enable the user to upload files for chat, support TXT, MD, PDF, DOC, DOCX format files' upload_file_desc: 'Enable the user to upload files for chat, support TXT, MD, PDF, DOC, DOCX, PNG, JPG, JPEG, WEBP format files'
dialogue: 'Dialogue' dialogue: 'Dialogue'
preamble: 'Opening remarks' preamble: 'Opening remarks'
preamble_input_placeholder: 'Please enter an opening statement' preamble_input_placeholder: 'Please enter an opening statement'
......
...@@ -160,6 +160,8 @@ common_module: ...@@ -160,6 +160,8 @@ common_module:
add_database_successfully: '数据库 {0} 添加成功' add_database_successfully: '数据库 {0} 添加成功'
remove_database_successfully: '数据库 {0} 移除成功' remove_database_successfully: '数据库 {0} 移除成功'
database_QA_executed_successfully: '数据库问答执行成功' database_QA_executed_successfully: '数据库问答执行成功'
input_parameter: '输入参数'
output_parameter: '输出参数'
dialogue_module: dialogue_module:
continue_question_message: '你可以继续提问' continue_question_message: '你可以继续提问'
...@@ -170,7 +172,7 @@ common_module: ...@@ -170,7 +172,7 @@ common_module:
clear_message_dialog_title: '确认要清空对话吗?' clear_message_dialog_title: '确认要清空对话吗?'
clear_message_dialog_content: '清空对话将清空当前所有对话内容,确定清空对话吗?' clear_message_dialog_content: '清空对话将清空当前所有对话内容,确定清空对话吗?'
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、PNG、JPG、JPEG、WEBP格式,最大10MB'
overwrite_file_tip: '新上传的文件会覆盖原有文件,是否继续上传' overwrite_file_tip: '新上传的文件会覆盖原有文件,是否继续上传'
stop_playing_and_then_operate: '音频播放中,请停止播放后再操作' stop_playing_and_then_operate: '音频播放中,请停止播放后再操作'
do_not_operate_until_the_reply_is_complete: '回复完成后再操作' do_not_operate_until_the_reply_is_complete: '回复完成后再操作'
...@@ -384,7 +386,7 @@ personal_space_module: ...@@ -384,7 +386,7 @@ personal_space_module:
only_one_database_can_be_added: '仅支持添加一个数据库' only_one_database_can_be_added: '仅支持添加一个数据库'
associated_database_desc: '可上传本地表格数据或连接业务数据库构建数据库。用户询问数值类问题时,应用能够查询、计算和分析数据并答复。应用最多可关联1个数据库' associated_database_desc: '可上传本地表格数据或连接业务数据库构建数据库。用户询问数值类问题时,应用能够查询、计算和分析数据并答复。应用最多可关联1个数据库'
upload_file: '上传文件' upload_file: '上传文件'
upload_file_desc: '开启后支持用户上传文件进行对话聊天, 支持TXT、MD、PDF、DOC、DOCX格式的文件' upload_file_desc: '开启后支持用户上传文件进行对话聊天, 支持TXT、MD、PDF、DOC、DOCX、PNG、JPG、JPEG、WEBP格式的文件'
dialogue: '对话' dialogue: '对话'
preamble: '开场白' preamble: '开场白'
preamble_input_placeholder: '请输入开场白' preamble_input_placeholder: '请输入开场白'
......
...@@ -160,6 +160,8 @@ common_module: ...@@ -160,6 +160,8 @@ common_module:
add_database_successfully: '數據庫 {0} 添加成功' add_database_successfully: '數據庫 {0} 添加成功'
remove_database_successfully: '數據庫 {0} 移除成功' remove_database_successfully: '數據庫 {0} 移除成功'
database_QA_executed_successfully: '數據庫問答執行成功' database_QA_executed_successfully: '數據庫問答執行成功'
input_parameter: '輸入參數'
output_parameter: '輸出參數'
dialogue_module: dialogue_module:
continue_question_message: '你可以繼續提問' continue_question_message: '你可以繼續提問'
...@@ -170,7 +172,7 @@ common_module: ...@@ -170,7 +172,7 @@ common_module:
clear_message_dialog_title: '確認要清空對話嗎?' clear_message_dialog_title: '確認要清空對話嗎?'
clear_message_dialog_content: '清空對話將清空當前所有對話內容,確定清空對話嗎?' clear_message_dialog_content: '清空對話將清空當前所有對話內容,確定清空對話嗎?'
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、PNG、JPG、JPEG、WEBP格式,最大10MB'
overwrite_file_tip: '新上傳的文件會覆蓋原有文件,是否繼續上傳' overwrite_file_tip: '新上傳的文件會覆蓋原有文件,是否繼續上傳'
stop_playing_and_then_operate: '音頻播放中,請停止播放後再操作' stop_playing_and_then_operate: '音頻播放中,請停止播放後再操作'
do_not_operate_until_the_reply_is_complete: '回覆完成後再操作' do_not_operate_until_the_reply_is_complete: '回覆完成後再操作'
...@@ -384,7 +386,7 @@ personal_space_module: ...@@ -384,7 +386,7 @@ personal_space_module:
only_one_database_can_be_added: '僅支持添加一個數據庫' only_one_database_can_be_added: '僅支持添加一個數據庫'
associated_database_desc: '可上傳本地表格數據或連接業務數據庫構建數據庫。用户詢問數值類問題時,應用能夠查詢、計算和分析數據並答覆。應用最多可關聯1個數據庫' associated_database_desc: '可上傳本地表格數據或連接業務數據庫構建數據庫。用户詢問數值類問題時,應用能夠查詢、計算和分析數據並答覆。應用最多可關聯1個數據庫'
upload_file: '上傳文件' upload_file: '上傳文件'
upload_file_desc: '開啓後支持用户上傳文件進行對話聊天, 支持TXT、MD、PDF、DOC、DOCX格式的文件' upload_file_desc: '開啓後支持用户上傳文件進行對話聊天, 支持TXT、MD、PDF、DOC、DOCX、PNG、JPG、JPEG、WEBP格式的文件'
dialogue: '對話' dialogue: '對話'
preamble: '開場白' preamble: '開場白'
preamble_input_placeholder: '請輸入開場白' preamble_input_placeholder: '請輸入開場白'
......
...@@ -11,7 +11,6 @@ import { UploadStatus } from '@/enums/upload-status' ...@@ -11,7 +11,6 @@ import { UploadStatus } from '@/enums/upload-status'
import { ChannelType } from '@/enums/channel' import { ChannelType } from '@/enums/channel'
import { useDialogueFile } from '@/composables/useDialogueFile' import { useDialogueFile } from '@/composables/useDialogueFile'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { useUploadImage } from '@/composables/useUploadImage'
const { t } = useI18n() const { t } = useI18n()
...@@ -19,7 +18,6 @@ interface Props { ...@@ -19,7 +18,6 @@ interface Props {
agentId: string agentId: string
isAnswerResponseWait: boolean isAnswerResponseWait: boolean
isEnableDocumentParse: boolean isEnableDocumentParse: boolean
isEnableUploadImage: boolean
} }
const props = defineProps<Props>() const props = defineProps<Props>()
...@@ -36,7 +34,6 @@ const emit = defineEmits<{ ...@@ -36,7 +34,6 @@ const emit = defineEmits<{
const userStore = useUserStore() const userStore = useUserStore()
const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile() const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile()
const { uploadImageList, handleLimitUploadImage, handleUploadImage, handleRemoveUploadImage } = useUploadImage()
const multiModelDialogueList = defineModel<MultiModelDialogueItem[]>('multiModelDialogueList', { required: true }) const multiModelDialogueList = defineModel<MultiModelDialogueItem[]>('multiModelDialogueList', { required: true })
...@@ -61,6 +58,14 @@ const isInputMessageDisabled = computed(() => { ...@@ -61,6 +58,14 @@ const isInputMessageDisabled = computed(() => {
) )
}) })
const uploadDocumentList = computed(() => {
return uploadFileList.value.filter((fileItem) => fileItem.format === 'document')
})
const uploadImageList = computed(() => {
return uploadFileList.value.filter((fileItem) => fileItem.format === 'image')
})
const isUploadFileDisabled = computed(() => { const isUploadFileDisabled = computed(() => {
return uploadFileList.value.length === 1 return uploadFileList.value.length === 1
}) })
...@@ -97,11 +102,16 @@ function messageItemFactory() { ...@@ -97,11 +102,16 @@ function messageItemFactory() {
timestamp: Date.now(), timestamp: Date.now(),
isTextContentLoading: false, isTextContentLoading: false,
isAnswerResponseLoading: false, isAnswerResponseLoading: false,
pluginName: '',
imageUrl: '', imageUrl: '',
reasoningContent: '', reasoningContent: '',
knowledgeContentResult: [], knowledgeContentResult: [],
dbChainSQLContent: '', dbChainSQLContent: '',
pluginResult: {
displayFormat: 'none',
pluginName: '',
arguments: '',
pluginContent: '',
},
} as MessageItemInterface } as MessageItemInterface
} }
...@@ -142,7 +152,7 @@ function handleQuestionSubmit() { ...@@ -142,7 +152,7 @@ function handleQuestionSubmit() {
type: 'text', type: 'text',
text: messageItem.content, text: messageItem.content,
image_url: { image_url: {
url: messageItem?.imageUrl || '', url: '',
}, },
}, },
], ],
...@@ -195,7 +205,19 @@ function handleQuestionSubmit() { ...@@ -195,7 +205,19 @@ function handleQuestionSubmit() {
// 插件 // 插件
if (data.function && data.function.name) { if (data.function && data.function.name) {
emit('updateMessageItem', answerMessageId, { pluginName: data.function.name }, modelIndex) emit(
'updateMessageItem',
answerMessageId,
{
pluginResult: {
displayFormat: data.function.displayFormat,
pluginName: data.function.name,
arguments: data.function.arguments,
pluginContent: data.function.result,
},
},
modelIndex,
)
emit('messageListScrollToBottom') emit('messageListScrollToBottom')
} }
...@@ -262,7 +284,7 @@ function handleQuestionSubmit() { ...@@ -262,7 +284,7 @@ function handleQuestionSubmit() {
}) })
questionContent.value = '' questionContent.value = ''
uploadImageList.value = [] uploadFileList.value = uploadFileList.value.filter((item) => item.format === 'document')
}) })
} }
...@@ -281,17 +303,6 @@ function handleSelectFile(cb: () => void) { ...@@ -281,17 +303,6 @@ function handleSelectFile(cb: () => void) {
cb() cb()
} }
function handleSelectUploadImage(cb: () => void) {
if (uploadImageList.value.length > 0) {
window.$message.ctWarning('', t('common_module.dialogue_module.overwrite_image_tip')).then(() => {
cb()
})
return
}
cb()
}
</script> </script>
<template> <template>
...@@ -317,31 +328,7 @@ function handleSelectUploadImage(cb: () => void) { ...@@ -317,31 +328,7 @@ function handleSelectUploadImage(cb: () => void) {
<n-upload <n-upload
:show-file-list="false" :show-file-list="false"
accept="image/png, image/jpeg, image/jpg, image/webp" accept=".doc, .pdf, .docx, .txt, .md, image/png, image/jpeg, image/jpg, image/webp"
abstract
@before-upload="handleLimitUploadImage"
@change="handleUploadImage"
>
<n-upload-trigger #="{ handleClick }" abstract>
<n-popover style="width: 210px" trigger="hover">
<template #trigger>
<div
v-show="isEnableUploadImage"
class="border-inactive-border-color text-font-color hover:text-theme-color flex h-[54px] w-[54px] shrink-0 cursor-pointer items-center justify-center rounded-full border bg-white"
@click="handleSelectUploadImage(handleClick)"
>
<i class="iconfont icon-upload-image text-xl leading-none" />
</div>
</template>
<span class="text-xs"> {{ t('common_module.dialogue_module.upload_image_limit') }} </span>
</n-popover>
</n-upload-trigger>
</n-upload>
<n-upload
:show-file-list="false"
accept=".doc, .pdf, .docx, .txt, .md"
abstract abstract
@before-upload="handleLimitUpload" @before-upload="handleLimitUpload"
@change="handleUpload" @change="handleUpload"
...@@ -374,7 +361,7 @@ function handleSelectUploadImage(cb: () => void) { ...@@ -374,7 +361,7 @@ function handleSelectUploadImage(cb: () => void) {
> >
<div <div
class="absolute right-[-4px] top-[-4px] flex h-4 w-4 cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.55)] hover:opacity-80" class="absolute right-[-4px] top-[-4px] flex h-4 w-4 cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.55)] hover:opacity-80"
@click="handleRemoveUploadImage(uploadImageItem.id)" @click="handleRemoveFile(uploadImageItem.id)"
> >
<CloseSmall theme="outline" size="16" fill="#fff" /> <CloseSmall theme="outline" size="16" fill="#fff" />
</div> </div>
...@@ -394,41 +381,41 @@ function handleSelectUploadImage(cb: () => void) { ...@@ -394,41 +381,41 @@ function handleSelectUploadImage(cb: () => void) {
</div> </div>
</div> </div>
<ul v-show="uploadFileList.length > 0" class="mb-1.5 grid gap-1.5"> <ul v-show="uploadDocumentList.length > 0" class="mb-1.5 grid gap-1.5">
<li <li
v-for="uploadFileItem in uploadFileList" v-for="uploadDocumentItem in uploadDocumentList"
:key="uploadFileItem.id" :key="uploadDocumentItem.id"
class="rounded-theme group relative flex h-[42px] w-full items-center overflow-hidden border bg-white/70" class="rounded-theme group relative flex h-[42px] w-full items-center overflow-hidden border bg-white/70"
:class="uploadFileItem.status === 'error' ? 'border-error-font-color' : 'border-transparent'" :class="uploadDocumentItem.status === 'error' ? 'border-error-font-color' : 'border-transparent'"
> >
<div class="flex w-full items-center justify-between px-3.5"> <div class="flex w-full items-center justify-between px-3.5">
<div class="flex w-full items-center overflow-hidden"> <div class="flex w-full items-center overflow-hidden">
<img :src="uploadFileIcon(uploadFileItem.type!)" class="h-7 w-7" /> <img :src="uploadFileIcon(uploadDocumentItem.type!)" class="h-7 w-7" />
<div class="mx-2.5 flex flex-1 flex-col overflow-hidden"> <div class="mx-2.5 flex flex-1 flex-col overflow-hidden">
<n-ellipsis> <n-ellipsis>
{{ uploadFileItem.name }} {{ uploadDocumentItem.name }}
</n-ellipsis> </n-ellipsis>
</div> </div>
</div> </div>
<n-progress <n-progress
v-show="!['finished', 'error'].includes(uploadFileItem.status)" v-show="!['finished', 'error'].includes(uploadDocumentItem.status)"
class="left-13.5 w-[calc(100%-78px)]! absolute bottom-0" class="left-13.5 w-[calc(100%-78px)]! absolute bottom-0"
type="line" type="line"
rail-color="#F3F3F3" rail-color="#F3F3F3"
:height="4" :height="4"
:percentage="uploadFileItem.percentage" :percentage="uploadDocumentItem.percentage"
:show-indicator="false" :show-indicator="false"
/> />
<div v-show="['finished', 'error'].includes(uploadFileItem.status)" class="hidden group-hover:block"> <div v-show="['finished', 'error'].includes(uploadDocumentItem.status)" class="hidden group-hover:block">
<n-popover trigger="hover" placement="top-end" :show-arrow="false"> <n-popover trigger="hover" placement="top-end" :show-arrow="false">
<template #trigger> <template #trigger>
<i <i
class="iconfont icon-close cursor-pointer hover:opacity-80" class="iconfont icon-close cursor-pointer hover:opacity-80"
:class="uploadFileItem.status === 'error' ? 'text-error-font-color' : 'text-font-color'" :class="uploadDocumentItem.status === 'error' ? 'text-error-font-color' : 'text-font-color'"
@click="handleRemoveFile(uploadFileItem.id)" @click="handleRemoveFile(uploadDocumentItem.id)"
/> />
</template> </template>
<span>{{ t('common_module.dialogue_module.cancel_associate_file_tip') }}</span> <span>{{ t('common_module.dialogue_module.cancel_associate_file_tip') }}</span>
......
<script setup lang="ts"> <script setup lang="ts">
import { computed, readonly, ref } from 'vue' import { computed, readonly, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { CheckOne, Down } from '@icon-park/vue-next' import { Down } from '@icon-park/vue-next'
import type { MessageItemInterface } from '../types' import type { MessageItemInterface } from '../types'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue' import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import ExecuteCodeRender from '@/components/execute-code-render/execute-code-render.vue' import ExecuteCodeRender from '@/components/execute-code-render/execute-code-render.vue'
import ExecutePluginRender from '@/components/execute-plugin-render/execute-plugin-render.vue'
import MessageBubbleLoading from './message-bubble-loading.vue' import MessageBubbleLoading from './message-bubble-loading.vue'
interface Props { interface Props {
...@@ -84,25 +85,6 @@ function handleShowReasoningContentSwitch() { ...@@ -84,25 +85,6 @@ function handleShowReasoningContentSwitch() {
{{ props.messageItem.nickName }} {{ props.messageItem.nickName }}
</div> </div>
<div
v-show="isAssistant && messageItem.pluginName"
class="mb-[7px] flex items-center gap-[5px] font-['Microsoft_YaHei_UI'] text-[#999]"
>
<div
v-show="messageItem.isTextContentLoading"
class="bg-px-plugin_loading-gif h-[14px] w-[14px] bg-contain bg-center bg-no-repeat"
/>
<CheckOne v-show="!messageItem.isTextContentLoading" theme="outline" size="16" fill="#40bd23" />
<span class="leading-5">
{{
messageItem.isTextContentLoading
? t('common_module.plugin_in_progress', { pluginName: messageItem.pluginName })
: t('common_module.plugin_executed_successfully', { pluginName: messageItem.pluginName })
}}
</span>
</div>
<div class="flex min-w-[40px] max-w-full flex-col items-start overflow-hidden"> <div class="flex min-w-[40px] max-w-full flex-col items-start overflow-hidden">
<div <div
class="min-h-[21px] w-full flex-wrap rounded-[10px] border border-[#9EA3FF] px-[15px] py-[12px] text-justify" class="min-h-[21px] w-full flex-wrap rounded-[10px] border border-[#9EA3FF] px-[15px] py-[12px] text-justify"
...@@ -118,7 +100,18 @@ function handleShowReasoningContentSwitch() { ...@@ -118,7 +100,18 @@ function handleShowReasoningContentSwitch() {
class="max-h-[120px]! mb-[12px] rounded-[10px] object-contain" class="max-h-[120px]! mb-[12px] rounded-[10px] object-contain"
/> />
<div v-if="messageItem.isTextContentLoading" class="flex h-[21px] w-full items-center justify-center"> <!-- 插件返回结果 -->
<div v-show="isAssistant && messageItem?.pluginResult?.pluginName" class="mb-[11px] w-full">
<ExecutePluginRender
:display-format="messageItem.pluginResult?.displayFormat || 'none'"
:name="messageItem.pluginResult?.pluginName!"
:content="messageItem.pluginResult?.pluginContent!"
:arguments="messageItem.pluginResult?.arguments!"
:plugin-loading="messageItem.isTextContentLoading"
/>
</div>
<div v-if="messageItem.isTextContentLoading" class="flex px-4 py-1.5">
<MessageBubbleLoading :active-color="isAssistant ? '#fff' : '#192338'" /> <MessageBubbleLoading :active-color="isAssistant ? '#fff' : '#192338'" />
</div> </div>
...@@ -166,7 +159,7 @@ function handleShowReasoningContentSwitch() { ...@@ -166,7 +159,7 @@ function handleShowReasoningContentSwitch() {
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.markdown-render-container .markdown-render-inner) { :deep(.markdown-render-container .markdown-render-inner) {
table { table {
color: #192338 !important; color: #fff !important;
} }
} }
</style> </style>
...@@ -33,11 +33,16 @@ export interface MessageItemInterface { ...@@ -33,11 +33,16 @@ export interface MessageItemInterface {
timestamp: number timestamp: number
isTextContentLoading: boolean isTextContentLoading: boolean
isAnswerResponseLoading: boolean isAnswerResponseLoading: boolean
pluginName?: string
imageUrl?: string imageUrl?: string
reasoningContent: string reasoningContent: string
knowledgeContentResult: KnowledgeContentResultItem[] knowledgeContentResult: KnowledgeContentResultItem[]
dbChainSQLContent: string dbChainSQLContent: string
pluginResult?: {
displayFormat: 'json' | 'markdown' | 'none'
pluginName: string
arguments: string
pluginContent: string
}
} }
export interface LargeModelItem { export interface LargeModelItem {
......
...@@ -8,7 +8,7 @@ import { languageKeyTransform } from '@/utils/language-key-transform' ...@@ -8,7 +8,7 @@ import { languageKeyTransform } from '@/utils/language-key-transform'
interface ResponseData { interface ResponseData {
message: string message: string
reasoningContent: string reasoningContent: string
function: { name: string } function: { displayFormat: 'json' | 'markdown' | 'none'; name: string; result: string; arguments: string }
knowledgeContentResult: KnowledgeContentResultItem[] knowledgeContentResult: KnowledgeContentResultItem[]
dbChainResult: DBChainResultItem[] dbChainResult: DBChainResultItem[]
} }
......
...@@ -13,8 +13,6 @@ import { TEXTTOSPEECH_WS_URL } from '@/config/base-url' ...@@ -13,8 +13,6 @@ import { TEXTTOSPEECH_WS_URL } from '@/config/base-url'
import WebSocketCtr from '@/utils/web-socket-ctr' import WebSocketCtr from '@/utils/web-socket-ctr'
import { ChannelType } from '@/enums/channel' import { ChannelType } from '@/enums/channel'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { useUploadImage } from '@/composables/useUploadImage'
import { PluginType } from '@/enums/plugin'
interface Props { interface Props {
messageList: Map<string, ConversationMessageItem> messageList: Map<string, ConversationMessageItem>
...@@ -48,7 +46,6 @@ const overwriteMessageTipOptions = reactive({ ...@@ -48,7 +46,6 @@ const overwriteMessageTipOptions = reactive({
}) })
const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile() const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile()
const { uploadImageList, handleLimitUploadImage, handleUploadImage, handleRemoveUploadImage } = useUploadImage()
const messageTipModalRef = useTemplateRef<InstanceType<typeof OverwriteMessageTipModal>>('messageTipModalRef') const messageTipModalRef = useTemplateRef<InstanceType<typeof OverwriteMessageTipModal>>('messageTipModalRef')
const isAnswerResponseLoading = defineModel<boolean>('isAnswerResponseLoading', { required: true }) const isAnswerResponseLoading = defineModel<boolean>('isAnswerResponseLoading', { required: true })
...@@ -87,8 +84,7 @@ const isSendBtnDisabled = computed(() => { ...@@ -87,8 +84,7 @@ const isSendBtnDisabled = computed(() => {
const isInputMessageDisabled = computed(() => { const isInputMessageDisabled = computed(() => {
return ( return (
uploadFileList.value.some((fileItem) => fileItem.status !== UploadStatus.FINISHED) || uploadFileList.value.some((fileItem) => fileItem.status !== UploadStatus.FINISHED) ||
!personalAppConfigStore.baseInfo.agentId || !personalAppConfigStore.baseInfo.agentId
uploadImageList.value.some((imageItem) => imageItem.status !== UploadStatus.FINISHED)
) )
}) })
...@@ -100,8 +96,14 @@ const isUploadFileDisabled = computed(() => { ...@@ -100,8 +96,14 @@ const isUploadFileDisabled = computed(() => {
return uploadFileList.value.length === 1 return uploadFileList.value.length === 1
}) })
const isEnableUploadImage = computed(() => { // 上传文档列表
return personalAppConfigStore.unitIds?.includes(PluginType.IMAGE_OCR) || false const uploadDocumentList = computed(() => {
return uploadFileList.value.filter((fileItem) => fileItem.format === 'document')
})
// 上传图片列表
const uploadImageList = computed(() => {
return uploadFileList.value.filter((fileItem) => fileItem.format === 'image')
}) })
const uploadFileIcon = (type: string) => { const uploadFileIcon = (type: string) => {
...@@ -146,12 +148,17 @@ function messageItemFactory(): ConversationMessageItem { ...@@ -146,12 +148,17 @@ function messageItemFactory(): ConversationMessageItem {
isVoicePlaying: false, isVoicePlaying: false,
voiceFragmentUrlList: [], voiceFragmentUrlList: [],
isVoiceEnabled: false, isVoiceEnabled: false,
pluginName: '',
imageUrl: '', imageUrl: '',
reasoningContent: '', reasoningContent: '',
modelName: '', modelName: '',
knowledgeContentResult: [], knowledgeContentResult: [],
dbChainSQLContent: '', dbChainSQLContent: '',
pluginResult: {
displayFormat: 'none',
pluginName: '',
pluginContent: '',
arguments: '',
},
} }
} }
...@@ -208,7 +215,7 @@ function handleMessageSend() { ...@@ -208,7 +215,7 @@ function handleMessageSend() {
type: 'text', type: 'text',
text: messageItem.textContent, text: messageItem.textContent,
image_url: { image_url: {
url: messageItem.imageUrl || '', url: '',
}, },
}, },
], ],
...@@ -264,7 +271,14 @@ function handleMessageSend() { ...@@ -264,7 +271,14 @@ function handleMessageSend() {
// 插件 // 插件
if (data.function && data.function.name) { if (data.function && data.function.name) {
emit('updateSpecifyMessageItem', latestAssistantMessageKey, { pluginName: data.function.name }) emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
pluginResult: {
displayFormat: data.function.displayFormat,
pluginName: data.function.name,
pluginContent: data.function.result,
arguments: data.function.arguments,
},
})
emit('updatePageScroll') emit('updatePageScroll')
} }
...@@ -328,7 +342,7 @@ function handleMessageSend() { ...@@ -328,7 +342,7 @@ function handleMessageSend() {
}) })
inputMessageContent.value = '' inputMessageContent.value = ''
uploadImageList.value = [] uploadFileList.value = uploadFileList.value.filter((item) => item.format === 'document')
} }
function errorMessageResponse() { function errorMessageResponse() {
...@@ -368,19 +382,6 @@ function handleSelectFile(cb: () => void) { ...@@ -368,19 +382,6 @@ function handleSelectFile(cb: () => void) {
cb() cb()
} }
function handleSelectImage(cb: () => void) {
if (uploadImageList.value.length > 0) {
overwriteMessageTipOptions.title = t('common_module.dialogue_module.overwrite_image_tip')
messageTipModalRef.value?.handleShowModal().then(() => {
cb()
})
return
}
cb()
}
function sentenceExtract(messageId: string) { function sentenceExtract(messageId: string) {
const symbolRegExp = /[。!?;.!?;~]/g const symbolRegExp = /[。!?;.!?;~]/g
let sentenceDraft = assistantFullAnswerContent.value let sentenceDraft = assistantFullAnswerContent.value
...@@ -502,7 +503,7 @@ defineExpose({ ...@@ -502,7 +503,7 @@ defineExpose({
> >
<div <div
class="absolute right-[-4px] top-[-4px] flex h-4 w-4 cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.55)] hover:opacity-80" class="absolute right-[-4px] top-[-4px] flex h-4 w-4 cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.55)] hover:opacity-80"
@click="handleRemoveUploadImage(uploadImageItem.id)" @click="handleRemoveFile(uploadImageItem.id)"
> >
<CloseSmall theme="outline" size="16" fill="#fff" /> <CloseSmall theme="outline" size="16" fill="#fff" />
</div> </div>
...@@ -521,41 +522,41 @@ defineExpose({ ...@@ -521,41 +522,41 @@ defineExpose({
</div> </div>
</div> </div>
<ul v-show="uploadFileList.length > 0" class="mb-1.5 grid gap-1.5"> <ul v-show="uploadDocumentList.length > 0" class="mb-1.5 grid gap-1.5">
<li <li
v-for="uploadFileItem in uploadFileList" v-for="uploadDocumentItem in uploadDocumentList"
:key="uploadFileItem.id" :key="uploadDocumentItem.id"
class="group relative flex h-[42px] w-full items-center overflow-hidden rounded-[10px] border bg-white/70" class="group relative flex h-[42px] w-full items-center overflow-hidden rounded-[10px] border bg-white/70"
:class="uploadFileItem.status === 'error' ? 'border-error-font-color' : 'border-transparent'" :class="uploadDocumentItem.status === 'error' ? 'border-error-font-color' : 'border-transparent'"
> >
<div class="flex w-full items-center justify-between px-3.5"> <div class="flex w-full items-center justify-between px-3.5">
<div class="flex w-full items-center overflow-hidden"> <div class="flex w-full items-center overflow-hidden">
<img :src="uploadFileIcon(uploadFileItem.type!)" class="h-7 w-7" /> <img :src="uploadFileIcon(uploadDocumentItem.type!)" class="h-7 w-7" />
<div class="mx-2.5 flex flex-1 flex-col overflow-hidden"> <div class="mx-2.5 flex flex-1 flex-col overflow-hidden">
<n-ellipsis> <n-ellipsis>
{{ uploadFileItem.name }} {{ uploadDocumentItem.name }}
</n-ellipsis> </n-ellipsis>
</div> </div>
</div> </div>
<n-progress <n-progress
v-show="!['finished', 'error'].includes(uploadFileItem.status)" v-show="!['finished', 'error'].includes(uploadDocumentItem.status)"
class="left-13.5 w-[calc(100%-78px)]! absolute bottom-0" class="left-13.5 w-[calc(100%-78px)]! absolute bottom-0"
type="line" type="line"
rail-color="#F3F3F3" rail-color="#F3F3F3"
:height="4" :height="4"
:percentage="uploadFileItem.percentage" :percentage="uploadDocumentItem.percentage"
:show-indicator="false" :show-indicator="false"
/> />
<div v-show="['finished', 'error'].includes(uploadFileItem.status)" class="hidden group-hover:block"> <div v-show="['finished', 'error'].includes(uploadDocumentItem.status)" class="hidden group-hover:block">
<n-popover trigger="hover" placement="top-end" :show-arrow="false"> <n-popover trigger="hover" placement="top-end" :show-arrow="false">
<template #trigger> <template #trigger>
<i <i
class="iconfont icon-close cursor-pointer hover:opacity-80" class="iconfont icon-close cursor-pointer hover:opacity-80"
:class="uploadFileItem.status === 'error' ? 'text-error-font-color' : 'text-font-color'" :class="uploadDocumentItem.status === 'error' ? 'text-error-font-color' : 'text-font-color'"
@click="handleRemoveFile(uploadFileItem.id)" @click="handleRemoveFile(uploadDocumentItem.id)"
/> />
</template> </template>
<span>{{ t('common_module.dialogue_module.cancel_associate_file_tip') }}</span> <span>{{ t('common_module.dialogue_module.cancel_associate_file_tip') }}</span>
...@@ -594,7 +595,7 @@ defineExpose({ ...@@ -594,7 +595,7 @@ defineExpose({
<n-upload <n-upload
:show-file-list="false" :show-file-list="false"
accept=".doc, .pdf, .docx, .txt, .md" accept=".doc, .pdf, .docx, .txt, .md, image/png, image/jpeg, image/jpg, image/webp"
abstract abstract
@before-upload="handleLimitUpload" @before-upload="handleLimitUpload"
@change="handleUpload" @change="handleUpload"
...@@ -616,30 +617,6 @@ defineExpose({ ...@@ -616,30 +617,6 @@ defineExpose({
</n-upload-trigger> </n-upload-trigger>
</n-upload> </n-upload>
<n-upload
:show-file-list="false"
accept="image/png, image/jpeg, image/jpg, image/webp"
abstract
@before-upload="handleLimitUploadImage"
@change="handleUploadImage"
>
<n-upload-trigger #="{ handleClick }" abstract>
<n-popover style="width: 210px" trigger="hover">
<template #trigger>
<div
v-show="isEnableUploadImage"
class="h-7.5 w-7.5 hover:text-theme-color text-font-color mb-1 flex cursor-pointer items-center justify-center rounded-full bg-white"
@click="handleSelectImage(handleClick)"
>
<i class="iconfont icon-upload-image flex h-4 w-4 items-center justify-center" />
</div>
</template>
<span class="text-xs"> {{ t('common_module.dialogue_module.upload_image_limit') }} </span>
</n-popover>
</n-upload-trigger>
</n-upload>
<n-popover trigger="hover"> <n-popover trigger="hover">
<template #trigger> <template #trigger>
<div <div
......
...@@ -2,11 +2,12 @@ ...@@ -2,11 +2,12 @@
import { computed, ref, useTemplateRef } from 'vue' import { computed, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { throttle } from 'lodash-es' import { throttle } from 'lodash-es'
import { CheckOne, Down } from '@icon-park/vue-next' import { Down } from '@icon-park/vue-next'
import CustomLoading from './custom-loading.vue' import CustomLoading from './custom-loading.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config' import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue' import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import ExecuteCodeRender from '@/components/execute-code-render/execute-code-render.vue' import ExecuteCodeRender from '@/components/execute-code-render/execute-code-render.vue'
import ExecutePluginRender from '@/components/execute-plugin-render/execute-plugin-render.vue'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { asBlob } from 'html-docx-js-typescript' import { asBlob } from 'html-docx-js-typescript'
import { downloadFile } from '@/utils/download-file' import { downloadFile } from '@/utils/download-file'
...@@ -156,7 +157,7 @@ const handleContentCopy = throttle( ...@@ -156,7 +157,7 @@ const handleContentCopy = throttle(
<div class="flex min-w-[80px] max-w-full flex-col items-start overflow-x-auto"> <div class="flex min-w-[80px] max-w-full flex-col items-start overflow-x-auto">
<div <div
class="w-full flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]" class="w-full flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
:class="role === 'user' ? 'user-content-container bg-[#4b87ff] text-white' : 'bg-white text-[#333]'" :class="role === 'user' ? 'user-content-container bg-[#777EF9] text-white' : 'bg-white text-[#333]'"
> >
<img <img
v-show="role === 'user' && messageItem.imageUrl" v-show="role === 'user' && messageItem.imageUrl"
...@@ -164,26 +165,18 @@ const handleContentCopy = throttle( ...@@ -164,26 +165,18 @@ const handleContentCopy = throttle(
class="max-h-[120px]! mb-[11px] rounded-[10px] object-contain" class="max-h-[120px]! mb-[11px] rounded-[10px] object-contain"
/> />
<div <!-- 插件返回结果 -->
v-show="role === 'assistant' && messageItem.pluginName" <div v-show="role === 'assistant' && messageItem?.pluginResult?.pluginName" class="mb-[11px] w-full">
class="mb-[8px] flex items-center gap-[5px] font-['Microsoft_YaHei_UI'] text-[#999]" <ExecutePluginRender
> :display-format="messageItem.pluginResult?.displayFormat || 'none'"
<div :name="messageItem.pluginResult?.pluginName!"
v-show="messageItem.isTextContentLoading" :content="messageItem.pluginResult?.pluginContent!"
class="bg-px-plugin_loading-gif h-[14px] w-[14px] bg-contain bg-center bg-no-repeat" :arguments="messageItem.pluginResult?.arguments!"
:plugin-loading="messageItem.isTextContentLoading"
/> />
<CheckOne v-show="!messageItem.isTextContentLoading" theme="outline" size="16" fill="#40bd23" />
<span class="leading-5">
{{
messageItem.isTextContentLoading
? t('common_module.plugin_in_progress', { pluginName: messageItem.pluginName })
: t('common_module.plugin_executed_successfully', { pluginName: messageItem.pluginName })
}}
</span>
</div> </div>
<div v-if="messageItem.isTextContentLoading" class="flex justify-center py-1.5"> <div v-if="messageItem.isTextContentLoading" class="flex px-4 py-1.5">
<CustomLoading /> <CustomLoading />
</div> </div>
...@@ -283,4 +276,10 @@ const handleContentCopy = throttle( ...@@ -283,4 +276,10 @@ const handleContentCopy = throttle(
color: #fff !important; color: #fff !important;
} }
} }
:deep(.markdown-render-container .markdown-render-inner) {
table {
color: #fff !important;
}
}
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue' import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
import { useElementVisibility } from '@vueuse/core'
import MessageItem from './message-item.vue' import MessageItem from './message-item.vue'
import ContinueQuestion from './continue-question.vue' import ContinueQuestion from './continue-question.vue'
import { useScroll } from '@/composables/useScroll' import { useScroll } from '@/composables/useScroll'
import { useBackBottom } from '@/composables/useBackBottom'
import HitKnowledge from './hit-knowledge.vue' import HitKnowledge from './hit-knowledge.vue'
interface Props { interface Props {
...@@ -22,8 +22,8 @@ defineEmits<{ ...@@ -22,8 +22,8 @@ defineEmits<{
}>() }>()
const { scrollRef, scrollToBottom } = useScroll() const { scrollRef, scrollToBottom } = useScroll()
const backBottomBtnFlagRef = useTemplateRef<HTMLDivElement | null>('backBottomBtnFlagRef')
const { visible, clickBackBottom, throttleScrollContainer } = useBackBottom(scrollRef, scrollToBottom) const isNotShowBackBottomBtn = useElementVisibility(backBottomBtnFlagRef)
const isShowKnowledgeContent = ref(false) const isShowKnowledgeContent = ref(false)
const currentKnowledgeContentResult = ref<KnowledgeContentResultItem[]>([]) const currentKnowledgeContentResult = ref<KnowledgeContentResultItem[]>([])
...@@ -48,9 +48,13 @@ defineExpose({ ...@@ -48,9 +48,13 @@ defineExpose({
scrollToBottom: handleScrollToBottom, scrollToBottom: handleScrollToBottom,
}) })
function clickBackBottom() {
scrollToBottom()
}
function handleScrollToBottom() { function handleScrollToBottom() {
nextTick(() => { nextTick(() => {
!visible.value && scrollToBottom() isNotShowBackBottomBtn.value && scrollToBottom()
}) })
} }
...@@ -61,8 +65,8 @@ function handleShowKnowledgeResult(knowledgeContentResult: KnowledgeContentResul ...@@ -61,8 +65,8 @@ function handleShowKnowledgeResult(knowledgeContentResult: KnowledgeContentResul
</script> </script>
<template> <template>
<main ref="scrollRef" class="h-full overflow-y-auto overflow-x-hidden px-5" @scroll="throttleScrollContainer"> <main ref="scrollRef" class="h-full overflow-y-auto overflow-x-hidden px-5">
<div> <div class="relative">
<MessageItem <MessageItem
v-for="[key, messageItem] in messageList" v-for="[key, messageItem] in messageList"
:key="key" :key="key"
...@@ -72,23 +76,41 @@ function handleShowKnowledgeResult(knowledgeContentResult: KnowledgeContentResul ...@@ -72,23 +76,41 @@ function handleShowKnowledgeResult(knowledgeContentResult: KnowledgeContentResul
@audio-pause="() => $emit('audioPause')" @audio-pause="() => $emit('audioPause')"
@show-knowledge-result="handleShowKnowledgeResult(messageItem.knowledgeContentResult)" @show-knowledge-result="handleShowKnowledgeResult(messageItem.knowledgeContentResult)"
/> />
</div>
<div v-show="isShowContinueQuestion"> <div v-show="isShowContinueQuestion">
<ContinueQuestion :continuous-question-list="continuousQuestionList" /> <ContinueQuestion :continuous-question-list="continuousQuestionList" />
</div> </div>
<div <div ref="backBottomBtnFlagRef" class="-z-1 absolute bottom-0 h-[100px] w-full"></div>
v-show="visible"
class="flex-center hover:text-theme-color absolute bottom-5 right-5 h-[26px] w-[26px] cursor-pointer rounded-full bg-white shadow-[0_0_0_1px_#ededed]"
@click.stop="clickBackBottom"
>
<i class="iconfont icon-huidaodibu text-[12px]" />
</div> </div>
<Transition name="back-bottom-btn" mode="out-in">
<div
v-show="!isNotShowBackBottomBtn"
class="flex-center hover:text-theme-color absolute bottom-5 right-5 h-[26px] w-[26px] cursor-pointer rounded-full bg-white shadow-[0_0_0_1px_#ededed]"
@click.stop="clickBackBottom"
>
<i class="iconfont icon-huidaodibu text-[12px]" />
</div>
</Transition>
<HitKnowledge <HitKnowledge
v-model:is-show-drawer="isShowKnowledgeContent" v-model:is-show-drawer="isShowKnowledgeContent"
:knowledge-content-result="currentKnowledgeContentResult!" :knowledge-content-result="currentKnowledgeContentResult!"
/> />
</main> </main>
</template> </template>
<style lang="scss" scoped>
.back-bottom-btn-enter-active,
.back-bottom-btn-leave-active {
transition-timing-function: ease-in-out;
transition-duration: 0.2s;
transition-property: opacity;
}
.back-bottom-btn-enter-from,
.back-bottom-btn-leave-to {
opacity: 0;
}
</style>
...@@ -13,7 +13,6 @@ import { useLayoutConfig } from '@/composables/useLayoutConfig' ...@@ -13,7 +13,6 @@ import { useLayoutConfig } from '@/composables/useLayoutConfig'
import { TEXTTOSPEECH_WS_URL } from '@/config/base-url' import { TEXTTOSPEECH_WS_URL } from '@/config/base-url'
import WebSocketCtr from '@/utils/web-socket-ctr' import WebSocketCtr from '@/utils/web-socket-ctr'
import { ChannelType } from '@/enums/channel' import { ChannelType } from '@/enums/channel'
import { useUploadImage } from '@/composables/useUploadImage'
interface Props { interface Props {
agentId: string agentId: string
...@@ -21,7 +20,6 @@ interface Props { ...@@ -21,7 +20,6 @@ interface Props {
messageList: Map<string, ConversationMessageItem> messageList: Map<string, ConversationMessageItem>
continuousQuestionStatus: 'default' | 'close' continuousQuestionStatus: 'default' | 'close'
isEnableDocumentParse: boolean isEnableDocumentParse: boolean
isEnableUploadImage: boolean
isEnableVoice: boolean isEnableVoice: boolean
answerAudioAutoPlay: boolean answerAudioAutoPlay: boolean
answerAudioPlaying: boolean answerAudioPlaying: boolean
...@@ -52,7 +50,6 @@ const { isMobile } = useLayoutConfig() ...@@ -52,7 +50,6 @@ const { isMobile } = useLayoutConfig()
const userStore = useUserStore() const userStore = useUserStore()
const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile() const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile()
const { uploadImageList, handleLimitUploadImage, handleUploadImage, handleRemoveUploadImage } = useUploadImage()
const isAnswerResponseWait = defineModel<boolean>('isAnswerResponseLoading', { required: true }) const isAnswerResponseWait = defineModel<boolean>('isAnswerResponseLoading', { required: true })
const isAnswerResponseInterrupt = defineModel<boolean>('isAnswerResponseInterrupt', { required: true }) const isAnswerResponseInterrupt = defineModel<boolean>('isAnswerResponseInterrupt', { required: true })
...@@ -97,6 +94,14 @@ const isInputMessageDisabled = computed(() => { ...@@ -97,6 +94,14 @@ const isInputMessageDisabled = computed(() => {
) )
}) })
const uploadDocumentList = computed(() => {
return uploadFileList.value.filter((fileItem) => fileItem.format === 'document')
})
const uploadImageList = computed(() => {
return uploadFileList.value.filter((fileItem) => fileItem.format === 'image')
})
const isUploadFileDisabled = computed(() => { const isUploadFileDisabled = computed(() => {
return uploadFileList.value.length === 1 return uploadFileList.value.length === 1
}) })
...@@ -140,11 +145,16 @@ function messageItemFactory(): ConversationMessageItem { ...@@ -140,11 +145,16 @@ function messageItemFactory(): ConversationMessageItem {
isVoiceLoading: false, isVoiceLoading: false,
isVoicePlaying: false, isVoicePlaying: false,
voiceFragmentUrlList: [], voiceFragmentUrlList: [],
pluginName: '',
imageUrl: '', imageUrl: '',
reasoningContent: '', reasoningContent: '',
knowledgeContentResult: [], knowledgeContentResult: [],
dbChainSQLContent: '', dbChainSQLContent: '',
pluginResult: {
displayFormat: 'none',
pluginName: '',
arguments: '',
pluginContent: '',
},
} }
} }
...@@ -236,7 +246,14 @@ function handleMessageSend(lastQuestionContent?: string) { ...@@ -236,7 +246,14 @@ function handleMessageSend(lastQuestionContent?: string) {
// 插件 // 插件
if (data.function && data.function.name) { if (data.function && data.function.name) {
emit('updateSpecifyMessageItem', latestAssistantMessageKey, { pluginName: data.function.name }) emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
pluginResult: {
displayFormat: data.function.displayFormat,
pluginName: data.function.name,
arguments: data.function.arguments,
pluginContent: data.function.result,
},
})
emit('updatePageScroll') emit('updatePageScroll')
} }
...@@ -292,7 +309,7 @@ function handleMessageSend(lastQuestionContent?: string) { ...@@ -292,7 +309,7 @@ function handleMessageSend(lastQuestionContent?: string) {
}) })
inputMessageContent.value = '' inputMessageContent.value = ''
uploadImageList.value = [] uploadFileList.value = uploadFileList.value.filter((item) => item.format === 'document')
} }
function errorMessageResponse() { function errorMessageResponse() {
...@@ -334,16 +351,16 @@ function handleSelectFile(cb: () => void) { ...@@ -334,16 +351,16 @@ function handleSelectFile(cb: () => void) {
cb() cb()
} }
function handleSelectImage(cb: () => void) { // function handleSelectImage(cb: () => void) {
if (uploadImageList.value.length > 0) { // if (uploadImageList.value.length > 0) {
window.$message.ctWarning('', t('common_module.dialogue_module.overwrite_file_tip')).then(() => { // window.$message.ctWarning('', t('common_module.dialogue_module.overwrite_file_tip')).then(() => {
cb() // cb()
}) // })
return // return
} // }
cb() // cb()
} // }
function sentenceExtract(messageId: string) { function sentenceExtract(messageId: string) {
const symbolRegExp = /[。!?;.!?;~]/g const symbolRegExp = /[。!?;.!?;~]/g
...@@ -468,7 +485,7 @@ defineExpose({ ...@@ -468,7 +485,7 @@ defineExpose({
> >
<div <div
class="absolute right-[-4px] top-[-4px] flex h-4 w-4 cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.55)] hover:opacity-80" class="absolute right-[-4px] top-[-4px] flex h-4 w-4 cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.55)] hover:opacity-80"
@click="handleRemoveUploadImage(uploadImageItem.id)" @click="handleRemoveFile(uploadImageItem.id)"
> >
<CloseSmall theme="outline" size="16" fill="#fff" /> <CloseSmall theme="outline" size="16" fill="#fff" />
</div> </div>
...@@ -487,36 +504,36 @@ defineExpose({ ...@@ -487,36 +504,36 @@ defineExpose({
</div> </div>
</div> </div>
<ul v-show="uploadFileList.length > 0" class="mb-1.5 grid gap-1.5"> <ul v-show="uploadDocumentList.length > 0" class="mb-1.5 grid gap-1.5">
<li <li
v-for="uploadFileItem in uploadFileList" v-for="uploadDocumentItem in uploadDocumentList"
:key="uploadFileItem.id" :key="uploadDocumentItem.id"
class="group relative flex h-[42px] w-full items-center overflow-hidden rounded-[10px] border bg-white/70" class="group relative flex h-[42px] w-full items-center overflow-hidden rounded-[10px] border bg-white/70"
:class="uploadFileItem.status === 'error' ? 'border-error-font-color' : 'border-transparent'" :class="uploadDocumentItem.status === 'error' ? 'border-error-font-color' : 'border-transparent'"
> >
<div class="flex w-full items-center justify-between px-3.5"> <div class="flex w-full items-center justify-between px-3.5">
<div class="flex w-full items-center overflow-hidden"> <div class="flex w-full items-center overflow-hidden">
<img :src="uploadFileIcon(uploadFileItem.type!)" class="h-7 w-7" /> <img :src="uploadFileIcon(uploadDocumentItem.type!)" class="h-7 w-7" />
<div class="mx-2.5 flex flex-1 flex-col overflow-hidden"> <div class="mx-2.5 flex flex-1 flex-col overflow-hidden">
<n-ellipsis> <n-ellipsis>
{{ uploadFileItem.name }} {{ uploadDocumentItem.name }}
</n-ellipsis> </n-ellipsis>
</div> </div>
</div> </div>
<n-progress <n-progress
v-show="!['finished', 'error'].includes(uploadFileItem.status)" v-show="!['finished', 'error'].includes(uploadDocumentItem.status)"
class="left-13.5 w-[calc(100%-78px)]! absolute bottom-0" class="left-13.5 w-[calc(100%-78px)]! absolute bottom-0"
type="line" type="line"
rail-color="#F3F3F3" rail-color="#F3F3F3"
:height="4" :height="4"
:percentage="uploadFileItem.percentage" :percentage="uploadDocumentItem.percentage"
:show-indicator="false" :show-indicator="false"
/> />
<div <div
v-show="['finished', 'error'].includes(uploadFileItem.status)" v-show="['finished', 'error'].includes(uploadDocumentItem.status)"
class="group-hover:block" class="group-hover:block"
:class="isMobile ? 'block' : 'hidden'" :class="isMobile ? 'block' : 'hidden'"
> >
...@@ -524,8 +541,8 @@ defineExpose({ ...@@ -524,8 +541,8 @@ defineExpose({
<template #trigger> <template #trigger>
<i <i
class="iconfont icon-close cursor-pointer hover:opacity-80" class="iconfont icon-close cursor-pointer hover:opacity-80"
:class="uploadFileItem.status === 'error' ? 'text-error-font-color' : 'text-font-color'" :class="uploadDocumentItem.status === 'error' ? 'text-error-font-color' : 'text-font-color'"
@click="handleRemoveFile(uploadFileItem.id)" @click="handleRemoveFile(uploadDocumentItem.id)"
/> />
</template> </template>
<span>{{ t('common_module.dialogue_module.cancel_associate_file_tip') }}</span> <span>{{ t('common_module.dialogue_module.cancel_associate_file_tip') }}</span>
...@@ -571,7 +588,7 @@ defineExpose({ ...@@ -571,7 +588,7 @@ defineExpose({
<n-upload <n-upload
:show-file-list="false" :show-file-list="false"
accept=".doc, .pdf, .docx, .txt, .md" accept=".doc, .pdf, .docx, .txt, .md, image/png, image/jpeg, image/jpg, image/webp"
:disabled="!isLogin" :disabled="!isLogin"
abstract abstract
@before-upload="handleLimitUpload" @before-upload="handleLimitUpload"
...@@ -599,30 +616,6 @@ defineExpose({ ...@@ -599,30 +616,6 @@ defineExpose({
</n-upload-trigger> </n-upload-trigger>
</n-upload> </n-upload>
<n-upload
:show-file-list="false"
accept="image/png, image/jpeg, image/jpg, image/webp"
abstract
@before-upload="handleLimitUploadImage"
@change="handleUploadImage"
>
<n-upload-trigger #="{ handleClick }" abstract>
<n-popover style="width: 210px" trigger="hover">
<template #trigger>
<div
v-show="isEnableUploadImage"
class="h-7.5 w-7.5 hover:text-theme-color text-font-color mb-1 flex cursor-pointer items-center justify-center rounded-full bg-white"
@click="handleSelectImage(handleClick)"
>
<i class="iconfont icon-upload-image flex h-4 w-4 items-center justify-center" />
</div>
</template>
<span class="text-xs"> {{ t('common_module.dialogue_module.upload_image_limit') }} </span>
</n-popover>
</n-upload-trigger>
</n-upload>
<n-popover trigger="hover"> <n-popover trigger="hover">
<template #trigger> <template #trigger>
<div <div
......
...@@ -2,11 +2,12 @@ ...@@ -2,11 +2,12 @@
import { computed, ref, useTemplateRef } from 'vue' import { computed, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { throttle } from 'lodash-es' import { throttle } from 'lodash-es'
import { CheckOne, Down } from '@icon-park/vue-next' import { Down } from '@icon-park/vue-next'
import CustomLoading from './custom-loading.vue' import CustomLoading from './custom-loading.vue'
import MusicWavesLoading from './music-waves-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 EditorDrawer from '@/components/editor-drawer/editor-drawer.vue' import EditorDrawer from '@/components/editor-drawer/editor-drawer.vue'
import ExecutePluginRender from '@/components/execute-plugin-render/execute-plugin-render.vue'
import ExecuteCodeRender from '@/components/execute-code-render/execute-code-render.vue' import ExecuteCodeRender from '@/components/execute-code-render/execute-code-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'
...@@ -174,7 +175,7 @@ const handleContentCopy = throttle( ...@@ -174,7 +175,7 @@ const handleContentCopy = throttle(
<div class="flex min-w-[80px] flex-col" :class="[isMobile ? 'max-w-[calc(100%-20px)]' : 'max-w-full']"> <div class="flex min-w-[80px] flex-col" :class="[isMobile ? 'max-w-[calc(100%-20px)]' : 'max-w-full']">
<div <div
class="w-full flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]" class="w-full flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
:class="[role === 'user' ? 'user-content-container bg-theme-color text-white' : 'bg-white text-[#333]']" :class="[role === 'user' ? 'user-content-container bg-[#777EF9] text-white' : 'bg-white text-[#333]']"
> >
<img <img
v-show="role === 'user' && messageItem.imageUrl" v-show="role === 'user' && messageItem.imageUrl"
...@@ -182,23 +183,15 @@ const handleContentCopy = throttle( ...@@ -182,23 +183,15 @@ const handleContentCopy = throttle(
class="max-h-[120px]! mb-[11px] rounded-[10px] object-contain" class="max-h-[120px]! mb-[11px] rounded-[10px] object-contain"
/> />
<div <!-- 插件返回结果 -->
v-show="role === 'assistant' && messageItem.pluginName" <div v-show="role === 'assistant' && messageItem?.pluginResult?.pluginName" class="mb-[11px] w-full">
class="mb-[8px] flex items-center gap-[5px] font-['Microsoft_YaHei_UI'] text-[#999]" <ExecutePluginRender
> :display-format="messageItem.pluginResult?.displayFormat || 'none'"
<div :name="messageItem.pluginResult?.pluginName!"
v-show="messageItem.isTextContentLoading" :content="messageItem.pluginResult?.pluginContent!"
class="bg-px-plugin_loading-gif h-[14px] w-[14px] bg-contain bg-center bg-no-repeat" :arguments="messageItem.pluginResult?.arguments!"
:plugin-loading="messageItem.isTextContentLoading"
/> />
<CheckOne v-show="!messageItem.isTextContentLoading" theme="outline" size="16" fill="#40bd23" />
<span class="leading-5">
{{
messageItem.isTextContentLoading
? t('common_module.plugin_in_progress', { pluginName: messageItem.pluginName })
: t('common_module.plugin_executed_successfully', { pluginName: messageItem.pluginName })
}}
</span>
</div> </div>
<div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4"> <div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4">
...@@ -313,4 +306,10 @@ const handleContentCopy = throttle( ...@@ -313,4 +306,10 @@ const handleContentCopy = throttle(
color: #fff !important; color: #fff !important;
} }
} }
:deep(.markdown-render-container .markdown-render-inner) {
table {
color: #fff !important;
}
}
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, watch } from 'vue' import { computed, nextTick, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useElementVisibility } from '@vueuse/core'
import MessageItem from './message-item.vue' import MessageItem from './message-item.vue'
import ContinueQuestion from './continue-question.vue' import ContinueQuestion from './continue-question.vue'
import { useScroll } from '@/composables/useScroll' import { useScroll } from '@/composables/useScroll'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { useBackBottom } from '@/composables/useBackBottom'
import { useLayoutConfig } from '@/composables/useLayoutConfig' import { useLayoutConfig } from '@/composables/useLayoutConfig'
interface Props { interface Props {
...@@ -30,8 +30,8 @@ const { t } = useI18n() ...@@ -30,8 +30,8 @@ const { t } = useI18n()
const { isMobile } = useLayoutConfig() const { isMobile } = useLayoutConfig()
const { scrollRef, scrollToBottom } = useScroll() const { scrollRef, scrollToBottom } = useScroll()
const backBottomBtnFlagRef = useTemplateRef<HTMLDivElement | null>('backBottomBtnFlagRef')
const { visible, clickBackBottom, throttleScrollContainer } = useBackBottom(scrollRef, scrollToBottom) const isNotShowBackBottomBtn = useElementVisibility(backBottomBtnFlagRef)
const isShowContinueQuestion = computed(() => { const isShowContinueQuestion = computed(() => {
return ( return (
...@@ -54,16 +54,20 @@ defineExpose({ ...@@ -54,16 +54,20 @@ defineExpose({
scrollToBottom: handleScrollToBottom, scrollToBottom: handleScrollToBottom,
}) })
function clickBackBottom() {
scrollToBottom()
}
function handleScrollToBottom() { function handleScrollToBottom() {
nextTick(() => { nextTick(() => {
!visible.value && scrollToBottom() isNotShowBackBottomBtn.value && scrollToBottom()
}) })
} }
</script> </script>
<template> <template>
<main ref="scrollRef" class="h-full overflow-y-auto overflow-x-hidden px-5" @scroll="throttleScrollContainer"> <main ref="scrollRef" class="h-full overflow-y-auto overflow-x-hidden px-5">
<div> <div class="relative">
<MessageItem <MessageItem
v-for="[key, messageItem] in messageList" v-for="[key, messageItem] in messageList"
:key="key" :key="key"
...@@ -73,19 +77,21 @@ function handleScrollToBottom() { ...@@ -73,19 +77,21 @@ function handleScrollToBottom() {
@audio-play="() => $emit('audioPlay', messageItem)" @audio-play="() => $emit('audioPlay', messageItem)"
@audio-pause="() => $emit('audioPause')" @audio-pause="() => $emit('audioPause')"
/> />
</div>
<p v-show="isMobile && isAnswerResponseLoading" class="ml-1 mt-[7px] text-xs text-[#84868c]"> <p v-show="isMobile && isAnswerResponseLoading" class="ml-1 mt-[7px] text-xs text-[#84868c]">
{{ t('common_module.dialogue_module.do_not_exit_page') }} {{ t('common_module.dialogue_module.do_not_exit_page') }}
</p> </p>
<div v-show="isShowContinueQuestion">
<ContinueQuestion :continuous-question-list="continuousQuestionList" />
</div>
<div v-show="isShowContinueQuestion"> <div ref="backBottomBtnFlagRef" class="-z-1 absolute bottom-0 h-[100px] w-full"></div>
<ContinueQuestion :continuous-question-list="continuousQuestionList" />
</div> </div>
<Transition name="back-bottom-btn" mode="out-in"> <Transition name="back-bottom-btn" mode="out-in">
<div <div
v-show="visible" v-show="!isNotShowBackBottomBtn"
class="flex-center hover:text-theme-color absolute bottom-5 right-5 h-6 w-6 cursor-pointer rounded-full bg-white shadow-[0_0_0_1px_#ededed]" class="flex-center hover:text-theme-color absolute bottom-5 right-5 h-6 w-6 cursor-pointer rounded-full bg-white shadow-[0_0_0_1px_#ededed]"
@click.stop="clickBackBottom" @click.stop="clickBackBottom"
> >
......
...@@ -13,7 +13,6 @@ import { useLayoutConfig } from '@/composables/useLayoutConfig' ...@@ -13,7 +13,6 @@ import { useLayoutConfig } from '@/composables/useLayoutConfig'
import { TEXTTOSPEECH_WS_URL } from '@/config/base-url' import { TEXTTOSPEECH_WS_URL } from '@/config/base-url'
import WebSocketCtr from '@/utils/web-socket-ctr' import WebSocketCtr from '@/utils/web-socket-ctr'
import { ChannelType } from '@/enums/channel' import { ChannelType } from '@/enums/channel'
import { useUploadImage } from '@/composables/useUploadImage'
import { showDialog } from 'vant' import { showDialog } from 'vant'
interface Props { interface Props {
...@@ -22,7 +21,6 @@ interface Props { ...@@ -22,7 +21,6 @@ interface Props {
messageList: Map<string, ConversationMessageItem> messageList: Map<string, ConversationMessageItem>
continuousQuestionStatus: 'default' | 'close' continuousQuestionStatus: 'default' | 'close'
isEnableDocumentParse: boolean isEnableDocumentParse: boolean
isEnableUploadImage: boolean
isEnableVoice: boolean isEnableVoice: boolean
answerAudioAutoPlay: boolean answerAudioAutoPlay: boolean
answerAudioPlaying: boolean answerAudioPlaying: boolean
...@@ -53,7 +51,6 @@ const { isMobile } = useLayoutConfig() ...@@ -53,7 +51,6 @@ const { isMobile } = useLayoutConfig()
const userStore = useUserStore() const userStore = useUserStore()
const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile() const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile()
const { uploadImageList, handleLimitUploadImage, handleUploadImage, handleRemoveUploadImage } = useUploadImage()
const isAnswerResponseWait = defineModel<boolean>('isAnswerResponseLoading', { required: true }) const isAnswerResponseWait = defineModel<boolean>('isAnswerResponseLoading', { required: true })
const isAnswerResponseInterrupt = defineModel<boolean>('isAnswerResponseInterrupt', { required: true }) const isAnswerResponseInterrupt = defineModel<boolean>('isAnswerResponseInterrupt', { required: true })
...@@ -98,6 +95,14 @@ const isInputMessageDisabled = computed(() => { ...@@ -98,6 +95,14 @@ const isInputMessageDisabled = computed(() => {
) )
}) })
const uploadDocumentList = computed(() => {
return uploadFileList.value.filter((fileItem) => fileItem.format === 'document')
})
const uploadImageList = computed(() => {
return uploadFileList.value.filter((fileItem) => fileItem.format === 'image')
})
const isUploadFileDisabled = computed(() => { const isUploadFileDisabled = computed(() => {
return uploadFileList.value.length === 1 return uploadFileList.value.length === 1
}) })
...@@ -141,11 +146,16 @@ function messageItemFactory(): ConversationMessageItem { ...@@ -141,11 +146,16 @@ function messageItemFactory(): ConversationMessageItem {
isVoiceLoading: false, isVoiceLoading: false,
isVoicePlaying: false, isVoicePlaying: false,
voiceFragmentUrlList: [], voiceFragmentUrlList: [],
pluginName: '',
imageUrl: '', imageUrl: '',
reasoningContent: '', reasoningContent: '',
knowledgeContentResult: [], knowledgeContentResult: [],
dbChainSQLContent: '', dbChainSQLContent: '',
pluginResult: {
displayFormat: 'none',
pluginName: '',
arguments: '',
pluginContent: '',
},
} }
} }
...@@ -237,7 +247,14 @@ function handleMessageSend(lastQuestionContent?: string) { ...@@ -237,7 +247,14 @@ function handleMessageSend(lastQuestionContent?: string) {
// 插件 // 插件
if (data.function && data.function.name) { if (data.function && data.function.name) {
emit('updateSpecifyMessageItem', latestAssistantMessageKey, { pluginName: data.function.name }) emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
pluginResult: {
displayFormat: data.function.displayFormat,
pluginName: data.function.name,
arguments: data.function.arguments,
pluginContent: data.function.result,
},
})
emit('updatePageScroll') emit('updatePageScroll')
} }
...@@ -293,7 +310,7 @@ function handleMessageSend(lastQuestionContent?: string) { ...@@ -293,7 +310,7 @@ function handleMessageSend(lastQuestionContent?: string) {
}) })
inputMessageContent.value = '' inputMessageContent.value = ''
uploadImageList.value = [] uploadFileList.value = uploadFileList.value.filter((item) => item.format === 'document')
} }
function errorMessageResponse() { function errorMessageResponse() {
...@@ -326,17 +343,6 @@ function handleToLogin() { ...@@ -326,17 +343,6 @@ function handleToLogin() {
function handleSelectFile(cb: () => void) { function handleSelectFile(cb: () => void) {
if (isUploadFileDisabled.value) { if (isUploadFileDisabled.value) {
window.$message.ctWarning('', t('common_module.dialogue_module.overwrite_file_tip')).then(() => {
cb()
})
return
}
cb()
}
function handleSelectImage(cb: () => void) {
if (uploadImageList.value.length > 0) {
showDialog({ showDialog({
title: '', title: '',
message: t('common_module.dialogue_module.overwrite_file_tip'), message: t('common_module.dialogue_module.overwrite_file_tip'),
...@@ -347,6 +353,7 @@ function handleSelectImage(cb: () => void) { ...@@ -347,6 +353,7 @@ function handleSelectImage(cb: () => void) {
}).then(() => { }).then(() => {
cb() cb()
}) })
return return
} }
...@@ -468,7 +475,7 @@ defineExpose({ ...@@ -468,7 +475,7 @@ defineExpose({
<div class="footer-main-container"> <div class="footer-main-container">
<n-upload <n-upload
:show-file-list="false" :show-file-list="false"
accept=".doc, .pdf, .docx, .txt, .md" accept=".doc, .pdf, .docx, .txt, .md, image/png, image/jpeg, image/jpg, image/webp"
:disabled="!isLogin" :disabled="!isLogin"
abstract abstract
@before-upload="handleLimitUpload" @before-upload="handleLimitUpload"
...@@ -494,33 +501,6 @@ defineExpose({ ...@@ -494,33 +501,6 @@ defineExpose({
</n-upload-trigger> </n-upload-trigger>
</n-upload> </n-upload>
<n-upload
:show-file-list="false"
accept="image/png, image/jpeg, image/jpg, image/webp"
abstract
@before-upload="handleLimitUploadImage"
@change="handleUploadImage"
>
<n-upload-trigger #="{ handleClick }" abstract>
<n-popover style="width: 210px" trigger="hover">
<template #trigger>
<div
v-show="isEnableUploadImage"
class="upload-image-btn"
:class="
isLogin ? 'text-theme-color cursor-pointer hover:opacity-80' : 'cursor-not-allowed text-[#b8babf]'
"
@click="handleSelectImage(handleClick)"
>
<i class="iconfont icon-upload-image" />
</div>
</template>
<span class="btn-tip"> {{ t('common_module.dialogue_module.upload_image_limit') }} </span>
</n-popover>
</n-upload-trigger>
</n-upload>
<n-popover trigger="hover"> <n-popover trigger="hover">
<template #trigger> <template #trigger>
<div <div
...@@ -546,7 +526,7 @@ defineExpose({ ...@@ -546,7 +526,7 @@ defineExpose({
class="upload-image-item" class="upload-image-item"
:class="{ 'upload-image-item-error': uploadImageItem.status === UploadStatus.ERROR }" :class="{ 'upload-image-item-error': uploadImageItem.status === UploadStatus.ERROR }"
> >
<div class="close-btn" @click="handleRemoveUploadImage(uploadImageItem.id)"> <div class="close-btn" @click="handleRemoveFile(uploadImageItem.id)">
<CloseSmall theme="outline" size="16" fill="#fff" /> <CloseSmall theme="outline" size="16" fill="#fff" />
</div> </div>
...@@ -564,36 +544,36 @@ defineExpose({ ...@@ -564,36 +544,36 @@ defineExpose({
</div> </div>
</div> </div>
<ul v-show="uploadFileList.length > 0" class="upload-file-list-container"> <ul v-show="uploadDocumentList.length > 0" class="upload-file-list-container">
<li <li
v-for="uploadFileItem in uploadFileList" v-for="uploadDocumentItem in uploadDocumentList"
:key="uploadFileItem.id" :key="uploadDocumentItem.id"
class="upload-file-item" class="upload-file-item"
:class="uploadFileItem.status === 'error' ? 'border-error-font-color' : 'border-transparent'" :class="uploadDocumentItem.status === 'error' ? 'border-error-font-color' : 'border-transparent'"
> >
<div class="upload-file-item-container"> <div class="upload-file-item-container">
<div class="upload-file-item-content"> <div class="upload-file-item-content">
<img :src="uploadFileIcon(uploadFileItem.type!)" class="upload-file-icon" /> <img :src="uploadFileIcon(uploadDocumentItem.type!)" class="upload-file-icon" />
<div class="upload-file-name"> <div class="upload-file-name">
<n-ellipsis> <n-ellipsis>
{{ uploadFileItem.name }} {{ uploadDocumentItem.name }}
</n-ellipsis> </n-ellipsis>
</div> </div>
</div> </div>
<n-progress <n-progress
v-show="!['finished', 'error'].includes(uploadFileItem.status)" v-show="!['finished', 'error'].includes(uploadDocumentItem.status)"
class="upload-file-item-progress" class="upload-file-item-progress"
type="line" type="line"
rail-color="#F3F3F3" rail-color="#F3F3F3"
:height="4" :height="4"
:percentage="uploadFileItem.percentage" :percentage="uploadDocumentItem.percentage"
:show-indicator="false" :show-indicator="false"
/> />
<div <div
v-show="['finished', 'error'].includes(uploadFileItem.status)" v-show="['finished', 'error'].includes(uploadDocumentItem.status)"
class="group-hover:block" class="group-hover:block"
:class="isMobile ? 'block' : 'hidden'" :class="isMobile ? 'block' : 'hidden'"
> >
...@@ -601,8 +581,10 @@ defineExpose({ ...@@ -601,8 +581,10 @@ defineExpose({
<template #trigger> <template #trigger>
<i <i
class="iconfont icon-close close-upload-file-icon" class="iconfont icon-close close-upload-file-icon"
:class="uploadFileItem.status === 'error' ? 'upload-file-error-icon' : 'upload-file-success-icon'" :class="
@click="handleRemoveFile(uploadFileItem.id)" uploadDocumentItem.status === 'error' ? 'upload-file-error-icon' : 'upload-file-success-icon'
"
@click="handleRemoveFile(uploadDocumentItem.id)"
/> />
</template> </template>
<span>{{ t('common_module.dialogue_module.cancel_associate_file_tip') }}</span> <span>{{ t('common_module.dialogue_module.cancel_associate_file_tip') }}</span>
......
...@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n' ...@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'
import { CheckOne, Down } from '@icon-park/vue-next' import { CheckOne, Down } from '@icon-park/vue-next'
import CustomLoading from '../custom-loading.vue' import CustomLoading from '../custom-loading.vue'
import MusicWavesLoading from './music-waves-loading.vue' import MusicWavesLoading from './music-waves-loading.vue'
import ExecutePluginRender from '@/components/execute-plugin-render/execute-plugin-render.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'
...@@ -117,17 +118,15 @@ function handleShowReasoningContentSwitch() { ...@@ -117,17 +118,15 @@ function handleShowReasoningContentSwitch() {
> >
<img v-show="role === 'user' && messageItem.imageUrl" :src="messageItem.imageUrl" class="upload-image" /> <img v-show="role === 'user' && messageItem.imageUrl" :src="messageItem.imageUrl" class="upload-image" />
<div v-show="role === 'assistant' && messageItem.pluginName" class="plugin-container"> <!-- 插件返回结果 -->
<div v-show="messageItem.isTextContentLoading" class="plugin-loading" /> <div v-show="role === 'assistant' && messageItem?.pluginResult?.pluginName" class="plugin-container">
<CheckOne v-show="!messageItem.isTextContentLoading" theme="outline" size="16" fill="#40bd23" /> <ExecutePluginRender
:display-format="messageItem.pluginResult?.displayFormat || 'none'"
<span class="plugin-name"> :name="messageItem.pluginResult?.pluginName!"
{{ :content="messageItem.pluginResult?.pluginContent!"
messageItem.isTextContentLoading :arguments="messageItem.pluginResult?.arguments!"
? t('common_module.plugin_in_progress', { pluginName: messageItem.pluginName }) :plugin-loading="messageItem.isTextContentLoading"
: t('common_module.plugin_executed_successfully', { pluginName: messageItem.pluginName }) />
}}
</span>
</div> </div>
<div v-if="messageItem.isTextContentLoading" class="content-loading"> <div v-if="messageItem.isTextContentLoading" class="content-loading">
...@@ -225,15 +224,7 @@ function handleShowReasoningContentSwitch() { ...@@ -225,15 +224,7 @@ function handleShowReasoningContentSwitch() {
} }
.plugin-container { .plugin-container {
@apply mb-[8px] flex items-center gap-[5px] font-['Microsoft_YaHei_UI'] text-[#999]; @apply mb-[8px] w-full;
.plugin-loading {
@apply bg-px-plugin_loading-gif h-[14px] w-[14px] bg-contain bg-center bg-no-repeat;
}
.plugin-name {
@apply text-[12px] leading-4;
}
} }
.database-container { .database-container {
...@@ -275,4 +266,18 @@ function handleShowReasoningContentSwitch() { ...@@ -275,4 +266,18 @@ function handleShowReasoningContentSwitch() {
} }
} }
} }
:deep(.markdown-render-container .markdown-render-inner) {
table {
color: #fff !important;
}
}
:deep(.jv-container.jv-light) {
font-size: 12px;
.jv-code {
padding: 10px 0 0 !important;
}
}
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, watch } from 'vue' import { computed, nextTick, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useElementVisibility } from '@vueuse/core'
import { useScroll } from '@/composables/useScroll' import { useScroll } from '@/composables/useScroll'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { useBackBottom } from '@/composables/useBackBottom'
import MessageItem from './message-item.vue' import MessageItem from './message-item.vue'
import ContinueQuestion from './continue-question.vue' import ContinueQuestion from './continue-question.vue'
...@@ -26,8 +26,8 @@ defineEmits<{ ...@@ -26,8 +26,8 @@ defineEmits<{
const { t } = useI18n() const { t } = useI18n()
const { scrollRef, scrollToBottom } = useScroll() const { scrollRef, scrollToBottom } = useScroll()
const backBottomBtnFlagRef = useTemplateRef<HTMLDivElement | null>('backBottomBtnFlagRef')
const { visible, clickBackBottom, throttleScrollContainer } = useBackBottom(scrollRef, scrollToBottom) const isNotShowBackBottomBtn = useElementVisibility(backBottomBtnFlagRef)
const continuousQuestionList = defineModel<string[]>('continuousQuestionList', { required: true }) const continuousQuestionList = defineModel<string[]>('continuousQuestionList', { required: true })
...@@ -52,16 +52,20 @@ defineExpose({ ...@@ -52,16 +52,20 @@ defineExpose({
scrollToBottom: handleScrollToBottom, scrollToBottom: handleScrollToBottom,
}) })
function clickBackBottom() {
scrollToBottom()
}
function handleScrollToBottom() { function handleScrollToBottom() {
nextTick(() => { nextTick(() => {
!visible.value && scrollToBottom() isNotShowBackBottomBtn.value && scrollToBottom()
}) })
} }
</script> </script>
<template> <template>
<main ref="scrollRef" class="message-list-container" @scroll="throttleScrollContainer"> <main ref="scrollRef" class="message-list-container">
<div> <div class="relative">
<MessageItem <MessageItem
v-for="[key, messageItem] in messageList" v-for="[key, messageItem] in messageList"
:key="key" :key="key"
...@@ -71,18 +75,20 @@ function handleScrollToBottom() { ...@@ -71,18 +75,20 @@ function handleScrollToBottom() {
@audio-play="() => $emit('audioPlay', messageItem)" @audio-play="() => $emit('audioPlay', messageItem)"
@audio-pause="() => $emit('audioPause')" @audio-pause="() => $emit('audioPause')"
/> />
</div>
<p v-show="isAnswerResponseLoading" class="answer-response-loading-text"> <p v-show="isAnswerResponseLoading" class="answer-response-loading-text">
{{ t('common_module.dialogue_module.do_not_exit_page') }} {{ t('common_module.dialogue_module.do_not_exit_page') }}
</p> </p>
<div v-show="isShowContinueQuestion"> <div v-show="isShowContinueQuestion">
<ContinueQuestion v-model:continuous-question-list="continuousQuestionList" :type="'continuous'" /> <ContinueQuestion v-model:continuous-question-list="continuousQuestionList" :type="'continuous'" />
</div>
<div ref="backBottomBtnFlagRef" class="back-bottom-flag"></div>
</div> </div>
<Transition name="back-bottom-btn" mode="out-in"> <Transition name="back-bottom-btn" mode="out-in">
<div v-show="visible" class="back-bottom-btn" @click.stop="clickBackBottom"> <div v-show="!isNotShowBackBottomBtn" class="back-bottom-btn" @click.stop="clickBackBottom">
<i class="iconfont icon-huidaodibu back-bottom-btn-icon" /> <i class="iconfont icon-huidaodibu back-bottom-btn-icon" />
</div> </div>
</Transition> </Transition>
...@@ -97,6 +103,10 @@ function handleScrollToBottom() { ...@@ -97,6 +103,10 @@ function handleScrollToBottom() {
@apply my-[7px] ml-[4px] text-[12px] text-[#84868c]; @apply my-[7px] ml-[4px] text-[12px] text-[#84868c];
} }
.back-bottom-flag {
@apply -z-1 absolute bottom-0 h-[70px] w-full;
}
.back-bottom-btn { .back-bottom-btn {
@apply flex-center hover:text-theme-color absolute bottom-[20px] right-[20px] h-[24px] w-[24px] cursor-pointer rounded-full bg-white shadow-[0_0_0_1px_#ededed]; @apply flex-center hover:text-theme-color absolute bottom-[20px] right-[20px] h-[24px] w-[24px] cursor-pointer rounded-full bg-white shadow-[0_0_0_1px_#ededed];
......
...@@ -23,7 +23,6 @@ import { ...@@ -23,7 +23,6 @@ import {
} from '@/apis/agent-application' } from '@/apis/agent-application'
import { useLayoutConfig } from '@/composables/useLayoutConfig' import { useLayoutConfig } from '@/composables/useLayoutConfig'
import { validBrowser } from '@/utils/browser-detection' import { validBrowser } from '@/utils/browser-detection'
import { PluginType } from '@/enums/plugin'
const { t } = useI18n() const { t } = useI18n()
...@@ -85,10 +84,6 @@ const isEnableDocumentParse = computed(() => { ...@@ -85,10 +84,6 @@ const isEnableDocumentParse = computed(() => {
return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y' return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y'
}) })
const isEnableUploadImage = computed(() => {
return agentApplicationConfig.value.unitIds?.includes(PluginType.IMAGE_OCR) || false
})
const isEnableVoice = computed(() => { const isEnableVoice = computed(() => {
return !!agentApplicationConfig.value.voiceConfig.timbreId return !!agentApplicationConfig.value.voiceConfig.timbreId
}) })
...@@ -422,7 +417,6 @@ function handleToLogoutPage() { ...@@ -422,7 +417,6 @@ function handleToLogoutPage() {
: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-upload-image="isEnableUploadImage"
:is-enable-voice="isEnableVoice" :is-enable-voice="isEnableVoice"
:answer-audio-auto-play="answerAudioAutoPlay" :answer-audio-auto-play="answerAudioAutoPlay"
:answer-audio-playing="answerAudioPlaying" :answer-audio-playing="answerAudioPlaying"
......
...@@ -22,7 +22,6 @@ import { useLayoutConfig } from '@/composables/useLayoutConfig' ...@@ -22,7 +22,6 @@ 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' import { validBrowser } from '@/utils/browser-detection'
import { PluginType } from '@/enums/plugin'
const { t } = useI18n() const { t } = useI18n()
...@@ -60,10 +59,6 @@ const isEnableDocumentParse = computed(() => { ...@@ -60,10 +59,6 @@ const isEnableDocumentParse = computed(() => {
return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y' return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y'
}) })
const isEnableUploadImage = computed(() => {
return agentApplicationConfig.value.unitIds?.includes(PluginType.IMAGE_OCR) || false
})
const isEnableVoice = computed(() => { const isEnableVoice = computed(() => {
return !!agentApplicationConfig.value.voiceConfig.timbreId return !!agentApplicationConfig.value.voiceConfig.timbreId
}) })
...@@ -371,7 +366,6 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -371,7 +366,6 @@ function handleAudioPause(isClearMessageList = false) {
: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-upload-image="isEnableUploadImage"
:is-enable-voice="isEnableVoice" :is-enable-voice="isEnableVoice"
:answer-audio-auto-play="answerAudioAutoPlay" :answer-audio-auto-play="answerAudioAutoPlay"
:answer-audio-playing="answerAudioPlaying" :answer-audio-playing="answerAudioPlaying"
......
...@@ -23,10 +23,15 @@ declare interface ConversationMessageItem { ...@@ -23,10 +23,15 @@ declare interface ConversationMessageItem {
isVoicePlaying: boolean isVoicePlaying: boolean
voiceFragmentUrlList: string[] voiceFragmentUrlList: string[]
isVoiceEnabled?: boolean isVoiceEnabled?: boolean
pluginName?: string
imageUrl?: string imageUrl?: string
reasoningContent: string reasoningContent: string
modelName?: string modelName?: string
knowledgeContentResult: KnowledgeContentResultItem[] knowledgeContentResult: KnowledgeContentResultItem[]
dbChainSQLContent: string dbChainSQLContent: string
pluginResult?: {
displayFormat: 'json' | 'markdown' | 'none'
pluginName: string
arguments: string
pluginContent: string
}
} }
...@@ -154,6 +154,14 @@ declare namespace I18n { ...@@ -154,6 +154,14 @@ declare namespace I18n {
cancel_authorization: string cancel_authorization: string
get_code: string get_code: string
database: string database: string
no_database_table: string
view_database_table: string
total_database_table: string
add_database_successfully: string
remove_database_successfully: string
database_QA_executed_successfully: string
input_parameter: string
output_parameter: string
dialogue_module: { dialogue_module: {
continue_question_message: string continue_question_message: string
......
declare module 'vue-json-viewer'
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