feat: 应用支持语音样式优化

This commit is contained in:
wangdan-fit2cloud 2024-09-13 15:47:05 +08:00
parent 22d08252c8
commit df1fd3f89e
7 changed files with 397 additions and 313 deletions

View File

@ -5,6 +5,15 @@
</el-text> </el-text>
</div> </div>
<div> <div>
<!-- 语音播放 -->
<span v-if="tts">
<el-tooltip effect="dark" content="语音播放" placement="top">
<el-button text @click="playAnswerText(data?.answer_text)">
<AppIcon iconName="app-video-play"></AppIcon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
</span>
<el-tooltip effect="dark" content="复制" placement="top"> <el-tooltip effect="dark" content="复制" placement="top">
<el-button text @click="copyClick(data?.answer_text)"> <el-button text @click="copyClick(data?.answer_text)">
<AppIcon iconName="app-copy"></AppIcon> <AppIcon iconName="app-copy"></AppIcon>
@ -38,6 +47,8 @@
</el-button> </el-button>
<EditContentDialog ref="EditContentDialogRef" @refresh="refreshContent" /> <EditContentDialog ref="EditContentDialogRef" @refresh="refreshContent" />
<EditMarkDialog ref="EditMarkDialogRef" @refresh="refreshMark" /> <EditMarkDialog ref="EditMarkDialogRef" @refresh="refreshMark" />
<!-- 先渲染不然不能播放 -->
<audio ref="audioPlayer" controls hidden="hidden"></audio>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -46,6 +57,7 @@ import { copyClick } from '@/utils/clipboard'
import EditContentDialog from '@/views/log/component/EditContentDialog.vue' import EditContentDialog from '@/views/log/component/EditContentDialog.vue'
import EditMarkDialog from '@/views/log/component/EditMarkDialog.vue' import EditMarkDialog from '@/views/log/component/EditMarkDialog.vue'
import { datetimeFormat } from '@/utils/time' import { datetimeFormat } from '@/utils/time'
import applicationApi from '@/api/application'
const props = defineProps({ const props = defineProps({
data: { data: {
@ -56,15 +68,18 @@ const props = defineProps({
type: String, type: String,
default: '' default: ''
}, },
log: Boolean tts: Boolean
}) })
const emit = defineEmits(['update:data']) const emit = defineEmits(['update:data'])
const audioPlayer = ref<HTMLAudioElement | null>(null)
const EditContentDialogRef = ref() const EditContentDialogRef = ref()
const EditMarkDialogRef = ref() const EditMarkDialogRef = ref()
const buttonData = ref(props.data) const buttonData = ref(props.data)
const loading = ref(false)
function editContent(data: any) { function editContent(data: any) {
EditContentDialogRef.value.open(data) EditContentDialogRef.value.open(data)
@ -74,6 +89,44 @@ function editMark(data: any) {
EditMarkDialogRef.value.open(data) EditMarkDialogRef.value.open(data)
} }
const playAnswerText = (text: string) => {
if (props.data.tts_type === 'BROWSER') {
// SpeechSynthesisUtterance
const utterance = new SpeechSynthesisUtterance(text)
//
window.speechSynthesis.speak(utterance)
}
if (props.data.tts_type === 'TTS') {
applicationApi
.postTextToSpeech(props.data.id as string, { text: text }, loading)
.then((res: any) => {
// MP3
// Blob
const blob = new Blob([res], { type: 'audio/mp3' })
// URL
const url = URL.createObjectURL(blob)
// blob
// const link = document.createElement('a')
// link.href = window.URL.createObjectURL(blob)
// link.download = "abc.mp3"
// link.click()
// audioPlayer DOM
if (audioPlayer.value instanceof HTMLAudioElement) {
audioPlayer.value.src = url
audioPlayer.value.play() //
} else {
console.error('audioPlayer.value is not an instance of HTMLAudioElement')
}
})
.catch((err) => {
console.log('err: ', err)
})
}
}
function refreshMark() { function refreshMark() {
buttonData.value.improve_paragraph_id_list = [] buttonData.value.improve_paragraph_id_list = []
emit('update:data', buttonData.value) emit('update:data', buttonData.value)

View File

@ -5,9 +5,18 @@
</el-text> </el-text>
</div> </div>
<div> <div>
<!-- 语音播放 -->
<span v-if="tts">
<el-tooltip effect="dark" content="语音播放" placement="top">
<el-button text :disabled="!data?.write_ed" @click="playAnswerText(data?.answer_text)">
<AppIcon iconName="VideoPlay"></AppIcon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
</span>
<el-tooltip effect="dark" content="换个答案" placement="top"> <el-tooltip effect="dark" content="换个答案" placement="top">
<el-button :disabled="chat_loading" text @click="regeneration"> <el-button :disabled="chat_loading" text @click="regeneration">
<AppIcon iconName="VideoPlay"></AppIcon> <el-icon><RefreshRight /></el-icon>
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<el-divider direction="vertical" /> <el-divider direction="vertical" />
@ -59,6 +68,8 @@
</el-button> </el-button>
</el-tooltip> </el-tooltip>
</div> </div>
<!-- 先渲染不然不能播放 -->
<audio ref="audioPlayer" controls hidden="hidden"></audio>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
@ -81,11 +92,13 @@ const props = defineProps({
chat_loading: { chat_loading: {
type: Boolean type: Boolean
}, },
log: Boolean log: Boolean,
tts: Boolean
}) })
const emit = defineEmits(['update:data', 'regeneration']) const emit = defineEmits(['update:data', 'regeneration'])
const audioPlayer = ref<HTMLAudioElement | null>(null)
const buttonData = ref(props.data) const buttonData = ref(props.data)
const loading = ref(false) const loading = ref(false)
@ -101,5 +114,43 @@ function voteHandle(val: string) {
emit('update:data', buttonData.value) emit('update:data', buttonData.value)
}) })
} }
const playAnswerText = (text: string) => {
if (props.data.tts_type === 'BROWSER') {
// SpeechSynthesisUtterance
const utterance = new SpeechSynthesisUtterance(text)
//
window.speechSynthesis.speak(utterance)
}
if (props.data.tts_type === 'TTS') {
applicationApi
.postTextToSpeech(props.data.id as string, { text: text }, loading)
.then((res: any) => {
// MP3
// Blob
const blob = new Blob([res], { type: 'audio/mp3' })
// URL
const url = URL.createObjectURL(blob)
// blob
// const link = document.createElement('a')
// link.href = window.URL.createObjectURL(blob)
// link.download = "abc.mp3"
// link.click()
// audioPlayer DOM
if (audioPlayer.value instanceof HTMLAudioElement) {
audioPlayer.value.src = url
audioPlayer.value.play() //
} else {
console.error('audioPlayer.value is not an instance of HTMLAudioElement')
}
})
.catch((err) => {
console.log('err: ', err)
})
}
}
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -108,7 +108,11 @@
</div> </div>
</el-card> </el-card>
<div class="flex-between mt-8" v-if="log"> <div class="flex-between mt-8" v-if="log">
<LogOperationButton v-model:data="chatList[index]" :applicationId="appId" /> <LogOperationButton
v-model:data="chatList[index]"
:applicationId="appId"
:tts="props.data.tts_model_enable"
/>
</div> </div>
<div class="flex-between mt-8" v-else> <div class="flex-between mt-8" v-else>
@ -127,6 +131,7 @@
</div> </div>
<div v-if="item.write_ed && props.appId && 500 != item.status" class="flex-between"> <div v-if="item.write_ed && props.appId && 500 != item.status" class="flex-between">
<OperationButton <OperationButton
:tts="props.data.tts_model_enable"
:data="item" :data="item"
:applicationId="appId" :applicationId="appId"
:chatId="chartOpenId" :chatId="chartOpenId"
@ -134,14 +139,6 @@
@regeneration="regenerationChart(item)" @regeneration="regenerationChart(item)"
/> />
</div> </div>
<!-- 语音播放 -->
<div style="float: right" v-if="props.data.tts_model_enable">
<el-button :disabled="!item.write_ed" @click="playAnswerText(item.answer_text)">
<el-icon>
<VideoPlay />
</el-icon>
</el-button>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -159,19 +156,22 @@
:maxlength="100000" :maxlength="100000"
@keydown.enter="sendChatHandle($event)" @keydown.enter="sendChatHandle($event)"
/> />
<div class="operate" v-if="props.data.stt_model_enable">
<el-button v-if="mediaRecorderStatus" @click="startRecording"> <div class="operate flex align-center">
<el-icon> <span v-if="props.data.stt_model_enable">
<Microphone /> <el-button text v-if="mediaRecorderStatus" @click="startRecording">
</el-icon> <el-icon>
</el-button> <Microphone />
<el-button v-else @click="stopRecording"> </el-icon>
<el-icon> </el-button>
<VideoPause /> <el-button text v-else @click="stopRecording">
</el-icon> <el-icon>
</el-button> <VideoPause />
</div> </el-icon>
<div class="operate"> </el-button>
<el-divider direction="vertical" />
</span>
<el-button <el-button
text text
class="sent-button" class="sent-button"
@ -180,17 +180,10 @@
> >
<img v-show="isDisabledChart || loading" src="@/assets/icon_send.svg" alt="" /> <img v-show="isDisabledChart || loading" src="@/assets/icon_send.svg" alt="" />
<SendIcon v-show="!isDisabledChart && !loading" /> <SendIcon v-show="!isDisabledChart && !loading" />
<!-- <img
src="@/assets/icon_send_colorful.svg"
alt=""
/> -->
</el-button> </el-button>
</div> </div>
</div> </div>
</div> </div>
<!-- 先渲染不然不能播放 -->
<audio ref="audioPlayer" controls hidden="hidden"></audio>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -318,50 +311,54 @@ function handleInputFieldList() {
?.filter((v: any) => v.id === 'base-node') ?.filter((v: any) => v.id === 'base-node')
.map((v: any) => { .map((v: any) => {
inputFieldList.value = v.properties.input_field_list inputFieldList.value = v.properties.input_field_list
? v.properties.input_field_list.filter((v: any) => v.assignment_method === 'user_input').map((v: any) => { ? v.properties.input_field_list
switch (v.type) { .filter((v: any) => v.assignment_method === 'user_input')
case 'input': .map((v: any) => {
return { switch (v.type) {
field: v.variable, case 'input':
input_type: 'TextInput', return {
label: v.name, field: v.variable,
required: v.is_required input_type: 'TextInput',
} label: v.name,
case 'select': required: v.is_required
return {
field: v.variable,
input_type: 'SingleSelect',
label: v.name,
required: v.is_required,
option_list: v.optionList.map((o: any) => {
return { key: o, value: o }
})
}
case 'date':
return {
field: v.variable,
input_type: 'DatePicker',
label: v.name,
required: v.is_required,
attrs: {
format: 'YYYY-MM-DD HH:mm:ss',
'value-format': 'YYYY-MM-DD HH:mm:ss',
type: 'datetime'
} }
} case 'select':
default: return {
break field: v.variable,
} input_type: 'SingleSelect',
}) label: v.name,
required: v.is_required,
option_list: v.optionList.map((o: any) => {
return { key: o, value: o }
})
}
case 'date':
return {
field: v.variable,
input_type: 'DatePicker',
label: v.name,
required: v.is_required,
attrs: {
format: 'YYYY-MM-DD HH:mm:ss',
'value-format': 'YYYY-MM-DD HH:mm:ss',
type: 'datetime'
}
}
default:
break
}
})
: [] : []
apiInputFieldList.value = v.properties.input_field_list apiInputFieldList.value = v.properties.input_field_list
? v.properties.input_field_list.filter((v: any) => v.assignment_method === 'api_input').map((v: any) => { ? v.properties.input_field_list
return { .filter((v: any) => v.assignment_method === 'api_input')
field: v.variable, .map((v: any) => {
label: v.name, return {
required: v.is_required field: v.variable,
} label: v.name,
}) required: v.is_required
}
})
: [] : []
}) })
} }
@ -720,7 +717,7 @@ const handleScroll = () => {
// //
const mediaRecorder = ref<any>(null) const mediaRecorder = ref<any>(null)
const audioPlayer = ref<HTMLAudioElement | null>(null)
const mediaRecorderStatus = ref(true) const mediaRecorderStatus = ref(true)
// //
@ -782,43 +779,6 @@ const uploadRecording = async (audioBlob: Blob) => {
} }
} }
const playAnswerText = (text: string) => {
if (props.data.tts_type === 'BROWSER') {
// SpeechSynthesisUtterance
const utterance = new SpeechSynthesisUtterance(text)
//
window.speechSynthesis.speak(utterance)
}
if (props.data.tts_type === 'TTS') {
applicationApi.postTextToSpeech(props.data.id as string, { 'text': text }, loading)
.then((res: any) => {
// MP3
// Blob
const blob = new Blob([res], { type: 'audio/mp3' })
// URL
const url = URL.createObjectURL(blob)
// blob
// const link = document.createElement('a')
// link.href = window.URL.createObjectURL(blob)
// link.download = "abc.mp3"
// link.click()
// audioPlayer DOM
if (audioPlayer.value instanceof HTMLAudioElement) {
audioPlayer.value.src = url
audioPlayer.value.play() //
} else {
console.error('audioPlayer.value is not an instance of HTMLAudioElement')
}
})
.catch((err) => {
console.log('err: ', err)
})
}
}
onMounted(() => { onMounted(() => {
handleInputFieldList() handleInputFieldList()
}) })
@ -946,10 +906,12 @@ defineExpose({
.operate { .operate {
padding: 6px 10px; padding: 6px 10px;
.el-icon {
font-size: 20px;
}
.sent-button { .sent-button {
max-height: none; max-height: none;
.el-icon { .el-icon {
font-size: 24px; font-size: 24px;
} }

View File

@ -1147,5 +1147,26 @@ export const iconMap: any = {
) )
]) ])
} }
},
'app-video-play': {
iconReader: () => {
return h('i', [
h(
'svg',
{
style: { height: '100%', width: '100%' },
viewBox: '0 0 16 16',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M7.66667 3.68233V12.3177L4.66667 10.01V5.99L7.66667 3.68233ZM7.89333 2C7.74633 2 7.60333 2.04867 7.487 2.13833L3.59367 5.13333C3.42933 5.25933 3.33333 5.45467 3.33333 5.66167V10.3383C3.33333 10.5453 3.42933 10.7407 3.59367 10.8667L7.487 13.8617C7.60333 13.9513 7.74633 14 7.89333 14H8.33333C8.70167 14 9 13.7017 9 13.3333V2.66667C9 2.29833 8.70167 2 8.33333 2H7.89333ZM1 5.66667C1 5.48267 1.14933 5.33333 1.33333 5.33333H2C2.184 5.33333 2.33333 5.48267 2.33333 5.66667V10.3333C2.33333 10.5173 2.184 10.6667 2 10.6667H1.33333C1.14933 10.6667 1 10.5173 1 10.3333V5.66667ZM13.2973 12.1873C13.1727 12.3153 12.968 12.3107 12.8417 12.184L12.3723 11.715C12.2373 11.58 12.244 11.36 12.3757 11.222C13.1757 10.3843 13.6667 9.24967 13.6667 8C13.6667 6.75467 13.179 5.62333 12.384 4.78667C12.253 4.64833 12.2467 4.42933 12.3813 4.29467L12.8507 3.82533C12.9773 3.69867 13.182 3.694 13.307 3.82267C14.355 4.903 15 6.376 15 8C15 9.62867 14.351 11.106 13.2973 12.1873ZM11.4043 10.3087C11.2833 10.4347 11.084 10.4263 10.9603 10.303L10.4987 9.84099C10.3573 9.69966 10.3737 9.46733 10.5053 9.31666C10.8133 8.96499 11 8.50433 11 7.99999C11 7.49966 10.816 7.04199 10.5123 6.69133C10.382 6.54066 10.3663 6.30933 10.507 6.16866L10.9693 5.70666C11.0933 5.58266 11.2933 5.57466 11.4143 5.70166C11.9837 6.29966 12.3333 7.10899 12.3333 7.99999C12.3333 8.89599 11.9797 9.70966 11.4043 10.3087Z',
fill: 'currentColor'
})
]
)
])
}
} }
} }

