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 @@
name="viewport"
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>
</head>
......
......@@ -23,6 +23,7 @@
"@vueuse/core": "^10.11.1",
"axios": "^1.7.7",
"clipboardy": "^4.0.0",
"cropperjs": "^1.6.2",
"dayjs": "^1.11.13",
"dompurify": "^3.2.0",
"github-markdown-css": "^5.7.0",
......
......@@ -29,6 +29,9 @@ importers:
clipboardy:
specifier: ^4.0.0
version: 4.0.0
cropperjs:
specifier: ^1.6.2
version: 1.6.2
dayjs:
specifier: ^1.11.13
version: 1.11.13
......@@ -1526,6 +1529,9 @@ packages:
typescript:
optional: true
cropperjs@1.6.2:
resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==}
cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
......@@ -4672,6 +4678,8 @@ snapshots:
optionalDependencies:
typescript: 5.6.2
cropperjs@1.6.2: {}
cross-spawn@7.0.3:
dependencies:
path-key: 3.1.1
......
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, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 120000,
...(config?.onUploadProgress ? { onUploadProgress: config.onUploadProgress } : {}),
})
}
......@@ -24,3 +24,17 @@ export function fetchUserDetailInfo<T>() {
export function fetchGetMemberInfoById<T>(memberId: number) {
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">
import { onMounted, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useLayoutConfig } from '@/composables/useLayoutConfig'
const { t } = useI18n()
const { isMobile } = useLayoutConfig()
const isShowModal = ref(false)
const modalOptions = reactive({
......@@ -45,14 +48,17 @@ function handleShowModal(content: string, title?: string) {
<template>
<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>
<h2>
<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] indent-4 text-[16px]">{{ modalOptions.content }}</div>
<div class="mt-[20px] pl-4 text-[16px]">{{ modalOptions.content }}</div>
</div>
<div class="mt-[50px] text-end">
......
<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 { fetchUpload } from '@/apis/upload'
import ImageCropper from '@/components/image-cropper/image-cropper.vue'
interface Emit {
(e: 'formatError'): void
......@@ -20,7 +21,7 @@ const props = defineProps({
},
listType: {
type: Array,
default: () => ['jpg', 'png', 'jpeg', 'svg', 'gif'],
default: () => ['jpg', 'png', 'jpeg', 'gif'],
},
width: {
type: Number,
......@@ -36,6 +37,8 @@ const emit = defineEmits<Emit>()
const slots = useSlots()
const imageCropperRef = useTemplateRef<InstanceType<typeof ImageCropper> | null>('imageCropperRef')
const uploadLoading = ref(false)
const isShowImageMask = ref(false)
......@@ -66,24 +69,23 @@ async function handleUploadImage(event: any) {
return
}
const URL = window.URL || window.webkitURL
const img = new Image()
img.src = URL.createObjectURL(file)
if (imageCropperRef.value && file) {
const URL = window.URL || window.webkitURL
imageCropperRef.value.cropImage(URL.createObjectURL(file)).then(async (file) => {
const formData = new FormData()
formData.append('file', file)
img.onload = async function () {
const formData = new FormData()
formData.append('file', file)
uploadLoading.value = true
uploadLoading.value = true
const res = await fetchUpload(formData).finally(() => {
uploadLoading.value = false
event.target.value = null
})
const res = await fetchUpload(formData).finally(() => {
uploadLoading.value = false
event.target.value = null
if (res.code === 0) {
uploadImageUrl.value = res.data as string
}
})
if (res.code === 0) {
uploadImageUrl.value = res.data as string
}
}
}
</script>
......@@ -120,5 +122,7 @@ async function handleUploadImage(event: any) {
<input id="upload" type="file" :accept="uploadImageType" class="hidden" @change="handleUploadImage" />
</div>
<ImageCropper ref="imageCropperRef" />
</div>
</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:
language: 'Language'
change: 'Change'
bind: 'Bind'
sms: 'Short message'
verificationCode: 'Verification code'
dialogue_module:
continue_question_message: 'You can keep asking questions'
......@@ -98,6 +100,9 @@ common_module:
clear_message_popover_message: 'Clear history session'
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?'
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:
action: 'Controls'
......@@ -214,7 +219,7 @@ personal_space_module:
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.'
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_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'
......@@ -244,6 +249,8 @@ personal_space_module:
knowledge: 'Knowledge'
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'
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'
preamble: 'Opening remarks'
preamble_input_placeholder: 'Please enter an opening statement'
......@@ -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'
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: 'Please enter the correct verification code'
binding_successful: 'Binding successful'
obtaining_the_verification_code: 'Obtaining the verification code'
......@@ -88,6 +88,8 @@ common_module:
language: '语言'
change: '更换'
bind: '绑定'
sms: '短信'
verificationCode: '验证码'
dialogue_module:
continue_question_message: '你可以继续提问'
......@@ -97,6 +99,9 @@ common_module:
clear_message_popover_message: '清空历史会话'
clear_message_dialog_title: '确认要清空对话吗?'
clear_message_dialog_content: '清空对话将清空调试区域所有历史对话内容,确定清空对话吗?'
cancel_associate_file_tip: '不再围绕这个文件回答'
upload_file_limit: '仅支持上传单个文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB'
overwrite_file_tip: '新上传的文件会覆盖原有文件,是否继续上传'
data_table_module:
action: '操作'
......@@ -242,6 +247,8 @@ personal_space_module:
knowledge: '知识'
knowledge_base: '知识库'
knowledge_base_desc: '引用文本数据、表格型知识数据(含FAQ问答,多列索引问答)以及网页数据,实现知识库问答,应用最多可关联5个知识库,请详细填写知识库描述信息以提高问答准确率'
upload_file: '上传文件'
upload_file_desc: '开启后支持用户上传文件进行对话聊天, 支持TXT、MD、PDF、DOC、DOCX格式的文件'
dialogue: '对话'
preamble: '开场白'
preamble_input_placeholder: '请输入开场白'
......@@ -450,3 +457,6 @@ personal_settings_module:
verify_that_the_new_password_is_inconsistent_with_the_new_password: '确认新密码与新密码不一致'
please_enter_the_account_nickname: '请输入账号昵称'
please_enter_a_personal_profile: '请输入个人简介'
please_enter_the_correct_verification_code: '请输入正确验证码'
binding_successful: '绑定成功'
obtaining_the_verification_code: '获取验证码方式'
......@@ -88,6 +88,8 @@ common_module:
language: '語言'
change: '更換'
bind: '綁定'
sms: '短信'
verificationCode: '驗證碼'
dialogue_module:
continue_question_message: '你可以繼續提問'
......@@ -97,6 +99,9 @@ common_module:
clear_message_popover_message: '清空歷史會話'
clear_message_dialog_title: '確認要清空對話嗎?'
clear_message_dialog_content: '清空對話將清空調試區域所有歷史對話內容,確定清空對話嗎?'
cancel_associate_file_tip: '不再圍繞這個文件回答'
upload_file_limit: '僅支持上傳單個文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB'
overwrite_file_tip: '新上傳的文件會覆蓋原有文件,是否繼續上傳'
data_table_module:
action: '操作'
......@@ -242,6 +247,8 @@ personal_space_module:
knowledge: '知識'
knowledge_base: '知識庫'
knowledge_base_desc: '引用文本數據、表格型知識數據(含FAQ問答,多列索引問答)以及網頁數據,實現知識庫問答,應用最多可關聯5個知識庫,請詳細填寫知識庫描述信息以提高問答準確率'
upload_file: '上傳文件'
upload_file_desc: '開啓後支持用户上傳文件進行對話聊天, 支持TXT、MD、PDF、DOC、DOCX格式的文件'
dialogue: '對話'
preamble: '開場白'
preamble_input_placeholder: '請輸入開場白'
......@@ -450,3 +457,6 @@ personal_settings_module:
verify_that_the_new_password_is_inconsistent_with_the_new_password: '確認新密碼與新密碼不一致'
please_enter_the_account_nickname: '請輸入賬號昵稱'
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 {
},
knowledgeConfig: {
knowledgeIds: [],
isDocumentParsing: 'N',
},
commModelConfig: {
largeModel: '文心4.0 (8K)',
......
......@@ -47,7 +47,7 @@ export const useUserStore = defineStore('user-store', {
ss.set(UserStoreStorageKeyEnum.userInfo, userInfo)
},
fetchUpdateUserInfo() {
fetchUserDetailInfo<UserInfo>().then((res) => {
return fetchUserDetailInfo<UserInfo>().then((res) => {
this.userInfo = res.data
})
},
......
......@@ -28,6 +28,7 @@ export interface PersonalAppConfigState {
}
knowledgeConfig: {
knowledgeIds: number[] //知识库ID
isDocumentParsing: 'Y' | 'N' //是否开启文档解析 Y-开启 N-关闭
}
commModelConfig: {
largeModel: string //大模型
......
......@@ -6,12 +6,15 @@ import { throttle } from 'lodash-es'
import CMessage from './c-message'
import { MessageItemInterface, MultiModelDialogueItem, QuestionMessageItem } from '../types'
import { fetchEventStreamSource } from '../utils/fetch-event-stream-source'
import { UploadStatus } from '@/enums/upload-status'
import { useDialogueFile } from '@/composables/useDialogueFile'
const { t } = useI18n()
interface Props {
agentId: string
isAnswerResponseWait: boolean
isEnableDocumentParse: boolean
}
const props = defineProps<Props>()
......@@ -25,6 +28,8 @@ const emit = defineEmits<{
clearAllMessage: []
}>()
const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile()
const multiModelDialogueList = defineModel<MultiModelDialogueItem[]>('multiModelDialogueList', { required: true })
const questionContent = ref('')
......@@ -41,6 +46,18 @@ const isAllowClearAllMessage = computed(() => {
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(() => {
emit('messageListScrollToBottom')
}, 1000)
......@@ -62,7 +79,7 @@ function messageItemFactory() {
}
function handleQuestionSubmit() {
if (isQuestionSubmitDisabled.value || props.isAnswerResponseWait) return
if (isQuestionSubmitDisabled.value || props.isAnswerResponseWait || isInputMessageDisabled.value) return
if (!isAllSelectedModelName.value) {
window.$message.warning(t('multi_model_dialogue_module.please_select_model_first'))
......@@ -116,6 +133,7 @@ function handleQuestionSubmit() {
path: '/api/rest/agentApplicationInfoRest/preview.json',
payload: {
agentId: props.agentId,
fileUrls: uploadFileList.value.map((item) => item.url),
messages,
topP,
temperature,
......@@ -167,44 +185,128 @@ function errorMessageResponse(questionMessageId: string, answerMessageId: string
emit('deleteMessageItem', questionMessageId, 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>
<template>
<footer class="flex flex-col items-center">
<div class="mb-4 flex items-center gap-[18px]">
<NPopover trigger="hover">
<template #trigger>
<div
class="border-inactive-border-color flex h-[54px] w-[54px] shrink-0 items-center justify-center rounded-full border bg-white"
<div class="mb-4 flex items-end gap-[18px]">
<div class="flex gap-3">
<n-popover trigger="hover">
<template #trigger>
<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="
isAllowClearAllMessage
? 'text-font-color hover:text-theme-color cursor-pointer'
: 'text-gray-font-color cursor-not-allowed'
isQuestionSubmitDisabled || isAnswerResponseWait || isInputMessageDisabled
? 'text-hover-theme-color cursor-not-allowed'
: 'text-theme-color cursor-pointer hover:opacity-80'
"
@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>
</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"
/>
@click="handleQuestionSubmit"
/>
</div>
</div>
</div>
<span class="text-gray-font-color select-none">
......
......@@ -45,6 +45,10 @@ const isHasMessageList = computed(() => {
return multiModelDialogueList.value.some((modelItem) => modelItem.messageList.size > 0)
})
const isEnableDocumentParse = computed(() => {
return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y'
})
onMounted(async () => {
if (!currentRoute.params.agentId) {
window.$message.warning(t('multi_model_dialogue_module.not_find_agent'))
......@@ -320,6 +324,7 @@ function handleBlockMessageResponse() {
v-model:multi-model-dialogue-list="multiModelDialogueList"
:agent-id="agentId"
:is-answer-response-wait="isAnswerResponseWait"
:is-enable-document-parse="isEnableDocumentParse"
@add-question-message-item="handleAddQuestionMessageItem"
@add-answer-message-item="handleAddAnswerMessageItem"
@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>
......@@ -38,6 +38,8 @@ const isHoverKnowledgeItem = computed(() => (kdId: number) => {
return hoverKdId.value === kdId
})
const isOpenDocumentParsing = computed(() => knowledgeConfig.value.isDocumentParsing === 'Y')
async function handleGetKnowledgeListByIds() {
const res = await fetchGetKnowledgeListByKdIds<KnowledgeItem[]>(knowledgeConfig.value.knowledgeIds)
......@@ -81,6 +83,10 @@ function handleCloseAssociatedKnowledgeModal() {
knowledgeConfigExpandedNames.value = ['knowledge']
handleGetKnowledgeListByIds()
}
function handleUpdateDocumentParsing(value: boolean) {
knowledgeConfig.value.isDocumentParsing = value ? 'Y' : 'N'
}
</script>
<template>
......@@ -156,6 +162,28 @@ function handleCloseAssociatedKnowledgeModal() {
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.knowledge_base_desc') }}
</span>
</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>
</div>
</section>
......
......@@ -62,17 +62,16 @@ function handleUpdatePageScroll() {
}
function handleClearAllMessage() {
window.$dialog.warning({
title: t('common_module.dialogue_module.clear_message_dialog_title'),
content: t('common_module.dialogue_module.clear_message_dialog_content'),
negativeText: t('common_module.cancel_btn_text'),
positiveText: t('common_module.confirm_btn_text'),
onPositiveClick: () => {
window.$message
.ctWarning(
t('common_module.dialogue_module.clear_message_dialog_content'),
t('common_module.dialogue_module.clear_message_dialog_title'),
)
.then(() => {
footerInputRef.value?.blockMessageResponse()
messageList.value = []
window.$message.success(t('common_module.clear_success_message'))
},
})
})
}
async function handleCreateContinueQuestions(replyTextContent: string) {
......
<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 { useI18n } from 'vue-i18n'
import OverwriteMessageTipModal from './overwrite-message-tip-modal.vue'
import { fetchCustomEventSource } from '@/composables/useEventSource'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { UploadStatus } from '@/enums/upload-status'
import { useDialogueFile } from '@/composables/useDialogueFile'
interface Props {
messageList: ConversationMessageItem[]
......@@ -26,10 +29,13 @@ const emit = defineEmits<{
const personalAppConfigStore = usePersonalAppConfigStore()
const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile()
const messageTipModalRef = useTemplateRef<InstanceType<typeof OverwriteMessageTipModal>>('messageTipModalRef')
const emitter = inject<Emitter<MittEvents>>('emitter')
const inputMessageContent = ref('')
const isAnswerResponseWait = ref(false)
let controller: AbortController | null = null
......@@ -50,6 +56,29 @@ const isSendBtnDisabled = computed(() => {
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(() => {
blockMessageResponse()
emitter?.off('selectQuestion')
......@@ -73,18 +102,8 @@ function messageItemFactory() {
} as const
}
function handleEnterKeypress(event: KeyboardEvent) {
if (event.code === 'Enter' && !event.shiftKey) {
event.preventDefault()
if (!inputMessageContent.value.trim() || isAnswerResponseWait.value) return ''
handleMessageSend()
}
}
function handleMessageSend() {
if (!inputMessageContent.value.trim() || isAnswerResponseWait.value) return ''
if (!inputMessageContent.value.trim() || isAnswerResponseWait.value || isInputMessageDisabled.value) return ''
const messages: {
content: {
......@@ -136,6 +155,7 @@ function handleMessageSend() {
path: '/api/rest/agentApplicationInfoRest/preview.json',
payload: {
agentId: agentId.value,
fileUrls: uploadFileList.value.map((item) => item.url),
messages,
},
controller,
......@@ -195,6 +215,17 @@ function blockMessageResponse() {
isAnswerResponseWait.value = false
}
function handleSelectFile(cb: () => void) {
if (isUploadFileDisabled.value) {
messageTipModalRef.value?.handleShowModal().then(() => {
cb()
})
return
}
cb()
}
defineExpose({
blockMessageResponse,
})
......@@ -202,45 +233,121 @@ defineExpose({
<template>
<div class="mb-3 mt-5 px-5">
<div class="flex">
<div class="mr-2 flex h-8 w-8 items-center justify-center">
<NPopover trigger="hover">
<template #trigger>
<div class="flex items-end gap-2.5">
<div class="flex-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="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
class="iconfont icon-clear text-base leading-none"
:class="
isAllowClearMessage
? 'hover:text-theme-color cursor-pointer text-[#5c5f66]'
: 'cursor-not-allowed text-[#b8babf]'
isAllowClearMessage ? 'hover:text-theme-color text-font-color' : 'cursor-not-allowed text-[#b8babf]'
"
@click="handleClearAllMessage"
/>
</template>
<span class="text-xs">
{{ t('common_module.dialogue_module.clear_message_popover_message') }}
</span>
</NPopover>
</div>
<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>
</template>
<span class="text-xs">
{{ t('common_module.dialogue_module.clear_message_popover_message') }}
</span>
</n-popover>
</div>
<div class="mt-[9px] pl-10">
<div class="mt-[9px] pl-2">
<span class="text-xs text-[#84868c]">
{{ t('common_module.dialogue_module.generate_warning_message') }}
</span>
</div>
</div>
<OverwriteMessageTipModal ref="messageTipModalRef" />
</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'
import { useI18n } from 'vue-i18n'
import { fetchCustomEventSource } from '@/composables/useEventSource'
import { useUserStore } from '@/store/modules/user'
import { UploadStatus } from '@/enums/upload-status'
import { useDialogueFile } from '@/composables/useDialogueFile'
import { useLayoutConfig } from '@/composables/useLayoutConfig'
interface Props {
......@@ -11,6 +13,7 @@ interface Props {
dialogsId: string
messageList: ConversationMessageItem[]
continuousQuestionStatus: 'default' | 'close'
isEnableDocumentParse: boolean
}
const { t } = useI18n()
......@@ -32,6 +35,8 @@ const { isMobile } = useLayoutConfig()
const userStore = useUserStore()
const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile()
const emitter = inject<Emitter<MittEvents>>('emitter')
const inputMessageContent = ref('')
......@@ -60,6 +65,18 @@ const isCreateContinueQuestions = computed(() => {
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(() => {
emitter?.on('selectQuestion', (featuredQuestion) => {
if (!isLogin.value) {
......@@ -88,23 +105,13 @@ function messageItemFactory() {
} as const
}
function handleEnterKeypress(event: KeyboardEvent) {
if (event.code === 'Enter' && !event.shiftKey) {
event.preventDefault()
if (!inputMessageContent.value.trim() || isAnswerResponseWait.value) return ''
handleMessageSend()
}
}
function handleMessageSend() {
if (!isLogin.value) {
window.$message.warning(t('common_module.not_login_text'))
return
}
if (!inputMessageContent.value.trim() || isAnswerResponseWait.value) return ''
if (!inputMessageContent.value.trim() || isAnswerResponseWait.value || isInputMessageDisabled.value) return ''
emit('resetContinueQuestionList')
emit('addMessageItem', { ...messageItemFactory(), textContent: inputMessageContent.value })
......@@ -131,6 +138,7 @@ function handleMessageSend() {
payload: {
agentId: props.agentId,
dialogsId: props.dialogsId,
fileUrls: uploadFileList.value.map((item) => item.url),
input,
},
controller,
......@@ -194,6 +202,17 @@ function handleToLogin() {
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({
blockMessageResponse,
})
......@@ -201,46 +220,128 @@ defineExpose({
<template>
<div class="my-5">
<div class="flex items-center">
<div class="mr-2 flex items-center justify-center" :class="isMobile ? 'h-6 w-6' : 'h-8 w-8'">
<NPopover trigger="hover">
<template #trigger>
<i
class="iconfont icon-clear text-base leading-none"
:class="
isAllowClearMessage
? 'hover:text-theme-color cursor-pointer text-[#5c5f66]'
: 'cursor-not-allowed text-[#b8babf]'
"
@click="handleClearAllMessage"
/>
</template>
<span class="text-xs"> {{ t('common_module.dialogue_module.clear_message_popover_message') }}</span>
</NPopover>
</div>
<div class="relative flex-1">
<NInput
v-model:value="inputMessageContent"
:placeholder="inputPlaceholder"
:disabled="!isLogin"
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 || !isLogin ? '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 class="flex items-end gap-2.5">
<div class="flex flex-1 flex-col">
<ul v-show="uploadFileList.length > 0" class="mb-1.5 grid gap-1.5">
<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="group-hover:block"
:class="isMobile ? 'block' : 'hidden'"
>
<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="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>
<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 class="mt-[9px]">
......
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import PageHeader from './components/mobile-page-header.vue'
......@@ -34,6 +34,10 @@ const messageList = ref<ConversationMessageItem[]>([])
const continuousQuestionStatus = ref<'default' | 'close'>('default')
const continueQuestionList = ref<string[]>([])
const isEnableDocumentParse = computed(() => {
return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y'
})
onMounted(async () => {
if (router.currentRoute.value.params.agentId) {
agentId.value = router.currentRoute.value.params.agentId as string
......@@ -113,17 +117,16 @@ function handleUpdatePageScroll() {
}
function handleClearAllMessage() {
window.$dialog.warning({
title: t('common_module.dialogue_module.clear_message_dialog_title'),
content: t('common_module.dialogue_module.clear_message_dialog_content'),
negativeText: t('common_module.cancel_btn_text'),
positiveText: t('common_module.confirm_btn_text'),
onPositiveClick: () => {
window.$message
.ctWarning(
t('common_module.dialogue_module.clear_message_dialog_content'),
t('common_module.dialogue_module.clear_message_dialog_title'),
)
.then(() => {
footerInputRef.value?.blockMessageResponse()
messageList.value = []
window.$message.success(t('common_module.clear_success_message'))
},
})
})
}
async function handleCreateContinueQuestions(replyTextContent: string) {
......@@ -172,6 +175,7 @@ function handleResetContinueQuestionList() {
:dialogs-id="dialogsId"
:agent-id="agentApplicationConfig.baseInfo.agentId"
:continuous-question-status="continuousQuestionStatus"
:is-enable-document-parse="isEnableDocumentParse"
@add-message-item="handleAddMessageItem"
@update-specify-message-item="handleUpdateSpecifyMessageItem"
@delete-last-message-item="handleDeleteLastMessageItem"
......
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import PageHeader from './components/web-page-header.vue'
......@@ -37,6 +37,10 @@ const messageList = ref<ConversationMessageItem[]>([])
const continuousQuestionStatus = ref<'default' | 'close'>('default')
const continueQuestionList = ref<string[]>([])
const isEnableDocumentParse = computed(() => {
return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y'
})
onMounted(async () => {
if (router.currentRoute.value.params.agentId) {
agentId.value = router.currentRoute.value.params.agentId as string
......@@ -133,17 +137,16 @@ function handleUpdatePageScroll() {
}
function handleClearAllMessage() {
window.$dialog.warning({
title: t('common_module.dialogue_module.clear_message_dialog_title'),
content: t('common_module.dialogue_module.clear_message_dialog_content'),
negativeText: t('common_module.cancel_btn_text'),
positiveText: t('common_module.confirm_btn_text'),
onPositiveClick: () => {
window.$message
.ctWarning(
t('common_module.dialogue_module.clear_message_dialog_content'),
t('common_module.dialogue_module.clear_message_dialog_title'),
)
.then(() => {
footerInputRef.value?.blockMessageResponse()
messageList.value = []
window.$message.success(t('common_module.clear_success_message'))
},
})
})
}
async function handleCreateContinueQuestions(replyTextContent: string) {
......@@ -210,6 +213,7 @@ function handleResetContinueQuestionList() {
:dialogs-id="dialogsId"
:agent-id="agentApplicationConfig.baseInfo.agentId"
:continuous-question-status="continuousQuestionStatus"
:is-enable-document-parse="isEnableDocumentParse"
@add-message-item="handleAddMessageItem"
@update-specify-message-item="handleUpdateSpecifyMessageItem"
@delete-last-message-item="handleDeleteLastMessageItem"
......
......@@ -89,6 +89,8 @@ declare namespace I18n {
language: string
change: string
bind: string
sms: string
verificationCode: string
dialogue_module: {
continue_question_message: string
......@@ -98,6 +100,9 @@ declare namespace I18n {
clear_message_popover_message: string
clear_message_dialog_title: string
clear_message_dialog_content: string
cancel_associate_file_tip: string
upload_file_limit: string
overwrite_file_tip: string
}
data_table_module: {
......@@ -244,6 +249,8 @@ declare namespace I18n {
knowledge: string
knowledge_base: string
knowledge_base_desc: string
upload_file: string
upload_file_desc: string
dialogue: string
preamble: string
preamble_input_placeholder: string
......@@ -466,6 +473,9 @@ declare namespace I18n {
verify_that_the_new_password_is_inconsistent_with_the_new_password: string
please_enter_the_account_nickname: 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