Files
voice-kiro/static/index.html

132 lines
4.9 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>Voice → Kiro</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui;background:#1a1a2e;color:#eee;display:flex;flex-direction:column;height:100dvh;padding:0}
#output{flex:1;width:100%;background:#0f0f1a;padding:.5rem;overflow-y:auto;font-family:monospace;font-size:.7rem;white-space:pre-wrap;color:#ccc}
#bottom{width:100%;padding:.4rem;background:#16213e;display:flex;gap:.4rem;align-items:center}
#status{font-size:.75rem;color:#aaa;padding:0 .4rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:40%}
#textinput{flex:1;padding:.5rem;border-radius:6px;border:1px solid #333;background:#0f0f1a;color:#eee;font-size:.9rem}
.fab{position:fixed;border-radius:50%;border:none;color:#fff;font-size:.7rem;cursor:pointer;touch-action:none;user-select:none;z-index:100;display:flex;align-items:center;justify-content:center;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,.5)}
#btn{width:70px;height:70px;background:#16213e;bottom:4rem;right:1rem;font-size:.8rem}
#btn.recording{background:#e94560;transform:scale(1.1)}
#cancelBtn{width:50px;height:50px;background:#e94560;bottom:4rem;left:1rem;font-size:.65rem}
</style>
</head>
<body>
<div id="output"></div>
<div id="bottom">
<span id="status"></span>
<form id="textform" autocomplete="off" style="flex:1;display:flex"><input id="textinput" type="text" placeholder="Message..." autocomplete="off"></form>
</div>
<button id="btn" class="fab">Hold<br>to Talk</button>
<button id="cancelBtn" class="fab">⌃C</button>
<script>
const btn=document.getElementById('btn'),status=document.getElementById('status'),output=document.getElementById('output');
let mediaRec,chunks=[];
function start(){
navigator.mediaDevices.getUserMedia({audio:true}).then(stream=>{
const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/mp4';
mediaRec=new MediaRecorder(stream,{mimeType});
chunks=[];
mediaRec.ondataavailable=e=>chunks.push(e.data);
mediaRec.onstop=()=>{
stream.getTracks().forEach(t=>t.stop());
const ext = mediaRec.mimeType.includes('mp4') ? 'mp4' : 'webm';
send(new Blob(chunks,{type:mediaRec.mimeType}), ext);
};
mediaRec.start();
btn.classList.add('recording');
btn.textContent='●';
status.textContent='';
}).catch(e=>{status.textContent='Mic: '+e.message});
}
function stop(){
if(mediaRec&&mediaRec.state==='recording'){
mediaRec.stop();
btn.classList.remove('recording');
btn.innerHTML='Hold<br>to Talk';
}
}
function send(blob, ext){
status.textContent='Transcribing...';
const fd=new FormData();
fd.append('audio',blob,'audio.'+ext);
fetch('/send',{method:'POST',body:fd})
.then(r=>r.json())
.then(d=>{
if(d.error){status.textContent='Err: '+d.error}
else{status.textContent='✓ '+d.text.slice(0,30); pollOutput()}
})
.catch(e=>{status.textContent='Fail: '+e.message});
}
function pollOutput(){
fetch('/output').then(r=>r.json()).then(d=>{
const atBottom=output.scrollHeight-output.scrollTop-output.clientHeight<50;
output.textContent=d.output;
if(atBottom)output.scrollTop=output.scrollHeight;
}).catch(()=>{});
}
setInterval(pollOutput, 1000);
pollOutput();
document.getElementById('textform').addEventListener('submit',e=>{
e.preventDefault();
const inp=document.getElementById('textinput');
const t=inp.value.trim();
if(!t)return;
inp.value='';
status.textContent='Sending...';
fetch('/text',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:t})})
.then(r=>r.json())
.then(d=>{
if(d.error){status.textContent='Err: '+d.error}
else{status.textContent='✓ '+d.text.slice(0,30); pollOutput()}
})
.catch(e=>{status.textContent='Fail: '+e.message});
});
document.getElementById('cancelBtn').addEventListener('click',()=>{
fetch('/cancel',{method:'POST'}).then(()=>{status.textContent='⌃C sent'}).catch(()=>{});
});
btn.addEventListener('pointerdown',e=>{if(e.pointerId!==undefined)btn.setPointerCapture(e.pointerId);e.preventDefault();start()});
btn.addEventListener('pointerup',stop);
btn.addEventListener('pointercancel',stop);
// Draggable FABs
document.querySelectorAll('.fab').forEach(fab=>{
let dragging=false,ox,oy,sx,sy;
fab.addEventListener('pointerdown',e=>{
ox=e.clientX;oy=e.clientY;
sx=fab.offsetLeft;sy=fab.offsetTop;
dragging=false;
});
fab.addEventListener('pointermove',e=>{
const dx=e.clientX-ox,dy=e.clientY-oy;
if(!dragging&&Math.abs(dx)+Math.abs(dy)>10)dragging=true;
if(dragging){
fab.style.left=(sx+dx)+'px';
fab.style.top=(sy+dy)+'px';
fab.style.right='auto';
fab.style.bottom='auto';
e.preventDefault();
}
});
fab.addEventListener('pointerup',e=>{
if(dragging)e.stopImmediatePropagation();
});
});
</script>
</body>
</html>