From 200b0178e79855b8f7177b7fff5590ccc8445dbc Mon Sep 17 00:00:00 2001 From: retoor Date: Wed, 25 Jun 2025 22:41:55 +0200 Subject: [PATCH] Update. --- src/snek/static/chat-input.js | 10 ++- src/snek/static/stt.js | 122 ++++++++++++++++++++++++++++++++++ src/snek/templates/app.html | 1 + 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 src/snek/static/stt.js diff --git a/src/snek/static/chat-input.js b/src/snek/static/chat-input.js index 61b0464..8b1b762 100644 --- a/src/snek/static/chat-input.js +++ b/src/snek/static/chat-input.js @@ -33,6 +33,7 @@ class ChatInputComponent extends NjetComponent { super(); this.lastUpdateEvent = new Date(); this.textarea = document.createElement("textarea"); + this.textarea.classList.add("chat-input-textarea"); this.value = this.getAttribute("value") || ""; } @@ -267,7 +268,9 @@ textToLeetAdvanced(text) { this.textarea.setAttribute("rows", "2"); this.appendChild(this.textarea); - + this.ttsButton = document.createElement("stt-button"); + + this.appendChild(this.ttsButton); this.uploadButton = document.createElement("upload-button"); this.uploadButton.setAttribute("channel", this.channelUid); this.uploadButton.addEventListener("upload", (e) => { @@ -302,7 +305,10 @@ textToLeetAdvanced(text) { }); - + this.textarea.addEventListener("change",(e)=>{ + this.value = this.textarea.value; + this.updateFromInput(e.target.value); + }) this.textarea.addEventListener("keyup", (e) => { if (e.key === "Enter" && !e.shiftKey) { const message = this.replaceMentionsWithAuthors(this.value); diff --git a/src/snek/static/stt.js b/src/snek/static/stt.js new file mode 100644 index 0000000..f5174a0 --- /dev/null +++ b/src/snek/static/stt.js @@ -0,0 +1,122 @@ +class STTButton extends HTMLElement { + /** monitor target attribute so it can change on-the-fly */ + static get observedAttributes() { return ['target']; } + + constructor() { + super(); + this.attachShadow({mode: 'open'}); + + /* —— UI —— */ + 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 + + 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:hover { background:#e8e8e8; } + :host([listening]) button { + background:#d32f2f; color:#fff; + animation:pulse 1.2s ease-in-out infinite; } + @keyframes pulse { + 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);} + } + `; + 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; } + + this.recog = new SR(); + this.recog.lang = 'en-US'; // always English + this.recog.continuous = true; + this.recog.interimResults = true; + + /* handle results */ +let interim = ''; // grey, volatile +let committed = ''; // sentences we've decided are final + +this.recog.onresult = (e) => { + interim = ''; // reset interim on every event + + for (let i = e.resultIndex; i < e.results.length; i++) { + const res = e.results[i]; + const txt = res[0].transcript.trim(); + + if (res.isFinal) { + // 1) Capitalise first letter + const sentence = + txt.charAt(0).toUpperCase() + txt.slice(1); + + // 2) Ensure closing punctuation + const punctuated = + /[.!?]$/.test(sentence) ? sentence : sentence + '.'; + + committed += punctuated + ' '; // add to permanent text + } else { + interim += txt + ' '; // live but volatile + } + } + + /* --- paint to DOM --- */ + if (this.targetEl) { + this.targetEl.setAttribute("value",committed + interim) +/*this.targetElement.dispatchEvent(new ChangeEvent('change', { + bubbles: true, + cancelable: true +}));*/ + + } +}; + + /* 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); + } + + get targetEl() { return document.activeElement || null; } + + + connectedCallback() { // initial target lookup + // if (this.getAttribute('target')) + // 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) { + 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; } + } +} + +customElements.define('stt-button', STTButton); + diff --git a/src/snek/templates/app.html b/src/snek/templates/app.html index f5b0481..050712f 100644 --- a/src/snek/templates/app.html +++ b/src/snek/templates/app.html @@ -9,6 +9,7 @@ +