Files
aionboarding/ai-patient-onboarding/docs/index.html
T

871 lines
31 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CureSelect AI - Clinical Voice Intake & Real-Time EHR Dashboard</title>
<!-- Google Fonts & FontAwesome Icons -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--bg-dark: #090d16;
--bg-card: rgba(17, 24, 39, 0.75);
--bg-card-hover: rgba(31, 41, 55, 0.85);
--border-color: rgba(255, 255, 255, 0.08);
--accent-blue: #3b82f6;
--accent-cyan: #06b6d4;
--accent-green: #10b981;
--accent-red: #ef4444;
--accent-amber: #f59e0b;
--accent-purple: #8b5cf6;
--text-main: #f3f4f6;
--text-muted: #9ca3af;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Inter', sans-serif;
}
body {
background-color: var(--bg-dark);
background-image:
radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.12) 0px, transparent 50%),
radial-gradient(at 100% 100%, rgba(139, 92, 246, 0.12) 0px, transparent 50%);
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Top Header Navigation */
header {
background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--border-color);
padding: 16px 32px;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.logo-container {
display: flex;
align-items: center;
gap: 12px;
}
.logo-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, var(--accent-blue), var(--accent-cyan));
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: white;
box-shadow: 0 0 20px rgba(6, 182, 212, 0.4);
}
.logo-text {
font-size: 20px;
font-weight: 700;
background: linear-gradient(90deg, #ffffff, #93c5fd);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.status-badge {
display: flex;
align-items: center;
gap: 8px;
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
color: var(--accent-green);
padding: 6px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
}
.pulse-dot {
width: 8px;
height: 8px;
background-color: var(--accent-green);
border-radius: 50%;
box-shadow: 0 0 10px var(--accent-green);
animation: pulse 1.8s infinite;
}
@keyframes pulse {
0% { transform: scale(0.95); opacity: 0.8; }
50% { transform: scale(1.3); opacity: 1; }
100% { transform: scale(0.95); opacity: 0.8; }
}
/* Critical Escalation Banner */
.nurse-banner {
display: none;
background: linear-gradient(90deg, #7f1d1d, #b91c1c);
border-bottom: 2px solid #f87171;
color: white;
padding: 14px 32px;
align-items: center;
justify-content: space-between;
font-weight: 600;
animation: bannerFlash 1s infinite alternate;
}
@keyframes bannerFlash {
from { background: linear-gradient(90deg, #7f1d1d, #b91c1c); }
to { background: linear-gradient(90deg, #991b1b, #dc2626); }
}
.nurse-banner.active {
display: flex;
}
/* Main Container Grid */
.main-container {
flex: 1;
padding: 24px 32px;
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 24px;
max-width: 1600px;
margin: 0 auto;
width: 100%;
}
/* Card Styles */
.glass-card {
background: var(--bg-card);
backdrop-filter: blur(16px);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 24px;
display: flex;
flex-direction: column;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #e5e7eb;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 10px;
}
/* Left Column: Voice Interaction & Live Feed */
.voice-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.mic-control-box {
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 16px;
padding: 24px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
position: relative;
overflow: hidden;
}
.waveform-canvas {
width: 100%;
height: 80px;
margin: 10px 0;
}
.mic-button {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent-blue), var(--accent-cyan));
border: none;
color: white;
font-size: 30px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 30px rgba(6, 182, 212, 0.4);
transition: all 0.3s ease;
outline: none;
}
.mic-button.recording {
background: linear-gradient(135deg, #ef4444, #dc2626);
box-shadow: 0 0 40px rgba(239, 68, 68, 0.6);
animation: pulseRecord 1.5s infinite;
}
@keyframes pulseRecord {
0% { transform: scale(1); }
50% { transform: scale(1.08); }
100% { transform: scale(1); }
}
.action-buttons {
display: flex;
gap: 12px;
margin-top: 16px;
}
.btn {
background: rgba(255, 255, 255, 0.08);
border: 1px solid var(--border-color);
color: var(--text-main);
padding: 8px 16px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn:hover {
background: rgba(255, 255, 255, 0.15);
}
.btn-danger {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.4);
color: #fca5a5;
}
.btn-danger:hover {
background: rgba(239, 68, 68, 0.35);
}
/* Conversation Transcript Area */
.chat-area {
flex: 1;
min-height: 380px;
max-height: 480px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 14px;
padding-right: 8px;
}
.chat-bubble {
max-width: 85%;
padding: 14px 18px;
border-radius: 14px;
font-size: 14px;
line-height: 1.5;
position: relative;
}
.chat-bubble.ai {
background: rgba(30, 41, 59, 0.8);
border: 1px solid rgba(59, 130, 246, 0.2);
align-self: flex-start;
border-bottom-left-radius: 2px;
}
.chat-bubble.patient {
background: rgba(14, 116, 144, 0.3);
border: 1px solid rgba(6, 182, 212, 0.3);
align-self: flex-end;
border-bottom-right-radius: 2px;
}
.chat-bubble.caregiver {
background: rgba(126, 34, 206, 0.25);
border: 1px solid rgba(168, 85, 247, 0.3);
align-self: flex-end;
border-bottom-right-radius: 2px;
}
.bubble-meta {
font-size: 11px;
color: var(--text-muted);
margin-bottom: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.speaker-tag {
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
}
.tag-patient { background: rgba(6, 182, 212, 0.2); color: var(--accent-cyan); }
.tag-caregiver { background: rgba(168, 85, 247, 0.2); color: #c084fc; }
.tag-ai { background: rgba(59, 130, 246, 0.2); color: #93c5fd; }
/* Right Column: Real-Time Clinical Intake State & EHR Controls */
.clinical-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.metric-card {
background: rgba(15, 23, 42, 0.6);
border: 1px solid var(--border-color);
padding: 14px;
border-radius: 10px;
}
.metric-label {
font-size: 12px;
color: var(--text-muted);
margin-bottom: 4px;
}
.metric-value {
font-size: 18px;
font-weight: 700;
color: #f3f4f6;
}
/* Drug Warning Alert Card */
.warning-card {
display: none;
background: rgba(245, 158, 11, 0.12);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: 12px;
padding: 14px;
margin-top: 10px;
}
.warning-card.active {
display: block;
}
.warning-header {
color: var(--accent-amber);
font-size: 13px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
/* State JSON Tree View */
.state-box {
background: #050811;
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 14px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: #38bdf8;
max-height: 250px;
overflow-y: auto;
white-space: pre-wrap;
}
/* Modal Overlay */
.modal-overlay {
display: none;
position.fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
z-index: 200;
align-items: center;
justify-content: center;
}
.modal-overlay.active {
display: flex;
}
.modal-card {
background: #0f172a;
border: 1px solid var(--border-color);
border-radius: 16px;
width: 90%;
max-width: 850px;
max-height: 85vh;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
}
.modal-header {
padding: 18px 24px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 24px;
overflow-y: auto;
flex: 1;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
background: #090d16;
color: #a7f3d0;
}
.modal-footer {
padding: 16px 24px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>
</head>
<body>
<!-- Header Navigation -->
<header>
<div class="logo-container">
<div class="logo-icon"><i class="fa-solid fa-heart-pulse"></i></div>
<div class="logo-text">CureSelect AI Voice Intake</div>
</div>
<div style="display: flex; gap: 16px; align-items: center;">
<div class="status-badge">
<div class="pulse-dot"></div>
<span id="wsStatus">Connected (ws://localhost:8000/v1/voice-onboarding/ws/auto)</span>
</div>
</div>
</header>
<!-- Critical Nurse Escalation Banner -->
<div id="nurseBanner" class="nurse-banner">
<div style="display: flex; align-items: center; gap: 12px;">
<i class="fa-solid fa-triangle-exclamation fa-lg"></i>
<div>
<span style="font-size: 15px;">CRITICAL NURSE ESCALATION ALERT:</span>
<span id="bannerText" style="font-weight: 400; margin-left: 8px;">High Pain Score (9/10) reported. Immediate clinical review required!</span>
</div>
</div>
<button class="btn btn-danger" onclick="dismissBanner()">Acknowledge</button>
</div>
<!-- Main Workspace Container -->
<div class="main-container">
<!-- Left Column: Voice Interaction & Live Feed -->
<div class="voice-section">
<div class="glass-card mic-control-box">
<div class="card-title" style="margin-bottom: 8px;">
<i class="fa-solid fa-microphone-lines" style="color: var(--accent-cyan);"></i>
Real-Time Audio Streamer
</div>
<canvas id="waveform" class="waveform-canvas"></canvas>
<button id="micBtn" class="mic-button" onclick="toggleMic()">
<i class="fa-solid fa-microphone"></i>
</button>
<div id="micStatusText" style="margin-top: 12px; font-size: 13px; color: var(--text-muted);">
Click microphone to start full-duplex voice intake
</div>
<div class="action-buttons">
<button class="btn btn-danger" onclick="triggerInterrupt()">
<i class="fa-solid fa-hand"></i> Interrupt AI (Barge-In)
</button>
<button class="btn" onclick="sendSampleTurn('Tanglish')">
<i class="fa-solid fa-language"></i> Tanglish Turn
</button>
<button class="btn" onclick="sendSampleTurn('Caregiver')">
<i class="fa-solid fa-people-roof"></i> Caregiver Turn
</button>
<button class="btn" onclick="sendSampleTurn('HighPain')">
<i class="fa-solid fa-heart-crack"></i> High Pain (9/10)
</button>
<button class="btn" onclick="sendSampleTurn('DrugInteraction')">
<i class="fa-solid fa-pills"></i> Drug Warning
</button>
</div>
</div>
<!-- Conversation Transcript Area -->
<div class="glass-card" style="flex: 1;">
<div class="card-title">
<i class="fa-solid fa-comments" style="color: var(--accent-purple);"></i>
Live Conversation Stream
</div>
<div id="chatArea" class="chat-area">
<div class="chat-bubble ai">
<div class="bubble-meta">
<span class="speaker-tag tag-ai">AI Assistant</span>
<span>Greeting</span>
</div>
Hello Patient, welcome to CureSelect Healthcare. Could you please share what health symptoms you are experiencing today?
</div>
</div>
</div>
</div>
<!-- Right Column: Real-Time Clinical Intake State & EHR Controls -->
<div class="clinical-section">
<div class="glass-card">
<div class="card-title">
<i class="fa-solid fa-notes-medical" style="color: var(--accent-green);"></i>
Live Clinical Metrics
</div>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-label">Pain Score</div>
<div id="metricPain" class="metric-value">3 / 10</div>
</div>
<div class="metric-card">
<div class="metric-label">Distress Score</div>
<div id="metricDistress" class="metric-value">0.20</div>
</div>
<div class="metric-card">
<div class="metric-label">TTFA Latency</div>
<div id="metricTtfa" class="metric-value">⚡ 180 ms</div>
</div>
<div class="metric-card">
<div class="metric-label">Speech Pace</div>
<div id="metricPace" class="metric-value">1.0x (Standard)</div>
</div>
</div>
<!-- RxNorm / FDA Drug Safety Warning Card -->
<div id="drugWarningCard" class="warning-card">
<div class="warning-header">
<i class="fa-solid fa-shield-virus"></i> RxNorm / FDA Drug Safety Warning
</div>
<div id="warningCardText" style="font-size: 12px; color: #fef08a;">
No drug interaction warnings detected.
</div>
</div>
</div>
<!-- State JSON Tree View -->
<div class="glass-card" style="flex: 1;">
<div class="card-title" style="justify-content: space-between;">
<div>
<i class="fa-solid fa-code" style="color: var(--accent-blue);"></i>
Extracted Clinical State
</div>
<div style="display: flex; gap: 8px;">
<button class="btn" onclick="openFhirModal()">
<i class="fa-solid fa-fire"></i> FHIR R4
</button>
<button class="btn" onclick="openSoapModal()">
<i class="fa-solid fa-file-medical"></i> SOAP Note
</button>
</div>
</div>
<div id="jsonState" class="state-box">
{
"reason_for_visit": "general_intake",
"symptoms": [],
"medical_history": [],
"medications": [],
"allergies": []
}
</div>
</div>
</div>
</div>
<!-- Modal Dialog for FHIR & SOAP Note Viewers -->
<div id="modalOverlay" class="modal-overlay">
<div class="modal-card">
<div class="modal-header">
<div id="modalTitle" style="font-size: 16px; font-weight: 600; color: white;">HL7 FHIR R4 Bundle JSON</div>
<button class="btn" onclick="closeModal()"><i class="fa-solid fa-xmark"></i> Close</button>
</div>
<div id="modalBody" class="modal-body">
Loading...
</div>
<div class="modal-footer">
<button class="btn" onclick="copyModalContent()"><i class="fa-solid fa-copy"></i> Copy JSON</button>
<button id="modalHtmlBtn" class="btn" style="display: none;" onclick="openPrintableHtml()"><i class="fa-solid fa-print"></i> Open Printable Report</button>
</div>
</div>
</div>
<!-- WebSockets & Web Audio Script -->
<script>
let ws;
let isRecording = false;
let audioContext;
let mediaStream;
let currentSessionId = "";
// Establish WebSocket Connection
function connectWebSocket() {
ws = new WebSocket("ws://localhost:8000/v1/voice-onboarding/ws/auto");
ws.onopen = () => {
document.getElementById("wsStatus").innerText = "Connected (ws://localhost:8000/v1/voice-onboarding/ws/auto)";
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleWSMessage(data);
};
ws.onclose = () => {
document.getElementById("wsStatus").innerText = "Disconnected. Retrying...";
setTimeout(connectWebSocket, 2000);
};
}
// Handle WebSocket Message Events
function handleWSMessage(data) {
console.log("WS Event Received:", data);
if (data.type === "connected") {
currentSessionId = data.session_id;
} else if (data.type === "transcript") {
addChatBubble(data.role, data.text, data.speaker_label, data.speaker_type);
} else if (data.type === "backchannel") {
console.log("⚡ Instant Backchannel:", data.reply);
} else if (data.type === "speculative_audio_chunk") {
document.getElementById("metricTtfa").innerText = `⚡ ${data.ttfa_ms} ms`;
} else if (data.type === "ai_response") {
addChatBubble("assistant", data.reply, "AI Assistant", "ai");
if (data.audio_base64) {
playAudioBase64(data.audio_base64);
}
if (data.state_updates) {
updateClinicalState(data.state_updates);
}
} else if (data.type === "cancel_playback") {
console.log("🛑 Playback Interrupted by User!");
} else if (data.type === "nurse_escalation_alert") {
showNurseBanner(data.message);
document.getElementById("metricPain").innerText = `${data.pain_score} / 10`;
document.getElementById("metricDistress").innerText = `${data.distress_score}`;
} else if (data.type === "drug_safety_warning") {
showDrugWarnings(data.warnings);
}
}
// Play Audio Chunk
function playAudioBase64(b64) {
try {
const audio = new Audio("data:audio/wav;base64," + b64);
audio.play().catch(e => console.error("Audio playback error:", e));
} catch (e) {
console.error("Audio error:", e);
}
}
// Render Chat Bubbles
function addChatBubble(role, text, label, speakerType) {
const chatArea = document.getElementById("chatArea");
const bubble = document.createElement("div");
let bubbleClass = "chat-bubble ";
let tagClass = "speaker-tag ";
if (role === "assistant" || speakerType === "ai") {
bubbleClass += "ai";
tagClass += "tag-ai";
} else if (speakerType === "caregiver") {
bubbleClass += "caregiver";
tagClass += "tag-caregiver";
} else {
bubbleClass += "patient";
tagClass += "tag-patient";
}
bubble.className = bubbleClass;
bubble.innerHTML = `
<div class="bubble-meta">
<span class="${tagClass}">${label || (role === 'assistant' ? 'AI Assistant' : 'Patient')}</span>
<span>${new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
</div>
${text}
`;
chatArea.appendChild(bubble);
chatArea.scrollTop = chatArea.scrollHeight;
}
// Update Clinical State Tree
function updateClinicalState(state) {
document.getElementById("jsonState").innerText = JSON.stringify(state, null, 2);
}
// Mic Toggle
async function toggleMic() {
const micBtn = document.getElementById("micBtn");
const statusText = document.getElementById("micStatusText");
if (!isRecording) {
try {
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
isRecording = true;
micBtn.classList.add("recording");
statusText.innerText = "Streaming microphone audio... Speak freely.";
const mediaRecorder = new MediaRecorder(mediaStream);
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0 && ws.readyState === WebSocket.OPEN) {
ws.send(e.data);
}
};
mediaRecorder.start(250);
window.currentMediaRecorder = mediaRecorder;
} catch (e) {
alert("Microphone permission denied or not available: " + e.message);
}
} else {
isRecording = false;
micBtn.classList.remove("recording");
statusText.innerText = "Microphone paused. Click to resume.";
if (window.currentMediaRecorder) window.currentMediaRecorder.stop();
if (mediaStream) mediaStream.getTracks().forEach(t => t.stop());
ws.send(JSON.stringify({ type: "finish_turn" }));
}
}
function triggerInterrupt() {
ws.send(JSON.stringify({ type: "interrupt" }));
}
function sendSampleTurn(type) {
if (type === "Tanglish") {
ws.send(JSON.stringify({
type: "text_turn",
text: "10 days-ah severe headache iruku, Metformin 500mg daily sapudren."
}));
} else if (type === "Caregiver") {
ws.send(JSON.stringify({
type: "text_turn",
text: "My mother has severe headache for 10 days and she is taking Metformin 500mg."
}));
} else if (type === "HighPain") {
ws.send(JSON.stringify({
type: "text_turn",
text: "My chest pain is 9 out of 10 unbearable, I cannot breathe properly!"
}));
} else if (type === "DrugInteraction") {
ws.send(JSON.stringify({
type: "text_turn",
text: "I am taking Warfarin and Aspirin daily, and I have severe Penicillin allergy."
}));
}
}
function showNurseBanner(msg) {
document.getElementById("bannerText").innerText = msg;
document.getElementById("nurseBanner").classList.add("active");
}
function dismissBanner() {
document.getElementById("nurseBanner").classList.remove("active");
}
function showDrugWarnings(warnings) {
const card = document.getElementById("drugWarningCard");
const text = document.getElementById("warningCardText");
if (warnings && warnings.length > 0) {
text.innerText = warnings.map(w => `[${w.severity}] ${w.category}: ${w.description}`).join("\n\n");
card.classList.add("active");
}
}
// Modals for FHIR & SOAP Notes
async function openFhirModal() {
if (!currentSessionId) return alert("No active session ID");
document.getElementById("modalTitle").innerText = "HL7 FHIR R4 Bundle JSON";
document.getElementById("modalBody").innerText = "Loading FHIR R4 Bundle...";
document.getElementById("modalHtmlBtn").style.display = "none";
document.getElementById("modalOverlay").classList.add("active");
try {
const res = await fetch(`http://localhost:8000/v1/voice-onboarding/fhir/${currentSessionId}`, {
headers: { "X-API-Key": "skyheal_secure_api_key_2026" }
});
const bundle = await res.json();
document.getElementById("modalBody").innerText = JSON.stringify(bundle, null, 2);
} catch (e) {
document.getElementById("modalBody").innerText = "Error fetching FHIR Bundle: " + e.message;
}
}
async function openSoapModal() {
if (!currentSessionId) return alert("No active session ID");
document.getElementById("modalTitle").innerText = "Clinical SOAP Note (Subjective, Objective, Assessment, Plan)";
document.getElementById("modalBody").innerText = "Compiling SOAP Note & ICD-10 Coding...";
document.getElementById("modalHtmlBtn").style.display = "inline-flex";
document.getElementById("modalOverlay").classList.add("active");
try {
const res = await fetch(`http://localhost:8000/v1/voice-onboarding/soap-note/${currentSessionId}`, {
headers: { "X-API-Key": "skyheal_secure_api_key_2026" }
});
const note = await res.json();
document.getElementById("modalBody").innerText = JSON.stringify(note, null, 2);
} catch (e) {
document.getElementById("modalBody").innerText = "Error compiling SOAP Note: " + e.message;
}
}
function closeModal() {
document.getElementById("modalOverlay").classList.remove("active");
}
function copyModalContent() {
const text = document.getElementById("modalBody").innerText;
navigator.clipboard.writeText(text);
alert("Copied to clipboard!");
}
function openPrintableHtml() {
if (currentSessionId) {
window.open(`http://localhost:8000/v1/voice-onboarding/soap-note/${currentSessionId}?format=html`, '_blank');
}
}
// Initialize on load
window.onload = connectWebSocket;
</script>
</body>
</html>