Compare commits

...

2 Commits

Author SHA1 Message Date
88bb78fb23 Update. 2025-06-29 20:56:44 +02:00
e96cb5bdaa Working perfect. 2025-06-29 20:34:25 +02:00

View File

@ -1,74 +1,61 @@
class STTButton extends HTMLElement { class STTButton extends HTMLElement {
/** 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)=>{ const promise = new Promise((resolve) => resolver = resolve);
resolver = resolve let index = 0;
})
let index = 0;
function triggerEvent(type, key) { const triggerEvent = (type, key) => {
const event = new KeyboardEvent(type, { const event = new KeyboardEvent(type, {
key: key, key: key,
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
}); });
element.dispatchEvent(event); element.dispatchEvent(event);
} };
const interval = setInterval(() => { const interval = setInterval(() => {
if (index < text.length) { if (index < text.length) {
const char = text.charAt(index); const char = text.charAt(index);
// Trigger keydown triggerEvent('keydown', char);
triggerEvent('keydown', char);
if (element.isContentEditable) {
// Update the value document.execCommand('insertText', false, char);
if (element.isContentEditable) { } else {
// For contentEditable elements element.value += char;
document.execCommand('insertText', false, char); }
triggerEvent('keypress', char);
triggerEvent('keyup', char);
index++;
} else { } else {
// For input or textarea clearInterval(interval);
element.value += char; resolver();
} }
}, delay);
// Trigger keypress
triggerEvent('keypress', char); return promise;
}
// Trigger keyup
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;
animation:pulse 1.2s ease-in-out infinite; } animation:pulse 1.2s ease-in-out infinite; }
@keyframes pulse { @keyframes pulse {
0%,100% { transform:scale(1); box-shadow:0 0 0 0 rgba(211,47,47,.6);} 0%,100% { transform:scale(1); box-shadow:0 0 0 0 rgba(211,47,47,.6);}
50% { transform:scale(1.12);box-shadow:0 0 0 12px rgba(211,47,47,0);} 50% { transform:scale(1.12);box-shadow:0 0 0 12px rgba(211,47,47,0);}
@ -76,106 +63,113 @@ 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 let previousInterim = '';
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 alt = res[0];
const txt = alt.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")
.replace(/\?/g, "?\n")
.replace(/\!/g, "!\n");
// 2) Ensure closing punctuation this.targetEl.value = ''; // punctuated;
let punctuated = this.simulateTypingWithEvents(this.targetEl, punctuated, 1).then(() => {
/[.!?]$/.test(sentence) ? sentence : sentence + '.'; const chatInput = document.querySelector('chat-input');
chatInput.finalizeMessage();
});
}
committed += punctuated + ' '; // add to permanent text previousInterim = '';
if (this.targetEl) { } else {
this.targetEl.focus() if (alt.confidence >= 0.85) {
interim += txt + ' ';
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
}));*/
}
} else { if (interim && this.targetEl) {
interim += txt + ' '; // live but volatile const el = this.targetEl;
} el.focus();
}
/* --- paint to DOM --- */ if (el.isContentEditable) {
}; el.innerText = el.innerText.replace(new RegExp(previousInterim + '$'), interim);
} else {
el.value = interim
//el.value = el.value.replace(new RegExp(previousInterim + '$'), interim);
}
/*
this.simulateTypingWithEvents(el, interim, 0).then(() => {
previousInterim = interim;
});*/
}
};
/* const transcript = Array.from(e.results) this.recog.onend = () => {
.map(res => res[0].transcript) if (this.listening) {
.join(''); try {
if (this.targetEl) this.targetEl.value = transcript; this.recog.start();
console.info(transcript)*/ } catch (e) {
console.warn('Failed to restart speech recognition:', e);
}
}
};
/* 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);
}
}
} }
} }