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 || []) .slice(event.resultIndex ?? 0) .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; }; 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…'; }); 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};