Commit edafe38c authored by nick zheng's avatar nick zheng

feat: agent应用智能表单(出差表单)

parent f36f9815
...@@ -24,6 +24,7 @@ export default [ ...@@ -24,6 +24,7 @@ export default [
AnyObject: 'readonly', AnyObject: 'readonly',
KnowledgeContentResultItem: 'readonly', KnowledgeContentResultItem: 'readonly',
DBChainResultItem: 'readonly', DBChainResultItem: 'readonly',
PluginDisplayFormatType: 'readonly',
ConversationMessageItem: 'readonly', ConversationMessageItem: 'readonly',
ConversationMessageItemInfo: 'readonly', ConversationMessageItemInfo: 'readonly',
MittEvents: 'readonly', MittEvents: 'readonly',
......
...@@ -7,7 +7,7 @@ import MarkdownRender from '@/components/markdown-render/markdown-render.vue' ...@@ -7,7 +7,7 @@ import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import { useLayoutConfig } from '@/composables/useLayoutConfig' import { useLayoutConfig } from '@/composables/useLayoutConfig'
interface Props { interface Props {
displayFormat: 'json' | 'markdown' | 'none' displayFormat: PluginDisplayFormatType
name: string name: string
arguments: string arguments: string
content: string content: string
...@@ -59,11 +59,12 @@ const parsedMarkdownPluginContent = computed(() => { ...@@ -59,11 +59,12 @@ const parsedMarkdownPluginContent = computed(() => {
<template> <template>
<div <div
class="flex w-full flex-col bg-[#f3f5f9]" class="flex w-full flex-col bg-[#f3f5f9]"
:class=" :class="isMobile ? 'rounded-[2.66667vw] py-[2.66667vw] text-[3.2vw]' : 'rounded-[10px] py-[10px]'"
isMobile ? 'rounded-[2.66667vw] px-[3.2vw] py-[2.66667vw] text-[3.2vw]' : 'rounded-[10px] px-[12px] py-[10px]'
"
> >
<div class="flex flex-1 items-center justify-between gap-[10px] overflow-hidden"> <div
class="flex flex-1 items-center justify-between gap-[10px] overflow-hidden"
:class="isMobile ? 'px-[3.2vw]' : 'px-[12px]'"
>
<div class="flex flex-1 items-center gap-[6px] overflow-hidden"> <div class="flex flex-1 items-center gap-[6px] overflow-hidden">
<div <div
v-show="pluginLoading" v-show="pluginLoading"
...@@ -91,53 +92,55 @@ const parsedMarkdownPluginContent = computed(() => { ...@@ -91,53 +92,55 @@ const parsedMarkdownPluginContent = computed(() => {
/> />
</div> </div>
<div <n-scrollbar style="max-height: 500px" :class="{ 'mt-[10px]': !isFolding }">
class="flex transform flex-col overflow-x-auto duration-200 ease-in-out" <div
:class="isFolding ? 'h-0 overflow-hidden' : 'h-fit'" class="flex transform flex-col overflow-x-auto duration-200 ease-in-out"
> :class="[isFolding ? 'h-0 overflow-hidden' : 'h-fit', isMobile ? 'px-[3.2vw]' : 'px-[12px]']"
<!-- json格式 --> >
<div v-if="displayFormat === 'json'"> <!-- json格式 -->
<div class="mt-[10px]"> <div v-if="displayFormat === 'json'">
<span <div>
class="rounded-theme inline-block bg-[#E8EAFF] text-[#0072FF]" <span
:class=" class="rounded-theme inline-block bg-[#E8EAFF] text-[#0072FF]"
isMobile :class="
? 'h-[7.2vw] px-[3.2vw] text-[2.93333vw] leading-[7.2vw]' isMobile
: 'h-[29px] px-[12px] text-[12px] leading-[29px]' ? 'h-[7.2vw] px-[3.2vw] text-[2.93333vw] leading-[7.2vw]'
" : 'h-[29px] px-[12px] text-[12px] leading-[29px]'
> "
{{ t('common_module.input_parameter') }} >
</span> {{ t('common_module.input_parameter') }}
<JsonViewer :value="parsedPluginArguments" :expand-depth="3" /> </span>
<JsonViewer :value="parsedPluginArguments" :expand-depth="3" />
</div>
<div class="mt-[10px]">
<span
class="rounded-theme inline-block bg-[#E8EAFF] text-[#0072FF]"
:class="
isMobile
? 'h-[7.2vw] px-[3.2vw] text-[2.93333vw] leading-[7.2vw]'
: 'h-[29px] px-[12px] text-[12px] leading-[29px]'
"
>
{{ t('common_module.output_parameter') }}
</span>
<JsonViewer :value="parsedPluginContent" :expand-depth="3" />
</div>
</div> </div>
<div class="mt-[10px]"> <!-- markdown格式 -->
<span <div v-else-if="displayFormat === 'markdown'">
class="rounded-theme inline-block bg-[#E8EAFF] text-[#0072FF]" <div>
:class=" <MarkdownRender :raw-text-content="parsedMarkdownPluginContent" :font-size="isMobile ? '3.2vw' : '14px'" />
isMobile </div>
? 'h-[7.2vw] px-[3.2vw] text-[2.93333vw] leading-[7.2vw]'
: 'h-[29px] px-[12px] text-[12px] leading-[29px]'
"
>
{{ t('common_module.output_parameter') }}
</span>
<JsonViewer :value="parsedPluginContent" :expand-depth="3" />
</div> </div>
</div>
<!-- markdown格式 --> <!-- none格式 -->
<div v-else-if="displayFormat === 'markdown'"> <div v-else-if="displayFormat === 'none'">
<div class="mt-[10px]"> <span class="text-font-color break-all">{{ content }}</span>
<MarkdownRender :raw-text-content="parsedMarkdownPluginContent" :font-size="isMobile ? '3.2vw' : '14px'" />
</div> </div>
</div> </div>
</n-scrollbar>
<!-- 其他格式 -->
<div v-else>
<span class="mt-[10px] break-all">{{ content }}</span>
</div>
</div>
</div> </div>
</template> </template>
......
...@@ -8,7 +8,12 @@ import { languageKeyTransform } from '@/utils/language-key-transform' ...@@ -8,7 +8,12 @@ import { languageKeyTransform } from '@/utils/language-key-transform'
interface ResponseData { interface ResponseData {
message: string message: string
reasoningContent: string reasoningContent: string
function: { displayFormat: 'json' | 'markdown' | 'none'; name: string; result: string; arguments: string } function: {
displayFormat: PluginDisplayFormatType
name: string
result: string
arguments: string
}
knowledgeContentResult: KnowledgeContentResultItem[] knowledgeContentResult: KnowledgeContentResultItem[]
dbChainResult: DBChainResultItem[] dbChainResult: DBChainResultItem[]
} }
......
...@@ -5,12 +5,14 @@ import { nanoid } from 'nanoid' ...@@ -5,12 +5,14 @@ import { nanoid } from 'nanoid'
import { throttle } from 'lodash-es' import { throttle } from 'lodash-es'
import { CloseSmall } from '@icon-park/vue-next' import { CloseSmall } from '@icon-park/vue-next'
import CMessage from './c-message' import CMessage from './c-message'
import { MessageItemInterface, MultiModelDialogueItem, QuestionMessageItem } from '../types' import { MessageItemInterface, MultiModelDialogueItem, QuestionMessageItem, SmartFormTypes } from '../types'
import { fetchEventStreamSource } from '../utils/fetch-event-stream-source' import { fetchEventStreamSource } from '../utils/fetch-event-stream-source'
import { UploadStatus } from '@/enums/upload-status' import { UploadStatus } from '@/enums/upload-status'
import { ChannelType } from '@/enums/channel' import { ChannelType } from '@/enums/channel'
import { useDialogueFile } from '@/composables/useDialogueFile' import { useDialogueFile } from '@/composables/useDialogueFile'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { smartFormTypeConverter } from './smart-forms/utils/smart-forms'
import { SmartFormDisplayFormat } from './smart-forms/types/types'
const { t } = useI18n() const { t } = useI18n()
...@@ -29,6 +31,7 @@ const emit = defineEmits<{ ...@@ -29,6 +31,7 @@ const emit = defineEmits<{
deleteMessageItem: [messageId: string, index: number] deleteMessageItem: [messageId: string, index: number]
messageListScrollToBottom: [autoScrollBottom?: boolean] messageListScrollToBottom: [autoScrollBottom?: boolean]
clearAllMessage: [] clearAllMessage: []
smartFormsStatusFreezeCheck: [value: SmartFormTypes, index: number]
}>() }>()
const userStore = useUserStore() const userStore = useUserStore()
...@@ -205,6 +208,35 @@ function handleQuestionSubmit() { ...@@ -205,6 +208,35 @@ function handleQuestionSubmit() {
// 插件 // 插件
if (data.function && data.function.name) { if (data.function && data.function.name) {
// 表单插件,展示表单内容
if (['travelForm'].includes(data.function.displayFormat)) {
emit(
'smartFormsStatusFreezeCheck',
smartFormTypeConverter(data.function.displayFormat as SmartFormDisplayFormat),
modelIndex,
)
emit(
'updateMessageItem',
answerMessageId,
{
pluginResult: {
displayFormat: data.function.displayFormat,
pluginName: data.function.name,
arguments: data.function.arguments,
pluginContent: data.function.result,
},
smartFormInfo: {
type: smartFormTypeConverter(data.function.displayFormat as SmartFormDisplayFormat),
isDisabled: false,
params: data.function.result,
},
},
modelIndex,
)
emit('messageListScrollToBottom')
return
}
emit( emit(
'updateMessageItem', 'updateMessageItem',
answerMessageId, answerMessageId,
......
<script setup lang="ts"> <script setup lang="ts">
import { computed, readonly, ref } from 'vue' import { computed, provide, readonly, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Down } from '@icon-park/vue-next' import { Down } from '@icon-park/vue-next'
import type { MessageItemInterface } from '../types' import type { MessageItemInterface } from '../types'
...@@ -7,9 +7,11 @@ import MarkdownRender from '@/components/markdown-render/markdown-render.vue' ...@@ -7,9 +7,11 @@ import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import ExecuteCodeRender from '@/components/execute-code-render/execute-code-render.vue' import ExecuteCodeRender from '@/components/execute-code-render/execute-code-render.vue'
import ExecutePluginRender from '@/components/execute-plugin-render/execute-plugin-render.vue' import ExecutePluginRender from '@/components/execute-plugin-render/execute-plugin-render.vue'
import MessageBubbleLoading from './message-bubble-loading.vue' import MessageBubbleLoading from './message-bubble-loading.vue'
import SmartForms from './smart-forms/index.vue'
interface Props { interface Props {
messageItem: MessageItemInterface messageItem: MessageItemInterface
messageItemId: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
...@@ -20,14 +22,33 @@ const emit = defineEmits<{ ...@@ -20,14 +22,33 @@ const emit = defineEmits<{
const { t } = useI18n() const { t } = useI18n()
provide('messageItemId', props.messageItemId)
const isShowReasoningContent = ref(true) const isShowReasoningContent = ref(true)
const agentAvatarUrl = readonly({ url: 'https://gsst-poe-sit.gz.bcebos.com/icon/agent-avatar.png' }) const agentAvatarUrl = readonly({ url: 'https://gsst-poe-sit.gz.bcebos.com/icon/agent-avatar.png' })
const isAssistant = computed(() => { const displaySmartFormsList = readonly(['travelForm'])
const isAgentMessage = computed(() => {
return props.messageItem.role === 'assistant' return props.messageItem.role === 'assistant'
}) })
const currentBubbleTextColor = computed(() => {
return isAgentMessage.value ? '#192338' : '#fff'
})
const messageAuthor = computed(() => {
return props.messageItem.nickName
})
const isShowSmartForms = computed(() => {
if (props.messageItem?.pluginResult?.displayFormat) {
return displaySmartFormsList.includes(props.messageItem?.pluginResult?.displayFormat)
}
return false
})
const assistantAvatarUrl = computed(() => { const assistantAvatarUrl = computed(() => {
return props.messageItem.avatar || agentAvatarUrl.url return props.messageItem.avatar || agentAvatarUrl.url
}) })
...@@ -42,12 +63,13 @@ function handleShowReasoningContentSwitch() { ...@@ -42,12 +63,13 @@ function handleShowReasoningContentSwitch() {
<div class="flex"> <div class="flex">
<img <img
class="h-[36px] w-[36px] flex-shrink-0 rounded-full object-cover" class="h-[36px] w-[36px] flex-shrink-0 rounded-full object-cover"
:src="isAssistant ? assistantAvatarUrl : props.messageItem.avatar" :src="isAgentMessage ? assistantAvatarUrl : props.messageItem.avatar"
alt="Avatar" alt="Avatar"
/> />
<div class="ml-[11px] flex flex-col items-start overflow-hidden"> <div v-if="!isShowSmartForms" class="ml-[11px] flex flex-col items-start overflow-hidden">
<template v-if="isAssistant && messageItem.nickName === 'DeepSeek'"> <!-- DeepSeek深度思考 -->
<template v-if="isAgentMessage && messageItem.nickName === 'DeepSeek'">
<div class="mb-[7px] select-none text-[14px]"> <div class="mb-[7px] select-none text-[14px]">
<div class="inline-flex cursor-pointer" @click="handleShowReasoningContentSwitch"> <div class="inline-flex cursor-pointer" @click="handleShowReasoningContentSwitch">
<span v-if="messageItem.isTextContentLoading" class="mr-[6px]"> <span v-if="messageItem.isTextContentLoading" class="mr-[6px]">
...@@ -89,19 +111,19 @@ function handleShowReasoningContentSwitch() { ...@@ -89,19 +111,19 @@ function handleShowReasoningContentSwitch() {
<div <div
class="min-h-[21px] w-full flex-wrap rounded-[10px] border border-[#9EA3FF] px-[15px] py-[12px] text-justify" class="min-h-[21px] w-full flex-wrap rounded-[10px] border border-[#9EA3FF] px-[15px] py-[12px] text-justify"
:class="{ :class="{
'bg-[#777EF9]': isAssistant, 'bg-[#777EF9]': !isAgentMessage,
'text-[#fff]': isAssistant, 'text-[#fff]': !isAgentMessage,
'!min-w-[80px]': messageItem.isTextContentLoading, '!min-w-[80px]': messageItem.isTextContentLoading,
}" }"
> >
<img <img
v-show="!isAssistant && messageItem.imageUrl" v-show="!isAgentMessage && messageItem.imageUrl"
:src="messageItem.imageUrl" :src="messageItem.imageUrl"
class="max-h-[120px]! mb-[12px] rounded-[10px] object-contain" class="max-h-[120px]! mb-[12px] rounded-[10px] object-contain"
/> />
<!-- 插件返回结果 --> <!-- 插件返回结果 -->
<div v-show="isAssistant && messageItem?.pluginResult?.pluginName" class="mb-[11px] w-full"> <div v-show="isAgentMessage && messageItem?.pluginResult?.pluginName" class="mb-[11px] w-full">
<ExecutePluginRender <ExecutePluginRender
:display-format="messageItem.pluginResult?.displayFormat || 'none'" :display-format="messageItem.pluginResult?.displayFormat || 'none'"
:name="messageItem.pluginResult?.pluginName!" :name="messageItem.pluginResult?.pluginName!"
...@@ -111,8 +133,8 @@ function handleShowReasoningContentSwitch() { ...@@ -111,8 +133,8 @@ function handleShowReasoningContentSwitch() {
/> />
</div> </div>
<div v-if="messageItem.isTextContentLoading" class="flex px-4 py-1.5"> <div v-if="messageItem.isTextContentLoading" class="flex px-4 py-[7.5px]">
<MessageBubbleLoading :active-color="isAssistant ? '#fff' : '#192338'" /> <MessageBubbleLoading :active-color="currentBubbleTextColor" />
</div> </div>
<div v-else> <div v-else>
...@@ -128,21 +150,21 @@ function handleShowReasoningContentSwitch() { ...@@ -128,21 +150,21 @@ function handleShowReasoningContentSwitch() {
:raw-text-content=" :raw-text-content="
messageItem.content ? messageItem.content : t('common_module.dialogue_module.empty_message_content') messageItem.content ? messageItem.content : t('common_module.dialogue_module.empty_message_content')
" "
:color="isAssistant ? '#fff' : '#192338'" :color="currentBubbleTextColor"
/> />
<div <div
v-show="isAssistant && messageItem.isAnswerResponseLoading" v-show="isAgentMessage && messageItem.isAnswerResponseLoading"
class="mt-2.5 flex h-[21px] w-[30px] items-center justify-center" class="mt-2.5 flex h-[21px] w-[30px] items-center justify-center"
> >
<MessageBubbleLoading :active-color="isAssistant ? '#fff' : '#192338'" /> <MessageBubbleLoading :active-color="currentBubbleTextColor" />
</div> </div>
</div> </div>
</div> </div>
<div class="flex justify-between py-[2px]"> <div class="flex justify-between py-[2px]">
<div <div
v-show="isAssistant && messageItem.knowledgeContentResult.length" v-show="isAgentMessage && messageItem.knowledgeContentResult.length"
class="flex-center rounded-theme h-8 cursor-pointer gap-[5px] px-[14px] font-['Microsoft_YaHei_UI'] text-[#0B7DFF] hover:opacity-80" class="flex-center rounded-theme h-8 cursor-pointer gap-[5px] px-[14px] font-['Microsoft_YaHei_UI'] text-[#0B7DFF] hover:opacity-80"
@click="emit('showKnowledgeResult')" @click="emit('showKnowledgeResult')"
> >
...@@ -152,6 +174,14 @@ function handleShowReasoningContentSwitch() { ...@@ -152,6 +174,14 @@ function handleShowReasoningContentSwitch() {
</div> </div>
</div> </div>
</div> </div>
<SmartForms
v-else
:message-item="messageItem"
:message-author="messageAuthor"
:is-agent-message="isAgentMessage"
:current-bubble-text-color="currentBubbleTextColor"
/>
</div> </div>
</div> </div>
</template> </template>
......
<script setup lang="ts"> <script setup lang="ts">
import { useTemplateRef } from 'vue' import { provide, useTemplateRef } from 'vue'
import { ScrollbarInst } from 'naive-ui' import { ScrollbarInst } from 'naive-ui'
import { useElementVisibility } from '@vueuse/core' import { useElementVisibility } from '@vueuse/core'
import MessageItem from './message-item.vue' import MessageItem from './message-item.vue'
...@@ -13,6 +13,7 @@ defineProps<Props>() ...@@ -13,6 +13,7 @@ defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
showKnowledgeResult: [knowledgeContentResult: KnowledgeContentResultItem[]] showKnowledgeResult: [knowledgeContentResult: KnowledgeContentResultItem[]]
updateSpecifyMessageItem: [messageId: string, newMessageItem: Partial<MessageItemInterface>]
}>() }>()
const scrollbarRef = useTemplateRef<ScrollbarInst | null>('scrollbarRef') const scrollbarRef = useTemplateRef<ScrollbarInst | null>('scrollbarRef')
...@@ -21,6 +22,12 @@ const backBottomBtnFlagRef = useTemplateRef<HTMLDivElement | null>('backBottomBt ...@@ -21,6 +22,12 @@ const backBottomBtnFlagRef = useTemplateRef<HTMLDivElement | null>('backBottomBt
const isNotShowBackBottomBtn = useElementVisibility(backBottomBtnFlagRef) const isNotShowBackBottomBtn = useElementVisibility(backBottomBtnFlagRef)
provide('updateSpecifyMessageItem', {
updateSpecifyMessageItem: (messageId: string, newMessageItem: Partial<MessageItemInterface>) => {
emit('updateSpecifyMessageItem', messageId, newMessageItem)
},
})
function scrollToBottom() { function scrollToBottom() {
if (scrollbarRef.value) { if (scrollbarRef.value) {
scrollbarRef.value.scrollTo({ top: 999999999, behavior: 'smooth' }) scrollbarRef.value.scrollTo({ top: 999999999, behavior: 'smooth' })
...@@ -41,6 +48,7 @@ defineExpose({ ...@@ -41,6 +48,7 @@ defineExpose({
<MessageItem <MessageItem
v-for="[key, messageItem] in messageList" v-for="[key, messageItem] in messageList"
:key="key" :key="key"
:message-item-id="key"
:message-item="messageItem" :message-item="messageItem"
@show-knowledge-result="emit('showKnowledgeResult', messageItem.knowledgeContentResult)" @show-knowledge-result="emit('showKnowledgeResult', messageItem.knowledgeContentResult)"
/> />
......
...@@ -5,7 +5,7 @@ import { SelectOption } from 'naive-ui' ...@@ -5,7 +5,7 @@ import { SelectOption } from 'naive-ui'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { useElementSize } from '@vueuse/core' import { useElementSize } from '@vueuse/core'
import { useSystemLanguageStore } from '@/store/modules/system-language' import { useSystemLanguageStore } from '@/store/modules/system-language'
import { MultiModelDialogueItem } from '../types' import { MultiModelDialogueItem, MessageItemInterface } from '../types'
import ModelSetting from './model-setting.vue' import ModelSetting from './model-setting.vue'
import MessageList from './message-list.vue' import MessageList from './message-list.vue'
import HitKnowledgeContent from './hit-knowledge-content.vue' import HitKnowledgeContent from './hit-knowledge-content.vue'
...@@ -15,6 +15,7 @@ interface Props { ...@@ -15,6 +15,7 @@ interface Props {
modelListOptions: SelectOption[] modelListOptions: SelectOption[]
totalNum: number totalNum: number
isCurrent: boolean isCurrent: boolean
modelDialogueId: number
} }
const { t } = useI18n() const { t } = useI18n()
...@@ -26,6 +27,7 @@ const emit = defineEmits<{ ...@@ -26,6 +27,7 @@ const emit = defineEmits<{
updateConfig: [modelDialogueItem: MultiModelDialogueItem] updateConfig: [modelDialogueItem: MultiModelDialogueItem]
replaceConfig: [modelDialogueItem: MultiModelDialogueItem] replaceConfig: [modelDialogueItem: MultiModelDialogueItem]
resetConversation: [] resetConversation: []
updateMessageItem: [messageId: string, messageItem: Partial<MessageItemInterface>, index: number]
}>() }>()
const systemLanguageStore = useSystemLanguageStore() const systemLanguageStore = useSystemLanguageStore()
...@@ -118,6 +120,10 @@ function handleShowKnowledgeResult(knowledgeContentResult: KnowledgeContentResul ...@@ -118,6 +120,10 @@ function handleShowKnowledgeResult(knowledgeContentResult: KnowledgeContentResul
isShowKnowledgeContentResult.value = true isShowKnowledgeContentResult.value = true
currentKnowledgeContentResult.value = knowledgeContentResult currentKnowledgeContentResult.value = knowledgeContentResult
} }
function handleUpdateSpecifyMessageItem(messageId: string, messageItem: Partial<MessageItemInterface>) {
emit('updateMessageItem', messageId, messageItem, props.modelDialogueId)
}
</script> </script>
<template> <template>
...@@ -205,6 +211,7 @@ function handleShowKnowledgeResult(knowledgeContentResult: KnowledgeContentResul ...@@ -205,6 +211,7 @@ function handleShowKnowledgeResult(knowledgeContentResult: KnowledgeContentResul
ref="messageListRef" ref="messageListRef"
:message-list="modelDialogueItem.messageList" :message-list="modelDialogueItem.messageList"
@show-knowledge-result="handleShowKnowledgeResult" @show-knowledge-result="handleShowKnowledgeResult"
@update-specify-message-item="handleUpdateSpecifyMessageItem"
/> />
<HitKnowledgeContent <HitKnowledgeContent
......
<script setup lang="ts">
import { readonly, ref } from 'vue'
import type { MessageItemInterface } from '../../../types'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import type { FormRules } from 'naive-ui'
import MessageBubbleLoading from '../../message-bubble-loading.vue'
interface Props {
isAgentMessage: boolean
messageItem: MessageItemInterface
currentBubbleTextColor: string
}
defineProps<Props>()
const formRules = readonly<FormRules>({
objective: {
required: true,
trigger: ['blur', 'input'],
message: '',
},
travelLocation: {
required: true,
trigger: ['blur', 'input'],
message: '',
},
departureTime: {
required: true,
trigger: ['blur', 'input'],
message: '',
},
returnTime: {
required: true,
trigger: ['blur', 'input'],
message: '',
},
email: {
required: true,
trigger: ['blur', 'input'],
message: '',
},
})
const formModel = ref({
objective: 'khbf',
travelLocation: '',
departureTime: null,
returnTime: null,
vehicle: '',
vehicleEstimatedCost: '',
residence: '',
residenceEstimatedCost: '',
estimatedAmount: '',
generalBudget: '',
email: '',
})
const objectiveOptions = readonly([
{
label: '客户拜访',
value: 'khbf',
},
{
label: '会议',
value: 'hy',
},
{
label: '培训',
value: 'px',
},
])
</script>
<template>
<div class="ml-[11px] overflow-hidden">
<div class="mb-[7px] text-[12px] text-[#999]">作者信息</div>
<div class="min-w-[420px]">
<div
class="box-content min-h-[21px] min-w-[10px] max-w-full rounded-[10px] border border-[#9EA3FF] bg-[#777EF9] px-[15px] py-[14px] text-justify"
:class="{
'!bg-[#fff]': isAgentMessage,
'!min-w-[80px]': messageItem.isAnswerResponseLoading,
}"
>
<div
v-if="messageItem.isAnswerResponseLoading && !messageItem.content"
class="flex h-[21px] items-center justify-center"
>
<MessageBubbleLoading :active-color="currentBubbleTextColor" />
</div>
<template v-else>
<div class="mb-[10px]">
<i class="iconfont icon-tongyi font-600 text-[14px] text-[#6ccb59]"></i>
<span class="ml-[5px] text-[14px] text-[#999]">出差表单插件执行成功</span>
</div>
<MarkdownRender
ref="markdownRenderRef"
raw-text-content="好的,我将为您自动生成出差表单,表单如下"
:color="currentBubbleTextColor"
/>
<!-- <div v-if="messageItem.isAnswerLoading" class="ml-[15px] pt-[12px]">
<MessageBubbleLoading active-color="#fff" width="5px" />
</div> -->
</template>
</div>
<div
class="mt-[10px] box-content min-h-[21px] min-w-[10px] max-w-full rounded-[10px] border border-[#9EA3FF] bg-[#777EF9] px-[15px] py-[14px] text-justify"
:class="{
'!bg-[#fff]': isAgentMessage,
}"
>
<h2 class="font-600 text-[15px] text-[#0B7DFF]">出差表单</h2>
<div class="mt-[12px]">
<n-form :model="formModel" :rules="formRules" label-width="90" label-placement="left" :show-feedback="false">
<n-form-item class="mb-[10px]" label="出差目的" path="objective">
<n-select v-model:value="formModel.objective" placeholder="Select" :options="objectiveOptions" />
</n-form-item>
<n-form-item class="mb-[10px]" label="出差地点" path="travelLocation">
<n-input v-model:value="formModel.travelLocation" placeholder="Input" />
</n-form-item>
<n-form-item class="mb-[10px]" label="出发时间" path="departureTime">
<n-date-picker v-model:value="formModel.departureTime" type="datetime" />
</n-form-item>
<n-form-item label="返回时间" path="returnTime">
<n-date-picker v-model:value="formModel.returnTime" type="datetime" />
</n-form-item>
<n-form-item label="">
<div class="h-[1px] w-full bg-[#B1B1B1] opacity-45"></div>
</n-form-item>
<n-form-item class="mb-[10px]" label="交通工具" path="vehicle">
<n-input v-model:value="formModel.vehicle" placeholder="Input" />
</n-form-item>
<n-form-item label="预计费用" path="vehicleEstimatedCost">
<n-input v-model:value="formModel.vehicleEstimatedCost" placeholder="Input" />
</n-form-item>
<n-form-item label="">
<div class="h-[1px] w-full bg-[#B1B1B1] opacity-45"></div>
</n-form-item>
<n-form-item class="mb-[10px]" label="住宿地点" path="residence">
<n-input v-model:value="formModel.residence" placeholder="Input" />
</n-form-item>
<n-form-item label="预计费用" path="residenceEstimatedCost">
<n-input v-model:value="formModel.residenceEstimatedCost" placeholder="Input" />
</n-form-item>
<n-form-item label="">
<div class="h-[1px] w-full bg-[#B1B1B1] opacity-45"></div>
</n-form-item>
<n-form-item class="mb-[10px]" label="预计金额" path="residence">
<n-input v-model:value="formModel.estimatedAmount" placeholder="Input" />
</n-form-item>
<n-form-item label="总预算" path="generalBudget">
<n-input v-model:value="formModel.generalBudget" placeholder="Input" />
</n-form-item>
<n-form-item label="">
<div class="h-[1px] w-full bg-[#B1B1B1] opacity-45"></div>
</n-form-item>
<n-form-item class="mb-[10px]" label="邮箱信息" path="email">
<n-input v-model:value="formModel.email" placeholder="Input" />
</n-form-item>
<div class="mt-[30px] text-end">
<n-button type="primary">确认提交</n-button>
</div>
</n-form>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { MessageItemInterface } from '../../types'
import BusinessTripForm from './components/business-trip-form.vue'
// import BusinessTripReimbursementForm from './business-trip-reimbursement-form.vue'
interface Props {
isAgentMessage: boolean
messageAuthor: string
messageItem: MessageItemInterface
currentBubbleTextColor: string
}
defineProps<Props>()
</script>
<template>
<BusinessTripForm
:message-item="messageItem"
:is-agent-message="isAgentMessage"
:message-author="messageAuthor"
:current-bubble-text-color="currentBubbleTextColor"
/>
<!-- <BusinessTripReimbursementForm
:is-agent-message="isAgentMessage"
:message-item="messageItem"
:current-bubble-text-color="currentBubbleTextColor"
/> -->
</template>
export interface ResponseBusinessTripForm {
purpose: string
place: string
departureDate: string
returnDate: string
vehicle: string
transportationFee: string
accommodation: string
accommodationCost: string
advancePaymentAmount: string
totalBudget: string
}
export interface BusinessTripForm {
objective: string
travelLocation: string
departureTime: number | null
returnTime: number | null
vehicle: string
vehicleEstimatedCost: number | null
residence: string
residenceEstimatedCost: number | null
advancePaymentAmount: number | null
generalBudget: string
email: string
}
export type SmartFormDisplayFormat = 'travelForm'
import { BusinessTripForm, ResponseBusinessTripForm } from '../types/types'
import type { SmartFormTypes } from '../../../types'
export function smartFormTypeConverter(type: 'travelForm'): SmartFormTypes {
switch (type) {
case 'travelForm':
return 'BusinessTripForm'
}
}
export function businessTripFormParser(resForm: Partial<ResponseBusinessTripForm>) {
return {
objective: resForm.purpose || 'CustomerVisits',
travelLocation: resForm.place || '',
departureTime: resForm.departureDate ? Date.parse(resForm.departureDate) : null,
returnTime: resForm.returnDate ? Date.parse(resForm.returnDate) : null,
vehicle: resForm.vehicle || '',
vehicleEstimatedCost: resForm.transportationFee ? Number.parseInt(resForm.transportationFee) : null,
residence: resForm.accommodation || '',
residenceEstimatedCost: resForm.accommodationCost ? Number.parseInt(resForm.accommodationCost) : null,
advancePaymentAmount: resForm.advancePaymentAmount ? Number.parseInt(resForm.advancePaymentAmount) : null,
generalBudget: resForm.totalBudget || '',
email: '',
}
}
export function businessTripFormReturner(form: BusinessTripForm) {
return {
purpose: form.objective,
place: form.travelLocation,
departureDate: form.departureTime ? new Date(form.departureTime).toISOString() : '',
returnDate: form.returnTime ? new Date(form.returnTime).toISOString() : '',
vehicle: form.vehicle,
transportationFee: form.vehicleEstimatedCost,
accommodation: form.residence,
accommodationCost: form.residenceEstimatedCost,
advancePaymentAmount: form.advancePaymentAmount,
totalBudget: form.generalBudget,
email: form.email,
}
}
...@@ -9,7 +9,7 @@ import type { ValueOf } from 'type-fest' ...@@ -9,7 +9,7 @@ import type { ValueOf } from 'type-fest'
import PageHeader from './components/page-header.vue' import PageHeader from './components/page-header.vue'
import ModelDialogueItem from './components/model-dialogue-item.vue' import ModelDialogueItem from './components/model-dialogue-item.vue'
import FooterOperation from './components/footer-operation.vue' import FooterOperation from './components/footer-operation.vue'
import { MessageItemInterface, MultiModelDialogueItem, LargeModelItem } from './types' import { MessageItemInterface, MultiModelDialogueItem, LargeModelItem, SmartFormTypes } from './types'
import { import {
fetchGetDebugApplicationInfo, fetchGetDebugApplicationInfo,
fetchGetLargeModelInfo, fetchGetLargeModelInfo,
...@@ -254,9 +254,7 @@ function handleUpdateSpecifyMessageItem(messageId: string, newObj: Partial<Messa ...@@ -254,9 +254,7 @@ function handleUpdateSpecifyMessageItem(messageId: string, newObj: Partial<Messa
if (currentMessageItemInfo) { if (currentMessageItemInfo) {
Object.entries<ValueOf<typeof newObj>>(newObj).forEach(([key, value]) => { Object.entries<ValueOf<typeof newObj>>(newObj).forEach(([key, value]) => {
if (Object.prototype.hasOwnProperty.call(currentMessageItemInfo, key)) { ;(currentMessageItemInfo as any)[key as keyof MessageItemInterface] = value
;(currentMessageItemInfo as any)[key as keyof MessageItemInterface] = value
}
}) })
} }
} }
...@@ -300,6 +298,17 @@ function handleBlockMessageResponse() { ...@@ -300,6 +298,17 @@ function handleBlockMessageResponse() {
modelDialogueItem.messageList.clear() modelDialogueItem.messageList.clear()
}) })
} }
function onSmartFormsStatusFreezeCheck(smartFormType: SmartFormTypes, index: number) {
/* 重置智能表单项目 */
if (smartFormType) {
multiModelDialogueList.value[index].messageList.forEach((item) => {
if (item.smartFormInfo && item.smartFormInfo.type === smartFormType) {
item.smartFormInfo.isDisabled = true
}
})
}
}
</script> </script>
<template> <template>
...@@ -328,6 +337,7 @@ function handleBlockMessageResponse() { ...@@ -328,6 +337,7 @@ function handleBlockMessageResponse() {
v-for="(modelDialogueItem, index) in multiModelDialogueList" v-for="(modelDialogueItem, index) in multiModelDialogueList"
:ref="(el) => (modelDialogueListRef[index] = el as InstanceType<typeof ModelDialogueItem>)" :ref="(el) => (modelDialogueListRef[index] = el as InstanceType<typeof ModelDialogueItem>)"
:key="modelDialogueItem.id" :key="modelDialogueItem.id"
:model-dialogue-id="index"
:model-dialogue-item="modelDialogueItem" :model-dialogue-item="modelDialogueItem"
:is-current="modelDialogueItem.id === multiModelDialogueList[0].id" :is-current="modelDialogueItem.id === multiModelDialogueList[0].id"
:model-list-options="modelListOptions" :model-list-options="modelListOptions"
...@@ -336,6 +346,7 @@ function handleBlockMessageResponse() { ...@@ -336,6 +346,7 @@ function handleBlockMessageResponse() {
@remove-model-dialogue-item="handleRemoveModelDialogueItem" @remove-model-dialogue-item="handleRemoveModelDialogueItem"
@replace-config="handleReplaceAgentConfig" @replace-config="handleReplaceAgentConfig"
@reset-conversation="handleResetConversation" @reset-conversation="handleResetConversation"
@update-message-item="handleUpdateSpecifyMessageItem"
/> />
</div> </div>
</main> </main>
...@@ -352,6 +363,7 @@ function handleBlockMessageResponse() { ...@@ -352,6 +363,7 @@ function handleBlockMessageResponse() {
@delete-message-item="handleDeleteMessageItem" @delete-message-item="handleDeleteMessageItem"
@clear-all-message="handleClearAllMessage" @clear-all-message="handleClearAllMessage"
@message-list-scroll-to-bottom="handleMessageListScrollToBottom" @message-list-scroll-to-bottom="handleMessageListScrollToBottom"
@smart-forms-status-freeze-check="onSmartFormsStatusFreezeCheck"
/> />
<Transition name="mask" mode="out-in"> <Transition name="mask" mode="out-in">
......
...@@ -25,6 +25,8 @@ export interface MultiModelDialogueItem { ...@@ -25,6 +25,8 @@ export interface MultiModelDialogueItem {
messageList: Map<string, MessageItemInterface> messageList: Map<string, MessageItemInterface>
} }
export type SmartFormTypes = 'BusinessTripForm'
export interface MessageItemInterface { export interface MessageItemInterface {
role: 'user' | 'assistant' role: 'user' | 'assistant'
avatar: string avatar: string
...@@ -38,11 +40,16 @@ export interface MessageItemInterface { ...@@ -38,11 +40,16 @@ export interface MessageItemInterface {
knowledgeContentResult: KnowledgeContentResultItem[] knowledgeContentResult: KnowledgeContentResultItem[]
dbChainSQLContent: string dbChainSQLContent: string
pluginResult?: { pluginResult?: {
displayFormat: 'json' | 'markdown' | 'none' displayFormat: PluginDisplayFormatType
pluginName: string pluginName: string
arguments: string arguments: string
pluginContent: string pluginContent: string
} }
smartFormInfo?: {
type: SmartFormTypes
isDisabled: boolean
params: string
}
} }
export interface LargeModelItem { export interface LargeModelItem {
......
...@@ -8,7 +8,7 @@ import { languageKeyTransform } from '@/utils/language-key-transform' ...@@ -8,7 +8,7 @@ import { languageKeyTransform } from '@/utils/language-key-transform'
interface ResponseData { interface ResponseData {
message: string message: string
reasoningContent: string reasoningContent: string
function: { displayFormat: 'json' | 'markdown' | 'none'; name: string; result: string; arguments: string } function: { displayFormat: PluginDisplayFormatType; name: string; result: string; arguments: string }
knowledgeContentResult: KnowledgeContentResultItem[] knowledgeContentResult: KnowledgeContentResultItem[]
dbChainResult: DBChainResultItem[] dbChainResult: DBChainResultItem[]
} }
......
...@@ -14,6 +14,7 @@ import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config' ...@@ -14,6 +14,7 @@ import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { useSystemLanguageStore } from '@/store/modules/system-language' import { useSystemLanguageStore } from '@/store/modules/system-language'
import { Brain, Down } from '@icon-park/vue-next' import { Brain, Down } from '@icon-park/vue-next'
import { validBrowser } from '@/utils/browser-detection' import { validBrowser } from '@/utils/browser-detection'
import { SmartFormTypes } from '../types'
const { t } = useI18n() const { t } = useI18n()
...@@ -78,9 +79,7 @@ function handleUpdateSpecifyMessageItem(messageId: string, newMessageItem: Parti ...@@ -78,9 +79,7 @@ function handleUpdateSpecifyMessageItem(messageId: string, newMessageItem: Parti
} }
Object.entries<ValueOf<typeof newMessageItem>>(newMessageItem).forEach(([key, value]) => { Object.entries<ValueOf<typeof newMessageItem>>(newMessageItem).forEach(([key, value]) => {
if (Object.prototype.hasOwnProperty.call(currentMessageItemInfo, key)) { ;(currentMessageItemInfo as any)[key as keyof ConversationMessageItem] = value
;(currentMessageItemInfo as any)[key as keyof ConversationMessageItem] = value
}
}) })
} }
} }
...@@ -235,6 +234,17 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -235,6 +234,17 @@ function handleAudioPause(isClearMessageList = false) {
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
} }
} }
function onSmartFormsStatusFreezeCheck(smartFormType: SmartFormTypes) {
/* 重置智能表单项目 */
if (smartFormType) {
messageList.value.forEach((item) => {
if (item.smartFormInfo && item.smartFormInfo.type === smartFormType) {
item.smartFormInfo.isDisabled = true
}
})
}
}
</script> </script>
<template> <template>
...@@ -328,6 +338,7 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -328,6 +338,7 @@ function handleAudioPause(isClearMessageList = false) {
:create-continue-questions-exception="createContinueQuestionsException" :create-continue-questions-exception="createContinueQuestionsException"
@audio-play="handleAudioPlay" @audio-play="handleAudioPlay"
@audio-pause="handleAudioPause" @audio-pause="handleAudioPause"
@update-specify-message-item="handleUpdateSpecifyMessageItem"
/> />
</div> </div>
</div> </div>
...@@ -348,6 +359,7 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -348,6 +359,7 @@ function handleAudioPause(isClearMessageList = false) {
@update-continuous-question-status="handleUpdateContinueQuestionStatus" @update-continuous-question-status="handleUpdateContinueQuestionStatus"
@audio-play="handleAudioPlay" @audio-play="handleAudioPlay"
@audio-pause="handleAudioPause" @audio-pause="handleAudioPause"
@smart-forms-status-freeze-check="onSmartFormsStatusFreezeCheck"
/> />
<MemoryPreviewModal v-model="isShowMemoryPreviewModal" :data="selectedMemoryTabName" /> <MemoryPreviewModal v-model="isShowMemoryPreviewModal" :data="selectedMemoryTabName" />
......
...@@ -13,6 +13,9 @@ import { TEXTTOSPEECH_WS_URL } from '@/config/base-url' ...@@ -13,6 +13,9 @@ import { TEXTTOSPEECH_WS_URL } from '@/config/base-url'
import WebSocketCtr from '@/utils/web-socket-ctr' import WebSocketCtr from '@/utils/web-socket-ctr'
import { ChannelType } from '@/enums/channel' import { ChannelType } from '@/enums/channel'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { smartFormTypeConverter } from '../components/smart-forms/utils/smart-forms'
import { SmartFormTypes } from '../../types'
import { SmartFormDisplayFormat } from './smart-forms/types/types'
interface Props { interface Props {
messageList: Map<string, ConversationMessageItem> messageList: Map<string, ConversationMessageItem>
...@@ -35,6 +38,7 @@ const emit = defineEmits<{ ...@@ -35,6 +38,7 @@ const emit = defineEmits<{
updateContinuousQuestionStatus: [value: 'default' | 'close'] updateContinuousQuestionStatus: [value: 'default' | 'close']
audioPlay: [messageItem: ConversationMessageItem, requestId?: string] audioPlay: [messageItem: ConversationMessageItem, requestId?: string]
audioPause: [] audioPause: []
smartFormsStatusFreezeCheck: [value: SmartFormTypes]
}>() }>()
const personalAppConfigStore = usePersonalAppConfigStore() const personalAppConfigStore = usePersonalAppConfigStore()
...@@ -61,6 +65,7 @@ const assistantFullAnswerContent = ref('') ...@@ -61,6 +65,7 @@ const assistantFullAnswerContent = ref('')
const currentAgentTimberId = ref('') const currentAgentTimberId = ref('')
const sentenceSpeechException = ref(false) const sentenceSpeechException = ref(false)
const messageAudioLoading = ref(false) const messageAudioLoading = ref(false)
const isSmartFormPlugins = ref(false) // 是否智能表单插件
const currentLatestMessageItemKeyMap = ref(new Map<'assistant' | 'user', string>()) const currentLatestMessageItemKeyMap = ref(new Map<'assistant' | 'user', string>())
let controller: AbortController | null = null let controller: AbortController | null = null
...@@ -233,6 +238,7 @@ function handleMessageSend() { ...@@ -233,6 +238,7 @@ function handleMessageSend() {
currentAgentTimberId.value = personalAppConfigStore.voiceConfig.timbreId currentAgentTimberId.value = personalAppConfigStore.voiceConfig.timbreId
sentenceSpeechException.value = false sentenceSpeechException.value = false
messageAudioLoading.value = false messageAudioLoading.value = false
isSmartFormPlugins.value = false
const isVoiceEnabled = !!personalAppConfigStore.voiceConfig.timbreId const isVoiceEnabled = !!personalAppConfigStore.voiceConfig.timbreId
...@@ -271,6 +277,31 @@ function handleMessageSend() { ...@@ -271,6 +277,31 @@ function handleMessageSend() {
// 插件 // 插件
if (data.function && data.function.name) { if (data.function && data.function.name) {
// 表单插件,展示表单内容
if (['travelForm'].includes(data.function.displayFormat)) {
emit(
'smartFormsStatusFreezeCheck',
smartFormTypeConverter(data.function.displayFormat as SmartFormDisplayFormat),
)
emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
pluginResult: {
displayFormat: data.function.displayFormat,
pluginName: data.function.name,
arguments: data.function.arguments,
pluginContent: data.function.result,
},
smartFormInfo: {
type: smartFormTypeConverter(data.function.displayFormat as SmartFormDisplayFormat),
isDisabled: false,
params: data.function.result,
},
})
emit('updatePageScroll')
isSmartFormPlugins.value = true
return
}
emit('updateSpecifyMessageItem', latestAssistantMessageKey, { emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
pluginResult: { pluginResult: {
displayFormat: data.function.displayFormat, displayFormat: data.function.displayFormat,
...@@ -313,7 +344,7 @@ function handleMessageSend() { ...@@ -313,7 +344,7 @@ function handleMessageSend() {
'', '',
) )
if (!sentenceExtractCheckEnabled.value && isVoiceEnabled) { if (!sentenceExtractCheckEnabled.value && isVoiceEnabled && !isSmartFormPlugins.value) {
sentenceExtract(latestAssistantMessageKey) sentenceExtract(latestAssistantMessageKey)
sentenceExtractCheckEnabled.value = true sentenceExtractCheckEnabled.value = true
messageAudioLoading.value = true messageAudioLoading.value = true
......
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, useTemplateRef } from 'vue' import { computed, ref, useTemplateRef, readonly, provide } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { throttle } from 'lodash-es' import { throttle } from 'lodash-es'
import { Down } from '@icon-park/vue-next' import { Down } from '@icon-park/vue-next'
import CustomLoading from './custom-loading.vue' import CustomLoading from './custom-loading.vue'
import SmartForms from './smart-forms/index.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config' import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue' import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import ExecuteCodeRender from '@/components/execute-code-render/execute-code-render.vue' import ExecuteCodeRender from '@/components/execute-code-render/execute-code-render.vue'
...@@ -14,7 +15,7 @@ import { downloadFile } from '@/utils/download-file' ...@@ -14,7 +15,7 @@ import { downloadFile } from '@/utils/download-file'
import { copyToClip } from '@/utils/copy' import { copyToClip } from '@/utils/copy'
interface Props { interface Props {
role: 'user' | 'assistant' messageItemId: string
messageItem: ConversationMessageItem messageItem: ConversationMessageItem
} }
...@@ -33,8 +34,12 @@ const personalAppConfigStore = usePersonalAppConfigStore() ...@@ -33,8 +34,12 @@ const personalAppConfigStore = usePersonalAppConfigStore()
const markdownRenderRef = useTemplateRef<InstanceType<typeof MarkdownRender>>('markdownRenderRef') const markdownRenderRef = useTemplateRef<InstanceType<typeof MarkdownRender>>('markdownRenderRef')
provide('messageItemId', props.messageItemId)
const isShowReasoningContent = ref(true) const isShowReasoningContent = ref(true)
const displaySmartFormsList = readonly(['travelForm'])
const useAvatar = computed(() => { const useAvatar = computed(() => {
return userStore.userInfo.avatarUrl || 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/1725952917468.png' return userStore.userInfo.avatarUrl || 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/1725952917468.png'
}) })
...@@ -43,8 +48,27 @@ const assistantAvatar = computed(() => { ...@@ -43,8 +48,27 @@ const assistantAvatar = computed(() => {
return personalAppConfigStore.baseInfo.agentAvatar return personalAppConfigStore.baseInfo.agentAvatar
}) })
const isAgentMessage = computed(() => {
return props.messageItem.role === 'assistant'
})
const currentBubbleTextColor = computed(() => {
return isAgentMessage.value ? '#192338' : '#fff'
})
const isShowSmartForms = computed(() => {
if (props.messageItem?.pluginResult?.displayFormat) {
return displaySmartFormsList.includes(props.messageItem?.pluginResult?.displayFormat)
}
return false
})
const messageAuthor = computed(() => {
return isAgentMessage.value ? props.messageItem.modelName || 'AI' : userStore.userInfo.nickName
})
const isShowAudioControl = computed(() => { const isShowAudioControl = computed(() => {
return props.role === 'assistant' && !props.messageItem.isVoiceLoading return isAgentMessage.value && !props.messageItem.isVoiceLoading
}) })
const isPlayableAudio = computed(() => { const isPlayableAudio = computed(() => {
...@@ -52,7 +76,7 @@ const isPlayableAudio = computed(() => { ...@@ -52,7 +76,7 @@ const isPlayableAudio = computed(() => {
}) })
const isShowVoiceLoading = computed(() => { const isShowVoiceLoading = computed(() => {
return props.role === 'assistant' && props.messageItem.isVoiceLoading && props.messageItem.isVoiceEnabled return isAgentMessage.value && props.messageItem.isVoiceLoading && props.messageItem.isVoiceEnabled
}) })
const isDeepSeekR1 = computed(() => { const isDeepSeekR1 = computed(() => {
...@@ -105,7 +129,7 @@ const handleContentCopy = throttle( ...@@ -105,7 +129,7 @@ const handleContentCopy = throttle(
<template> <template>
<div class="mb-5 flex last:mb-0"> <div class="mb-5 flex last:mb-0">
<NImage <NImage
:src="role === 'user' ? useAvatar : assistantAvatar" :src="isAgentMessage ? assistantAvatar : useAvatar"
preview-disabled preview-disabled
:width="32" :width="32"
:height="32" :height="32"
...@@ -113,9 +137,9 @@ const handleContentCopy = throttle( ...@@ -113,9 +137,9 @@ const handleContentCopy = throttle(
class="mr-2 mt-1.5 h-8 w-8 flex-shrink-0 rounded-full" class="mr-2 mt-1.5 h-8 w-8 flex-shrink-0 rounded-full"
/> />
<div class="flex flex-col items-start overflow-x-auto"> <div v-if="!isShowSmartForms" class="flex flex-col items-start overflow-x-auto">
<!-- DeepSeek大模型思考 --> <!-- DeepSeek大模型思考 -->
<template v-if="role === 'assistant' && isDeepSeekR1"> <template v-if="isAgentMessage && isDeepSeekR1">
<div class="my-[7px] select-none text-[14px]"> <div class="my-[7px] select-none text-[14px]">
<div class="inline-flex cursor-pointer" @click="handleShowReasoningContentSwitch"> <div class="inline-flex cursor-pointer" @click="handleShowReasoningContentSwitch">
<span v-if="messageItem.isTextContentLoading" class="mr-[6px]"> <span v-if="messageItem.isTextContentLoading" class="mr-[6px]">
...@@ -156,17 +180,17 @@ const handleContentCopy = throttle( ...@@ -156,17 +180,17 @@ const handleContentCopy = throttle(
<!-- 大模型内容 --> <!-- 大模型内容 -->
<div class="flex min-w-[80px] max-w-full flex-col items-start overflow-x-auto"> <div class="flex min-w-[80px] max-w-full flex-col items-start overflow-x-auto">
<div <div
class="w-full flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]" class="w-full flex-wrap rounded-xl border border-[#9EA3FF] px-4 py-[11px]"
:class="role === 'user' ? 'user-content-container bg-[#777EF9] text-white' : 'bg-white text-[#333]'" :class="isAgentMessage ? 'bg-white text-[#333]' : 'user-content-container bg-[#777EF9] text-white'"
> >
<img <img
v-show="role === 'user' && messageItem.imageUrl" v-show="!isAgentMessage && messageItem.imageUrl"
:src="messageItem.imageUrl" :src="messageItem.imageUrl"
class="max-h-[120px]! mb-[11px] rounded-[10px] object-contain" class="max-h-[120px]! mb-[11px] rounded-[10px] object-contain"
/> />
<!-- 插件返回结果 --> <!-- 插件返回结果 -->
<div v-show="role === 'assistant' && messageItem?.pluginResult?.pluginName" class="mb-[11px] w-full"> <div v-show="isAgentMessage && messageItem?.pluginResult?.pluginName" class="mb-[11px] w-full">
<ExecutePluginRender <ExecutePluginRender
:display-format="messageItem.pluginResult?.displayFormat || 'none'" :display-format="messageItem.pluginResult?.displayFormat || 'none'"
:name="messageItem.pluginResult?.pluginName!" :name="messageItem.pluginResult?.pluginName!"
...@@ -197,12 +221,12 @@ const handleContentCopy = throttle( ...@@ -197,12 +221,12 @@ const handleContentCopy = throttle(
? t('common_module.dialogue_module.empty_message_content') ? t('common_module.dialogue_module.empty_message_content')
: messageItem.textContent : messageItem.textContent
" "
:color="role === 'user' ? '#fff' : '#192338'" :color="currentBubbleTextColor"
/> />
</p> </p>
<div <div
v-show="(role === 'assistant' && messageItem.isAnswerResponseLoading) || isShowVoiceLoading" v-show="(isAgentMessage && messageItem.isAnswerResponseLoading) || isShowVoiceLoading"
class="mt-4 px-4" class="mt-4 px-4"
:class="isShowAudioControl ? 'mb-2.5' : 'mb-[5px]'" :class="isShowAudioControl ? 'mb-2.5' : 'mb-[5px]'"
> >
...@@ -242,7 +266,7 @@ const handleContentCopy = throttle( ...@@ -242,7 +266,7 @@ const handleContentCopy = throttle(
<div class="flex w-full items-center justify-between py-[5px]"> <div class="flex w-full items-center justify-between py-[5px]">
<div> <div>
<div <div
v-show="role === 'assistant' && messageItem.knowledgeContentResult.length" v-show="isAgentMessage && messageItem.knowledgeContentResult.length"
class="flex-center rounded-theme h-8 cursor-pointer gap-[5px] px-[13px] font-['Microsoft_YaHei_UI'] text-[#0B7DFF] hover:opacity-80" class="flex-center rounded-theme h-8 cursor-pointer gap-[5px] px-[13px] font-['Microsoft_YaHei_UI'] text-[#0B7DFF] hover:opacity-80"
@click="emit('showKnowledgeResult')" @click="emit('showKnowledgeResult')"
> >
...@@ -252,7 +276,7 @@ const handleContentCopy = throttle( ...@@ -252,7 +276,7 @@ const handleContentCopy = throttle(
</div> </div>
<div <div
v-show="role === 'assistant' && messageItem.textContent && !messageItem.isAnswerResponseLoading" v-show="isAgentMessage && messageItem.textContent && !messageItem.isAnswerResponseLoading"
class="pr-[13px] text-end" class="pr-[13px] text-end"
> >
<i <i
...@@ -267,6 +291,14 @@ const handleContentCopy = throttle( ...@@ -267,6 +291,14 @@ const handleContentCopy = throttle(
</div> </div>
</div> </div>
</div> </div>
<SmartForms
v-else
:message-author="messageAuthor"
:is-agent-message="isAgentMessage"
:message-item="messageItem"
:current-bubble-text-color="currentBubbleTextColor"
/>
</div> </div>
</template> </template>
......
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue' import { computed, nextTick, provide, ref, useTemplateRef, watch } from 'vue'
import { useElementVisibility } from '@vueuse/core' import { useElementVisibility } from '@vueuse/core'
import MessageItem from './message-item.vue' import MessageItem from './message-item.vue'
import ContinueQuestion from './continue-question.vue' import ContinueQuestion from './continue-question.vue'
...@@ -16,15 +16,22 @@ interface Props { ...@@ -16,15 +16,22 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
defineEmits<{ const emit = defineEmits<{
audioPlay: [messageItem: ConversationMessageItem] audioPlay: [messageItem: ConversationMessageItem]
audioPause: [] audioPause: []
updateSpecifyMessageItem: [messageId: string, newMessageItem: Partial<ConversationMessageItem>]
}>() }>()
const { scrollRef, scrollToBottom } = useScroll() const { scrollRef, scrollToBottom } = useScroll()
const backBottomBtnFlagRef = useTemplateRef<HTMLDivElement | null>('backBottomBtnFlagRef') const backBottomBtnFlagRef = useTemplateRef<HTMLDivElement | null>('backBottomBtnFlagRef')
const isNotShowBackBottomBtn = useElementVisibility(backBottomBtnFlagRef) const isNotShowBackBottomBtn = useElementVisibility(backBottomBtnFlagRef)
provide('updateSpecifyMessageItem', {
updateSpecifyMessageItem: (messageId: string, newMessageItem: Partial<ConversationMessageItem>) => {
emit('updateSpecifyMessageItem', messageId, newMessageItem)
},
})
const isShowKnowledgeContent = ref(false) const isShowKnowledgeContent = ref(false)
const currentKnowledgeContentResult = ref<KnowledgeContentResultItem[]>([]) const currentKnowledgeContentResult = ref<KnowledgeContentResultItem[]>([])
...@@ -70,7 +77,7 @@ function handleShowKnowledgeResult(knowledgeContentResult: KnowledgeContentResul ...@@ -70,7 +77,7 @@ function handleShowKnowledgeResult(knowledgeContentResult: KnowledgeContentResul
<MessageItem <MessageItem
v-for="[key, messageItem] in messageList" v-for="[key, messageItem] in messageList"
:key="key" :key="key"
:role="messageItem.role" :message-item-id="key"
:message-item="messageItem" :message-item="messageItem"
@audio-play="() => $emit('audioPlay', messageItem)" @audio-play="() => $emit('audioPlay', messageItem)"
@audio-pause="() => $emit('audioPause')" @audio-pause="() => $emit('audioPause')"
......
<script setup lang="ts">
import { readonly, ref } from 'vue'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import type { FormRules } from 'naive-ui'
import CustomLoading from '../../custom-loading.vue'
interface Props {
isAgentMessage: boolean
messageItem: ConversationMessageItem
currentBubbleTextColor: string
}
defineProps<Props>()
const formRules = readonly<FormRules>({
objective: {
required: true,
trigger: ['blur', 'input'],
message: '',
},
travelLocation: {
required: true,
trigger: ['blur', 'input'],
message: '',
},
departureTime: {
required: true,
trigger: ['blur', 'input'],
message: '',
},
returnTime: {
required: true,
trigger: ['blur', 'input'],
message: '',
},
email: {
required: true,
trigger: ['blur', 'input'],
message: '',
},
})
const formModel = ref({
objective: 'khbf',
travelLocation: '',
departureTime: null,
returnTime: null,
vehicle: '',
vehicleEstimatedCost: '',
residence: '',
residenceEstimatedCost: '',
estimatedAmount: '',
generalBudget: '',
email: '',
})
const objectiveOptions = readonly([
{
label: '客户拜访',
value: 'khbf',
},
{
label: '会议',
value: 'hy',
},
{
label: '培训',
value: 'px',
},
])
</script>
<template>
<div class="ml-[11px] overflow-hidden">
<div class="mb-[7px] text-[12px] text-[#999]">作者信息</div>
<div class="min-w-[420px]">
<div
class="box-content min-h-[21px] min-w-[10px] max-w-full rounded-[10px] border border-[#9EA3FF] bg-[#777EF9] px-[15px] py-[14px] text-justify"
:class="{
'!bg-[#fff]': isAgentMessage,
'!min-w-[80px]': messageItem.isAnswerResponseLoading,
}"
>
<div
v-if="messageItem.isAnswerResponseLoading && !messageItem.textContent"
class="flex h-[21px] items-center justify-center"
>
<CustomLoading :active-color="currentBubbleTextColor" />
</div>
<template v-else>
<div class="mb-[10px]">
<i class="iconfont icon-tongyi font-600 text-[14px] text-[#6ccb59]"></i>
<span class="ml-[5px] text-[14px] text-[#999]">出差表单插件执行成功</span>
</div>
<MarkdownRender
ref="markdownRenderRef"
raw-text-content="好的,我将为您自动生成出差表单,表单如下"
:color="currentBubbleTextColor"
/>
<!-- <div v-if="messageItem.isAnswerLoading" class="ml-[15px] pt-[12px]">
<CustomLoading active-color="#fff" width="5px" />
</div> -->
</template>
</div>
<div
class="mt-[10px] box-content min-h-[21px] min-w-[10px] max-w-full rounded-[10px] border border-[#9EA3FF] bg-[#777EF9] px-[15px] py-[14px] text-justify"
:class="{
'!bg-[#fff]': isAgentMessage,
}"
>
<h2 class="font-600 text-[15px] text-[#0B7DFF]">出差表单</h2>
<div class="mt-[12px]">
<n-form :model="formModel" :rules="formRules" label-width="90" label-placement="left" :show-feedback="false">
<n-form-item class="mb-[10px]" label="出差目的" path="objective">
<n-select v-model:value="formModel.objective" placeholder="Select" :options="objectiveOptions" />
</n-form-item>
<n-form-item class="mb-[10px]" label="出差地点" path="travelLocation">
<n-input v-model:value="formModel.travelLocation" placeholder="Input" />
</n-form-item>
<n-form-item class="mb-[10px]" label="出发时间" path="departureTime">
<n-date-picker v-model:value="formModel.departureTime" type="datetime" />
</n-form-item>
<n-form-item label="返回时间" path="returnTime">
<n-date-picker v-model:value="formModel.returnTime" type="datetime" />
</n-form-item>
<n-form-item label="">
<div class="h-[1px] w-full bg-[#B1B1B1] opacity-45"></div>
</n-form-item>
<n-form-item class="mb-[10px]" label="交通工具" path="vehicle">
<n-input v-model:value="formModel.vehicle" placeholder="Input" />
</n-form-item>
<n-form-item label="预计费用" path="vehicleEstimatedCost">
<n-input v-model:value="formModel.vehicleEstimatedCost" placeholder="Input" />
</n-form-item>
<n-form-item label="">
<div class="h-[1px] w-full bg-[#B1B1B1] opacity-45"></div>
</n-form-item>
<n-form-item class="mb-[10px]" label="住宿地点" path="residence">
<n-input v-model:value="formModel.residence" placeholder="Input" />
</n-form-item>
<n-form-item label="预计费用" path="residenceEstimatedCost">
<n-input v-model:value="formModel.residenceEstimatedCost" placeholder="Input" />
</n-form-item>
<n-form-item label="">
<div class="h-[1px] w-full bg-[#B1B1B1] opacity-45"></div>
</n-form-item>
<n-form-item class="mb-[10px]" label="预计金额" path="residence">
<n-input v-model:value="formModel.estimatedAmount" placeholder="Input" />
</n-form-item>
<n-form-item label="总预算" path="generalBudget">
<n-input v-model:value="formModel.generalBudget" placeholder="Input" />
</n-form-item>
<n-form-item label="">
<div class="h-[1px] w-full bg-[#B1B1B1] opacity-45"></div>
</n-form-item>
<n-form-item class="mb-[10px]" label="邮箱信息" path="email">
<n-input v-model:value="formModel.email" placeholder="Input" />
</n-form-item>
<div class="mt-[30px] text-end">
<n-button type="primary">确认提交</n-button>
</div>
</n-form>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import BusinessTripForm from './components/business-trip-form.vue'
// import BusinessTripReimbursementForm from './business-trip-reimbursement-form.vue'
interface Props {
isAgentMessage: boolean
messageAuthor: string
messageItem: ConversationMessageItem
currentBubbleTextColor: string
}
defineProps<Props>()
</script>
<template>
<BusinessTripForm
:message-item="messageItem"
:is-agent-message="isAgentMessage"
:message-author="messageAuthor"
:current-bubble-text-color="currentBubbleTextColor"
/>
<!-- <BusinessTripReimbursementForm
:is-agent-message="isAgentMessage"
:message-item="messageItem"
:current-bubble-text-color="currentBubbleTextColor"
/> -->
</template>
export interface ResponseBusinessTripForm {
purpose: string
place: string
departureDate: string
returnDate: string
vehicle: string
transportationFee: string
accommodation: string
accommodationCost: string
advancePaymentAmount: string
totalBudget: string
}
export interface BusinessTripForm {
objective: string
travelLocation: string
departureTime: number | null
returnTime: number | null
vehicle: string
vehicleEstimatedCost: number | null
residence: string
residenceEstimatedCost: number | null
advancePaymentAmount: number | null
generalBudget: string
email: string
}
export type SmartFormDisplayFormat = 'travelForm'
import type { SmartFormTypes } from '../../../../types'
import { BusinessTripForm, ResponseBusinessTripForm } from '../types/types'
export function smartFormTypeConverter(type: 'travelForm'): SmartFormTypes {
switch (type) {
case 'travelForm':
return 'BusinessTripForm'
}
}
export function businessTripFormParser(resForm: Partial<ResponseBusinessTripForm>) {
return {
objective: resForm.purpose || 'CustomerVisits',
travelLocation: resForm.place || '',
departureTime: resForm.departureDate ? Date.parse(resForm.departureDate) : null,
returnTime: resForm.returnDate ? Date.parse(resForm.returnDate) : null,
vehicle: resForm.vehicle || '',
vehicleEstimatedCost: resForm.transportationFee ? Number.parseInt(resForm.transportationFee) : null,
residence: resForm.accommodation || '',
residenceEstimatedCost: resForm.accommodationCost ? Number.parseInt(resForm.accommodationCost) : null,
advancePaymentAmount: resForm.advancePaymentAmount ? Number.parseInt(resForm.advancePaymentAmount) : null,
generalBudget: resForm.totalBudget || '',
email: '',
}
}
export function businessTripFormReturner(form: BusinessTripForm) {
return {
purpose: form.objective,
place: form.travelLocation,
departureDate: form.departureTime ? new Date(form.departureTime).toISOString() : '',
returnDate: form.returnTime ? new Date(form.returnTime).toISOString() : '',
vehicle: form.vehicle,
transportationFee: form.vehicleEstimatedCost,
accommodation: form.residence,
accommodationCost: form.residenceEstimatedCost,
advancePaymentAmount: form.advancePaymentAmount,
totalBudget: form.generalBudget,
email: form.email,
}
}
...@@ -10,3 +10,5 @@ export interface TimbreLanguageInfoItem { ...@@ -10,3 +10,5 @@ export interface TimbreLanguageInfoItem {
matchLang: string matchLang: string
timbreInfo: TimbreInfoItem[] timbreInfo: TimbreInfoItem[]
} }
export type SmartFormTypes = 'BusinessTripForm'
...@@ -13,6 +13,9 @@ import { useLayoutConfig } from '@/composables/useLayoutConfig' ...@@ -13,6 +13,9 @@ import { useLayoutConfig } from '@/composables/useLayoutConfig'
import { TEXTTOSPEECH_WS_URL } from '@/config/base-url' import { TEXTTOSPEECH_WS_URL } from '@/config/base-url'
import WebSocketCtr from '@/utils/web-socket-ctr' import WebSocketCtr from '@/utils/web-socket-ctr'
import { ChannelType } from '@/enums/channel' import { ChannelType } from '@/enums/channel'
import { smartFormTypeConverter } from '@/views/personal-space/personal-app-setting/components/agent-config/agent-preview/components/smart-forms/utils/smart-forms'
import { SmartFormTypes } from '@/views/personal-space/personal-app-setting/components/agent-config/types'
import { SmartFormDisplayFormat } from '@/views/personal-space/personal-app-setting/components/agent-config/agent-preview/components/smart-forms/types/types'
interface Props { interface Props {
agentId: string agentId: string
...@@ -43,6 +46,7 @@ const emit = defineEmits<{ ...@@ -43,6 +46,7 @@ const emit = defineEmits<{
resetContinueQuestionList: [] resetContinueQuestionList: []
audioPlay: [messageItem: ConversationMessageItem, requestId?: string] audioPlay: [messageItem: ConversationMessageItem, requestId?: string]
audioPause: [] audioPause: []
smartFormsStatusFreezeCheck: [value: SmartFormTypes]
}>() }>()
const { isMobile } = useLayoutConfig() const { isMobile } = useLayoutConfig()
...@@ -63,6 +67,7 @@ const sentenceExtractCheckEnabled = ref(false) ...@@ -63,6 +67,7 @@ const sentenceExtractCheckEnabled = ref(false)
const assistantFullAnswerContent = ref('') const assistantFullAnswerContent = ref('')
const sentenceSpeechException = ref(false) const sentenceSpeechException = ref(false)
const messageAudioLoading = ref(false) const messageAudioLoading = ref(false)
const isSmartFormPlugins = ref(false)
const currentLatestMessageItemKeyMap = ref(new Map<'assistant' | 'user', string>()) const currentLatestMessageItemKeyMap = ref(new Map<'assistant' | 'user', string>())
let controller: AbortController | null = null let controller: AbortController | null = null
...@@ -221,6 +226,7 @@ function handleMessageSend(lastQuestionContent?: string) { ...@@ -221,6 +226,7 @@ function handleMessageSend(lastQuestionContent?: string) {
assistantFullAnswerContent.value = '' assistantFullAnswerContent.value = ''
sentenceSpeechException.value = false sentenceSpeechException.value = false
messageAudioLoading.value = false messageAudioLoading.value = false
isSmartFormPlugins.value = false
controller = new AbortController() controller = new AbortController()
...@@ -246,6 +252,31 @@ function handleMessageSend(lastQuestionContent?: string) { ...@@ -246,6 +252,31 @@ function handleMessageSend(lastQuestionContent?: string) {
// 插件 // 插件
if (data.function && data.function.name) { if (data.function && data.function.name) {
// 表单插件,展示表单内容
if (['travelForm'].includes(data.function.displayFormat)) {
emit(
'smartFormsStatusFreezeCheck',
smartFormTypeConverter(data.function.displayFormat as SmartFormDisplayFormat),
)
emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
pluginResult: {
displayFormat: data.function.displayFormat,
pluginName: data.function.name,
arguments: data.function.arguments,
pluginContent: data.function.result,
},
smartFormInfo: {
type: smartFormTypeConverter(data.function.displayFormat as SmartFormDisplayFormat),
isDisabled: false,
params: data.function.result,
},
})
emit('updatePageScroll')
isSmartFormPlugins.value = true
return
}
emit('updateSpecifyMessageItem', latestAssistantMessageKey, { emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
pluginResult: { pluginResult: {
displayFormat: data.function.displayFormat, displayFormat: data.function.displayFormat,
...@@ -280,7 +311,7 @@ function handleMessageSend(lastQuestionContent?: string) { ...@@ -280,7 +311,7 @@ function handleMessageSend(lastQuestionContent?: string) {
'', '',
) )
if (!sentenceExtractCheckEnabled.value && props.isEnableVoice) { if (!sentenceExtractCheckEnabled.value && props.isEnableVoice && !isSmartFormPlugins.value) {
sentenceExtract(latestAssistantMessageKey) sentenceExtract(latestAssistantMessageKey)
sentenceExtractCheckEnabled.value = true sentenceExtractCheckEnabled.value = true
messageAudioLoading.value = true messageAudioLoading.value = true
......
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, useTemplateRef } from 'vue' import { computed, provide, readonly, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { throttle } from 'lodash-es' import { throttle } from 'lodash-es'
import { Down } from '@icon-park/vue-next' import { Down } from '@icon-park/vue-next'
...@@ -13,9 +13,10 @@ import { PersonalAppConfigState } from '@/store/types/personal-app-config' ...@@ -13,9 +13,10 @@ import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { useLayoutConfig } from '@/composables/useLayoutConfig' import { useLayoutConfig } from '@/composables/useLayoutConfig'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { copyToClip } from '@/utils/copy' import { copyToClip } from '@/utils/copy'
import SmartForms from '@/views/personal-space/personal-app-setting/components/agent-config/agent-preview/components/smart-forms/index.vue'
interface Props { interface Props {
role: 'user' | 'assistant' messageItemId: string
messageItem: ConversationMessageItem messageItem: ConversationMessageItem
agentApplicationConfig: PersonalAppConfigState agentApplicationConfig: PersonalAppConfigState
} }
...@@ -35,10 +36,14 @@ const { isMobile } = useLayoutConfig() ...@@ -35,10 +36,14 @@ const { isMobile } = useLayoutConfig()
const markdownRenderRef = useTemplateRef<InstanceType<typeof MarkdownRender>>('markdownRenderRef') const markdownRenderRef = useTemplateRef<InstanceType<typeof MarkdownRender>>('markdownRenderRef')
provide('messageItemId', props.messageItemId)
const isShowReasoningContent = ref(true) const isShowReasoningContent = ref(true)
const isShowEditorDrawer = ref(false) const isShowEditorDrawer = ref(false)
const editorDrawerContent = ref('') const editorDrawerContent = ref('')
const displaySmartFormsList = readonly(['travelForm'])
const useAvatar = computed(() => { const useAvatar = computed(() => {
return userStore.userInfo.avatarUrl || 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/1725952917468.png' return userStore.userInfo.avatarUrl || 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/1725952917468.png'
}) })
...@@ -50,12 +55,31 @@ const assistantAvatar = computed(() => { ...@@ -50,12 +55,31 @@ const assistantAvatar = computed(() => {
) )
}) })
const isAgentMessage = computed(() => {
return props.messageItem.role === 'assistant'
})
const currentBubbleTextColor = computed(() => {
return isAgentMessage.value ? '#192338' : '#fff'
})
const isShowSmartForms = computed(() => {
if (props.messageItem?.pluginResult?.displayFormat) {
return displaySmartFormsList.includes(props.messageItem?.pluginResult?.displayFormat)
}
return false
})
const messageAuthor = computed(() => {
return isAgentMessage.value ? props.messageItem.modelName || 'AI' : userStore.userInfo.nickName
})
const timbreEnabled = computed(() => { const timbreEnabled = computed(() => {
return !!props.agentApplicationConfig.voiceConfig.timbreId return !!props.agentApplicationConfig.voiceConfig.timbreId
}) })
const isShowAudioControl = computed(() => { const isShowAudioControl = computed(() => {
return props.role === 'assistant' && !props.messageItem.isVoiceLoading && timbreEnabled.value return isAgentMessage.value && !props.messageItem.isVoiceLoading && timbreEnabled.value
}) })
const isPlayableAudio = computed(() => { const isPlayableAudio = computed(() => {
...@@ -64,7 +88,7 @@ const isPlayableAudio = computed(() => { ...@@ -64,7 +88,7 @@ const isPlayableAudio = computed(() => {
const isShowVoiceLoading = computed(() => { const isShowVoiceLoading = computed(() => {
return ( return (
props.role === 'assistant' && isAgentMessage.value &&
(props.messageItem.isAnswerResponseLoading || (props.messageItem.isVoiceLoading && timbreEnabled.value)) (props.messageItem.isAnswerResponseLoading || (props.messageItem.isVoiceLoading && timbreEnabled.value))
) )
}) })
...@@ -113,14 +137,11 @@ const handleContentCopy = throttle( ...@@ -113,14 +137,11 @@ const handleContentCopy = throttle(
<template> <template>
<div <div
class="mb-5 flex last:mb-0" class="mb-5 flex last:mb-0"
:class="[ :class="[isMobile ? 'flex-row-reverse' : 'flex-row', isAgentMessage && isMobile ? 'justify-end' : 'justify-start']"
isMobile ? 'flex-row-reverse' : 'flex-row',
role === 'assistant' && isMobile ? 'justify-end' : 'justify-start',
]"
> >
<NImage <NImage
v-show="!isMobile" v-show="!isMobile"
:src="role === 'user' ? useAvatar : assistantAvatar" :src="isAgentMessage ? assistantAvatar : useAvatar"
preview-disabled preview-disabled
:width="32" :width="32"
:height="32" :height="32"
...@@ -129,11 +150,12 @@ const handleContentCopy = throttle( ...@@ -129,11 +150,12 @@ const handleContentCopy = throttle(
/> />
<div <div
v-if="!isShowSmartForms"
class="flex flex-col overflow-x-auto" class="flex flex-col overflow-x-auto"
:class="[isMobile && role === 'user' ? 'items-end' : 'items-start', isMobile ? 'w-full' : '']" :class="[isMobile && !isAgentMessage ? 'items-end' : 'items-start', isMobile ? 'w-full' : '']"
> >
<!-- 大模型深度思考 --> <!-- 大模型深度思考 -->
<template v-if="role === 'assistant' && isDeepSeekR1"> <template v-if="isAgentMessage && isDeepSeekR1">
<div class="my-[7px] select-none text-[14px]"> <div class="my-[7px] select-none text-[14px]">
<div class="inline-flex cursor-pointer" @click="handleShowReasoningContentSwitch"> <div class="inline-flex cursor-pointer" @click="handleShowReasoningContentSwitch">
<span v-if="messageItem.isTextContentLoading" class="mr-[6px]"> <span v-if="messageItem.isTextContentLoading" class="mr-[6px]">
...@@ -174,17 +196,17 @@ const handleContentCopy = throttle( ...@@ -174,17 +196,17 @@ const handleContentCopy = throttle(
<!-- 模型内容 --> <!-- 模型内容 -->
<div class="flex min-w-[80px] flex-col" :class="[isMobile ? 'max-w-[calc(100%-20px)]' : 'max-w-full']"> <div class="flex min-w-[80px] flex-col" :class="[isMobile ? 'max-w-[calc(100%-20px)]' : 'max-w-full']">
<div <div
class="w-full flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]" class="w-full flex-wrap rounded-xl border border-[#9EA3FF] px-4 py-[11px]"
:class="[role === 'user' ? 'user-content-container bg-[#777EF9] text-white' : 'bg-white text-[#333]']" :class="[isAgentMessage ? 'bg-white text-[#333]' : 'user-content-container bg-[#777EF9] text-white']"
> >
<img <img
v-show="role === 'user' && messageItem.imageUrl" v-show="!isAgentMessage && messageItem.imageUrl"
:src="messageItem.imageUrl" :src="messageItem.imageUrl"
class="max-h-[120px]! mb-[11px] rounded-[10px] object-contain" class="max-h-[120px]! mb-[11px] rounded-[10px] object-contain"
/> />
<!-- 插件返回结果 --> <!-- 插件返回结果 -->
<div v-show="role === 'assistant' && messageItem?.pluginResult?.pluginName" class="mb-[11px] w-full"> <div v-show="isAgentMessage && messageItem?.pluginResult?.pluginName" class="mb-[11px] w-full">
<ExecutePluginRender <ExecutePluginRender
:display-format="messageItem.pluginResult?.displayFormat || 'none'" :display-format="messageItem.pluginResult?.displayFormat || 'none'"
:name="messageItem.pluginResult?.pluginName!" :name="messageItem.pluginResult?.pluginName!"
...@@ -215,7 +237,7 @@ const handleContentCopy = throttle( ...@@ -215,7 +237,7 @@ const handleContentCopy = throttle(
? t('common_module.dialogue_module.empty_message_content') ? t('common_module.dialogue_module.empty_message_content')
: messageItem.textContent : messageItem.textContent
" "
:color="role === 'user' ? '#fff' : '#192338'" :color="currentBubbleTextColor"
/> />
</p> </p>
...@@ -280,7 +302,7 @@ const handleContentCopy = throttle( ...@@ -280,7 +302,7 @@ const handleContentCopy = throttle(
<div></div> <div></div>
<div <div
v-show="role === 'assistant' && messageItem.textContent && !messageItem.isAnswerResponseLoading" v-show="isAgentMessage && messageItem.textContent && !messageItem.isAnswerResponseLoading"
class="py-[10px] pr-[14px] text-end" class="py-[10px] pr-[14px] text-end"
> >
<i <i
...@@ -295,6 +317,14 @@ const handleContentCopy = throttle( ...@@ -295,6 +317,14 @@ const handleContentCopy = throttle(
</div> </div>
</div> </div>
</div> </div>
<SmartForms
v-else
:message-author="messageAuthor"
:is-agent-message="isAgentMessage"
:message-item="messageItem"
:current-bubble-text-color="currentBubbleTextColor"
/>
</div> </div>
<EditorDrawer v-model:is-show-editor-drawer="isShowEditorDrawer" v-model:content-edit="editorDrawerContent" /> <EditorDrawer v-model:is-show-editor-drawer="isShowEditorDrawer" v-model:content-edit="editorDrawerContent" />
......
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, useTemplateRef, watch } from 'vue' import { computed, nextTick, provide, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useElementVisibility } from '@vueuse/core' import { useElementVisibility } from '@vueuse/core'
import MessageItem from './message-item.vue' import MessageItem from './message-item.vue'
...@@ -20,9 +20,10 @@ interface Props { ...@@ -20,9 +20,10 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
defineEmits<{ const emit = defineEmits<{
audioPlay: [messageItem: ConversationMessageItem] audioPlay: [messageItem: ConversationMessageItem]
audioPause: [] audioPause: []
updateSpecifyMessageItem: [messageId: string, newMessageItem: Partial<ConversationMessageItem>]
}>() }>()
const { t } = useI18n() const { t } = useI18n()
...@@ -33,6 +34,12 @@ const { scrollRef, scrollToBottom } = useScroll() ...@@ -33,6 +34,12 @@ const { scrollRef, scrollToBottom } = useScroll()
const backBottomBtnFlagRef = useTemplateRef<HTMLDivElement | null>('backBottomBtnFlagRef') const backBottomBtnFlagRef = useTemplateRef<HTMLDivElement | null>('backBottomBtnFlagRef')
const isNotShowBackBottomBtn = useElementVisibility(backBottomBtnFlagRef) const isNotShowBackBottomBtn = useElementVisibility(backBottomBtnFlagRef)
provide('updateSpecifyMessageItem', {
updateSpecifyMessageItem: (messageId: string, newMessageItem: Partial<ConversationMessageItem>) => {
emit('updateSpecifyMessageItem', messageId, newMessageItem)
},
})
const isShowContinueQuestion = computed(() => { const isShowContinueQuestion = computed(() => {
return ( return (
props.continuousQuestionStatus === 'default' && props.continuousQuestionStatus === 'default' &&
...@@ -71,7 +78,7 @@ function handleScrollToBottom() { ...@@ -71,7 +78,7 @@ function handleScrollToBottom() {
<MessageItem <MessageItem
v-for="[key, messageItem] in messageList" v-for="[key, messageItem] in messageList"
:key="key" :key="key"
:role="messageItem.role" :message-item-id="key"
:message-item="messageItem" :message-item="messageItem"
:agent-application-config="agentApplicationConfig" :agent-application-config="agentApplicationConfig"
@audio-play="() => $emit('audioPlay', messageItem)" @audio-play="() => $emit('audioPlay', messageItem)"
......
...@@ -22,6 +22,7 @@ import { useLayoutConfig } from '@/composables/useLayoutConfig' ...@@ -22,6 +22,7 @@ import { useLayoutConfig } from '@/composables/useLayoutConfig'
import { fetchGetMemberInfoById } from '@/apis/user' import { fetchGetMemberInfoById } from '@/apis/user'
import { UserInfo } from '@/store/types/user' import { UserInfo } from '@/store/types/user'
import { validBrowser } from '@/utils/browser-detection' import { validBrowser } from '@/utils/browser-detection'
import { SmartFormTypes } from '../personal-space/personal-app-setting/components/agent-config/types'
const { t } = useI18n() const { t } = useI18n()
...@@ -177,9 +178,7 @@ function handleUpdateSpecifyMessageItem(messageId: string, newMessageItem: Parti ...@@ -177,9 +178,7 @@ function handleUpdateSpecifyMessageItem(messageId: string, newMessageItem: Parti
} }
Object.entries<ValueOf<typeof newMessageItem>>(newMessageItem).forEach(([key, value]) => { Object.entries<ValueOf<typeof newMessageItem>>(newMessageItem).forEach(([key, value]) => {
if (Object.prototype.hasOwnProperty.call(currentMessageItemInfo, key)) { ;(currentMessageItemInfo as any)[key as keyof ConversationMessageItem] = value
;(currentMessageItemInfo as any)[key as keyof ConversationMessageItem] = value
}
}) })
} }
} }
...@@ -313,6 +312,17 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -313,6 +312,17 @@ function handleAudioPause(isClearMessageList = false) {
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
} }
} }
function onSmartFormsStatusFreezeCheck(smartFormType: SmartFormTypes) {
/* 重置智能表单项目 */
if (smartFormType) {
messageList.value.forEach((item) => {
if (item.smartFormInfo && item.smartFormInfo.type === smartFormType) {
item.smartFormInfo.isDisabled = true
}
})
}
}
</script> </script>
<template> <template>
...@@ -352,6 +362,7 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -352,6 +362,7 @@ function handleAudioPause(isClearMessageList = false) {
:create-continue-questions-exception="createContinueQuestionsException" :create-continue-questions-exception="createContinueQuestionsException"
@audio-play="handleAudioPlay" @audio-play="handleAudioPlay"
@audio-pause="handleAudioPause" @audio-pause="handleAudioPause"
@update-specify-message-item="handleUpdateSpecifyMessageItem"
/> />
</div> </div>
</div> </div>
...@@ -380,6 +391,7 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -380,6 +391,7 @@ function handleAudioPause(isClearMessageList = false) {
@reset-continue-question-list="handleResetContinueQuestionList" @reset-continue-question-list="handleResetContinueQuestionList"
@audio-play="handleAudioPlay" @audio-play="handleAudioPlay"
@audio-pause="handleAudioPause" @audio-pause="handleAudioPause"
@smart-forms-status-freeze-check="onSmartFormsStatusFreezeCheck"
/> />
</div> </div>
</div> </div>
......
...@@ -12,6 +12,8 @@ declare interface DBChainResultItem { ...@@ -12,6 +12,8 @@ declare interface DBChainResultItem {
status: string status: string
} }
declare type PluginDisplayFormatType = 'json' | 'markdown' | 'none' | 'travelForm'
declare interface ConversationMessageItem { declare interface ConversationMessageItem {
timestamp: number timestamp: number
role: 'user' | 'assistant' role: 'user' | 'assistant'
...@@ -29,9 +31,14 @@ declare interface ConversationMessageItem { ...@@ -29,9 +31,14 @@ declare interface ConversationMessageItem {
knowledgeContentResult: KnowledgeContentResultItem[] knowledgeContentResult: KnowledgeContentResultItem[]
dbChainSQLContent: string dbChainSQLContent: string
pluginResult?: { pluginResult?: {
displayFormat: 'json' | 'markdown' | 'none' displayFormat: PluginDisplayFormatType
pluginName: string pluginName: string
arguments: string arguments: string
pluginContent: string pluginContent: string
} }
smartFormInfo?: {
type: 'BusinessTripForm' | ''
isDisabled: boolean
params: 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