Commit b9b62616 authored by nick zheng's avatar nick zheng

feat: 支持上传文件进行对话

parent f3d95013
......@@ -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>
......
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 } : {}),
})
}
<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">
......
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',
}
......@@ -100,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'
......@@ -216,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'
......@@ -246,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'
......
......@@ -99,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: '操作'
......@@ -244,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: '请输入开场白'
......
......@@ -99,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: '操作'
......@@ -244,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: '請輸入開場白'
......
......@@ -24,6 +24,7 @@ export function defaultPersonalAppConfigState(): PersonalAppConfigState {
},
knowledgeConfig: {
knowledgeIds: [],
isDocumentParsing: 'N',
},
commModelConfig: {
largeModel: '文心4.0 (8K)',
......
......@@ -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"
......
......@@ -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"
......
......@@ -100,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: {
......@@ -246,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
......
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