Commit e165cc1e authored by nick zheng's avatar nick zheng

feat: 应用语音功能

parent a4cf1c15
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/> />
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_4711453_f5muspehl1h.css" /> <link rel="stylesheet" href="//at.alicdn.com/t/c/font_4711453_04ol63hssfos.css" />
<title>Model Link</title> <title>Model Link</title>
</head> </head>
......
...@@ -22,12 +22,14 @@ ...@@ -22,12 +22,14 @@
"@unocss/reset": "^0.61.9", "@unocss/reset": "^0.61.9",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^10.11.1",
"axios": "^1.7.7", "axios": "^1.7.7",
"bowser": "^2.11.0",
"clipboardy": "^4.0.0", "clipboardy": "^4.0.0",
"cropperjs": "^1.6.2", "cropperjs": "^1.6.2",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dompurify": "^3.2.0", "dompurify": "^3.2.0",
"github-markdown-css": "^5.7.0", "github-markdown-css": "^5.7.0",
"highlight.js": "^11.10.0", "highlight.js": "^11.10.0",
"howler": "^2.2.4",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"marked": "^15.0.0", "marked": "^15.0.0",
"marked-highlight": "^2.2.1", "marked-highlight": "^2.2.1",
...@@ -46,6 +48,7 @@ ...@@ -46,6 +48,7 @@
"@commitlint/config-conventional": "^19.5.0", "@commitlint/config-conventional": "^19.5.0",
"@commitlint/types": "^19.5.0", "@commitlint/types": "^19.5.0",
"@intlify/unplugin-vue-i18n": "^4.0.0", "@intlify/unplugin-vue-i18n": "^4.0.0",
"@types/howler": "^2.2.12",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^20.16.5", "@types/node": "^20.16.5",
"@types/spark-md5": "^3.0.4", "@types/spark-md5": "^3.0.4",
......
This diff is collapsed.
...@@ -170,3 +170,20 @@ export function fetchRemoveSalePublishApplication<T>(agentPublishId: number) { ...@@ -170,3 +170,20 @@ export function fetchRemoveSalePublishApplication<T>(agentPublishId: number) {
export function fetchGetApplicationMallInfo<T>(agentId: string) { export function fetchGetApplicationMallInfo<T>(agentId: string) {
return request.post<T>(`/bizAgentApplicationMallRest/getMallInfoByAgentId.json?agentId=${agentId}`) return request.post<T>(`/bizAgentApplicationMallRest/getMallInfoByAgentId.json?agentId=${agentId}`)
} }
/**
* @query agentId 应用Id
* @returns 获取用户自动播放配置
*/
export function fetchGetAutoPlayByAgentId<T>(agentId: string) {
return request.post<T>(`/agentApplicationRest/autoPlayByAgentId.json?agentId=${agentId}`)
}
/**
* @query agentId 应用Id
* @query autoPlay 是否自动播放
* @returns 设置用户自动播放配置
*/
export function fetchUpdateAutoPlay<T>(agentId: string, autoPlay: 'Y' | 'N') {
return request.post<T>(`/agentApplicationRest/enableAutoPlay.json?agentId=${agentId}&autoPlay=${autoPlay}`)
}
import { request } from '@/utils/request'
/**
* @returns 获取音色列表
*/
export function fetchGetTimbreList<T>() {
return request.post<T>('/bizVoiceTimbreRest/getTimbreList.json')
}
/**
* @query timbreId 音色Id
* @returns 获取音色详情
*/
export function fetchGetTimbreInfoDetail<T>(timbreId: string) {
return request.post<T>(`/bizVoiceTimbreRest/getTimbreInfo.json?timbreId=${timbreId}`)
}
<?xml version="1.0" encoding="UTF-8"?>
<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>黑色暂停</title>
<defs>
<rect id="path-1" x="0" y="0" width="34" height="34"></rect>
</defs>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="对话-进入" transform="translate(-181.000000, -310.000000)">
<g id="编组-2" transform="translate(181.000000, 310.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="矩形"></g>
<path d="M17.0006063,1.5 C21.2734934,1.5 25.1479946,3.23913189 27.9545032,6.04584067 C30.7609677,8.85250533 32.5,12.7272396 32.5,17.0006063 C32.5,21.2738531 30.7610549,25.1482139 27.9547057,27.9545631 C25.1481717,30.7610971 21.2735836,32.5 17.0006063,32.5 C12.7272396,32.5 8.85250534,30.7609677 6.04584068,27.9545032 C3.23913189,25.1479946 1.5,21.2734933 1.5,17.0006063 C1.5,12.7275994 3.23921912,8.8527246 6.04604319,6.04590054 C8.85268248,3.23926124 12.72733,1.5 17.0006063,1.5 L17.0006063,1.5 Z" id="路径" stroke="#333333" stroke-width="3" fill-rule="nonzero" mask="url(#mask-2)"></path>
<rect id="矩形" fill="#000DFF" mask="url(#mask-2)" x="11.3333333" y="11.3333333" width="3.23809524" height="11.3333333" rx="1.61904762"></rect>
<rect id="矩形备份" fill="#000DFF" mask="url(#mask-2)" x="19.4285714" y="11.3333333" width="3.23809524" height="11.3333333" rx="1.61904762"></rect>
</g>
</g>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>播放</title>
<defs>
<rect id="path-1" x="0" y="0" width="34" height="34"></rect>
</defs>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="对话-按住语音-提示语" transform="translate(-181.000000, -310.000000)">
<g id="播放" transform="translate(181.000000, 310.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="矩形"></g>
<g mask="url(#mask-2)" fill-rule="nonzero">
<path d="M17.0005306,1.5 C21.2734482,1.5 25.1478582,3.23921344 27.9543229,6.04585326 C30.7609106,8.85261617 32.5,12.7274291 32.5,17.0005306 C32.5,21.2735273 30.7609869,25.1480133 27.9545001,27.9545001 C25.1480133,30.7609869 21.2735273,32.5 17.0005306,32.5 C12.7269453,32.5 8.85210629,30.7611966 6.04545533,27.9547208 C3.23892745,25.1483681 1.5,21.273932 1.5,17.0005306 C1.5,12.7270243 3.23900379,8.85226132 6.04563255,6.04563255 C8.85226132,3.23900379 12.7270243,1.5 17.0005306,1.5 Z" id="路径" stroke="#CCCCCC" stroke-width="3"></path>
<path d="M13.6855279,9.59377047 C14.4484879,9.59377047 15.1658188,9.88982862 15.2443432,9.92378513 L15.2443432,9.92378513 L15.3812303,9.99488155 L24.6725758,15.6783496 L24.8073406,15.9213507 C25.5108767,17.1883524 24.8275023,18.3131612 24.1950626,18.7609625 L24.1950626,18.7609625 L24.1048657,18.8193252 L14.9196343,24.02528 C14.471833,24.2544864 14.0654162,24.3606004 13.6727943,24.3606004 L13.6727943,24.3606004 L13.4721536,24.3516533 C12.36468,24.2516108 11.7569642,23.3358336 11.5685528,22.7816235 L11.5685528,22.7816235 L11.5112512,22.6150245 L11.5112512,11.827471 C11.5112512,10.4713336 12.3654693,9.59377047 13.6855279,9.59377047 Z M13.64096,11.7171125 C13.6367155,11.7447021 13.6345932,11.781842 13.6345932,11.8274711 L13.6345932,11.8274711 L13.6345932,22.1884461 C13.6515714,22.21073 13.6674885,22.2287694 13.6802222,22.2393808 C13.7046285,22.2372585 13.7799694,22.2266471 13.9136731,22.1587341 L13.9136731,22.1587341 L22.8856153,17.071627 L14.3551075,11.8539996 C14.2044256,11.7988203 13.837271,11.7065011 13.64096,11.7171125 Z" id="形状结合" stroke="#000DFF" stroke-width="0.5" fill="#000DFF"></path>
</g>
</g>
</g>
</g>
</svg>
...@@ -7,3 +7,12 @@ export const INDEX_URLS: Record<'DEV' | 'PROD', string> = { ...@@ -7,3 +7,12 @@ export const INDEX_URLS: Record<'DEV' | 'PROD', string> = {
DEV: 'https://poc-sit.gsstcloud.com/fe/', DEV: 'https://poc-sit.gsstcloud.com/fe/',
PROD: 'https://model-link.gsstcloud.com/fe/', PROD: 'https://model-link.gsstcloud.com/fe/',
} }
const ENV = import.meta.env.VITE_APP_ENV
export const Domain_Name: Record<'DEV' | 'PROD', string> = {
DEV: 'poc-sit.gsstcloud.com',
PROD: 'model-link.gsstcloud.com',
}
export const TEXTTOSPEECH_WS_URL = `wss://${Domain_Name[ENV || 'DEV']}/websocket/textToSpeechTC.ws`
...@@ -91,6 +91,17 @@ common_module: ...@@ -91,6 +91,17 @@ common_module:
bind: 'Bind' bind: 'Bind'
sms: 'Short message' sms: 'Short message'
verificationCode: 'Verification code' verificationCode: 'Verification code'
role: 'Role'
mandarin: 'Mandarin'
cantonese: 'Cantonese'
english: 'English'
voice: 'Voice'
sound: 'Sound'
voice_auto_play: 'Voice auto play'
start_playing: 'play'
stop_playing: 'stop'
response_error: 'Response error'
agent_exception: 'Agent exception, please try again later!'
dialogue_module: dialogue_module:
continue_question_message: 'You can keep asking questions' continue_question_message: 'You can keep asking questions'
...@@ -281,6 +292,9 @@ personal_space_module: ...@@ -281,6 +292,9 @@ 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_timbre_message: 'You can set the language and timbre'
setting_timbre_desc: 'You can customize the voice and timbre for voice broadcast, and you can set only one at a time.'
memory_variable_modal: memory_variable_modal:
edit_memory_variable: 'Edit memory variable' edit_memory_variable: 'Edit memory variable'
......
...@@ -90,6 +90,17 @@ common_module: ...@@ -90,6 +90,17 @@ common_module:
bind: '绑定' bind: '绑定'
sms: '短信' sms: '短信'
verificationCode: '验证码' verificationCode: '验证码'
role: '角色'
mandarin: '普通话'
cantonese: '粤语'
english: '英语'
voice: '语音'
sound: '声音'
voice_auto_play: '语音自动播放'
start_playing: '开始播放'
stop_playing: '停止播放'
response_error: '响应错误'
agent_exception: '应用异常,请稍后重试!'
dialogue_module: dialogue_module:
continue_question_message: '你可以继续提问' continue_question_message: '你可以继续提问'
...@@ -279,6 +290,9 @@ personal_space_module: ...@@ -279,6 +290,9 @@ 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_timbre_message: '你可以设置语言和音色'
setting_timbre_desc: '您可自定义语音及音色,用于语音播报,且每次仅可设置一种。'
memory_variable_modal: memory_variable_modal:
edit_memory_variable: '编辑记忆变量' edit_memory_variable: '编辑记忆变量'
......
...@@ -90,6 +90,17 @@ common_module: ...@@ -90,6 +90,17 @@ common_module:
bind: '綁定' bind: '綁定'
sms: '短信' sms: '短信'
verificationCode: '驗證碼' verificationCode: '驗證碼'
role: '角色'
mandarin: '普通話'
cantonese: '粵語'
english: '英語'
voice: '語音'
sound: '聲音'
voice_auto_play: '語音自動播放'
start_playing: '開始播放'
stop_playing: '停止播放'
response_error: '響應錯誤'
agent_exception: '應用異常,請稍後重試!'
dialogue_module: dialogue_module:
continue_question_message: '你可以繼續提問' continue_question_message: '你可以繼續提問'
...@@ -279,6 +290,9 @@ personal_space_module: ...@@ -279,6 +290,9 @@ 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_timbre_message: '你可以設置語言和音色'
setting_timbre_desc: '您可自定義語音及音色,用於語音播報,且每次僅可設置一種。'
memory_variable_modal: memory_variable_modal:
edit_memory_variable: '編輯記憶變數' edit_memory_variable: '編輯記憶變數'
......
...@@ -32,6 +32,10 @@ export function defaultPersonalAppConfigState(): PersonalAppConfigState { ...@@ -32,6 +32,10 @@ export function defaultPersonalAppConfigState(): PersonalAppConfigState {
communicationTurn: 3, communicationTurn: 3,
temperature: 0.5, temperature: 0.5,
}, },
voiceConfig: {
defaultOpen: 'Y',
timbreId: '',
},
modifiedTime: new Date(), modifiedTime: new Date(),
createdTime: '', createdTime: '',
isCollect: '', isCollect: '',
......
...@@ -36,6 +36,10 @@ export interface PersonalAppConfigState { ...@@ -36,6 +36,10 @@ export interface PersonalAppConfigState {
communicationTurn: number //参考对话轮次 0-100 communicationTurn: number //参考对话轮次 0-100
temperature: number //多样性 0-1.00 temperature: number //多样性 0-1.00
} }
voiceConfig: {
defaultOpen: 'Y' | 'N' //是否默认开启 Y-开启 N-关闭
timbreId: string //音色ID
}
popularity?: number popularity?: number
modifiedTime: Date modifiedTime: Date
createdTime: string createdTime: string
......
import Bowser from 'bowser'
export function validBrowser() {
const browser = Bowser.getParser(window.navigator.userAgent)
return browser.satisfies({
android: {
chrome: '> 80',
},
})
}
import i18n from '@/locales'
const { t } = i18n.global
export default class WebSocketCtr {
private socket: WebSocket | null = null
private readonly url: string
public isDisconnect: boolean = true
constructor(url: string) {
this.url = url
}
private initSocket(callBack?: () => void) {
const socket = new WebSocket(this.url)
socket.onopen = () => {
this.isDisconnect = false
this.onConnect()
callBack && callBack()
}
socket.onmessage = (event: any) => {
let data: any = { final: false }
try {
data = JSON.parse(event.data)
} catch (err) {
window.$message.error(t('common_module.response_error'))
this.onMessageError()
}
if (data.code && data.code !== '0') {
if (this.socket) {
this.socket.close()
this.socket = null
}
window.$message.error(t('common_module.agent_exception'))
this.onMessageError()
return
}
this.onMessage(data)
}
socket.onerror = (event) => {
this.onError(event)
}
socket.onclose = () => {
this.isDisconnect = true
this.onDisconnect()
}
this.socket = socket
}
connect(callBack?: () => void) {
if (this.socket && [0, 1].includes(this.socket.readyState)) {
callBack && callBack()
return
}
this.initSocket(callBack)
}
send(content: { [k: string]: any }) {
this.socket && this.socket.send(JSON.stringify(content))
}
disconnect() {
if (this.socket) {
this.socket.close()
this.socket = null
}
this.onDisconnect()
}
/* call back */
onConnect() {}
onMessage(_message: any) {}
onMessageError() {}
onError(_event?: any) {}
onDisconnect() {}
}
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import { Plus, RightOne, MoreOne, Edit, ReduceOne } from '@icon-park/vue-next'
import TimbreSettingModal from './timbre-setting-modal.vue'
import { TimbreLanguageInfoItem } from '../types'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { fetchGetTimbreInfoDetail } from '@/apis/timber'
const { t } = useI18n()
const personalAppConfigStore = usePersonalAppConfigStore()
const { voiceConfig } = storeToRefs(personalAppConfigStore)
const roleConfigExpandedNames = ref<string[]>([])
const isShowTimbreSettingModal = ref(false)
const timbreInfoDetail = ref<TimbreLanguageInfoItem>()
let timbreInfo = reactive<TimbreLanguageInfoItem>({ language: 0, matchLang: '', timbreInfo: [] })
const timberFullName = computed(() => {
const languageOptions = [
{ label: 'common_module.mandarin', value: 'zh-CN' },
{ label: 'common_module.cantonese', value: 'zh-HK' },
{ label: 'common_module.english', value: 'en' },
]
const currentLanguage = languageOptions.find((item) => item.value === timbreInfoDetail.value?.matchLang)
return `${timbreInfoDetail.value?.timbreInfo?.[0]?.timbreName || '--'}(${t(currentLanguage?.label || 'common_module.sound')})`
})
onMounted(() => {
voiceConfig.value.timbreId && handleGetTimberInfoDetail()
})
async function handleGetTimberInfoDetail() {
const res = await fetchGetTimbreInfoDetail<TimbreLanguageInfoItem>(voiceConfig.value.timbreId)
if (res.code === 0) {
timbreInfoDetail.value = res.data
roleConfigExpandedNames.value = ['timbre']
}
}
function handleUpdateRoleConfigExpandedNames(expandedNames: string[]) {
roleConfigExpandedNames.value = expandedNames
}
function handleShowAssociatedTimbreModel() {
isShowTimbreSettingModal.value = true
timbreInfo = { language: 0, matchLang: '', timbreInfo: [] }
}
function handleUpdateTimbreId(timberId: string) {
isShowTimbreSettingModal.value = false
voiceConfig.value.timbreId = timberId
voiceConfig.value.defaultOpen = 'Y'
handleGetTimberInfoDetail()
}
function handleRemoveTimbreId() {
voiceConfig.value.timbreId = ''
}
function handleEditAssociatedTimbreModel() {
isShowTimbreSettingModal.value = true
timbreInfo = timbreInfoDetail.value!
}
</script>
<template>
<section class="border-b border-[#e8e9eb] px-5">
<div class="pt-4">
<h2 class="my-3 text-[#84868c]">{{ t('common_module.role') }}</h2>
<NCollapse
:expanded-names="roleConfigExpandedNames"
:trigger-areas="['main', 'arrow']"
@update:expanded-names="handleUpdateRoleConfigExpandedNames"
>
<template #arrow>
<RightOne theme="filled" size="17" fill="#333" :stroke-width="3" />
</template>
<NCollapseItem :title="t('common_module.sound')" name="timbre" class="my-[13px]!">
<template #header-extra>
<NTooltip trigger="hover">
<template #trigger>
<Plus
theme="outline"
size="22"
:stroke-width="3"
class="text-theme-color cursor-pointer"
@click="handleShowAssociatedTimbreModel"
/>
</template>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_timbre') }}
</NTooltip>
</template>
<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') }}
</span>
<div class="flex flex-1 flex-wrap items-center gap-[12px] overflow-hidden">
<div
v-show="voiceConfig.timbreId"
class="font-400 line-height-[20px] flex cursor-pointer items-center rounded-[4px] bg-[#f2f5f9] py-[2px] pl-[8px] text-[12px] hover:bg-[#e3e8f0]"
@click="handleEditAssociatedTimbreModel"
>
<div class="max-w-[205px] truncate text-[#151b26]">{{ timberFullName }}</div>
<n-popover placement="bottom" trigger="hover" :show-arrow="false" class="p-[4px]!">
<template #trigger>
<MoreOne theme="outline" size="14" fill="#333" :stroke-width="3" class="mr-[4px] mt-[2px]" />
</template>
<div class="text-[12px]">
<div
class="flex h-[30px] w-[90px] cursor-pointer items-center justify-start px-[8px] py-[5px] hover:rounded-[4px] hover:bg-[#f2f5f9]"
@click="handleEditAssociatedTimbreModel"
>
<Edit theme="outline" size="16" fill="#333" :stroke-width="3" /><span class="ml-[4px]">
{{
t(
'personal_space_module.agent_module.agent_setting_module.agent_config_module.memory_variable_action_edit',
)
}}
</span>
</div>
<n-space>
<div
class="flex h-[30px] w-[90px] cursor-pointer items-center justify-start px-[8px] py-[5px] hover:rounded-[4px] hover:bg-[#f2f5f9]"
@click="handleRemoveTimbreId"
>
<ReduceOne theme="outline" size="16" fill="#333" :stroke-width="3" />
<span class="ml-[4px]">
{{ t('common_module.delete') }}
</span>
</div>
</n-space>
</div>
</n-popover>
</div>
</div>
</NCollapseItem>
</NCollapse>
</div>
</section>
<TimbreSettingModal
v-model:is-show-modal="isShowTimbreSettingModal"
:btn-loading="false"
:modal-title="t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_timbre')"
:timbre-info="timbreInfo"
@confirm="handleUpdateTimbreId"
/>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { inject, onMounted, onUnmounted, ref } from 'vue' import { computed, inject, onMounted, onUnmounted, ref, shallowRef } from 'vue'
import Preamble from './preamble.vue' import Preamble from './preamble.vue'
import { useI18n } from 'vue-i18n' 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 MessageList from './message-list.vue' import MessageList from './message-list.vue'
import FooterInput from './footer-input.vue' import FooterInput from './footer-input.vue'
import { fetchCreateContinueQuestions } from '@/apis/agent-application' import { fetchCreateContinueQuestions } from '@/apis/agent-application'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config' import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { useSystemLanguageStore } from '@/store/modules/system-language'
import { Brain, Down } from '@icon-park/vue-next' import { Brain, Down } from '@icon-park/vue-next'
import MemoryPreviewModal from './memory-preview-modal.vue' import MemoryPreviewModal from './memory-preview-modal.vue'
import { validBrowser } from '@/utils/browser-detection'
const { t } = useI18n() const { t } = useI18n()
const router = useRouter() const router = useRouter()
const personalAppConfigStore = usePersonalAppConfigStore() const personalAppConfigStore = usePersonalAppConfigStore()
const systemLanguageStore = useSystemLanguageStore()
const emitter = inject<Emitter<MittEvents>>('emitter') const emitter = inject<Emitter<MittEvents>>('emitter')
...@@ -29,9 +33,20 @@ const continuousQuestionStatus = ref<'default' | 'close'>(personalAppConfigStore ...@@ -29,9 +33,20 @@ const continuousQuestionStatus = ref<'default' | 'close'>(personalAppConfigStore
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 currentPlayMessageItem = ref<ConversationMessageItem | null>(null)
const currentPlayAudioFragmentSerialNo = ref(0)
const currentSoundCtl = shallowRef<Howl | null>(null)
const isSoundCtlCreated = ref(false)
const isEnLanguage = computed(() => {
return systemLanguageStore.currentLanguageInfo.key === 'en'
})
onMounted(() => { onMounted(() => {
emitter?.on('resetAgent', () => { emitter?.on('resetAgent', () => {
handleAudioPause()
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
messageList.value = [] messageList.value = []
}) })
...@@ -39,6 +54,9 @@ onMounted(() => { ...@@ -39,6 +54,9 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
emitter?.off('resetAgent') emitter?.off('resetAgent')
handleAudioPause()
footerInputRef.value?.blockMessageResponse()
messageList.value = []
}) })
function handleAddMessageItem(messageItem: ConversationMessageItem) { function handleAddMessageItem(messageItem: ConversationMessageItem) {
...@@ -68,6 +86,7 @@ function handleClearAllMessage() { ...@@ -68,6 +86,7 @@ function handleClearAllMessage() {
t('common_module.dialogue_module.clear_message_dialog_title'), t('common_module.dialogue_module.clear_message_dialog_title'),
) )
.then(() => { .then(() => {
handleAudioPause()
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
messageList.value = [] messageList.value = []
window.$message.success(t('common_module.clear_success_message')) window.$message.success(t('common_module.clear_success_message'))
...@@ -88,6 +107,10 @@ function handleUpdateContinueQuestionStatus(status: 'default' | 'close') { ...@@ -88,6 +107,10 @@ function handleUpdateContinueQuestionStatus(status: 'default' | 'close') {
continuousQuestionList.value = [] continuousQuestionList.value = []
} }
function handleUpdateAudioAutoPlaying(isAutoPlaying: boolean) {
personalAppConfigStore.voiceConfig.defaultOpen = isAutoPlaying ? 'Y' : 'N'
}
function handleTurnMultiModelDialogue() { function handleTurnMultiModelDialogue() {
if (!personalAppConfigStore.baseInfo.agentId) return if (!personalAppConfigStore.baseInfo.agentId) return
...@@ -103,6 +126,91 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) { ...@@ -103,6 +126,91 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) {
selectedMemoryTabName.value = MemoryTabName selectedMemoryTabName.value = MemoryTabName
isShowMemoryPreviewModal.value = true isShowMemoryPreviewModal.value = true
} }
function howlSoundFactory(url: string) {
const soundCtl = new Howl({
src: [url],
format: ['mpeg'],
html5: !validBrowser(),
preload: true,
autoplay: true,
onplay: () => {
currentSoundCtl.value = soundCtl
if (currentPlayMessageItem.value) {
currentPlayMessageItem.value.isVoiceLoading = false
currentPlayMessageItem.value.isVoicePlaying = true
}
},
onend: () => {
soundCtl.unload()
currentPlayAudioFragmentSerialNo.value += 1
if (
currentPlayMessageItem.value &&
currentPlayAudioFragmentSerialNo.value > currentPlayMessageItem.value.voiceFragmentUrlList.length - 1
) {
currentPlayMessageItem.value.isVoicePlaying = false
}
let audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value]
if (audioFragmentUrl) {
howlSoundFactory(audioFragmentUrl)
} else if (
(currentPlayMessageItem.value?.voiceFragmentUrlList || []).length > currentPlayAudioFragmentSerialNo.value
) {
let timerId: NodeJS.Timeout | null = null
const audioFragmentCheck = () => {
audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value]
if (audioFragmentUrl) {
timerId && clearInterval(timerId)
howlSoundFactory(audioFragmentUrl)
}
}
timerId = setInterval(audioFragmentCheck, 600)
}
},
})
return soundCtl
}
function handleAudioPlay(currentMessageItem: ConversationMessageItem, specificPlayUrl?: string) {
handleAudioPause()
currentPlayMessageItem.value = currentMessageItem
currentPlayAudioFragmentSerialNo.value = 0
const audioUrl = specificPlayUrl || currentMessageItem.voiceFragmentUrlList[0]
howlSoundFactory(audioUrl)
isSoundCtlCreated.value = true
}
function handleAudioPause(isClearMessageList = false) {
if (currentSoundCtl.value) {
currentSoundCtl.value.pause()
currentSoundCtl.value.unload()
currentSoundCtl.value = null
isSoundCtlCreated.value = false
}
currentPlayMessageItem.value && (currentPlayMessageItem.value.isVoicePlaying = false)
if (isClearMessageList) {
messageList.value = []
footerInputRef.value?.blockMessageResponse()
}
}
</script> </script>
<template> <template>
...@@ -113,7 +221,27 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) { ...@@ -113,7 +221,27 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) {
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.preview') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.preview') }}
</p> </p>
<div class="flex items-center"> <div class="flex items-center" :class="isEnLanguage ? 'gap-1.5 2xl:gap-4' : 'gap-4'">
<n-popover placement="bottom" trigger="hover" :show-arrow="false">
<template #trigger>
<div v-show="personalAppConfigStore.voiceConfig.timbreId" class="text-font-color cursor-pointer">
<i class="iconfont icon-yuyin mr-1 text-sm" />
<span>{{ t('common_module.voice') }}</span>
</div>
</template>
<div class="flex items-center gap-2.5">
<span>{{ t('common_module.voice_auto_play') }}</span>
<n-switch
v-model:value="answerAudioAutoPlaying"
size="small"
@update:value="handleUpdateAudioAutoPlaying"
>
<template #checked> {{ t('common_module.open') }} </template>
<template #unchecked> {{ t('common_module.close') }} </template>
</n-switch>
</div>
</n-popover>
<div <div
:class=" :class="
personalAppConfigStore.baseInfo.agentId personalAppConfigStore.baseInfo.agentId
...@@ -135,7 +263,7 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) { ...@@ -135,7 +263,7 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) {
> >
<n-popover placement="bottom" trigger="hover" class="p-[4px]!" :show-arrow="false"> <n-popover placement="bottom" trigger="hover" class="p-[4px]!" :show-arrow="false">
<template #trigger> <template #trigger>
<div class="flex items-center justify-center pl-5 text-[14px]"> <div class="flex items-center justify-center text-[14px]">
<Brain theme="outline" size="15" fill="#333" /> <Brain theme="outline" size="15" fill="#333" />
<div class="mx-[4px]"> <div class="mx-[4px]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.memory') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.memory') }}
...@@ -176,6 +304,8 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) { ...@@ -176,6 +304,8 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) {
:message-list="messageList" :message-list="messageList"
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:continuous-question-list="continuousQuestionList" :continuous-question-list="continuousQuestionList"
@audio-play="handleAudioPlay"
@audio-pause="handleAudioPause"
/> />
</div> </div>
</div> </div>
...@@ -184,6 +314,7 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) { ...@@ -184,6 +314,7 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) {
ref="footerInputRef" ref="footerInputRef"
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:message-list="messageList" :message-list="messageList"
:answer-audio-auto-playing="answerAudioAutoPlaying"
@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"
...@@ -191,6 +322,7 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) { ...@@ -191,6 +322,7 @@ function handleOpenMemoryPreviewModal(MemoryTabName: string) {
@clear-all-message="handleClearAllMessage" @clear-all-message="handleClearAllMessage"
@create-continue-questions="handleCreateContinueQuestions" @create-continue-questions="handleCreateContinueQuestions"
@update-continuous-question-status="handleUpdateContinueQuestionStatus" @update-continuous-question-status="handleUpdateContinueQuestionStatus"
@audio-play="handleAudioPlay"
/> />
<MemoryPreviewModal v-model="isShowMemoryPreviewModal" :data="selectedMemoryTabName" /> <MemoryPreviewModal v-model="isShowMemoryPreviewModal" :data="selectedMemoryTabName" />
......
...@@ -24,6 +24,7 @@ import AgentModelSetting from './agent-model-setting.vue' ...@@ -24,6 +24,7 @@ import AgentModelSetting from './agent-model-setting.vue'
import AgentAssociatedKnowledge from './agent-associated-knowledge.vue' import AgentAssociatedKnowledge from './agent-associated-knowledge.vue'
import AgentMemorySetting from './agent-memory-setting.vue' import AgentMemorySetting from './agent-memory-setting.vue'
import AgentDialogueSetting from './agent-dialogue-setting.vue' import AgentDialogueSetting from './agent-dialogue-setting.vue'
import AgentRoleSetting from './agent-role-setting.vue'
const { t } = useI18n() const { t } = useI18n()
...@@ -581,6 +582,8 @@ function handleStopGenerate() { ...@@ -581,6 +582,8 @@ function handleStopGenerate() {
@generate-preamble="handleAIGeneratePreamble" @generate-preamble="handleAIGeneratePreamble"
@generate-featured-questions="handleAIGenerateFeaturedQuestions" @generate-featured-questions="handleAIGenerateFeaturedQuestions"
/> />
<AgentRoleSetting />
</div> </div>
</div> </div>
</div> </div>
......
...@@ -7,10 +7,13 @@ import { fetchCustomEventSource } from '@/composables/useEventSource' ...@@ -7,10 +7,13 @@ import { fetchCustomEventSource } from '@/composables/useEventSource'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config' import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { UploadStatus } from '@/enums/upload-status' import { UploadStatus } from '@/enums/upload-status'
import { useDialogueFile } from '@/composables/useDialogueFile' import { useDialogueFile } from '@/composables/useDialogueFile'
import { TEXTTOSPEECH_WS_URL } from '@/config/base-url'
import WebSocketCtr from '@/utils/web-socket-ctr'
interface Props { interface Props {
messageList: ConversationMessageItem[] messageList: ConversationMessageItem[]
continuousQuestionStatus: 'default' | 'close' continuousQuestionStatus: 'default' | 'close'
answerAudioAutoPlaying: boolean
} }
const { t } = useI18n() const { t } = useI18n()
...@@ -25,6 +28,7 @@ const emit = defineEmits<{ ...@@ -25,6 +28,7 @@ const emit = defineEmits<{
clearAllMessage: [] clearAllMessage: []
createContinueQuestions: [value: string] createContinueQuestions: [value: string]
updateContinuousQuestionStatus: [value: 'default' | 'close'] updateContinuousQuestionStatus: [value: 'default' | 'close']
audioPlay: [messageItem: ConversationMessageItem, requestId?: string]
}>() }>()
const personalAppConfigStore = usePersonalAppConfigStore() const personalAppConfigStore = usePersonalAppConfigStore()
...@@ -37,6 +41,10 @@ const emitter = inject<Emitter<MittEvents>>('emitter') ...@@ -37,6 +41,10 @@ const emitter = inject<Emitter<MittEvents>>('emitter')
const inputMessageContent = ref('') const inputMessageContent = ref('')
const isAnswerResponseWait = ref(false) const isAnswerResponseWait = ref(false)
const currentReplyContentSentenceExtractIndex = ref(0)
const sentenceFragmentSerialNo = ref(0)
const sentenceExtractCheckEnabled = ref(false)
const assistantFullAnswerContent = ref('')
let controller: AbortController | null = null let controller: AbortController | null = null
...@@ -57,7 +65,10 @@ const isSendBtnDisabled = computed(() => { ...@@ -57,7 +65,10 @@ const isSendBtnDisabled = computed(() => {
}) })
const isInputMessageDisabled = computed(() => { const isInputMessageDisabled = computed(() => {
return uploadFileList.value.some((fileItem) => fileItem.status !== UploadStatus.FINISHED) return (
uploadFileList.value.some((fileItem) => fileItem.status !== UploadStatus.FINISHED) ||
!personalAppConfigStore.baseInfo.agentId
)
}) })
const isEnableDocumentParse = computed(() => { const isEnableDocumentParse = computed(() => {
...@@ -68,6 +79,10 @@ const isUploadFileDisabled = computed(() => { ...@@ -68,6 +79,10 @@ const isUploadFileDisabled = computed(() => {
return uploadFileList.value.length === 1 return uploadFileList.value.length === 1
}) })
const isEnableVoice = computed(() => {
return !!personalAppConfigStore.voiceConfig.timbreId
})
const uploadFileIcon = (type: string) => { const uploadFileIcon = (type: string) => {
return `https://gsst-poe-sit.gz.bcebos.com/icon/${type}.svg` return `https://gsst-poe-sit.gz.bcebos.com/icon/${type}.svg`
} }
...@@ -91,7 +106,7 @@ onMounted(() => { ...@@ -91,7 +106,7 @@ onMounted(() => {
}) })
}) })
function messageItemFactory() { function messageItemFactory(): ConversationMessageItem {
return { return {
timestamp: Date.now(), timestamp: Date.now(),
role: 'user', role: 'user',
...@@ -99,7 +114,10 @@ function messageItemFactory() { ...@@ -99,7 +114,10 @@ function messageItemFactory() {
isEmptyContent: false, isEmptyContent: false,
isTextContentLoading: false, isTextContentLoading: false,
isAnswerResponseLoading: false, isAnswerResponseLoading: false,
} as const isVoiceLoading: false,
isVoicePlaying: false,
voiceFragmentUrlList: [],
}
} }
function handleMessageSend() { function handleMessageSend() {
...@@ -138,11 +156,17 @@ function handleMessageSend() { ...@@ -138,11 +156,17 @@ function handleMessageSend() {
inputMessageContent.value = '' inputMessageContent.value = ''
isAnswerResponseWait.value = true isAnswerResponseWait.value = true
currentReplyContentSentenceExtractIndex.value = 0
sentenceFragmentSerialNo.value = 0
sentenceExtractCheckEnabled.value = false
assistantFullAnswerContent.value = ''
emit('addMessageItem', { emit('addMessageItem', {
...messageItemFactory(), ...messageItemFactory(),
role: 'assistant', role: 'assistant',
isTextContentLoading: true, isTextContentLoading: true,
isAnswerResponseLoading: true, isAnswerResponseLoading: true,
isVoiceLoading: true,
}) })
emit('updatePageScroll') emit('updatePageScroll')
...@@ -166,6 +190,12 @@ function handleMessageSend() { ...@@ -166,6 +190,12 @@ function handleMessageSend() {
isTextContentLoading: false, isTextContentLoading: false,
isAnswerResponseLoading: false, isAnswerResponseLoading: false,
}) })
if (!props.answerAudioAutoPlaying) {
emit('updateSpecifyMessageItem', currentMessageIndex, {
isVoiceLoading: false,
})
}
isCreateContinueQuestions.value && emit('createContinueQuestions', replyTextContent) isCreateContinueQuestions.value && emit('createContinueQuestions', replyTextContent)
emit('updatePageScroll') emit('updatePageScroll')
blockMessageResponse() blockMessageResponse()
...@@ -175,6 +205,13 @@ function handleMessageSend() { ...@@ -175,6 +205,13 @@ function handleMessageSend() {
if (data) { if (data) {
replyTextContent += data replyTextContent += data
assistantFullAnswerContent.value = (assistantFullAnswerContent.value + data).replace(
/\^\[[\d\\[\]-]+?\]\^/g,
'',
)
!sentenceExtractCheckEnabled.value && isEnableVoice.value && sentenceExtract()
emit('updateSpecifyMessageItem', currentMessageIndex, { emit('updateSpecifyMessageItem', currentMessageIndex, {
textContent: replyTextContent, textContent: replyTextContent,
isTextContentLoading: false, isTextContentLoading: false,
...@@ -226,6 +263,95 @@ function handleSelectFile(cb: () => void) { ...@@ -226,6 +263,95 @@ function handleSelectFile(cb: () => void) {
cb() cb()
} }
function sentenceExtract() {
sentenceExtractCheckEnabled.value = true
const symbolRegExp = /[。!?;.!?;]/g
let sentenceDraft = assistantFullAnswerContent.value
.replace(/\s/gi, '')
.slice(currentReplyContentSentenceExtractIndex.value)
let matchResult = symbolRegExp.exec(sentenceDraft)
function matchExtract() {
const article = assistantFullAnswerContent.value.replace(/\s/gi, '')
if (matchResult && matchResult.index && matchResult.index > 60) {
sentenceDraft = article.slice(
currentReplyContentSentenceExtractIndex.value,
currentReplyContentSentenceExtractIndex.value + matchResult.index + matchResult['0'].length,
)
currentReplyContentSentenceExtractIndex.value += sentenceDraft.length
ttsSocketSendText(sentenceDraft, sentenceFragmentSerialNo.value)
sentenceFragmentSerialNo.value += 1
if (article.length - currentReplyContentSentenceExtractIndex.value > 60) {
sentenceDraft = article.slice(currentReplyContentSentenceExtractIndex.value)
matchResult = symbolRegExp.exec(sentenceDraft)
matchExtract()
} else {
setTimeout(() => sentenceExtract(), 600)
}
} else if (!isAnswerResponseWait.value) {
/* 延时避免最后回复内容没有更新全 */
setTimeout(() => {
sentenceDraft = article.slice(currentReplyContentSentenceExtractIndex.value)
ttsSocketSendText(sentenceDraft, sentenceFragmentSerialNo.value)
sentenceFragmentSerialNo.value += 1
}, 700)
} else {
sentenceDraft = assistantFullAnswerContent.value
.replace(/\s/gi, '')
.slice(currentReplyContentSentenceExtractIndex.value)
matchResult = symbolRegExp.exec(sentenceDraft)
setTimeout(() => matchExtract(), 500)
}
}
if (matchResult) matchExtract()
else setTimeout(() => sentenceExtract(), 600)
}
function ttsSocketSendText(text: string, audioUrlSerialNo: number) {
const ttsSocketCtl = new WebSocketCtr(TEXTTOSPEECH_WS_URL)
ttsSocketCtl.onMessage = (data: { audio: string; replyVoiceUrl: string; final: boolean }) => {
if (data.replyVoiceUrl) {
const currentMessageIndex = props.messageList.length - 1
if (props.messageList[currentMessageIndex]?.voiceFragmentUrlList) {
const voiceFragmentUrlListDraft = [...props.messageList[currentMessageIndex].voiceFragmentUrlList]
voiceFragmentUrlListDraft[audioUrlSerialNo] = data.replyVoiceUrl
emit('updateSpecifyMessageItem', currentMessageIndex, { voiceFragmentUrlList: voiceFragmentUrlListDraft })
if (props.answerAudioAutoPlaying && audioUrlSerialNo === 0 && voiceFragmentUrlListDraft[audioUrlSerialNo]) {
emit('audioPlay', props.messageList[currentMessageIndex])
}
}
}
}
const content = (text || '').replace(/\^\[[\d\\[\]-]+?\]\^/g, '')
if (content) {
ttsSocketCtl.connect(() => {
ttsSocketCtl.send({
codec: 'wav',
sampleRate: 16000,
speed: 0,
voiceType: personalAppConfigStore.voiceConfig.timbreId,
volume: 0,
content,
})
})
}
}
defineExpose({ defineExpose({
blockMessageResponse, blockMessageResponse,
}) })
......
...@@ -11,12 +11,16 @@ interface Props { ...@@ -11,12 +11,16 @@ interface Props {
messageItem: ConversationMessageItem messageItem: ConversationMessageItem
} }
const { t } = useI18n() const props = defineProps<Props>()
const userStore = useUserStore() const emit = defineEmits<{
audioPlay: []
audioPause: []
}>()
defineProps<Props>() const { t } = useI18n()
const userStore = useUserStore()
const personalAppConfigStore = usePersonalAppConfigStore() const personalAppConfigStore = usePersonalAppConfigStore()
const useAvatar = computed(() => { const useAvatar = computed(() => {
...@@ -26,6 +30,20 @@ const useAvatar = computed(() => { ...@@ -26,6 +30,20 @@ const useAvatar = computed(() => {
const assistantAvatar = computed(() => { const assistantAvatar = computed(() => {
return personalAppConfigStore.baseInfo.agentAvatar return personalAppConfigStore.baseInfo.agentAvatar
}) })
const isShowAudioControl = computed(() => {
return (
props.role === 'assistant' && !props.messageItem.isVoiceLoading && !!props.messageItem.voiceFragmentUrlList.length
)
})
function handleAudioControl() {
if (props.messageItem.isVoicePlaying) {
emit('audioPause')
} else {
emit('audioPlay')
}
}
</script> </script>
<template> <template>
...@@ -36,33 +54,47 @@ const assistantAvatar = computed(() => { ...@@ -36,33 +54,47 @@ const assistantAvatar = computed(() => {
:width="32" :width="32"
:height="32" :height="32"
object-fit="cover" object-fit="cover"
class="mr-2 mt-1.5 h-8 w-8 rounded-full" class="mr-2 mt-1.5 h-8 w-8 flex-shrink-0 rounded-full"
/> />
<div <div class="flex flex-col items-start">
class="min-w-[80px] max-w-[calc(100%-32px-12px)] flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]" <div
:class="role === 'user' ? 'bg-[#4b87ff] text-white' : 'bg-white text-[#333]'" class="min-w-[80px] max-w-full flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
> :class="role === 'user' ? 'bg-[#4b87ff] text-white' : 'bg-white text-[#333]'"
<div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4"> >
<CustomLoading /> <div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4">
</div> <CustomLoading />
</div>
<div v-else> <div v-else>
<p class="break-all"> <p class="break-all">
<MarkdownRender <MarkdownRender
:raw-text-content=" :raw-text-content="
messageItem.isEmptyContent messageItem.isEmptyContent
? 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="role === 'user' ? '#fff' : '#192338'"
/> />
</p> </p>
<div v-show="role === 'assistant' && messageItem.isAnswerResponseLoading" class="mb-[5px] mt-4 px-4"> <div v-show="role === 'assistant' && messageItem.isAnswerResponseLoading" class="mb-[5px] mt-4 px-4">
<CustomLoading /> <CustomLoading />
</div>
</div> </div>
</div> </div>
<div
v-show="isShowAudioControl"
class="hover:text-theme-color text-font-color flex cursor-pointer items-center gap-0.5 hover:opacity-80"
@click="handleAudioControl"
>
<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%]" />
<span class="text-[12px]" :class="messageItem.isVoicePlaying ? 'text-theme-color' : ''">
{{ messageItem.isVoicePlaying ? t('common_module.stop_playing') : t('common_module.start_playing') }}
</span>
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -12,6 +12,11 @@ interface Props { ...@@ -12,6 +12,11 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
defineEmits<{
audioPlay: [messageItem: ConversationMessageItem]
audioPause: []
}>()
const { scrollRef, scrollToBottom } = useScroll() const { scrollRef, scrollToBottom } = useScroll()
const isShowContinueQuestion = computed(() => { const isShowContinueQuestion = computed(() => {
...@@ -35,6 +40,8 @@ defineExpose({ ...@@ -35,6 +40,8 @@ defineExpose({
:key="messageItem.timestamp" :key="messageItem.timestamp"
:role="messageItem.role" :role="messageItem.role"
:message-item="messageItem" :message-item="messageItem"
@audio-play="() => $emit('audioPlay', messageItem)"
@audio-pause="() => $emit('audioPause')"
/> />
</div> </div>
......
<script setup lang="ts">
import { computed, h, readonly, ref, shallowRef, watch } from 'vue'
import { SelectOption, SelectRenderTag } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { Howl } from 'howler'
import { useSystemLanguageStore } from '@/store/modules/system-language'
import CustomModal from '@/components/custom-modal/custom-modal.vue'
import { fetchGetTimbreList } from '@/apis/timber'
import { TimbreLanguageInfoItem, TimbreInfoItem } from '../types'
import { validBrowser } from '@/utils/browser-detection'
interface Props {
modalTitle: string
isShowModal: boolean
btnLoading: boolean
timbreInfo?: TimbreLanguageInfoItem
}
interface Emits {
(e: 'update:isShowModal', value: boolean): void
(e: 'confirm', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const { t } = useI18n()
const systemLanguageStore = useSystemLanguageStore()
const languageInfoList = ref<TimbreLanguageInfoItem[]>([])
const timbreInfoList = ref<TimbreInfoItem[]>([])
const timbreOptionList = ref<SelectOption[]>([])
const selectedLanguage = ref('')
const selectedTimberId = ref('')
const timberUrl = ref('')
const isPlaySound = ref(false)
const requestDataLoading = ref(false)
const currentSoundCtl = shallowRef<Howl | null>(null)
let timbreRenderTag: SelectRenderTag
const languageOptions = readonly([
{ label: () => h('span', {}, t('common_module.mandarin')), value: 'zh-CN' },
{ label: () => h('span', {}, t('common_module.cantonese')), value: 'zh-HK' },
{ label: () => h('span', {}, t('common_module.english')), value: 'en' },
])
const showModal = computed({
get() {
return props.isShowModal
},
set(value: boolean) {
handleAudioPause()
emit('update:isShowModal', value)
},
})
const isHasTimberUrl = computed(() => {
return !!timberUrl.value
})
const confirmBtnDisabled = computed(() => {
return requestDataLoading.value || !selectedTimberId.value
})
watch(
() => showModal.value,
(value) => {
value && handleGetTimberList()
},
)
async function handleGetTimberList() {
requestDataLoading.value = true
selectedLanguage.value = props.timbreInfo?.matchLang || systemLanguageStore.currentLanguageInfo.key
const res = await fetchGetTimbreList<TimbreLanguageInfoItem[]>()
if (res.code === 0) {
requestDataLoading.value = false
languageInfoList.value = res.data
timbreInfoList.value =
languageInfoList.value.find((item) => item.matchLang === selectedLanguage.value)?.timbreInfo || []
selectedTimberId.value = props.timbreInfo?.timbreInfo?.[0]?.timbreId || timbreInfoList.value?.[0]?.timbreId || ''
timberUrl.value = props.timbreInfo?.timbreInfo?.[0]?.voiceUrl || timbreInfoList.value?.[0]?.voiceUrl || ''
handleRenderTimbreOption()
}
}
function handleRenderTimbreOption() {
timbreOptionList.value = timbreInfoList.value.map((item) => {
return {
label: () =>
h('div', { class: 'flex items-baseline justify-between w-[180px]' }, [
h('span', {}, { default: () => item.timbreName }),
h('i', {
class: {
'iconfont icon-playing ': item.isPlaying,
'iconfont icon-stop text-[14px]': !item.isPlaying,
'text-theme-color text-[14px]': true,
},
onClick: (e) => {
e.stopPropagation()
timbreInfoList.value.forEach((mockItem) => {
if (mockItem.timbreId !== item.timbreId) {
mockItem.isPlaying = false
}
})
isPlaySound.value = false
item.isPlaying = !item.isPlaying
if (item.isPlaying) {
handleAudioPlay(item.voiceUrl)
}
},
}),
]),
value: item.timbreId,
nickName: item.timbreName,
timberUrl: item.voiceUrl,
}
})
timbreRenderTag = ({ option }) => {
return h('span', {}, { default: () => option.nickName as string })
}
}
function handleUpdateTimberLanguage(value: string) {
timbreInfoList.value = languageInfoList.value.find((item) => item.matchLang === value)?.timbreInfo || []
selectedTimberId.value = timbreInfoList.value?.[0].timbreId || ''
timberUrl.value = timbreInfoList.value?.[0].voiceUrl || ''
isPlaySound.value = false
handleAudioPause()
handleRenderTimbreOption()
}
function handleUpdateTimber(_value: string, option: SelectOption) {
timberUrl.value = option.timberUrl as string
}
function handleClickTimber() {
isPlaySound.value = !isPlaySound.value
if (isPlaySound.value) {
timbreInfoList.value.forEach((item) => (item.isPlaying = false))
handleAudioPlay(timberUrl.value)
} else {
handleAudioPause()
}
}
function handleUpdateTimberId() {
selectedTimberId.value && emit('confirm', selectedTimberId.value)
}
function howlSoundFactory(url: string) {
const soundCtl = new Howl({
src: [url],
format: ['mpeg'],
html5: !validBrowser(),
preload: true,
autoplay: true,
onplay: () => {
currentSoundCtl.value = soundCtl
},
onend: () => {
soundCtl.unload()
timbreInfoList.value.forEach((timbreItem) => (timbreItem.isPlaying = false))
isPlaySound.value = false
},
})
return soundCtl
}
function handleAudioPlay(audioUrl: string) {
if (currentSoundCtl.value) {
currentSoundCtl.value.pause()
currentSoundCtl.value.unload()
currentSoundCtl.value = null
}
howlSoundFactory(audioUrl)
}
function handleAudioPause() {
if (currentSoundCtl.value) {
currentSoundCtl.value.pause()
currentSoundCtl.value.unload()
currentSoundCtl.value = null
}
timbreInfoList.value.forEach((timbreItem) => (timbreItem.isPlaying = false))
isPlaySound.value = false
}
</script>
<template>
<CustomModal
v-model:is-show="showModal"
:title="modalTitle"
:btn-loading="btnLoading"
:btn-disabled="confirmBtnDisabled"
:width="515"
:height="378"
@confirm="handleUpdateTimberId"
>
<template #content>
<div class="text-gray-font-color mb-2">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_timbre_message') }}
</div>
<div v-show="!requestDataLoading" class="flex items-center gap-3">
<n-select
v-model:value="selectedLanguage"
class="w-[211px]!"
:options="languageOptions"
@update:value="handleUpdateTimberLanguage"
/>
<n-select
v-model:value="selectedTimberId"
class="w-[211px]!"
:options="timbreOptionList"
:render-tag="timbreRenderTag"
:show-checkmark="false"
@update:value="handleUpdateTimber"
/>
<div
class="flex h-5 w-5 items-center justify-center rounded-full bg-[#eff0ff]"
:class="isHasTimberUrl ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'"
>
<i
class="iconfont text-theme-color text-[13px] leading-[13px]"
:class="isPlaySound ? 'icon-playing' : 'icon-stop'"
@click="handleClickTimber"
/>
</div>
</div>
<div v-show="requestDataLoading" class="flex items-center gap-3">
<n-skeleton height="32px" />
<n-skeleton height="32px" />
<n-skeleton height="20px" width="20px" circle class="flex-shrink-0" />
</div>
</template>
</CustomModal>
</template>
export interface TimbreInfoItem {
timbreId: string
timbreName: string
voiceUrl: string
isPlaying: boolean
}
export interface TimbreLanguageInfoItem {
language: number
matchLang: string
timbreInfo: TimbreInfoItem[]
}
...@@ -7,6 +7,8 @@ import { useUserStore } from '@/store/modules/user' ...@@ -7,6 +7,8 @@ import { useUserStore } from '@/store/modules/user'
import { UploadStatus } from '@/enums/upload-status' import { UploadStatus } from '@/enums/upload-status'
import { useDialogueFile } from '@/composables/useDialogueFile' import { useDialogueFile } from '@/composables/useDialogueFile'
import { useLayoutConfig } from '@/composables/useLayoutConfig' import { useLayoutConfig } from '@/composables/useLayoutConfig'
import { TEXTTOSPEECH_WS_URL } from '@/config/base-url'
import WebSocketCtr from '@/utils/web-socket-ctr'
interface Props { interface Props {
agentId: string agentId: string
...@@ -14,6 +16,9 @@ interface Props { ...@@ -14,6 +16,9 @@ interface Props {
messageList: ConversationMessageItem[] messageList: ConversationMessageItem[]
continuousQuestionStatus: 'default' | 'close' continuousQuestionStatus: 'default' | 'close'
isEnableDocumentParse: boolean isEnableDocumentParse: boolean
isEnableVoice: boolean
answerAudioAutoPlaying: boolean
timbreId: string
} }
const { t } = useI18n() const { t } = useI18n()
...@@ -29,6 +34,7 @@ const emit = defineEmits<{ ...@@ -29,6 +34,7 @@ const emit = defineEmits<{
toLogin: [] toLogin: []
createContinueQuestions: [value: string] createContinueQuestions: [value: string]
resetContinueQuestionList: [] resetContinueQuestionList: []
audioPlay: [messageItem: ConversationMessageItem, requestId?: string]
}>() }>()
const { isMobile } = useLayoutConfig() const { isMobile } = useLayoutConfig()
...@@ -40,8 +46,11 @@ const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = us ...@@ -40,8 +46,11 @@ const { uploadFileList, handleLimitUpload, handleUpload, handleRemoveFile } = us
const emitter = inject<Emitter<MittEvents>>('emitter') const emitter = inject<Emitter<MittEvents>>('emitter')
const inputMessageContent = ref('') const inputMessageContent = ref('')
const isAnswerResponseWait = ref(false) const isAnswerResponseWait = ref(false)
const currentReplyContentSentenceExtractIndex = ref(0)
const sentenceFragmentSerialNo = ref(0)
const sentenceExtractCheckEnabled = ref(false)
const assistantFullAnswerContent = ref('')
let controller: AbortController | null = null let controller: AbortController | null = null
...@@ -94,7 +103,7 @@ onUnmounted(() => { ...@@ -94,7 +103,7 @@ onUnmounted(() => {
emitter?.off('selectQuestion') emitter?.off('selectQuestion')
}) })
function messageItemFactory() { function messageItemFactory(): ConversationMessageItem {
return { return {
timestamp: Date.now(), timestamp: Date.now(),
role: 'user', role: 'user',
...@@ -102,7 +111,10 @@ function messageItemFactory() { ...@@ -102,7 +111,10 @@ function messageItemFactory() {
isEmptyContent: false, isEmptyContent: false,
isTextContentLoading: false, isTextContentLoading: false,
isAnswerResponseLoading: false, isAnswerResponseLoading: false,
} as const isVoiceLoading: false,
isVoicePlaying: false,
voiceFragmentUrlList: [],
}
} }
function handleMessageSend() { function handleMessageSend() {
...@@ -122,6 +134,7 @@ function handleMessageSend() { ...@@ -122,6 +134,7 @@ function handleMessageSend() {
role: 'assistant', role: 'assistant',
isTextContentLoading: true, isTextContentLoading: true,
isAnswerResponseLoading: true, isAnswerResponseLoading: true,
isVoiceLoading: true,
}) })
emit('updatePageScroll') emit('updatePageScroll')
...@@ -130,6 +143,10 @@ function handleMessageSend() { ...@@ -130,6 +143,10 @@ function handleMessageSend() {
let replyTextContent = '' let replyTextContent = ''
isAnswerResponseWait.value = true isAnswerResponseWait.value = true
inputMessageContent.value = '' inputMessageContent.value = ''
currentReplyContentSentenceExtractIndex.value = 0
sentenceFragmentSerialNo.value = 0
sentenceExtractCheckEnabled.value = false
assistantFullAnswerContent.value = ''
controller = new AbortController() controller = new AbortController()
...@@ -149,6 +166,13 @@ function handleMessageSend() { ...@@ -149,6 +166,13 @@ function handleMessageSend() {
isTextContentLoading: false, isTextContentLoading: false,
isAnswerResponseLoading: false, isAnswerResponseLoading: false,
}) })
if (!props.answerAudioAutoPlaying) {
emit('updateSpecifyMessageItem', currentMessageIndex, {
isVoiceLoading: false,
})
}
isCreateContinueQuestions.value && emit('createContinueQuestions', replyTextContent) isCreateContinueQuestions.value && emit('createContinueQuestions', replyTextContent)
emit('updatePageScroll') emit('updatePageScroll')
blockMessageResponse() blockMessageResponse()
...@@ -158,6 +182,13 @@ function handleMessageSend() { ...@@ -158,6 +182,13 @@ function handleMessageSend() {
if (data) { if (data) {
replyTextContent += data replyTextContent += data
assistantFullAnswerContent.value = (assistantFullAnswerContent.value + data).replace(
/\^\[[\d\\[\]-]+?\]\^/g,
'',
)
!sentenceExtractCheckEnabled.value && props.isEnableVoice && sentenceExtract()
emit('updateSpecifyMessageItem', currentMessageIndex, { emit('updateSpecifyMessageItem', currentMessageIndex, {
textContent: replyTextContent, textContent: replyTextContent,
isTextContentLoading: false, isTextContentLoading: false,
...@@ -213,6 +244,95 @@ function handleSelectFile(cb: () => void) { ...@@ -213,6 +244,95 @@ function handleSelectFile(cb: () => void) {
cb() cb()
} }
function sentenceExtract() {
sentenceExtractCheckEnabled.value = true
const symbolRegExp = /[。!?;.!?;]/g
let sentenceDraft = assistantFullAnswerContent.value
.replace(/\s/gi, '')
.slice(currentReplyContentSentenceExtractIndex.value)
let matchResult = symbolRegExp.exec(sentenceDraft)
function matchExtract() {
const article = assistantFullAnswerContent.value.replace(/\s/gi, '')
if (matchResult && matchResult.index && matchResult.index > 60) {
sentenceDraft = article.slice(
currentReplyContentSentenceExtractIndex.value,
currentReplyContentSentenceExtractIndex.value + matchResult.index + matchResult['0'].length,
)
currentReplyContentSentenceExtractIndex.value += sentenceDraft.length
ttsSocketSendText(sentenceDraft, sentenceFragmentSerialNo.value)
sentenceFragmentSerialNo.value += 1
if (article.length - currentReplyContentSentenceExtractIndex.value > 60) {
sentenceDraft = article.slice(currentReplyContentSentenceExtractIndex.value)
matchResult = symbolRegExp.exec(sentenceDraft)
matchExtract()
} else {
setTimeout(() => sentenceExtract(), 600)
}
} else if (!isAnswerResponseWait.value) {
/* 延时避免最后回复内容没有更新全 */
setTimeout(() => {
sentenceDraft = article.slice(currentReplyContentSentenceExtractIndex.value)
ttsSocketSendText(sentenceDraft, sentenceFragmentSerialNo.value)
sentenceFragmentSerialNo.value += 1
}, 700)
} else {
sentenceDraft = assistantFullAnswerContent.value
.replace(/\s/gi, '')
.slice(currentReplyContentSentenceExtractIndex.value)
matchResult = symbolRegExp.exec(sentenceDraft)
setTimeout(() => matchExtract(), 500)
}
}
if (matchResult) matchExtract()
else setTimeout(() => sentenceExtract(), 600)
}
function ttsSocketSendText(text: string, audioUrlSerialNo: number) {
const ttsSocketCtl = new WebSocketCtr(TEXTTOSPEECH_WS_URL)
ttsSocketCtl.onMessage = (data: { audio: string; replyVoiceUrl: string; final: boolean }) => {
if (data.replyVoiceUrl) {
const currentMessageIndex = props.messageList.length - 1
if (props.messageList[currentMessageIndex]?.voiceFragmentUrlList) {
const voiceFragmentUrlListDraft = [...props.messageList[currentMessageIndex].voiceFragmentUrlList]
voiceFragmentUrlListDraft[audioUrlSerialNo] = data.replyVoiceUrl
emit('updateSpecifyMessageItem', currentMessageIndex, { voiceFragmentUrlList: voiceFragmentUrlListDraft })
if (props.answerAudioAutoPlaying && audioUrlSerialNo === 0 && voiceFragmentUrlListDraft[audioUrlSerialNo]) {
emit('audioPlay', props.messageList[currentMessageIndex])
}
}
}
}
const content = (text || '').replace(/\^\[[\d\\[\]-]+?\]\^/g, '')
if (content) {
ttsSocketCtl.connect(() => {
ttsSocketCtl.send({
codec: 'wav',
sampleRate: 16000,
speed: 0,
voiceType: props.timbreId,
volume: 0,
content,
})
})
}
}
defineExpose({ defineExpose({
blockMessageResponse, blockMessageResponse,
}) })
...@@ -336,8 +456,9 @@ defineExpose({ ...@@ -336,8 +456,9 @@ defineExpose({
? 'hover:text-theme-color text-font-color cursor-pointer' ? 'hover:text-theme-color text-font-color cursor-pointer'
: 'cursor-not-allowed text-[#b8babf]' : 'cursor-not-allowed text-[#b8babf]'
" "
@click="handleClearAllMessage"
> >
<i class="iconfont icon-clear text-base leading-none" @click="handleClearAllMessage" /> <i class="iconfont icon-clear text-base leading-none" />
</div> </div>
</template> </template>
<span class="text-xs"> {{ t('common_module.dialogue_module.clear_message_popover_message') }}</span> <span class="text-xs"> {{ t('common_module.dialogue_module.clear_message_popover_message') }}</span>
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import CustomLoading from './custom-loading.vue' import CustomLoading from './custom-loading.vue'
import MusicWavesLoading from './music-waves-loading.vue'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue' import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { useLayoutConfig } from '@/composables/useLayoutConfig' import { useLayoutConfig } from '@/composables/useLayoutConfig'
...@@ -17,6 +18,11 @@ const { t } = useI18n() ...@@ -17,6 +18,11 @@ const { t } = useI18n()
const props = defineProps<Props>() const props = defineProps<Props>()
const emit = defineEmits<{
audioPlay: []
audioPause: []
}>()
const userStore = useUserStore() const userStore = useUserStore()
const { isMobile } = useLayoutConfig() const { isMobile } = useLayoutConfig()
...@@ -31,6 +37,32 @@ const assistantAvatar = computed(() => { ...@@ -31,6 +37,32 @@ const assistantAvatar = computed(() => {
'https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp' 'https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp'
) )
}) })
const isShowWebAudioControl = computed(() => {
return (
props.role === 'assistant' &&
!props.messageItem.isVoiceLoading &&
!isMobile.value &&
!!props.messageItem.voiceFragmentUrlList.length
)
})
const isShowMobileAudioControl = computed(() => {
return (
props.role === 'assistant' &&
!props.messageItem.isVoiceLoading &&
isMobile.value &&
!!props.messageItem.voiceFragmentUrlList.length
)
})
function handleAudioControl() {
if (props.messageItem.isVoicePlaying) {
emit('audioPause')
} else {
emit('audioPlay')
}
}
</script> </script>
<template> <template>
...@@ -48,36 +80,60 @@ const assistantAvatar = computed(() => { ...@@ -48,36 +80,60 @@ const assistantAvatar = computed(() => {
:width="32" :width="32"
:height="32" :height="32"
object-fit="cover" object-fit="cover"
class="mr-2 mt-1.5 h-8 w-8 rounded-full" class="mr-2 mt-1.5 h-8 w-8 flex-shrink-0 rounded-full"
/> />
<div <div class="flex w-full flex-col" :class="isMobile && role === 'user' ? 'items-end' : 'items-start'">
class="min-w-[80px] flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]" <div
:class="[ class="min-w-[80px] flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
role === 'user' ? 'bg-theme-color text-white' : 'bg-white text-[#333]', :class="[
isMobile ? 'max-w-[calc(100%-20px)]' : 'max-w-[calc(100%-32px-12px)]', role === 'user' ? 'bg-theme-color text-white' : 'bg-white text-[#333]',
]" isMobile ? 'max-w-[calc(100%-20px)]' : 'max-w-full',
> ]"
<div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4"> >
<CustomLoading /> <div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4">
</div> <CustomLoading />
</div>
<div v-else>
<p class="break-all">
<MarkdownRender
:raw-text-content="
messageItem.isEmptyContent
? t('common_module.dialogue_module.empty_message_content')
: messageItem.textContent
"
:color="role === 'user' ? '#fff' : '#192338'"
/>
</p>
<div v-else> <div v-show="role === 'assistant' && messageItem.isAnswerResponseLoading" class="mb-[5px] mt-4 px-4">
<p class="break-all"> <CustomLoading />
<MarkdownRender </div>
:raw-text-content=" </div>
messageItem.isEmptyContent
? t('common_module.dialogue_module.empty_message_content') <div v-show="isShowMobileAudioControl" class="mt-[13px] flex items-center gap-2">
: messageItem.textContent <div
" class="h-[18px] w-[18px] cursor-pointer"
:color="role === 'user' ? '#fff' : '#192338'" :class="messageItem.isVoicePlaying ? 'bg-svg-pause' : 'bg-svg-play'"
@click="handleAudioControl"
/> />
</p>
<div v-show="role === 'assistant' && messageItem.isAnswerResponseLoading" class="mb-[5px] mt-4 px-4"> <MusicWavesLoading v-show="messageItem.isVoicePlaying" bar-bg-color="#333" />
<CustomLoading />
</div> </div>
</div> </div>
<div
v-show="isShowWebAudioControl"
class="hover:text-theme-color text-font-color flex cursor-pointer items-center gap-0.5 hover:opacity-80"
@click="handleAudioControl"
>
<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%]" />
<span class="text-[12px]" :class="messageItem.isVoicePlaying ? 'text-theme-color' : ''">
{{ messageItem.isVoicePlaying ? t('common_module.stop_playing') : t('common_module.start_playing') }}
</span>
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -14,6 +14,11 @@ interface Props { ...@@ -14,6 +14,11 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
defineEmits<{
audioPlay: [messageItem: ConversationMessageItem]
audioPause: []
}>()
const { scrollRef, scrollToBottom } = useScroll() const { scrollRef, scrollToBottom } = useScroll()
const isShowContinueQuestion = computed(() => { const isShowContinueQuestion = computed(() => {
...@@ -38,6 +43,8 @@ defineExpose({ ...@@ -38,6 +43,8 @@ defineExpose({
:role="messageItem.role" :role="messageItem.role"
:message-item="messageItem" :message-item="messageItem"
:agent-application-config="agentApplicationConfig" :agent-application-config="agentApplicationConfig"
@audio-play="() => $emit('audioPlay', messageItem)"
@audio-pause="() => $emit('audioPause')"
/> />
</div> </div>
......
<script setup lang="ts">
interface Props {
barBgColor?: string
height?: string
}
withDefaults(defineProps<Props>(), {
barBgColor: '#363636',
height: '12px',
})
</script>
<template>
<div class="music">
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
</div>
</template>
<style lang="scss" scoped>
.music {
display: flex;
align-items: center;
justify-content: space-between;
width: 42px;
height: v-bind(height);
.bar {
width: 2px;
/* stylelint-disable-next-line value-keyword-case */
background: v-bind(barBgColor);
border-radius: 50px;
animation: loader 1.5s ease-in-out infinite;
&:nth-child(1) {
/* background: purple; */
animation-delay: 1s;
}
&:nth-child(2) {
/* background: crimson; */
animation-delay: 0.8s;
}
&:nth-child(3) {
/* background: deeppink; */
animation-delay: 0.6s;
}
&:nth-child(4) {
/* background: orange; */
animation-delay: 0.4s;
}
&:nth-child(5) {
/* background: gold; */
animation-delay: 0.2s;
}
&:nth-child(6) {
/* background: gold; */
animation-delay: 0.2s;
}
&:nth-child(7) {
/* background: gold; */
animation-delay: 0.4s;
}
&:nth-child(8) {
/* background: deeppink; */
animation-delay: 0.6s;
}
&:nth-child(9) {
/* background: crimson; */
animation-delay: 0.8s;
}
&:nth-child(10) {
/* background: purple; */
animation-delay: 1s;
}
}
}
@keyframes loader {
0% {
height: 4px;
}
50% {
height: v-bind(height);
}
100% {
height: 4px;
}
}
</style>
...@@ -55,8 +55,8 @@ function handleToLogin() { ...@@ -55,8 +55,8 @@ function handleToLogin() {
<span class="mb-1 line-clamp-1 max-w-[200px] break-all">{{ agentApplicationConfig.baseInfo.agentTitle }}</span> <span class="mb-1 line-clamp-1 max-w-[200px] break-all">{{ agentApplicationConfig.baseInfo.agentTitle }}</span>
<div class="flex items-center text-xs text-[#84868c]"> <div class="flex items-center text-xs text-[#84868c]">
<img v-show="isLogin" :src="agentMemberInfo.avatarUrl" class="mr-2 h-5 w-5 rounded-full" /> <img v-show="isLogin" :src="agentMemberInfo.avatarUrl" class="mr-2 h-5 w-5 rounded-full" />
<n-ellipsis v-show="isLogin" class="max-w-[120px]! mr-4"> <n-ellipsis class="max-w-[120px]! mr-4">
<span class="select-none">{{ agentMemberInfo.nickName }}</span> <span v-show="isLogin" class="select-none">{{ agentMemberInfo.nickName }}</span>
</n-ellipsis> </n-ellipsis>
<span> <span>
{{ t('common_module.publish_time_in') }} {{ t('common_module.publish_time_in') }}
......
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' 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 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'
...@@ -9,8 +10,15 @@ import FooterInput from './components/footer-input.vue' ...@@ -9,8 +10,15 @@ import FooterInput from './components/footer-input.vue'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { defaultPersonalAppConfigState } from '@/store/modules/personal-app-config' import { defaultPersonalAppConfigState } from '@/store/modules/personal-app-config'
import { fetchCreateContinueQuestions, fetchCreateDialogues, fetchGetApplicationInfo } from '@/apis/agent-application' import {
fetchCreateContinueQuestions,
fetchCreateDialogues,
fetchGetApplicationInfo,
fetchGetAutoPlayByAgentId,
fetchUpdateAutoPlay,
} from '@/apis/agent-application'
import { useLayoutConfig } from '@/composables/useLayoutConfig' import { useLayoutConfig } from '@/composables/useLayoutConfig'
import { validBrowser } from '@/utils/browser-detection'
const { t } = useI18n() const { t } = useI18n()
...@@ -33,11 +41,20 @@ const messageList = ref<ConversationMessageItem[]>([]) ...@@ -33,11 +41,20 @@ const messageList = ref<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 currentPlayMessageItem = ref<ConversationMessageItem | null>(null)
const currentPlayAudioFragmentSerialNo = ref(0)
const currentSoundCtl = shallowRef<Howl | null>(null)
const isSoundCtlCreated = ref(false)
const isEnableDocumentParse = computed(() => { const isEnableDocumentParse = computed(() => {
return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y' return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y'
}) })
const isEnableVoice = computed(() => {
return !!agentApplicationConfig.value.voiceConfig.timbreId
})
onMounted(async () => { onMounted(async () => {
if (router.currentRoute.value.params.agentId) { if (router.currentRoute.value.params.agentId) {
agentId.value = router.currentRoute.value.params.agentId as string agentId.value = router.currentRoute.value.params.agentId as string
...@@ -53,7 +70,10 @@ onMounted(async () => { ...@@ -53,7 +70,10 @@ onMounted(async () => {
if (agentId.value) { if (agentId.value) {
fullScreenLoading.value = true fullScreenLoading.value = true
await handleGetApplicationDetail() await handleGetApplicationDetail()
userStore.isLogin && (await handleCreateDialogues()) if (userStore.isLogin) {
await handleCreateDialogues()
await handleGetAutoPlayByAgentId()
}
fullScreenLoading.value = false fullScreenLoading.value = false
return return
} }
...@@ -61,6 +81,11 @@ onMounted(async () => { ...@@ -61,6 +81,11 @@ onMounted(async () => {
router.replace({ name: 'Home' }) router.replace({ name: 'Home' })
}) })
onUnmounted(() => {
handleAudioPause()
footerInputRef.value?.blockMessageResponse()
})
async function handleGetApplicationDetail() { async function handleGetApplicationDetail() {
fetchGetApplicationInfo<PersonalAppConfigState>(agentId.value) fetchGetApplicationInfo<PersonalAppConfigState>(agentId.value)
.then((res) => { .then((res) => {
...@@ -81,6 +106,14 @@ async function handleCreateDialogues() { ...@@ -81,6 +106,14 @@ async function handleCreateDialogues() {
} }
} }
async function handleGetAutoPlayByAgentId() {
const res = await fetchGetAutoPlayByAgentId<'Y' | 'N'>(agentId.value)
if (res.code === 0) {
answerAudioAutoPlaying.value = res.data === 'Y'
}
}
function handleToLoginPage() { function handleToLoginPage() {
router.push({ router.push({
name: 'Login', name: 'Login',
...@@ -96,6 +129,11 @@ function handleCreateApplicationPage() { ...@@ -96,6 +129,11 @@ function handleCreateApplicationPage() {
}) })
} }
async function handleUpdateAutoPlaying(isAutoPlaying: boolean) {
const autoplay = isAutoPlaying ? 'Y' : 'N'
await fetchUpdateAutoPlay(agentId.value, autoplay)
}
function handleAddMessageItem(messageItem: ConversationMessageItem) { function handleAddMessageItem(messageItem: ConversationMessageItem) {
messageList.value.push(messageItem) messageList.value.push(messageItem)
} }
...@@ -123,6 +161,7 @@ function handleClearAllMessage() { ...@@ -123,6 +161,7 @@ function handleClearAllMessage() {
t('common_module.dialogue_module.clear_message_dialog_title'), t('common_module.dialogue_module.clear_message_dialog_title'),
) )
.then(() => { .then(() => {
handleAudioPause()
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
messageList.value = [] messageList.value = []
window.$message.success(t('common_module.clear_success_message')) window.$message.success(t('common_module.clear_success_message'))
...@@ -141,6 +180,91 @@ async function handleCreateContinueQuestions(replyTextContent: string) { ...@@ -141,6 +180,91 @@ async function handleCreateContinueQuestions(replyTextContent: string) {
function handleResetContinueQuestionList() { function handleResetContinueQuestionList() {
continueQuestionList.value = [] continueQuestionList.value = []
} }
function howlSoundFactory(url: string) {
const soundCtl = new Howl({
src: [url],
format: ['mpeg'],
html5: !validBrowser(),
preload: true,
autoplay: true,
onplay: () => {
currentSoundCtl.value = soundCtl
if (currentPlayMessageItem.value) {
currentPlayMessageItem.value.isVoiceLoading = false
currentPlayMessageItem.value.isVoicePlaying = true
}
},
onend: () => {
soundCtl.unload()
currentPlayAudioFragmentSerialNo.value += 1
if (
currentPlayMessageItem.value &&
currentPlayAudioFragmentSerialNo.value > currentPlayMessageItem.value.voiceFragmentUrlList.length - 1
) {
currentPlayMessageItem.value.isVoicePlaying = false
}
let audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value]
if (audioFragmentUrl) {
howlSoundFactory(audioFragmentUrl)
} else if (
(currentPlayMessageItem.value?.voiceFragmentUrlList || []).length > currentPlayAudioFragmentSerialNo.value
) {
let timerId: NodeJS.Timeout | null = null
const audioFragmentCheck = () => {
audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value]
if (audioFragmentUrl) {
timerId && clearInterval(timerId)
howlSoundFactory(audioFragmentUrl)
}
}
timerId = setInterval(audioFragmentCheck, 600)
}
},
})
return soundCtl
}
function handleAudioPlay(currentMessageItem: ConversationMessageItem, specificPlayUrl?: string) {
handleAudioPause()
currentPlayMessageItem.value = currentMessageItem
currentPlayAudioFragmentSerialNo.value = 0
const audioUrl = specificPlayUrl || currentMessageItem.voiceFragmentUrlList[0]
howlSoundFactory(audioUrl)
isSoundCtlCreated.value = true
}
function handleAudioPause(isClearMessageList = false) {
if (currentSoundCtl.value) {
currentSoundCtl.value.pause()
currentSoundCtl.value.unload()
currentSoundCtl.value = null
isSoundCtlCreated.value = false
}
currentPlayMessageItem.value && (currentPlayMessageItem.value.isVoicePlaying = false)
if (isClearMessageList) {
messageList.value = []
footerInputRef.value?.blockMessageResponse()
}
}
</script> </script>
<template> <template>
...@@ -152,6 +276,16 @@ function handleResetContinueQuestionList() { ...@@ -152,6 +276,16 @@ function handleResetContinueQuestionList() {
/> />
<div class="flex h-[calc(100%-48px)] w-full flex-col bg-[#f2f5f9]"> <div class="flex h-[calc(100%-48px)] w-full flex-col bg-[#f2f5f9]">
<div class="mt-5 flex select-none justify-end px-4">
<div v-show="isEnableVoice" class="flex items-center gap-2">
<span>{{ t('common_module.voice_auto_play') }}</span>
<n-switch v-model:value="answerAudioAutoPlaying" size="small" @update:value="handleUpdateAutoPlaying">
<template #checked> {{ t('common_module.open') }} </template>
<template #unchecked> {{ t('common_module.close') }} </template>
</n-switch>
</div>
</div>
<div v-if="messageList.length === 0" class="w-full flex-1 overflow-auto px-4"> <div v-if="messageList.length === 0" class="w-full flex-1 overflow-auto px-4">
<Preamble :agent-application-config="agentApplicationConfig" /> <Preamble :agent-application-config="agentApplicationConfig" />
</div> </div>
...@@ -164,6 +298,8 @@ function handleResetContinueQuestionList() { ...@@ -164,6 +298,8 @@ function handleResetContinueQuestionList() {
:message-list="messageList" :message-list="messageList"
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:continuous-question-list="continueQuestionList" :continuous-question-list="continueQuestionList"
@audio-play="handleAudioPlay"
@audio-pause="handleAudioPause"
/> />
</div> </div>
</div> </div>
...@@ -176,6 +312,9 @@ function handleResetContinueQuestionList() { ...@@ -176,6 +312,9 @@ function handleResetContinueQuestionList() {
:agent-id="agentApplicationConfig.baseInfo.agentId" :agent-id="agentApplicationConfig.baseInfo.agentId"
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:is-enable-document-parse="isEnableDocumentParse" :is-enable-document-parse="isEnableDocumentParse"
:is-enable-voice="isEnableVoice"
:answer-audio-auto-playing="answerAudioAutoPlaying"
:timbre-id="agentApplicationConfig.voiceConfig.timbreId"
@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"
...@@ -184,6 +323,7 @@ function handleResetContinueQuestionList() { ...@@ -184,6 +323,7 @@ function handleResetContinueQuestionList() {
@to-login="handleToLoginPage" @to-login="handleToLoginPage"
@create-continue-questions="handleCreateContinueQuestions" @create-continue-questions="handleCreateContinueQuestions"
@reset-continue-question-list="handleResetContinueQuestionList" @reset-continue-question-list="handleResetContinueQuestionList"
@audio-play="handleAudioPlay"
/> />
</div> </div>
</div> </div>
......
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' 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 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'
import FooterInput from './components/footer-input.vue' import FooterInput from './components/footer-input.vue'
import { fetchCreateContinueQuestions, fetchCreateDialogues, fetchGetApplicationInfo } from '@/apis/agent-application' import {
fetchCreateContinueQuestions,
fetchCreateDialogues,
fetchGetApplicationInfo,
fetchGetAutoPlayByAgentId,
fetchUpdateAutoPlay,
} from '@/apis/agent-application'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { defaultPersonalAppConfigState } from '@/store/modules/personal-app-config' import { defaultPersonalAppConfigState } from '@/store/modules/personal-app-config'
import { createDefaultUserInfoFactory, useUserStore } from '@/store/modules/user' import { createDefaultUserInfoFactory, useUserStore } from '@/store/modules/user'
import { useLayoutConfig } from '@/composables/useLayoutConfig' 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'
const { t } = useI18n() const { t } = useI18n()
...@@ -36,11 +44,20 @@ const messageList = ref<ConversationMessageItem[]>([]) ...@@ -36,11 +44,20 @@ const messageList = ref<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 currentPlayMessageItem = ref<ConversationMessageItem | null>(null)
const currentPlayAudioFragmentSerialNo = ref(0)
const currentSoundCtl = shallowRef<Howl | null>(null)
const isSoundCtlCreated = ref(false)
const isEnableDocumentParse = computed(() => { const isEnableDocumentParse = computed(() => {
return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y' return agentApplicationConfig.value.knowledgeConfig.isDocumentParsing === 'Y'
}) })
const isEnableVoice = computed(() => {
return !!agentApplicationConfig.value.voiceConfig.timbreId
})
onMounted(async () => { onMounted(async () => {
if (router.currentRoute.value.params.agentId) { if (router.currentRoute.value.params.agentId) {
agentId.value = router.currentRoute.value.params.agentId as string agentId.value = router.currentRoute.value.params.agentId as string
...@@ -56,7 +73,11 @@ onMounted(async () => { ...@@ -56,7 +73,11 @@ onMounted(async () => {
if (agentId.value) { if (agentId.value) {
fullScreenLoading.value = true fullScreenLoading.value = true
await handleGetApplicationDetail() await handleGetApplicationDetail()
userStore.isLogin && (await handleCreateDialogues()) if (userStore.isLogin) {
await handleCreateDialogues()
await handleGetAutoPlayByAgentId()
}
fullScreenLoading.value = false fullScreenLoading.value = false
return return
} }
...@@ -64,6 +85,11 @@ onMounted(async () => { ...@@ -64,6 +85,11 @@ onMounted(async () => {
router.replace({ name: 'Home' }) router.replace({ name: 'Home' })
}) })
onUnmounted(() => {
handleAudioPause()
footerInputRef.value?.blockMessageResponse()
})
async function handleGetApplicationDetail() { async function handleGetApplicationDetail() {
fetchGetApplicationInfo<PersonalAppConfigState>(agentId.value) fetchGetApplicationInfo<PersonalAppConfigState>(agentId.value)
.then((res) => { .then((res) => {
...@@ -93,6 +119,14 @@ async function handleCreateDialogues() { ...@@ -93,6 +119,14 @@ async function handleCreateDialogues() {
} }
} }
async function handleGetAutoPlayByAgentId() {
const res = await fetchGetAutoPlayByAgentId<'Y' | 'N'>(agentId.value)
if (res.code === 0) {
answerAudioAutoPlaying.value = res.data === 'Y'
}
}
function handleBack() { function handleBack() {
if (!history.state.back) { if (!history.state.back) {
router.replace({ router.replace({
...@@ -116,6 +150,11 @@ function handleCreateApplicationPage() { ...@@ -116,6 +150,11 @@ function handleCreateApplicationPage() {
}) })
} }
async function handleUpdateAutoPlaying(isAutoPlaying: boolean) {
const autoplay = isAutoPlaying ? 'Y' : 'N'
await fetchUpdateAutoPlay(agentId.value, autoplay)
}
function handleAddMessageItem(messageItem: ConversationMessageItem) { function handleAddMessageItem(messageItem: ConversationMessageItem) {
messageList.value.push(messageItem) messageList.value.push(messageItem)
} }
...@@ -143,6 +182,7 @@ function handleClearAllMessage() { ...@@ -143,6 +182,7 @@ function handleClearAllMessage() {
t('common_module.dialogue_module.clear_message_dialog_title'), t('common_module.dialogue_module.clear_message_dialog_title'),
) )
.then(() => { .then(() => {
handleAudioPause()
footerInputRef.value?.blockMessageResponse() footerInputRef.value?.blockMessageResponse()
messageList.value = [] messageList.value = []
window.$message.success(t('common_module.clear_success_message')) window.$message.success(t('common_module.clear_success_message'))
...@@ -161,6 +201,91 @@ async function handleCreateContinueQuestions(replyTextContent: string) { ...@@ -161,6 +201,91 @@ async function handleCreateContinueQuestions(replyTextContent: string) {
function handleResetContinueQuestionList() { function handleResetContinueQuestionList() {
continueQuestionList.value = [] continueQuestionList.value = []
} }
function howlSoundFactory(url: string) {
const soundCtl = new Howl({
src: [url],
format: ['mpeg'],
html5: !validBrowser(),
preload: true,
autoplay: true,
onplay: () => {
currentSoundCtl.value = soundCtl
if (currentPlayMessageItem.value) {
currentPlayMessageItem.value.isVoiceLoading = false
currentPlayMessageItem.value.isVoicePlaying = true
}
},
onend: () => {
soundCtl.unload()
currentPlayAudioFragmentSerialNo.value += 1
if (
currentPlayMessageItem.value &&
currentPlayAudioFragmentSerialNo.value > currentPlayMessageItem.value.voiceFragmentUrlList.length - 1
) {
currentPlayMessageItem.value.isVoicePlaying = false
}
let audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value]
if (audioFragmentUrl) {
howlSoundFactory(audioFragmentUrl)
} else if (
(currentPlayMessageItem.value?.voiceFragmentUrlList || []).length > currentPlayAudioFragmentSerialNo.value
) {
let timerId: NodeJS.Timeout | null = null
const audioFragmentCheck = () => {
audioFragmentUrl = currentPlayMessageItem.value?.voiceFragmentUrlList[currentPlayAudioFragmentSerialNo.value]
if (audioFragmentUrl) {
timerId && clearInterval(timerId)
howlSoundFactory(audioFragmentUrl)
}
}
timerId = setInterval(audioFragmentCheck, 600)
}
},
})
return soundCtl
}
function handleAudioPlay(currentMessageItem: ConversationMessageItem, specificPlayUrl?: string) {
handleAudioPause()
currentPlayMessageItem.value = currentMessageItem
currentPlayAudioFragmentSerialNo.value = 0
const audioUrl = specificPlayUrl || currentMessageItem.voiceFragmentUrlList[0]
howlSoundFactory(audioUrl)
isSoundCtlCreated.value = true
}
function handleAudioPause(isClearMessageList = false) {
if (currentSoundCtl.value) {
currentSoundCtl.value.pause()
currentSoundCtl.value.unload()
currentSoundCtl.value = null
isSoundCtlCreated.value = false
}
currentPlayMessageItem.value && (currentPlayMessageItem.value.isVoicePlaying = false)
if (isClearMessageList) {
messageList.value = []
footerInputRef.value?.blockMessageResponse()
}
}
</script> </script>
<template> <template>
...@@ -175,33 +300,29 @@ function handleResetContinueQuestionList() { ...@@ -175,33 +300,29 @@ function handleResetContinueQuestionList() {
/> />
<div class="h-[calc(100%-68px)] w-full bg-[#f2f5f9]"> <div class="h-[calc(100%-68px)] w-full bg-[#f2f5f9]">
<div class="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">
<span>{{ t('common_module.voice_auto_play') }}</span>
<n-switch v-model:value="answerAudioAutoPlaying" size="small" @update:value="handleUpdateAutoPlaying">
<template #checked> {{ t('common_module.open') }} </template>
<template #unchecked> {{ t('common_module.close') }} </template>
</n-switch>
</div>
<div v-if="messageList.length === 0" class="w-full flex-1 overflow-auto px-5"> <div v-if="messageList.length === 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.length > 0" class="flex w-full flex-1 flex-col overflow-hidden">
<!-- <div class="my-5 ml-16 mr-5 border-b border-[#e8e9eb] py-6"> <div class="mt-20 flex-1 overflow-auto">
<div class="flex items-start">
<img :src="agentApplicationConfig.baseInfo.agentAvatar" class="h-14 w-14 rounded-xl" />
<p class="font-500 ml-4 line-clamp-1 h-8 max-w-[90%] break-all text-2xl leading-8 text-[#151b26]">
{{ agentApplicationConfig.baseInfo.agentTitle }}
</p>
</div>
<div
v-show="agentApplicationConfig.commConfig.preamble"
class="mt-3 rounded-xl border border-[#e8e9eb] bg-white px-4 py-3 shadow-[0_2px_2px_#0000000a]"
>
{{ agentApplicationConfig.commConfig.preamble }}
</div>
</div> -->
<div class="mt-16 flex-1 overflow-auto">
<MessageList <MessageList
ref="messageListRef" ref="messageListRef"
:agent-application-config="agentApplicationConfig" :agent-application-config="agentApplicationConfig"
:message-list="messageList" :message-list="messageList"
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:continuous-question-list="continueQuestionList" :continuous-question-list="continueQuestionList"
@audio-play="handleAudioPlay"
@audio-pause="handleAudioPause"
/> />
</div> </div>
</div> </div>
...@@ -214,6 +335,9 @@ function handleResetContinueQuestionList() { ...@@ -214,6 +335,9 @@ function handleResetContinueQuestionList() {
:agent-id="agentApplicationConfig.baseInfo.agentId" :agent-id="agentApplicationConfig.baseInfo.agentId"
:continuous-question-status="continuousQuestionStatus" :continuous-question-status="continuousQuestionStatus"
:is-enable-document-parse="isEnableDocumentParse" :is-enable-document-parse="isEnableDocumentParse"
:is-enable-voice="isEnableVoice"
:answer-audio-auto-playing="answerAudioAutoPlaying"
:timbre-id="agentApplicationConfig.voiceConfig.timbreId"
@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"
...@@ -222,6 +346,7 @@ function handleResetContinueQuestionList() { ...@@ -222,6 +346,7 @@ function handleResetContinueQuestionList() {
@to-login="handleToLoginPage" @to-login="handleToLoginPage"
@create-continue-questions="handleCreateContinueQuestions" @create-continue-questions="handleCreateContinueQuestions"
@reset-continue-question-list="handleResetContinueQuestionList" @reset-continue-question-list="handleResetContinueQuestionList"
@audio-play="handleAudioPlay"
/> />
</div> </div>
</div> </div>
......
...@@ -5,4 +5,7 @@ declare interface ConversationMessageItem { ...@@ -5,4 +5,7 @@ declare interface ConversationMessageItem {
isEmptyContent: boolean isEmptyContent: boolean
isTextContentLoading: boolean isTextContentLoading: boolean
isAnswerResponseLoading: boolean isAnswerResponseLoading: boolean
isVoiceLoading: boolean
isVoicePlaying: boolean
voiceFragmentUrlList: string[]
} }
...@@ -91,6 +91,17 @@ declare namespace I18n { ...@@ -91,6 +91,17 @@ declare namespace I18n {
bind: string bind: string
sms: string sms: string
verificationCode: string verificationCode: string
role: string
mandarin: string
cantonese: string
english: string
voice: string
sound: string
voice_auto_play: string
start_playing: string
stop_playing: string
response_error: string
agent_exception: string
dialogue_module: { dialogue_module: {
continue_question_message: string continue_question_message: string
...@@ -279,6 +290,9 @@ declare namespace I18n { ...@@ -279,6 +290,9 @@ 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_timbre_message: string
setting_timbre_desc: 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