flat: 优化语音
This commit is contained in:
@@ -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;
|
||||
|
Reference in New Issue
Block a user