|
|
@@ -107,26 +107,21 @@
|
|
|
/>
|
|
|
<!-- 消息输入框 -->
|
|
|
<el-input
|
|
|
- v-model="prompt"
|
|
|
+ v-model="displayedPrompt"
|
|
|
placeholder="输入问题..."
|
|
|
class="user-input"
|
|
|
@keyup.enter="handleSendByKeydown"
|
|
|
>
|
|
|
<!-- 语音输入 -->
|
|
|
- <template #prepend>
|
|
|
- <el-button
|
|
|
- @click="toggleSpeechInput"
|
|
|
- size="small"
|
|
|
- :class="{ 'recording': isRecording }"
|
|
|
- circle
|
|
|
- >
|
|
|
- <el-icon v-if="!isRecording"><Microphone /></el-icon>
|
|
|
- <el-icon v-else><Mute /></el-icon>
|
|
|
- <!-- 显示倒计时(仅录音时显示) -->
|
|
|
- <span v-if="isRecording" class="countdown-text">{{ countdown }}s</span>
|
|
|
- </el-button>
|
|
|
- </template>
|
|
|
-
|
|
|
+ <template #prepend>
|
|
|
+ <VoiceInput
|
|
|
+ @voiceRecognized="handleVoiceRecognized"
|
|
|
+ @recordingStatusChanged="handleRecordingStatusChanged"
|
|
|
+ lang="zh-CN"
|
|
|
+ maxDuration="10"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+
|
|
|
<!-- 终止按钮和发送按钮条件渲染 -->
|
|
|
<template #append>
|
|
|
<!-- 终止问答按钮 -->
|
|
|
@@ -151,16 +146,14 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import {ref,onUnmounted, defineProps, defineEmits, onMounted, watch, nextTick} from 'vue'
|
|
|
+import {ref,onUnmounted, defineProps, defineEmits, onMounted, watch, nextTick, computed} from 'vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
import { CreateDialogue, sendChatMessageStream } from '@/api/questions.js'
|
|
|
import { teacherList } from '@/api/teachers.js'
|
|
|
import DefaultMessage from '@/components/DefaultMessage/index.vue'
|
|
|
import MarkdownView from '@/components/MarkdownView/index.vue'
|
|
|
import { saveRecord } from '@/api/personalized/index.js'
|
|
|
-
|
|
|
-// 语音图标导入
|
|
|
-import { Microphone, Mute } from '@element-plus/icons-vue'
|
|
|
+import VoiceInput from '../ai/voice/VoiceInput.vue'
|
|
|
|
|
|
// 终止
|
|
|
import stopicon from '@/assets/icon/stopicon.png'
|
|
|
@@ -201,11 +194,27 @@ const enableContext = ref(true)
|
|
|
import { useAudioPlayer } from '@/api/tts/useAudioPlayer';
|
|
|
const { playAudioChunk , stopPlayback } = useAudioPlayer();
|
|
|
|
|
|
-// 语音输入响应式变量
|
|
|
-const isRecording = ref(false) // 录音状态
|
|
|
-const recognition = ref(null) // 语音识别实例
|
|
|
-const countdown = ref(0) // 倒计时剩余秒数
|
|
|
-const countdownTimer = ref(null) // 倒计时定时器
|
|
|
+// 语音输入状态跟踪
|
|
|
+const isVoiceRecording = ref(false); // 当前是否正在录音
|
|
|
+const voiceRecognizedText = ref(""); // 实时语音识别结果
|
|
|
+
|
|
|
+// 用于控制输入框显示的内容
|
|
|
+const displayedPrompt = computed({
|
|
|
+ get() {
|
|
|
+ // 录音时,显示prompt.value + 实时语音识别结果
|
|
|
+ if (isVoiceRecording.value) {
|
|
|
+ return prompt.value ? `${prompt.value} ${voiceRecognizedText.value}` : voiceRecognizedText.value;
|
|
|
+ }
|
|
|
+ // 不录音时,只显示prompt.value
|
|
|
+ return prompt.value;
|
|
|
+ },
|
|
|
+ set(newValue) {
|
|
|
+ // 只在用户手动输入时更新prompt.value
|
|
|
+ if (!isVoiceRecording.value) {
|
|
|
+ prompt.value = newValue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+});
|
|
|
|
|
|
// 处理选择的默认消息
|
|
|
const handleSelectMessage = message => {
|
|
|
@@ -299,7 +308,7 @@ const createAiChart = async () => {
|
|
|
console.error('请求出错:', error)
|
|
|
})
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 发送消息
|
|
|
const sendMessage = async () => {
|
|
|
if (prompt.value.trim()) {
|
|
|
@@ -322,86 +331,33 @@ const sendMessage = async () => {
|
|
|
}
|
|
|
|
|
|
|
|
|
-// =========== 【语音录入】相关 ===========
|
|
|
-// 初始化语音识别
|
|
|
-const initSpeechRecognition = () => {
|
|
|
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
|
|
|
- if (!SpeechRecognition) {
|
|
|
- ElMessage.warning('当前浏览器不支持语音输入功能')
|
|
|
- return null
|
|
|
+// 语音输入识别结果处理
|
|
|
+const handleVoiceRecognized = (text) => {
|
|
|
+ if (isVoiceRecording.value) {
|
|
|
+ // 在同一次录音过程中,只更新临时变量,不修改prompt.value
|
|
|
+ voiceRecognizedText.value = text;
|
|
|
+ } else {
|
|
|
+ // 在录音结束时,将最终的语音内容追加到prompt.value
|
|
|
+ prompt.value = prompt.value ? `${prompt.value} ${text}` : text;
|
|
|
+ // 清空临时变量
|
|
|
+ voiceRecognizedText.value = "";
|
|
|
}
|
|
|
-
|
|
|
- const instance = new SpeechRecognition()
|
|
|
- instance.lang = "zh-CN"
|
|
|
- instance.interimResults = false
|
|
|
-
|
|
|
- instance.onresult = (event) => {
|
|
|
- if (event.results?.[0]?.[0]) {
|
|
|
- prompt.value += event.results[0][0].transcript
|
|
|
+};
|
|
|
+
|
|
|
+// 处理录音状态变化
|
|
|
+const handleRecordingStatusChanged = (isRecording) => {
|
|
|
+ const wasRecording = isVoiceRecording.value;
|
|
|
+ isVoiceRecording.value = isRecording;
|
|
|
+
|
|
|
+ // 如果是从录音状态切换到非录音状态,需要将临时的语音识别结果追加到prompt.value
|
|
|
+ if (wasRecording && !isRecording) {
|
|
|
+ if (voiceRecognizedText.value) {
|
|
|
+ prompt.value = prompt.value ? `${prompt.value} ${voiceRecognizedText.value}` : voiceRecognizedText.value;
|
|
|
+ // 清空临时变量
|
|
|
+ voiceRecognizedText.value = "";
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- // 识别器结束时清除定时器
|
|
|
- instance.onend = () => {
|
|
|
- clearInterval(countdownTimer.value)
|
|
|
- isRecording.value = false
|
|
|
- countdown.value = 0
|
|
|
- }
|
|
|
-
|
|
|
- instance.onerror = (event) => {
|
|
|
- console.error("语音识别错误:", event.error)
|
|
|
- clearInterval(countdownTimer.value) // 出错时清除定时器
|
|
|
- isRecording.value = false
|
|
|
- ElMessage.error('语音输入失败,请重试')
|
|
|
- countdown.value = 0
|
|
|
- }
|
|
|
- return instance
|
|
|
-}
|
|
|
-
|
|
|
-// 切换录音状态
|
|
|
-const toggleSpeechInput = () => {
|
|
|
- // 无论当前状态如何,先清除可能存在的旧定时器
|
|
|
- clearInterval(countdownTimer.value)
|
|
|
- countdownTimer.value = null
|
|
|
-
|
|
|
- if (isRecording.value) {
|
|
|
- // 手动停止时重置状态
|
|
|
- countdown.value = 0
|
|
|
- recognition.value?.stop()
|
|
|
- isRecording.value = false
|
|
|
- } else {
|
|
|
- // 初始化倒计时前再次清除定时器(防止快速点击)
|
|
|
- clearInterval(countdownTimer.value)
|
|
|
- countdown.value = 10 // 重置为10秒
|
|
|
-
|
|
|
- recognition.value = initSpeechRecognition()
|
|
|
- if (!recognition.value) return
|
|
|
-
|
|
|
- navigator.mediaDevices.getUserMedia({ audio: true })
|
|
|
- .then(() => {
|
|
|
- recognition.value.start()
|
|
|
- isRecording.value = true
|
|
|
-
|
|
|
- // 启动新的倒计时定时器
|
|
|
- countdownTimer.value = setInterval(() => {
|
|
|
- countdown.value--
|
|
|
- if (countdown.value <= 0) {
|
|
|
- clearInterval(countdownTimer.value) // 倒计时结束清除
|
|
|
- recognition.value.stop()
|
|
|
- isRecording.value = false
|
|
|
- countdown.value = 0
|
|
|
- }
|
|
|
- }, 1000)
|
|
|
- })
|
|
|
- .catch((err) => {
|
|
|
- console.error("麦克风权限获取失败:", err)
|
|
|
- ElMessage.warning('请允许麦克风权限以使用语音输入')
|
|
|
- // 出错时重置状态
|
|
|
- isRecording.value = false
|
|
|
- countdown.value = 0
|
|
|
- })
|
|
|
- }
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
|
|
|
// 模拟 AI 回复
|
|
|
@@ -1096,10 +1052,21 @@ $text-color: #483d8b; // 文本颜色:靛蓝色
|
|
|
|
|
|
// 语音按钮样式
|
|
|
::v-deep(.el-input-group__prepend) {
|
|
|
- width: rpx(15);
|
|
|
+ width: rpx(25); // 非录音状态的默认宽度
|
|
|
+ min-width: rpx(25);
|
|
|
background: white;
|
|
|
border-radius: rpx(5);
|
|
|
text-align: center;
|
|
|
+ padding: 0;
|
|
|
+ transition: width 0.3s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 录音状态下的样式
|
|
|
+ ::v-deep(.el-input-group__prepend) {
|
|
|
+ &.recording {
|
|
|
+ width: rpx(60); // 录音状态下增加宽度
|
|
|
+ min-width: rpx(60);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
::v-deep(.el-input-group__prepend .el-button.recording) {
|