mirror of
https://github.com/OpenBMB/MiniCPM-V.git
synced 2026-02-05 18:29:18 +08:00
Update to MiniCPM-o 2.6
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div class="call-header"></div>
|
||||
</template>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="time">
|
||||
<div class="time-minute">{{ minute || '00' }}</div>
|
||||
<div class="time-colon">:</div>
|
||||
<div class="time-second">{{ second || '00' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { limitTime, tipsRemainingTime } from '@/enums';
|
||||
|
||||
const start = defineModel();
|
||||
|
||||
const emits = defineEmits(['timeUp']);
|
||||
|
||||
const remainingTime = ref();
|
||||
const minute = ref();
|
||||
const second = ref();
|
||||
const timeInterval = ref(null);
|
||||
|
||||
const startCount = () => {
|
||||
remainingTime.value = limitTime;
|
||||
updateCountDown();
|
||||
timeInterval.value = setInterval(() => {
|
||||
updateCountDown();
|
||||
}, 1000);
|
||||
};
|
||||
const updateCountDown = () => {
|
||||
let minutes = Math.floor(remainingTime.value / 60);
|
||||
let seconds = remainingTime.value % 60;
|
||||
|
||||
// 格式化分钟和秒,确保它们是两位数
|
||||
minute.value = minutes < 10 ? '0' + minutes : minutes;
|
||||
second.value = seconds < 10 ? '0' + seconds : seconds;
|
||||
|
||||
// 剩余1分钟提示用户
|
||||
if (remainingTime.value === tipsRemainingTime) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: `This call will disconnect in ${tipsRemainingTime} seconds.`,
|
||||
duration: 3000,
|
||||
customClass: 'time-warning'
|
||||
});
|
||||
}
|
||||
// 防止倒计时变成负数
|
||||
if (remainingTime.value > 0) {
|
||||
remainingTime.value--;
|
||||
} else {
|
||||
clearInterval(timeInterval);
|
||||
emits('timeUp');
|
||||
}
|
||||
};
|
||||
watch(
|
||||
() => start.value,
|
||||
newVal => {
|
||||
timeInterval.value && clearInterval(timeInterval.value);
|
||||
if (newVal) {
|
||||
startCount();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.time-minute,
|
||||
.time-second {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 3.848px;
|
||||
background: rgba(47, 47, 47, 0.5);
|
||||
}
|
||||
.time-colon {
|
||||
margin: 0 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="delay-tips">
|
||||
<span>当前发生延迟,目前延迟{{ delayTimestamp }}ms,积压{{ delayCount * 200 }}ms未发</span>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
defineProps({
|
||||
delayTimestamp: {
|
||||
type: Number,
|
||||
defalult: 0
|
||||
},
|
||||
delayCount: {
|
||||
type: Number,
|
||||
defalult: 0
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.delay-tips {
|
||||
font-size: 12px;
|
||||
color: #dc3545;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="extra-info">
|
||||
<div class="model-version" v-if="modelVersion">模型版本: {{ modelVersion }}</div>
|
||||
<div class="web-version">前端版本: {{ webVersion }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
modelVersion: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
webVersion: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.extra-info {
|
||||
position: fixed;
|
||||
top: 62px;
|
||||
left: 4vw;
|
||||
display: flex;
|
||||
.model-version,
|
||||
.web-version {
|
||||
font-size: 12px;
|
||||
color: red;
|
||||
}
|
||||
.model-version {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="ideas">
|
||||
<div class="ideas-title">
|
||||
<img src="@/assets/images/ideas-icon.png " />
|
||||
<span>Convsersation ideas</span>
|
||||
</div>
|
||||
<div class="ideas-content">
|
||||
<div class="ideas-content-item" v-for="(item, index) in ideasList" :key="index">{{ item }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
ideasList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ideas {
|
||||
margin-top: 16px;
|
||||
box-shadow: 0 0 0 0.5px #e0e0e0;
|
||||
border-radius: 12px;
|
||||
padding: 18px 28px;
|
||||
&-title {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
span {
|
||||
color: #171717;
|
||||
font-family: PingFang SC;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
&-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
background: #eaefff;
|
||||
padding: 10px 24px;
|
||||
color: #7579eb;
|
||||
font-family: PingFang SC;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="like-box">
|
||||
<div class="like-btn" @click="selectFeedbackStatus('like')">
|
||||
<img v-if="feedbackStatus === '' || feedbackStatus === 'dislike'" src="@/assets/images/zan.png" />
|
||||
<img v-else src="@/assets/images/zan-active.png" />
|
||||
</div>
|
||||
<div class="dislike-btn" @click="selectFeedbackStatus('dislike')">
|
||||
<img v-if="feedbackStatus === '' || feedbackStatus === 'like'" src="@/assets/images/cai.png" />
|
||||
<img v-else src="@/assets/images/cai-active.png" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="t('feedbackDialogTitle')"
|
||||
width="400"
|
||||
:align-center="true"
|
||||
@close="cancelFeedback"
|
||||
>
|
||||
<el-input type="textarea" :rows="4" v-model="comment" />
|
||||
<div class="operate-btn">
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitFeedback">确定</el-button>
|
||||
<el-button @click="cancelFeedback">取消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { feedback } from '@/apis';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const feedbackStatus = defineModel('feedbackStatus');
|
||||
const curResponseId = defineModel('curResponseId');
|
||||
const dialogVisible = ref(false);
|
||||
const comment = ref('');
|
||||
const submitLoading = ref(false);
|
||||
const selectFeedbackStatus = val => {
|
||||
if (!curResponseId.value) {
|
||||
return;
|
||||
}
|
||||
feedbackStatus.value = val;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
// 提交反馈
|
||||
const submitFeedback = async () => {
|
||||
submitLoading.value = true;
|
||||
const { code, message } = await feedback({
|
||||
response_id: curResponseId.value,
|
||||
rating: feedbackStatus.value,
|
||||
comment: comment.value
|
||||
});
|
||||
submitLoading.value = false;
|
||||
if (code !== 0) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: message,
|
||||
duration: 3000,
|
||||
customClass: 'system-error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
ElMessage.success('反馈成功');
|
||||
dialogVisible.value = false;
|
||||
setTimeout(() => {
|
||||
feedbackStatus.value = '';
|
||||
}, 2000);
|
||||
};
|
||||
const cancelFeedback = () => {
|
||||
dialogVisible.value = false;
|
||||
feedbackStatus.value = '';
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.like-box {
|
||||
display: flex;
|
||||
margin: 0 16px;
|
||||
.like-btn,
|
||||
.dislike-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
background: #f3f3f3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: #d1d1d1;
|
||||
}
|
||||
img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
.dislike-btn {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
.operate-btn {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
.el-button--primary {
|
||||
background: #647fff;
|
||||
border-color: #647fff;
|
||||
&:hover {
|
||||
border-color: #647fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,404 @@
|
||||
<template>
|
||||
<div class="user-config">
|
||||
<div class="user-config-title">模型配置</div>
|
||||
<div class="config-item">
|
||||
<div class="config-item-label">语音打断:</div>
|
||||
<div class="config-item-content">
|
||||
<el-switch
|
||||
v-model="configData.canStopByVoice"
|
||||
inline-prompt
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
size="small"
|
||||
:disabled="isCalling"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<div class="config-item-label">视频画质:</div>
|
||||
<div class="config-item-content">
|
||||
<el-radio-group v-model="configData.videoQuality" :disabled="isCalling">
|
||||
<el-radio :value="true">高清</el-radio>
|
||||
<el-radio :value="false">低清</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<div class="config-item-label">VAD阈值:</div>
|
||||
<div class="config-item-content vad-slider">
|
||||
<el-slider
|
||||
v-model="configData.vadThreshold"
|
||||
:min="0.5"
|
||||
:max="1"
|
||||
:step="0.1"
|
||||
size="small"
|
||||
:disabled="isCalling"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="timbre-model">
|
||||
<div class="timbre-model-label">音色人物:</div>
|
||||
<div class="timbre-model-content">
|
||||
<el-select
|
||||
v-model="configData.timbreId"
|
||||
style="width: 100%"
|
||||
@change="handleChangePeople"
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option v-for="item in peopleList" :key="item.id" :value="item.id" :label="item.name">
|
||||
{{ item.name }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="prompt-item">
|
||||
<div class="prompt-item-label">Assistant_prompt:</div>
|
||||
<div class="prompt-item-content">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
v-model="configData.assistantPrompt"
|
||||
resize="none"
|
||||
:disabled="isCalling"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<div class="config-item-label">使用语音prompt:</div>
|
||||
<div class="config-item-content">
|
||||
<el-switch
|
||||
v-model="configData.useAudioPrompt"
|
||||
inline-prompt
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
size="small"
|
||||
:disabled="isCalling"
|
||||
@change="handleSelectUseAudioPrompt"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="voice-prompt-box">
|
||||
<div class="prompt-item" v-if="configData.useAudioPrompt">
|
||||
<div class="prompt-item-label">Voice_clone_prompt:</div>
|
||||
<div class="prompt-item-content">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
v-model="configData.voiceClonePrompt"
|
||||
resize="none"
|
||||
:disabled="isCalling"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timbre-config" v-if="configData.useAudioPrompt">
|
||||
<div class="timbre-config-label">音色选择:</div>
|
||||
<div class="timbre-config-content">
|
||||
<el-checkbox-group v-model="configData.timbre" @change="handleSelectTimbre" :disabled="isCalling">
|
||||
<el-checkbox :value="1" label="Default Audio"></el-checkbox>
|
||||
<el-upload
|
||||
v-model:file-list="fileList"
|
||||
action=""
|
||||
:multiple="false"
|
||||
:on-change="handleChangeFile"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:disabled="isCalling"
|
||||
accept="audio/*"
|
||||
>
|
||||
<el-checkbox :value="2">
|
||||
<!-- <span>Customization: Upload Audio</span> -->
|
||||
<span>Customization</span>
|
||||
<SvgIcon name="upload" className="checkbox-icon" />
|
||||
</el-checkbox>
|
||||
</el-upload>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-content" v-if="fileName">
|
||||
<SvgIcon name="document" class="document-icon" />
|
||||
<span class="file-name">{{ fileName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const isCalling = defineModel('isCalling');
|
||||
const type = defineModel('type');
|
||||
|
||||
let defaultVoiceClonePrompt =
|
||||
'你是一个AI助手。你能接受视频,音频和文本输入并输出语音和文本。模仿输入音频中的声音特征。';
|
||||
let defaultAssistantPrompt = '作为助手,你将使用这种声音风格说话。';
|
||||
|
||||
const fileList = ref([]);
|
||||
const fileName = ref('');
|
||||
|
||||
const configData = ref({
|
||||
canStopByVoice: false,
|
||||
videoQuality: false,
|
||||
useAudioPrompt: true,
|
||||
vadThreshold: 0.8,
|
||||
voiceClonePrompt: defaultVoiceClonePrompt,
|
||||
assistantPrompt: defaultAssistantPrompt,
|
||||
timbre: [1],
|
||||
audioFormat: 'mp3',
|
||||
base64Str: '',
|
||||
timbreId: ''
|
||||
});
|
||||
|
||||
const peopleList = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Trump',
|
||||
voiceClonePrompt: '',
|
||||
assistantPrompt: ''
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '说相声',
|
||||
voiceClonePrompt: '克隆音频提示中的音色以生成语音',
|
||||
assistantPrompt: '请角色扮演这段音频,请以相声演员的口吻说话'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '默认',
|
||||
voiceClonePrompt: defaultVoiceClonePrompt,
|
||||
assistantPrompt: defaultAssistantPrompt
|
||||
}
|
||||
];
|
||||
watch(
|
||||
() => type.value,
|
||||
val => {
|
||||
if (val === 'video') {
|
||||
console.log('val: ', val);
|
||||
defaultVoiceClonePrompt =
|
||||
'你是一个AI助手。你能接受视频,音频和文本输入并输出语音和文本。模仿输入音频中的声音特征。';
|
||||
defaultAssistantPrompt = '作为助手,你将使用这种声音风格说话。';
|
||||
} else {
|
||||
defaultVoiceClonePrompt = '克隆音频提示中的音色以生成语音。';
|
||||
defaultAssistantPrompt = 'Your task is to be a helpful assistant using this voice pattern.';
|
||||
}
|
||||
configData.value.voiceClonePrompt = defaultVoiceClonePrompt;
|
||||
configData.value.assistantPrompt = defaultAssistantPrompt;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
onMounted(() => {
|
||||
handleSetStorage();
|
||||
});
|
||||
const handleSelectTimbre = e => {
|
||||
if (e.length > 1) {
|
||||
const val = e[e.length - 1];
|
||||
configData.value.timbre = [val];
|
||||
// 默认音色
|
||||
if (val === 1) {
|
||||
configData.value.audioFormat = 'mp3';
|
||||
configData.value.base64Str = '';
|
||||
fileList.value = [];
|
||||
fileName.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleChangeFile = file => {
|
||||
if (isAudio(file) && sizeNotExceed(file)) {
|
||||
fileList.value = [file];
|
||||
fileName.value = file.name;
|
||||
configData.value.timbre = [2];
|
||||
handleUpload();
|
||||
} else {
|
||||
ElMessage.error('Please upload audio file and size not exceed 10MB');
|
||||
}
|
||||
};
|
||||
const isAudio = file => {
|
||||
return file.raw.type.includes('audio');
|
||||
};
|
||||
const sizeNotExceed = file => {
|
||||
return file.size / 1024 / 1024 <= 10;
|
||||
};
|
||||
const handleUpload = async () => {
|
||||
const file = fileList.value[0].raw;
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const base64String = e.target.result.split(',')[1];
|
||||
configData.value.audioFormat = file.name.split('.')[1];
|
||||
configData.value.base64Str = base64String;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
const handleSelectUseAudioPrompt = val => {
|
||||
if (val) {
|
||||
configData.value.voiceClonePrompt = defaultVoiceClonePrompt;
|
||||
configData.value.assistantPrompt = defaultAssistantPrompt;
|
||||
}
|
||||
};
|
||||
// 配置发生变化,更新到localstorage中
|
||||
watch(configData.value, () => {
|
||||
handleSetStorage();
|
||||
});
|
||||
const handleSetStorage = () => {
|
||||
const { timbre, canStopByVoice, ...others } = configData.value;
|
||||
const defaultConfigData = {
|
||||
canStopByVoice,
|
||||
...others
|
||||
};
|
||||
localStorage.setItem('configData', JSON.stringify(defaultConfigData));
|
||||
localStorage.setItem('canStopByVoice', canStopByVoice);
|
||||
};
|
||||
const handleChangePeople = val => {
|
||||
console.log('val: ', val);
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
const index = peopleList.findIndex(item => item.id === val);
|
||||
configData.value.voiceClonePrompt = peopleList[index].voiceClonePrompt;
|
||||
configData.value.assistantPrompt = peopleList[index].assistantPrompt;
|
||||
configData.value.timbre = [1];
|
||||
};
|
||||
</script>
|
||||
<style lang="less">
|
||||
.user-config {
|
||||
&-title {
|
||||
height: 61px;
|
||||
padding: 18px 18px 0;
|
||||
color: rgba(23, 23, 23, 0.9);
|
||||
font-family: PingFang SC;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
}
|
||||
.config-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 0 0 18px;
|
||||
margin-bottom: 12px;
|
||||
&-label {
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
&-content {
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
.el-radio-group {
|
||||
.el-radio {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-content.vad-slider {
|
||||
width: 80%;
|
||||
padding-left: 7px;
|
||||
margin-right: 20px;
|
||||
.el-slider__button {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.timbre-config {
|
||||
padding: 0 0 0 18px;
|
||||
&-label {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
&-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.el-checkbox-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
> .el-checkbox {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
.el-checkbox {
|
||||
padding: 8px 16px;
|
||||
border-radius: 10px;
|
||||
background: #eaefff;
|
||||
margin-bottom: 12px;
|
||||
height: 40px;
|
||||
.el-checkbox__input {
|
||||
.el-checkbox__inner {
|
||||
border: 1px solid #4dc100;
|
||||
}
|
||||
}
|
||||
.el-checkbox__input.is-checked {
|
||||
.el-checkbox__inner {
|
||||
background: #4dc100;
|
||||
}
|
||||
}
|
||||
.el-checkbox__input.is-checked.is-disabled {
|
||||
.el-checkbox__inner::after {
|
||||
border-color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-checkbox__label {
|
||||
color: #7579eb !important;
|
||||
font-family: PingFang SC;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.checkbox-icon {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
.el-checkbox + .el-checkbox {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.prompt-item {
|
||||
// padding: 0 0 0 18px;
|
||||
margin-bottom: 12px;
|
||||
&-label {
|
||||
// margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
.file-content {
|
||||
padding: 0 0 0 18px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.document-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.file-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
.timbre-model {
|
||||
padding: 0 0 0 18px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&-label {
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
&-content {
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
.voice-prompt-box {
|
||||
border: 1px solid #eaefff;
|
||||
margin-left: 18px;
|
||||
padding: 12px;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,456 @@
|
||||
<template>
|
||||
<div :class="`user-config ${t('modelConfigTitle') === '模型配置' ? '' : 'en-user-config'}`">
|
||||
<div class="user-config-title">{{ t('modelConfigTitle') }}</div>
|
||||
<div class="config-item">
|
||||
<div class="config-item-label">
|
||||
<span>{{ t('audioInterruptionBtn') }}</span>
|
||||
<el-tooltip class="box-item" effect="dark" :content="t('audioInterruptionTips')" placement="top">
|
||||
<SvgIcon name="question" class="question-icon" /> </el-tooltip
|
||||
>:
|
||||
</div>
|
||||
<div class="config-item-content">
|
||||
<el-switch
|
||||
v-model="configData.canStopByVoice"
|
||||
inline-prompt
|
||||
:active-text="t('yes')"
|
||||
:inactive-text="t('no')"
|
||||
size="small"
|
||||
:disabled="isCalling"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item" v-if="type === 'video'">
|
||||
<div class="config-item-label">
|
||||
<span>{{ t('videoQualityBtn') }}</span>
|
||||
<el-tooltip class="box-item" effect="dark" :content="t('videoQualityTips')" placement="top">
|
||||
<SvgIcon name="question" class="question-icon" /> </el-tooltip
|
||||
>:
|
||||
</div>
|
||||
<div class="config-item-content">
|
||||
<el-switch
|
||||
v-model="configData.videoQuality"
|
||||
inline-prompt
|
||||
:active-text="t('yes')"
|
||||
:inactive-text="t('no')"
|
||||
size="small"
|
||||
:disabled="isCalling"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<div class="config-item-label">
|
||||
<span>{{ t('vadThresholdBtn') }}</span>
|
||||
<el-tooltip class="box-item" effect="dark" :content="t('vadThresholdTips')" placement="top">
|
||||
<SvgIcon name="question" class="question-icon" /> </el-tooltip
|
||||
>:
|
||||
</div>
|
||||
<div class="config-item-content vad-slider">
|
||||
<el-slider
|
||||
v-model="configData.vadThreshold"
|
||||
:min="0.5"
|
||||
:max="1"
|
||||
:step="0.1"
|
||||
size="small"
|
||||
:disabled="isCalling"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prompt-item" v-if="type === 'voice'">
|
||||
<div class="prompt-item-label">
|
||||
<span>{{ t('assistantPromptBtn') }}</span>
|
||||
<el-tooltip class="box-item" effect="dark" :content="t('assistantPromptTips')" placement="top">
|
||||
<SvgIcon name="question" class="question-icon" /> </el-tooltip
|
||||
>:
|
||||
</div>
|
||||
<div class="prompt-item-content">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
v-model="configData.assistantPrompt"
|
||||
resize="none"
|
||||
:disabled="isCalling"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="config-item">
|
||||
<div class="config-item-label">{{ t('useVoicePromptBtn') }}:</div>
|
||||
<div class="config-item-content">
|
||||
<el-switch
|
||||
v-model="configData.useAudioPrompt"
|
||||
inline-prompt
|
||||
:active-text="t('yes')"
|
||||
:inactive-text="t('no')"
|
||||
size="small"
|
||||
:disabled="isCalling"
|
||||
@change="handleSelectUseAudioPrompt"
|
||||
/>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="timbre-model">
|
||||
<div class="timbre-model-label">
|
||||
<span>{{ t('toneColorOptions') }}</span>
|
||||
<el-tooltip class="box-item" effect="dark" :content="t('toneColorOptionsTips')" placement="top">
|
||||
<SvgIcon name="question" class="question-icon" /> </el-tooltip
|
||||
>:
|
||||
</div>
|
||||
<div class="timbre-model-content">
|
||||
<el-select
|
||||
v-model="configData.useAudioPrompt"
|
||||
style="width: 100%"
|
||||
@change="handleChangePeople"
|
||||
placeholder="请选择"
|
||||
:disabled="isCalling"
|
||||
>
|
||||
<el-option :value="0" :label="t('nullOption')">{{ t('nullOption') }}</el-option>
|
||||
<el-option :value="1" :label="t('defaultOption')">{{ t('defaultOption') }}</el-option>
|
||||
<el-option :value="2" :label="t('femaleOption')">{{ t('femaleOption') }}</el-option>
|
||||
<el-option :value="3" :label="t('maleOption')">{{ t('maleOption') }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="prompt-item">
|
||||
<div class="prompt-item-label">
|
||||
<span>{{ t('voiceClonePromptInput') }}</span>
|
||||
<el-tooltip class="box-item" effect="dark" :content="t('voiceClonePromptTips')" placement="top">
|
||||
<SvgIcon name="question" class="question-icon" /> </el-tooltip
|
||||
>:
|
||||
</div>
|
||||
<div class="prompt-item-content">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
v-model="configData.voiceClonePrompt"
|
||||
resize="none"
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- <div class="timbre-config" v-if="configData.useAudioPrompt">
|
||||
<div class="timbre-config-label">{{ t('audioChoiceBtn') }}:</div>
|
||||
<div class="timbre-config-content">
|
||||
<el-checkbox-group v-model="configData.timbre" @change="handleSelectTimbre" :disabled="isCalling">
|
||||
<el-checkbox :value="1" :label="t('defaultAudioBtn')"></el-checkbox>
|
||||
<el-upload
|
||||
v-model:file-list="fileList"
|
||||
action=""
|
||||
:multiple="false"
|
||||
:on-change="handleChangeFile"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:disabled="isCalling"
|
||||
accept="audio/*"
|
||||
>
|
||||
<el-checkbox :value="2">
|
||||
<span>{{ t('customizationBtn') }}</span>
|
||||
<SvgIcon name="upload" className="checkbox-icon" />
|
||||
</el-checkbox>
|
||||
</el-upload>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-content" v-if="fileName">
|
||||
<SvgIcon name="document" class="document-icon" />
|
||||
<span class="file-name">{{ fileName }}</span>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const isCalling = defineModel('isCalling');
|
||||
const type = defineModel('type');
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
let defaultVoiceClonePrompt =
|
||||
'你是一个AI助手。你能接受视频,音频和文本输入并输出语音和文本。模仿输入音频中的声音特征。';
|
||||
let defaultAssistantPrompt = '';
|
||||
|
||||
const fileList = ref([]);
|
||||
const fileName = ref('');
|
||||
|
||||
const configData = ref({
|
||||
canStopByVoice: false,
|
||||
videoQuality: false,
|
||||
useAudioPrompt: 1,
|
||||
vadThreshold: 0.8,
|
||||
voiceClonePrompt: defaultVoiceClonePrompt,
|
||||
assistantPrompt: defaultAssistantPrompt,
|
||||
timbre: [1],
|
||||
audioFormat: 'mp3',
|
||||
base64Str: ''
|
||||
});
|
||||
|
||||
// let peopleList = [];
|
||||
// watch(
|
||||
// () => type.value,
|
||||
// val => {
|
||||
// console.log('val: ', val);
|
||||
// if (val === 'video') {
|
||||
// defaultVoiceClonePrompt =
|
||||
// '你是一个AI助手。你能接受视频,音频和文本输入并输出语音和文本。模仿输入音频中的声音特征。';
|
||||
// defaultAssistantPrompt = '作为助手,你将使用这种声音风格说话。';
|
||||
// } else {
|
||||
// defaultVoiceClonePrompt = '克隆音频提示中的音色以生成语音。';
|
||||
// defaultAssistantPrompt = 'Your task is to be a helpful assistant using this voice pattern.';
|
||||
// }
|
||||
// configData.value.voiceClonePrompt = defaultVoiceClonePrompt;
|
||||
// configData.value.assistantPrompt = defaultAssistantPrompt;
|
||||
// },
|
||||
// { immediate: true }
|
||||
// );
|
||||
watch(
|
||||
locale,
|
||||
(newLocale, oldLocale) => {
|
||||
console.log(`Language switched from ${oldLocale} to ${newLocale}`);
|
||||
if (newLocale === 'zh' && type.value === 'video') {
|
||||
defaultAssistantPrompt = '作为助手,你将使用这种声音风格说话。';
|
||||
} else if (newLocale === 'zh' && type.value === 'voice') {
|
||||
defaultAssistantPrompt = '作为助手,你将使用这种声音风格说话。';
|
||||
} else if (newLocale === 'en' && type.value === 'video') {
|
||||
defaultAssistantPrompt = 'As an assistant, you will speak using this voice style.';
|
||||
} else {
|
||||
defaultAssistantPrompt = 'As an assistant, you will speak using this voice style.';
|
||||
}
|
||||
configData.value.assistantPrompt = defaultAssistantPrompt;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
onMounted(() => {
|
||||
handleSetStorage();
|
||||
});
|
||||
const handleSelectTimbre = e => {
|
||||
if (e.length > 1) {
|
||||
const val = e[e.length - 1];
|
||||
configData.value.timbre = [val];
|
||||
// 默认音色
|
||||
if (val === 1) {
|
||||
configData.value.audioFormat = 'mp3';
|
||||
configData.value.base64Str = '';
|
||||
fileList.value = [];
|
||||
fileName.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleChangeFile = file => {
|
||||
if (isAudio(file) && sizeNotExceed(file)) {
|
||||
fileList.value = [file];
|
||||
fileName.value = file.name;
|
||||
configData.value.timbre = [2];
|
||||
handleUpload();
|
||||
} else {
|
||||
ElMessage.error('Please upload audio file and size not exceed 10MB');
|
||||
}
|
||||
};
|
||||
const isAudio = file => {
|
||||
return file.raw.type.includes('audio');
|
||||
};
|
||||
const sizeNotExceed = file => {
|
||||
return file.size / 1024 / 1024 <= 10;
|
||||
};
|
||||
const handleUpload = async () => {
|
||||
const file = fileList.value[0].raw;
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const base64String = e.target.result.split(',')[1];
|
||||
configData.value.audioFormat = file.name.split('.')[1];
|
||||
configData.value.base64Str = base64String;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
const handleSelectUseAudioPrompt = val => {
|
||||
if (val) {
|
||||
configData.value.voiceClonePrompt = defaultVoiceClonePrompt;
|
||||
configData.value.assistantPrompt = defaultAssistantPrompt;
|
||||
}
|
||||
};
|
||||
// 配置发生变化,更新到localstorage中
|
||||
watch(configData.value, () => {
|
||||
handleSetStorage();
|
||||
});
|
||||
const handleSetStorage = () => {
|
||||
const { timbre, canStopByVoice, ...others } = configData.value;
|
||||
const defaultConfigData = {
|
||||
canStopByVoice,
|
||||
...others
|
||||
};
|
||||
localStorage.setItem('configData', JSON.stringify(defaultConfigData));
|
||||
localStorage.setItem('canStopByVoice', canStopByVoice);
|
||||
};
|
||||
const handleChangePeople = val => {
|
||||
console.log('val: ', val);
|
||||
// const index = peopleList.findIndex(item => item.id === val);
|
||||
configData.value.voiceClonePrompt = defaultVoiceClonePrompt;
|
||||
configData.value.assistantPrompt = defaultAssistantPrompt;
|
||||
configData.value.timbre = [1];
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.user-config {
|
||||
&-title {
|
||||
height: 61px;
|
||||
padding: 18px 18px 0;
|
||||
color: rgba(23, 23, 23, 0.9);
|
||||
font-family: PingFang SC;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
}
|
||||
.config-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 0 0 18px;
|
||||
margin-bottom: 20px;
|
||||
&-label {
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
&-content {
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
.el-radio-group {
|
||||
.el-radio {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-content.vad-slider {
|
||||
width: 80%;
|
||||
padding-left: 7px;
|
||||
margin-right: 20px;
|
||||
.el-slider__button {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.timbre-config {
|
||||
padding: 0 0 0 18px;
|
||||
&-label {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
&-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.el-checkbox-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
> .el-checkbox {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
.el-checkbox {
|
||||
padding: 8px 16px;
|
||||
border-radius: 10px;
|
||||
background: #eaefff;
|
||||
margin-bottom: 12px;
|
||||
height: 40px;
|
||||
.el-checkbox__input {
|
||||
.el-checkbox__inner {
|
||||
border: 1px solid #4dc100;
|
||||
}
|
||||
}
|
||||
.el-checkbox__input.is-checked {
|
||||
.el-checkbox__inner {
|
||||
background: #4dc100;
|
||||
}
|
||||
}
|
||||
.el-checkbox__input.is-checked.is-disabled {
|
||||
.el-checkbox__inner::after {
|
||||
border-color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-checkbox__label {
|
||||
color: #7579eb !important;
|
||||
font-family: PingFang SC;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.checkbox-icon {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
.el-checkbox + .el-checkbox {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.prompt-item {
|
||||
padding: 0 0 0 18px;
|
||||
margin-bottom: 20px;
|
||||
&-label {
|
||||
// margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.file-content {
|
||||
padding: 0 0 0 18px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.document-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.file-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
.timbre-model {
|
||||
padding: 0 0 0 18px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&-label {
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
&-content {
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.en-user-config {
|
||||
.config-item-label {
|
||||
width: 160px;
|
||||
}
|
||||
.timbre-model-label {
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
.question-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
margin-left: 6px;
|
||||
}
|
||||
</style>
|
||||
<style lang="less">
|
||||
.el-switch--small .el-switch__core {
|
||||
min-width: 50px;
|
||||
}
|
||||
.el-popper.is-dark {
|
||||
max-width: 300px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="output-area">
|
||||
<div
|
||||
:class="`output-area-item ${item.type === 'USER' ? 'user-item' : 'bot-item'}`"
|
||||
:key="index"
|
||||
v-for="(item, index) in outputData"
|
||||
>
|
||||
<div v-if="item.type === 'USER'" class="user-input">
|
||||
<audio v-if="item.audio" :src="item.audio" controls></audio>
|
||||
</div>
|
||||
<div v-else class="bot-output">
|
||||
<div class="output-item">{{ item.text }}</div>
|
||||
<audio v-if="item.audio" :src="item.audio" controls></audio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
outputData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
watch(
|
||||
() => props.outputData,
|
||||
newVal => {
|
||||
nextTick(() => {
|
||||
if (newVal && props.containerClass) {
|
||||
let dom = document.querySelector(`.${props.containerClass}`);
|
||||
if (dom) {
|
||||
dom.scrollTop = dom.scrollHeight;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.output-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
&-item {
|
||||
width: fit-content;
|
||||
}
|
||||
&-item + &-item {
|
||||
margin-top: 16px;
|
||||
}
|
||||
&-item.user-item {
|
||||
align-self: flex-end;
|
||||
.user-input {
|
||||
}
|
||||
}
|
||||
&-item.bot-item {
|
||||
align-self: flex-start;
|
||||
width: 100%;
|
||||
.bot-output {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.output-item {
|
||||
padding: 8px 24px;
|
||||
border-radius: 10px;
|
||||
color: #202224;
|
||||
background: #f3f3f3;
|
||||
max-width: 90%;
|
||||
width: fit-content;
|
||||
font-family: PingFang SC;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
display: inline-block;
|
||||
}
|
||||
.output-item + audio {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="select-timbre">
|
||||
<el-checkbox-group v-model="timbre" @change="handleSelectTimbre" :disabled="disabled">
|
||||
<el-checkbox :value="1" label="Default Audio"></el-checkbox>
|
||||
<!-- <el-upload
|
||||
v-model:file-list="fileList"
|
||||
action=""
|
||||
:multiple="false"
|
||||
:on-change="handleChangeFile"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:disabled="disabled"
|
||||
accept="audio/*"
|
||||
>
|
||||
<el-checkbox :value="2">
|
||||
<span>Customization: Upload Audio</span>
|
||||
<SvgIcon name="upload" className="checkbox-icon" />
|
||||
</el-checkbox>
|
||||
</el-upload> -->
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const timbre = defineModel('timbre');
|
||||
const audioData = defineModel('audioData');
|
||||
const disabled = defineModel('disabled');
|
||||
const fileList = ref([]);
|
||||
|
||||
const handleSelectTimbre = e => {
|
||||
if (e.length > 1) {
|
||||
const val = e[e.length - 1];
|
||||
timbre.value = [val];
|
||||
// 默认音色
|
||||
if (val === 1) {
|
||||
audioData.value = {
|
||||
base64Str: '',
|
||||
type: 'mp3'
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleChangeFile = file => {
|
||||
if (isAudio(file) && sizeNotExceed(file)) {
|
||||
fileList.value = [file];
|
||||
timbre.value = [2];
|
||||
handleUpload();
|
||||
} else {
|
||||
ElMessage.error('Please upload audio file and size not exceed 1MB');
|
||||
}
|
||||
};
|
||||
const isAudio = file => {
|
||||
return file.name.endsWith('.mp3') || file.name.endsWith('.wav');
|
||||
};
|
||||
const sizeNotExceed = file => {
|
||||
return file.size / 1024 / 1024 <= 1;
|
||||
};
|
||||
const handleUpload = async () => {
|
||||
const file = fileList.value[0].raw;
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const base64String = e.target.result.split(',')[1];
|
||||
audioData.value = {
|
||||
base64Str: base64String,
|
||||
type: file.name.split('.')[1]
|
||||
};
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="less">
|
||||
.select-timbre {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.el-checkbox-group {
|
||||
display: flex;
|
||||
> .el-checkbox {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
.el-checkbox {
|
||||
padding: 8px 16px;
|
||||
border-radius: 10px;
|
||||
background: #eaefff;
|
||||
margin-right: 0;
|
||||
height: 40px;
|
||||
.el-checkbox__input {
|
||||
.el-checkbox__inner {
|
||||
border: 1px solid #4dc100;
|
||||
}
|
||||
}
|
||||
.el-checkbox__input.is-checked {
|
||||
.el-checkbox__inner {
|
||||
background: #4dc100;
|
||||
}
|
||||
}
|
||||
.el-checkbox__input.is-checked.is-disabled {
|
||||
.el-checkbox__inner::after {
|
||||
border-color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-checkbox__label {
|
||||
color: #7579eb !important;
|
||||
font-family: PingFang SC;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.checkbox-icon {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
.el-checkbox + .el-checkbox {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div :class="`skip-btn ${disabled ? 'disabled-btn' : ''}`">
|
||||
<div class="pause-icon">
|
||||
<SvgIcon name="pause" className="pause-svg" />
|
||||
</div>
|
||||
<span class="btn-text">{{ t('skipMessageBtn') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.skip-btn {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 14px 8px 10px;
|
||||
border-radius: 90px;
|
||||
background: #5865f2;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
.pause-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: #ffffff;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
.pause-svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #5865f2;
|
||||
}
|
||||
}
|
||||
.btn-text {
|
||||
color: #fff;
|
||||
font-family: PingFang SC;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
.disabled-btn {
|
||||
cursor: not-allowed;
|
||||
background: #f3f3f3;
|
||||
.pause-icon {
|
||||
background: #d1d1d1;
|
||||
.pause-svg {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
.btn-text {
|
||||
color: #d1d1d1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<svg :class="iconClass" v-html="content"></svg>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const content = ref('');
|
||||
|
||||
const iconClass = computed(() => ['svg-icon', props.className]);
|
||||
onMounted(() => {
|
||||
import(`@/assets/svg/${props.name}.svg`)
|
||||
.then(module => {
|
||||
fetch(module.default)
|
||||
.then(response => response.text())
|
||||
.then(svg => {
|
||||
content.value = svg;
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Error loading SVG icon: ${props.name}`, error);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.svg-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div class="bars" id="bars" :style="boxStyle">
|
||||
<!-- 柱形条 -->
|
||||
<div class="bar" v-for="(item, index) in defaultList" :key="index" :style="itemAttr(item)"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
analyser: {
|
||||
type: Object
|
||||
},
|
||||
dataArray: {
|
||||
type: [Array, Uint8Array]
|
||||
},
|
||||
isCalling: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isPlaying: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 容器高度
|
||||
boxStyle: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
height: '80px'
|
||||
};
|
||||
}
|
||||
},
|
||||
// 柱形条宽度
|
||||
itemStyle: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
width: '6px',
|
||||
margin: '0 2px',
|
||||
borderRadius: '5px'
|
||||
};
|
||||
}
|
||||
},
|
||||
configList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
const animationFrameId = ref();
|
||||
const defaultList = ref([]);
|
||||
const bgColor = ref('#4c5cf8');
|
||||
const itemAttr = computed(() => item => {
|
||||
return {
|
||||
height: item + 'px',
|
||||
...props.itemStyle
|
||||
};
|
||||
});
|
||||
watch(
|
||||
() => props.dataArray,
|
||||
newVal => {
|
||||
if (newVal && props.isCalling) {
|
||||
console.log('draw');
|
||||
drawBars();
|
||||
} else {
|
||||
console.log('stop');
|
||||
stopDraw();
|
||||
}
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => props.configList,
|
||||
newVal => {
|
||||
if (newVal.length > 0) {
|
||||
defaultList.value = newVal;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
watch(
|
||||
() => props.isPlaying,
|
||||
newVal => {
|
||||
if (newVal) {
|
||||
// 绿色
|
||||
bgColor.value = '#4dc100';
|
||||
} else {
|
||||
// 蓝色
|
||||
bgColor.value = '#4c5cf8';
|
||||
}
|
||||
}
|
||||
);
|
||||
function drawBars() {
|
||||
const bars = document.querySelectorAll('.bar');
|
||||
if (bars.length === 0) {
|
||||
cancelAnimationFrame(animationFrameId.value);
|
||||
return;
|
||||
}
|
||||
|
||||
const maxHeight = document.querySelector('.bars').clientHeight; // 最大高度为容器的高度
|
||||
|
||||
const averageVolume = props.dataArray.reduce((sum, value) => sum + value, 0) / props.dataArray.length;
|
||||
const normalizedVolume = props.isPlaying ? Math.random() : averageVolume / 128; // 将音量数据归一化为0到1之间
|
||||
|
||||
bars.forEach((bar, index) => {
|
||||
const minHeight = defaultList.value[index];
|
||||
const randomFactor = Math.random() * 1.5 + 0.5; // 随机因子
|
||||
const newHeight = Math.min(
|
||||
maxHeight,
|
||||
minHeight + (maxHeight - minHeight) * normalizedVolume * randomFactor
|
||||
); // 根据音量设置高度
|
||||
bar.style.height = `${newHeight}px`; // 设置新的高度
|
||||
bar.style.backgroundColor = bgColor.value;
|
||||
});
|
||||
|
||||
animationFrameId.value = requestAnimationFrame(drawBars);
|
||||
}
|
||||
const stopDraw = () => {
|
||||
if (animationFrameId.value) {
|
||||
cancelAnimationFrame(animationFrameId.value);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.bars {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.bar {
|
||||
// width: 6px;
|
||||
// margin: 0 2px;
|
||||
background-color: #4c5cf8;
|
||||
transition:
|
||||
height 0.1s,
|
||||
background-color 0.1s;
|
||||
border-radius: 5px; /* 圆角 */
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user