112 lines
4.8 KiB
JavaScript
112 lines
4.8 KiB
JavaScript
function mapVoiceTranscript(value) {
|
|
const transcript = String(value || '').replace(/\s+/g, ' ').trim();
|
|
if (!transcript) return {title:'', body:''};
|
|
const sentence = transcript.match(/^(.+?[.!?])(?:\s+|$)(.*)$/);
|
|
const title = (sentence ? sentence[1] : transcript).trim().slice(0, 255);
|
|
const body = (sentence ? sentence[2] : '').trim().slice(0, 10000);
|
|
return {title, body};
|
|
}
|
|
|
|
function createVoiceIssueCapture({
|
|
Recognition, elements, createEvent = name => new Event(name, {bubbles:true}),
|
|
transcriptStore = null, getLogin = () => '',
|
|
}) {
|
|
const supported = typeof Recognition === 'function';
|
|
const login = String(getLogin() || '').trim();
|
|
const ready = supported && login && transcriptStore ? Promise.resolve(transcriptStore.load(login)).then(value => {
|
|
const recovered = String(value || '').replace(/\s+/g, ' ').trim().slice(0, 10255);
|
|
if (recovered) showReview(recovered, {persist:false, recovered:true});
|
|
}) : Promise.resolve();
|
|
elements.root.hidden = !supported;
|
|
if (!supported) {
|
|
elements.status.textContent = 'Voice capture is unavailable; type the title and note instead.';
|
|
return {supported:false, ready, cancel() {}};
|
|
}
|
|
let recognition = null;
|
|
|
|
function showReview(value, {persist = true, recovered = false} = {}) {
|
|
elements.transcript.value = value;
|
|
const activeLogin = String(getLogin() || '').trim();
|
|
if (persist && activeLogin && transcriptStore) void transcriptStore.save(activeLogin, value);
|
|
const hasDraft = Boolean(elements.title.value.trim() || elements.body.value.trim());
|
|
elements.append.textContent = hasDraft ? 'Append to draft' : 'Use transcript';
|
|
elements.replace.hidden = !hasDraft;
|
|
elements.review.hidden = false;
|
|
elements.status.textContent = recovered ?
|
|
'Recovered transcript. Review it before using or discarding it.' :
|
|
'Transcript ready. Review it before using it.';
|
|
}
|
|
|
|
elements.start.addEventListener('click', () => {
|
|
recognition = new Recognition();
|
|
recognition.continuous = true;
|
|
recognition.interimResults = false;
|
|
recognition.onresult = event => {
|
|
const finalText = Array.from(event.results || [])
|
|
.filter(result => result.isFinal)
|
|
.map(result => result[0]?.transcript || '').join(' ').replace(/\s+/g, ' ').trim();
|
|
if (finalText) {
|
|
const accumulated = [elements.transcript.value.trim(), finalText]
|
|
.filter(Boolean).join(' ').replace(/\s+/g, ' ').trim().slice(0, 10255);
|
|
showReview(accumulated);
|
|
}
|
|
};
|
|
recognition.onerror = event => {
|
|
elements.start.hidden = false;
|
|
elements.stop.hidden = true;
|
|
elements.status.textContent = event.error === 'not-allowed' || event.error === 'service-not-allowed' ?
|
|
'Microphone permission was denied. Your draft and transcript are unchanged.' :
|
|
'Voice capture stopped. Your draft and transcript are unchanged.';
|
|
};
|
|
recognition.onend = () => {
|
|
elements.start.hidden = false;
|
|
elements.stop.hidden = true;
|
|
};
|
|
recognition.start();
|
|
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…';
|
|
});
|
|
|
|
function clearCheckpoint() {
|
|
const activeLogin = String(getLogin() || '').trim();
|
|
if (activeLogin && transcriptStore) void transcriptStore.clear(activeLogin);
|
|
}
|
|
|
|
function commit(mode) {
|
|
const mapped = mapVoiceTranscript(elements.transcript.value);
|
|
if (mode === 'replace') {
|
|
elements.title.value = mapped.title;
|
|
elements.body.value = mapped.body;
|
|
} else if (elements.title.value.trim()) {
|
|
elements.body.value = [elements.body.value.trim(), mapped.title, mapped.body]
|
|
.filter(Boolean).join('\n\n').slice(0, 10000);
|
|
} else {
|
|
elements.title.value = mapped.title;
|
|
elements.body.value = [elements.body.value.trim(), mapped.body]
|
|
.filter(Boolean).join('\n\n').slice(0, 10000);
|
|
}
|
|
elements.title.dispatchEvent(createEvent('input'));
|
|
elements.body.dispatchEvent(createEvent('input'));
|
|
clearCheckpoint();
|
|
elements.review.hidden = true;
|
|
elements.status.textContent = 'Transcript added. Review the title and note before saving.';
|
|
}
|
|
elements.append.addEventListener('click', () => commit('append'));
|
|
elements.replace.addEventListener('click', () => commit('replace'));
|
|
elements.discard?.addEventListener('click', () => {
|
|
clearCheckpoint();
|
|
elements.transcript.value = '';
|
|
elements.review.hidden = true;
|
|
elements.status.textContent = 'Recovered transcript discarded.';
|
|
});
|
|
|
|
return {supported:true, ready, cancel() { recognition?.abort(); recognition = null; }};
|
|
}
|
|
|
|
if (typeof module !== 'undefined') module.exports = {mapVoiceTranscript, createVoiceIssueCapture};
|