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 @@
+