|
|
<!doctype html> |
|
|
<html> |
|
|
<head> |
|
|
<meta charset="utf-8" /> |
|
|
<title>Ortho Buddy — Voice</title> |
|
|
<style> |
|
|
html,body { height:100%; margin:0; font-family: "Segoe UI", Roboto, Arial; background: radial-gradient(circle at 50% 10%, #5ca4b0, #062219); color:#bfe6c9; } |
|
|
.container { width:360px; margin:40px auto; text-align:center; } |
|
|
h1 { letter-spacing:4px; font-size:36px; color:#2de08b; text-shadow:0 6px 30px rgba(0,0,0,0.7); margin:20px 0; } |
|
|
.subtitle { color:#d6f3de; margin-bottom:10px; font-size:24px; } |
|
|
.robot { width:270px; height:270px; background: url('/static/robot.gif') center/contain no-repeat; margin: 40px auto; border-radius:8px; } |
|
|
.control { margin-top:20px; } |
|
|
.record-btn { width:50px; height:50px; border-radius:60px; border:none; background:linear-gradient(rgb(248, 245, 248), rgb(248, 245, 248)); box-shadow: 0 10px 30px rgba(0,0,0,0.6); color:white; font-size:18px; cursor:pointer; } |
|
|
.record-btn.recording { background: linear-gradient(#ff6666, #cc2222); box-shadow: 0 10px 30px rgba(0,0,0,0.7); } |
|
|
.heading { |
|
|
text-align: center; |
|
|
} |
|
|
.reset-btn { |
|
|
position: fixed; |
|
|
top: 20px; |
|
|
right: 20px; |
|
|
padding: 10px 20px; |
|
|
background: linear-gradient(#2de08b, #0a8f5c); |
|
|
border: none; |
|
|
border-radius: 6px; |
|
|
font-size: 16px; |
|
|
font-weight: bold; |
|
|
color: #062219; |
|
|
cursor: pointer; |
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); |
|
|
z-index: 1000; |
|
|
transition: background 0.3s ease; |
|
|
} |
|
|
.reset-btn:hover { |
|
|
background: linear-gradient(#1bc47a, #07734f); |
|
|
} |
|
|
</style> |
|
|
<h1 class = "heading">Summit Clinics</h1> |
|
|
</head> |
|
|
<body> |
|
|
<div class="container"> |
|
|
|
|
|
<div class="subtitle"><b>KAMMI</b></div> |
|
|
|
|
|
<div class="robot" id="robotImg"></div> |
|
|
|
|
|
<div class="control"> |
|
|
<button id="recBtn" class="record-btn">🎤</button> |
|
|
</div> |
|
|
|
|
|
<button id="resetBtn" class="reset-btn">New User</button> |
|
|
|
|
|
<div class="text-stream" id="textStream"></div> |
|
|
|
|
|
|
|
|
<audio id="player" controls autoplay hidden></audio> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
let mediaRecorder; |
|
|
let audioChunks = []; |
|
|
let recBtn = document.getElementById("recBtn"); |
|
|
let textStream = document.getElementById("textStream"); |
|
|
let recording = false; |
|
|
let player = document.getElementById("player"); |
|
|
document.getElementById("resetBtn").addEventListener("click", async () => { |
|
|
try { |
|
|
const response = await fetch("/reset_chat", { |
|
|
method: "POST" |
|
|
}); |
|
|
if (response.ok) { |
|
|
showTempMessage("Please proceed.", "lightgreen"); |
|
|
} else { |
|
|
const errorText = await response.text(); |
|
|
showTempMessage("Reset failed: " + errorText, "#ffb3b3"); |
|
|
} |
|
|
} catch (error) { |
|
|
showTempMessage("Reset error: " + error.message, "#ffb3b3"); |
|
|
} |
|
|
}); |
|
|
|
|
|
function showTempMessage(msg, color) { |
|
|
const msgDiv = document.createElement("div"); |
|
|
msgDiv.style.color = color; |
|
|
msgDiv.textContent = msg; |
|
|
textStream.appendChild(msgDiv); |
|
|
setTimeout(() => { |
|
|
msgDiv.remove(); |
|
|
}, 2000); |
|
|
} |
|
|
recBtn.addEventListener("click", async () => { |
|
|
if (!recording) { |
|
|
await startRecording(); |
|
|
} else { |
|
|
stopRecordingAndSend(); |
|
|
} |
|
|
recording = !recording; |
|
|
recBtn.textContent = recording ? "Stop" : "🎤"; |
|
|
recBtn.classList.toggle("recording", recording); |
|
|
}); |
|
|
async function startRecording() { |
|
|
textStream.innerHTML = ""; |
|
|
audioChunks = []; |
|
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { |
|
|
alert("Your browser does not support microphone capture."); |
|
|
return; |
|
|
} |
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
|
|
mediaRecorder = new MediaRecorder(stream); |
|
|
mediaRecorder.ondataavailable = e => { |
|
|
if (e.data && e.data.size > 0) audioChunks.push(e.data); |
|
|
}; |
|
|
mediaRecorder.start(); |
|
|
} |
|
|
function stopRecordingAndSend() { |
|
|
if (!mediaRecorder) return; |
|
|
mediaRecorder.stop(); |
|
|
mediaRecorder.onstop = async () => { |
|
|
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); |
|
|
const form = new FormData(); |
|
|
form.append("file", audioBlob, "recording.webm"); |
|
|
|
|
|
console.log("Sending audio to server..."); |
|
|
const resp = await fetch("/chat_stream", { |
|
|
method: "POST", |
|
|
body: form, |
|
|
}); |
|
|
if (!resp.ok) { |
|
|
const txt = await resp.text(); |
|
|
textStream.innerHTML += "<div style='color:#ffb3b3'>Server error: " + txt + "</div>"; |
|
|
return; |
|
|
} |
|
|
|
|
|
const mediaSource = new MediaSource(); |
|
|
player.src = URL.createObjectURL(mediaSource); |
|
|
mediaSource.addEventListener('sourceopen', async () => { |
|
|
const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg'); |
|
|
const reader = resp.body.getReader(); |
|
|
while (true) { |
|
|
const { done, value } = await reader.read(); |
|
|
if (done) break; |
|
|
sourceBuffer.appendBuffer(value); |
|
|
await new Promise(resolve => sourceBuffer.addEventListener('updateend', resolve, { once: true })); |
|
|
} |
|
|
mediaSource.endOfStream(); |
|
|
}); |
|
|
player.play(); |
|
|
}; |
|
|
} |
|
|
</script> |
|
|
</body> |
|
|
</html> |