feat: add voice input mic button via Web Speech API
- index.html: add #btnMic (hidden by default, shown if browser supports SpeechRecognition) and #micStatus listening indicator inside .composer-box - boot.js: IIFE-scoped mic handler wired to Web Speech API * recognition.continuous=false (auto-stops after ~2s silence) * recognition.interimResults=true (live transcript preview in textarea) * Toggles .recording class + shows #micStatus while active * Handles 'not-allowed', 'no-speech', 'network' errors via showToast() * btnSend.onclick stops active recognition before sending * Entire feature disabled/hidden gracefully when API unavailable - style.css: .mic-btn, .mic-btn.recording (red pulse animation), .mic-status, .mic-dot, @keyframes mic-pulse - tests/test_sprint20.py: 46 tests covering HTML structure, CSS rules, JS logic, error handling, and regression checks (376 total, all pass) No API keys, no external libraries, no server changes. Browser-only. Works in Chrome, Edge, Safari (partial). Firefox unsupported (hides button).
This commit is contained in:
@@ -8,8 +8,81 @@ async function cancelStream(){
|
||||
}catch(e){setStatus('Cancel failed: '+e.message);}
|
||||
}
|
||||
|
||||
$('btnSend').onclick=send;
|
||||
$('btnSend').onclick=()=>{if(window._micActive)_stopMic();send();};
|
||||
$('btnAttach').onclick=()=>$('fileInput').click();
|
||||
|
||||
// ── Voice input (Web Speech API) ─────────────────────────────────────────
|
||||
(function(){
|
||||
const SpeechRecognition=window.SpeechRecognition||window.webkitSpeechRecognition;
|
||||
if(!SpeechRecognition) return; // Browser unsupported — mic button stays hidden
|
||||
|
||||
const btn=$('btnMic');
|
||||
const status=$('micStatus');
|
||||
const ta=$('msg');
|
||||
btn.style.display=''; // Show button — browser supports speech
|
||||
|
||||
const recognition=new SpeechRecognition();
|
||||
recognition.continuous=false;
|
||||
recognition.interimResults=true;
|
||||
recognition.lang='en-US';
|
||||
|
||||
let _finalText='';
|
||||
|
||||
function _setRecording(on){
|
||||
window._micActive=on;
|
||||
btn.classList.toggle('recording',on);
|
||||
status.style.display=on?'':'none';
|
||||
if(!on) _finalText='';
|
||||
}
|
||||
|
||||
recognition.onstart=()=>{ _finalText=''; };
|
||||
|
||||
recognition.onresult=(event)=>{
|
||||
let interim='';
|
||||
let final=_finalText;
|
||||
for(let i=event.resultIndex;i<event.results.length;i++){
|
||||
const t=event.results[i][0].transcript;
|
||||
if(event.results[i].isFinal){ final+=t; _finalText=final; }
|
||||
else{ interim+=t; }
|
||||
}
|
||||
ta.value=final||interim;
|
||||
autoResize();
|
||||
};
|
||||
|
||||
recognition.onend=()=>{
|
||||
_setRecording(false);
|
||||
// Ensure textarea has the committed final text (handles auto-stop on silence)
|
||||
if(_finalText) ta.value=_finalText;
|
||||
autoResize();
|
||||
};
|
||||
|
||||
recognition.onerror=(event)=>{
|
||||
_setRecording(false);
|
||||
const msgs={
|
||||
'not-allowed':'Microphone access denied. Check browser permissions.',
|
||||
'no-speech':'No speech detected. Try again.',
|
||||
'network':'Speech recognition unavailable.',
|
||||
};
|
||||
showToast(msgs[event.error]||'Voice input error: '+event.error);
|
||||
};
|
||||
|
||||
function _stopMic(){
|
||||
if(window._micActive){ recognition.stop(); }
|
||||
}
|
||||
window._stopMic=_stopMic; // expose for send-guard above
|
||||
|
||||
btn.onclick=()=>{
|
||||
if(window._micActive){
|
||||
recognition.stop();
|
||||
// _setRecording(false) will be called by onend
|
||||
} else {
|
||||
_finalText='';
|
||||
recognition.start();
|
||||
_setRecording(true);
|
||||
}
|
||||
};
|
||||
})();
|
||||
window._micActive=window._micActive||false;
|
||||
$('fileInput').onchange=e=>{addFiles(Array.from(e.target.files));e.target.value='';};
|
||||
$('btnNewChat').onclick=async()=>{await newSession();await renderSessionList();$('msg').focus();};
|
||||
$('btnDownload').onclick=()=>{
|
||||
|
||||
@@ -213,6 +213,7 @@
|
||||
Drop files to upload to workspace
|
||||
</div>
|
||||
<div class="attach-tray" id="attachTray"></div>
|
||||
<div class="mic-status" id="micStatus" style="display:none"><span class="mic-dot"></span> Listening…</div>
|
||||
<textarea id="msg" rows="1" placeholder="Message Hermes…"></textarea>
|
||||
<div class="composer-footer">
|
||||
<div class="composer-left">
|
||||
@@ -220,6 +221,14 @@
|
||||
<button class="icon-btn" id="btnAttach" title="Attach files">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
|
||||
</button>
|
||||
<button class="icon-btn mic-btn" id="btnMic" title="Voice input" style="display:none">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="1" width="6" height="12" rx="3"/>
|
||||
<path d="M5 10a7 7 0 0 0 14 0"/>
|
||||
<line x1="12" y1="19" x2="12" y2="23"/>
|
||||
<line x1="8" y1="23" x2="16" y2="23"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="composer-right">
|
||||
<button class="send-btn" id="btnSend">
|
||||
|
||||
@@ -187,6 +187,11 @@
|
||||
.icon-btn{width:34px;height:34px;border-radius:8px;background:none;border:none;color:var(--muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;transition:all .15s;}
|
||||
.icon-btn{opacity:.75;}
|
||||
.icon-btn:hover{background:rgba(255,255,255,.08);color:var(--text);opacity:1;}
|
||||
.mic-btn{transition:color .15s,background .15s;}
|
||||
.mic-btn.recording{color:#e94560;background:rgba(233,69,96,.12);animation:mic-pulse 1.2s ease-in-out infinite;}
|
||||
@keyframes mic-pulse{0%,100%{box-shadow:0 0 0 0 rgba(233,69,96,.3);}50%{box-shadow:0 0 0 6px rgba(233,69,96,0);}}
|
||||
.mic-status{font-size:11px;color:#e94560;padding:4px 12px;display:flex;align-items:center;gap:6px;}
|
||||
.mic-dot{width:6px;height:6px;border-radius:50%;background:#e94560;animation:mic-pulse 1.2s ease-in-out infinite;flex-shrink:0;}
|
||||
.status-text{font-size:11px;color:var(--muted);padding-left:4px;}
|
||||
.send-btn{padding:7px 18px;border-radius:10px;font-size:13px;font-weight:600;background:linear-gradient(135deg,#5ba8f5,#7cb9ff);border:none;color:#0a1628;cursor:pointer;display:flex;align-items:center;gap:6px;transition:all .15s;flex-shrink:0;letter-spacing:.01em;}
|
||||
.send-btn:hover{background:linear-gradient(135deg,#7cb9ff,#a0d0ff);transform:translateY(-1px);}
|
||||
|
||||
Reference in New Issue
Block a user