125 lines
4.9 KiB
JavaScript
125 lines
4.9 KiB
JavaScript
function createVoiceConversationCapture({
|
|
Recognition, elements, transcriptStore = null, getLogin = () => '',
|
|
createEvent = name => new Event(name, {bubbles:true}),
|
|
}) {
|
|
const supported = typeof Recognition === 'function';
|
|
let recognition = null;
|
|
let target = '';
|
|
let generation = 0;
|
|
elements.root.hidden = !supported;
|
|
if (!supported) {
|
|
elements.status.textContent = 'Voice capture is unavailable; type the reply instead.';
|
|
}
|
|
|
|
const normalize = value => String(value || '').replace(/\s+/g, ' ').trim();
|
|
const login = () => String(getLogin() || '').trim();
|
|
|
|
function showReview(value, recovered = false) {
|
|
const transcript = normalize(value).slice(0, 10000);
|
|
elements.transcript.value = transcript;
|
|
elements.review.hidden = false;
|
|
elements.append.textContent = elements.draft.value.trim() ? 'Append to draft' : 'Use transcript';
|
|
elements.replace.hidden = !elements.draft.value.trim();
|
|
elements.status.textContent = recovered ?
|
|
'Recovered transcript. Review it before using or discarding it.' :
|
|
'Transcript ready. Review it before using it.';
|
|
if (!recovered && transcriptStore && login() && target) {
|
|
void transcriptStore.save(login(), transcript, target);
|
|
}
|
|
}
|
|
|
|
async function open(nextTarget) {
|
|
cancel();
|
|
target = String(nextTarget || '').trim();
|
|
const opening = ++generation;
|
|
elements.transcript.value = '';
|
|
elements.review.hidden = true;
|
|
elements.status.textContent = '';
|
|
if (!supported || !target || !transcriptStore || !login()) return;
|
|
const recovered = normalize(await transcriptStore.load(login(), target)).slice(0, 10000);
|
|
if (opening === generation && recovered) showReview(recovered, true);
|
|
}
|
|
|
|
function clearCheckpoint() {
|
|
if (transcriptStore && login() && target) void transcriptStore.clear(login(), target);
|
|
}
|
|
|
|
function commit(replace) {
|
|
const transcript = normalize(elements.transcript.value).slice(0, 10000);
|
|
elements.draft.value = replace ? transcript :
|
|
[elements.draft.value.trim(), transcript].filter(Boolean).join('\n\n').slice(0, 10000);
|
|
elements.draft.dispatchEvent(createEvent('input'));
|
|
clearCheckpoint();
|
|
elements.review.hidden = true;
|
|
elements.status.textContent = 'Transcript added. Review the reply before sending.';
|
|
}
|
|
|
|
elements.start.addEventListener('click', () => {
|
|
if (!supported || !target) return;
|
|
const sessionGeneration = generation;
|
|
const currentRecognition = new Recognition();
|
|
recognition = currentRecognition;
|
|
recognition.continuous = true;
|
|
recognition.interimResults = false;
|
|
recognition.onresult = event => {
|
|
if (recognition !== currentRecognition || generation !== sessionGeneration) return;
|
|
const finalText = Array.from(event.results || []).slice(event.resultIndex ?? 0)
|
|
.filter(result => result.isFinal)
|
|
.map(result => result[0]?.transcript || '').join(' ');
|
|
if (normalize(finalText)) {
|
|
showReview([elements.transcript.value, finalText].filter(Boolean).join(' '));
|
|
}
|
|
};
|
|
recognition.onerror = event => {
|
|
if (recognition !== currentRecognition || generation !== sessionGeneration) return;
|
|
elements.start.hidden = false;
|
|
elements.stop.hidden = true;
|
|
elements.status.textContent = ['not-allowed', 'service-not-allowed'].includes(event.error) ?
|
|
'Microphone permission was denied. Your draft and transcript are unchanged.' :
|
|
'Voice capture stopped. Your draft and transcript are unchanged.';
|
|
};
|
|
recognition.onend = () => {
|
|
if (recognition !== currentRecognition || generation !== sessionGeneration) return;
|
|
recognition = null;
|
|
elements.start.hidden = false;
|
|
elements.stop.hidden = true;
|
|
};
|
|
try {
|
|
recognition.start();
|
|
} catch (_) {
|
|
recognition = null;
|
|
elements.start.hidden = false;
|
|
elements.stop.hidden = true;
|
|
elements.status.textContent = 'Voice capture could not start. Your draft and transcript are unchanged.';
|
|
return;
|
|
}
|
|
elements.start.hidden = true;
|
|
elements.stop.hidden = false;
|
|
elements.status.textContent = 'Listening… Tap stop when you are finished.';
|
|
});
|
|
elements.stop.addEventListener('click', () => {
|
|
recognition?.stop();
|
|
elements.status.textContent = 'Finishing transcript…';
|
|
});
|
|
elements.append.addEventListener('click', () => commit(false));
|
|
elements.replace.addEventListener('click', () => commit(true));
|
|
elements.discard.addEventListener('click', () => {
|
|
clearCheckpoint();
|
|
elements.transcript.value = '';
|
|
elements.review.hidden = true;
|
|
elements.status.textContent = 'Transcript discarded.';
|
|
});
|
|
|
|
function cancel() {
|
|
generation += 1;
|
|
recognition?.abort();
|
|
recognition = null;
|
|
elements.start.hidden = false;
|
|
elements.stop.hidden = true;
|
|
}
|
|
|
|
return {supported, open, cancel};
|
|
}
|
|
|
|
if (typeof module !== 'undefined') module.exports = {createVoiceConversationCapture};
|