新增H5长按录音并接入百度语音转文字
This commit is contained in:
+70
-8
@@ -10,6 +10,7 @@ let uploadVersion = 0;
|
||||
let uploadController = null;
|
||||
let historyRequestVersion = 0;
|
||||
let followLatest = true;
|
||||
let voiceInput = null;
|
||||
let creatingConv = null;
|
||||
let sendPreparing = false;
|
||||
let conversationVersion = 0;
|
||||
@@ -239,6 +240,7 @@ const I18N = {
|
||||
copy: "复制",
|
||||
copied: "已复制",
|
||||
retry: "重答",
|
||||
retryAIOnly: "请先返回 AI 问答,再重答这条消息。",
|
||||
clickRetry: "点击重试",
|
||||
welcomeTitle: "物流问题,随时为您解答",
|
||||
welcomeSub: "查询订单状态、了解专线报价,或联系人工客服。",
|
||||
@@ -256,6 +258,18 @@ const I18N = {
|
||||
uploading: "附件正在上传,请稍候…",
|
||||
uploadFailed: "附件上传失败,请重新选择文件",
|
||||
serviceUnavailable: "服务暂时不可用,请稍后重试,或联系人工客服。",
|
||||
voiceInput: "语音输入", voiceKeyboard: "键盘输入", voiceHold: "按住说话",
|
||||
voiceRelease: "松开发送,上滑取消", voicePreparing: "正在准备麦克风…",
|
||||
voiceCancelTip: "正在录音,松开发送,上滑取消", voiceCancelRelease: "松开取消发送",
|
||||
voiceTranscribing: "正在转文字…", voiceCancelled: "已取消语音",
|
||||
voiceUnavailable: "语音输入暂不可用,请使用文字输入。",
|
||||
voiceUnsupported: "当前浏览器不支持录音,请使用文字输入或在其他浏览器打开。",
|
||||
voiceHttps: "语音输入需要 HTTPS 安全连接,请使用安全访问地址。",
|
||||
voicePermission: "未获得麦克风权限,请允许录音后重试。", voiceNoMic: "没有找到麦克风,请使用文字输入。",
|
||||
voiceTooShort: "按住时间太短,请按住按钮说话后再松开。", voiceTooLarge: "录音过大,请缩短后重试。",
|
||||
voiceEmpty: "没有识别到文字,请靠近麦克风重试。", voiceFailed: "语音输入失败,请重试或使用文字输入。",
|
||||
voiceTimeout: "语音处理超时,请重试。", voiceWait: "请等待当前操作完成后,再使用语音输入。",
|
||||
voiceDraft: "输入框还有未发送内容,请先发送或清空后再使用语音。",
|
||||
attachmentReady: "已准备好,发送后将附在消息中",
|
||||
removeAttachment: "移除待发送附件",
|
||||
you: "您",
|
||||
@@ -336,6 +350,7 @@ const I18N = {
|
||||
copy: "Copy",
|
||||
copied: "Copied",
|
||||
retry: "Regenerate",
|
||||
retryAIOnly: "Return to AI chat before regenerating this answer.",
|
||||
clickRetry: "Retry",
|
||||
welcomeTitle: "Your next shipment, made clearer.",
|
||||
welcomeSub: "Track a parcel, explore freight options, or speak to our team.",
|
||||
@@ -353,6 +368,20 @@ const I18N = {
|
||||
uploading: "Uploading your attachment…",
|
||||
uploadFailed: "Upload failed. Please select the file again.",
|
||||
serviceUnavailable: "Service is temporarily unavailable. Please try again later or contact support.",
|
||||
voiceInput: "Voice input", voiceKeyboard: "Keyboard input", voiceHold: "Hold to speak",
|
||||
voiceRelease: "Release to send, slide up to cancel", voicePreparing: "Preparing the microphone…",
|
||||
voiceCancelTip: "Recording · Release to send · Slide up to cancel", voiceCancelRelease: "Release to cancel",
|
||||
voiceTranscribing: "Converting speech to text…", voiceCancelled: "Voice input cancelled",
|
||||
voiceUnavailable: "Voice input is unavailable. Please type your message.",
|
||||
voiceUnsupported: "This browser cannot record audio. Please type or open another browser.",
|
||||
voiceHttps: "Voice input requires a secure HTTPS connection.",
|
||||
voicePermission: "Microphone permission is required. Please allow recording and try again.",
|
||||
voiceNoMic: "No microphone found. Please type your message.",
|
||||
voiceTooShort: "Hold the button while speaking, then release.", voiceTooLarge: "The recording is too large. Please record a shorter message.",
|
||||
voiceEmpty: "No speech was recognized. Please try again closer to the microphone.",
|
||||
voiceFailed: "Voice input failed. Please try again or type your message.", voiceTimeout: "Voice input timed out. Please try again.",
|
||||
voiceWait: "Please wait for the current operation to finish before recording.",
|
||||
voiceDraft: "Please send or clear your existing draft before recording.",
|
||||
attachmentReady: "Ready to attach to your next message",
|
||||
removeAttachment: "Remove pending attachment",
|
||||
you: "You",
|
||||
@@ -428,6 +457,7 @@ function applyLanguage(lang){
|
||||
const tg = document.querySelector(".toggle-divider");
|
||||
if(tg) tg.title = tg.getAttribute('aria-expanded') === 'false' ? dict.sideExpand : dict.sideCollapse;
|
||||
updateHumanMode();
|
||||
if(voiceInput) voiceInput.refresh();
|
||||
|
||||
// 欢迎页(若当前显示的是欢迎页则重绘)
|
||||
if(chatDom.querySelector(".ai-welcome-card")){
|
||||
@@ -788,6 +818,7 @@ function ensureConv(){
|
||||
}
|
||||
|
||||
function resetConversation(id){
|
||||
if(voiceInput) voiceInput.cancel();
|
||||
if(cancelActive){ cancelActive(); cancelActive = null; }
|
||||
if(sseInstance){ sseInstance.close(); sseInstance = null; }
|
||||
conversationVersion += 1;
|
||||
@@ -1083,6 +1114,7 @@ async function humanRequest(path, data){
|
||||
}
|
||||
|
||||
window.showHuman = async function(){
|
||||
if(voiceInput) voiceInput.cancel();
|
||||
if(openingConv === 'clearing') return;
|
||||
if(humanRequestToken || humanClosing) return;
|
||||
if(window.humanSid && humanConvId === currentConvId){ updateHumanMode(); return; }
|
||||
@@ -1152,10 +1184,12 @@ async function sendHumanMessage(content){
|
||||
if(window.__pendingImg === attachment) clearAttachment();
|
||||
setComposerStatus(t(humanStatus === 'active' ? 'humanSent' : 'humanWaiting'));
|
||||
inputDom.focus();
|
||||
return true;
|
||||
}catch(error){
|
||||
if(version !== conversationVersion || humanSendingToken !== token) return;
|
||||
if(error.status === 404 || error.status === 409) stopHumanMode();
|
||||
setComposerStatus(t('humanSendFailed'), true);
|
||||
return false;
|
||||
}finally{
|
||||
if(humanSendingToken === token){ humanSendingToken = null; updateHumanMode(); }
|
||||
}
|
||||
@@ -1348,6 +1382,14 @@ function mdToPlain(md){
|
||||
.replace(/“|”/g, '"');
|
||||
}
|
||||
|
||||
async function retryAnswer(bubble, question){
|
||||
if(window.humanSid || humanRequestToken){ setComposerStatus(t('retryAIOnly')); return; }
|
||||
if(await doSend(question)){
|
||||
const row = bubble.closest('.msg-row');
|
||||
if(row) row.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// 给一条回答挂上「复制 / 重答」按钮
|
||||
function attachActions(bubble, rawText, question, showRetry){
|
||||
const old = bubble.querySelector(".msg-actions");
|
||||
@@ -1390,11 +1432,7 @@ function attachActions(bubble, rawText, question, showRetry){
|
||||
retryBtn.className = "msg-action-btn";
|
||||
retryBtn.dataset.action = "retry";
|
||||
retryBtn.textContent = t("retry");
|
||||
retryBtn.onclick = function(){
|
||||
const row = bubble.closest(".msg-row");
|
||||
if(row) row.remove();
|
||||
doSend(question); // 传字符串 = 只重发,不重复插入提问气泡
|
||||
};
|
||||
retryBtn.onclick = function(){ return retryAnswer(bubble, question); };
|
||||
bar.appendChild(retryBtn);
|
||||
}
|
||||
|
||||
@@ -1443,6 +1481,7 @@ function renderFollowups(bubble){
|
||||
}
|
||||
|
||||
async function doSend(retryText){
|
||||
if(voiceInput && voiceInput.busy()){ setComposerStatus(t('voiceWait')); return false; }
|
||||
const isRetry = typeof retryText === "string";
|
||||
const sendWord = (isRetry ? retryText : inputDom.value).trim();
|
||||
var imgUrl = isRetry ? "" : (window.__pendingImg || "");
|
||||
@@ -1540,9 +1579,7 @@ async function doSend(retryText){
|
||||
setBusy(false);
|
||||
if(activeBubble === aiBubble) activeBubble = null;
|
||||
renderError(aiBubble, text || t("busyRetry"), function(){
|
||||
const row = aiBubble.closest(".msg-row");
|
||||
if(row) row.remove();
|
||||
doSend(fullContent);
|
||||
return retryAnswer(aiBubble, fullContent);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1672,6 +1709,7 @@ async function doSend(retryText){
|
||||
if(settled) return;
|
||||
fail(t("networkFail"));
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== 发送 / 停止 按钮状态 =====
|
||||
@@ -1792,6 +1830,30 @@ window.addEventListener("resize", function(){
|
||||
if(window.visualViewport) window.visualViewport.addEventListener('resize', syncChatViewport);
|
||||
syncChatViewport();
|
||||
|
||||
voiceInput = ChatVoice.create({
|
||||
toggle:document.getElementById('voice-toggle'), hold:document.getElementById('voice-hold'),
|
||||
panel:document.getElementById('voice-panel'), label:document.getElementById('voice-label'),
|
||||
clock:document.getElementById('voice-clock'), inputArea:document.querySelector('.input-area'), input:inputDom,
|
||||
text:t, status:setComposerStatus, language:() => LANG,
|
||||
context:() => conversationVersion + ':' + currentConvId + ':' + window.humanSid,
|
||||
canStart:function(){
|
||||
if(uploadPending || sendPreparing || window.__busy || openingConv !== null || humanRequestToken || humanSendingToken || humanClosing) return t('voiceWait');
|
||||
return inputDom.value.trim() ? t('voiceDraft') : '';
|
||||
},
|
||||
onText:async function(text){
|
||||
if(inputDom.value.trim()){
|
||||
inputDom.value += '\n' + text;
|
||||
autoGrow(); voiceInput.setMode(false); return;
|
||||
}
|
||||
inputDom.value = text;
|
||||
autoGrow();
|
||||
if(!await doSend()) voiceInput.setMode(false);
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', function(){
|
||||
if(!isNarrow()) voiceInput.setMode(false);
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-question-index]').forEach(function(button){
|
||||
button.addEventListener('click', function(){
|
||||
window.askNewTopic(t('quick')[Number(button.dataset.questionIndex)]);
|
||||
|
||||
Reference in New Issue
Block a user