Working perfect.

This commit is contained in:
retoor 2025-06-29 20:34:25 +02:00
parent 86844e330e
commit e96cb5bdaa

View File

@ -2,69 +2,60 @@ class STTButton extends HTMLElement {
/** monitor target attribute so it can change on-the-fly */ /** monitor target attribute so it can change on-the-fly */
static get observedAttributes() { return ['target']; } static get observedAttributes() { return ['target']; }
simulateTypingWithEvents(element, text, delay = 100) { simulateTypingWithEvents(element, text, delay = 100) {
let resolver = null let resolver = null;
let promise = new Promise((resolve, reject)=>{ let promise = new Promise((resolve) => {
resolver = resolve resolver = resolve;
})
let index = 0;
function triggerEvent(type, key) {
const event = new KeyboardEvent(type, {
key: key,
bubbles: true,
cancelable: true,
}); });
element.dispatchEvent(event); let index = 0;
}
const interval = setInterval(() => { function triggerEvent(type, key) {
if (index < text.length) { const event = new KeyboardEvent(type, {
const char = text.charAt(index); key: key,
bubbles: true,
// Trigger keydown cancelable: true,
triggerEvent('keydown', char); });
element.dispatchEvent(event);
// 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()
} }
}, 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() { constructor() {
super(); 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('part', 'button');
btn.setAttribute('aria-label', 'Start voice dictation'); btn.setAttribute('aria-label', 'Start voice dictation');
btn.innerHTML = '🎤'; // tiny icon swap with SVG if you prefer btn.innerHTML = '🎤';
const style = document.createElement('style'); const style = document.createElement('style');
style.textContent = ` style.textContent = `
:host { display:inline-block; } :host { display:inline-block; }
button { all:unset; cursor:pointer;/* font-size:1.6rem; button { all:unset; cursor:pointer; }
padding:.6rem; border-radius:50%; background:#f2f2f2;
transition:background .25s, transform .25s, box-shadow .25s;*/ }
button:hover { background:#e8e8e8; } button:hover { background:#e8e8e8; }
:host([listening]) button { :host([listening]) button {
background:#d32f2f; color:#fff; background:#d32f2f; color:#fff;
@ -76,106 +67,91 @@ class STTButton extends HTMLElement {
`; `;
this.shadowRoot.append(style, btn); this.shadowRoot.append(style, btn);
/* —— speech recognition setup —— */
const SR = window.SpeechRecognition || window.webkitSpeechRecognition; 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 = new SR();
this.recog.lang = 'en-US'; // always English this.recog.lang = 'en-US';
this.recog.continuous = true; this.recog.continuous = true;
this.recog.interimResults = true; this.recog.interimResults = true;
/* handle results */ let interim = '';
let interim = ''; // grey, volatile let committed = '';
let committed = ''; // sentences we've decided are final
this.recog.onresult = (e) => { this.recog.onresult = (e) => {
interim = ''; // reset interim on every event 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++) { if (res.isFinal) {
const res = e.results[i]; const sentence = txt.charAt(0).toUpperCase() + txt.slice(1);
const txt = res[0].transcript.trim(); let punctuated = /[.!?]$/.test(sentence) ? sentence : sentence + '.';
if (res.isFinal) { committed += punctuated + ' ';
// 1) Capitalise first letter if (this.targetEl) {
const sentence = this.targetEl.focus();
txt.charAt(0).toUpperCase() + txt.slice(1); punctuated = punctuated.replace(/\./g, ".\n");
punctuated = punctuated.replace(/\?/g, "?\n");
punctuated = punctuated.replace(/\!/g, "!\n");
// 2) Ensure closing punctuation this.simulateTypingWithEvents(this.targetEl, punctuated, 1).then(() => {
let punctuated = const chatInput = document.querySelector('chat-input');
/[.!?]$/.test(sentence) ? sentence : sentence + '.'; chatInput.finalizeMessage();
});
}
} else {
interim += txt + ' ';
}
}
};
committed += punctuated + ' '; // add to permanent text this.recog.onend = () => {
if (this.targetEl) { // Auto-restart recognition unless we explicitly stopped
this.targetEl.focus() if (this.listening) {
try {
punctuated = punctuated.replace(/\./g, ".\n") this.recog.start(); // attempt restart
punctuated = punctuated.replace(/\?/g, "?\n") } catch (e) {
punctuated = punctuated.replace(/\!/g, "!\n") console.warn('Failed to restart speech recognition:', e);
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
}));*/
}
} 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()); btn.addEventListener('click', () => this.toggle());
} }
/* react to attribute changes or late target wiring */ attributeChangedCallback(name, _, newVal) {}
attributeChangedCallback(name, _, newVal) {
//if (name === 'target') this.targetEl = document.querySelector(newVal); get targetEl() {
return document.querySelector("textarea") || null;
} }
get targetEl() { return document.querySelector("textarea") || null; } connectedCallback() {}
connectedCallback() { // initial target lookup get listening() {
// if (this.getAttribute('target')) return this.hasAttribute('listening');
// this.targetEl = document.activeElement || null;
//document.querySelector(this.getAttribute('target'));
} }
/* property reflects listening status via [listening] attribute */
get listening() { return this.hasAttribute('listening'); }
set listening(val) { set listening(val) {
if (val) this.setAttribute('listening',''); if (val) this.setAttribute('listening', '');
else this.removeAttribute('listening'); else this.removeAttribute('listening');
} }
toggle() { toggle() {
if (!this.recog) return; if (!this.recog) return;
if (this.listening) { this.recog.stop(); this.listening = false; } if (this.listening) {
else { this.recog.start(); this.listening = true; } this.recog.stop();
this.listening = false;
} else {
try {
this.recog.start();
this.listening = true;
} catch (e) {
console.warn('Error starting recognition:', e);
}
}
} }
} }