新增H5长按录音并接入百度语音转文字
This commit is contained in:
@@ -8,6 +8,7 @@ const flush = () => new Promise(setImmediate);
|
||||
async function customerPage(pageOptions = {}){
|
||||
const elements = new Map(), listeners = {}, windowListeners = {}, viewportListeners = {}, intervals = new Map(), timers = new Map(), calls = [], hooks = {};
|
||||
let intervalId = 0, timerId = 0, nextConvId = 1;
|
||||
let voiceOptions;
|
||||
class Element {
|
||||
constructor(){
|
||||
this.children = []; this.attributes = {}; this.value = ''; this.style = {};
|
||||
@@ -97,14 +98,18 @@ async function customerPage(pageOptions = {}){
|
||||
return {ok:true, json:async () => data};
|
||||
},
|
||||
});
|
||||
for(const file of ['support.js', 'chat.js']){
|
||||
for(const file of ['support.js', 'voice.js', 'chat.js']){
|
||||
vm.runInContext(fs.readFileSync(path.join(__dirname, '../../static/js', file), 'utf8'), context, {filename:file});
|
||||
if(file === 'voice.js'){
|
||||
const createVoice=context.ChatVoice.create;
|
||||
context.ChatVoice.create=options => {voiceOptions=options; return createVoice(options);};
|
||||
}
|
||||
}
|
||||
listeners.DOMContentLoaded();
|
||||
await flush();
|
||||
if(pageOptions.human !== false) await context.window.showHuman();
|
||||
await flush();
|
||||
return {context, element, calls, hooks, quickButtons,
|
||||
return {context, element, calls, hooks, quickButtons, voiceOptions,
|
||||
resize:async height => {
|
||||
if(context.window.visualViewport) context.window.visualViewport.height=height;
|
||||
(windowListeners.resize || []).forEach(callback => callback());
|
||||
@@ -628,3 +633,56 @@ test('clearing a sent draft restores a single-row composer instead of sizing to
|
||||
assert.equal(input.value,'');
|
||||
assert.equal(input.style.height,'');
|
||||
});
|
||||
|
||||
test('recognized voice uses the normal AI and human send routes with a visible user message', async () => {
|
||||
for(const human of [false,true]){
|
||||
const app=await customerPage({human,width:390});
|
||||
await app.voiceOptions.onText('语音识别的物流问题');
|
||||
await flush();
|
||||
const call=app.calls.find(call=>call.url===(human ? '/api/human/send' : '/stream_chat'));
|
||||
assert.ok(call);
|
||||
assert.equal(human ? call.body.content : call.body.msg,'语音识别的物流问题');
|
||||
assert.match(app.element('chatDom').querySelector('.user-message').textContent,/语音识别的物流问题/);
|
||||
assert.equal(app.element('input-text').value,'');
|
||||
}
|
||||
});
|
||||
|
||||
test('a failed voice send keeps the transcript editable and a newer draft is not auto-submitted', async () => {
|
||||
const app=await customerPage({human:false,width:390});
|
||||
app.hooks['/api/new_chat']=()=>Promise.reject(new Error('offline'));
|
||||
await app.voiceOptions.onText('语音内容');
|
||||
assert.equal(app.element('input-text').value,'语音内容');
|
||||
assert.equal(app.element('.input-area').classList.contains('voice-mode'),false);
|
||||
app.element('input-text').value='新草稿';
|
||||
await app.voiceOptions.onText('后到的识别文字');
|
||||
assert.equal(app.element('input-text').value,'新草稿\n后到的识别文字');
|
||||
assert.equal(app.calls.filter(call=>call.url==='/stream_chat').length,0);
|
||||
});
|
||||
|
||||
test('a blocked retry retains its previous answer until a replacement can start', async () => {
|
||||
const app=await customerPage({human:false});
|
||||
await app.context.window.fillInput('之前的问题');await flush();
|
||||
const log=app.element('chatDom'), retry=log.querySelectorAll('.msg-action-btn').find(button=>button.dataset.action==='retry');
|
||||
app.hooks['/api/upload']=options=>new Promise((resolve,reject)=>{
|
||||
options.signal.addEventListener('abort',()=>reject(Object.assign(new Error(),{name:'AbortError'})));
|
||||
});
|
||||
app.element('upload-input').files=[{name:'pending.pdf'}];
|
||||
app.element('upload-input').onchange();await flush();
|
||||
await app.click(retry);
|
||||
assert.match(log.textContent,/测试 AI 回复/);
|
||||
assert.equal(app.calls.filter(call=>call.url==='/stream_chat').length,1);
|
||||
await app.runTimers(15000);
|
||||
await app.click(retry);await flush();
|
||||
assert.equal(app.calls.filter(call=>call.url==='/stream_chat').length,2);
|
||||
assert.equal(log.querySelectorAll('.msg-action-btn').filter(button=>button.dataset.action==='retry').length,1);
|
||||
});
|
||||
|
||||
test('an AI regeneration is not forwarded to a human session', async () => {
|
||||
const app=await customerPage({human:false});
|
||||
await app.context.window.fillInput('之前的问题');await flush();
|
||||
const retry=app.element('chatDom').querySelectorAll('.msg-action-btn').find(button=>button.dataset.action==='retry');
|
||||
await app.context.window.showHuman();await app.click(retry);
|
||||
assert.equal(app.calls.filter(call=>call.url==='/api/human/send').length,0);
|
||||
assert.match(app.element('chatDom').textContent,/测试 AI 回复/);
|
||||
assert.match(app.element('composer-status').textContent,/先返回 AI/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user