View File

@ -289,17 +289,19 @@
<el-switch size="small" v-model="applicationForm.problem_optimization"></el-switch> <el-switch size="small" v-model="applicationForm.problem_optimization"></el-switch>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<template #label> <template #label>
<div class="flex align-center"> <div class="flex-between">
<span class="mr-4">语音输入</span> <div class="flex align-center">
<el-tooltip <span class="mr-4">语音输入</span>
effect="dark" <el-tooltip
content="开启后,需要设定语音转文本模型,语音输入完成后会转化为文字直接发送提问" effect="dark"
placement="right" content="开启后,需要设定语音转文本模型,语音输入完成后会转化为文字直接发送提问"
> placement="right"
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon> >
</el-tooltip> <AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
<el-switch v-model="applicationForm.stt_model_enable"/> </el-tooltip>
</div>
<el-switch size="small" v-model="applicationForm.stt_model_enable" />
</div> </div>
</template> </template>
<el-select <el-select
@ -363,15 +365,15 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<template #label> <template #label>
<div class="flex align-center"> <div class="flex-between">
<span class="mr-4">语音播放</span> <span class="mr-4">语音播放</span>
<el-switch v-model="applicationForm.tts_model_enable"/> <el-switch size="small" v-model="applicationForm.tts_model_enable" />
</div> </div>
</template> </template>
<el-radio-group v-model="applicationForm.tts_type"> <el-radio-group v-model="applicationForm.tts_type">
<el-radio label="浏览器播放(免费)" value="BROWSER"/> <el-radio label="BROWSER">浏览器播放(免费)</el-radio>
<el-radio label="TTS模型" value="TTS"/> <el-radio label="TTS">TTS模型</el-radio>
</el-radio-group> </el-radio-group>
<el-select <el-select
v-if="applicationForm.tts_type === 'TTS'" v-if="applicationForm.tts_type === 'TTS'"

