AfriHealth / index.html
sirlaw-dev's picture
Update index.html
6a63327 verified
Raw
History Blame Contribute Delete
20.1 kB
<!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 (Pidgin)</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;
}
}
.mode-tabs {
display: flex;
background: rgba(255, 255, 255, 0.05);
border-radius: 14px;
padding: 4px;
margin-bottom: 30px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.mode-tab {
flex: 1;
padding: 10px 16px;
border: none;
background: transparent;
color: #94a3b8;
font-size: 0.95rem;
font-weight: 500;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
}
.mode-tab.active {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
box-shadow: 0 4px 15px rgba(0, 210, 255, 0.3);
}
.chat-input-container {
display: flex;
gap: 10px;
margin: 30px 0;
width: 100%;
}
.chat-input {
flex: 1;
background: rgba(255, 255, 255, 0.07);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 14px;
padding: 14px 18px;
color: white;
font-size: 0.95rem;
outline: none;
transition: all 0.3s;
}
.chat-input:focus {
border-color: var(--primary);
box-shadow: 0 0 15px rgba(0, 210, 255, 0.2);
}
.chat-submit-btn {
background: linear-gradient(135deg, var(--primary), var(--secondary));
border: none;
border-radius: 14px;
padding: 0 20px;
color: white;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.chat-submit-btn:hover {
transform: scale(1.05);
box-shadow: 0 0 20px rgba(0, 210, 255, 0.4);
}
.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 (Pidgin)</h1>
<p class="subtitle">Talk how your body dey do you make I help you</p>
<div class="mode-tabs">
<button class="mode-tab active" id="tabVoice" onclick="switchMode('voice')">🎤 Talk (Voice)</button>
<button class="mode-tab" id="tabChat" onclick="switchMode('chat')">💬 Type (Chat)</button>
</div>
<div class="mic-container" id="voiceModeArea">
<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 class="chat-input-container" id="chatModeArea" style="display: none;">
<input type="text" id="chatInput" class="chat-input" placeholder="Type wetin dey do you... (e.g. body dey scratch me)">
<button class="chat-submit-btn" id="chatSubmitBtn">Send</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 dey think<span>.</span><span>.</span><span>.</span>
</div>
<div class="chat-box" id="chatBox">
<div class="message">
<div class="label">Wetin you talk</div>
<div class="content" id="userMsg">...</div>
</div>
<div class="message" id="matchedSymptomContainer" style="display: none;">
<div class="label">Symptom wey match (from Knowledge Base)</div>
<div class="content" id="matchedSymptomMsg" style="border-left-color: var(--secondary);">...</div>
</div>
<div class="message">
<div class="label">Wetin AI recommend</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 Voice (Osagie)</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 = "Dey process...";
// 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 = "Dey listen...";
} catch (err) {
console.error("Error accessing mic:", err);
alert("Abeg allow micro-phone make you fit talk!");
}
};
// --- 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);
}
}
// --------------------------
function switchMode(mode) {
if (mode === 'voice') {
document.getElementById('tabVoice').classList.add('active');
document.getElementById('tabChat').classList.remove('active');
document.getElementById('voiceModeArea').style.display = 'flex';
document.getElementById('chatModeArea').style.display = 'none';
} else {
document.getElementById('tabVoice').classList.remove('active');
document.getElementById('tabChat').classList.add('active');
document.getElementById('voiceModeArea').style.display = 'none';
document.getElementById('chatModeArea').style.display = 'flex';
document.getElementById('chatInput').focus();
}
}
async function handleResponse(response) {
if (!response.ok) throw new Error("Server error");
// Get text from headers and decode Pidgin characters
const inputVal = decodeURIComponent(response.headers.get('X-Input-Text') || "No pidgin text detected");
const responseVal = decodeURIComponent(response.headers.get('X-Response-Text') || "No recommendation generated");
const matchedSymptom = decodeURIComponent(response.headers.get('X-Matched-Symptom') || "");
const matchedPhrase = decodeURIComponent(response.headers.get('X-Matched-Phrase') || "");
userMsg.innerText = inputVal;
assistantMsg.innerText = responseVal;
const matchedSymptomContainer = document.getElementById('matchedSymptomContainer');
const matchedSymptomMsg = document.getElementById('matchedSymptomMsg');
if (matchedSymptom) {
matchedSymptomMsg.innerText = `${matchedSymptom.toUpperCase()} (Wey you talk: "${matchedPhrase}")`;
matchedSymptomContainer.style.display = 'block';
} else {
matchedSymptomMsg.innerText = "No exact symptom match found (Dey use default advice)";
matchedSymptomContainer.style.display = 'block';
}
// 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 = "Press play to hear voice";
});
}
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
});
await handleResponse(response);
} catch (err) {
console.error("Upload failed:", err);
status.innerText = "Error!";
loader.style.display = 'none';
}
}
async function sendChatText(text) {
if (!text || !text.trim()) return;
loader.style.display = 'block';
chatBox.style.display = 'none';
status.innerText = "Dey process chat...";
try {
const response = await fetch('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text.trim() })
});
await handleResponse(response);
document.getElementById('chatInput').value = '';
} catch (err) {
console.error("Chat failed:", err);
status.innerText = "Error!";
loader.style.display = 'none';
}
}
document.getElementById('chatSubmitBtn').onclick = () => {
const text = document.getElementById('chatInput').value;
sendChatText(text);
};
document.getElementById('chatInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const text = document.getElementById('chatInput').value;
sendChatText(text);
}
});
</script>
</body>
</html>