chore(chat-input-area): remove voice input support
Remove all voice input related UI elements, speech recognition logic, and external RecordingWaveBtn dependency from the chat input area, simplifying the component to only support text input.
This commit is contained in:
@@ -1,31 +1,18 @@
|
||||
<template>
|
||||
<div class="input-area-wrapper" @touchend="handleVoiceTouchEndFromContainer"
|
||||
@touchcancel="handleVoiceTouchEndFromContainer">
|
||||
<div v-if="!visibleWaveBtn || speechProvider !== 'wechat'"
|
||||
class="mx-[8px] mb-[6px] flex min-h-[42px] items-end rounded-[21px] bg-white px-[8px] py-[6px] shadow-[0_2px_12px_rgba(18,39,75,0.06)]">
|
||||
<!-- 语音/键盘切换 -->
|
||||
<div v-if="isSpeechRecognitionSupported" class="flex h-[30px] w-[30px] shrink-0 items-center justify-center"
|
||||
@click="toggleVoiceMode">
|
||||
<img class="h-[22px] w-[22px]" v-if="!isVoiceMode"
|
||||
src="https://oss.nianxx.cn/mp/static/version_101/home/input_voice_icon.png" />
|
||||
<img class="h-[22px] w-[22px]" v-else
|
||||
src="https://oss.nianxx.cn/mp/static/version_101/home/input_keyboard_icon.png" />
|
||||
<div
|
||||
class="mx-[8px] mb-[6px] flex min-h-[42px] items-center rounded-[21px] bg-white px-[8px] py-[6px] shadow-[0_2px_12px_rgba(18,39,75,0.06)]">
|
||||
<div class="flex h-[30px] w-[30px] shrink-0 items-center justify-center">
|
||||
<img class="h-[22px] w-[22px]" src="https://oss.nianxx.cn/mp/static/version_101/home/input_keyboard_icon.png" />
|
||||
</div>
|
||||
|
||||
<!-- 输入框/语音按钮容器 -->
|
||||
<div class="min-w-0 flex-1" :class="{ 'pl-[4px]': !isSpeechRecognitionSupported }">
|
||||
<textarea ref="textareaRef" v-if="!isVoiceMode" v-model="inputMessage" rows="1" maxlength="300"
|
||||
:placeholder="placeholder"
|
||||
<!-- 输入框 -->
|
||||
<div class="flex min-h-[30px] min-w-0 flex-1 items-center">
|
||||
<textarea ref="textareaRef" v-model="inputMessage" rows="1" maxlength="300" :placeholder="placeholder"
|
||||
class="block h-[22px] max-h-[92px] min-h-[22px] w-full resize-none overflow-x-hidden overflow-y-auto border-0 bg-transparent p-0 text-[14px] leading-[22px] text-[#333] outline-none appearance-none placeholder:text-[#A5AAB4] scrollbar-none [&::-webkit-scrollbar]:hidden"
|
||||
@input="adjustTextareaHeight" @focus="handleFocus" @blur="handleBlur" @touchstart="handleTouchStart"
|
||||
@touchend="handleTouchEnd" />
|
||||
|
||||
|
||||
<div v-if="isVoiceMode"
|
||||
class="flex h-[36px] w-full select-none items-center justify-center bg-transparent text-[14px] leading-[22px] text-[#333] active:bg-gray-100"
|
||||
@longpress="handleVoiceTouchStart" @touchend="handleVoiceTouchEnd" @touchcancel="handleVoiceTouchEnd">
|
||||
按住 说话
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex h-[30px] w-[34px] shrink-0 items-center justify-end">
|
||||
@@ -39,10 +26,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 录音按钮 -->
|
||||
<RecordingWaveBtn v-if="visibleWaveBtn" ref="recordingWaveBtnRef" />
|
||||
|
||||
<div class="text-center text-[9px] leading-[12px] text-[#A5AAB4] pb-[6px]">
|
||||
内容由AI大模型生成,请仔细鉴别
|
||||
</div>
|
||||
@@ -51,14 +34,6 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick, onMounted, defineExpose, onUnmounted } from "vue";
|
||||
import RecordingWaveBtn from "@/components/Speech/RecordingWaveBtn.vue";
|
||||
|
||||
|
||||
let manager = null;
|
||||
let speechProvider = "";
|
||||
const isSpeechRecognitionEnabled = ref(true);
|
||||
const isSpeechRecognitionSupported = ref(true);
|
||||
let appSpeechOptions = {};
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: String,
|
||||
@@ -72,58 +47,14 @@ const emit = defineEmits([
|
||||
"noHideKeyboard",
|
||||
"keyboardShow",
|
||||
"keyboardHide",
|
||||
"sendVoice",
|
||||
]);
|
||||
|
||||
const textareaRef = ref(null);
|
||||
const recordingWaveBtnRef = ref(null);
|
||||
const appSpeechRef = ref(null);
|
||||
const appSpeechKey = ref(0);
|
||||
const appSpeechVisible = ref(true);
|
||||
const placeholder = ref("快告诉小七您在想什么~");
|
||||
const inputMessage = ref(props.modelValue || "");
|
||||
const maxTextareaHeight = 92;
|
||||
const isFocused = ref(false);
|
||||
const keyboardHeight = ref(0);
|
||||
const isVoiceMode = ref(false);
|
||||
const visibleWaveBtn = ref(false);
|
||||
const isRecording = ref(false);
|
||||
const isVoicePressing = ref(false);
|
||||
const appRecognizedText = ref("");
|
||||
const isAppSpeechStarting = ref(false);
|
||||
const isAppWaitingToSend = ref(false);
|
||||
let watchDogTimer = null;
|
||||
let appStopFallbackTimer = null;
|
||||
let hasSentAppRecognition = false;
|
||||
|
||||
const resetUI = () => {
|
||||
isRecording.value = false;
|
||||
visibleWaveBtn.value = false;
|
||||
try {
|
||||
if (recordingWaveBtnRef.value) {
|
||||
recordingWaveBtnRef.value.stopAnimation();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("resetUI stopAnimation error", e);
|
||||
}
|
||||
if (watchDogTimer) {
|
||||
clearTimeout(watchDogTimer);
|
||||
watchDogTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startWatchDog = (timeout = 10000) => {
|
||||
if (watchDogTimer) clearTimeout(watchDogTimer);
|
||||
watchDogTimer = setTimeout(() => {
|
||||
watchDogTimer = null;
|
||||
console.warn("recording watchdog triggered, forcing UI reset");
|
||||
if (speechProvider === "app" && hasActiveVoiceRecognition()) {
|
||||
stopActiveVoiceRecognition({ shouldSend: true });
|
||||
return;
|
||||
}
|
||||
resetUI();
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
const adjustTextareaHeight = () => {
|
||||
const textarea = textareaRef.value;
|
||||
@@ -133,55 +64,6 @@ const adjustTextareaHeight = () => {
|
||||
textarea.style.height = `${Math.min(textarea.scrollHeight, maxTextareaHeight)}px`;
|
||||
};
|
||||
|
||||
const clearAppStopFallback = () => {
|
||||
if (appStopFallbackTimer) {
|
||||
clearTimeout(appStopFallbackTimer);
|
||||
appStopFallbackTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const showRecordingUI = () => {
|
||||
isRecording.value = true;
|
||||
visibleWaveBtn.value = true;
|
||||
|
||||
nextTick(() => {
|
||||
if (recordingWaveBtnRef.value) {
|
||||
recordingWaveBtnRef.value.startAnimation();
|
||||
}
|
||||
});
|
||||
startWatchDog(10000);
|
||||
};
|
||||
|
||||
const resetAppSpeechComponent = () => {
|
||||
if (speechProvider !== "app") return;
|
||||
|
||||
isAppSpeechStarting.value = false;
|
||||
appSpeechVisible.value = false;
|
||||
nextTick(() => {
|
||||
appSpeechKey.value += 1;
|
||||
appSpeechVisible.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
const getPendingAppSpeechText = () => {
|
||||
return (appRecognizedText.value || inputMessage.value || "").trim();
|
||||
};
|
||||
|
||||
const scheduleAppRecognizedTextSend = (timeout = 1200) => {
|
||||
isAppWaitingToSend.value = true;
|
||||
clearAppStopFallback();
|
||||
|
||||
if (getPendingAppSpeechText()) {
|
||||
sendAppRecognizedText();
|
||||
return;
|
||||
}
|
||||
|
||||
appStopFallbackTimer = setTimeout(() => {
|
||||
appStopFallbackTimer = null;
|
||||
sendAppRecognizedText();
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
// 保持和父组件同步
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
@@ -200,304 +82,11 @@ watch(inputMessage, (val) => {
|
||||
nextTick(adjustTextareaHeight);
|
||||
});
|
||||
|
||||
// 切换语音/文本模式
|
||||
const toggleVoiceMode = () => {
|
||||
if (!isSpeechRecognitionSupported.value) return;
|
||||
if (speechProvider === "app" && isVoiceMode.value) {
|
||||
stopActiveVoiceRecognition({ shouldSend: false });
|
||||
}
|
||||
isVoiceMode.value = !isVoiceMode.value;
|
||||
};
|
||||
|
||||
// 处理语音按钮按下开始
|
||||
const handleVoiceTouchStart = () => {
|
||||
if (!isSpeechRecognitionSupported.value) return;
|
||||
if (isRecording.value || isAppSpeechStarting.value || visibleWaveBtn.value) return;
|
||||
|
||||
isVoicePressing.value = true;
|
||||
|
||||
try {
|
||||
if (speechProvider === "wechat") {
|
||||
if (!manager) return;
|
||||
manager.start({ lang: "zh_CN" });
|
||||
showRecordingUI();
|
||||
} else if (speechProvider === "app") {
|
||||
if (!appSpeechOptions.apikey) {
|
||||
showToast("请先配置语音识别API Key");
|
||||
return;
|
||||
}
|
||||
|
||||
appRecognizedText.value = "";
|
||||
inputMessage.value = "";
|
||||
isAppWaitingToSend.value = false;
|
||||
hasSentAppRecognition = false;
|
||||
clearAppStopFallback();
|
||||
isAppSpeechStarting.value = true;
|
||||
|
||||
const appSpeech = appSpeechRef.value;
|
||||
if (!appSpeech || typeof appSpeech.start !== "function") {
|
||||
isAppSpeechStarting.value = false;
|
||||
showToast("语音组件未初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
appSpeech.start();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("record start error:", err);
|
||||
isAppSpeechStarting.value = false;
|
||||
// 保底清理
|
||||
resetUI();
|
||||
}
|
||||
};
|
||||
|
||||
const hasActiveVoiceRecognition = () => {
|
||||
return (
|
||||
isVoicePressing.value ||
|
||||
isRecording.value ||
|
||||
isAppSpeechStarting.value ||
|
||||
visibleWaveBtn.value
|
||||
);
|
||||
};
|
||||
|
||||
const stopActiveVoiceRecognition = ({ shouldSend = true } = {}) => {
|
||||
isVoicePressing.value = false;
|
||||
|
||||
if (!isSpeechRecognitionSupported.value) {
|
||||
resetUI();
|
||||
return;
|
||||
}
|
||||
|
||||
if (speechProvider === "app") {
|
||||
const wasStarting = isAppSpeechStarting.value;
|
||||
const wasRecording = isRecording.value;
|
||||
|
||||
if (!wasStarting && !wasRecording) {
|
||||
if (!shouldSend) {
|
||||
isAppWaitingToSend.value = false;
|
||||
clearAppStopFallback();
|
||||
resetAppSpeechComponent();
|
||||
}
|
||||
resetUI();
|
||||
return false;
|
||||
}
|
||||
|
||||
isAppSpeechStarting.value = false;
|
||||
|
||||
try {
|
||||
appSpeechRef.value?.stop?.();
|
||||
if (wasRecording && shouldSend) {
|
||||
scheduleAppRecognizedTextSend();
|
||||
} else {
|
||||
isAppWaitingToSend.value = false;
|
||||
clearAppStopFallback();
|
||||
resetAppSpeechComponent();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("record stop error:", err);
|
||||
resetAppSpeechComponent();
|
||||
} finally {
|
||||
resetUI();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果本地状态不是录音中,也确保 UI 恢复
|
||||
if (!isRecording.value) {
|
||||
if (recordingWaveBtnRef.value) {
|
||||
recordingWaveBtnRef.value.stopAnimation();
|
||||
}
|
||||
visibleWaveBtn.value = false;
|
||||
if (watchDogTimer) {
|
||||
clearTimeout(watchDogTimer);
|
||||
watchDogTimer = null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
manager?.stop?.();
|
||||
} catch (err) {
|
||||
console.error("record stop error:", err);
|
||||
} finally {
|
||||
// 无论 stop 是否抛错,都保证 UI 恢复
|
||||
resetUI();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 处理语音按钮长按结束
|
||||
const handleVoiceTouchEnd = () => {
|
||||
stopActiveVoiceRecognition({ shouldSend: true });
|
||||
};
|
||||
|
||||
const handleVoiceTouchEndFromContainer = () => {
|
||||
if (speechProvider !== "app") return;
|
||||
if (!hasActiveVoiceRecognition()) return;
|
||||
stopActiveVoiceRecognition({ shouldSend: true });
|
||||
};
|
||||
|
||||
// 处理发送原语音
|
||||
const initRecord = () => {
|
||||
if (!manager) return;
|
||||
|
||||
manager.onRecognize = (res) => {
|
||||
let text = res.result || "";
|
||||
inputMessage.value = text;
|
||||
};
|
||||
// 识别结束事件
|
||||
manager.onStop = (res) => {
|
||||
console.log(res, 37);
|
||||
let text = (res && res.result) || "";
|
||||
|
||||
// 保证 UI 恢复(防止未走 handleVoiceTouchEnd)
|
||||
resetUI();
|
||||
|
||||
if (text === "") {
|
||||
console.log("没有说话");
|
||||
return;
|
||||
}
|
||||
|
||||
inputMessage.value = text;
|
||||
// 在语音识别完成后发送消息
|
||||
emit("send", text);
|
||||
};
|
||||
|
||||
// 错误处理,确保 UI 重置
|
||||
manager.onError = (err) => {
|
||||
console.error("record manager error", err);
|
||||
resetUI();
|
||||
};
|
||||
};
|
||||
|
||||
const getAppSpeechText = (res) => {
|
||||
return (
|
||||
res?.sentence?.text ||
|
||||
res?.text ||
|
||||
res?.result ||
|
||||
res?.payload?.output?.sentence?.text ||
|
||||
""
|
||||
);
|
||||
};
|
||||
|
||||
const sendAppRecognizedText = () => {
|
||||
if (hasSentAppRecognition) return false;
|
||||
|
||||
const text = getPendingAppSpeechText();
|
||||
if (!text) {
|
||||
console.log("没有说话");
|
||||
return false;
|
||||
}
|
||||
|
||||
hasSentAppRecognition = true;
|
||||
isAppWaitingToSend.value = false;
|
||||
clearAppStopFallback();
|
||||
inputMessage.value = text;
|
||||
emit("send", text);
|
||||
appRecognizedText.value = "";
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleAppSpeechResult = (res) => {
|
||||
if (hasSentAppRecognition) return;
|
||||
|
||||
const text = getAppSpeechText(res);
|
||||
if (!text) return;
|
||||
|
||||
appRecognizedText.value = text;
|
||||
inputMessage.value = text;
|
||||
|
||||
if (isAppWaitingToSend.value) {
|
||||
sendAppRecognizedText();
|
||||
}
|
||||
};
|
||||
|
||||
const handleAppSpeechChange = (msg) => {
|
||||
if (!msg || !msg.status) return;
|
||||
|
||||
if (msg.status === "START") {
|
||||
isAppSpeechStarting.value = false;
|
||||
if (!isVoicePressing.value) {
|
||||
appSpeechRef.value?.stop?.();
|
||||
resetAppSpeechComponent();
|
||||
resetUI();
|
||||
return;
|
||||
}
|
||||
|
||||
showRecordingUI();
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.status === "STOP") {
|
||||
isAppSpeechStarting.value = false;
|
||||
resetUI();
|
||||
if (!sendAppRecognizedText() && !isAppWaitingToSend.value) {
|
||||
resetAppSpeechComponent();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.status === "FINISH") {
|
||||
isAppSpeechStarting.value = false;
|
||||
resetUI();
|
||||
sendAppRecognizedText();
|
||||
resetAppSpeechComponent();
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.status === "CLOSE") {
|
||||
isAppSpeechStarting.value = false;
|
||||
resetUI();
|
||||
if (isAppWaitingToSend.value) {
|
||||
sendAppRecognizedText();
|
||||
}
|
||||
resetAppSpeechComponent();
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.status === "ERROR") {
|
||||
console.error("app speech recognition error", msg.msg);
|
||||
isAppSpeechStarting.value = false;
|
||||
isAppWaitingToSend.value = false;
|
||||
clearAppStopFallback();
|
||||
resetUI();
|
||||
resetAppSpeechComponent();
|
||||
}
|
||||
};
|
||||
|
||||
// 监听键盘高度变化
|
||||
onMounted(() => {
|
||||
initRecord();
|
||||
nextTick(adjustTextareaHeight);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (manager) {
|
||||
try {
|
||||
manager.stop && manager.stop();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
manager.onRecognize = null;
|
||||
manager.onStop = null;
|
||||
manager.onError = null;
|
||||
}
|
||||
if (appSpeechRef.value) {
|
||||
try {
|
||||
appSpeechRef.value.stop && appSpeechRef.value.stop();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
clearAppStopFallback();
|
||||
isAppSpeechStarting.value = false;
|
||||
isAppWaitingToSend.value = false;
|
||||
isVoicePressing.value = false;
|
||||
resetUI();
|
||||
});
|
||||
|
||||
const hideKeyboardAfterSend = () => {
|
||||
isFocused.value = false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user