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 */
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);
}
}
}
}