Commit 38b905f1 authored by shirlyn.guo's avatar shirlyn.guo 👌🏻

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

parents 6c9cd7fc 5fbf2a29
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/> />
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_4711453_a5ytfgvaagl.css" /> <link rel="stylesheet" href="//at.alicdn.com/t/c/font_4711453_f5muspehl1h.css" />
<title>Model Link</title> <title>Model Link</title>
</head> </head>
......
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
"@vueuse/core": "^10.11.1", "@vueuse/core": "^10.11.1",
"axios": "^1.7.7", "axios": "^1.7.7",
"clipboardy": "^4.0.0", "clipboardy": "^4.0.0",
"cropperjs": "^1.6.2",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dompurify": "^3.2.0", "dompurify": "^3.2.0",
"github-markdown-css": "^5.7.0", "github-markdown-css": "^5.7.0",
......
...@@ -29,6 +29,9 @@ importers: ...@@ -29,6 +29,9 @@ importers:
clipboardy: clipboardy:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
cropperjs:
specifier: ^1.6.2
version: 1.6.2
dayjs: dayjs:
specifier: ^1.11.13 specifier: ^1.11.13
version: 1.11.13 version: 1.11.13
...@@ -1526,6 +1529,9 @@ packages: ...@@ -1526,6 +1529,9 @@ packages:
typescript: typescript:
optional: true optional: true
cropperjs@1.6.2:
resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==}
cross-spawn@7.0.3: cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
...@@ -4672,6 +4678,8 @@ snapshots: ...@@ -4672,6 +4678,8 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.6.2 typescript: 5.6.2
cropperjs@1.6.2: {}
cross-spawn@7.0.3: cross-spawn@7.0.3:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
......
import { request } from '@/utils/request' import { request } from '@/utils/request'
import { AxiosProgressEvent } from 'axios'
export function fetchUpload<T>(formdata: FormData) { export function fetchUpload<T>(
formdata: FormData,
config?: { onUploadProgress?: (progressEvent: AxiosProgressEvent) => void },
) {
return request.post<T>('/bosRest/upload.json', formdata, { return request.post<T>('/bosRest/upload.json', formdata, {
headers: { 'Content-Type': 'multipart/form-data' }, headers: { 'Content-Type': 'multipart/form-data' },
timeout: 120000, timeout: 120000,
...(config?.onUploadProgress ? { onUploadProgress: config.onUploadProgress } : {}),
}) })
} }
...@@ -24,3 +24,17 @@ export function fetchUserDetailInfo<T>() { ...@@ -24,3 +24,17 @@ export function fetchUserDetailInfo<T>() {
export function fetchGetMemberInfoById<T>(memberId: number) { export function fetchGetMemberInfoById<T>(memberId: number) {
return request.post<T>(`/bizMemberInfoRest/getMemberNickName.json?memberId=${memberId}`) return request.post<T>(`/bizMemberInfoRest/getMemberNickName.json?memberId=${memberId}`)
} }
export function fetchUserInfoUpdate<T>(userInfo: object) {
return request.post<T>('/bizMemberInfoRest/updateMemberInfo.json', userInfo)
}
export function fetchVerifyCode<T>(account: string, code: string) {
return request.post<T>('/judgeCodeRest/judgeCodeReturnAuthCode.json', null, { params: { account, code } })
}
export function fetchUserPasswordUpdate<T>(authCode: string, password: string) {
return request.post<T>('/bizMemberInfoRest/changeMemberPassword.json', null, {
params: { authCode, password },
})
}
<script setup lang="ts">
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
import { nextTick, ref, shallowRef, useTemplateRef } from 'vue'
const imageRef = useTemplateRef('imageRef')
// const rectanglePreview = useTemplateRef('rectanglePreview')
const roundPreview = useTemplateRef('roundPreview')
const isShowImageCropperModal = ref(false)
const cropperIns = shallowRef<Cropper | null>(null)
const isInitCropper = ref(false)
const confirmBtnLoading = ref(false)
const currentEditImageUrl = ref('')
let cropImageResolve: (value: any) => void = () => {}
let cropImageReject: (value: any) => void = () => {}
function initCropper() {
return new Promise((resolve) => {
nextTick(() => {
if (imageRef.value) {
cropperIns.value = new Cropper(imageRef.value, {
viewMode: 3,
dragMode: 'move',
aspectRatio: 1,
// cropBoxMovable: false, // 可通过拖动移动裁剪框
// cropBoxResizable: false, // 可通过拖动调整裁剪框的大小
minCropBoxWidth: 50, // 裁剪框的最小宽度
minCropBoxHeight: 50, // 裁剪框的最小高度
// autoCropArea: 1,
// preview: previewRef.value! || [],
preview: [roundPreview.value!],
// rotatable: false, // 旋转
// scalable: false, // 可伸缩
// zoomable: false, // 可缩放
// zoomOnTouch: false, // 缩放触摸
ready: () => {
if (!isInitCropper.value) {
isInitCropper.value = true
resolve('加载成功')
}
},
})
}
})
})
}
function handleCropConfirm() {
if (cropperIns.value) {
confirmBtnLoading.value = true
cropperIns.value.getCroppedCanvas().toBlob((blob) => {
if (blob) {
const fd = new FormData()
const file = new File([blob], `image.${blob.type.split('/')[1]}`, { type: blob.type })
fd.append('file', file)
cropImageResolve(file)
confirmBtnLoading.value = false
isShowImageCropperModal.value = false
}
})
}
}
function handleCropCancel() {
isShowImageCropperModal.value = false
cropImageReject(new Error('Cancel'))
}
function cropImage(url: string): Promise<File> {
currentEditImageUrl.value = url
isShowImageCropperModal.value = true
return initCropper().then(() => {
return new Promise((resolve, reject) => {
cropImageResolve = resolve
cropImageReject = reject
})
})
}
function onModalAfterLeave() {
if (cropperIns.value) {
cropperIns.value.destroy()
cropperIns.value = null
isInitCropper.value = false
}
}
defineExpose({
cropImage,
})
</script>
<template>
<n-modal v-model:show="isShowImageCropperModal" :on-after-leave="onModalAfterLeave">
<n-card class="!w-[800px]" title="图片裁切" :bordered="false" size="huge" role="dialog" aria-modal="true">
<div class="relative flex">
<div class="absolute inset-0">
<n-skeleton height="400px" width="400px" />
</div>
<div
class="h-[400px] w-[400px] transition-[opacity] duration-300 ease-in-out"
:class="{ 'opacity-0': !isInitCropper }"
>
<img ref="imageRef" class="block h-full w-full" alt="Picture" :src="currentEditImageUrl" />
</div>
<div class="ml-[40px]">
<div class="mb-[10px] text-[16px]">图片预览:</div>
<!-- <div ref="rectanglePreview" class="h-[180px] w-[180px] overflow-hidden rounded-[6px] bg-[#f3f3f3]"></div> -->
<div ref="roundPreview" class="mt-[20px] h-[180px] w-[180px] overflow-hidden rounded-full bg-[#f3f3f3]"></div>
</div>
</div>
<template #footer>
<div class="text-end">
<n-button class="!mr-[20px]" @click="handleCropCancel">取消</n-button>
<n-button type="primary" :loading="confirmBtnLoading" @click="handleCropConfirm">确定</n-button>
</div>
</template>
</n-card>
</n-modal>
</template>
<style lang="scss" scoped>
// :global(.cropper-view-box) {
// .cropper-view-box,
// .cropper-face {
// border-radius: 50%;
// }
// .cropper-view-box {
// outline: 0;
// box-shadow: 0 0 0 1px #39f;
// }
// }
// :global(.cropper-container .cropper-crop-box) {
// outline: 1px solid #000dff;
// .cropper-line {
// background-color: #000dff !important;
// }
// .cropper-point {
// background-color: #000dff !important;
// }
// }
// .cropper-container .cropper-crop-box {
// outline: 1px solid #000dff;
// .cropper-line {
// background-color: #000dff !important;
// }
// .cropper-point {
// background-color: #000dff !important;
// }
// }
</style>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, reactive, ref } from 'vue' import { onMounted, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useLayoutConfig } from '@/composables/useLayoutConfig'
const { t } = useI18n() const { t } = useI18n()
const { isMobile } = useLayoutConfig()
const isShowModal = ref(false) const isShowModal = ref(false)
const modalOptions = reactive({ const modalOptions = reactive({
...@@ -45,14 +48,17 @@ function handleShowModal(content: string, title?: string) { ...@@ -45,14 +48,17 @@ function handleShowModal(content: string, title?: string) {
<template> <template>
<n-modal v-model:show="isShowModal"> <n-modal v-model:show="isShowModal">
<div class="min-w-[420px] max-w-[600px] rounded-[10px] bg-[#fff] p-[30px]"> <div
class="rounded-[10px] bg-[#fff]"
:class="isMobile ? 'max-w-[calc(100%-40px)] p-[20px]' : 'min-w-[420px] max-w-[600px] p-[30px]'"
>
<div> <div>
<h2> <h2 class="flex items-baseline">
<i class="iconfont icon-tishi text-[18px] text-[#f25744]"></i> <i class="iconfont icon-tishi text-[18px] text-[#f25744]"></i>
<span class="font-600 ml-[5px] text-[18px]">{{ modalOptions.title || t('common_module.tip') }}</span> <span class="font-600 ml-[5px] text-[18px]">{{ modalOptions.title || t('common_module.tip') }}</span>
</h2> </h2>
<div class="mt-[20px] indent-4 text-[16px]">{{ modalOptions.content }}</div> <div class="mt-[20px] pl-4 text-[16px]">{{ modalOptions.content }}</div>
</div> </div>
<div class="mt-[50px] text-end"> <div class="mt-[50px] text-end">
......
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, useSlots } from 'vue' import { computed, ref, useSlots, useTemplateRef } from 'vue'
import CustomIcon from '../custom-icon/custom-icon.vue' import CustomIcon from '../custom-icon/custom-icon.vue'
import { fetchUpload } from '@/apis/upload' import { fetchUpload } from '@/apis/upload'
import ImageCropper from '@/components/image-cropper/image-cropper.vue'
interface Emit { interface Emit {
(e: 'formatError'): void (e: 'formatError'): void
...@@ -20,7 +21,7 @@ const props = defineProps({ ...@@ -20,7 +21,7 @@ const props = defineProps({
}, },
listType: { listType: {
type: Array, type: Array,
default: () => ['jpg', 'png', 'jpeg', 'svg', 'gif'], default: () => ['jpg', 'png', 'jpeg', 'gif'],
}, },
width: { width: {
type: Number, type: Number,
...@@ -36,6 +37,8 @@ const emit = defineEmits<Emit>() ...@@ -36,6 +37,8 @@ const emit = defineEmits<Emit>()
const slots = useSlots() const slots = useSlots()
const imageCropperRef = useTemplateRef<InstanceType<typeof ImageCropper> | null>('imageCropperRef')
const uploadLoading = ref(false) const uploadLoading = ref(false)
const isShowImageMask = ref(false) const isShowImageMask = ref(false)
...@@ -66,24 +69,23 @@ async function handleUploadImage(event: any) { ...@@ -66,24 +69,23 @@ async function handleUploadImage(event: any) {
return return
} }
const URL = window.URL || window.webkitURL if (imageCropperRef.value && file) {
const img = new Image() const URL = window.URL || window.webkitURL
img.src = URL.createObjectURL(file) imageCropperRef.value.cropImage(URL.createObjectURL(file)).then(async (file) => {
const formData = new FormData()
formData.append('file', file)
img.onload = async function () { uploadLoading.value = true
const formData = new FormData()
formData.append('file', file)
uploadLoading.value = true const res = await fetchUpload(formData).finally(() => {
uploadLoading.value = false
event.target.value = null
})
const res = await fetchUpload(formData).finally(() => { if (res.code === 0) {
uploadLoading.value = false uploadImageUrl.value = res.data as string
event.target.value = null }
}) })
if (res.code === 0) {
uploadImageUrl.value = res.data as string
}
} }
} }
</script> </script>
...@@ -120,5 +122,7 @@ async function handleUploadImage(event: any) { ...@@ -120,5 +122,7 @@ async function handleUploadImage(event: any) {
<input id="upload" type="file" :accept="uploadImageType" class="hidden" @change="handleUploadImage" /> <input id="upload" type="file" :accept="uploadImageType" class="hidden" @change="handleUploadImage" />
</div> </div>
<ImageCropper ref="imageCropperRef" />
</div> </div>
</template> </template>
import { reactive, ref } from 'vue'
import { UploadFileInfo } from 'naive-ui'
import { UploadStatus } from '@/enums/upload-status'
import { fetchUpload } from '@/apis/upload'
import { AxiosProgressEvent } from 'axios'
import i18n from '@/locales'
interface FileInfoItem {
id: string
name: string
size: number
status: 'pending' | 'uploading' | 'finished' | 'removed' | 'error'
url: string
percentage: number
type?: string
}
const { t } = i18n.global
export function useDialogueFile() {
const uploadFileList = ref<FileInfoItem[]>([])
function handleLimitUpload(data: { file: UploadFileInfo }) {
const fileType = (data.file.file && data.file.file?.name.split('.')?.pop()?.toLowerCase()) || ''
if (data.file.file && data.file.file?.size === 0) {
window.$message.error(
t('personal_space_module.knowledge_module.upload_document_module.empty_document_content_message'),
)
const fileData = reactive({
id: data.file.id,
name: data.file.name,
status: UploadStatus.ERROR,
size: data.file.file?.size || 0,
type: fileType,
percentage: 0,
url: '',
})
uploadFileList.value = []
uploadFileList.value.push(fileData)
return false
}
if (data.file.file && data.file.file?.size > 10 * 1024 * 1024) {
window.$message.error(
t('personal_space_module.knowledge_module.upload_document_module.upload_size_error_message'),
)
const fileData = reactive({
id: data.file.id,
name: data.file.name,
status: UploadStatus.ERROR,
size: data.file.file?.size || 0,
url: '',
percentage: 0,
type: fileType,
})
uploadFileList.value = []
uploadFileList.value.push(fileData)
return false
}
return true
}
async function handleUpload(file: any) {
const formData = new FormData()
formData.append('file', file.file.file)
const fileData = reactive({
id: file.file.id,
name: file.file.name,
status: UploadStatus.UPLOADING,
size: file.file?.file?.size || 0,
type: file.file?.name.split('.')?.pop()?.toLowerCase(),
percentage: 0,
url: '',
})
if (uploadFileList.value.length <= 1) {
uploadFileList.value = []
await uploadFileList.value.push(fileData)
fetchUpload(formData, {
onUploadProgress: (progressEvent: AxiosProgressEvent) => {
if (progressEvent.total) {
fileData.percentage = Number(((progressEvent.loaded / progressEvent.total) * 100).toFixed(1))
} else {
fileData.percentage = 0
}
},
})
.then((res) => {
if (res.code === 0) {
fileData.status = UploadStatus.FINISHED
fileData.url = res.data as string
}
})
.catch(() => {
fileData.status = UploadStatus.ERROR
})
}
}
function handleRemoveFile(id: string) {
uploadFileList.value = uploadFileList.value.filter((fileItem) => fileItem.id !== id)
}
return { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile }
}
export enum UploadStatus {
PENDING = 'pending',
UPLOADING = 'uploading',
FINISHED = 'finished',
REMOVED = 'removed',
ERROR = 'error',
}
...@@ -89,6 +89,8 @@ common_module: ...@@ -89,6 +89,8 @@ common_module:
language: 'Language' language: 'Language'
change: 'Change' change: 'Change'
bind: 'Bind' bind: 'Bind'
sms: 'Short message'
verificationCode: 'Verification code'
dialogue_module: dialogue_module:
continue_question_message: 'You can keep asking questions' continue_question_message: 'You can keep asking questions'
...@@ -98,6 +100,9 @@ common_module: ...@@ -98,6 +100,9 @@ common_module:
clear_message_popover_message: 'Clear history session' clear_message_popover_message: 'Clear history session'
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 session will clear all the history of the session in the debug area. Are you sure to clear the session?' clear_message_dialog_content: 'Clearing the session will clear all the history of the session in the debug area. Are you sure to clear the session?'
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'
overwrite_file_tip: 'The newly uploaded file overwrites the original file, whether to continue uploading'
data_table_module: data_table_module:
action: 'Controls' action: 'Controls'
...@@ -214,7 +219,7 @@ personal_space_module: ...@@ -214,7 +219,7 @@ personal_space_module:
topP: 'Top P' topP: 'Top P'
topP_popover_message: 'When the model generates the output, it starts with the words with the highest probability until the total probability of these words accumulates to a value of Top p. This limits the model to choosing only these high-probability terms, thereby controlling the diversity of output content.' topP_popover_message: 'When the model generates the output, it starts with the words with the highest probability until the total probability of these words accumulates to a value of Top p. This limits the model to choosing only these high-probability terms, thereby controlling the diversity of output content.'
temperature: 'Generative randomness' temperature: 'Generative randomness'
temperature_popover_message: 'Used to control the diversity of model outputs. The recommended value is 0, and the larger the value, the greater the difference in the output content of the model推荐值为 0,数值越大,模型每次输出内容的差异性越大' temperature_popover_message: 'Used to control the diversity of model outputs. The recommended value is 0, and the larger the value, the greater the difference in the output content of the model'
communication_turn: 'Refer to session rounds' communication_turn: 'Refer to session rounds'
communication_turn_popover_message: 'The maximum number of session rounds passed into the large model context. The recommended value is 2, the higher the value, the stronger the context correlation in multiple rounds of conversations, but the more Tokens are consumed' communication_turn_popover_message: 'The maximum number of session rounds passed into the large model context. The recommended value is 2, the higher the value, the stronger the context correlation in multiple rounds of conversations, but the more Tokens are consumed'
agent_setting: 'Application setting' agent_setting: 'Application setting'
...@@ -244,6 +249,8 @@ personal_space_module: ...@@ -244,6 +249,8 @@ personal_space_module:
knowledge: 'Knowledge' knowledge: 'Knowledge'
knowledge_base: 'Knowledge base' knowledge_base: 'Knowledge base'
knowledge_base_desc: 'Reference text data, tabular knowledge data (including FAQ questions, multi-column index questions) and web data to achieve knowledge base questions and answers. The application can be associated with a maximum of 5 knowledge bases. Please fill in the detailed description of the knowledge base to improve the accuracy of questions and answers' knowledge_base_desc: 'Reference text data, tabular knowledge data (including FAQ questions, multi-column index questions) and web data to achieve knowledge base questions and answers. The application can be associated with a maximum of 5 knowledge bases. Please fill in the detailed description of the knowledge base to improve the accuracy of questions and answers'
upload_file: 'Upload file'
upload_file_desc: 'Enable the user to upload files for chat, support TXT, MD, PDF, DOC, DOCX 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'
...@@ -452,3 +459,6 @@ personal_settings_module: ...@@ -452,3 +459,6 @@ personal_settings_module:
verify_that_the_new_password_is_inconsistent_with_the_new_password: 'Verify that the new password is inconsistent with the new password' verify_that_the_new_password_is_inconsistent_with_the_new_password: 'Verify that the new password is inconsistent with the new password'
please_enter_the_account_nickname: 'Please enter the account nickname' please_enter_the_account_nickname: 'Please enter the account nickname'
please_enter_a_personal_profile: 'Please enter a personal profile' please_enter_a_personal_profile: 'Please enter a personal profile'
please_enter_the_correct_verification_code: 'Please enter the correct verification code'
binding_successful: 'Binding successful'
obtaining_the_verification_code: 'Obtaining the verification code'
...@@ -88,6 +88,8 @@ common_module: ...@@ -88,6 +88,8 @@ common_module:
language: '语言' language: '语言'
change: '更换' change: '更换'
bind: '绑定' bind: '绑定'
sms: '短信'
verificationCode: '验证码'
dialogue_module: dialogue_module:
continue_question_message: '你可以继续提问' continue_question_message: '你可以继续提问'
...@@ -97,6 +99,9 @@ common_module: ...@@ -97,6 +99,9 @@ common_module:
clear_message_popover_message: '清空历史会话' clear_message_popover_message: '清空历史会话'
clear_message_dialog_title: '确认要清空对话吗?' clear_message_dialog_title: '确认要清空对话吗?'
clear_message_dialog_content: '清空对话将清空调试区域所有历史对话内容,确定清空对话吗?' clear_message_dialog_content: '清空对话将清空调试区域所有历史对话内容,确定清空对话吗?'
cancel_associate_file_tip: '不再围绕这个文件回答'
upload_file_limit: '仅支持上传单个文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB'
overwrite_file_tip: '新上传的文件会覆盖原有文件,是否继续上传'
data_table_module: data_table_module:
action: '操作' action: '操作'
...@@ -242,6 +247,8 @@ personal_space_module: ...@@ -242,6 +247,8 @@ personal_space_module:
knowledge: '知识' knowledge: '知识'
knowledge_base: '知识库' knowledge_base: '知识库'
knowledge_base_desc: '引用文本数据、表格型知识数据(含FAQ问答,多列索引问答)以及网页数据,实现知识库问答,应用最多可关联5个知识库,请详细填写知识库描述信息以提高问答准确率' knowledge_base_desc: '引用文本数据、表格型知识数据(含FAQ问答,多列索引问答)以及网页数据,实现知识库问答,应用最多可关联5个知识库,请详细填写知识库描述信息以提高问答准确率'
upload_file: '上传文件'
upload_file_desc: '开启后支持用户上传文件进行对话聊天, 支持TXT、MD、PDF、DOC、DOCX格式的文件'
dialogue: '对话' dialogue: '对话'
preamble: '开场白' preamble: '开场白'
preamble_input_placeholder: '请输入开场白' preamble_input_placeholder: '请输入开场白'
...@@ -450,3 +457,6 @@ personal_settings_module: ...@@ -450,3 +457,6 @@ personal_settings_module:
verify_that_the_new_password_is_inconsistent_with_the_new_password: '确认新密码与新密码不一致' verify_that_the_new_password_is_inconsistent_with_the_new_password: '确认新密码与新密码不一致'
please_enter_the_account_nickname: '请输入账号昵称' please_enter_the_account_nickname: '请输入账号昵称'
please_enter_a_personal_profile: '请输入个人简介' please_enter_a_personal_profile: '请输入个人简介'
please_enter_the_correct_verification_code: '请输入正确验证码'
binding_successful: '绑定成功'
obtaining_the_verification_code: '获取验证码方式'
...@@ -88,6 +88,8 @@ common_module: ...@@ -88,6 +88,8 @@ common_module:
language: '語言' language: '語言'
change: '更換' change: '更換'
bind: '綁定' bind: '綁定'
sms: '短信'
verificationCode: '驗證碼'
dialogue_module: dialogue_module:
continue_question_message: '你可以繼續提問' continue_question_message: '你可以繼續提問'
...@@ -97,6 +99,9 @@ common_module: ...@@ -97,6 +99,9 @@ common_module:
clear_message_popover_message: '清空歷史會話' clear_message_popover_message: '清空歷史會話'
clear_message_dialog_title: '確認要清空對話嗎?' clear_message_dialog_title: '確認要清空對話嗎?'
clear_message_dialog_content: '清空對話將清空調試區域所有歷史對話內容,確定清空對話嗎?' clear_message_dialog_content: '清空對話將清空調試區域所有歷史對話內容,確定清空對話嗎?'
cancel_associate_file_tip: '不再圍繞這個文件回答'
upload_file_limit: '僅支持上傳單個文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB'
overwrite_file_tip: '新上傳的文件會覆蓋原有文件,是否繼續上傳'
data_table_module: data_table_module:
action: '操作' action: '操作'
...@@ -242,6 +247,8 @@ personal_space_module: ...@@ -242,6 +247,8 @@ personal_space_module:
knowledge: '知識' knowledge: '知識'
knowledge_base: '知識庫' knowledge_base: '知識庫'
knowledge_base_desc: '引用文本數據、表格型知識數據(含FAQ問答,多列索引問答)以及網頁數據,實現知識庫問答,應用最多可關聯5個知識庫,請詳細填寫知識庫描述信息以提高問答準確率' knowledge_base_desc: '引用文本數據、表格型知識數據(含FAQ問答,多列索引問答)以及網頁數據,實現知識庫問答,應用最多可關聯5個知識庫,請詳細填寫知識庫描述信息以提高問答準確率'
upload_file: '上傳文件'
upload_file_desc: '開啓後支持用户上傳文件進行對話聊天, 支持TXT、MD、PDF、DOC、DOCX格式的文件'
dialogue: '對話' dialogue: '對話'
preamble: '開場白' preamble: '開場白'
preamble_input_placeholder: '請輸入開場白' preamble_input_placeholder: '請輸入開場白'
...@@ -450,3 +457,6 @@ personal_settings_module: ...@@ -450,3 +457,6 @@ personal_settings_module:
verify_that_the_new_password_is_inconsistent_with_the_new_password: '確認新密碼與新密碼不一致' verify_that_the_new_password_is_inconsistent_with_the_new_password: '確認新密碼與新密碼不一致'
please_enter_the_account_nickname: '請輸入賬號昵稱' please_enter_the_account_nickname: '請輸入賬號昵稱'
please_enter_a_personal_profile: '請輸入個人簡介' please_enter_a_personal_profile: '請輸入個人簡介'
please_enter_the_correct_verification_code: '請輸入正確驗證碼'
binding_successful: '綁定成功'
obtaining_the_verification_code: '獲取驗證碼方式'
...@@ -24,6 +24,7 @@ export function defaultPersonalAppConfigState(): PersonalAppConfigState { ...@@ -24,6 +24,7 @@ export function defaultPersonalAppConfigState(): PersonalAppConfigState {
}, },
knowledgeConfig: { knowledgeConfig: {
knowledgeIds: [], knowledgeIds: [],
isDocumentParsing: 'N',
}, },
commModelConfig: { commModelConfig: {
largeModel: '文心4.0 (8K)', largeModel: '文心4.0 (8K)',
......
...@@ -47,7 +47,7 @@ export const useUserStore = defineStore('user-store', { ...@@ -47,7 +47,7 @@ export const useUserStore = defineStore('user-store', {
ss.set(UserStoreStorageKeyEnum.userInfo, userInfo) ss.set(UserStoreStorageKeyEnum.userInfo, userInfo)
}, },
fetchUpdateUserInfo() { fetchUpdateUserInfo() {
fetchUserDetailInfo<UserInfo>().then((res) => { return fetchUserDetailInfo<UserInfo>().then((res) => {
this.userInfo = res.data this.userInfo = res.data
}) })
}, },
......
...@@ -28,6 +28,7 @@ export interface PersonalAppConfigState { ...@@ -28,6 +28,7 @@ export interface PersonalAppConfigState {
} }
knowledgeConfig: { knowledgeConfig: {
knowledgeIds: number[] //知识库ID knowledgeIds: number[] //知识库ID
isDocumentParsing: 'Y' | 'N' //是否开启文档解析 Y-开启 N-关闭
} }
commModelConfig: { commModelConfig: {
largeModel: string //大模型 largeModel: string //大模型
......
...@@ -6,12 +6,15 @@ import { throttle } from 'lodash-es' ...@@ -6,12 +6,15 @@ import { throttle } from 'lodash-es'
import CMessage from './c-message' import CMessage from './c-message'
import { MessageItemInterface, MultiModelDialogueItem, QuestionMessageItem } from '../types' import { MessageItemInterface, MultiModelDialogueItem, QuestionMessageItem } from '../types'
import { fetchEventStreamSource } from '../utils/fetch-event-stream-source' import { fetchEventStreamSource } from '../utils/fetch-event-stream-source'
import { UploadStatus } from '@/enums/upload-status'
import { useDialogueFile } from '@/composables/useDialogueFile'
const { t } = useI18n() const { t } = useI18n()
interface Props { interface Props {
agentId: string agentId: string
isAnswerResponseWait: boolean isAnswerResponseWait: boolean
isEnableDocumentParse: boolean
} }
const props = defineProps<Props>() const props = defineProps<Props>()
...@@ -25,6 +28,8 @@ const emit = defineEmits<{ ...@@ -25,6 +28,8 @@ const emit = defineEmits<{
clearAllMessage: [] clearAllMessage: []
}>() }>()
const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile()
const multiModelDialogueList = defineModel<MultiModelDialogueItem[]>('multiModelDialogueList', { required: true }) const multiModelDialogueList = defineModel<MultiModelDialogueItem[]>('multiModelDialogueList', { required: true })
const questionContent = ref('') const questionContent = ref('')
...@@ -41,6 +46,18 @@ const isAllowClearAllMessage = computed(() => { ...@@ -41,6 +46,18 @@ const isAllowClearAllMessage = computed(() => {
return multiModelDialogueList.value.some((modelItem) => modelItem.messageList.size > 0) return multiModelDialogueList.value.some((modelItem) => modelItem.messageList.size > 0)
}) })
const isInputMessageDisabled = computed(() => {
return uploadFileList.value.some((fileItem) => fileItem.status !== UploadStatus.FINISHED)
})
const isUploadFileDisabled = computed(() => {
return uploadFileList.value.length === 1
})
const uploadFileIcon = (type: string) => {
return `https://gsst-poe-sit.gz.bcebos.com/icon/${type}.svg`
}
const messageListScrollToBottomThrottle = throttle(() => { const messageListScrollToBottomThrottle = throttle(() => {
emit('messageListScrollToBottom') emit('messageListScrollToBottom')
}, 1000) }, 1000)
...@@ -62,7 +79,7 @@ function messageItemFactory() { ...@@ -62,7 +79,7 @@ function messageItemFactory() {
} }
function handleQuestionSubmit() { function handleQuestionSubmit() {
if (isQuestionSubmitDisabled.value || props.isAnswerResponseWait) return if (isQuestionSubmitDisabled.value || props.isAnswerResponseWait || isInputMessageDisabled.value) return
if (!isAllSelectedModelName.value) { if (!isAllSelectedModelName.value) {
window.$message.warning(t('multi_model_dialogue_module.please_select_model_first')) window.$message.warning(t('multi_model_dialogue_module.please_select_model_first'))
...@@ -116,6 +133,7 @@ function handleQuestionSubmit() { ...@@ -116,6 +133,7 @@ function handleQuestionSubmit() {
path: '/api/rest/agentApplicationInfoRest/preview.json', path: '/api/rest/agentApplicationInfoRest/preview.json',
payload: { payload: {
agentId: props.agentId, agentId: props.agentId,
fileUrls: uploadFileList.value.map((item) => item.url),
messages, messages,
topP, topP,
temperature, temperature,
...@@ -167,44 +185,128 @@ function errorMessageResponse(questionMessageId: string, answerMessageId: string ...@@ -167,44 +185,128 @@ function errorMessageResponse(questionMessageId: string, answerMessageId: string
emit('deleteMessageItem', questionMessageId, modelIndex) emit('deleteMessageItem', questionMessageId, modelIndex)
emit('deleteMessageItem', answerMessageId, modelIndex) emit('deleteMessageItem', answerMessageId, modelIndex)
} }
function handleSelectFile(cb: () => void) {
if (isUploadFileDisabled.value) {
window.$message.ctWarning('', t('common_module.dialogue_module.overwrite_file_tip')).then(() => {
cb()
})
return
}
cb()
}
</script> </script>
<template> <template>
<footer class="flex flex-col items-center"> <footer class="flex flex-col items-center">
<div class="mb-4 flex items-center gap-[18px]"> <div class="mb-4 flex items-end gap-[18px]">
<NPopover trigger="hover"> <div class="flex gap-3">
<template #trigger> <n-popover trigger="hover">
<div <template #trigger>
class="border-inactive-border-color flex h-[54px] w-[54px] shrink-0 items-center justify-center rounded-full border bg-white" <div
class="border-inactive-border-color flex h-[54px] w-[54px] shrink-0 items-center justify-center rounded-full border bg-white"
:class="
isAllowClearAllMessage
? 'text-font-color hover:text-theme-color cursor-pointer'
: 'text-gray-font-color cursor-not-allowed'
"
@click="handleClearAllMessage"
>
<i class="iconfont icon-clear text-xl leading-none" />
</div>
</template>
<span class="text-xs"> {{ t('common_module.dialogue_module.clear_message_popover_message') }}</span>
</n-popover>
<n-upload
:show-file-list="false"
accept=".doc, .pdf, .docx, .txt, .md"
abstract
@before-upload="handleLimitUpload"
@change="handleUpload"
>
<n-upload-trigger #="{ handleClick }" abstract>
<n-popover style="width: 210px" trigger="hover">
<template #trigger>
<div
v-show="isEnableDocumentParse"
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="handleSelectFile(handleClick)"
>
<i class="iconfont icon-upload text-xl leading-none" />
</div>
</template>
<span class="text-xs"> {{ t('common_module.dialogue_module.upload_file_limit') }} </span>
</n-popover>
</n-upload-trigger>
</n-upload>
</div>
<div class="flex flex-col gap-1">
<ul v-show="uploadFileList.length > 0" class="mb-1.5 grid gap-1.5">
<li
v-for="uploadFileItem in uploadFileList"
:key="uploadFileItem.id"
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'"
>
<div class="flex w-full items-center justify-between px-3.5">
<div class="flex w-full items-center overflow-hidden">
<img :src="uploadFileIcon(uploadFileItem.type!)" class="h-7 w-7" />
<div class="mx-2.5 flex flex-1 flex-col overflow-hidden">
<n-ellipsis>
{{ uploadFileItem.name }}
</n-ellipsis>
</div>
</div>
<n-progress
v-show="!['finished', 'error'].includes(uploadFileItem.status)"
class="left-13.5 w-[calc(100%-78px)]! absolute bottom-0"
type="line"
rail-color="#F3F3F3"
:height="4"
:percentage="uploadFileItem.percentage"
:show-indicator="false"
/>
<div v-show="['finished', 'error'].includes(uploadFileItem.status)" class="hidden group-hover:block">
<n-popover trigger="hover" placement="top-end" :show-arrow="false">
<template #trigger>
<i
class="iconfont icon-close cursor-pointer hover:opacity-80"
:class="uploadFileItem.status === 'error' ? 'text-error-font-color' : 'text-font-color'"
@click="handleRemoveFile(uploadFileItem.id)"
/>
</template>
<span>{{ t('common_module.dialogue_module.cancel_associate_file_tip') }}</span>
</n-popover>
</div>
</div>
</li>
</ul>
<div class="relative">
<n-input
v-model:value="questionContent"
:placeholder="t('common_module.dialogue_module.question_input_placeholder')"
:disabled="isInputMessageDisabled"
class="rounded-theme! w-[725px]! border-[#9EA3FF]! border py-[10px] pl-3 pr-[44px]"
@keydown.enter="handleQuestionSubmit"
/>
<i
class="iconfont icon-send-icon absolute right-6 top-[18px] text-xl leading-none"
:class=" :class="
isAllowClearAllMessage isQuestionSubmitDisabled || isAnswerResponseWait || isInputMessageDisabled
? 'text-font-color hover:text-theme-color cursor-pointer' ? 'text-hover-theme-color cursor-not-allowed'
: 'text-gray-font-color cursor-not-allowed' : 'text-theme-color cursor-pointer hover:opacity-80'
" "
@click="handleClearAllMessage" @click="handleQuestionSubmit"
> />
<i class="iconfont icon-clear text-xl leading-none" /> </div>
</div>
</template>
<span class="text-xs"> {{ t('common_module.dialogue_module.clear_message_popover_message') }}</span>
</NPopover>
<div class="relative">
<NInput
v-model:value="questionContent"
:placeholder="t('common_module.dialogue_module.question_input_placeholder')"
class="rounded-[26px]! w-[725px]! border-[#9EA3FF]! border py-[10px] pl-3 pr-[44px]"
@keydown.enter="handleQuestionSubmit"
/>
<i
class="iconfont icon-send-icon absolute right-6 top-[18px] text-xl leading-none"
:class="
isQuestionSubmitDisabled || isAnswerResponseWait
? 'text-hover-theme-color cursor-not-allowed'
: 'text-theme-color cursor-pointer hover:opacity-80'
"
@click="handleQuestionSubmit"
/>
</div> </div>
</div> </div>
<span class="text-gray-font-color select-none"> <span class="text-gray-font-color select-none">
......
...@@ -45,6 +45,10 @@ const isHasMessageList = computed(() => { ...@@ -45,6 +45,10 @@ const isHasMessageList = computed(() => {
return multiModelDialogueList.value.some((modelItem) => modelItem.messageList.size > 0) return multiModelDialogueList.value.some((modelItem) => modelItem.messageList.size > 0)
}) })
const isEnableDocumentParse = computed(() => {
return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y'
})
onMounted(async () => { onMounted(async () => {
if (!currentRoute.params.agentId) { if (!currentRoute.params.agentId) {
window.$message.warning(t('multi_model_dialogue_module.not_find_agent')) window.$message.warning(t('multi_model_dialogue_module.not_find_agent'))
...@@ -320,6 +324,7 @@ function handleBlockMessageResponse() { ...@@ -320,6 +324,7 @@ function handleBlockMessageResponse() {
v-model:multi-model-dialogue-list="multiModelDialogueList" v-model:multi-model-dialogue-list="multiModelDialogueList"
:agent-id="agentId" :agent-id="agentId"
:is-answer-response-wait="isAnswerResponseWait" :is-answer-response-wait="isAnswerResponseWait"
:is-enable-document-parse="isEnableDocumentParse"
@add-question-message-item="handleAddQuestionMessageItem" @add-question-message-item="handleAddQuestionMessageItem"
@add-answer-message-item="handleAddAnswerMessageItem" @add-answer-message-item="handleAddAnswerMessageItem"
@update-message-item="handleUpdateSpecifyMessageItem" @update-message-item="handleUpdateSpecifyMessageItem"
......
<script setup lang="ts">
import type { CountdownInst, FormInst, FormItemRule } from 'naive-ui'
import { onMounted, ref, shallowReadonly, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/store/modules/user'
import isEmail from 'validator/es/lib/isEmail'
import { fetchEmailCode, fetchUserInfoUpdate, fetchVerifyCode } from '@/apis/user'
import { ss } from '@/utils/storage'
const { t } = useI18n()
const userStore = useUserStore()
const emailInfoFormRef = useTemplateRef<FormInst>('emailInfoFormRef')
const countdownRef = useTemplateRef<CountdownInst>('countdownRef')
const isShowMailboxBindingModal = defineModel<boolean>('isShowMailboxBindingModal', { default: false })
const mailboxBindingSubmitBtnLoading = ref(false)
const emailInfoForm = ref({
email: '',
verifyCode: '',
})
const isShowCountdown = ref(false)
const countdownDuration = ref<number>(60000)
const countdownActive = ref(true)
const emailInfoFormRules = shallowReadonly({
email: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('login_module.please_enter_your_email_address'))
} else if (!isEmail(value)) {
return new Error(t('login_module.please_enter_the_correct_email_address'))
}
return
},
},
verifyCode: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('login_module.please_enter_the_verification_code'))
} else if (value.length < 6) {
return new Error(t('personal_settings_module.please_enter_the_correct_verification_code'))
}
return
},
},
})
onMounted(() => {
let timeStringDraft = ss.get('PASSWORD_CHANGE_CODE')
if (timeStringDraft) {
const time = Math.floor(Date.now() - parseInt(timeStringDraft))
if (time < 60000) {
countdownDuration.value = 60000 - time
countdownRef.value?.reset()
isShowCountdown.value = true
}
}
})
function onModalAfterLeave() {
emailInfoForm.value.email = ''
}
function handleMailboxBindingSubmit() {
emailInfoFormRef.value?.validate((errors) => {
if (errors) return ''
mailboxBindingSubmitBtnLoading.value = true
fetchVerifyCode<string>(emailInfoForm.value.email, emailInfoForm.value.verifyCode)
.then((res) => {
if (res.code !== 0) return ''
fetchUserInfoUpdate({
email: emailInfoForm.value.email,
})
.then(() => {
userStore.fetchUpdateUserInfo()
window.$message.success(t('personal_settings_module.binding_successful'))
})
.finally(() => {
isShowMailboxBindingModal.value = false
mailboxBindingSubmitBtnLoading.value = false
isShowCountdown.value = false
})
})
.catch(() => {
mailboxBindingSubmitBtnLoading.value = false
})
})
}
function countdownRender({ seconds, minutes }: { seconds: number; minutes: number }) {
if (minutes && minutes === 1) {
return '60 s'
}
return `${seconds} s`
}
function onCountdownFinish() {
isShowCountdown.value = false
}
function handleSMSCodeGain() {
emailInfoFormRef.value?.validate(
(errors) => {
if (errors) return ''
countdownDuration.value = 60000
ss.set('MAILBOX_BINDING_CODE', Date.now())
countdownRef.value?.reset()
isShowCountdown.value = true
fetchEmailCode(encodeURIComponent(emailInfoForm.value.email)).then((res) => {
if (res.code !== 0) return ''
window.$message.success(t('login_module.successful'))
})
},
(rule) => {
return rule.key === 'email'
},
)
}
</script>
<template>
<n-modal v-model:show="isShowMailboxBindingModal" :mask-closable="false" :on-after-leave="onModalAfterLeave">
<n-card
class="!w-[600px]"
:title="t('personal_settings_module.email_binding')"
:bordered="false"
size="medium"
closable
@close="() => (isShowMailboxBindingModal = false)"
>
<n-form
ref="emailInfoFormRef"
label-placement="left"
label-width="auto"
:model="emailInfoForm"
:rules="emailInfoFormRules"
>
<n-form-item :label="t('common_module.email')" path="email">
<n-input v-model:value="emailInfoForm.email" :placeholder="t('login_module.please_enter_your_email_address')">
<template #suffix>
<span class="mx-[10px] inline-block h-[50%] w-[2px] bg-[#e0e0e6]"></span>
<span
v-show="!isShowCountdown"
class="text-theme-color cursor-pointer text-[12px]"
@click="handleSMSCodeGain"
>
{{ t('login_module.get_verification_code') }}
</span>
<div v-show="isShowCountdown" class="inline-block w-[50px] text-center">
<n-countdown
ref="countdownRef"
:duration="countdownDuration"
:active="countdownActive"
:render="countdownRender"
:on-finish="onCountdownFinish"
/>
</div>
</template>
</n-input>
</n-form-item>
<n-form-item :label="t('common_module.verificationCode')" path="verifyCode">
<n-input
v-model:value="emailInfoForm.verifyCode"
:placeholder="t('login_module.please_enter_the_verification_code')"
:maxlength="6"
/>
</n-form-item>
</n-form>
<template #footer>
<div class="text-end">
<n-space justify="end">
<n-button @click="() => (isShowMailboxBindingModal = false)">
{{ t('common_module.cancel_btn_text') }}
</n-button>
<n-button type="primary" :loading="mailboxBindingSubmitBtnLoading" @click="handleMailboxBindingSubmit">
{{ t('common_module.confirm_btn_text') }}
</n-button>
</n-space>
</div>
</template>
</n-card>
</n-modal>
</template>
<script setup lang="ts">
import { fetchEmailCode, fetchSMSCode, fetchUserPasswordUpdate, fetchVerifyCode } from '@/apis/user'
import { useUserStore } from '@/store/modules/user'
import { ss } from '@/utils/storage'
import type { CountdownInst, FormInst, FormItemRule } from 'naive-ui'
import SparkMD5 from 'spark-md5'
import { onMounted, ref, shallowReadonly, useTemplateRef, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const passwordInfoFormRef = useTemplateRef<FormInst>('passwordInfoFormRef')
const countdownRef = useTemplateRef<CountdownInst>('countdownRef')
const userStore = useUserStore()
const isShowPasswordChangeModal = defineModel<boolean>('isShowPasswordChangeModal', { default: false })
const passwordChangeSubmitBtnLoading = ref(false)
const passwordInfoForm = ref<{
verificationMethod: 'email' | 'sms'
verifyCode: string
password: string
confirmPassword: string
}>({
verificationMethod: 'email',
verifyCode: '',
password: '',
confirmPassword: '',
})
const isShowCountdown = ref(false)
const countdownDuration = ref<number>(60000)
const countdownActive = ref(true)
const passwordFormRules = shallowReadonly({
verifyCode: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('login_module.please_enter_the_verification_code'))
} else if (value.length < 6) {
return new Error(t('personal_settings_module.please_enter_the_correct_verification_code'))
}
return
},
},
password: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('personal_settings_module.please_enter_your_new_password'))
} else if (value.length <= 6) {
return new Error(t('personal_settings_module.the_password_contains_a_maximum_of_6_characters'))
}
return
},
},
confirmPassword: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('personal_settings_module.please_enter_confirm_new_password'))
} else if (value !== passwordInfoForm.value.password) {
return new Error(
t('personal_settings_module.verify_that_the_new_password_is_inconsistent_with_the_new_password'),
)
}
return
},
},
})
const verificationMethodDisable = ref({
email: false,
sms: false,
})
watchEffect(() => {
if (userStore.userInfo) {
!userStore.userInfo.email
? (verificationMethodDisable.value.email = true)
: (verificationMethodDisable.value.email = false)
!userStore.userInfo.mobilePhone
? (verificationMethodDisable.value.sms = true)
: (verificationMethodDisable.value.sms = false)
if (userStore.userInfo.email) {
passwordInfoForm.value.verificationMethod = 'email'
} else if (userStore.userInfo.mobilePhone) {
passwordInfoForm.value.verificationMethod = 'sms'
}
}
})
onMounted(() => {
let timeStringDraft = ss.get('PASSWORD_CHANGE_CODE')
if (timeStringDraft) {
const time = Math.floor(Date.now() - parseInt(timeStringDraft))
if (time < 60000) {
countdownDuration.value = 60000 - time
countdownRef.value?.reset()
isShowCountdown.value = true
}
}
})
function countdownRender({ seconds, minutes }: { seconds: number; minutes: number }) {
if (minutes && minutes === 1) {
return '60 s'
}
return `${seconds} s`
}
function onModalAfterLeave() {
passwordInfoForm.value = {
password: '',
confirmPassword: '',
verificationMethod: 'email',
verifyCode: '',
}
}
function handlePasswordChangeSubmit() {
passwordInfoFormRef.value?.validate((errors) => {
if (errors) return ''
passwordChangeSubmitBtnLoading.value = true
fetchVerifyCode<string>(
passwordInfoForm.value.verificationMethod === 'email' ? userStore.userInfo.email : userStore.userInfo.mobilePhone,
passwordInfoForm.value.verifyCode,
)
.then((res) => {
if (res.code !== 0) return ''
fetchUserPasswordUpdate(res.data, SparkMD5.hash(passwordInfoForm.value.confirmPassword)).then(() => {
window.$message.success(t('common_module.successful_update'))
isShowPasswordChangeModal.value = false
passwordChangeSubmitBtnLoading.value = false
isShowCountdown.value = false
})
})
.catch(() => {
passwordChangeSubmitBtnLoading.value = false
})
})
}
function onCountdownFinish() {
isShowCountdown.value = false
}
function handleSMSCodeGain() {
countdownDuration.value = 60000
ss.set('PASSWORD_CHANGE_CODE', Date.now())
countdownRef.value?.reset()
isShowCountdown.value = true
if (passwordInfoForm.value.verificationMethod === 'sms') {
fetchSMSCode(userStore.userInfo.mobilePhone).then((res) => {
if (res.code !== 0) return ''
window.$message.success(t('login_module.successful'))
})
} else if (passwordInfoForm.value.verificationMethod === 'email') {
fetchEmailCode(encodeURIComponent(userStore.userInfo.email)).then((res) => {
if (res.code !== 0) return ''
window.$message.success(t('login_module.successful'))
})
}
}
</script>
<template>
<n-modal v-model:show="isShowPasswordChangeModal" :mask-closable="false" :on-after-leave="onModalAfterLeave">
<n-card
class="!w-[600px]"
:title="t('personal_settings_module.password_change')"
:bordered="false"
size="medium"
closable
@close="() => (isShowPasswordChangeModal = false)"
>
<n-form
ref="passwordInfoFormRef"
label-placement="left"
label-width="auto"
:model="passwordInfoForm"
:rules="passwordFormRules"
>
<n-form-item :label="t('personal_settings_module.obtaining_the_verification_code')">
<div>
<n-radio-group v-model:value="passwordInfoForm.verificationMethod" name="verificationMethod" size="small">
<n-radio-button :disabled="verificationMethodDisable.email" value="email">
{{ t('common_module.email') }}
</n-radio-button>
<n-radio-button :disabled="verificationMethodDisable.sms" value="sms">
{{ t('common_module.sms') }}
</n-radio-button>
</n-radio-group>
</div>
</n-form-item>
<n-form-item :label="t('common_module.verificationCode')" path="verifyCode">
<n-input
v-model:value="passwordInfoForm.verifyCode"
:placeholder="t('login_module.please_enter_the_verification_code')"
:maxlength="6"
>
<template #suffix>
<span class="mx-[10px] inline-block h-[50%] w-[2px] bg-[#e0e0e6]"></span>
<span
v-show="!isShowCountdown"
class="text-theme-color cursor-pointer text-[12px]"
@click="handleSMSCodeGain"
>
{{ t('login_module.get_verification_code') }}
</span>
<div v-show="isShowCountdown" class="inline-block w-[50px] text-center">
<n-countdown
ref="countdownRef"
:duration="countdownDuration"
:active="countdownActive"
:render="countdownRender"
:on-finish="onCountdownFinish"
/>
</div>
</template>
</n-input>
</n-form-item>
<n-form-item :label="t('personal_settings_module.new_password')" path="password">
<n-input
v-model:value="passwordInfoForm.password"
type="password"
show-password-on="click"
class="font-sans"
:placeholder="t('personal_settings_module.please_enter_your_new_password')"
/>
</n-form-item>
<n-form-item :label="t('personal_settings_module.confirm_new_password')" path="confirmPassword">
<n-input
v-model:value="passwordInfoForm.confirmPassword"
type="password"
show-password-on="click"
class="font-sans"
:placeholder="t('personal_settings_module.please_enter_confirm_new_password')"
/>
</n-form-item>
</n-form>
<template #footer>
<div class="text-end">
<n-space justify="end">
<n-button @click="() => (isShowPasswordChangeModal = false)">
{{ t('common_module.cancel_btn_text') }}
</n-button>
<n-button type="primary" :loading="passwordChangeSubmitBtnLoading" @click="handlePasswordChangeSubmit">
{{ t('common_module.confirm_btn_text') }}
</n-button>
</n-space>
</div>
</template>
</n-card>
</n-modal>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { useUserStore } from '@/store/modules/user' import { fetchUserInfoUpdate } from '@/apis/user'
import { computed, reactive, ref, shallowReadonly, useTemplateRef, nextTick } from 'vue' import ImageCropper from '@/components/image-cropper/image-cropper.vue'
import type { FormItemRule, FormInst, InputInst } from 'naive-ui'
import isEmail from 'validator/es/lib/isEmail'
import { useSystemLanguageStore } from '@/store/modules/system-language' import { useSystemLanguageStore } from '@/store/modules/system-language'
import { useRouter } from 'vue-router' import { useUserStore } from '@/store/modules/user'
import type { InputInst } from 'naive-ui'
import { computed, nextTick, reactive, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import PasswordChangeModal from './components/password-change-modal.vue'
import MailboxBindingModal from './components/mailbox-binding-modal.vue'
import { fetchUpload } from '@/apis/upload'
const userStore = useUserStore() const userStore = useUserStore()
const systemLanguageStore = useSystemLanguageStore() const systemLanguageStore = useSystemLanguageStore()
...@@ -14,11 +18,10 @@ const { t } = useI18n() ...@@ -14,11 +18,10 @@ const { t } = useI18n()
const inputRefs = useTemplateRef<InputInst | InputInst[]>('inputRefs') const inputRefs = useTemplateRef<InputInst | InputInst[]>('inputRefs')
const avatarInputRef = useTemplateRef('avatarInputRef') const avatarInputRef = useTemplateRef('avatarInputRef')
const emailInfoFormRef = useTemplateRef<FormInst>('emailInfoFormRef') const imageCropperRef = useTemplateRef<InstanceType<typeof ImageCropper> | null>('imageCropperRef')
const passwordInfoFormRef = useTemplateRef<FormInst>('passwordInfoFormRef')
const userInfoForm = reactive({ const userInfoForm = ref({
nickName: userStore.userInfo.nickName, nickName: '',
remark: userStore.userInfo.remark, remark: userStore.userInfo.remark,
}) })
...@@ -27,70 +30,12 @@ const userInfoFormItemEdit = reactive({ ...@@ -27,70 +30,12 @@ const userInfoFormItemEdit = reactive({
remark: false, remark: false,
}) })
/* 邮箱绑定 */
const isShowMailboxBindingModal = ref(false)
const mailboxBindingSubmitBtnLoading = ref(false)
const emailInfoForm = ref({
email: '',
})
const emailInfoFormRules = shallowReadonly({
email: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('login_module.please_enter_your_email_address'))
} else if (!isEmail(value)) {
return new Error(t('login_module.please_enter_the_correct_email_address'))
}
return
},
},
})
/* 密码修改 */
const isShowPasswordChangeModal = ref(false) const isShowPasswordChangeModal = ref(false)
const passwordChangeSubmitBtnLoading = ref(false) const isShowMailboxBindingModal = ref(false)
const passwordInfoForm = ref({
password: '',
confirmPassword: '',
})
const passwordFormRules = shallowReadonly({
password: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('personal_settings_module.please_enter_your_new_password'))
} else if (value.length <= 6) {
return new Error(t('personal_settings_module.the_password_contains_a_maximum_of_6_characters'))
}
return
},
},
confirmPassword: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('personal_settings_module.please_enter_confirm_new_password'))
} else if (value !== passwordInfoForm.value.password) {
return new Error(
t('personal_settings_module.verify_that_the_new_password_is_inconsistent_with_the_new_password'),
)
}
return
},
},
})
const listItemLabelWidth = ref(systemLanguageStore.currentLanguageInfo.key === 'en' ? '168px' : '100px') const listItemLabelWidth = ref(systemLanguageStore.currentLanguageInfo.key === 'en' ? '168px' : '100px')
// const confirmBtnLoading = ref(true) // const confirmBtnLoading = ref(true)
const saveBtnLoading = ref(false)
const currentLanguage = computed({ const currentLanguage = computed({
get() { get() {
...@@ -109,6 +54,18 @@ const languageOptions = computed(() => { ...@@ -109,6 +54,18 @@ const languageOptions = computed(() => {
return systemLanguageStore.languageOptions.map((item) => ({ value: item.key, label: item.label })) return systemLanguageStore.languageOptions.map((item) => ({ value: item.key, label: item.label }))
}) })
function userInfoUpdate(userInfo: {
nickName?: string
avatarUrl?: string
mobilePhone?: string
email?: string
remark?: string
}) {
return fetchUserInfoUpdate(userInfo).then(() => {
userStore.fetchUpdateUserInfo()
})
}
function handleAvatarUpdate() { function handleAvatarUpdate() {
avatarInputRef.value && avatarInputRef.value.click() avatarInputRef.value && avatarInputRef.value.click()
} }
...@@ -117,50 +74,64 @@ function handleAvatarUpload(e: Event) { ...@@ -117,50 +74,64 @@ function handleAvatarUpload(e: Event) {
const target = e.target as HTMLInputElement const target = e.target as HTMLInputElement
const file = target.files && target.files[0] const file = target.files && target.files[0]
if (file) { if (imageCropperRef.value && file) {
const reader = new FileReader() imageCropperRef.value.cropImage(URL.createObjectURL(file)).then((file) => {
reader.onload = (e) => { const formData = new FormData()
const result = e.target?.result formData.append('file', file)
console.log('🟰🟰🟰🟰🟰🟰test🟰🟰🟰🟰🟰🟰') const loadingCtl = window.$message.loading(t('common_module.uploading'), { duration: 0 })
console.log(result)
} fetchUpload<string>(formData)
.then((res) => {
userInfoUpdate({
avatarUrl: res.data,
})
.then(() => {
window.$message.success(t('common_module.save_success_message'))
userStore.fetchUpdateUserInfo()
})
.catch(() => {
window.$message.loading(t('common_module.save_fail_message'))
})
.finally(() => {
loadingCtl.destroy()
})
})
.catch(() => {
window.$message.loading(t('common_module.save_fail_message'))
})
})
} }
} }
function handleUserInfoFormItemEditUpdate(key: keyof typeof userInfoFormItemEdit, status: boolean) { function handleUserInfoFormItemEditUpdate(key: keyof typeof userInfoFormItemEdit, isSave = false) {
userInfoFormItemEdit[key] = status !isSave && (userInfoForm.value[key] = userInfo.value[key])
nextTick(() => { userInfoFormItemEdit[key] = !userInfoFormItemEdit[key]
if (inputRefs.value && !Array.isArray(inputRefs.value)) {
inputRefs.value.focus()
}
})
}
function onModalAfterLeave() { if (userInfoFormItemEdit[key]) {
emailInfoForm.value.email = '' nextTick(() => {
} if (inputRefs.value && !Array.isArray(inputRefs.value)) {
inputRefs.value.focus()
function handleMailboxBindingSubmit() { }
emailInfoFormRef.value?.validate((errors) => { })
if (errors) return '' }
console.log('🟰🟰🟰🟰🟰🟰提交🟰🟰🟰🟰🟰🟰')
})
}
function handlePasswordChangeSubmit() {
passwordInfoFormRef.value?.validate((errors) => {
if (errors) return ''
console.log('🟰🟰🟰🟰🟰🟰提交🟰🟰🟰🟰🟰🟰') if (isSave) {
}) userInfoUpdate({
[key]: userInfoForm.value[key],
}).then(() => {
window.$message.success(t('common_module.successful_update'))
})
}
} }
</script> </script>
<template> <template>
<div class="h-full min-h-fit flex-col rounded-[20px] bg-white p-6 shadow-[0_2px_2px_#0000000a]"> <div class="h-full min-h-fit flex-col rounded-[20px] bg-white p-6 shadow-[0_2px_2px_#0000000a]">
<ImageCropper ref="imageCropperRef" />
<div class="ml-[56px] pt-[6px]"> <div class="ml-[56px] pt-[6px]">
<ul> <ul>
<li class="font-600 flex items-center text-[18px]"> <li class="font-600 flex items-center text-[18px]">
...@@ -204,10 +175,15 @@ function handlePasswordChangeSubmit() { ...@@ -204,10 +175,15 @@ function handlePasswordChangeSubmit() {
</div> </div>
<div class="ml-[20px] flex items-center"> <div class="ml-[20px] flex items-center">
<n-button class="!mr-[6px]" size="tiny" @click="handleUserInfoFormItemEditUpdate('nickName', false)"> <n-button class="!mr-[6px]" size="tiny" @click="handleUserInfoFormItemEditUpdate('nickName')">
<i class="iconfont icon-close"></i> <i class="iconfont icon-close"></i>
</n-button> </n-button>
<n-button type="primary" size="tiny" @click="handleUserInfoFormItemEditUpdate('nickName', true)"> <n-button
type="primary"
size="tiny"
:loading="saveBtnLoading"
@click="handleUserInfoFormItemEditUpdate('nickName', true)"
>
<i class="iconfont icon-queren"></i> <i class="iconfont icon-queren"></i>
</n-button> </n-button>
</div> </div>
...@@ -218,7 +194,7 @@ function handlePasswordChangeSubmit() { ...@@ -218,7 +194,7 @@ function handlePasswordChangeSubmit() {
<i <i
class="iconfont icon-edit1 cursor-pointer px-[5px]" class="iconfont icon-edit1 cursor-pointer px-[5px]"
@click="handleUserInfoFormItemEditUpdate('nickName', true)" @click="handleUserInfoFormItemEditUpdate('nickName')"
></i> ></i>
</div> </div>
</li> </li>
...@@ -241,10 +217,15 @@ function handlePasswordChangeSubmit() { ...@@ -241,10 +217,15 @@ function handlePasswordChangeSubmit() {
</div> </div>
<div class="ml-[20px] flex items-center"> <div class="ml-[20px] flex items-center">
<n-button class="!mr-[6px]" size="tiny" @click="handleUserInfoFormItemEditUpdate('remark', false)"> <n-button class="!mr-[6px]" size="tiny" @click="handleUserInfoFormItemEditUpdate('remark')">
<i class="iconfont icon-close"></i> <i class="iconfont icon-close"></i>
</n-button> </n-button>
<n-button type="primary" size="tiny" @click="handleUserInfoFormItemEditUpdate('remark', true)"> <n-button
type="primary"
size="tiny"
:loading="saveBtnLoading"
@click="handleUserInfoFormItemEditUpdate('remark', true)"
>
<i class="iconfont icon-queren"></i> <i class="iconfont icon-queren"></i>
</n-button> </n-button>
</div> </div>
...@@ -255,7 +236,7 @@ function handlePasswordChangeSubmit() { ...@@ -255,7 +236,7 @@ function handlePasswordChangeSubmit() {
<i <i
class="iconfont icon-edit1 cursor-pointer px-[5px]" class="iconfont icon-edit1 cursor-pointer px-[5px]"
@click="handleUserInfoFormItemEditUpdate('remark', true)" @click="handleUserInfoFormItemEditUpdate('remark')"
></i> ></i>
</div> </div>
</li> </li>
...@@ -309,96 +290,14 @@ function handlePasswordChangeSubmit() { ...@@ -309,96 +290,14 @@ function handlePasswordChangeSubmit() {
</ul> </ul>
</div> </div>
<n-modal v-model:show="isShowMailboxBindingModal" :mask-closable="false" :on-after-leave="onModalAfterLeave"> <PasswordChangeModal v-model:is-show-password-change-modal="isShowPasswordChangeModal" />
<n-card <MailboxBindingModal v-model:is-show-mailbox-binding-modal="isShowMailboxBindingModal" />
class="!w-[600px]"
:title="t('personal_settings_module.email_binding')"
:bordered="false"
size="medium"
closable
@close="() => (isShowMailboxBindingModal = false)"
>
<n-form
ref="emailInfoFormRef"
label-placement="left"
label-width="auto"
:model="emailInfoForm"
:rules="emailInfoFormRules"
>
<n-form-item :label="t('common_module.email')" path="email">
<n-input
v-model:value="emailInfoForm.email"
:placeholder="t('login_module.please_enter_your_email_address')"
/>
</n-form-item>
</n-form>
<template #footer>
<div class="text-end">
<n-space justify="end">
<n-button @click="() => (isShowMailboxBindingModal = false)">{{
t('common_module.cancel_btn_text')
}}</n-button>
<n-button type="primary" :loading="mailboxBindingSubmitBtnLoading" @click="handleMailboxBindingSubmit">
{{ t('common_module.confirm_btn_text') }}
</n-button>
</n-space>
</div>
</template>
</n-card>
</n-modal>
<n-modal v-model:show="isShowPasswordChangeModal" :mask-closable="false" :on-after-leave="onModalAfterLeave">
<n-card
class="!w-[600px]"
:title="t('personal_settings_module.password_change')"
:bordered="false"
size="medium"
closable
@close="() => (isShowPasswordChangeModal = false)"
>
<n-form
ref="passwordInfoFormRef"
label-placement="left"
label-width="auto"
:model="passwordInfoForm"
:rules="passwordFormRules"
>
<n-form-item :label="t('personal_settings_module.new_password')" path="password">
<n-input
v-model:value="passwordInfoForm.password"
:placeholder="t('personal_settings_module.please_enter_your_new_password')"
/>
</n-form-item>
<n-form-item :label="t('personal_settings_module.confirm_new_password')" path="confirmPassword">
<n-input
v-model:value="passwordInfoForm.confirmPassword"
:placeholder="t('personal_settings_module.please_enter_confirm_new_password')"
/>
</n-form-item>
</n-form>
<template #footer>
<div class="text-end">
<n-space justify="end">
<n-button @click="() => (isShowPasswordChangeModal = false)">{{
t('common_module.cancel_btn_text')
}}</n-button>
<n-button type="primary" :loading="passwordChangeSubmitBtnLoading" @click="handlePasswordChangeSubmit">
{{ t('common_module.confirm_btn_text') }}
</n-button>
</n-space>
</div>
</template>
</n-card>
</n-modal>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.list-item-label { .list-item-label {
/* stylelint-disable-next-line value-keyword-case */ width: v-bind('listItemLabelWidth');
width: v-bind(listItemLabelWidth);
text-align: end; text-align: end;
} }
</style> </style>
...@@ -38,6 +38,8 @@ const isHoverKnowledgeItem = computed(() => (kdId: number) => { ...@@ -38,6 +38,8 @@ const isHoverKnowledgeItem = computed(() => (kdId: number) => {
return hoverKdId.value === kdId return hoverKdId.value === kdId
}) })
const isOpenDocumentParsing = computed(() => knowledgeConfig.value.isDocumentParsing === 'Y')
async function handleGetKnowledgeListByIds() { async function handleGetKnowledgeListByIds() {
const res = await fetchGetKnowledgeListByKdIds<KnowledgeItem[]>(knowledgeConfig.value.knowledgeIds) const res = await fetchGetKnowledgeListByKdIds<KnowledgeItem[]>(knowledgeConfig.value.knowledgeIds)
...@@ -81,6 +83,10 @@ function handleCloseAssociatedKnowledgeModal() { ...@@ -81,6 +83,10 @@ function handleCloseAssociatedKnowledgeModal() {
knowledgeConfigExpandedNames.value = ['knowledge'] knowledgeConfigExpandedNames.value = ['knowledge']
handleGetKnowledgeListByIds() handleGetKnowledgeListByIds()
} }
function handleUpdateDocumentParsing(value: boolean) {
knowledgeConfig.value.isDocumentParsing = value ? 'Y' : 'N'
}
</script> </script>
<template> <template>
...@@ -156,6 +162,28 @@ function handleCloseAssociatedKnowledgeModal() { ...@@ -156,6 +162,28 @@ function handleCloseAssociatedKnowledgeModal() {
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.knowledge_base_desc') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.knowledge_base_desc') }}
</span> </span>
</NCollapseItem> </NCollapseItem>
<NCollapseItem name="uploadFile" class="my-[13px]!">
<template #header>
<span class="mr-[5px] min-w-[60px]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.upload_file') }}
</span>
</template>
<template #header-extra>
<n-switch :value="isOpenDocumentParsing" size="small" @update:value="handleUpdateDocumentParsing">
<template #checked> {{ t('common_module.open') }} </template>
<template #unchecked> {{ t('common_module.close') }} </template>
</n-switch>
</template>
<div>
<div class="text-xs text-[#84868c]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.upload_file_desc') }}
</div>
<div class="flex flex-1 flex-wrap items-center gap-[12px] overflow-hidden"></div>
</div>
</NCollapseItem>
</NCollapse> </NCollapse>
</div> </div>
</section> </section>
......
...@@ -62,17 +62,16 @@ function handleUpdatePageScroll() { ...@@ -62,17 +62,16 @@ function handleUpdatePageScroll() {
} }
function handleClearAllMessage() { function handleClearAllMessage() {
window.$dialog.warning({ window.$message
title: t('common_module.dialogue_module.clear_message_dialog_title'), .ctWarning(
content: t('common_module.dialogue_module.clear_message_dialog_content'), t('common_module.dialogue_module.clear_message_dialog_content'),
negativeText: t('common_module.cancel_btn_text'), t('common_module.dialogue_module.clear_message_dialog_title'),
positiveText: t('common_module.confirm_btn_text'), )
onPositiveClick: () => { .then(() => {
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
messageList.value = [] messageList.value = []
window.$message.success(t('common_module.clear_success_message')) window.$message.success(t('common_module.clear_success_message'))
}, })
})
} }
async function handleCreateContinueQuestions(replyTextContent: string) { async function handleCreateContinueQuestions(replyTextContent: string) {
......
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, onMounted, onUnmounted, ref } from 'vue' import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
import { Emitter } from 'mitt' import { Emitter } from 'mitt'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import OverwriteMessageTipModal from './overwrite-message-tip-modal.vue'
import { fetchCustomEventSource } from '@/composables/useEventSource' import { fetchCustomEventSource } from '@/composables/useEventSource'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config' import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { UploadStatus } from '@/enums/upload-status'
import { useDialogueFile } from '@/composables/useDialogueFile'
interface Props { interface Props {
messageList: ConversationMessageItem[] messageList: ConversationMessageItem[]
...@@ -26,10 +29,13 @@ const emit = defineEmits<{ ...@@ -26,10 +29,13 @@ const emit = defineEmits<{
const personalAppConfigStore = usePersonalAppConfigStore() const personalAppConfigStore = usePersonalAppConfigStore()
const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile()
const messageTipModalRef = useTemplateRef<InstanceType<typeof OverwriteMessageTipModal>>('messageTipModalRef')
const emitter = inject<Emitter<MittEvents>>('emitter') const emitter = inject<Emitter<MittEvents>>('emitter')
const inputMessageContent = ref('') const inputMessageContent = ref('')
const isAnswerResponseWait = ref(false) const isAnswerResponseWait = ref(false)
let controller: AbortController | null = null let controller: AbortController | null = null
...@@ -50,6 +56,29 @@ const isSendBtnDisabled = computed(() => { ...@@ -50,6 +56,29 @@ const isSendBtnDisabled = computed(() => {
return !inputMessageContent.value.trim() return !inputMessageContent.value.trim()
}) })
const isInputMessageDisabled = computed(() => {
return uploadFileList.value.some((fileItem) => fileItem.status !== UploadStatus.FINISHED)
})
const isEnableDocumentParse = computed(() => {
return personalAppConfigStore.knowledgeConfig.isDocumentParsing === 'Y'
})
const isUploadFileDisabled = computed(() => {
return uploadFileList.value.length === 1
})
const uploadFileIcon = (type: string) => {
return `https://gsst-poe-sit.gz.bcebos.com/icon/${type}.svg`
}
watch(
() => isEnableDocumentParse.value,
() => {
uploadFileList.value = []
},
)
onUnmounted(() => { onUnmounted(() => {
blockMessageResponse() blockMessageResponse()
emitter?.off('selectQuestion') emitter?.off('selectQuestion')
...@@ -73,18 +102,8 @@ function messageItemFactory() { ...@@ -73,18 +102,8 @@ function messageItemFactory() {
} as const } as const
} }
function handleEnterKeypress(event: KeyboardEvent) {
if (event.code === 'Enter' && !event.shiftKey) {
event.preventDefault()
if (!inputMessageContent.value.trim() || isAnswerResponseWait.value) return ''
handleMessageSend()
}
}
function handleMessageSend() { function handleMessageSend() {
if (!inputMessageContent.value.trim() || isAnswerResponseWait.value) return '' if (!inputMessageContent.value.trim() || isAnswerResponseWait.value || isInputMessageDisabled.value) return ''
const messages: { const messages: {
content: { content: {
...@@ -136,6 +155,7 @@ function handleMessageSend() { ...@@ -136,6 +155,7 @@ function handleMessageSend() {
path: '/api/rest/agentApplicationInfoRest/preview.json', path: '/api/rest/agentApplicationInfoRest/preview.json',
payload: { payload: {
agentId: agentId.value, agentId: agentId.value,
fileUrls: uploadFileList.value.map((item) => item.url),
messages, messages,
}, },
controller, controller,
...@@ -195,6 +215,17 @@ function blockMessageResponse() { ...@@ -195,6 +215,17 @@ function blockMessageResponse() {
isAnswerResponseWait.value = false isAnswerResponseWait.value = false
} }
function handleSelectFile(cb: () => void) {
if (isUploadFileDisabled.value) {
messageTipModalRef.value?.handleShowModal().then(() => {
cb()
})
return
}
cb()
}
defineExpose({ defineExpose({
blockMessageResponse, blockMessageResponse,
}) })
...@@ -202,45 +233,121 @@ defineExpose({ ...@@ -202,45 +233,121 @@ defineExpose({
<template> <template>
<div class="mb-3 mt-5 px-5"> <div class="mb-3 mt-5 px-5">
<div class="flex"> <div class="flex items-end gap-2.5">
<div class="mr-2 flex h-8 w-8 items-center justify-center"> <div class="flex-1">
<NPopover trigger="hover"> <ul v-show="uploadFileList.length > 0" class="mb-1.5 grid gap-1.5">
<template #trigger> <li
v-for="uploadFileItem in uploadFileList"
:key="uploadFileItem.id"
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'"
>
<div class="flex w-full items-center justify-between px-3.5">
<div class="flex w-full items-center overflow-hidden">
<img :src="uploadFileIcon(uploadFileItem.type!)" class="h-7 w-7" />
<div class="mx-2.5 flex flex-1 flex-col overflow-hidden">
<n-ellipsis>
{{ uploadFileItem.name }}
</n-ellipsis>
</div>
</div>
<n-progress
v-show="!['finished', 'error'].includes(uploadFileItem.status)"
class="left-13.5 w-[calc(100%-78px)]! absolute bottom-0"
type="line"
rail-color="#F3F3F3"
:height="4"
:percentage="uploadFileItem.percentage"
:show-indicator="false"
/>
<div v-show="['finished', 'error'].includes(uploadFileItem.status)" class="hidden group-hover:block">
<n-popover trigger="hover" placement="top-end" :show-arrow="false">
<template #trigger>
<i
class="iconfont icon-close cursor-pointer hover:opacity-80"
:class="uploadFileItem.status === 'error' ? 'text-error-font-color' : 'text-font-color'"
@click="handleRemoveFile(uploadFileItem.id)"
/>
</template>
<span>{{ t('common_module.dialogue_module.cancel_associate_file_tip') }}</span>
</n-popover>
</div>
</div>
</li>
</ul>
<div class="relative flex-1">
<n-input
v-model:value="inputMessageContent"
:placeholder="t('common_module.dialogue_module.question_input_placeholder')"
:disabled="isInputMessageDisabled"
class="rounded-xl! shadow-[0_1px_#09122105,0_1px_1px_#09122105,0_3px_3px_#09122103,0_9px_9px_#09122103]! py-[4px] pr-[50px]"
@keydown.enter="handleMessageSend"
/>
<div
class="bg-px-send-png absolute bottom-2 right-[20px] h-[24px] w-[24px]"
:class="
isSendBtnDisabled || isAnswerResponseWait || isInputMessageDisabled ? 'opacity-60' : 'cursor-pointer'
"
@click="handleMessageSend"
/>
</div>
</div>
<n-upload
:show-file-list="false"
accept=".doc, .pdf, .docx, .txt, .md"
abstract
@before-upload="handleLimitUpload"
@change="handleUpload"
>
<n-upload-trigger #="{ handleClick }" abstract>
<n-popover style="width: 210px" trigger="hover">
<template #trigger>
<div
v-show="isEnableDocumentParse"
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="handleSelectFile(handleClick)"
>
<i class="iconfont icon-upload flex h-4 w-4 items-center justify-center" />
</div>
</template>
<span class="text-xs"> {{ t('common_module.dialogue_module.upload_file_limit') }} </span>
</n-popover>
</n-upload-trigger>
</n-upload>
<n-popover trigger="hover">
<template #trigger>
<div
class="h-7.5 w-7.5 mb-1 flex cursor-pointer items-center justify-center rounded-full bg-white"
@click="handleClearAllMessage"
>
<i <i
class="iconfont icon-clear text-base leading-none" class="iconfont icon-clear text-base leading-none"
:class=" :class="
isAllowClearMessage isAllowClearMessage ? 'hover:text-theme-color text-font-color' : 'cursor-not-allowed text-[#b8babf]'
? 'hover:text-theme-color cursor-pointer text-[#5c5f66]'
: 'cursor-not-allowed text-[#b8babf]'
" "
@click="handleClearAllMessage"
/> />
</template> </div>
<span class="text-xs"> </template>
{{ t('common_module.dialogue_module.clear_message_popover_message') }} <span class="text-xs">
</span> {{ t('common_module.dialogue_module.clear_message_popover_message') }}
</NPopover> </span>
</div> </n-popover>
<div class="relative flex-1">
<NInput
v-model:value="inputMessageContent"
:placeholder="t('common_module.dialogue_module.question_input_placeholder')"
class="rounded-xl! shadow-[0_1px_#09122105,0_1px_1px_#09122105,0_3px_3px_#09122103,0_9px_9px_#09122103]! py-[4px] pr-[50px]"
@keypress="handleEnterKeypress"
/>
<div
class="bg-px-send-png absolute bottom-2 right-[20px] h-[24px] w-[24px]"
:class="isSendBtnDisabled || isAnswerResponseWait ? 'opacity-60' : 'cursor-pointer'"
@click="handleMessageSend"
/>
</div>
</div> </div>
<div class="mt-[9px] pl-10"> <div class="mt-[9px] pl-2">
<span class="text-xs text-[#84868c]"> <span class="text-xs text-[#84868c]">
{{ t('common_module.dialogue_module.generate_warning_message') }} {{ t('common_module.dialogue_module.generate_warning_message') }}
</span> </span>
</div> </div>
</div> </div>
<OverwriteMessageTipModal ref="messageTipModalRef" />
</template> </template>
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const isShowModal = ref(false)
const pageWidth = ref(window.innerWidth)
const modalOptions = reactive({
title: t('common_module.dialogue_module.overwrite_file_tip'),
content: '',
})
let _modalStatusResolve = (_value?: any) => {}
let _modalStatusReject = (_reason?: any) => {}
const modalFixRight = computed(() => {
return ((pageWidth.value / 5) * 2 - 443) / 2
})
watch(
() => isShowModal.value,
() => {
pageWidth.value = window.innerWidth
},
)
function handleCancel() {
isShowModal.value = false
_modalStatusReject(new Error('cancel show modal'))
}
function handleConfirm() {
isShowModal.value = false
_modalStatusResolve(true)
}
function handleShowModal() {
isShowModal.value = true
return new Promise<boolean | Error>((resolve, reject) => {
_modalStatusResolve = resolve
_modalStatusReject = reject
})
}
defineExpose({
handleShowModal,
})
</script>
<template>
<n-modal v-model:show="isShowModal">
<div class="fixed! w-[443px] rounded-[10px] bg-[#fff] p-[30px]" :style="{ right: modalFixRight + 'px' }">
<div>
<h2 class="flex items-baseline">
<i class="iconfont icon-tishi text-[18px] text-[#f25744]"></i>
<span class="font-600 ml-[5px] text-[18px]">{{ modalOptions.title || t('common_module.tip') }}</span>
</h2>
<div class="mt-[20px] pl-4 text-[16px]">{{ modalOptions.content }}</div>
</div>
<div class="mt-[50px] text-end">
<n-button color="#F5F5F5" round class="!px-[34px] !py-[10px] !text-[14px] !text-[#333]" @click="handleCancel">
{{ t('common_module.cancel_btn_text') }}
</n-button>
<n-button color="#6F77FF" round class="!ml-[12px] !px-[34px] !py-[10px] !text-[14px]" @click="handleConfirm">
{{ t('common_module.confirm_btn_text') }}
</n-button>
</div>
</div>
</n-modal>
</template>
...@@ -4,6 +4,8 @@ import { Emitter } from 'mitt' ...@@ -4,6 +4,8 @@ import { Emitter } from 'mitt'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { fetchCustomEventSource } from '@/composables/useEventSource' import { fetchCustomEventSource } from '@/composables/useEventSource'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { UploadStatus } from '@/enums/upload-status'
import { useDialogueFile } from '@/composables/useDialogueFile'
import { useLayoutConfig } from '@/composables/useLayoutConfig' import { useLayoutConfig } from '@/composables/useLayoutConfig'
interface Props { interface Props {
...@@ -11,6 +13,7 @@ interface Props { ...@@ -11,6 +13,7 @@ interface Props {
dialogsId: string dialogsId: string
messageList: ConversationMessageItem[] messageList: ConversationMessageItem[]
continuousQuestionStatus: 'default' | 'close' continuousQuestionStatus: 'default' | 'close'
isEnableDocumentParse: boolean
} }
const { t } = useI18n() const { t } = useI18n()
...@@ -32,6 +35,8 @@ const { isMobile } = useLayoutConfig() ...@@ -32,6 +35,8 @@ const { isMobile } = useLayoutConfig()
const userStore = useUserStore() const userStore = useUserStore()
const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile()
const emitter = inject<Emitter<MittEvents>>('emitter') const emitter = inject<Emitter<MittEvents>>('emitter')
const inputMessageContent = ref('') const inputMessageContent = ref('')
...@@ -60,6 +65,18 @@ const isCreateContinueQuestions = computed(() => { ...@@ -60,6 +65,18 @@ const isCreateContinueQuestions = computed(() => {
return props.continuousQuestionStatus === 'default' return props.continuousQuestionStatus === 'default'
}) })
const isInputMessageDisabled = computed(() => {
return uploadFileList.value.some((fileItem) => fileItem.status !== UploadStatus.FINISHED)
})
const isUploadFileDisabled = computed(() => {
return uploadFileList.value.length === 1
})
const uploadFileIcon = (type: string) => {
return `https://gsst-poe-sit.gz.bcebos.com/icon/${type}.svg`
}
onMounted(() => { onMounted(() => {
emitter?.on('selectQuestion', (featuredQuestion) => { emitter?.on('selectQuestion', (featuredQuestion) => {
if (!isLogin.value) { if (!isLogin.value) {
...@@ -88,23 +105,13 @@ function messageItemFactory() { ...@@ -88,23 +105,13 @@ function messageItemFactory() {
} as const } as const
} }
function handleEnterKeypress(event: KeyboardEvent) {
if (event.code === 'Enter' && !event.shiftKey) {
event.preventDefault()
if (!inputMessageContent.value.trim() || isAnswerResponseWait.value) return ''
handleMessageSend()
}
}
function handleMessageSend() { function handleMessageSend() {
if (!isLogin.value) { if (!isLogin.value) {
window.$message.warning(t('common_module.not_login_text')) window.$message.warning(t('common_module.not_login_text'))
return return
} }
if (!inputMessageContent.value.trim() || isAnswerResponseWait.value) return '' if (!inputMessageContent.value.trim() || isAnswerResponseWait.value || isInputMessageDisabled.value) return ''
emit('resetContinueQuestionList') emit('resetContinueQuestionList')
emit('addMessageItem', { ...messageItemFactory(), textContent: inputMessageContent.value }) emit('addMessageItem', { ...messageItemFactory(), textContent: inputMessageContent.value })
...@@ -131,6 +138,7 @@ function handleMessageSend() { ...@@ -131,6 +138,7 @@ function handleMessageSend() {
payload: { payload: {
agentId: props.agentId, agentId: props.agentId,
dialogsId: props.dialogsId, dialogsId: props.dialogsId,
fileUrls: uploadFileList.value.map((item) => item.url),
input, input,
}, },
controller, controller,
...@@ -194,6 +202,17 @@ function handleToLogin() { ...@@ -194,6 +202,17 @@ function handleToLogin() {
emit('toLogin') emit('toLogin')
} }
function handleSelectFile(cb: () => void) {
if (isUploadFileDisabled.value) {
window.$message.ctWarning('', t('common_module.dialogue_module.overwrite_file_tip')).then(() => {
cb()
})
return
}
cb()
}
defineExpose({ defineExpose({
blockMessageResponse, blockMessageResponse,
}) })
...@@ -201,46 +220,128 @@ defineExpose({ ...@@ -201,46 +220,128 @@ defineExpose({
<template> <template>
<div class="my-5"> <div class="my-5">
<div class="flex items-center"> <div class="flex items-end gap-2.5">
<div class="mr-2 flex items-center justify-center" :class="isMobile ? 'h-6 w-6' : 'h-8 w-8'"> <div class="flex flex-1 flex-col">
<NPopover trigger="hover"> <ul v-show="uploadFileList.length > 0" class="mb-1.5 grid gap-1.5">
<template #trigger> <li
<i v-for="uploadFileItem in uploadFileList"
class="iconfont icon-clear text-base leading-none" :key="uploadFileItem.id"
:class=" class="group relative flex h-[42px] w-full items-center overflow-hidden rounded-[10px] border bg-white/70"
isAllowClearMessage :class="uploadFileItem.status === 'error' ? 'border-error-font-color' : 'border-transparent'"
? 'hover:text-theme-color cursor-pointer text-[#5c5f66]' >
: 'cursor-not-allowed text-[#b8babf]' <div class="flex w-full items-center justify-between px-3.5">
" <div class="flex w-full items-center overflow-hidden">
@click="handleClearAllMessage" <img :src="uploadFileIcon(uploadFileItem.type!)" class="h-7 w-7" />
/>
</template> <div class="mx-2.5 flex flex-1 flex-col overflow-hidden">
<span class="text-xs"> {{ t('common_module.dialogue_module.clear_message_popover_message') }}</span> <n-ellipsis>
</NPopover> {{ uploadFileItem.name }}
</div> </n-ellipsis>
</div>
<div class="relative flex-1"> </div>
<NInput
v-model:value="inputMessageContent" <n-progress
:placeholder="inputPlaceholder" v-show="!['finished', 'error'].includes(uploadFileItem.status)"
:disabled="!isLogin" class="left-13.5 w-[calc(100%-78px)]! absolute bottom-0"
class="rounded-xl! shadow-[0_1px_#09122105,0_1px_1px_#09122105,0_3px_3px_#09122103,0_9px_9px_#09122103]! py-[4px] pr-[50px]" type="line"
@keypress="handleEnterKeypress" rail-color="#F3F3F3"
/> :height="4"
<div :percentage="uploadFileItem.percentage"
class="bg-px-send-png absolute bottom-2 right-[20px] h-[24px] w-[24px]" :show-indicator="false"
:class="isSendBtnDisabled || isAnswerResponseWait || !isLogin ? 'opacity-60' : 'cursor-pointer'" />
@click="handleMessageSend"
/> <div
v-show="['finished', 'error'].includes(uploadFileItem.status)"
<div v-show="!isLogin" class="absolute left-3 top-[5px] flex h-[30px] items-center text-[#84868c]"> class="group-hover:block"
{{ t('share_agent_module.please') }} :class="isMobile ? 'block' : 'hidden'"
<span class="text-theme-color cursor-pointer px-1 hover:opacity-80" @click="handleToLogin"> >
{{ t('common_module.login') }} <n-popover trigger="hover" placement="top-end" :show-arrow="false">
</span> <template #trigger>
{{ t('share_agent_module.after_action') }} <i
class="iconfont icon-close cursor-pointer hover:opacity-80"
:class="uploadFileItem.status === 'error' ? 'text-error-font-color' : 'text-font-color'"
@click="handleRemoveFile(uploadFileItem.id)"
/>
</template>
<span>{{ t('common_module.dialogue_module.cancel_associate_file_tip') }}</span>
</n-popover>
</div>
</div>
</li>
</ul>
<div class="relative flex-1">
<n-input
v-model:value="inputMessageContent"
:placeholder="inputPlaceholder"
:disabled="!isLogin || isInputMessageDisabled"
class="rounded-xl! shadow-[0_1px_#09122105,0_1px_1px_#09122105,0_3px_3px_#09122103,0_9px_9px_#09122103]! py-[4px] pr-[50px]"
@keydown.enter="handleMessageSend"
/>
<div
class="bg-px-send-png absolute bottom-2 right-[20px] h-[24px] w-[24px]"
:class="
isSendBtnDisabled || isAnswerResponseWait || !isLogin || isInputMessageDisabled
? 'opacity-60'
: 'cursor-pointer'
"
@click="handleMessageSend"
/>
<div v-show="!isLogin" class="absolute left-3 top-[5px] flex h-[30px] items-center text-[#84868c]">
{{ t('share_agent_module.please') }}
<span class="text-theme-color cursor-pointer px-1 hover:opacity-80" @click="handleToLogin">
{{ t('common_module.login') }}
</span>
{{ t('share_agent_module.after_action') }}
</div>
</div> </div>
</div> </div>
<n-upload
:show-file-list="false"
accept=".doc, .pdf, .docx, .txt, .md"
:disabled="!isLogin"
abstract
@before-upload="handleLimitUpload"
@change="handleUpload"
>
<n-upload-trigger #="{ handleClick }" abstract>
<n-popover style="width: 210px" trigger="hover">
<template #trigger>
<div
v-show="isEnableDocumentParse"
class="h-7.5 w-7.5 mb-1 flex items-center justify-center rounded-full bg-white"
:class="
isLogin
? 'hover:text-theme-color text-font-color cursor-pointer'
: 'cursor-not-allowed text-[#b8babf]'
"
@click="handleSelectFile(handleClick)"
>
<i class="iconfont icon-upload flex h-4 w-4 items-center justify-center" />
</div>
</template>
<span class="text-xs"> {{ t('common_module.dialogue_module.upload_file_limit') }} </span>
</n-popover>
</n-upload-trigger>
</n-upload>
<n-popover trigger="hover">
<template #trigger>
<div
class="h-7.5 w-7.5 mb-1 flex items-center justify-center rounded-full bg-white"
:class="
isAllowClearMessage
? 'hover:text-theme-color text-font-color cursor-pointer'
: 'cursor-not-allowed text-[#b8babf]'
"
>
<i class="iconfont icon-clear text-base leading-none" @click="handleClearAllMessage" />
</div>
</template>
<span class="text-xs"> {{ t('common_module.dialogue_module.clear_message_popover_message') }}</span>
</n-popover>
</div> </div>
<div class="mt-[9px]"> <div class="mt-[9px]">
......
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import PageHeader from './components/mobile-page-header.vue' import PageHeader from './components/mobile-page-header.vue'
...@@ -34,6 +34,10 @@ const messageList = ref<ConversationMessageItem[]>([]) ...@@ -34,6 +34,10 @@ const messageList = ref<ConversationMessageItem[]>([])
const continuousQuestionStatus = ref<'default' | 'close'>('default') const continuousQuestionStatus = ref<'default' | 'close'>('default')
const continueQuestionList = ref<string[]>([]) const continueQuestionList = ref<string[]>([])
const isEnableDocumentParse = computed(() => {
return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y'
})
onMounted(async () => { onMounted(async () => {
if (router.currentRoute.value.params.agentId) { if (router.currentRoute.value.params.agentId) {
agentId.value = router.currentRoute.value.params.agentId as string agentId.value = router.currentRoute.value.params.agentId as string
...@@ -113,17 +117,16 @@ function handleUpdatePageScroll() { ...@@ -113,17 +117,16 @@ function handleUpdatePageScroll() {
} }
function handleClearAllMessage() { function handleClearAllMessage() {
window.$dialog.warning({ window.$message
title: t('common_module.dialogue_module.clear_message_dialog_title'), .ctWarning(
content: t('common_module.dialogue_module.clear_message_dialog_content'), t('common_module.dialogue_module.clear_message_dialog_content'),
negativeText: t('common_module.cancel_btn_text'), t('common_module.dialogue_module.clear_message_dialog_title'),
positiveText: t('common_module.confirm_btn_text'), )
onPositiveClick: () => { .then(() => {
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
messageList.value = [] messageList.value = []
window.$message.success(t('common_module.clear_success_message')) window.$message.success(t('common_module.clear_success_message'))
}, })
})
} }
async function handleCreateContinueQuestions(replyTextContent: string) { async function handleCreateContinueQuestions(replyTextContent: string) {
...@@ -172,6 +175,7 @@ function handleResetContinueQuestionList() { ...@@ -172,6 +175,7 @@ function handleResetContinueQuestionList() {
:dialogs-id="dialogsId" :dialogs-id="dialogsId"
:agent-id="agentApplicationConfig.baseInfo.agentId" :agent-id="agentApplicationConfig.baseInfo.agentId"
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:is-enable-document-parse="isEnableDocumentParse"
@add-message-item="handleAddMessageItem" @add-message-item="handleAddMessageItem"
@update-specify-message-item="handleUpdateSpecifyMessageItem" @update-specify-message-item="handleUpdateSpecifyMessageItem"
@delete-last-message-item="handleDeleteLastMessageItem" @delete-last-message-item="handleDeleteLastMessageItem"
......
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import PageHeader from './components/web-page-header.vue' import PageHeader from './components/web-page-header.vue'
...@@ -37,6 +37,10 @@ const messageList = ref<ConversationMessageItem[]>([]) ...@@ -37,6 +37,10 @@ const messageList = ref<ConversationMessageItem[]>([])
const continuousQuestionStatus = ref<'default' | 'close'>('default') const continuousQuestionStatus = ref<'default' | 'close'>('default')
const continueQuestionList = ref<string[]>([]) const continueQuestionList = ref<string[]>([])
const isEnableDocumentParse = computed(() => {
return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y'
})
onMounted(async () => { onMounted(async () => {
if (router.currentRoute.value.params.agentId) { if (router.currentRoute.value.params.agentId) {
agentId.value = router.currentRoute.value.params.agentId as string agentId.value = router.currentRoute.value.params.agentId as string
...@@ -133,17 +137,16 @@ function handleUpdatePageScroll() { ...@@ -133,17 +137,16 @@ function handleUpdatePageScroll() {
} }
function handleClearAllMessage() { function handleClearAllMessage() {
window.$dialog.warning({ window.$message
title: t('common_module.dialogue_module.clear_message_dialog_title'), .ctWarning(
content: t('common_module.dialogue_module.clear_message_dialog_content'), t('common_module.dialogue_module.clear_message_dialog_content'),
negativeText: t('common_module.cancel_btn_text'), t('common_module.dialogue_module.clear_message_dialog_title'),
positiveText: t('common_module.confirm_btn_text'), )
onPositiveClick: () => { .then(() => {
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
messageList.value = [] messageList.value = []
window.$message.success(t('common_module.clear_success_message')) window.$message.success(t('common_module.clear_success_message'))
}, })
})
} }
async function handleCreateContinueQuestions(replyTextContent: string) { async function handleCreateContinueQuestions(replyTextContent: string) {
...@@ -210,6 +213,7 @@ function handleResetContinueQuestionList() { ...@@ -210,6 +213,7 @@ function handleResetContinueQuestionList() {
:dialogs-id="dialogsId" :dialogs-id="dialogsId"
:agent-id="agentApplicationConfig.baseInfo.agentId" :agent-id="agentApplicationConfig.baseInfo.agentId"
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:is-enable-document-parse="isEnableDocumentParse"
@add-message-item="handleAddMessageItem" @add-message-item="handleAddMessageItem"
@update-specify-message-item="handleUpdateSpecifyMessageItem" @update-specify-message-item="handleUpdateSpecifyMessageItem"
@delete-last-message-item="handleDeleteLastMessageItem" @delete-last-message-item="handleDeleteLastMessageItem"
......
...@@ -89,6 +89,8 @@ declare namespace I18n { ...@@ -89,6 +89,8 @@ declare namespace I18n {
language: string language: string
change: string change: string
bind: string bind: string
sms: string
verificationCode: string
dialogue_module: { dialogue_module: {
continue_question_message: string continue_question_message: string
...@@ -98,6 +100,9 @@ declare namespace I18n { ...@@ -98,6 +100,9 @@ declare namespace I18n {
clear_message_popover_message: string clear_message_popover_message: string
clear_message_dialog_title: string clear_message_dialog_title: string
clear_message_dialog_content: string clear_message_dialog_content: string
cancel_associate_file_tip: string
upload_file_limit: string
overwrite_file_tip: string
} }
data_table_module: { data_table_module: {
...@@ -244,6 +249,8 @@ declare namespace I18n { ...@@ -244,6 +249,8 @@ declare namespace I18n {
knowledge: string knowledge: string
knowledge_base: string knowledge_base: string
knowledge_base_desc: string knowledge_base_desc: string
upload_file: string
upload_file_desc: string
dialogue: string dialogue: string
preamble: string preamble: string
preamble_input_placeholder: string preamble_input_placeholder: string
...@@ -466,6 +473,9 @@ declare namespace I18n { ...@@ -466,6 +473,9 @@ declare namespace I18n {
verify_that_the_new_password_is_inconsistent_with_the_new_password: string verify_that_the_new_password_is_inconsistent_with_the_new_password: string
please_enter_the_account_nickname: string please_enter_the_account_nickname: string
please_enter_a_personal_profile: string please_enter_a_personal_profile: string
please_enter_the_correct_verification_code: string
binding_successful: string
obtaining_the_verification_code: string
} }
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment