AfriHealth / index.html
sirlaw-dev's picture
Rename index (2).html to index.html
b8c20b2 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Naija Medical Voice Assistant</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
<style>
:root {
--primary: #00d2ff;
--secondary: #3a7bd5;
--bg: #0f172a;
--card: #1e293b;
--text: #f8fafc;
--accent: #ef4444;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Outfit', sans-serif;
}
body {
background: var(--bg);
background: radial-gradient(circle at top right, #1e293b, #0f172a);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
width: 100%;
max-width: 500px;
background: rgba(30, 41, 59, 0.7);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 24px;
padding: 40px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
text-align: center;
}
h1 {
font-size: 2rem;
margin-bottom: 8px;
background: linear-gradient(to right, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
p.subtitle {
color: #94a3b8;
margin-bottom: 40px;
font-weight: 300;
}
.status-badge {
display: inline-block;
padding: 6px 12px;
border-radius: 100px;
background: rgba(255, 255, 255, 0.05);
font-size: 0.8rem;
margin-bottom: 24px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.mic-container {
position: relative;
margin: 40px 0;
display: flex;
justify-content: center;
}
.mic-button {
width: 100px;
height: 100px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary), var(--secondary));
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 0 30px rgba(0, 210, 255, 0.3);
z-index: 2;
}
.mic-button svg {
width: 40px;
height: 40px;
fill: white;
}
.mic-button:hover {
transform: scale(1.05);
box-shadow: 0 0 50px rgba(0, 210, 255, 0.5);
}
.mic-button.recording {
background: var(--accent);
box-shadow: 0 0 50px rgba(239, 68, 68, 0.5);
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
.pulse-rings {
position: absolute;
width: 100px;
height: 100px;
border-radius: 50%;
border: 2px solid var(--primary);
opacity: 0;
pointer-events: none;
}
.recording .pulse-rings {
animation: ripple 1.5s infinite;
}
@keyframes ripple {
0% {
transform: scale(1);
opacity: 0.5;
}
100% {
transform: scale(2);
opacity: 0;
}
}
.chat-box {
margin-top: 30px;
text-align: left;
display: none;
}
.message {
margin-bottom: 20px;
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 1px;
color: #64748b;
margin-bottom: 4px;
}
.content {
background: rgba(255, 255, 255, 0.05);
padding: 12px 16px;
border-radius: 12px;
font-size: 0.95rem;
line-height: 1.5;
border-left: 3px solid var(--primary);
}
.content.assistant {
border-left-color: #10b981;
}
.loader {
display: none;
margin: 20px 0;
color: var(--primary);
font-size: 0.9rem;
}
.loader span {
display: inline-block;
animation: bounce 1.4s infinite ease-in-out both;
}
.loader span:nth-child(1) {
animation-delay: -0.32s;
}
.loader span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes bounce {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
audio {
display: block;
margin: 20px auto;
width: 100%;
height: 40px;
}
</style>
</head>
<body>
<div class="container">
<div class="status-badge" id="status">System: Ready</div>
<h1>Naija Health AI</h1>
<p class="subtitle">Your empathetic medical assistant</p>
<div class="mic-container">
<div class="pulse-rings"></div>
<button class="mic-button" id="micBtn" title="Click to Record">
<svg viewBox="0 0 24 24">
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z" />
<path
d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" />
</svg>
</button>
</div>
<!-- <div style="margin-bottom: 30px;" style="display: none;">
<input type="file" id="fileInput" accept="audio/*">
<button onclick="document.getElementById('fileInput').click()"
style="background: transparent; border: 1px solid var(--primary); color: var(--primary); padding: 10px 20px; border-radius: 12px; cursor: pointer; font-size: 0.9rem; transition: all 0.3s;">
📁 Upload Audio File
</button>
</div> -->
<div class="loader" id="loader">
AI is thinking<span>.</span><span>.</span><span>.</span>
</div>
<div class="chat-box" id="chatBox">
<div class="message">
<div class="label">You said</div>
<div class="content" id="userMsg">...</div>
</div>
<div class="message">
<div class="label">Assistant</div>
<div class="content assistant" id="assistantMsg">...</div>
</div>
</div>
</div>
<div class="container" style="margin-top: 20px; padding: 20px;">
<div class="label" style="text-align: center;">Assistant's Voice</div>
<audio id="audioPlayer" controls></audio>
</div>
<script>
// --- DOM Elements ---
const micBtn = document.getElementById('micBtn');
const status = document.getElementById('status');
const loader = document.getElementById('loader');
const chatBox = document.getElementById('chatBox');
const userMsg = document.getElementById('userMsg');
const assistantMsg = document.getElementById('assistantMsg');
const audioPlayer = document.getElementById('audioPlayer');
// const fileInput = document.getElementById('fileInput');
// --- Audio Recording Variables ---
let audioContext;
let mediaStreamSource;
let processor;
let isRecording = false;
let recordedChunks = [];
// --- File Upload Logic ---
// fileInput.onchange = (e) => {
// const file = e.target.files[0];
// if (file) {
// status.innerText = "Processing uploaded file...";
// sendAudio(file);
// }
// };
// --- Microphone Recording Logic ---
micBtn.onclick = async () => {
if (isRecording) {
isRecording = false;
if (processor) processor.disconnect();
if (mediaStreamSource) mediaStreamSource.disconnect();
micBtn.classList.remove('recording');
status.innerText = "Processing...";
// Export the recorded audio chunks to a real WAV Blob
const wavBlob = exportWAV(recordedChunks, audioContext.sampleRate);
sendAudio(wavBlob);
return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
audioContext = new (window.AudioContext || window.webkitAudioContext)();
mediaStreamSource = audioContext.createMediaStreamSource(stream);
// Create a ScriptProcessorNode to capture raw audio data
processor = audioContext.createScriptProcessor(4096, 1, 1);
recordedChunks = [];
processor.onaudioprocess = function (e) {
if (!isRecording) return;
const channelData = e.inputBuffer.getChannelData(0);
// Copy the Float32Array
recordedChunks.push(new Float32Array(channelData));
};
mediaStreamSource.connect(processor);
processor.connect(audioContext.destination);
isRecording = true;
micBtn.classList.add('recording');
status.innerText = "Listening...";
} catch (err) {
console.error("Error accessing mic:", err);
alert("Please allow microphone access!");
}
};
// --- WAV Encoding Logic ---
function exportWAV(chunks, sampleRate) {
let length = 0;
for (let i = 0; i < chunks.length; i++) {
length += chunks[i].length;
}
const buffer = new Float32Array(length);
let offset = 0;
for (let i = 0; i < chunks.length; i++) {
buffer.set(chunks[i], offset);
offset += chunks[i].length;
}
const wavBuffer = new ArrayBuffer(44 + buffer.length * 2);
const view = new DataView(wavBuffer);
writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + buffer.length * 2, true);
writeString(view, 8, 'WAVE');
writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true); // PCM format
view.setUint16(22, 1, true); // Mono channel
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * 2, true);
view.setUint16(32, 2, true);
view.setUint16(34, 16, true); // 16-bit
writeString(view, 36, 'data');
view.setUint32(40, buffer.length * 2, true);
floatTo16BitPCM(view, 44, buffer);
return new Blob([view], { type: 'audio/wav' });
}
function writeString(view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
function floatTo16BitPCM(output, offset, input) {
for (let i = 0; i < input.length; i++, offset += 2) {
let s = Math.max(-1, Math.min(1, input[i]));
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
}
// --------------------------
async function sendAudio(blob) {
loader.style.display = 'block';
chatBox.style.display = 'none';
const formData = new FormData();
formData.append('file', blob, 'recording.wav');
try {
const response = await fetch('/voice', {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error("Server error");
// Get text from headers and decode Yoruba characters
userMsg.innerText = decodeURIComponent(response.headers.get('X-Input-Text') || "No text detected");
assistantMsg.innerText = decodeURIComponent(response.headers.get('X-Response-Text') || "No response generated");
// Get audio blob
const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
loader.style.display = 'none';
chatBox.style.display = 'block';
status.innerText = "Ready";
audioPlayer.src = audioUrl;
audioPlayer.load(); // Ensure it's loaded
audioPlayer.play().catch(e => {
console.error("Auto-play failed. Please click play manually:", e);
status.innerText = "Click play to hear response";
});
} catch (err) {
console.error("Upload failed:", err);
status.innerText = "Error!";
loader.style.display = 'none';
}
}
</script>
</body>
</html>