Commit c4f583b1 authored by nick zheng's avatar nick zheng

feat: 插件配置及使用

parent 336c88db
......@@ -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_qvvmczfd1pl.css" />
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_4711453_mmmsje9d36.css" />
<title>Model Link</title>
</head>
......
......@@ -37,6 +37,7 @@
"mitt": "^3.0.1",
"nanoid": "^5.0.7",
"pinia": "^2.2.2",
"qs": "^6.14.0",
"spark-md5": "^3.0.2",
"type-fest": "^4.26.1",
"validator": "^13.12.0",
......@@ -52,6 +53,7 @@
"@types/howler": "^2.2.12",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.16.5",
"@types/qs": "^6.9.18",
"@types/spark-md5": "^3.0.4",
"@types/validator": "^13.12.2",
"@typescript-eslint/parser": "^7.18.0",
......
This diff is collapsed.
import qs from 'qs'
import { request } from '@/utils/request'
export function fetchPluginCategoryKeyList<T>() {
......@@ -5,5 +6,14 @@ export function fetchPluginCategoryKeyList<T>() {
}
export function fetchPluginCategoryDetailInfoList<T>(payload: object) {
return request.post<T>('/bizAgentApplicationPluginRest/getList.json', null, { params: payload })
return request.post<T>(
'/bizAgentApplicationPluginRest/getList.json',
{ pagingInfo: { pageNo: 1, pageSize: 9999 } },
{
params: payload,
paramsSerializer: function (params) {
return qs.stringify(params, { arrayFormat: 'repeat' })
},
},
)
}
......@@ -66,6 +66,12 @@ export function fetchCustomEventSource(config: {
return
}
// 插件调用
if (data.function && data.function.name) {
config.onMessage(data.function)
return
}
config.onMessage(data.message)
} catch (err) {
config.onRequestError(err)
......
import { UploadFileInfo } from 'naive-ui'
import { reactive, ref } from 'vue'
import { UploadStatus } from '@/enums/upload-status'
import { fetchUpload } from '@/apis/upload'
import i18n from '@/locales'
const { t } = i18n.global
interface UploadImageItem {
id: string
url: string
status: 'pending' | 'uploading' | 'finished' | 'error' | 'removed'
}
export function useUploadImage() {
const uploadImageList = ref<UploadImageItem[]>([])
function handleLimitUploadImage(data: { file: UploadFileInfo }) {
if (data.file.file && data.file.file?.size > 1024 * 1024 * 5) {
window.$message.error(t('common_module.dialogue_module.upload_image_size_error_message'))
return false
}
const imageTypeList = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp']
if (data.file && data.file.type && !imageTypeList.includes(data.file.type)) {
window.$message.error(t('common_module.dialogue_module.upload_image_format_error_message'))
return false
}
}
function handleUploadImage(data: { file: UploadFileInfo }) {
uploadImageList.value = []
if (data.file.file) {
const formData = new FormData()
formData.append('file', data.file.file)
const currentUploadImage = reactive({
id: data.file.id,
status: UploadStatus.UPLOADING,
url: '',
})
uploadImageList.value.push(currentUploadImage)
fetchUpload<string>(formData)
.then((res) => {
if (res.code === 0) {
currentUploadImage.status = UploadStatus.FINISHED
currentUploadImage.url = res.data
}
})
.catch(() => {
currentUploadImage.status = UploadStatus.ERROR
})
}
}
function handleRemoveUploadImage(id: string) {
uploadImageList.value = uploadImageList.value.filter((fileItem) => fileItem.id !== id)
}
return {
uploadImageList,
handleLimitUploadImage,
handleUploadImage,
handleRemoveUploadImage,
}
}
......@@ -137,6 +137,9 @@ common_module:
or: 'or'
search_keyword_empty_tip: 'The search content cannot be empty'
added: 'Added'
plugin_in_progress: '{pluginName} Plugin in Progress'
plugin_executed_successfully: '{pluginName} Plugin Executed Successfully'
upload_image: 'Upload Image'
dialogue_module:
continue_question_message: 'You can keep asking questions'
......@@ -151,6 +154,10 @@ common_module:
overwrite_file_tip: 'The newly uploaded file overwrites the original file, whether to continue uploading'
stop_playing_and_then_operate: 'When the audio is playing, stop playing and then perform the operation'
do_not_operate_until_the_reply_is_complete: 'Do not operate until the reply is complete'
upload_image_limit: 'Only a single image can be uploaded in png, jpg, jpeg, webp format, up to 5MB'
overwrite_image_tip: 'The newly uploaded image overwrites the original image, whether to continue uploading'
upload_image_size_error_message: 'The size of the uploaded image cannot exceed 5 MB'
upload_image_format_error_message: 'Only png, jpg, jpeg, webp format images can be uploaded, please upload again'
data_table_module:
action: 'Controls'
......@@ -294,6 +301,11 @@ personal_space_module:
agent_system_input_placeholder: 'Please enter the task objectives that you want the character to complete, the component capabilities that they have, and the requirements and limitations on the output answers'
ability_expand: 'Capacity expansion'
plugin: 'Plugin'
plugin_desc: 'Plugin enable intelligences to call external APIs, expanding their capabilities and usage scenarios'
add_plugin: 'Add Plugin'
add_plugin_successfully: 'Plugin {0} was added successfully'
remove_plugin_successfully: 'Plugin {0} was removed successfully'
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'
......
......@@ -136,6 +136,9 @@ common_module:
or: '或'
search_keyword_empty_tip: '搜索内容不能为空'
added: '已添加'
plugin_in_progress: '{pluginName}插件执行中...'
plugin_executed_successfully: '{pluginName}插件执行成功'
upload_image: '上传图片'
dialogue_module:
continue_question_message: '你可以继续提问'
......@@ -150,6 +153,10 @@ common_module:
overwrite_file_tip: '新上传的文件会覆盖原有文件,是否继续上传'
stop_playing_and_then_operate: '音频播放中,请停止播放后再操作'
do_not_operate_until_the_reply_is_complete: '回复完成后再操作'
upload_image_limit: '仅支持上传一张图片,支持png、jpg、jpeg、webp格式,最大5MB'
overwrite_image_tip: '新上传的图片会覆盖原有图片,是否继续上传'
upload_image_size_error_message: '上传图片大小不能超过5M'
upload_image_format_error_message: '只能上传png、jpg、jpeg、webp格式图片,请重新上传'
data_table_module:
action: '操作'
......@@ -292,6 +299,11 @@ personal_space_module:
agent_system_input_placeholder: '请输入希望角色完成的任务目标、具备的组件能力以及对输出答案的要求与限制等'
ability_expand: '能力扩展'
plugin: '插件'
plugin_desc: '插件能够让智能体调用外部 API,扩展智能体的能力和使用场景'
add_plugin: '添加插件'
add_plugin_successfully: '插件 {0} 添加成功'
remove_plugin_successfully: '插件 {0} 移除成功'
knowledge: '知识'
knowledge_base: '知识库'
knowledge_base_desc: '引用文本数据、表格型知识数据(含FAQ问答,多列索引问答)以及网页数据,实现知识库问答,应用最多可关联5个知识库,请详细填写知识库描述信息以提高问答准确率'
......
......@@ -136,6 +136,9 @@ common_module:
or: '或'
search_keyword_empty_tip: '搜索內容不能為空'
added: '已添加'
plugin_in_progress: '{pluginName}插件執行中...'
plugin_executed_successfully: '{pluginName}插件執行成功'
upload_image: '上傳圖片'
dialogue_module:
continue_question_message: '你可以繼續提問'
......@@ -150,6 +153,10 @@ common_module:
overwrite_file_tip: '新上傳的文件會覆蓋原有文件,是否繼續上傳'
stop_playing_and_then_operate: '音頻播放中,請停止播放後再操作'
do_not_operate_until_the_reply_is_complete: '回覆完成後再操作'
upload_image_limit: '僅支持上傳一張圖片,支持png、jpg、jpeg、webp格式,最大5MB'
overwrite_image_tip: '新上傳的圖片會覆蓋原有圖片,是否繼續上傳'
upload_image_size_error_message: '上傳圖片大小不能超過5M'
upload_image_format_error_message: '只能上傳png、jpg、jpeg、webp格式圖片,請重新上傳'
data_table_module:
action: '操作'
......@@ -292,6 +299,11 @@ personal_space_module:
agent_system_input_placeholder: '請輸入希望角色完成的任務目標、具備的組件能力以及對輸出答案的要求與限制等'
ability_expand: '能力擴展'
plugin: '插件'
plugin_desc: '插件能夠讓智能體調用外部 API,擴展智能體的能力和使用場景'
add_plugin: '添加插件'
add_plugin_successfully: '插件 {0} 添加成功'
remove_plugin_successfully: '插件 {0} 移除成功'
knowledge: '知識'
knowledge_base: '知識庫'
knowledge_base_desc: '引用文本數據、表格型知識數據(含FAQ問答,多列索引問答)以及網頁數據,實現知識庫問答,應用最多可關聯5個知識庫,請詳細填寫知識庫描述信息以提高問答準確率'
......
<script setup lang="ts">
import { computed, nextTick, ref, shallowRef, toValue, useTemplateRef } from 'vue'
import { computed, nextTick, ref, shallowRef, toValue, useTemplateRef, watch } from 'vue'
import type { AgentApplicationRecordItem, MessageItemInterface } from '../types'
import { fetchAgentApplicationSelectList, fetchFileUpload } from '@/apis/home-agent'
import { nanoid } from 'nanoid'
import { CloseSmall } from '@icon-park/vue-next'
import fetchEventStreamSource from '../utils/fetch-event-stream-source'
import { throttle } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/store/modules/user'
import { ChannelType } from '@/enums/channel'
import { UploadStatus } from '@/enums/upload-status'
import { useUploadImage } from '@/composables/useUploadImage'
import { useSystemLanguageStore } from '@/store/modules/system-language'
interface Props {
currentSessionId: string
......@@ -36,6 +40,10 @@ const { t } = useI18n()
const inputFileRef = useTemplateRef<HTMLInputElement | null>('inputFileRef')
const userStore = useUserStore()
const systemLanguageStore = useSystemLanguageStore()
const { uploadImageList, handleLimitUploadImage, handleUploadImage, handleRemoveUploadImage } = useUploadImage()
let fileUploadController = shallowRef<AbortController | null>(null)
const isShowApplicationSelectMenu = ref(false)
......@@ -51,9 +59,20 @@ const currentInputFileInfo = ref({
})
const isQuestionSubmitBtnDisabled = computed(() => {
return questionContent.value.trim().length === 0 || isAgentResponding.value || currentInputFileInfo.value.uploading
return (
questionContent.value.trim().length === 0 ||
isAgentResponding.value ||
currentInputFileInfo.value.uploading ||
uploadImageList.value.some((imageItem) => imageItem.status !== UploadStatus.FINISHED)
)
})
const isHasUploadImage = computed(() => {
return uploadImageList.value.length > 0
})
const isEnglishLanguage = computed(() => systemLanguageStore.currentLanguageInfo.key === 'en')
;(function () {
getAgentApplicationSelectList()
})()
......@@ -66,6 +85,15 @@ const messageListScrollToBottomThrottle = throttle(
{ leading: true, trailing: true },
)
watch(
() => uploadImageList.value.length,
() => {
nextTick(() => {
emit('messageListScrollToBottom')
})
},
)
function getAgentApplicationSelectList() {
fetchAgentApplicationSelectList<AgentApplicationRecordItem[]>().then((res) => {
agentApplicationSelectList.value = res.data
......@@ -134,6 +162,7 @@ function questionSubmit() {
isAnswerLoading: false,
avatar: '',
name: '',
imageUrl: uploadImageList.value?.[0]?.url || '',
})
const agentAvatar = currentAgentApplication.value.agentAvatar
......@@ -148,6 +177,7 @@ function questionSubmit() {
isAnswerLoading: true,
avatar: agentAvatar,
name: agentName,
pluginName: '',
})
setTimeout(() => {
......@@ -168,6 +198,7 @@ function questionSubmit() {
input: questionContent.value.trim(), //提问文本
fileUrls: currentInputFileInfo.value.url ? [currentInputFileInfo.value.url] : [],
channel: ChannelType.index,
imageUrl: uploadImageList.value?.[0]?.url || '', // 图片链接
},
{
onmessage: (message) => {
......@@ -179,6 +210,16 @@ function questionSubmit() {
messageListScrollToBottomThrottle()
},
onFunction: (name) => {
emit('updateSpecifyMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!, {
pluginName: name,
})
nextTick(() => {
emit('messageListScrollToBottom')
})
},
onend: () => {
setTimeout(() => {
emit('updateSpecifyMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!, {
......@@ -206,13 +247,14 @@ function questionSubmit() {
)
questionContent.value = ''
uploadImageList.value = []
}
function handleQuestionSubmitEnter(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
if (questionContent.value.trim().length > 0 && !isAgentResponding.value && !currentInputFileInfo.value.uploading) {
if (!isQuestionSubmitBtnDisabled.value) {
questionSubmit()
}
}
......@@ -292,6 +334,17 @@ function handleFileUploadReplace() {
handleFileUploadPopup()
}
function handleSelectImage(cb: () => void) {
if (uploadImageList.value.length > 0) {
window.$message.ctWarning('', t('common_module.dialogue_module.overwrite_image_tip')).then(() => {
cb()
})
return
}
cb()
}
defineExpose({
clearSessionReferenceFile: handleFileUploadCancel,
})
......@@ -411,7 +464,7 @@ defineExpose({
class="mr-[5px] h-[18px] w-[18px] bg-[url('https://gsst-poe-sit.gz.bcebos.com/icon/doc.svg')] bg-contain bg-no-repeat"
></div>
<div class="w-[260px] text-[14px]">
<div class="text-[14px]" :class="isEnglishLanguage ? 'w-[110px]' : 'w-[210px]'">
<n-ellipsis :tooltip="{ width: 400 }">
{{ currentInputFileInfo.fileName }}
</n-ellipsis>
......@@ -443,6 +496,32 @@ defineExpose({
</div>
</Transition>
<n-upload
:show-file-list="false"
accept="image/png, image/jpeg, image/jpg, image/webp"
abstract
@before-upload="handleLimitUploadImage"
@change="handleUploadImage"
>
<n-upload-trigger #="{ handleClick }" abstract>
<n-popover style="width: 210px" trigger="hover">
<template #trigger>
<n-button
class="upload-image-btn !mr-[14px] !h-[34px] !rounded-[10px] !p-0"
@click="handleSelectImage(handleClick)"
>
<div class="box-border flex w-full items-center justify-between px-[12px]">
<i class="iconfont icon-upload-image mr-[5px] text-[14px]"></i>
<span class="text-[14px]">{{ t('common_module.upload_image') }}</span>
</div>
</n-button>
</template>
<span>{{ t('common_module.dialogue_module.upload_image_limit') }}</span>
</n-popover>
</n-upload-trigger>
</n-upload>
<n-button class="application-select-btn !h-[34px] !rounded-[10px] !p-0" @click="handleCreateNewSession">
<div class="box-border flex w-full items-center justify-between px-[12px]">
<i class="iconfont icon-session mr-[5px] text-[14px]"></i>
......@@ -460,23 +539,53 @@ defineExpose({
</div>
</div>
<div>
<div class="relative">
<n-input
v-model:value.trim="questionContent"
class="content-input"
type="textarea"
:autosize="{ minRows: 5, maxRows: 5 }"
:autosize="{ minRows: isHasUploadImage ? 3 : 5, maxRows: isHasUploadImage ? 3 : 5 }"
:class="isHasUploadImage ? 'carry-image' : ''"
:placeholder="t('home_module.please_enter_a_question')"
@keydown="handleQuestionSubmitEnter"
>
<template #suffix>
<div class="flex h-full items-end pb-[10px]">
<div :class="isHasUploadImage ? 'absolute bottom-[9px] right-[11px]' : 'flex h-full items-end pb-[10px]'">
<n-button type="primary" :disabled="isQuestionSubmitBtnDisabled" @click="() => questionSubmit()">
<i class="iconfont icon-send-icon"></i>
</n-button>
</div>
</template>
</n-input>
<div v-show="isHasUploadImage" class="absolute bottom-[9px] left-[11px] flex gap-[11px]">
<div
v-for="uploadImageItem in uploadImageList"
:key="uploadImageItem.id"
class="border-inactive-border-color relative h-[66px] w-[66px] rounded-[10px] border bg-white"
:class="{ 'border-[#F25744]!': uploadImageItem.status === UploadStatus.ERROR }"
>
<div
class="absolute right-[-4px] top-[-4px] flex h-4 w-4 cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.55)] hover:opacity-80"
@click="handleRemoveUploadImage(uploadImageItem.id)"
>
<CloseSmall theme="outline" size="16" fill="#fff" />
</div>
<div class="flex h-full w-full items-center justify-center overflow-hidden rounded-[10px]">
<n-spin v-show="uploadImageItem.status === UploadStatus.UPLOADING" :size="20" />
<n-image
v-show="uploadImageItem.status === UploadStatus.FINISHED"
width="100"
object-fit="contain"
:src="uploadImageItem.url"
preview-disabled
class="h-full w-full flex-shrink-0"
/>
</div>
</div>
</div>
</div>
<div class="mt-[20px] text-center text-[13px] text-[#999]">
......@@ -492,6 +601,11 @@ defineExpose({
}
}
:deep(.carry-image .n-input-wrapper) {
padding-bottom: 84px;
}
:deep(.upload-image-btn),
:deep(.application-select-btn) {
.n-button__border {
border-color: #9ea3ff;
......
<script setup lang="ts">
import { computed, readonly } from 'vue'
import { CheckOne } from '@icon-park/vue-next'
import type { MessageItemInterface } from '../types'
import { useUserStore } from '@/store/modules/user'
import MessageBubbleLoading from './message-bubble-loading.vue'
......@@ -38,6 +39,7 @@ const name = computed(() => {
<div class="mb-[7px] text-[12px] text-[#999]">
{{ name }}
</div>
<div
class="box-content min-h-[21px] min-w-[10px] max-w-full rounded-[10px] border border-[#9EA3FF] px-[15px] py-[14px] text-justify"
:class="{
......@@ -46,6 +48,11 @@ const name = computed(() => {
'!min-w-[80px]': messageItem.isAnswerLoading,
}"
>
<img
v-show="!isAgentMessage && messageItem.imageUrl"
:src="messageItem.imageUrl"
class="max-h-[120px]! mb-[11px] rounded-[10px] object-contain"
/>
<div
v-if="messageItem.isAnswerLoading && !messageItem.content"
class="flex h-[21px] items-center justify-center"
......@@ -65,6 +72,25 @@ const name = computed(() => {
</div>
</template>
</div>
<div
v-show="isAgentMessage && messageItem.pluginName"
class="mt-[7px] flex items-center gap-[5px] font-['Microsoft_YaHei_UI'] text-[#999]"
>
<div
v-show="!messageItem.content"
class="bg-px-plugin_loading-gif h-[14px] w-[14px] bg-contain bg-center bg-no-repeat"
/>
<CheckOne v-show="messageItem.content" theme="outline" size="16" fill="#40bd23" />
<span class="leading-5">
{{
!messageItem.content
? t('common_module.plugin_in_progress', { pluginName: messageItem.pluginName })
: t('common_module.plugin_executed_successfully', { pluginName: messageItem.pluginName })
}}
</span>
</div>
</div>
</div>
</div>
......
......@@ -30,6 +30,7 @@ const currentAgentApplication = ref<AgentApplicationRecordItem>({
agentDesc: '',
creator: '',
publishedTime: '',
points: 0,
})
const currentSessionId = ref('')
......@@ -161,6 +162,7 @@ function onGetMessageRecordList(recordId: string) {
agentTitle: string
content: string
timestamp: number
imageUrl: string
}[]
>(recordId)
.then((res) => {
......@@ -176,6 +178,7 @@ function onGetMessageRecordList(recordId: string) {
name: recordItem.agentTitle,
avatar: recordItem.agentAvatar,
timestamp: recordItem.timestamp,
imageUrl: recordItem?.imageUrl || '',
},
]
})
......
......@@ -16,4 +16,6 @@ export interface MessageItemInterface {
isAnswerLoading: boolean
avatar: string
name: string
pluginName?: string
imageUrl?: string
}
......@@ -5,6 +5,7 @@ import { languageKeyTransform } from '@/utils/language-key-transform'
import { fetchEventSource } from '@microsoft/fetch-event-source'
interface Options {
onFunction?: (name: string) => void
onmessage?: (message: string) => void
onend?: () => void
onclose?: () => void
......@@ -15,6 +16,7 @@ export default function fetchEventStreamSource(
url: string,
payload: object = {},
options: Options = {
onFunction: (_name: string) => {},
onmessage: (_message: string) => {},
onend: () => {},
onclose: () => {},
......@@ -46,6 +48,11 @@ export default function fetchEventStreamSource(
try {
const data = JSON.parse(e.data)
if (data.function && data.function.name) {
options.onFunction && options.onFunction(data.function.name)
return
}
if (data.code === 0 || data.code === '0') {
data.message && options.onmessage && options.onmessage(data.message)
} else {
......
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { nanoid } from 'nanoid'
import { throttle } from 'lodash-es'
import { CloseSmall } from '@icon-park/vue-next'
import CMessage from './c-message'
import { MessageItemInterface, MultiModelDialogueItem, QuestionMessageItem } from '../types'
import { fetchEventStreamSource } from '../utils/fetch-event-stream-source'
......@@ -10,6 +11,7 @@ import { UploadStatus } from '@/enums/upload-status'
import { ChannelType } from '@/enums/channel'
import { useDialogueFile } from '@/composables/useDialogueFile'
import { useUserStore } from '@/store/modules/user'
import { useUploadImage } from '@/composables/useUploadImage'
const { t } = useI18n()
......@@ -33,6 +35,7 @@ const emit = defineEmits<{
const userStore = useUserStore()
const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile()
const { uploadImageList, handleLimitUploadImage, handleUploadImage, handleRemoveUploadImage } = useUploadImage()
const multiModelDialogueList = defineModel<MultiModelDialogueItem[]>('multiModelDialogueList', { required: true })
......@@ -51,7 +54,10 @@ const isAllowClearAllMessage = computed(() => {
})
const isInputMessageDisabled = computed(() => {
return uploadFileList.value.some((fileItem) => fileItem.status !== UploadStatus.FINISHED)
return (
uploadFileList.value.some((fileItem) => fileItem.status !== UploadStatus.FINISHED) ||
uploadImageList.value.some((imageItem) => imageItem.status !== UploadStatus.FINISHED)
)
})
const isUploadFileDisabled = computed(() => {
......@@ -64,7 +70,14 @@ const uploadFileIcon = (type: string) => {
const messageListScrollToBottomThrottle = throttle(() => {
emit('messageListScrollToBottom')
}, 1000)
}, 300)
watch(
() => uploadImageList.value.length,
() => {
emit('messageListScrollToBottom')
},
)
function handleClearAllMessage() {
isAllowClearAllMessage.value && emit('clearAllMessage')
......@@ -79,6 +92,8 @@ function messageItemFactory() {
timestamp: Date.now(),
isTextContentLoading: false,
isAnswerResponseLoading: false,
pluginName: '',
imageUrl: '',
} as MessageItemInterface
}
......@@ -99,7 +114,11 @@ function handleQuestionSubmit() {
const questionMessageId = nanoid()
emit('addQuestionMessageItem', questionMessageId, { ...messageItemFactory(), content: questionContent.value })
emit('addQuestionMessageItem', questionMessageId, {
...messageItemFactory(),
content: questionContent.value,
imageUrl: uploadImageList.value?.[0]?.url || '',
})
emit('messageListScrollToBottom')
......@@ -115,7 +134,7 @@ function handleQuestionSubmit() {
type: 'text',
text: messageItem.content,
image_url: {
url: '',
url: messageItem?.imageUrl || '',
},
},
],
......@@ -123,7 +142,6 @@ function handleQuestionSubmit() {
})
})
questionContent.value = ''
modelItem.isAnswerResponseWait = true
const answerMessageId = nanoid()
......@@ -161,6 +179,12 @@ function handleQuestionSubmit() {
return
}
if (data && data.name) {
emit('updateMessageItem', answerMessageId, { pluginName: data.name }, modelIndex)
emit('messageListScrollToBottom')
return
}
if (data) {
messageContent += data
......@@ -189,8 +213,12 @@ function handleQuestionSubmit() {
modelItem.controller = null
modelItem.isAnswerResponseWait = false
userStore.fetchUpdateEquityInfo()
emit('messageListScrollToBottom')
},
})
questionContent.value = ''
uploadImageList.value = []
})
}
......@@ -209,6 +237,17 @@ function handleSelectFile(cb: () => void) {
cb()
}
function handleSelectUploadImage(cb: () => void) {
if (uploadImageList.value.length > 0) {
window.$message.ctWarning('', t('common_module.dialogue_module.overwrite_image_tip')).then(() => {
cb()
})
return
}
cb()
}
</script>
<template>
......@@ -232,6 +271,29 @@ function handleSelectFile(cb: () => void) {
<span class="text-xs"> {{ t('common_module.dialogue_module.clear_message_popover_message') }}</span>
</n-popover>
<n-upload
:show-file-list="false"
accept="image/png, image/jpeg, image/jpg, image/webp"
abstract
@before-upload="handleLimitUploadImage"
@change="handleUploadImage"
>
<n-upload-trigger #="{ handleClick }" abstract>
<n-popover style="width: 210px" trigger="hover">
<template #trigger>
<div
class="border-inactive-border-color text-font-color hover:text-theme-color flex h-[54px] w-[54px] shrink-0 cursor-pointer items-center justify-center rounded-full border bg-white"
@click="handleSelectUploadImage(handleClick)"
>
<i class="iconfont icon-upload-image text-xl leading-none" />
</div>
</template>
<span class="text-xs"> {{ t('common_module.dialogue_module.upload_image_limit') }} </span>
</n-popover>
</n-upload-trigger>
</n-upload>
<n-upload
:show-file-list="false"
accept=".doc, .pdf, .docx, .txt, .md"
......@@ -258,6 +320,35 @@ function handleSelectFile(cb: () => void) {
</div>
<div class="flex flex-col gap-1">
<div class="mb-1.5 flex gap-[14px]">
<div
v-for="uploadImageItem in uploadImageList"
:key="uploadImageItem.id"
class="border-inactive-border-color relative h-[66px] w-[66px] rounded-[10px] border bg-white"
:class="{ 'border-[#F25744]!': uploadImageItem.status === UploadStatus.ERROR }"
>
<div
class="absolute right-[-4px] top-[-4px] flex h-4 w-4 cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.55)] hover:opacity-80"
@click="handleRemoveUploadImage(uploadImageItem.id)"
>
<CloseSmall theme="outline" size="16" fill="#fff" />
</div>
<div class="flex h-full w-full items-center justify-center overflow-hidden rounded-[10px]">
<n-spin v-show="uploadImageItem.status === UploadStatus.UPLOADING" :size="20" />
<n-image
v-show="uploadImageItem.status === UploadStatus.FINISHED"
width="100"
object-fit="contain"
:src="uploadImageItem.url"
preview-disabled
class="h-full w-full flex-shrink-0"
/>
</div>
</div>
</div>
<ul v-show="uploadFileList.length > 0" class="mb-1.5 grid gap-1.5">
<li
v-for="uploadFileItem in uploadFileList"
......
<script setup lang="ts">
import { computed, readonly } from 'vue'
import { useI18n } from 'vue-i18n'
import { CheckOne } from '@icon-park/vue-next'
import type { MessageItemInterface } from '../types'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import MessageBubbleLoading from './message-bubble-loading.vue'
......@@ -45,6 +46,12 @@ const assistantAvatarUrl = computed(() => {
'!min-w-[80px]': messageItem.isTextContentLoading,
}"
>
<img
v-show="!isAssistant && messageItem.imageUrl"
:src="messageItem.imageUrl"
class="max-h-[120px]! mb-2 rounded-[10px] object-contain"
/>
<div v-if="messageItem.isTextContentLoading" class="flex h-[21px] w-[30px] items-center justify-center">
<MessageBubbleLoading :active-color="isAssistant ? '#fff' : '#192338'" />
</div>
......@@ -65,6 +72,25 @@ const assistantAvatarUrl = computed(() => {
</div>
</div>
</div>
<div
v-show="isAssistant && messageItem.pluginName"
class="mt-[7px] flex items-center gap-[5px] font-['Microsoft_YaHei_UI'] text-[#999]"
>
<div
v-show="messageItem.isTextContentLoading"
class="bg-px-plugin_loading-gif h-[14px] w-[14px] bg-contain bg-center bg-no-repeat"
/>
<CheckOne v-show="!messageItem.isTextContentLoading" theme="outline" size="16" fill="#40bd23" />
<span class="leading-5">
{{
messageItem.isTextContentLoading
? t('common_module.plugin_in_progress', { pluginName: messageItem.pluginName })
: t('common_module.plugin_executed_successfully', { pluginName: messageItem.pluginName })
}}
</span>
</div>
</div>
</div>
</div>
......
......@@ -162,7 +162,7 @@ async function handleGetLargeModelInfo() {
// 返回应用设置页
function handleBackAgentSetting() {
handleBlockMessageResponse()
router.replace({ name: 'PersonalAppSetting', params: { agentId: agentId.value } })
router.back()
}
// 添加模型
......
......@@ -33,6 +33,8 @@ export interface MessageItemInterface {
timestamp: number
isTextContentLoading: boolean
isAnswerResponseLoading: boolean
pluginName?: string
imageUrl?: string
}
export interface LargeModelItem {
......
......@@ -65,6 +65,12 @@ export function fetchEventStreamSource(config: {
return
}
// 插件调用
if (data.function && data.function.name) {
config.onMessage(data.function)
return
}
config.onMessage(data.message)
} catch (err) {
config.onRequestError(err)
......
<script setup lang="ts">
import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
import { computed, inject, onMounted, onUnmounted, reactive, ref, useTemplateRef, watch } from 'vue'
import { Emitter } from 'mitt'
import { useI18n } from 'vue-i18n'
import { nanoid } from 'nanoid'
import { CloseSmall } from '@icon-park/vue-next'
import OverwriteMessageTipModal from './overwrite-message-tip-modal.vue'
import { fetchCustomEventSource } from '@/composables/useEventSource'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
......@@ -12,6 +13,7 @@ import { TEXTTOSPEECH_WS_URL } from '@/config/base-url'
import WebSocketCtr from '@/utils/web-socket-ctr'
import { ChannelType } from '@/enums/channel'
import { useUserStore } from '@/store/modules/user'
import { useUploadImage } from '@/composables/useUploadImage'
interface Props {
messageList: Map<string, ConversationMessageItem>
......@@ -39,7 +41,13 @@ const emit = defineEmits<{
const personalAppConfigStore = usePersonalAppConfigStore()
const userStore = useUserStore()
const overwriteMessageTipOptions = reactive({
title: '',
content: '',
})
const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile()
const { uploadImageList, handleLimitUploadImage, handleUploadImage, handleRemoveUploadImage } = useUploadImage()
const messageTipModalRef = useTemplateRef<InstanceType<typeof OverwriteMessageTipModal>>('messageTipModalRef')
const isAnswerResponseLoading = defineModel<boolean>('isAnswerResponseLoading', { required: true })
......@@ -78,7 +86,8 @@ const isSendBtnDisabled = computed(() => {
const isInputMessageDisabled = computed(() => {
return (
uploadFileList.value.some((fileItem) => fileItem.status !== UploadStatus.FINISHED) ||
!personalAppConfigStore.baseInfo.agentId
!personalAppConfigStore.baseInfo.agentId ||
uploadImageList.value.some((imageItem) => imageItem.status !== UploadStatus.FINISHED)
)
})
......@@ -101,6 +110,13 @@ watch(
},
)
watch(
() => uploadImageList.value.length,
() => {
emit('updatePageScroll')
},
)
onUnmounted(() => {
blockMessageResponse()
emitter?.off('selectQuestion')
......@@ -125,6 +141,8 @@ function messageItemFactory(): ConversationMessageItem {
isVoicePlaying: false,
voiceFragmentUrlList: [],
isVoiceEnabled: false,
pluginName: '',
imageUrl: '',
}
}
......@@ -160,14 +178,18 @@ function handleMessageSend() {
type: string
text: string
image_url: {
url: ''
url: string
}
}[]
role: string
}[] = []
emit('updateContinuousQuestionStatus', personalAppConfigStore.commConfig.continuousQuestionStatus)
emit('addMessageItem', latestUserMessageKey, { ...messageItemFactory(), textContent: inputMessageContent.value })
emit('addMessageItem', latestUserMessageKey, {
...messageItemFactory(),
textContent: inputMessageContent.value,
imageUrl: uploadImageList.value?.[0]?.url || '',
})
emit('updatePageScroll')
props.messageList.forEach((messageItem) => {
......@@ -177,7 +199,7 @@ function handleMessageSend() {
type: 'text',
text: messageItem.textContent,
image_url: {
url: '',
url: messageItem.imageUrl || '',
},
},
],
......@@ -186,7 +208,6 @@ function handleMessageSend() {
})
isAnswerResponseLoading.value = true
inputMessageContent.value = ''
isAnswerResponseWait.value = true
currentReplyContentSentenceExtractIndex.value = 0
......@@ -237,6 +258,12 @@ function handleMessageSend() {
return
}
if (data && data.name) {
emit('updateSpecifyMessageItem', latestAssistantMessageKey, { pluginName: data.name })
emit('updatePageScroll')
return
}
if (data) {
replyTextContent += data
......@@ -269,6 +296,9 @@ function handleMessageSend() {
userStore.fetchUpdateEquityInfo()
},
})
inputMessageContent.value = ''
uploadImageList.value = []
}
function errorMessageResponse() {
......@@ -296,6 +326,21 @@ function blockMessageResponse() {
function handleSelectFile(cb: () => void) {
if (isUploadFileDisabled.value) {
overwriteMessageTipOptions.title = t('common_module.dialogue_module.overwrite_file_tip')
messageTipModalRef.value?.handleShowModal().then(() => {
cb()
})
return
}
cb()
}
function handleSelectImage(cb: () => void) {
if (uploadImageList.value.length > 0) {
overwriteMessageTipOptions.title = t('common_module.dialogue_module.overwrite_image_tip')
messageTipModalRef.value?.handleShowModal().then(() => {
cb()
})
......@@ -416,6 +461,34 @@ defineExpose({
<div class="mb-3 mt-5 px-5">
<div class="flex items-end gap-2.5">
<div class="flex-1">
<div class="mb-1.5 flex gap-[14px]">
<div
v-for="uploadImageItem in uploadImageList"
:key="uploadImageItem.id"
class="border-inactive-border-color relative h-[66px] w-[66px] rounded-[10px] border bg-white"
:class="{ 'border-[#F25744]!': uploadImageItem.status === UploadStatus.ERROR }"
>
<div
class="absolute right-[-4px] top-[-4px] flex h-4 w-4 cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.55)] hover:opacity-80"
@click="handleRemoveUploadImage(uploadImageItem.id)"
>
<CloseSmall theme="outline" size="16" fill="#fff" />
</div>
<div class="flex h-full w-full items-center justify-center overflow-hidden rounded-[10px]">
<n-spin v-show="uploadImageItem.status === UploadStatus.UPLOADING" :size="20" />
<n-image
v-show="uploadImageItem.status === UploadStatus.FINISHED"
object-fit="contain"
:src="uploadImageItem.url"
preview-disabled
class="h-full w-full flex-shrink-0"
/>
</div>
</div>
</div>
<ul v-show="uploadFileList.length > 0" class="mb-1.5 grid gap-1.5">
<li
v-for="uploadFileItem in uploadFileList"
......@@ -511,6 +584,29 @@ defineExpose({
</n-upload-trigger>
</n-upload>
<n-upload
:show-file-list="false"
accept="image/png, image/jpeg, image/jpg, image/webp"
abstract
@before-upload="handleLimitUploadImage"
@change="handleUploadImage"
>
<n-upload-trigger #="{ handleClick }" abstract>
<n-popover style="width: 210px" trigger="hover">
<template #trigger>
<div
class="h-7.5 w-7.5 hover:text-theme-color text-font-color mb-1 flex cursor-pointer items-center justify-center rounded-full bg-white"
@click="handleSelectImage(handleClick)"
>
<i class="iconfont icon-upload-image flex h-4 w-4 items-center justify-center" />
</div>
</template>
<span class="text-xs"> {{ t('common_module.dialogue_module.upload_image_limit') }} </span>
</n-popover>
</n-upload-trigger>
</n-upload>
<n-popover trigger="hover">
<template #trigger>
<div
......@@ -538,5 +634,5 @@ defineExpose({
</div>
</div>
<OverwriteMessageTipModal ref="messageTipModalRef" />
<OverwriteMessageTipModal ref="messageTipModalRef" :modal-options="overwriteMessageTipOptions" />
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { CheckOne } from '@icon-park/vue-next'
import CustomLoading from './custom-loading.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
......@@ -72,6 +73,31 @@ function handleAudioControl() {
class="min-w-[80px] max-w-full flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
:class="role === 'user' ? 'bg-[#4b87ff] text-white' : 'bg-white text-[#333]'"
>
<img
v-show="role === 'user' && messageItem.imageUrl"
:src="messageItem.imageUrl"
class="max-h-[120px]! mb-1.5 rounded-[10px] object-contain"
/>
<div
v-show="role === 'assistant' && messageItem.pluginName"
class="mb-[8px] flex items-center gap-[5px] font-['Microsoft_YaHei_UI'] text-[#999]"
>
<div
v-show="messageItem.isTextContentLoading"
class="bg-px-plugin_loading-gif h-[14px] w-[14px] bg-contain bg-center bg-no-repeat"
/>
<CheckOne v-show="!messageItem.isTextContentLoading" theme="outline" size="16" fill="#40bd23" />
<span class="leading-5">
{{
messageItem.isTextContentLoading
? t('common_module.plugin_in_progress', { pluginName: messageItem.pluginName })
: t('common_module.plugin_executed_successfully', { pluginName: messageItem.pluginName })
}}
</span>
</div>
<div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4">
<CustomLoading />
</div>
......
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
interface ModalOptionInterface {
title: string
content: string
}
interface Props {
modalOptions: ModalOptionInterface
}
defineProps<Props>()
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) => {}
......
......@@ -23,6 +23,7 @@ import {
} from '@/apis/agent-application'
import { fetchCustomEventSource } from '@/composables/useEventSource'
import AgentModelSetting from './components/agent-model-setting.vue'
import AgentPlugin from './components/agent-plugin.vue'
import AgentAssociatedKnowledge from './components/agent-associated-knowledge.vue'
import AgentMemorySetting from './components/agent-memory-setting.vue'
import AgentDialogueSetting from './components/agent-dialogue-setting.vue'
......@@ -35,7 +36,7 @@ const router = useRouter()
const personalAppConfigStore = usePersonalAppConfigStore()
const userStore = useUserStore()
const { baseInfo, commConfig, commModelConfig, knowledgeConfig } = storeToRefs(personalAppConfigStore)
const { baseInfo, commConfig, commModelConfig, knowledgeConfig, unitIds } = storeToRefs(personalAppConfigStore)
const emitter = inject<Emitter<MittEvents>>('emitter')
......@@ -378,7 +379,7 @@ async function handleEquityInfoValidate() {
</script>
<template>
<div class="flex h-full flex-[3_3_0%] flex-col">
<div class="flex h-full flex-[3_3_0%] flex-col overflow-hidden">
<div
class="flex h-[56px] w-full items-center justify-between border-r border-[#e8e9eb] px-5 text-[#333] shadow-[inset_0_-1px_#e8e9eb]"
>
......@@ -605,6 +606,8 @@ async function handleEquityInfoValidate() {
</div>
<div class="flex-1 overflow-auto">
<AgentPlugin v-model:unit-ids="unitIds" />
<AgentAssociatedKnowledge v-model:knowledge-config="knowledgeConfig" />
<AgentMemorySetting
......
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { Plus, RightOne } from '@icon-park/vue-next'
import { useI18n } from 'vue-i18n'
import AddAgentPluginModal from './add-agent-plugin-modal.vue'
import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { fetchPluginCategoryDetailInfoList } from '@/apis/plugin-center'
interface PluginItemInterface {
pluginId: string
title: string
points: number
description: string
icon: string
}
const { t } = useI18n()
const unitIds = defineModel<PersonalAppConfigState['unitIds']>('unitIds', { required: true })
const pluginExpandedNames = ref<string[]>([])
const selectedPluginList = ref<PluginItemInterface[]>([])
const isShowAddAgentPluginModal = ref(false)
const hoverUnitId = ref('')
const defaultPluginIconUrl = 'https://gsst-poe-sit.gz.bcebos.com/data/20250115/1736924150557.png'
const isHoverPluginItem = computed(() => (pluginId: string) => {
return hoverUnitId.value === pluginId
})
watch(
() => unitIds.value,
async (newUnitIds) => {
if (newUnitIds.length > 0) {
await handleGetPluginList()
pluginExpandedNames.value = ['plugin']
}
},
{ once: true, immediate: true },
)
function handleUpdatePluginExpandedNames(expandedNames: string[]) {
pluginExpandedNames.value = expandedNames
}
function handleShowAddAgentPluginModel() {
isShowAddAgentPluginModal.value = true
}
async function handleGetPluginList() {
const payload = {
classification: '',
pluginIds: unitIds.value,
}
const res = await fetchPluginCategoryDetailInfoList<{ pluginInfos: PluginItemInterface[] }[]>(payload)
if (res.code === 0) {
selectedPluginList.value = []
res.data.forEach((pluginClassificationItem) => {
pluginClassificationItem.pluginInfos.forEach((pluginItem) => {
selectedPluginList.value.push(pluginItem)
})
})
}
}
function handleMouseoverPluginItem(pluginId: string) {
hoverUnitId.value = pluginId
}
function handleMouseleavePluginItem() {
hoverUnitId.value = ''
}
function handleDeleteUnitId(pluginId: string) {
unitIds.value = unitIds.value.filter((id) => id !== pluginId)
selectedPluginList.value = selectedPluginList.value.filter((pluginItem) => pluginItem.pluginId !== pluginId)
}
async function handleCloseAgentPluginModal() {
if (!unitIds.value.length) {
selectedPluginList.value = []
return
}
await handleGetPluginList()
pluginExpandedNames.value = ['plugin']
}
</script>
<template>
<section class="border-b border-[#e8e9eb] px-5">
<div class="pt-4">
<h2 class="mb-3 text-[#84868c]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.plugin') }}
</h2>
<NCollapse
:expanded-names="pluginExpandedNames"
:trigger-areas="['main', 'arrow']"
@update:expanded-names="handleUpdatePluginExpandedNames"
>
<template #arrow>
<RightOne theme="filled" size="17" fill="#333" :stroke-width="3" />
</template>
<NCollapseItem
:title="t('personal_space_module.agent_module.agent_setting_module.agent_config_module.plugin')"
name="plugin"
class="my-[13px]!"
>
<template #header-extra>
<NTooltip trigger="hover">
<template #trigger>
<Plus
theme="outline"
size="22"
:stroke-width="3"
class="text-theme-color cursor-pointer"
@click="handleShowAddAgentPluginModel"
/>
</template>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.add_plugin') }}
</NTooltip>
</template>
<ul v-show="selectedPluginList.length" class="flex flex-col gap-2">
<li
v-for="pluginItem in selectedPluginList"
:key="pluginItem.pluginId"
class="flex h-[66px] items-center gap-[12px] overflow-hidden rounded-[10px] border border-[#f2f5f9] px-4 hover:bg-[#f2f5f9]"
@mouseover="handleMouseoverPluginItem(pluginItem.pluginId)"
@mouseleave="handleMouseleavePluginItem"
>
<div
class="h-[36px] w-[36px] flex-shrink-0 bg-contain bg-center bg-no-repeat"
:style="
pluginItem.icon
? { backgroundImage: `url(${pluginItem.icon})` }
: { backgroundImage: `url(${defaultPluginIconUrl})` }
"
/>
<div class="flex h-full flex-1 flex-col justify-center overflow-hidden">
<div class="w-full text-[14px]">
<n-ellipsis class="text-font-color w-full" :tooltip="{ 'content-style': 'max-width: 600px;' }">
{{ pluginItem.title }}
</n-ellipsis>
</div>
<div class="w-full text-[12px]">
<n-ellipsis class="text-gray-font-color w-full" :tooltip="{ 'content-style': 'max-width: 600px;' }">
{{ pluginItem.description }}
</n-ellipsis>
</div>
</div>
<n-tooltip placement="top">
<template #trigger>
<div
v-show="isHoverPluginItem(pluginItem.pluginId)"
class="flex cursor-pointer items-center justify-center"
@click="handleDeleteUnitId(pluginItem.pluginId)"
>
<i class="hover:text-error-font-color text-font-color iconfont icon-reduce outline-none" />
</div>
</template>
<span>{{ t('common_module.remove') }}</span>
</n-tooltip>
</li>
</ul>
<div v-show="selectedPluginList.length === 0">
<span class="text-xs text-[#84868c]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.plugin_desc') }}
</span>
</div>
</NCollapseItem>
</NCollapse>
</div>
</section>
<AddAgentPluginModal
v-model:is-show-modal="isShowAddAgentPluginModal"
v-model:plugin-id-list="unitIds"
:modal-title="t('personal_space_module.agent_module.agent_setting_module.agent_config_module.add_plugin')"
@close="handleCloseAgentPluginModal"
/>
</template>
......@@ -102,7 +102,7 @@ onMounted(() => {
})
function handleBackPreviousPage() {
router.replace({ name: 'PersonalSpaceApp' })
router.back()
}
function handleDropdownSelect(key: string) {
......
<script setup lang="ts">
import { computed, inject, onMounted, onUnmounted, ref } from 'vue'
import { computed, inject, onMounted, onUnmounted, ref, watch } from 'vue'
import { Emitter } from 'mitt'
import { useI18n } from 'vue-i18n'
import { nanoid } from 'nanoid'
import { useRoute } from 'vue-router'
import { CloseSmall } from '@icon-park/vue-next'
import { fetchCustomEventSource } from '@/composables/useEventSource'
import { useUserStore } from '@/store/modules/user'
import { UploadStatus } from '@/enums/upload-status'
......@@ -12,6 +13,7 @@ import { useLayoutConfig } from '@/composables/useLayoutConfig'
import { TEXTTOSPEECH_WS_URL } from '@/config/base-url'
import WebSocketCtr from '@/utils/web-socket-ctr'
import { ChannelType } from '@/enums/channel'
import { useUploadImage } from '@/composables/useUploadImage'
interface Props {
agentId: string
......@@ -49,6 +51,7 @@ const { isMobile } = useLayoutConfig()
const userStore = useUserStore()
const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = useDialogueFile()
const { uploadImageList, handleLimitUploadImage, handleUploadImage, handleRemoveUploadImage } = useUploadImage()
const isAnswerResponseLoading = defineModel<boolean>('isAnswerResponseLoading', { required: true })
......@@ -87,13 +90,23 @@ const isCreateContinueQuestions = computed(() => {
})
const isInputMessageDisabled = computed(() => {
return uploadFileList.value.some((fileItem) => fileItem.status !== UploadStatus.FINISHED)
return (
uploadFileList.value.some((fileItem) => fileItem.status !== UploadStatus.FINISHED) ||
uploadImageList.value.some((imageItem) => imageItem.status !== UploadStatus.FINISHED)
)
})
const isUploadFileDisabled = computed(() => {
return uploadFileList.value.length === 1
})
watch(
() => uploadImageList.value.length,
() => {
emit('updatePageScroll')
},
)
const uploadFileIcon = (type: string) => {
return `https://gsst-poe-sit.gz.bcebos.com/icon/${type}.svg`
}
......@@ -126,6 +139,8 @@ function messageItemFactory(): ConversationMessageItem {
isVoiceLoading: false,
isVoicePlaying: false,
voiceFragmentUrlList: [],
pluginName: '',
imageUrl: '',
}
}
......@@ -162,7 +177,11 @@ function handleMessageSend() {
currentLatestMessageItemKeyMap.value.set('assistant', latestAssistantMessageKey)
emit('resetContinueQuestionList')
emit('addMessageItem', latestUserMessageKey, { ...messageItemFactory(), textContent: inputMessageContent.value })
emit('addMessageItem', latestUserMessageKey, {
...messageItemFactory(),
textContent: inputMessageContent.value,
imageUrl: uploadImageList.value?.[0]?.url || '',
})
emit('updatePageScroll')
emit('addMessageItem', latestAssistantMessageKey, {
......@@ -174,12 +193,9 @@ function handleMessageSend() {
})
emit('updatePageScroll')
const input = inputMessageContent.value
let replyTextContent = ''
isAnswerResponseLoading.value = true
isAnswerResponseWait.value = true
inputMessageContent.value = ''
currentReplyContentSentenceExtractIndex.value = 0
sentenceFragmentSerialNo.value = 0
sentenceExtractCheckEnabled.value = false
......@@ -195,8 +211,9 @@ function handleMessageSend() {
agentId: props.agentId,
dialogsId: props.dialogsId,
fileUrls: uploadFileList.value.map((item) => item.url),
input,
input: inputMessageContent.value,
channel: query.channel || ChannelType.link_share,
imageUrl: uploadImageList.value?.[0]?.url || '',
},
controller,
onMessage: (data: any) => {
......@@ -214,6 +231,12 @@ function handleMessageSend() {
return
}
if (data && data.name) {
emit('updateSpecifyMessageItem', latestAssistantMessageKey, { pluginName: data.name })
emit('updatePageScroll')
return
}
if (data) {
replyTextContent += data
......@@ -246,6 +269,9 @@ function handleMessageSend() {
userStore.fetchUpdateEquityInfo()
},
})
inputMessageContent.value = ''
uploadImageList.value = []
}
function errorMessageResponse() {
......@@ -286,6 +312,17 @@ function handleSelectFile(cb: () => void) {
cb()
}
function handleSelectImage(cb: () => void) {
if (uploadImageList.value.length > 0) {
window.$message.ctWarning('', t('common_module.dialogue_module.overwrite_file_tip')).then(() => {
cb()
})
return
}
cb()
}
function sentenceExtract(messageId: string) {
const symbolRegExp = /[。!?;.!?;]/g
let sentenceDraft = assistantFullAnswerContent.value
......@@ -397,6 +434,34 @@ defineExpose({
<div class="my-5">
<div class="flex items-end gap-2.5">
<div class="flex flex-1 flex-col">
<div class="mb-1.5 flex gap-[14px]">
<div
v-for="uploadImageItem in uploadImageList"
:key="uploadImageItem.id"
class="border-inactive-border-color relative h-[66px] w-[66px] rounded-[10px] border bg-white"
:class="{ 'border-[#F25744]!': uploadImageItem.status === UploadStatus.ERROR }"
>
<div
class="absolute right-[-4px] top-[-4px] flex h-4 w-4 cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.55)] hover:opacity-80"
@click="handleRemoveUploadImage(uploadImageItem.id)"
>
<CloseSmall theme="outline" size="16" fill="#fff" />
</div>
<div class="flex h-full w-full items-center justify-center overflow-hidden rounded-[10px]">
<n-spin v-show="uploadImageItem.status === UploadStatus.UPLOADING" :size="20" />
<n-image
v-show="uploadImageItem.status === UploadStatus.FINISHED"
object-fit="contain"
:src="uploadImageItem.url"
preview-disabled
class="h-full w-full flex-shrink-0"
/>
</div>
</div>
</div>
<ul v-show="uploadFileList.length > 0" class="mb-1.5 grid gap-1.5">
<li
v-for="uploadFileItem in uploadFileList"
......@@ -509,6 +574,29 @@ defineExpose({
</n-upload-trigger>
</n-upload>
<n-upload
:show-file-list="false"
accept="image/png, image/jpeg, image/jpg, image/webp"
abstract
@before-upload="handleLimitUploadImage"
@change="handleUploadImage"
>
<n-upload-trigger #="{ handleClick }" abstract>
<n-popover style="width: 210px" trigger="hover">
<template #trigger>
<div
class="h-7.5 w-7.5 hover:text-theme-color text-font-color mb-1 flex cursor-pointer items-center justify-center rounded-full bg-white"
@click="handleSelectImage(handleClick)"
>
<i class="iconfont icon-upload-image flex h-4 w-4 items-center justify-center" />
</div>
</template>
<span class="text-xs"> {{ t('common_module.dialogue_module.upload_image_limit') }} </span>
</n-popover>
</n-upload-trigger>
</n-upload>
<n-popover trigger="hover">
<template #trigger>
<div
......
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { CheckOne } from '@icon-park/vue-next'
import CustomLoading from './custom-loading.vue'
import MusicWavesLoading from './music-waves-loading.vue'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
......@@ -101,6 +102,31 @@ function handleAudioControl() {
isMobile ? 'max-w-[calc(100%-20px)]' : 'max-w-full',
]"
>
<img
v-show="role === 'user' && messageItem.imageUrl"
:src="messageItem.imageUrl"
class="max-h-[120px]! mb-1.5 rounded-[10px] object-contain"
/>
<div
v-show="role === 'assistant' && messageItem.pluginName"
class="mb-[8px] flex items-center gap-[5px] font-['Microsoft_YaHei_UI'] text-[#999]"
>
<div
v-show="messageItem.isTextContentLoading"
class="bg-px-plugin_loading-gif h-[14px] w-[14px] bg-contain bg-center bg-no-repeat"
/>
<CheckOne v-show="!messageItem.isTextContentLoading" theme="outline" size="16" fill="#40bd23" />
<span class="leading-5">
{{
messageItem.isTextContentLoading
? t('common_module.plugin_in_progress', { pluginName: messageItem.pluginName })
: t('common_module.plugin_executed_successfully', { pluginName: messageItem.pluginName })
}}
</span>
</div>
<div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4">
<CustomLoading />
</div>
......
......@@ -9,4 +9,6 @@ declare interface ConversationMessageItem {
isVoicePlaying: boolean
voiceFragmentUrlList: string[]
isVoiceEnabled?: boolean
pluginName?: string
imageUrl?: string
}
......@@ -136,6 +136,9 @@ declare namespace I18n {
or: string
search_keyword_empty_tip: string
added: string
plugin_in_progress: string
plugin_executed_successfully: string
upload_image: string
dialogue_module: {
continue_question_message: string
......@@ -150,6 +153,10 @@ declare namespace I18n {
overwrite_file_tip: string
stop_playing_and_then_operate: string
do_not_operate_until_the_reply_is_complete: string
upload_image_limit: string
overwrite_image_tip: string
upload_image_size_error_message: string
upload_image_format_error_message: string
}
data_table_module: {
......@@ -293,6 +300,11 @@ declare namespace I18n {
agent_system_input_placeholder: string
ability_expand: string
plugin: string
plugin_desc: string
add_plugin: string
add_plugin_successfully: string
remove_plugin_successfully: string
knowledge: string
knowledge_base: string
knowledge_base_desc: 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