Commit 9083858c authored by nick zheng's avatar nick zheng

Merge branch 'beta' into 'master'

fix: 语音交互优化

See merge request !98
parents bd14bf2f 2bd4cd34
...@@ -100,6 +100,8 @@ common_module: ...@@ -100,6 +100,8 @@ common_module:
voice_auto_play: 'Voice auto play' voice_auto_play: 'Voice auto play'
start_playing: 'play' start_playing: 'play'
stop_playing: 'stop' stop_playing: 'stop'
unplayable: 'unplayable'
unplayable_tip: 'The voice setting do not match the model output language'
response_error: 'Response error' response_error: 'Response error'
agent_exception: 'Agent exception, please try again later!' agent_exception: 'Agent exception, please try again later!'
equity: 'Equity' equity: 'Equity'
...@@ -116,6 +118,8 @@ common_module: ...@@ -116,6 +118,8 @@ common_module:
cancel_associate_file_tip: 'No longer answer around this file' cancel_associate_file_tip: 'No longer answer around this file'
upload_file_limit: 'Only a single file can be uploaded in PDF, DOC, DOCX, MD, TXT format, up to 10MB' upload_file_limit: 'Only a single file can be uploaded in PDF, DOC, DOCX, MD, TXT format, up to 10MB'
overwrite_file_tip: 'The newly uploaded file overwrites the original file, whether to continue uploading' 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'
data_table_module: data_table_module:
action: 'Controls' action: 'Controls'
...@@ -294,9 +298,10 @@ personal_space_module: ...@@ -294,9 +298,10 @@ personal_space_module:
memory_fragment_delete_row_tip_content: 'After data deletion, it cannot be revoked. Are you sure you want to delete it?' memory_fragment_delete_row_tip_content: 'After data deletion, it cannot be revoked. Are you sure you want to delete it?'
add_knowledge_successfully: 'Data set {0} was added successfully' add_knowledge_successfully: 'Data set {0} was added successfully'
remove_knowledge_successfully: 'Data set {0} was removed successfully' remove_knowledge_successfully: 'Data set {0} was removed successfully'
setting_timbre: 'Setting timbre' setting_voice: 'Setting voice'
setting_timbre_message: 'You can set the language and timbre' setting_voice_message: 'You can set the language and tone. If the selected language is inconsistent with the model language, the speech cannot be played'
setting_timbre_desc: 'You can customize the voice and timbre for voice broadcast, and you can set only one at a time.' setting_voice_desc: 'You can customize the voice and timbre for voice broadcast, and you can set only one at a time.'
currently_only_one_voice_can_be_set: 'Currently, you can set only one voice'
memory_variable_modal: memory_variable_modal:
edit_memory_variable: 'Edit memory variable' edit_memory_variable: 'Edit memory variable'
......
...@@ -99,6 +99,8 @@ common_module: ...@@ -99,6 +99,8 @@ common_module:
voice_auto_play: '语音自动播放' voice_auto_play: '语音自动播放'
start_playing: '开始播放' start_playing: '开始播放'
stop_playing: '停止播放' stop_playing: '停止播放'
unplayable: '不可播放'
unplayable_tip: '语音设置与模型输出语言不匹配'
response_error: '响应错误' response_error: '响应错误'
agent_exception: '应用异常,请稍后重试!' agent_exception: '应用异常,请稍后重试!'
equity: '权益' equity: '权益'
...@@ -115,6 +117,8 @@ common_module: ...@@ -115,6 +117,8 @@ common_module:
cancel_associate_file_tip: '不再围绕这个文件回答' cancel_associate_file_tip: '不再围绕这个文件回答'
upload_file_limit: '仅支持上传单个文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB' upload_file_limit: '仅支持上传单个文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB'
overwrite_file_tip: '新上传的文件会覆盖原有文件,是否继续上传' overwrite_file_tip: '新上传的文件会覆盖原有文件,是否继续上传'
stop_playing_and_then_operate: '音频播放中,请停止播放后再操作'
do_not_operate_until_the_reply_is_complete: '回复完成后再操作'
data_table_module: data_table_module:
action: '操作' action: '操作'
...@@ -292,9 +296,10 @@ personal_space_module: ...@@ -292,9 +296,10 @@ personal_space_module:
memory_fragment_delete_row_tip_content: '数据删除后不可撤销,确定要删除吗?' memory_fragment_delete_row_tip_content: '数据删除后不可撤销,确定要删除吗?'
add_knowledge_successfully: '数据集 {0} 添加成功' add_knowledge_successfully: '数据集 {0} 添加成功'
remove_knowledge_successfully: '数据集 {0} 移除成功' remove_knowledge_successfully: '数据集 {0} 移除成功'
setting_timbre: '设置音色' setting_voice: '设置语音'
setting_timbre_message: '你可以设置语言和音色' setting_voice_message: '你可以设置语言和音色,若所选语言与模型语言不一致,则无法播放语音'
setting_timbre_desc: '您可自定义语音及音色,用于语音播报,且每次仅可设置一种。' setting_voice_desc: '您可自定义语音及音色,用于语音播报,且每次仅可设置一种。'
currently_only_one_voice_can_be_set: '当前仅可设置一种声音'
memory_variable_modal: memory_variable_modal:
edit_memory_variable: '编辑记忆变量' edit_memory_variable: '编辑记忆变量'
......
...@@ -99,6 +99,8 @@ common_module: ...@@ -99,6 +99,8 @@ common_module:
voice_auto_play: '語音自動播放' voice_auto_play: '語音自動播放'
start_playing: '開始播放' start_playing: '開始播放'
stop_playing: '停止播放' stop_playing: '停止播放'
unplayable: '不可播放'
unplayable_tip: '語音設置與模型輸出語言不匹配'
response_error: '響應錯誤' response_error: '響應錯誤'
agent_exception: '應用異常,請稍後重試!' agent_exception: '應用異常,請稍後重試!'
equity: '权益' equity: '权益'
...@@ -115,6 +117,8 @@ common_module: ...@@ -115,6 +117,8 @@ common_module:
cancel_associate_file_tip: '不再圍繞這個文件回答' cancel_associate_file_tip: '不再圍繞這個文件回答'
upload_file_limit: '僅支持上傳單個文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB' upload_file_limit: '僅支持上傳單個文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB'
overwrite_file_tip: '新上傳的文件會覆蓋原有文件,是否繼續上傳' overwrite_file_tip: '新上傳的文件會覆蓋原有文件,是否繼續上傳'
stop_playing_and_then_operate: '音頻播放中,請停止播放後再操作'
do_not_operate_until_the_reply_is_complete: '回覆完成後再操作'
data_table_module: data_table_module:
action: '操作' action: '操作'
...@@ -292,9 +296,10 @@ personal_space_module: ...@@ -292,9 +296,10 @@ personal_space_module:
memory_fragment_delete_row_tip_content: '數據删除後不可撤銷,確定要删除嗎?' memory_fragment_delete_row_tip_content: '數據删除後不可撤銷,確定要删除嗎?'
add_knowledge_successfully: '數據集 {0} 添加成功' add_knowledge_successfully: '數據集 {0} 添加成功'
remove_knowledge_successfully: '數據集 {0} 移除成功' remove_knowledge_successfully: '數據集 {0} 移除成功'
setting_timbre: '設置音色' setting_voice: '設置語音'
setting_timbre_message: '你可以設置語言和音色' setting_voice_message: '你可以設置語言和音色,若所選語言與模型語言不一致,則無法播放語音'
setting_timbre_desc: '您可自定義語音及音色,用於語音播報,且每次僅可設置一種。' setting_voice_desc: '您可自定義語音及音色,用於語音播報,且每次僅可設置一種。'
currently_only_one_voice_can_be_set: '當前僅可設置一種聲音'
memory_variable_modal: memory_variable_modal:
edit_memory_variable: '編輯記憶變數' edit_memory_variable: '編輯記憶變數'
......
...@@ -39,7 +39,6 @@ export default class WebSocketCtr { ...@@ -39,7 +39,6 @@ export default class WebSocketCtr {
this.socket.close() this.socket.close()
this.socket = null this.socket = null
} }
window.$message.error(t('common_module.agent_exception'))
this.onMessageError() this.onMessageError()
......
...@@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n' ...@@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Emitter } from 'mitt' import { Emitter } from 'mitt'
import { Howl } from 'howler' import { Howl } from 'howler'
import { ValueOf } from 'type-fest'
import MessageList from './components/message-list.vue' import MessageList from './components/message-list.vue'
import FooterInput from './components/footer-input.vue' import FooterInput from './components/footer-input.vue'
import MemoryPreviewModal from './components/memory-preview-modal.vue' import MemoryPreviewModal from './components/memory-preview-modal.vue'
...@@ -26,15 +27,15 @@ const emitter = inject<Emitter<MittEvents>>('emitter') ...@@ -26,15 +27,15 @@ const emitter = inject<Emitter<MittEvents>>('emitter')
const messageListRef = ref<InstanceType<typeof MessageList> | null>(null) const messageListRef = ref<InstanceType<typeof MessageList> | null>(null)
const footerInputRef = ref<InstanceType<typeof FooterInput> | null>(null) const footerInputRef = ref<InstanceType<typeof FooterInput> | null>(null)
const messageList = ref<ConversationMessageItem[]>([]) const messageList = ref(new Map<string, ConversationMessageItem>())
const continuousQuestionStatus = ref<'default' | 'close'>(personalAppConfigStore.commConfig.continuousQuestionStatus) const continuousQuestionStatus = ref<'default' | 'close'>(personalAppConfigStore.commConfig.continuousQuestionStatus)
const continuousQuestionList = ref<string[]>([]) const continuousQuestionList = ref<string[]>([])
const isShowMemoryPreviewModal = ref(false) const isShowMemoryPreviewModal = ref(false)
const selectedMemoryTabName = ref('memoryVariable') const selectedMemoryTabName = ref('memoryVariable')
const answerAudioAutoPlaying = ref(personalAppConfigStore.voiceConfig.defaultOpen === 'Y') const answerAudioAutoPlay = ref(personalAppConfigStore.voiceConfig.defaultOpen === 'Y')
const answerAudioPlaying = ref(false) // 语音播放中
const currentPlayMessageItem = ref<ConversationMessageItem | null>(null) const currentPlayMessageItem = ref<ConversationMessageItem | null>(null)
const currentPlayAudioFragmentSerialNo = ref(0) const currentPlayAudioFragmentSerialNo = ref(0)
const currentSoundCtl = shallowRef<Howl | null>(null) const currentSoundCtl = shallowRef<Howl | null>(null)
...@@ -48,7 +49,7 @@ onMounted(() => { ...@@ -48,7 +49,7 @@ onMounted(() => {
emitter?.on('resetAgent', () => { emitter?.on('resetAgent', () => {
handleAudioPause() handleAudioPause()
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
messageList.value = [] messageList.value.clear()
}) })
}) })
...@@ -56,23 +57,34 @@ onUnmounted(() => { ...@@ -56,23 +57,34 @@ onUnmounted(() => {
emitter?.off('resetAgent') emitter?.off('resetAgent')
handleAudioPause() handleAudioPause()
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
messageList.value = [] messageList.value.clear()
}) })
function handleAddMessageItem(messageItem: ConversationMessageItem) { function handleAddMessageItem(messageId: string, messageItem: ConversationMessageItem) {
messageList.value.push(messageItem) messageList.value.set(messageId, messageItem)
} }
function handleUpdateSpecifyMessageItem(messageItemIndex: number, newObj: Partial<ConversationMessageItem>) { function handleUpdateSpecifyMessageItem(messageId: string, newMessageItem: Partial<ConversationMessageItem>) {
if (messageList.value[messageItemIndex]) { const currentMessageItemInfo = messageList.value.get(messageId)
Object.entries(newObj).forEach(([k, v]) => {
;(messageList.value[messageItemIndex] as any)[k as keyof typeof newObj] = v if (currentMessageItemInfo) {
const updatePropertyLength = Object.keys(newMessageItem).length
if (updatePropertyLength > 4) {
messageList.value.set(messageId, Object.assign({}, currentMessageItemInfo, newMessageItem))
return
}
Object.entries<ValueOf<typeof newMessageItem>>(newMessageItem).forEach(([key, value]) => {
if (Object.prototype.hasOwnProperty.call(currentMessageItemInfo, key)) {
;(currentMessageItemInfo as any)[key as keyof ConversationMessageItem] = value
}
}) })
} }
} }
function handleDeleteLastMessageItem() { function handleDeleteLastMessageItem(messageId: string) {
messageList.value.pop() messageList.value.delete(messageId)
} }
function handleUpdatePageScroll() { function handleUpdatePageScroll() {
...@@ -88,7 +100,8 @@ function handleClearAllMessage() { ...@@ -88,7 +100,8 @@ function handleClearAllMessage() {
.then(() => { .then(() => {
handleAudioPause() handleAudioPause()
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
messageList.value = [] messageList.value.clear()
answerAudioPlaying.value = false
window.$message.success(t('common_module.clear_success_message')) window.$message.success(t('common_module.clear_success_message'))
}) })
} }
...@@ -135,6 +148,7 @@ function howlSoundFactory(url: string) { ...@@ -135,6 +148,7 @@ function howlSoundFactory(url: string) {
preload: true, preload: true,
autoplay: true, autoplay: true,
onplay: () => { onplay: () => {
answerAudioPlaying.value = true
currentSoundCtl.value = soundCtl currentSoundCtl.value = soundCtl
if (currentPlayMessageItem.value) { if (currentPlayMessageItem.value) {
...@@ -152,6 +166,7 @@ function howlSoundFactory(url: string) { ...@@ -152,6 +166,7 @@ function howlSoundFactory(url: string) {
currentPlayAudioFragmentSerialNo.value > currentPlayMessageItem.value.voiceFragmentUrlList.length - 1 currentPlayAudioFragmentSerialNo.value > currentPlayMessageItem.value.voiceFragmentUrlList.length - 1
) { ) {
currentPlayMessageItem.value.isVoicePlaying = false currentPlayMessageItem.value.isVoicePlaying = false
answerAudioPlaying.value = false
} }
let audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value] let audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value]
...@@ -205,9 +220,10 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -205,9 +220,10 @@ function handleAudioPause(isClearMessageList = false) {
} }
currentPlayMessageItem.value && (currentPlayMessageItem.value.isVoicePlaying = false) currentPlayMessageItem.value && (currentPlayMessageItem.value.isVoicePlaying = false)
answerAudioPlaying.value = false
if (isClearMessageList) { if (isClearMessageList) {
messageList.value = [] messageList.value.clear()
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
} }
} }
...@@ -231,11 +247,7 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -231,11 +247,7 @@ function handleAudioPause(isClearMessageList = false) {
</template> </template>
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<span>{{ t('common_module.voice_auto_play') }}</span> <span>{{ t('common_module.voice_auto_play') }}</span>
<n-switch <n-switch v-model:value="answerAudioAutoPlay" size="small" @update:value="handleUpdateAudioAutoPlaying">
v-model:value="answerAudioAutoPlaying"
size="small"
@update:value="handleUpdateAudioAutoPlaying"
>
<template #checked> {{ t('common_module.open') }} </template> <template #checked> {{ t('common_module.open') }} </template>
<template #unchecked> {{ t('common_module.close') }} </template> <template #unchecked> {{ t('common_module.close') }} </template>
</n-switch> </n-switch>
...@@ -294,11 +306,11 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -294,11 +306,11 @@ function handleAudioPause(isClearMessageList = false) {
</div> </div>
<div class="flex w-full flex-1 overflow-hidden"> <div class="flex w-full flex-1 overflow-hidden">
<div v-show="messageList.length === 0" class="flex w-full"> <div v-show="messageList.size === 0" class="flex w-full">
<Preamble /> <Preamble />
</div> </div>
<div v-show="messageList.length > 0" class="w-full"> <div v-show="messageList.size > 0" class="w-full">
<MessageList <MessageList
ref="messageListRef" ref="messageListRef"
:message-list="messageList" :message-list="messageList"
...@@ -314,7 +326,8 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -314,7 +326,8 @@ function handleAudioPause(isClearMessageList = false) {
ref="footerInputRef" ref="footerInputRef"
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:message-list="messageList" :message-list="messageList"
:answer-audio-auto-playing="answerAudioAutoPlaying" :answer-audio-auto-play="answerAudioAutoPlay"
:answer-audio-playing="answerAudioPlaying"
@add-message-item="handleAddMessageItem" @add-message-item="handleAddMessageItem"
@update-specify-message-item="handleUpdateSpecifyMessageItem" @update-specify-message-item="handleUpdateSpecifyMessageItem"
@delete-last-message-item="handleDeleteLastMessageItem" @delete-last-message-item="handleDeleteLastMessageItem"
...@@ -323,6 +336,7 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -323,6 +336,7 @@ function handleAudioPause(isClearMessageList = false) {
@create-continue-questions="handleCreateContinueQuestions" @create-continue-questions="handleCreateContinueQuestions"
@update-continuous-question-status="handleUpdateContinueQuestionStatus" @update-continuous-question-status="handleUpdateContinueQuestionStatus"
@audio-play="handleAudioPlay" @audio-play="handleAudioPlay"
@audio-pause="handleAudioPause"
/> />
<MemoryPreviewModal v-model="isShowMemoryPreviewModal" :data="selectedMemoryTabName" /> <MemoryPreviewModal v-model="isShowMemoryPreviewModal" :data="selectedMemoryTabName" />
......
...@@ -31,13 +31,27 @@ const assistantAvatar = computed(() => { ...@@ -31,13 +31,27 @@ const assistantAvatar = computed(() => {
return personalAppConfigStore.baseInfo.agentAvatar return personalAppConfigStore.baseInfo.agentAvatar
}) })
const timbreEnabled = computed(() => {
return !!personalAppConfigStore.voiceConfig.timbreId
})
const isShowAudioControl = computed(() => { const isShowAudioControl = computed(() => {
return ( return props.role === 'assistant' && !props.messageItem.isVoiceLoading
props.role === 'assistant' && !props.messageItem.isVoiceLoading && !!props.messageItem.voiceFragmentUrlList.length })
)
const isPlayableAudio = computed(() => {
return isShowAudioControl.value && !!props.messageItem.voiceFragmentUrlList.length
})
const isShowVoiceLoading = computed(() => {
return props.role === 'assistant' && props.messageItem.isVoiceLoading && timbreEnabled.value
}) })
function handleAudioControl() { function handleAudioControl() {
if (!isPlayableAudio.value) {
return
}
if (props.messageItem.isVoicePlaying) { if (props.messageItem.isVoicePlaying) {
emit('audioPause') emit('audioPause')
} else { } else {
...@@ -86,14 +100,30 @@ function handleAudioControl() { ...@@ -86,14 +100,30 @@ function handleAudioControl() {
<div <div
v-show="isShowAudioControl" v-show="isShowAudioControl"
class="hover:text-theme-color text-font-color flex cursor-pointer items-center gap-0.5 hover:opacity-80" class="text-font-color flex items-center gap-0.5"
:class="isPlayableAudio ? 'hover:text-theme-color cursor-pointer hover:opacity-80' : 'cursor-not-allowed'"
@click="handleAudioControl" @click="handleAudioControl"
> >
<i v-if="!messageItem.isVoicePlaying" class="iconfont icon-play text-[24px]" /> <i v-if="!messageItem.isVoicePlaying" class="iconfont icon-play text-[24px]" />
<div v-else class="mx-1.5 my-3 h-[12px] w-[12px] bg-[url(@/assets/images/playing.gif)] bg-[length:100%_100%]" /> <div v-else class="mx-1.5 my-3 h-[12px] w-[12px] bg-[url(@/assets/images/playing.gif)] bg-[length:100%_100%]" />
<span class="text-[12px]" :class="messageItem.isVoicePlaying ? 'text-theme-color' : ''">
<span
v-show="isPlayableAudio"
class="text-[12px]"
:class="messageItem.isVoicePlaying ? 'text-theme-color' : ''"
>
{{ messageItem.isVoicePlaying ? t('common_module.stop_playing') : t('common_module.start_playing') }} {{ messageItem.isVoicePlaying ? t('common_module.stop_playing') : t('common_module.start_playing') }}
</span> </span>
<n-popover style="max-width: 310px">
<template #trigger>
<span v-show="!isPlayableAudio" class="text-[12px]"> {{ t('common_module.unplayable') }} </span>
</template>
{{ t('common_module.unplayable_tip') }}
</n-popover>
</div>
<div v-if="isShowVoiceLoading" class="py-3.5 pl-6">
<CustomLoading />
</div> </div>
</div> </div>
</div> </div>
......
...@@ -5,7 +5,7 @@ import ContinueQuestion from './continue-question.vue' ...@@ -5,7 +5,7 @@ import ContinueQuestion from './continue-question.vue'
import { useScroll } from '@/composables/useScroll' import { useScroll } from '@/composables/useScroll'
interface Props { interface Props {
messageList: ConversationMessageItem[] messageList: Map<string, ConversationMessageItem>
continuousQuestionStatus: 'default' | 'close' continuousQuestionStatus: 'default' | 'close'
continuousQuestionList: string[] continuousQuestionList: string[]
} }
...@@ -22,8 +22,8 @@ const { scrollRef, scrollToBottom } = useScroll() ...@@ -22,8 +22,8 @@ const { scrollRef, scrollToBottom } = useScroll()
const isShowContinueQuestion = computed(() => { const isShowContinueQuestion = computed(() => {
return ( return (
props.continuousQuestionStatus === 'default' && props.continuousQuestionStatus === 'default' &&
props.messageList.length > 1 && props.messageList.size > 1 &&
!props.messageList[props.messageList.length - 1].isAnswerResponseLoading !Array.from(props.messageList.entries()).pop()?.[1].isAnswerResponseLoading
) )
}) })
...@@ -36,8 +36,8 @@ defineExpose({ ...@@ -36,8 +36,8 @@ defineExpose({
<main ref="scrollRef" class="h-full overflow-y-auto px-5"> <main ref="scrollRef" class="h-full overflow-y-auto px-5">
<div> <div>
<MessageItem <MessageItem
v-for="messageItem in messageList" v-for="[key, messageItem] in messageList"
:key="messageItem.timestamp" :key="key"
:role="messageItem.role" :role="messageItem.role"
:message-item="messageItem" :message-item="messageItem"
@audio-play="() => $emit('audioPlay', messageItem)" @audio-play="() => $emit('audioPlay', messageItem)"
......
...@@ -31,6 +31,8 @@ const timberFullName = computed(() => { ...@@ -31,6 +31,8 @@ const timberFullName = computed(() => {
return `${timbreInfoDetail.value?.timbreInfo?.[0]?.timbreName || '--'}(${t(currentLanguage?.label || 'common_module.sound')})` return `${timbreInfoDetail.value?.timbreInfo?.[0]?.timbreName || '--'}(${t(currentLanguage?.label || 'common_module.sound')})`
}) })
const isHasTimbreId = computed(() => !!voiceConfig.value.timbreId)
onMounted(() => { onMounted(() => {
voiceConfig.value.timbreId && handleGetTimberInfoDetail() voiceConfig.value.timbreId && handleGetTimberInfoDetail()
}) })
...@@ -49,6 +51,10 @@ function handleUpdateRoleConfigExpandedNames(expandedNames: string[]) { ...@@ -49,6 +51,10 @@ function handleUpdateRoleConfigExpandedNames(expandedNames: string[]) {
} }
function handleShowAssociatedTimbreModel() { function handleShowAssociatedTimbreModel() {
if (voiceConfig.value.timbreId) {
return
}
isShowTimbreSettingModal.value = true isShowTimbreSettingModal.value = true
timbreInfo = { language: 0, matchLang: '', timbreInfo: [] } timbreInfo = { language: 0, matchLang: '', timbreInfo: [] }
} }
...@@ -84,7 +90,7 @@ function handleEditAssociatedTimbreModel() { ...@@ -84,7 +90,7 @@ function handleEditAssociatedTimbreModel() {
<RightOne theme="filled" size="17" fill="#333" :stroke-width="3" /> <RightOne theme="filled" size="17" fill="#333" :stroke-width="3" />
</template> </template>
<NCollapseItem :title="t('common_module.sound')" name="timbre" class="my-[13px]!"> <NCollapseItem :title="t('common_module.voice')" name="timbre" class="my-[13px]!">
<template #header-extra> <template #header-extra>
<NTooltip trigger="hover"> <NTooltip trigger="hover">
<template #trigger> <template #trigger>
...@@ -92,16 +98,23 @@ function handleEditAssociatedTimbreModel() { ...@@ -92,16 +98,23 @@ function handleEditAssociatedTimbreModel() {
theme="outline" theme="outline"
size="22" size="22"
:stroke-width="3" :stroke-width="3"
class="text-theme-color cursor-pointer" class="text-theme-color"
:class="isHasTimbreId ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="handleShowAssociatedTimbreModel" @click="handleShowAssociatedTimbreModel"
/> />
</template> </template>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_timbre') }} {{
isHasTimbreId
? t(
'personal_space_module.agent_module.agent_setting_module.agent_config_module.currently_only_one_voice_can_be_set',
)
: t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_voice')
}}
</NTooltip> </NTooltip>
</template> </template>
<span v-show="!voiceConfig.timbreId" class="text-xs text-[#84868c]"> <span v-show="!voiceConfig.timbreId" class="text-xs text-[#84868c]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_timbre_desc') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_voice_desc') }}
</span> </span>
<div class="flex flex-1 flex-wrap items-center gap-[12px] overflow-hidden"> <div class="flex flex-1 flex-wrap items-center gap-[12px] overflow-hidden">
...@@ -153,7 +166,7 @@ function handleEditAssociatedTimbreModel() { ...@@ -153,7 +166,7 @@ function handleEditAssociatedTimbreModel() {
<TimbreSettingModal <TimbreSettingModal
v-model:is-show-modal="isShowTimbreSettingModal" v-model:is-show-modal="isShowTimbreSettingModal"
:btn-loading="false" :btn-loading="false"
:modal-title="t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_timbre')" :modal-title="t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_voice')"
:timbre-info="timbreInfo" :timbre-info="timbreInfo"
@confirm="handleUpdateTimbreId" @confirm="handleUpdateTimbreId"
/> />
......
...@@ -215,7 +215,7 @@ function handleAudioPause() { ...@@ -215,7 +215,7 @@ function handleAudioPause() {
> >
<template #content> <template #content>
<div class="text-gray-font-color mb-2"> <div class="text-gray-font-color mb-2">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_timbre_message') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_voice_message') }}
</div> </div>
<div v-show="!requestDataLoading" class="flex items-center gap-3"> <div v-show="!requestDataLoading" class="flex items-center gap-3">
......
This diff is collapsed.
...@@ -38,25 +38,27 @@ const assistantAvatar = computed(() => { ...@@ -38,25 +38,27 @@ const assistantAvatar = computed(() => {
) )
}) })
const isShowWebAudioControl = computed(() => { const timbreEnabled = computed(() => {
return ( return !!props.agentApplicationConfig.voiceConfig.timbreId
props.role === 'assistant' &&
!props.messageItem.isVoiceLoading &&
!isMobile.value &&
!!props.messageItem.voiceFragmentUrlList.length
)
}) })
const isShowMobileAudioControl = computed(() => { const isShowAudioControl = computed(() => {
return ( return props.role === 'assistant' && !props.messageItem.isVoiceLoading && timbreEnabled.value
props.role === 'assistant' && })
!props.messageItem.isVoiceLoading &&
isMobile.value && const isPlayableAudio = computed(() => {
!!props.messageItem.voiceFragmentUrlList.length return isShowAudioControl.value && !!props.messageItem.voiceFragmentUrlList.length
) })
const isShowWebVoiceLoading = computed(() => {
return props.role === 'assistant' && !isMobile.value && props.messageItem.isVoiceLoading && timbreEnabled.value
}) })
function handleAudioControl() { function handleAudioControl() {
if (!isPlayableAudio.value) {
return
}
if (props.messageItem.isVoicePlaying) { if (props.messageItem.isVoicePlaying) {
emit('audioPause') emit('audioPause')
} else { } else {
...@@ -107,32 +109,60 @@ function handleAudioControl() { ...@@ -107,32 +109,60 @@ function handleAudioControl() {
/> />
</p> </p>
<div v-show="role === 'assistant' && messageItem.isAnswerResponseLoading" class="mb-[5px] mt-4 px-4"> <div
v-show="
role === 'assistant' && (messageItem.isAnswerResponseLoading || (isMobile && messageItem.isVoiceLoading))
"
class="mb-[5px] mt-4 px-4"
>
<CustomLoading /> <CustomLoading />
</div> </div>
</div> </div>
<div v-show="isShowMobileAudioControl" class="mt-[13px] flex items-center gap-2"> <div v-show="isShowAudioControl && isMobile" class="mt-[13px] flex items-center gap-2">
<div <div
class="h-[18px] w-[18px] cursor-pointer" class="h-[18px] w-[18px] cursor-pointer"
:class="messageItem.isVoicePlaying ? 'bg-svg-pause' : 'bg-svg-play'" :class="messageItem.isVoicePlaying ? 'bg-svg-pause' : 'bg-svg-play'"
@click="handleAudioControl" @click="handleAudioControl"
/> />
<MusicWavesLoading v-show="messageItem.isVoicePlaying" bar-bg-color="#333" /> <MusicWavesLoading v-show="messageItem.isVoicePlaying && isPlayableAudio" bar-bg-color="#333" />
<n-popover style="max-width: 310px">
<template #trigger>
<span v-show="!isPlayableAudio" class="text-[12px]"> {{ t('common_module.unplayable') }} </span>
</template>
{{ t('common_module.unplayable_tip') }}
</n-popover>
</div> </div>
</div> </div>
<div <div
v-show="isShowWebAudioControl" v-show="isShowAudioControl && !isMobile"
class="hover:text-theme-color text-font-color flex cursor-pointer items-center gap-0.5 hover:opacity-80" class="text-font-color flex items-center gap-0.5"
:class="isPlayableAudio ? 'hover:text-theme-color cursor-pointer hover:opacity-80' : 'cursor-not-allowed'"
@click="handleAudioControl" @click="handleAudioControl"
> >
<i v-if="!messageItem.isVoicePlaying" class="iconfont icon-play text-[24px]" /> <i v-if="!messageItem.isVoicePlaying" class="iconfont icon-play text-[24px]" />
<div v-else class="mx-1.5 my-3 h-[12px] w-[12px] bg-[url(@/assets/images/playing.gif)] bg-[length:100%_100%]" /> <div v-else class="mx-1.5 my-3 h-[12px] w-[12px] bg-[url(@/assets/images/playing.gif)] bg-[length:100%_100%]" />
<span class="text-[12px]" :class="messageItem.isVoicePlaying ? 'text-theme-color' : ''">
<span
v-show="isPlayableAudio"
class="text-[12px]"
:class="messageItem.isVoicePlaying ? 'text-theme-color' : ''"
>
{{ messageItem.isVoicePlaying ? t('common_module.stop_playing') : t('common_module.start_playing') }} {{ messageItem.isVoicePlaying ? t('common_module.stop_playing') : t('common_module.start_playing') }}
</span> </span>
<n-popover style="max-width: 310px">
<template #trigger>
<span v-show="!isPlayableAudio" class="text-[12px]"> {{ t('common_module.unplayable') }} </span>
</template>
{{ t('common_module.unplayable_tip') }}
</n-popover>
</div>
<div v-if="isShowWebVoiceLoading" class="py-3.5 pl-5">
<CustomLoading />
</div> </div>
</div> </div>
</div> </div>
......
...@@ -6,7 +6,7 @@ import { PersonalAppConfigState } from '@/store/types/personal-app-config' ...@@ -6,7 +6,7 @@ import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { computed } from 'vue' import { computed } from 'vue'
interface Props { interface Props {
messageList: ConversationMessageItem[] messageList: Map<string, ConversationMessageItem>
agentApplicationConfig: PersonalAppConfigState agentApplicationConfig: PersonalAppConfigState
continuousQuestionStatus: 'default' | 'close' continuousQuestionStatus: 'default' | 'close'
continuousQuestionList: string[] continuousQuestionList: string[]
...@@ -24,8 +24,8 @@ const { scrollRef, scrollToBottom } = useScroll() ...@@ -24,8 +24,8 @@ const { scrollRef, scrollToBottom } = useScroll()
const isShowContinueQuestion = computed(() => { const isShowContinueQuestion = computed(() => {
return ( return (
props.continuousQuestionStatus === 'default' && props.continuousQuestionStatus === 'default' &&
props.messageList.length > 1 && props.messageList.size > 1 &&
!props.messageList[props.messageList.length - 1].isAnswerResponseLoading !Array.from(props.messageList.entries()).pop()?.[1].isAnswerResponseLoading
) )
}) })
...@@ -38,8 +38,8 @@ defineExpose({ ...@@ -38,8 +38,8 @@ defineExpose({
<main ref="scrollRef" class="h-full overflow-y-auto px-5"> <main ref="scrollRef" class="h-full overflow-y-auto px-5">
<div> <div>
<MessageItem <MessageItem
v-for="messageItem in messageList" v-for="[key, messageItem] in messageList"
:key="messageItem.timestamp" :key="key"
:role="messageItem.role" :role="messageItem.role"
:message-item="messageItem" :message-item="messageItem"
:agent-application-config="agentApplicationConfig" :agent-application-config="agentApplicationConfig"
......
...@@ -38,7 +38,7 @@ function handleToLogin() { ...@@ -38,7 +38,7 @@ function handleToLogin() {
<NButton <NButton
v-show="isLogin" v-show="isLogin"
type="primary" type="primary"
class="rounded-md! h-[32px]! text-xs! w-[80px]!" class="rounded-md! h-[32px]! text-xs! min-w-[80px]!"
@click="handleToCreateApplication" @click="handleToCreateApplication"
> >
{{ t('common_module.create_agent_btn_text') }} {{ t('common_module.create_agent_btn_text') }}
......
...@@ -3,6 +3,7 @@ import { computed, onMounted, onUnmounted, ref, shallowRef } from 'vue' ...@@ -3,6 +3,7 @@ import { computed, onMounted, onUnmounted, ref, shallowRef } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Howl } from 'howler' import { Howl } from 'howler'
import { ValueOf } from 'type-fest'
import PageHeader from './components/mobile-page-header.vue' import PageHeader from './components/mobile-page-header.vue'
import Preamble from './components/preamble.vue' import Preamble from './components/preamble.vue'
import MessageList from './components/message-list.vue' import MessageList from './components/message-list.vue'
...@@ -37,11 +38,12 @@ const agentApplicationConfig = ref<PersonalAppConfigState>(defaultPersonalAppCon ...@@ -37,11 +38,12 @@ const agentApplicationConfig = ref<PersonalAppConfigState>(defaultPersonalAppCon
const messageListRef = ref<InstanceType<typeof MessageList> | null>(null) const messageListRef = ref<InstanceType<typeof MessageList> | null>(null)
const footerInputRef = ref<InstanceType<typeof FooterInput> | null>(null) const footerInputRef = ref<InstanceType<typeof FooterInput> | null>(null)
const messageList = ref<ConversationMessageItem[]>([]) const messageList = ref(new Map<string, ConversationMessageItem>())
const continuousQuestionStatus = ref<'default' | 'close'>('default') const continuousQuestionStatus = ref<'default' | 'close'>('default')
const continueQuestionList = ref<string[]>([]) const continueQuestionList = ref<string[]>([])
const answerAudioAutoPlaying = ref(false) const answerAudioAutoPlay = ref(false) // 语音是否自动播放
const answerAudioPlaying = ref(false) // 语音播放中
const currentPlayMessageItem = ref<ConversationMessageItem | null>(null) const currentPlayMessageItem = ref<ConversationMessageItem | null>(null)
const currentPlayAudioFragmentSerialNo = ref(0) const currentPlayAudioFragmentSerialNo = ref(0)
const currentSoundCtl = shallowRef<Howl | null>(null) const currentSoundCtl = shallowRef<Howl | null>(null)
...@@ -110,7 +112,7 @@ async function handleGetAutoPlayByAgentId() { ...@@ -110,7 +112,7 @@ async function handleGetAutoPlayByAgentId() {
const res = await fetchGetAutoPlayByAgentId<'Y' | 'N'>(agentId.value) const res = await fetchGetAutoPlayByAgentId<'Y' | 'N'>(agentId.value)
if (res.code === 0) { if (res.code === 0) {
answerAudioAutoPlaying.value = res.data === 'Y' answerAudioAutoPlay.value = res.data === 'Y'
} }
} }
...@@ -134,20 +136,31 @@ async function handleUpdateAutoPlaying(isAutoPlaying: boolean) { ...@@ -134,20 +136,31 @@ async function handleUpdateAutoPlaying(isAutoPlaying: boolean) {
await fetchUpdateAutoPlay(agentId.value, autoplay) await fetchUpdateAutoPlay(agentId.value, autoplay)
} }
function handleAddMessageItem(messageItem: ConversationMessageItem) { function handleAddMessageItem(messageId: string, messageItem: ConversationMessageItem) {
messageList.value.push(messageItem) messageList.value.set(messageId, messageItem)
} }
function handleUpdateSpecifyMessageItem(messageItemIndex: number, newObj: Partial<ConversationMessageItem>) { function handleUpdateSpecifyMessageItem(messageId: string, newMessageItem: Partial<ConversationMessageItem>) {
if (messageList.value[messageItemIndex]) { const currentMessageItemInfo = messageList.value.get(messageId)
Object.entries(newObj).forEach(([k, v]) => {
;(messageList.value[messageItemIndex] as any)[k as keyof typeof newObj] = v if (currentMessageItemInfo) {
const updatePropertyLength = Object.keys(newMessageItem).length
if (updatePropertyLength > 4) {
messageList.value.set(messageId, Object.assign({}, currentMessageItemInfo, newMessageItem))
return
}
Object.entries<ValueOf<typeof newMessageItem>>(newMessageItem).forEach(([key, value]) => {
if (Object.prototype.hasOwnProperty.call(currentMessageItemInfo, key)) {
;(currentMessageItemInfo as any)[key as keyof ConversationMessageItem] = value
}
}) })
} }
} }
function handleDeleteLastMessageItem() { function handleDeleteLastMessageItem(messageId: string) {
messageList.value.pop() messageList.value.delete(messageId)
} }
function handleUpdatePageScroll() { function handleUpdatePageScroll() {
...@@ -163,7 +176,8 @@ function handleClearAllMessage() { ...@@ -163,7 +176,8 @@ function handleClearAllMessage() {
.then(() => { .then(() => {
handleAudioPause() handleAudioPause()
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
messageList.value = [] messageList.value.clear()
answerAudioPlaying.value = false
window.$message.success(t('common_module.clear_success_message')) window.$message.success(t('common_module.clear_success_message'))
}) })
} }
...@@ -189,6 +203,7 @@ function howlSoundFactory(url: string) { ...@@ -189,6 +203,7 @@ function howlSoundFactory(url: string) {
preload: true, preload: true,
autoplay: true, autoplay: true,
onplay: () => { onplay: () => {
answerAudioPlaying.value = true
currentSoundCtl.value = soundCtl currentSoundCtl.value = soundCtl
if (currentPlayMessageItem.value) { if (currentPlayMessageItem.value) {
...@@ -206,6 +221,7 @@ function howlSoundFactory(url: string) { ...@@ -206,6 +221,7 @@ function howlSoundFactory(url: string) {
currentPlayAudioFragmentSerialNo.value > currentPlayMessageItem.value.voiceFragmentUrlList.length - 1 currentPlayAudioFragmentSerialNo.value > currentPlayMessageItem.value.voiceFragmentUrlList.length - 1
) { ) {
currentPlayMessageItem.value.isVoicePlaying = false currentPlayMessageItem.value.isVoicePlaying = false
answerAudioPlaying.value = false
} }
let audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value] let audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value]
...@@ -259,9 +275,10 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -259,9 +275,10 @@ function handleAudioPause(isClearMessageList = false) {
} }
currentPlayMessageItem.value && (currentPlayMessageItem.value.isVoicePlaying = false) currentPlayMessageItem.value && (currentPlayMessageItem.value.isVoicePlaying = false)
answerAudioPlaying.value = false
if (isClearMessageList) { if (isClearMessageList) {
messageList.value = [] messageList.value.clear()
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
} }
} }
...@@ -279,18 +296,18 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -279,18 +296,18 @@ function handleAudioPause(isClearMessageList = false) {
<div class="mt-5 flex select-none justify-end px-4"> <div class="mt-5 flex select-none justify-end px-4">
<div v-show="isEnableVoice" class="flex items-center gap-2"> <div v-show="isEnableVoice" class="flex items-center gap-2">
<span>{{ t('common_module.voice_auto_play') }}</span> <span>{{ t('common_module.voice_auto_play') }}</span>
<n-switch v-model:value="answerAudioAutoPlaying" size="small" @update:value="handleUpdateAutoPlaying"> <n-switch v-model:value="answerAudioAutoPlay" size="small" @update:value="handleUpdateAutoPlaying">
<template #checked> {{ t('common_module.open') }} </template> <template #checked> {{ t('common_module.open') }} </template>
<template #unchecked> {{ t('common_module.close') }} </template> <template #unchecked> {{ t('common_module.close') }} </template>
</n-switch> </n-switch>
</div> </div>
</div> </div>
<div v-if="messageList.length === 0" class="w-full flex-1 overflow-auto px-4"> <div v-if="messageList.size === 0" class="w-full flex-1 overflow-auto px-4">
<Preamble :agent-application-config="agentApplicationConfig" /> <Preamble :agent-application-config="agentApplicationConfig" />
</div> </div>
<div v-if="messageList.length > 0" class="flex w-full flex-1 flex-col overflow-hidden pt-5"> <div v-if="messageList.size > 0" class="flex w-full flex-1 flex-col overflow-hidden pt-5">
<div class="flex-1 overflow-auto"> <div class="flex-1 overflow-auto">
<MessageList <MessageList
ref="messageListRef" ref="messageListRef"
...@@ -313,7 +330,8 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -313,7 +330,8 @@ function handleAudioPause(isClearMessageList = false) {
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:is-enable-document-parse="isEnableDocumentParse" :is-enable-document-parse="isEnableDocumentParse"
:is-enable-voice="isEnableVoice" :is-enable-voice="isEnableVoice"
:answer-audio-auto-playing="answerAudioAutoPlaying" :answer-audio-auto-play="answerAudioAutoPlay"
:answer-audio-playing="answerAudioPlaying"
:timbre-id="agentApplicationConfig.voiceConfig.timbreId" :timbre-id="agentApplicationConfig.voiceConfig.timbreId"
@add-message-item="handleAddMessageItem" @add-message-item="handleAddMessageItem"
@update-specify-message-item="handleUpdateSpecifyMessageItem" @update-specify-message-item="handleUpdateSpecifyMessageItem"
...@@ -324,6 +342,7 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -324,6 +342,7 @@ function handleAudioPause(isClearMessageList = false) {
@create-continue-questions="handleCreateContinueQuestions" @create-continue-questions="handleCreateContinueQuestions"
@reset-continue-question-list="handleResetContinueQuestionList" @reset-continue-question-list="handleResetContinueQuestionList"
@audio-play="handleAudioPlay" @audio-play="handleAudioPlay"
@audio-pause="handleAudioPause"
/> />
</div> </div>
</div> </div>
......
...@@ -3,6 +3,7 @@ import { computed, onMounted, onUnmounted, ref, shallowRef } from 'vue' ...@@ -3,6 +3,7 @@ import { computed, onMounted, onUnmounted, ref, shallowRef } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Howl } from 'howler' import { Howl } from 'howler'
import { ValueOf } from 'type-fest'
import PageHeader from './components/web-page-header.vue' import PageHeader from './components/web-page-header.vue'
import Preamble from './components/preamble.vue' import Preamble from './components/preamble.vue'
import MessageList from './components/message-list.vue' import MessageList from './components/message-list.vue'
...@@ -40,11 +41,12 @@ const agentApplicationConfig = ref<PersonalAppConfigState>(defaultPersonalAppCon ...@@ -40,11 +41,12 @@ const agentApplicationConfig = ref<PersonalAppConfigState>(defaultPersonalAppCon
const messageListRef = ref<InstanceType<typeof MessageList> | null>(null) const messageListRef = ref<InstanceType<typeof MessageList> | null>(null)
const footerInputRef = ref<InstanceType<typeof FooterInput> | null>(null) const footerInputRef = ref<InstanceType<typeof FooterInput> | null>(null)
const messageList = ref<ConversationMessageItem[]>([]) const messageList = ref(new Map<string, ConversationMessageItem>())
const continuousQuestionStatus = ref<'default' | 'close'>('default') const continuousQuestionStatus = ref<'default' | 'close'>('default')
const continueQuestionList = ref<string[]>([]) const continueQuestionList = ref<string[]>([])
const answerAudioAutoPlaying = ref(false) const answerAudioAutoPlay = ref(false)
const answerAudioPlaying = ref(false) // 语音播放中
const currentPlayMessageItem = ref<ConversationMessageItem | null>(null) const currentPlayMessageItem = ref<ConversationMessageItem | null>(null)
const currentPlayAudioFragmentSerialNo = ref(0) const currentPlayAudioFragmentSerialNo = ref(0)
const currentSoundCtl = shallowRef<Howl | null>(null) const currentSoundCtl = shallowRef<Howl | null>(null)
...@@ -123,7 +125,7 @@ async function handleGetAutoPlayByAgentId() { ...@@ -123,7 +125,7 @@ async function handleGetAutoPlayByAgentId() {
const res = await fetchGetAutoPlayByAgentId<'Y' | 'N'>(agentId.value) const res = await fetchGetAutoPlayByAgentId<'Y' | 'N'>(agentId.value)
if (res.code === 0) { if (res.code === 0) {
answerAudioAutoPlaying.value = res.data === 'Y' answerAudioAutoPlay.value = res.data === 'Y'
} }
} }
...@@ -155,20 +157,31 @@ async function handleUpdateAutoPlaying(isAutoPlaying: boolean) { ...@@ -155,20 +157,31 @@ async function handleUpdateAutoPlaying(isAutoPlaying: boolean) {
await fetchUpdateAutoPlay(agentId.value, autoplay) await fetchUpdateAutoPlay(agentId.value, autoplay)
} }
function handleAddMessageItem(messageItem: ConversationMessageItem) { function handleAddMessageItem(messageId: string, messageItem: ConversationMessageItem) {
messageList.value.push(messageItem) messageList.value.set(messageId, messageItem)
} }
function handleUpdateSpecifyMessageItem(messageItemIndex: number, newObj: Partial<ConversationMessageItem>) { function handleUpdateSpecifyMessageItem(messageId: string, newMessageItem: Partial<ConversationMessageItem>) {
if (messageList.value[messageItemIndex]) { const currentMessageItemInfo = messageList.value.get(messageId)
Object.entries(newObj).forEach(([k, v]) => {
;(messageList.value[messageItemIndex] as any)[k as keyof typeof newObj] = v if (currentMessageItemInfo) {
const updatePropertyLength = Object.keys(newMessageItem).length
if (updatePropertyLength > 4) {
messageList.value.set(messageId, Object.assign({}, currentMessageItemInfo, newMessageItem))
return
}
Object.entries<ValueOf<typeof newMessageItem>>(newMessageItem).forEach(([key, value]) => {
if (Object.prototype.hasOwnProperty.call(currentMessageItemInfo, key)) {
;(currentMessageItemInfo as any)[key as keyof ConversationMessageItem] = value
}
}) })
} }
} }
function handleDeleteLastMessageItem() { function handleDeleteLastMessageItem(messageId: string) {
messageList.value.pop() messageList.value.delete(messageId)
} }
function handleUpdatePageScroll() { function handleUpdatePageScroll() {
...@@ -184,7 +197,8 @@ function handleClearAllMessage() { ...@@ -184,7 +197,8 @@ function handleClearAllMessage() {
.then(() => { .then(() => {
handleAudioPause() handleAudioPause()
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
messageList.value = [] messageList.value.clear()
answerAudioPlaying.value = false
window.$message.success(t('common_module.clear_success_message')) window.$message.success(t('common_module.clear_success_message'))
}) })
} }
...@@ -210,6 +224,7 @@ function howlSoundFactory(url: string) { ...@@ -210,6 +224,7 @@ function howlSoundFactory(url: string) {
preload: true, preload: true,
autoplay: true, autoplay: true,
onplay: () => { onplay: () => {
answerAudioPlaying.value = true
currentSoundCtl.value = soundCtl currentSoundCtl.value = soundCtl
if (currentPlayMessageItem.value) { if (currentPlayMessageItem.value) {
...@@ -227,6 +242,7 @@ function howlSoundFactory(url: string) { ...@@ -227,6 +242,7 @@ function howlSoundFactory(url: string) {
currentPlayAudioFragmentSerialNo.value > currentPlayMessageItem.value.voiceFragmentUrlList.length - 1 currentPlayAudioFragmentSerialNo.value > currentPlayMessageItem.value.voiceFragmentUrlList.length - 1
) { ) {
currentPlayMessageItem.value.isVoicePlaying = false currentPlayMessageItem.value.isVoicePlaying = false
answerAudioPlaying.value = false
} }
let audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value] let audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value]
...@@ -280,9 +296,10 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -280,9 +296,10 @@ function handleAudioPause(isClearMessageList = false) {
} }
currentPlayMessageItem.value && (currentPlayMessageItem.value.isVoicePlaying = false) currentPlayMessageItem.value && (currentPlayMessageItem.value.isVoicePlaying = false)
answerAudioPlaying.value = false
if (isClearMessageList) { if (isClearMessageList) {
messageList.value = [] messageList.value.clear()
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
} }
} }
...@@ -303,17 +320,17 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -303,17 +320,17 @@ function handleAudioPause(isClearMessageList = false) {
<div class="relative mx-auto flex h-full w-[1000px] flex-col overflow-hidden"> <div class="relative mx-auto flex h-full w-[1000px] flex-col overflow-hidden">
<div v-show="isEnableVoice" class="absolute right-10 top-7 flex select-none items-center gap-2"> <div v-show="isEnableVoice" class="absolute right-10 top-7 flex select-none items-center gap-2">
<span>{{ t('common_module.voice_auto_play') }}</span> <span>{{ t('common_module.voice_auto_play') }}</span>
<n-switch v-model:value="answerAudioAutoPlaying" size="small" @update:value="handleUpdateAutoPlaying"> <n-switch v-model:value="answerAudioAutoPlay" size="small" @update:value="handleUpdateAutoPlaying">
<template #checked> {{ t('common_module.open') }} </template> <template #checked> {{ t('common_module.open') }} </template>
<template #unchecked> {{ t('common_module.close') }} </template> <template #unchecked> {{ t('common_module.close') }} </template>
</n-switch> </n-switch>
</div> </div>
<div v-if="messageList.length === 0" class="w-full flex-1 overflow-auto px-5"> <div v-if="messageList.size === 0" class="w-full flex-1 overflow-auto px-5">
<Preamble :agent-application-config="agentApplicationConfig" /> <Preamble :agent-application-config="agentApplicationConfig" />
</div> </div>
<div v-if="messageList.length > 0" class="flex w-full flex-1 flex-col overflow-hidden"> <div v-if="messageList.size > 0" class="flex w-full flex-1 flex-col overflow-hidden">
<div class="mt-20 flex-1 overflow-auto"> <div class="mt-20 flex-1 overflow-auto">
<MessageList <MessageList
ref="messageListRef" ref="messageListRef"
...@@ -336,7 +353,8 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -336,7 +353,8 @@ function handleAudioPause(isClearMessageList = false) {
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:is-enable-document-parse="isEnableDocumentParse" :is-enable-document-parse="isEnableDocumentParse"
:is-enable-voice="isEnableVoice" :is-enable-voice="isEnableVoice"
:answer-audio-auto-playing="answerAudioAutoPlaying" :answer-audio-auto-play="answerAudioAutoPlay"
:answer-audio-playing="answerAudioPlaying"
:timbre-id="agentApplicationConfig.voiceConfig.timbreId" :timbre-id="agentApplicationConfig.voiceConfig.timbreId"
@add-message-item="handleAddMessageItem" @add-message-item="handleAddMessageItem"
@update-specify-message-item="handleUpdateSpecifyMessageItem" @update-specify-message-item="handleUpdateSpecifyMessageItem"
...@@ -347,6 +365,7 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -347,6 +365,7 @@ function handleAudioPause(isClearMessageList = false) {
@create-continue-questions="handleCreateContinueQuestions" @create-continue-questions="handleCreateContinueQuestions"
@reset-continue-question-list="handleResetContinueQuestionList" @reset-continue-question-list="handleResetContinueQuestionList"
@audio-play="handleAudioPlay" @audio-play="handleAudioPlay"
@audio-pause="handleAudioPause"
/> />
</div> </div>
</div> </div>
......
...@@ -100,6 +100,8 @@ declare namespace I18n { ...@@ -100,6 +100,8 @@ declare namespace I18n {
voice_auto_play: string voice_auto_play: string
start_playing: string start_playing: string
stop_playing: string stop_playing: string
unplayable: string
unplayable_tip: string
response_error: string response_error: string
agent_exception: string agent_exception: string
equity: string equity: string
...@@ -116,6 +118,8 @@ declare namespace I18n { ...@@ -116,6 +118,8 @@ declare namespace I18n {
cancel_associate_file_tip: string cancel_associate_file_tip: string
upload_file_limit: string upload_file_limit: string
overwrite_file_tip: string overwrite_file_tip: string
stop_playing_and_then_operate: string
do_not_operate_until_the_reply_is_complete: string
} }
data_table_module: { data_table_module: {
...@@ -292,9 +296,10 @@ declare namespace I18n { ...@@ -292,9 +296,10 @@ declare namespace I18n {
memory_fragment_delete_row_tip_content: string memory_fragment_delete_row_tip_content: string
add_knowledge_successfully: string add_knowledge_successfully: string
remove_knowledge_successfully: string remove_knowledge_successfully: string
setting_timbre: string setting_voice: string
setting_timbre_message: string setting_voice_message: string
setting_timbre_desc: string setting_voice_desc: string
currently_only_one_voice_can_be_set: string
memory_variable_modal: { memory_variable_modal: {
edit_memory_variable: string edit_memory_variable: 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