flat: 优化语音

This commit is contained in:
史典卓
2025-04-16 14:24:06 +08:00
parent 0d2b8ae65f
commit 446b48ef6d
28 changed files with 1059 additions and 264 deletions

View File

@@ -1,116 +1,114 @@
<template>
<view class="chat-container">
<FadeView :show="!messages.length" :duration="600">
<view class="chat-background">
<image class="backlogo" src="/static/icon/backAI.png"></image>
<view class="back-rowTitle">欢迎使用青岛AI智能求职</view>
<view class="back-rowText">
我可以根据您的简历和求职需求帮你精准匹配青岛市互联网招聘信息对比招聘信息的优缺点提供面试指导等请把你的任务交给我吧~
</view>
<view class="back-rowh3">猜你所想</view>
<view class="back-rowmsg" v-for="item in queries" @click="sendMessageGuess(item)">
{{ item }}
<view class="chat-background" v-fade:600="!messages.length">
<image class="backlogo" src="/static/icon/backAI.png"></image>
<view class="back-rowTitle">欢迎使用青岛AI智能求职</view>
<view class="back-rowText">
我可以根据您的简历和求职需求帮你精准匹配青岛市互联网招聘信息对比招聘信息的优缺点提供面试指导等请把你的任务交给我吧~
</view>
<view class="back-rowh3">猜你所想</view>
<view class="back-rowmsg" v-for="item in queries" @click="sendMessageGuess(item)">
{{ item }}
</view>
<view class="chat-item self" v-if="isRecording">
<view class="message">{{ recognizedText }} {{ lastFinalText }}</view>
</view>
</view>
<scroll-view class="chat-list scrollView" :scroll-top="scrollTop" :scroll-y="true" scroll-with-animation>
<view class="chat-list list-content" v-fade:600="messages.length >= 1">
<view
v-for="(msg, index) in messages"
:key="index"
:id="'msg-' + index"
class="chat-item"
:class="{ self: msg.self }"
>
<view class="message" v-if="msg.self">
<view class="msg-filecontent" v-if="msg.files.length">
<view
class="msg-files btn-light"
v-for="(file, vInex) in msg.files"
:key="vInex"
@click="jumpUrl(file)"
>
<image class="msg-file-icon" src="/static/icon/Vector2.png"></image>
<text class="msg-file-text">{{ file.name || '附件' }}</text>
</view>
</view>
{{ msg.displayText }}
</view>
<view class="message" :class="{ messageNull: !msg.displayText }" v-else>
<!-- {{ msg.displayText }} -->
<view class="message-markdown">
<md-render
:content="msg.displayText"
:typing="isTyping && messages.length - 1 === index"
></md-render>
<view class="message-controll" v-show="showControll(index)">
<view class="controll-left">
<image
class="controll-icon btn-light"
src="/static/icon/copy.png"
@click="copyMarkdown(msg.displayText)"
></image>
<image
class="controll-icon mar_le10 btn-light"
src="/static/icon/feedback.png"
v-if="!msg.userBadFeedback"
@click="userGoodFeedback(msg)"
></image>
<image
v-if="isSpeaking && !isPaused && speechIndex === index"
class="controll-icon mar_le10 btn-light"
src="/static/icon/stop.png"
@click="stopMarkdown(msg.displayText, index)"
></image>
<image
class="controll-icon mar_le10 btn-light"
src="/static/icon/broadcast.png"
@click="readMarkdown(msg.displayText, index)"
v-else
></image>
</view>
<view class="controll-right">
<image
class="controll-icon mar_ri10 btn-light"
src="/static/icon/refresh.png"
@click="refreshMarkdown(index)"
></image>
</view>
</view>
</view>
<!-- guess -->
<view
class="guess"
v-if="showGuess && !msg.self && messages.length - 1 === index && msg.displayText"
>
<view class="gulist">
<view
class="guess-list"
@click="sendMessageGuess(item)"
v-for="(item, index) in guessList"
:key="index"
>
{{ item }}
</view>
</view>
</view>
</view>
</view>
<view class="chat-item self" v-if="isRecording">
<view class="message">{{ recognizedText }} {{ lastFinalText }}</view>
</view>
</view>
</FadeView>
<scroll-view class="chat-list scrollView" :scroll-top="scrollTop" :scroll-y="true" scroll-with-animation>
<FadeView :show="messages.length >= 1" :duration="600">
<view class="chat-list list-content">
<view
v-for="(msg, index) in messages"
:key="index"
:id="'msg-' + index"
class="chat-item"
:class="{ self: msg.self }"
>
<view class="message" v-if="msg.self">
<view class="msg-filecontent" v-if="msg.files.length">
<view
class="msg-files btn-light"
v-for="(file, vInex) in msg.files"
:key="vInex"
@click="jumpUrl(file)"
>
<image class="msg-file-icon" src="/static/icon/Vector2.png"></image>
<text class="msg-file-text">{{ file.name || '附件' }}</text>
</view>
</view>
{{ msg.displayText }}
</view>
<view class="message" :class="{ messageNull: !msg.displayText }" v-else>
<!-- {{ msg.displayText }} -->
<view class="message-markdown">
<md-render
:content="msg.displayText"
:typing="isTyping && messages.length - 1 === index"
></md-render>
<view class="message-controll" v-show="showControll(index)">
<view class="controll-left">
<image
class="controll-icon btn-light"
src="/static/icon/copy.png"
@click="copyMarkdown(msg.displayText)"
></image>
<image
class="controll-icon mar_le10 btn-light"
src="/static/icon/feedback.png"
@click="userGoodFeedback(msg)"
></image>
<image
v-if="isSpeaking && !isPaused && speechIndex === index"
class="controll-icon mar_le10 btn-light"
src="/static/icon/stop.png"
@click="stopMarkdown(msg.displayText, index)"
></image>
<image
class="controll-icon mar_le10 btn-light"
src="/static/icon/broadcast.png"
@click="readMarkdown(msg.displayText, index)"
v-else
></image>
</view>
<view class="controll-right">
<image
class="controll-icon mar_ri10 btn-light"
src="/static/icon/refresh.png"
@click="refreshMarkdown(index)"
></image>
</view>
</view>
</view>
<!-- guess -->
<view
class="guess"
v-if="showGuess && !msg.self && messages.length - 1 === index && msg.displayText"
>
<view class="gulist">
<view
class="guess-list"
@click="sendMessageGuess(item)"
v-for="(item, index) in guessList"
:key="index"
>
{{ item }}
</view>
</view>
</view>
</view>
</view>
<view class="chat-item self" v-if="isRecording">
<view class="message">{{ recognizedText }} {{ lastFinalText }}</view>
</view>
<view v-if="isTyping" class="self">
<text class="message msg-loading">
<span class="ai-loading"></span>
</text>
</view>
<view v-if="isTyping" class="self">
<text class="message msg-loading">
<span class="ai-loading"></span>
</text>
</view>
</FadeView>
</view>
</scroll-view>
<view class="vio_container" @click="handleTouchEnd" :class="status" v-if="status !== 'idle'">
<!-- 使用 v-show 保证不销毁事件 -->
<view class="vio_container" :class="status" v-show="status !== 'idle'">
<view class="record-tip">{{ statusText }}</view>
<WaveDisplay
:background="audiowaveStyle"
@@ -119,7 +117,7 @@
:showInfo="isRecording"
/>
</view>
<view class="input-area" v-else>
<view class="input-area" v-show="status === 'idle'">
<view class="areatext">
<input
v-model="textInput"
@@ -133,7 +131,7 @@
/>
<view
class="input_vio"
@touchstart="handleTouchStart"
@touchstart.prevent="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@touchcancel="handleTouchCancel"
@@ -222,28 +220,42 @@
</scroll-view>
</view>
</view>
<PopupFeeBack ref="feeback" @onSend="confirmFeeBack"></PopupFeeBack>
<MsgTips ref="feeBackTips" content="已收到反馈,感谢您的关注" title="反馈成功" :icon="successIcon"></MsgTips>
</view>
</template>
<script setup>
import { ref, inject, nextTick, defineProps, defineEmits, onMounted, toRaw, reactive, computed } from 'vue';
import {
ref,
inject,
nextTick,
defineProps,
defineEmits,
onMounted,
onUnmounted,
toRaw,
reactive,
computed,
watch,
} from 'vue';
import { storeToRefs } from 'pinia';
import config from '@/config.js';
import useChatGroupDBStore from '@/stores/userChatGroupStore';
import MdRender from '@/components/md-render/md-render.vue';
import CollapseTransition from '@/components/CollapseTransition/CollapseTransition.vue';
import FadeView from '@/components/FadeView/FadeView.vue';
import PopupFeeBack from './popupbadFeeback.vue';
import AudioWave from './AudioWave.vue';
import WaveDisplay from './WaveDisplay.vue';
import FileIcon from './fileIcon.vue';
import FileText from './fileText.vue';
import { useSpeechReader } from '@/hook/useSpeechReader';
import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
import { useTTSPlayer } from '@/hook/useTTSPlayer.js';
// 全局
const { $api, navTo, throttle } = inject('globalFunction');
const emit = defineEmits(['onConfirm']);
const { messages, isTyping, textInput, chatSessionID } = storeToRefs(useChatGroupDBStore());
import successIcon from '@/static/icon/success.png';
// hook
const {
isRecording,
@@ -256,7 +268,7 @@ const {
lastFinalText,
} = useAudioRecorder(config.vioceBaseURl);
const { speak, pause, resume, isSpeaking, isPaused, cancelAudio } = useSpeechReader();
const { speak, pause, resume, isSpeaking, isPaused, cancelAudio, audioUrl } = useTTSPlayer(config.speechSynthesis);
// state
const queries = ref([]);
@@ -272,7 +284,11 @@ const startY = ref(0);
const cancelThreshold = 100;
const speechIndex = ref(0);
const isAudioPermission = ref(false);
const feebackData = ref(null);
// ref for DOM element
const voiceBtn = ref(null);
const feeback = ref(null);
const feeBackTips = ref(null);
const state = reactive({
uploadFileTips: '请根据以上附件,帮我推荐岗位。',
});
@@ -510,7 +526,6 @@ const handleTouchStart = async (e) => {
return tipsPermisson();
}
cancelAudio();
console.log('handleTouchStart');
startY.value = e.touches[0].clientY;
status.value = 'recording';
showfile.value = false;
@@ -521,7 +536,6 @@ const handleTouchStart = async (e) => {
};
const handleTouchMove = (e) => {
console.log('handleTouchMove');
const moveY = e.touches[0].clientY;
if (startY.value - moveY > cancelThreshold) {
status.value = 'cancel';
@@ -531,28 +545,23 @@ const handleTouchMove = (e) => {
};
const handleTouchEnd = () => {
console.log('handleTouchEnd');
if (status.value === 'cancel') {
console.log('取消发送');
cancelRecording();
} else {
console.log('stopRecording');
stopRecording();
$api.sleep(1000).then(() => {
if (isAudioPermission.value) {
if (recognizedText.value) {
sendMessage(recognizedText.value);
} else {
$api.msg('说话时长太短');
}
if (isAudioPermission.value) {
if (recognizedText.value) {
sendMessage(recognizedText.value);
} else {
$api.msg('说话时长太短');
}
});
}
}
status.value = 'idle';
};
const handleTouchCancel = () => {
console.log('handleTouchCancel');
stopRecording();
status.value = 'idle';
};
@@ -578,9 +587,18 @@ function copyMarkdown(value) {
}
function userGoodFeedback(msg) {
$api.msg('该功能正在开发中,敬请期待后续更新!');
console.log(msg.dataId);
// useChatGroupDBStore().badFeedback(msg.dataId, msg.parentGroupId);
// $api.msg('该功能正在开发中,敬请期待后续更新!');
feeback.value?.open();
feebackData.value = msg;
}
function confirmFeeBack(value) {
useChatGroupDBStore()
.badFeedback(feebackData.value, value)
.then(() => {
feeback.value?.close();
feeBackTips.value?.open();
});
}
function readMarkdown(value, index) {
@@ -788,14 +806,14 @@ image-margin-top = 40rpx
-webkit-user-select: text;
.message-markdown
border-radius: 0 20rpx 20rpx 20rpx;
padding: 20rpx 20rpx 0 20rpx;
padding: 20rpx 20rpx 20rpx 20rpx;
background: #F6F6F6;
.message-controll
display: flex
justify-content: space-between
align-items: center
border-top: 2rpx solid #EAEAEA
padding: 24rpx 0
padding: 24rpx 0 4rpx 0
margin-top: 10rpx
.controll-left
.controll-right
@@ -870,12 +888,19 @@ image-margin-top = 40rpx
-khtml-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
touch-action: none; /* 禁用默认滚动 */
.input_vio:active
background: #e8e8e8
.vio_container
background: transparent
padding: 28rpx
text-align: center
-webkit-touch-callout:none;
-webkit-user-select:none;
-khtml-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
touch-action: none; /* 禁用默认滚动 */
.record-tip
font-weight: 400;
color: #909090;