feat: voice input button — Web Speech API (ru-RU), pulse animation, live transcription

This commit is contained in:
wasrusgen 2026-05-30 12:40:01 +03:00
parent 9a6623fbc0
commit 24c6757d6f

View File

@ -38,6 +38,14 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);displ
.inp:focus{border-color:var(--mid);box-shadow:0 0 0 3px rgba(16,185,129,.1)}
.send{width:44px;height:44px;border-radius:11px;background:var(--primary);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.send:hover{background:var(--dark)}.send:disabled{opacity:.4;cursor:default}
/* Microphone */
.mic{width:44px;height:44px;border-radius:11px;background:var(--light);border:1.5px solid rgba(4,120,87,.2);cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:var(--primary);transition:all .15s;position:relative}
.mic:hover{background:#D1FAE5;border-color:var(--primary)}
.mic.rec{background:#FEF2F2;border-color:#EF4444;color:#EF4444}
.mic.rec::after{content:'';position:absolute;inset:-4px;border-radius:14px;border:2px solid #EF4444;animation:micPulse 1.3s ease-out infinite;pointer-events:none}
@keyframes micPulse{0%{transform:scale(1);opacity:.7}100%{transform:scale(1.25);opacity:0}}
.mic-hint{position:absolute;bottom:64px;left:50%;transform:translateX(-50%);background:var(--ink);color:#fff;font-size:12px;padding:6px 12px;border-radius:8px;white-space:nowrap;display:none;z-index:20}
.mic-hint.show{display:block}
.build-btn{margin:0 20px 14px;padding:13px;border-radius:12px;background:linear-gradient(135deg,var(--dark),var(--primary));color:#fff;border:none;cursor:pointer;font-family:'Inter';font-weight:700;font-size:14px;flex-shrink:0;display:flex;align-items:center;justify-content:center;gap:8px}
.build-btn:hover{opacity:.92}.build-btn:disabled{opacity:.5;cursor:default}
.model-col{width:42%;max-width:560px;overflow-y:auto;padding:24px;background:#fafbfc;display:none}
@ -92,7 +100,11 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);displ
Построить мою бизнес-модель →
</button>
<div class="inbar">
<textarea class="inp" id="inp" rows="1" placeholder="Напишите Елене..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMsg()}"></textarea>
<button class="mic" id="micBtn" onclick="toggleMic()" title="Голосовой ввод">
<div class="mic-hint" id="micHint">Слушаю... говорите</div>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
</button>
<textarea class="inp" id="inp" rows="1" placeholder="Напишите или скажите Елене..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMsg()}"></textarea>
<button class="send" id="sendBtn" onclick="sendMsg()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
@ -112,6 +124,47 @@ const inp = document.getElementById("inp");
function esc(s){return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}
function fmt(s){return esc(s).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>")}
// ── Голосовой ввод (Web Speech API, русский) ──────────
let recog=null, recording=false;
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if(SR){
recog = new SR();
recog.lang = "ru-RU";
recog.continuous = true;
recog.interimResults = true;
let baseText = "";
recog.onresult = e=>{
let fin="", interim="";
for(let i=e.resultIndex;i<e.results.length;i++){
const t=e.results[i][0].transcript;
if(e.results[i].isFinal) fin+=t; else interim+=t;
}
if(fin) baseText += fin;
inp.value = (baseText + interim).trim();
inp.style.height="auto"; inp.style.height=Math.min(inp.scrollHeight,120)+"px";
};
recog.onstart = ()=>{ baseText = inp.value ? inp.value+" " : ""; };
recog.onend = ()=>{ if(recording){ try{recog.start()}catch(e){} } };
recog.onerror = e=>{ if(e.error==="not-allowed"){ alert("Разрешите доступ к микрофону в браузере"); stopMic(); } };
}
function toggleMic(){
if(!recog){ alert("Голосовой ввод не поддерживается этим браузером. Используйте Chrome."); return; }
recording ? stopMic() : startMic();
}
function startMic(){
recording=true;
document.getElementById("micBtn").classList.add("rec");
document.getElementById("micHint").classList.add("show");
try{ recog.start() }catch(e){}
}
function stopMic(){
recording=false;
document.getElementById("micBtn").classList.remove("rec");
document.getElementById("micHint").classList.remove("show");
try{ recog.stop() }catch(e){}
inp.focus();
}
function addMsg(role, text){
const m = document.createElement("div");
m.className = "msg " + (role==="user"?"user":"elena");
@ -144,6 +197,7 @@ async function init(){
}
async function sendMsg(){
if(recording) stopMic();
const text = inp.value.trim();
if(!text) return;
inp.value=""; inp.style.height="auto";