feat: complete real-time voice-ai engine upgrades and frontend integration guide
This commit is contained in:
@@ -0,0 +1,870 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user