From e96cb5bdaa8d78c1d401868ac49a64b975408f68 Mon Sep 17 00:00:00 2001 From: retoor Date: Sun, 29 Jun 2025 20:34:25 +0200 Subject: [PATCH] Working perfect. --- src/snek/static/stt.js | 232 ++++++++++++++++++----------------------- 1 file changed, 104 insertions(+), 128 deletions(-) diff --git a/src/snek/static/stt.js b/src/snek/static/stt.js index 21f72ab..da462f8 100644 --- a/src/snek/static/stt.js +++ b/src/snek/static/stt.js @@ -2,69 +2,60 @@ class STTButton extends HTMLElement { /** monitor target attribute so it can change on-the-fly */ static get observedAttributes() { return ['target']; } - simulateTypingWithEvents(element, text, delay = 100) { - let resolver = null - let promise = new Promise((resolve, reject)=>{ - resolver = resolve - }) - let index = 0; - - function triggerEvent(type, key) { - const event = new KeyboardEvent(type, { - key: key, - bubbles: true, - cancelable: true, + simulateTypingWithEvents(element, text, delay = 100) { + let resolver = null; + let promise = new Promise((resolve) => { + resolver = resolve; }); - element.dispatchEvent(event); - } + let index = 0; - const interval = setInterval(() => { - if (index < text.length) { - const char = text.charAt(index); - - // Trigger keydown - triggerEvent('keydown', char); - - // Update the value - if (element.isContentEditable) { - // For contentEditable elements - document.execCommand('insertText', false, char); - } else { - // For input or textarea - element.value += char; - } - - // Trigger keypress - triggerEvent('keypress', char); - - // Trigger keyup - triggerEvent('keyup', char); - - index++; - } else { - clearInterval(interval); - resolver() + function triggerEvent(type, key) { + const event = new KeyboardEvent(type, { + key: key, + bubbles: true, + cancelable: true, + }); + element.dispatchEvent(event); } - }, delay); - return promise -} + + const interval = setInterval(() => { + if (index < text.length) { + const char = text.charAt(index); + + triggerEvent('keydown', char); + + if (element.isContentEditable) { + document.execCommand('insertText', false, char); + } else { + element.value += char; + } + + triggerEvent('keypress', char); + triggerEvent('keyup', char); + + index++; + } else { + clearInterval(interval); + resolver(); + } + }, delay); + + return promise; + } constructor() { super(); - this.attachShadow({mode: 'open'}); + this.attachShadow({ mode: 'open' }); - /* —— UI —— */ - const btn = document.createElement('button'); + const btn = document.createElement('button'); btn.setAttribute('part', 'button'); btn.setAttribute('aria-label', 'Start voice dictation'); - btn.innerHTML = '🎤'; // tiny icon – swap with SVG if you prefer + btn.innerHTML = '🎤'; const style = document.createElement('style'); style.textContent = ` :host { display:inline-block; } - button { all:unset; cursor:pointer;/* font-size:1.6rem; - padding:.6rem; border-radius:50%; background:#f2f2f2; - transition:background .25s, transform .25s, box-shadow .25s;*/ } + button { all:unset; cursor:pointer; } button:hover { background:#e8e8e8; } :host([listening]) button { background:#d32f2f; color:#fff; @@ -76,106 +67,91 @@ class STTButton extends HTMLElement { `; this.shadowRoot.append(style, btn); - /* —— speech recognition setup —— */ const SR = window.SpeechRecognition || window.webkitSpeechRecognition; - if (!SR) { console.warn('Web Speech API not supported in this browser.'); return; } + if (!SR) { + console.warn('Web Speech API not supported in this browser.'); + return; + } - this.recog = new SR(); - this.recog.lang = 'en-US'; // always English - this.recog.continuous = true; + this.recog = new SR(); + this.recog.lang = 'en-US'; + this.recog.continuous = true; this.recog.interimResults = true; - /* handle results */ -let interim = ''; // grey, volatile -let committed = ''; // sentences we've decided are final + let interim = ''; + let committed = ''; -this.recog.onresult = (e) => { - interim = ''; // reset interim on every event + this.recog.onresult = (e) => { + interim = ''; + for (let i = e.resultIndex; i < e.results.length; i++) { + const res = e.results[i]; + const txt = res[0].transcript.trim(); - for (let i = e.resultIndex; i < e.results.length; i++) { - const res = e.results[i]; - const txt = res[0].transcript.trim(); + if (res.isFinal) { + const sentence = txt.charAt(0).toUpperCase() + txt.slice(1); + let punctuated = /[.!?]$/.test(sentence) ? sentence : sentence + '.'; - if (res.isFinal) { - // 1) Capitalise first letter - const sentence = - txt.charAt(0).toUpperCase() + txt.slice(1); + committed += punctuated + ' '; + if (this.targetEl) { + this.targetEl.focus(); + punctuated = punctuated.replace(/\./g, ".\n"); + punctuated = punctuated.replace(/\?/g, "?\n"); + punctuated = punctuated.replace(/\!/g, "!\n"); - // 2) Ensure closing punctuation - let punctuated = - /[.!?]$/.test(sentence) ? sentence : sentence + '.'; + this.simulateTypingWithEvents(this.targetEl, punctuated, 1).then(() => { + const chatInput = document.querySelector('chat-input'); + chatInput.finalizeMessage(); + }); + } + } else { + interim += txt + ' '; + } + } + }; - committed += punctuated + ' '; // add to permanent text - if (this.targetEl) { - this.targetEl.focus() - - punctuated = punctuated.replace(/\./g, ".\n") - punctuated = punctuated.replace(/\?/g, "?\n") - punctuated = punctuated.replace(/\!/g, "!\n") - this.simulateTypingWithEvents(this.targetEl, punctuated,1).then(()=>{ - const chatInput = document.querySelector('chat-input') - chatInput.finalizeMessage() - }) - //triggerEvent('keydown', "Enter"); - - - //this.targetEl.value = committed + interim - -/*this.targetElement.dispatchEvent(new ChangeEvent('change', { - bubbles: true, - cancelable: true -}));*/ - - } + this.recog.onend = () => { + // Auto-restart recognition unless we explicitly stopped + if (this.listening) { + try { + this.recog.start(); // attempt restart + } catch (e) { + console.warn('Failed to restart speech recognition:', e); + } + } + }; - } else { - interim += txt + ' '; // live but volatile - } - } - - /* --- paint to DOM --- */ -}; - - /* const transcript = Array.from(e.results) - .map(res => res[0].transcript) - .join(''); - if (this.targetEl) this.targetEl.value = transcript; - console.info(transcript)*/ - - - /* update state when recognition stops unexpectedly */ - this.recog.onend = () => { this.listening = false; }; - - /* click toggles listening state */ btn.addEventListener('click', () => this.toggle()); } - /* react to attribute changes or late target wiring */ - attributeChangedCallback(name, _, newVal) { - //if (name === 'target') this.targetEl = document.querySelector(newVal); + attributeChangedCallback(name, _, newVal) {} + + get targetEl() { + return document.querySelector("textarea") || null; } - get targetEl() { return document.querySelector("textarea") || null; } - + connectedCallback() {} - connectedCallback() { // initial target lookup - // if (this.getAttribute('target')) - // this.targetEl = document.activeElement || null; - - //document.querySelector(this.getAttribute('target')); + get listening() { + return this.hasAttribute('listening'); } - - /* property reflects listening status via [listening] attribute */ - get listening() { return this.hasAttribute('listening'); } set listening(val) { - if (val) this.setAttribute('listening',''); - else this.removeAttribute('listening'); + if (val) this.setAttribute('listening', ''); + else this.removeAttribute('listening'); } toggle() { if (!this.recog) return; - if (this.listening) { this.recog.stop(); this.listening = false; } - else { this.recog.start(); this.listening = true; } + if (this.listening) { + this.recog.stop(); + this.listening = false; + } else { + try { + this.recog.start(); + this.listening = true; + } catch (e) { + console.warn('Error starting recognition:', e); + } + } } }