|
@@ -65,11 +65,22 @@
|
|
|
<!-- 输入框和发送按钮 -->
|
|
<!-- 输入框和发送按钮 -->
|
|
|
<div class="input-section">
|
|
<div class="input-section">
|
|
|
<input
|
|
<input
|
|
|
- type="text"
|
|
|
|
|
- v-model="prompt"
|
|
|
|
|
- placeholder="问我任何问题..."
|
|
|
|
|
- @keyup.enter="handleSendByKeydown"
|
|
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ v-model="prompt"
|
|
|
|
|
+ placeholder="问我任何问题..."
|
|
|
|
|
+ @keyup.enter="handleSendByKeydown"
|
|
|
/>
|
|
/>
|
|
|
|
|
+ <!-- 添加语音输入按钮 -->
|
|
|
|
|
+ <button
|
|
|
|
|
+ @click="toggleSpeechInput"
|
|
|
|
|
+ class="speech-btn"
|
|
|
|
|
+ :class="{ 'recording': isRecording }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <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>
|
|
|
|
|
+ </button>
|
|
|
<button @click="handleSendByButton">发送</button>
|
|
<button @click="handleSendByButton">发送</button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -111,10 +122,20 @@ import DefaultMessage from "@/components/DefaultMessage/index.vue";
|
|
|
// import painting from '@/assets/icon/painting.png'
|
|
// import painting from '@/assets/icon/painting.png'
|
|
|
// import human from '@/assets/icon/human.png'
|
|
// import human from '@/assets/icon/human.png'
|
|
|
|
|
|
|
|
|
|
+// 语音图标
|
|
|
|
|
+import { Microphone, Mute } from "@element-plus/icons-vue";
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
import LeftPanel from "@/components/LeftPanel.vue";
|
|
import LeftPanel from "@/components/LeftPanel.vue";
|
|
|
const leftPanelRef = ref(null);
|
|
const leftPanelRef = ref(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+// 语音输入响应式变量
|
|
|
|
|
+const isRecording = ref(false); // 录音状态
|
|
|
|
|
+const recognition = ref(null); // 语音识别实例
|
|
|
|
|
+const countdown = ref(0); // 倒计时剩余秒数
|
|
|
|
|
+const countdownTimer = ref(null); // 倒计时定时器
|
|
|
|
|
+
|
|
|
// 默认消息控制
|
|
// 默认消息控制
|
|
|
const showDefaultMessages = ref(true);
|
|
const showDefaultMessages = ref(true);
|
|
|
const handleDefaultMessageSelect = (message) => {
|
|
const handleDefaultMessageSelect = (message) => {
|
|
@@ -174,7 +195,7 @@ const textRoleRunning = ref(false); // Typing speed in milliseconds
|
|
|
const isComposing = ref(false); // 判断用户是否在输入
|
|
const isComposing = ref(false); // 判断用户是否在输入
|
|
|
const conversationInAbortController = ref(); // 对话进行中 abort 控制器(控制 stream 对话)
|
|
const conversationInAbortController = ref(); // 对话进行中 abort 控制器(控制 stream 对话)
|
|
|
const inputTimeout = ref(); // 处理输入中回车的定时器
|
|
const inputTimeout = ref(); // 处理输入中回车的定时器
|
|
|
-const prompt = ref(); // prompt
|
|
|
|
|
|
|
+const prompt = ref(''); // prompt
|
|
|
const enableContext = ref(true); // 是否开启上下文
|
|
const enableContext = ref(true); // 是否开启上下文
|
|
|
// 接收 Stream 消息
|
|
// 接收 Stream 消息
|
|
|
const receiveMessageFullText = ref("");
|
|
const receiveMessageFullText = ref("");
|
|
@@ -199,7 +220,89 @@ const getConversation = async (id) => {
|
|
|
activeConversationModelPath.value = personImage.value;
|
|
activeConversationModelPath.value = personImage.value;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-// =========== 【发送消息】相关 ===========
|
|
|
|
|
|
|
+// =========== 【语音录入】相关 ===========
|
|
|
|
|
+// 初始化语音识别
|
|
|
|
|
+const initSpeechRecognition = () => {
|
|
|
|
|
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
|
|
|
+ if (!SpeechRecognition) {
|
|
|
|
|
+ alert("当前浏览器不支持语音输入功能");
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 新增:识别器结束时清除定时器
|
|
|
|
|
+ instance.onend = () => {
|
|
|
|
|
+ clearInterval(countdownTimer.value);
|
|
|
|
|
+ isRecording.value = false;
|
|
|
|
|
+ countdown.value = 0;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ instance.onerror = (event) => {
|
|
|
|
|
+ console.error('语音识别错误:', event.error);
|
|
|
|
|
+ clearInterval(countdownTimer.value); // 出错时清除定时器
|
|
|
|
|
+ isRecording.value = false;
|
|
|
|
|
+ alert('语音输入失败,请重试');
|
|
|
|
|
+ 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);
|
|
|
|
|
+ alert('请允许麦克风权限以使用语音输入');
|
|
|
|
|
+ // 出错时重置状态
|
|
|
|
|
+ isRecording.value = false;
|
|
|
|
|
+ countdown.value = 0;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// =========== 【聊天对话】相关 ===========
|
|
|
|
|
|
|
|
/** 处理来自 keydown 的发送消息 */
|
|
/** 处理来自 keydown 的发送消息 */
|
|
|
const handleSendByKeydown = async (event) => {
|
|
const handleSendByKeydown = async (event) => {
|
|
@@ -725,6 +828,30 @@ onMounted(async () => {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
padding: rpx(10);
|
|
padding: rpx(10);
|
|
|
gap: rpx(10);
|
|
gap: rpx(10);
|
|
|
|
|
+
|
|
|
|
|
+ .speech-btn {
|
|
|
|
|
+ padding: rpx(5) rpx(10);
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border: 1px solid #ffce1b;
|
|
|
|
|
+ border-radius: rpx(5);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+
|
|
|
|
|
+ &.recording {
|
|
|
|
|
+ background: #ffeeba;
|
|
|
|
|
+ border-color: #ffc107;
|
|
|
|
|
+
|
|
|
|
|
+ .el-icon {
|
|
|
|
|
+ color: #dc3545;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-icon {
|
|
|
|
|
+ font-size: rpx(8);
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
.input-section input {
|
|
.input-section input {
|
|
|
flex: 1;
|
|
flex: 1;
|