View File

@ -34,9 +34,9 @@
</el-form-item> </el-form-item>
<el-form-item label="输入类型"> <el-form-item label="输入类型">
<el-select v-model="form.type"> <el-select v-model="form.type">
<el-option label="文本框" value="input"/> <el-option label="文本框" value="input" />
<el-option label="日期" value="date"/> <el-option label="日期" value="date" />
<el-option label="下拉选项" value="select"/> <el-option label="下拉选项" value="select" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item v-if="form.type === 'select'"> <el-form-item v-if="form.type === 'select'">
@ -49,10 +49,18 @@
</div> </div>
</template> </template>
<template #default> <template #default>
<div class="w-full flex-between" :key="option" v-for="(option, $index) in form.optionList"> <div
<input class="el-textarea__inner" v-model.lazy="form.optionList[$index]" placeholder="请输入选项值"/> class="w-full flex-between"
<el-button link type="primary" @click="delOption($index)"> :key="option"
<el-icon class="mr-4"><Remove /></el-icon> v-for="(option, $index) in form.optionList"
>
<input
class="el-textarea__inner"
v-model.lazy="form.optionList[$index]"
placeholder="请输入选项值"
/>
<el-button link class="ml-8" @click="delOption($index)">
<el-icon><Delete /></el-icon>
</el-button> </el-button>
</div> </div>
</template> </template>
@ -62,11 +70,10 @@
</el-form-item> </el-form-item>
<el-form-item label="赋值方式"> <el-form-item label="赋值方式">
<el-radio-group v-model="form.assignment_method"> <el-radio-group v-model="form.assignment_method">
<el-radio label="user_input">用户输入</el-radio> <el-radio value="user_input">用户输入</el-radio>
<el-radio label="api_input">接口传参</el-radio> <el-radio value="api_input">接口传参</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
@ -153,7 +160,6 @@ const delOption = (index: number) => {
form.value.optionList.splice(index, 1) form.value.optionList.splice(index, 1)
} }
defineExpose({ open, close }) defineExpose({ open, close })
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -50,164 +50,16 @@
<el-button text type="info" @click="openDialog"> <el-button text type="info" @click="openDialog">
<AppIcon iconName="app-magnify" style="font-size: 16px"></AppIcon> <AppIcon iconName="app-magnify" style="font-size: 16px"></AppIcon>
</el-button> </el-button>
</template </template>
>
</MdEditor> </MdEditor>
</el-form-item> </el-form-item>
<el-form-item> <div class="flex-between mb-8">
<template #label> <h5 class="lighter">输入变量</h5>
<div class="flex align-center"> <el-button link type="primary" @click="openAddDialog()">
<span class="mr-4">语音输入</span> <el-icon class="mr-4"><Plus /></el-icon>
<el-tooltip </el-button>
effect="dark" </div>
content="开启后,需要设定语音转文本模型,语音输入完成后会转化为文字直接发送提问" <el-table :data="props.nodeModel.properties.input_field_list" class="mb-16">
placement="right"
>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
<el-switch v-model="form_data.stt_model_enable" />
</div>
</template>
<el-select
v-model="form_data.stt_model_id"
class="w-full"
popper-class="select-model"
>
<el-option-group
v-for="(value, label) in sttModelOptions"
:key="value"
:label="relatedObject(providerOptions, label, 'provider')?.name"
>
<el-option
v-for="item in value.filter((v: any) => v.status === 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
>
<div class="flex align-center">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<el-tag
v-if="item.permission_type === 'PUBLIC'"
type="info"
class="info-tag ml-8"
>公用
</el-tag>
</div>
<el-icon class="check-icon" v-if="item.id === form_data.stt_model_id">
<Check />
</el-icon>
</el-option>
<!-- 不可用 -->
<el-option
v-for="item in value.filter((v: any) => v.status !== 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
disabled
>
<div class="flex">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<span class="danger">{{
$t('views.application.applicationForm.form.aiModel.unavailable')
}}</span>
</div>
<el-icon class="check-icon" v-if="item.id === form_data.stt_model_id">
<Check />
</el-icon>
</el-option>
</el-option-group>
</el-select>
</el-form-item>
<el-form-item>
<template #label>
<div class="flex align-center">
<span class="mr-4">语音播放</span>
<el-switch v-model="form_data.tts_model_enable" />
</div>
</template>
<el-radio-group v-model="form_data.tts_type">
<el-radio label="浏览器播放(免费)" value="BROWSER"/>
<el-radio label="TTS模型" value="TTS"/>
</el-radio-group>
<el-select
v-if="form_data.tts_type === 'TTS'"
v-model="form_data.tts_model_id"
class="w-full"
popper-class="select-model"
>
<el-option-group
v-for="(value, label) in ttsModelOptions"
:key="value"
:label="relatedObject(providerOptions, label, 'provider')?.name"
>
<el-option
v-for="item in value.filter((v: any) => v.status === 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
>
<div class="flex align-center">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<el-tag
v-if="item.permission_type === 'PUBLIC'"
type="info"
class="info-tag ml-8"
>公用
</el-tag>
</div>
<el-icon class="check-icon" v-if="item.id === form_data.tts_model_id">
<Check />
</el-icon>
</el-option>
<!-- 不可用 -->
<el-option
v-for="item in value.filter((v: any) => v.status !== 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
disabled
>
<div class="flex">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<span class="danger">{{
$t('views.application.applicationForm.form.aiModel.unavailable')
}}</span>
</div>
<el-icon class="check-icon" v-if="item.id === form_data.tts_model_id">
<Check />
</el-icon>
</el-option>
</el-option-group>
</el-select>
</el-form-item>
</el-form>
<div class="flex-between">
全局变量
<el-button link type="primary" @click="openAddDialog()">
<el-icon class="mr-4"><Plus /></el-icon>
</el-button>
</div>
<el-table :data="props.nodeModel.properties.input_field_list" class="mb-16">
<el-table-column prop="name" label="变量名" /> <el-table-column prop="name" label="变量名" />
<el-table-column prop="variable" label="变量" /> <el-table-column prop="variable" label="变量" />
<el-table-column label="输入类型"> <el-table-column label="输入类型">
@ -248,6 +100,152 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-form-item>
<template #label>
<div class="flex-between">
<div class="flex align-center">
<span class="mr-4">语音输入</span>
<el-tooltip
effect="dark"
content="开启后,需要设定语音转文本模型,语音输入完成后会转化为文字直接发送提问"
placement="right"
>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
<el-switch size="small" v-model="form_data.stt_model_enable" />
</div>
</template>
<el-select
v-model="form_data.stt_model_id"
class="w-full"
popper-class="select-model"
placeholder="请输入"
>
<el-option-group
v-for="(value, label) in sttModelOptions"
:key="value"
:label="relatedObject(providerOptions, label, 'provider')?.name"
>
<el-option
v-for="item in value.filter((v: any) => v.status === 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
>
<div class="flex align-center">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<el-tag v-if="item.permission_type === 'PUBLIC'" type="info" class="info-tag ml-8"
>公用
</el-tag>
</div>
<el-icon class="check-icon" v-if="item.id === form_data.stt_model_id">
<Check />
</el-icon>
</el-option>
<!-- 不可用 -->
<el-option
v-for="item in value.filter((v: any) => v.status !== 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
disabled
>
<div class="flex">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<span class="danger">{{
$t('views.application.applicationForm.form.aiModel.unavailable')
}}</span>
</div>
<el-icon class="check-icon" v-if="item.id === form_data.stt_model_id">
<Check />
</el-icon>
</el-option>
</el-option-group>
</el-select>
</el-form-item>
<el-form-item>
<template #label>
<div class="flex-between">
<span class="mr-4">语音播放</span>
<el-switch size="small" v-model="form_data.tts_model_enable" />
</div>
</template>
<el-radio-group v-model="form_data.tts_type">
<el-radio value="BROWSER">浏览器播放(免费)</el-radio>
<el-radio value="TTS">TTS模型</el-radio>
</el-radio-group>
<el-select
v-if="form_data.tts_type === 'TTS'"
v-model="form_data.tts_model_id"
class="w-full"
popper-class="select-model"
placeholder="请输入"
>
<el-option-group
v-for="(value, label) in ttsModelOptions"
:key="value"
:label="relatedObject(providerOptions, label, 'provider')?.name"
>
<el-option
v-for="item in value.filter((v: any) => v.status === 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
>
<div class="flex align-center">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<el-tag v-if="item.permission_type === 'PUBLIC'" type="info" class="info-tag ml-8"
>公用
</el-tag>
</div>
<el-icon class="check-icon" v-if="item.id === form_data.tts_model_id">
<Check />
</el-icon>
</el-option>
<!-- 不可用 -->
<el-option
v-for="item in value.filter((v: any) => v.status !== 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
disabled
>
<div class="flex">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<span class="danger">{{
$t('views.application.applicationForm.form.aiModel.unavailable')
}}</span>
</div>
<el-icon class="check-icon" v-if="item.id === form_data.tts_model_id">
<Check />
</el-icon>
</el-option>
</el-option-group>
</el-select>
</el-form-item>
</el-form>
<!-- 回复内容弹出层 --> <!-- 回复内容弹出层 -->
<el-dialog v-model="dialogVisible" title="开场白" append-to-body> <el-dialog v-model="dialogVisible" title="开场白" append-to-body>
<MdEditor v-model="cloneContent" :preview="false" :toolbars="[]" :footers="[]"></MdEditor> <MdEditor v-model="cloneContent" :preview="false" :toolbars="[]" :footers="[]"></MdEditor>
@ -335,28 +333,21 @@ const validate = () => {
} }
function getProvider() { function getProvider() {
model model.asyncGetProvider().then((res: any) => {
.asyncGetProvider() providerOptions.value = res?.data
.then((res: any) => { })
providerOptions.value = res?.data
})
} }
function getSTTModel() { function getSTTModel() {
applicationApi applicationApi.getApplicationSTTModel(id).then((res: any) => {
.getApplicationSTTModel(id) sttModelOptions.value = groupBy(res?.data, 'provider')
.then((res: any) => { })
sttModelOptions.value = groupBy(res?.data, 'provider')
})
} }
function getTTSModel() { function getTTSModel() {
applicationApi applicationApi.getApplicationTTSModel(id).then((res: any) => {
.getApplicationTTSModel(id) ttsModelOptions.value = groupBy(res?.data, 'provider')
.then((res: any) => { })
ttsModelOptions.value = groupBy(res?.data, 'provider')
})
} }
const currentIndex = ref(null) const currentIndex = ref(null)
@ -391,7 +382,6 @@ function refreshFieldList(data: any) {
FieldFormDialogRef.value.close() FieldFormDialogRef.value.close()
} }
onMounted(() => { onMounted(() => {
set(props.nodeModel, 'validate', validate) set(props.nodeModel, 'validate', validate)
if (props.nodeModel.properties.input_field_list) { if (props.nodeModel.properties.input_field_list) {
@ -403,7 +393,6 @@ onMounted(() => {
getProvider() getProvider()
getTTSModel() getTTSModel()
getSTTModel() getSTTModel()
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>