Update.
This commit is contained in:
parent
9e237a1d98
commit
200b0178e7
@ -33,6 +33,7 @@ class ChatInputComponent extends NjetComponent {
|
|||||||
super();
|
super();
|
||||||
this.lastUpdateEvent = new Date();
|
this.lastUpdateEvent = new Date();
|
||||||
this.textarea = document.createElement("textarea");
|
this.textarea = document.createElement("textarea");
|
||||||
|
this.textarea.classList.add("chat-input-textarea");
|
||||||
this.value = this.getAttribute("value") || "";
|
this.value = this.getAttribute("value") || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,7 +268,9 @@ textToLeetAdvanced(text) {
|
|||||||
this.textarea.setAttribute("rows", "2");
|
this.textarea.setAttribute("rows", "2");
|
||||||
|
|
||||||
this.appendChild(this.textarea);
|
this.appendChild(this.textarea);
|
||||||
|
this.ttsButton = document.createElement("stt-button");
|
||||||
|
|
||||||
|
this.appendChild(this.ttsButton);
|
||||||
this.uploadButton = document.createElement("upload-button");
|
this.uploadButton = document.createElement("upload-button");
|
||||||
this.uploadButton.setAttribute("channel", this.channelUid);
|
this.uploadButton.setAttribute("channel", this.channelUid);
|
||||||
this.uploadButton.addEventListener("upload", (e) => {
|
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) => {
|
this.textarea.addEventListener("keyup", (e) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
const message = this.replaceMentionsWithAuthors(this.value);
|
const message = this.replaceMentionsWithAuthors(this.value);
|
||||||
|
122
src/snek/static/stt.js
Normal file
122
src/snek/static/stt.js
Normal file
@ -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);
|
||||||
|
|
@ -9,6 +9,7 @@
|
|||||||
<link rel="stylesheet" href="/file-upload-grid.css">
|
<link rel="stylesheet" href="/file-upload-grid.css">
|
||||||
|
|
||||||
<script src="/njet.js" type="module"></script>
|
<script src="/njet.js" type="module"></script>
|
||||||
|
<script src="/stt.js" type="module"></script>
|
||||||
<script src="/file-upload-grid.js" type="module"></script>
|
<script src="/file-upload-grid.js" type="module"></script>
|
||||||
<script src="/polyfills/Promise.withResolvers.js" type="module"></script>
|
<script src="/polyfills/Promise.withResolvers.js" type="module"></script>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user