新增agent语音输入控制、优化web界面ui布局和样式、优化摄像头控制响应
This commit is contained in:
1 parent
7896e90602
commit
2f8c3bfdca
11 files changed
+1415
-737
No files matched your search
@@ -0,0 +1,288 @@
|
||||
// 语音输入功能
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
const agentInput = document.getElementById('agentInput');
|
||||
const sendAgentBtn = document.getElementById('sendAgentBtn');
|
||||
const status = document.getElementById('status');
|
||||
|
||||
if (!agentInput || !sendAgentBtn || !sendAgentBtn.parentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const voiceBtn = document.createElement('button');
|
||||
voiceBtn.className = 'btn btn-voice';
|
||||
voiceBtn.id = 'voiceBtn';
|
||||
voiceBtn.type = 'button';
|
||||
voiceBtn.textContent = '🎤';
|
||||
voiceBtn.title = '按住说话,松开发送';
|
||||
sendAgentBtn.parentNode.insertBefore(voiceBtn, sendAgentBtn);
|
||||
|
||||
let recognition = null;
|
||||
let isRecordingVoice = false;
|
||||
let microphoneReady = false;
|
||||
let spacePressed = false;
|
||||
let shouldAutoSend = false;
|
||||
|
||||
function setStatusMessage(message) {
|
||||
if (status) {
|
||||
status.innerHTML = `<p>${message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateVoiceButton(recording) {
|
||||
voiceBtn.classList.toggle('recording', recording);
|
||||
voiceBtn.textContent = recording ? '🔴' : '🎤';
|
||||
}
|
||||
|
||||
function hasSecureVoiceContext() {
|
||||
return window.isSecureContext;
|
||||
}
|
||||
|
||||
function getSecureContextMessage() {
|
||||
return '语音输入需要在 HTTPS 或 localhost 环境下使用;当前地址无法申请麦克风权限。';
|
||||
}
|
||||
|
||||
function getRecognitionErrorMessage(error) {
|
||||
switch (error) {
|
||||
case 'not-allowed':
|
||||
case 'service-not-allowed':
|
||||
return '浏览器已阻止麦克风权限,请在地址栏站点设置中允许麦克风后刷新页面。';
|
||||
case 'audio-capture':
|
||||
return '没有检测到可用麦克风,请检查设备或系统录音权限。';
|
||||
case 'network':
|
||||
return '语音识别服务连接失败,请检查网络后重试。';
|
||||
case 'no-speech':
|
||||
return '没有识别到语音,请按住按钮后再说话。';
|
||||
case 'aborted':
|
||||
return '语音识别已取消。';
|
||||
default:
|
||||
return `语音识别失败:${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePermissionError(error) {
|
||||
const permissionDeniedNames = ['NotAllowedError', 'PermissionDeniedError', 'SecurityError'];
|
||||
const deviceMissingNames = ['NotFoundError', 'DevicesNotFoundError'];
|
||||
|
||||
if (permissionDeniedNames.includes(error.name)) {
|
||||
setStatusMessage('浏览器未授予麦克风权限,请在站点权限中允许麦克风后重试。');
|
||||
return;
|
||||
}
|
||||
|
||||
if (deviceMissingNames.includes(error.name)) {
|
||||
setStatusMessage('没有找到可用的麦克风设备,请检查设备连接。');
|
||||
return;
|
||||
}
|
||||
|
||||
setStatusMessage(`麦克风初始化失败:${error.message || error.name}`);
|
||||
}
|
||||
|
||||
async function ensureMicrophonePermission() {
|
||||
if (microphoneReady) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!hasSecureVoiceContext()) {
|
||||
setStatusMessage(getSecureContextMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
// 某些支持 SpeechRecognition 的浏览器不会暴露 getUserMedia;
|
||||
// 这种情况下交给 recognition.start() 自己触发权限请求。
|
||||
microphoneReady = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
microphoneReady = true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('麦克风权限申请失败:', error);
|
||||
handlePermissionError(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function initSpeechRecognition() {
|
||||
if (!SpeechRecognition) {
|
||||
return;
|
||||
}
|
||||
|
||||
recognition = new SpeechRecognition();
|
||||
recognition.continuous = false;
|
||||
recognition.interimResults = true;
|
||||
recognition.lang = 'zh-CN';
|
||||
|
||||
recognition.onstart = function() {
|
||||
isRecordingVoice = true;
|
||||
updateVoiceButton(true);
|
||||
setStatusMessage('正在聆听,请开始说话...');
|
||||
};
|
||||
|
||||
recognition.onresult = function(event) {
|
||||
let finalTranscript = '';
|
||||
let interimTranscript = '';
|
||||
|
||||
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||
const transcript = event.results[i][0].transcript;
|
||||
if (event.results[i].isFinal) {
|
||||
finalTranscript += transcript;
|
||||
} else {
|
||||
interimTranscript += transcript;
|
||||
}
|
||||
}
|
||||
|
||||
if (finalTranscript) {
|
||||
agentInput.value = finalTranscript.trim();
|
||||
} else if (interimTranscript) {
|
||||
agentInput.value = interimTranscript.trim();
|
||||
}
|
||||
};
|
||||
|
||||
recognition.onerror = function(event) {
|
||||
console.error('语音识别错误:', event.error);
|
||||
isRecordingVoice = false;
|
||||
updateVoiceButton(false);
|
||||
|
||||
if (event.error !== 'aborted') {
|
||||
setStatusMessage(getRecognitionErrorMessage(event.error));
|
||||
}
|
||||
};
|
||||
|
||||
recognition.onend = function() {
|
||||
const hasText = Boolean(agentInput.value.trim());
|
||||
isRecordingVoice = false;
|
||||
updateVoiceButton(false);
|
||||
|
||||
if (hasText && shouldAutoSend) {
|
||||
window.setTimeout(function() {
|
||||
sendAgentBtn.click();
|
||||
}, 300);
|
||||
} else if (!hasText) {
|
||||
setStatusMessage('语音识别已结束。');
|
||||
}
|
||||
|
||||
shouldAutoSend = false;
|
||||
};
|
||||
}
|
||||
|
||||
async function startVoiceRecognition() {
|
||||
if (!SpeechRecognition) {
|
||||
setStatusMessage('当前浏览器不支持语音识别,请使用最新版 Chrome 或 Edge。');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRecordingVoice) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!recognition) {
|
||||
initSpeechRecognition();
|
||||
}
|
||||
|
||||
const permissionReady = await ensureMicrophonePermission();
|
||||
if (!permissionReady || !recognition) {
|
||||
return;
|
||||
}
|
||||
|
||||
shouldAutoSend = true;
|
||||
|
||||
try {
|
||||
recognition.start();
|
||||
} catch (error) {
|
||||
console.error('启动语音识别失败:', error);
|
||||
|
||||
if (error.name === 'InvalidStateError') {
|
||||
try {
|
||||
recognition.stop();
|
||||
} catch (stopError) {
|
||||
console.error('停止语音识别失败:', stopError);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setStatusMessage(`启动语音识别失败:${error.message || error.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
function stopVoiceRecognition() {
|
||||
if (recognition && isRecordingVoice) {
|
||||
recognition.stop();
|
||||
}
|
||||
}
|
||||
|
||||
if (!SpeechRecognition) {
|
||||
voiceBtn.disabled = true;
|
||||
voiceBtn.title = '当前浏览器不支持语音识别';
|
||||
setStatusMessage('当前浏览器不支持语音识别,请使用最新版 Chrome 或 Edge。');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasSecureVoiceContext()) {
|
||||
voiceBtn.title = '当前页面不是 HTTPS 或 localhost,浏览器不会授予麦克风权限';
|
||||
setStatusMessage(getSecureContextMessage());
|
||||
}
|
||||
|
||||
voiceBtn.addEventListener('pointerdown', async function(event) {
|
||||
if (event.pointerType === 'mouse' && event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
shouldAutoSend = true;
|
||||
|
||||
if (voiceBtn.setPointerCapture) {
|
||||
try {
|
||||
voiceBtn.setPointerCapture(event.pointerId);
|
||||
} catch (error) {
|
||||
console.debug('setPointerCapture skipped:', error);
|
||||
}
|
||||
}
|
||||
|
||||
await startVoiceRecognition();
|
||||
});
|
||||
|
||||
voiceBtn.addEventListener('pointerup', function(event) {
|
||||
event.preventDefault();
|
||||
stopVoiceRecognition();
|
||||
});
|
||||
|
||||
voiceBtn.addEventListener('pointercancel', function() {
|
||||
stopVoiceRecognition();
|
||||
});
|
||||
|
||||
voiceBtn.addEventListener('lostpointercapture', function() {
|
||||
stopVoiceRecognition();
|
||||
});
|
||||
|
||||
voiceBtn.addEventListener('contextmenu', function(event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.repeat) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.code === 'Space' && event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') {
|
||||
event.preventDefault();
|
||||
|
||||
if (!spacePressed) {
|
||||
spacePressed = true;
|
||||
shouldAutoSend = true;
|
||||
startVoiceRecognition();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', function(event) {
|
||||
if (event.code === 'Space' && event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') {
|
||||
event.preventDefault();
|
||||
spacePressed = false;
|
||||
stopVoiceRecognition();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user