1070 lines
31 KiB
HTML
1070 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>AI Patient Onboarding - Real-Time WebSocket Tester</title>
|
|
<!-- Google Fonts -->
|
|
<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&family=Outfit:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
|
|
<style>
|
|
:root {
|
|
--bg-dark: #090d16;
|
|
--bg-card: rgba(17, 24, 39, 0.75);
|
|
--bg-card-border: rgba(255, 255, 255, 0.08);
|
|
--primary: #6366f1;
|
|
--primary-glow: rgba(99, 102, 241, 0.35);
|
|
--cyan: #06b6d4;
|
|
--cyan-glow: rgba(6, 182, 212, 0.3);
|
|
--emerald: #10b981;
|
|
--emerald-glow: rgba(16, 185, 129, 0.3);
|
|
--amber: #f59e0b;
|
|
--rose: #f43f5e;
|
|
--rose-glow: rgba(244, 63, 94, 0.4);
|
|
--text-main: #f3f4f6;
|
|
--text-muted: #9ca3af;
|
|
--text-dim: #6b7280;
|
|
--radius-lg: 16px;
|
|
--radius-md: 12px;
|
|
--radius-sm: 8px;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Inter', sans-serif;
|
|
background: var(--bg-dark);
|
|
background-image:
|
|
radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.15) 0px, transparent 50%),
|
|
radial-gradient(at 100% 100%, rgba(6, 182, 212, 0.12) 0px, transparent 50%),
|
|
radial-gradient(at 50% 50%, rgba(17, 24, 39, 0.9) 0px, transparent 100%);
|
|
color: var(--text-main);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* Top Navbar */
|
|
header {
|
|
background: rgba(15, 23, 42, 0.8);
|
|
backdrop-filter: blur(16px);
|
|
-webkit-backdrop-filter: blur(16px);
|
|
border-bottom: 1px solid var(--bg-card-border);
|
|
padding: 1rem 1.75rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
}
|
|
|
|
.brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.brand-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
background: linear-gradient(135deg, var(--primary), var(--cyan));
|
|
border-radius: var(--radius-md);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.25rem;
|
|
box-shadow: 0 0 20px var(--primary-glow);
|
|
}
|
|
|
|
.brand-title {
|
|
font-family: 'Outfit', sans-serif;
|
|
font-weight: 700;
|
|
font-size: 1.25rem;
|
|
letter-spacing: -0.02em;
|
|
background: linear-gradient(90deg, #ffffff, #cbd5e1);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.brand-subtitle {
|
|
font-size: 0.75rem;
|
|
color: var(--cyan);
|
|
font-weight: 600;
|
|
letter-spacing: 0.05em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* Live Connection Indicator */
|
|
.status-badge {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
background: rgba(30, 41, 59, 0.7);
|
|
border: 1px solid var(--bg-card-border);
|
|
padding: 0.4rem 0.9rem;
|
|
border-radius: 9999px;
|
|
font-size: 0.825rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
background: var(--text-dim);
|
|
position: relative;
|
|
}
|
|
|
|
.status-dot.connected {
|
|
background: var(--emerald);
|
|
box-shadow: 0 0 10px var(--emerald);
|
|
}
|
|
|
|
.status-dot.connected::after {
|
|
content: '';
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 50%;
|
|
background: var(--emerald);
|
|
animation: pulse 1.8s infinite;
|
|
}
|
|
|
|
.status-dot.connecting {
|
|
background: var(--amber);
|
|
box-shadow: 0 0 10px var(--amber);
|
|
}
|
|
|
|
.status-dot.disconnected {
|
|
background: var(--rose);
|
|
box-shadow: 0 0 10px var(--rose);
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% { transform: scale(1); opacity: 0.8; }
|
|
100% { transform: scale(2.5); opacity: 0; }
|
|
}
|
|
|
|
/* Emergency Alert Toast Overlay */
|
|
.emergency-toast {
|
|
position: fixed;
|
|
top: 5rem;
|
|
left: 50%;
|
|
transform: translateX(-50%) translateY(-20px);
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
background: linear-gradient(135deg, rgba(244, 63, 94, 0.95), rgba(159, 18, 57, 0.95));
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
box-shadow: 0 10px 40px var(--rose-glow);
|
|
backdrop-filter: blur(12px);
|
|
color: #ffffff;
|
|
padding: 1rem 1.75rem;
|
|
border-radius: var(--radius-md);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
z-index: 1000;
|
|
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
|
max-width: 600px;
|
|
width: 90%;
|
|
}
|
|
|
|
.emergency-toast.active {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
transform: translateX(-50%) translateY(0);
|
|
}
|
|
|
|
.emergency-icon {
|
|
font-size: 1.75rem;
|
|
animation: alertBounce 1s infinite alternate;
|
|
}
|
|
|
|
@keyframes alertBounce {
|
|
from { transform: scale(1); }
|
|
to { transform: scale(1.15); }
|
|
}
|
|
|
|
/* Main Container Layout */
|
|
main {
|
|
flex: 1;
|
|
display: grid;
|
|
grid-template-columns: 340px 1fr 380px;
|
|
gap: 1.25rem;
|
|
padding: 1.25rem;
|
|
max-width: 1750px;
|
|
margin: 0 auto;
|
|
width: 100%;
|
|
height: calc(100vh - 75px);
|
|
}
|
|
|
|
@media (max-width: 1400px) {
|
|
main {
|
|
grid-template-columns: 300px 1fr 320px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 1100px) {
|
|
main {
|
|
grid-template-columns: 1fr;
|
|
height: auto;
|
|
}
|
|
}
|
|
|
|
/* Card Panels */
|
|
.panel {
|
|
background: var(--bg-card);
|
|
backdrop-filter: blur(16px);
|
|
-webkit-backdrop-filter: blur(16px);
|
|
border: 1px solid var(--bg-card-border);
|
|
border-radius: var(--radius-lg);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.37);
|
|
}
|
|
|
|
.panel-header {
|
|
padding: 1rem 1.25rem;
|
|
background: rgba(255, 255, 255, 0.02);
|
|
border-bottom: 1px solid var(--bg-card-border);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.panel-title {
|
|
font-family: 'Outfit', sans-serif;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: var(--text-main);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.panel-body {
|
|
padding: 1.25rem;
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
/* Form Controls */
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.4rem;
|
|
}
|
|
|
|
.form-label {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.form-input, .form-select {
|
|
background: rgba(15, 23, 42, 0.6);
|
|
border: 1px solid var(--bg-card-border);
|
|
color: var(--text-main);
|
|
padding: 0.65rem 0.85rem;
|
|
border-radius: var(--radius-sm);
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 0.825rem;
|
|
outline: none;
|
|
transition: border-color 0.2s, box-shadow 0.2s;
|
|
}
|
|
|
|
.form-input:focus, .form-select:focus {
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 12px var(--primary-glow);
|
|
}
|
|
|
|
/* Buttons */
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
padding: 0.7rem 1.25rem;
|
|
border-radius: var(--radius-md);
|
|
font-weight: 600;
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
border: none;
|
|
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, var(--primary), #4f46e5);
|
|
color: white;
|
|
box-shadow: 0 4px 16px var(--primary-glow);
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 6px 20px var(--primary-glow);
|
|
}
|
|
|
|
.btn-cyan {
|
|
background: linear-gradient(135deg, var(--cyan), #0891b2);
|
|
color: white;
|
|
box-shadow: 0 4px 16px var(--cyan-glow);
|
|
}
|
|
|
|
.btn-cyan:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 6px 20px var(--cyan-glow);
|
|
}
|
|
|
|
.btn-danger {
|
|
background: rgba(244, 63, 94, 0.2);
|
|
border: 1px solid rgba(244, 63, 94, 0.4);
|
|
color: #fda4af;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background: rgba(244, 63, 94, 0.35);
|
|
}
|
|
|
|
.btn-sm {
|
|
padding: 0.4rem 0.75rem;
|
|
font-size: 0.775rem;
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
|
|
.btn-full {
|
|
width: 100%;
|
|
}
|
|
|
|
/* Chat Messages Stream */
|
|
.chat-messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
padding-right: 0.4rem;
|
|
}
|
|
|
|
.message-bubble {
|
|
max-width: 82%;
|
|
padding: 0.85rem 1.1rem;
|
|
border-radius: var(--radius-md);
|
|
font-size: 0.9rem;
|
|
line-height: 1.5;
|
|
position: relative;
|
|
animation: fadeIn 0.25s ease-out;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(8px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.message-bubble.assistant {
|
|
align-self: flex-start;
|
|
background: rgba(30, 41, 59, 0.9);
|
|
border: 1px solid rgba(99, 102, 241, 0.25);
|
|
border-bottom-left-radius: 2px;
|
|
color: #f1f5f9;
|
|
}
|
|
|
|
.message-bubble.patient {
|
|
align-self: flex-end;
|
|
background: linear-gradient(135deg, var(--primary), #4338ca);
|
|
color: white;
|
|
border-bottom-right-radius: 2px;
|
|
box-shadow: 0 4px 14px var(--primary-glow);
|
|
}
|
|
|
|
.message-bubble.system {
|
|
align-self: center;
|
|
background: rgba(15, 23, 42, 0.7);
|
|
border: 1px solid var(--bg-card-border);
|
|
color: var(--text-muted);
|
|
font-size: 0.775rem;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
padding: 0.4rem 0.85rem;
|
|
border-radius: 9999px;
|
|
}
|
|
|
|
.message-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
font-size: 0.7rem;
|
|
opacity: 0.7;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
/* Typing Indicator */
|
|
.typing-indicator {
|
|
align-self: flex-start;
|
|
background: rgba(30, 41, 59, 0.8);
|
|
border: 1px solid var(--bg-card-border);
|
|
padding: 0.6rem 1rem;
|
|
border-radius: var(--radius-md);
|
|
display: none;
|
|
align-items: center;
|
|
gap: 0.35rem;
|
|
}
|
|
|
|
.typing-indicator.active {
|
|
display: flex;
|
|
}
|
|
|
|
.typing-dot {
|
|
width: 7px;
|
|
height: 7px;
|
|
background: var(--cyan);
|
|
border-radius: 50%;
|
|
animation: bounce 1.4s infinite ease-in-out both;
|
|
}
|
|
|
|
.typing-dot:nth-child(1) { animation-delay: -0.32s; }
|
|
.typing-dot:nth-child(2) { animation-delay: -0.16s; }
|
|
|
|
@keyframes bounce {
|
|
0%, 80%, 100% { transform: scale(0); }
|
|
40% { transform: scale(1); }
|
|
}
|
|
|
|
/* Quick Chips */
|
|
.quick-chips {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem 0;
|
|
}
|
|
|
|
.chip {
|
|
background: rgba(30, 41, 59, 0.6);
|
|
border: 1px solid var(--bg-card-border);
|
|
color: var(--text-muted);
|
|
font-size: 0.775rem;
|
|
padding: 0.35rem 0.75rem;
|
|
border-radius: 9999px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.chip:hover {
|
|
background: rgba(99, 102, 241, 0.25);
|
|
border-color: var(--primary);
|
|
color: var(--text-main);
|
|
}
|
|
|
|
/* Clinical State Dashboard */
|
|
.stage-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
background: rgba(6, 182, 212, 0.15);
|
|
border: 1px solid rgba(6, 182, 212, 0.3);
|
|
color: var(--cyan);
|
|
padding: 0.3rem 0.75rem;
|
|
border-radius: var(--radius-sm);
|
|
font-size: 0.8rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
.state-card {
|
|
background: rgba(15, 23, 42, 0.6);
|
|
border: 1px solid var(--bg-card-border);
|
|
border-radius: var(--radius-md);
|
|
padding: 0.85rem;
|
|
}
|
|
|
|
.state-card-title {
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
margin-bottom: 0.5rem;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.fact-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.3rem 0;
|
|
font-size: 0.825rem;
|
|
border-bottom: 1px dashed rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
.fact-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.fact-key {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.fact-val {
|
|
font-weight: 600;
|
|
color: var(--cyan);
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
/* Protocol Event Log */
|
|
.event-log {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 0.75rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
overflow-y: auto;
|
|
max-height: 100%;
|
|
}
|
|
|
|
.event-item {
|
|
background: rgba(15, 23, 42, 0.7);
|
|
border-left: 3px solid var(--primary);
|
|
padding: 0.6rem 0.75rem;
|
|
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.event-item.patient\.message { border-color: var(--primary); }
|
|
.event-item.assistant\.message { border-color: var(--cyan); }
|
|
.event-item.state\.updated { border-color: var(--emerald); }
|
|
.event-item.safety\.alert { border-color: var(--rose); background: rgba(244, 63, 94, 0.1); }
|
|
.event-item.error { border-color: var(--rose); }
|
|
|
|
.event-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 0.25rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.event-type {
|
|
color: var(--cyan);
|
|
}
|
|
|
|
.event-time {
|
|
color: var(--text-dim);
|
|
font-size: 0.7rem;
|
|
}
|
|
|
|
.event-json {
|
|
color: var(--text-muted);
|
|
white-space: pre-wrap;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Top Header -->
|
|
<header>
|
|
<div class="brand">
|
|
<div class="brand-icon">🏥</div>
|
|
<div>
|
|
<div class="brand-title">AI Patient Onboarding</div>
|
|
<div class="brand-subtitle">Real-Time WebSocket Suite</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="status-badge" id="statusBadge">
|
|
<span class="status-dot disconnected" id="statusDot"></span>
|
|
<span id="statusText">Disconnected</span>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Emergency Toast Overlay -->
|
|
<div class="emergency-toast" id="emergencyToast">
|
|
<div class="emergency-icon">🚨</div>
|
|
<div>
|
|
<div style="font-weight: 700; font-size: 1rem;">CRITICAL SAFETY ALERT</div>
|
|
<div style="font-size: 0.85rem; opacity: 0.9;" id="emergencyText">Emergency referral triggered.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Grid -->
|
|
<main>
|
|
|
|
<!-- Left Column: Session Controls & Config -->
|
|
<div class="panel">
|
|
<div class="panel-header">
|
|
<div class="panel-title">⚙️ Connection Config</div>
|
|
</div>
|
|
<div class="panel-body">
|
|
|
|
<button class="btn btn-cyan btn-full" id="btnCreateSession">
|
|
⚡ Create REST Session
|
|
</button>
|
|
|
|
<hr style="border: 0; border-top: 1px solid var(--bg-card-border);">
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Session ID</label>
|
|
<input type="text" class="form-input" id="inputSessionId" placeholder="ONB-XXXXXXXX">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">API Key / Token</label>
|
|
<input type="text" class="form-input" id="inputApiKey" value="dev_api_key_123">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Patient ID</label>
|
|
<input type="text" class="form-input" id="inputPatientId" value="patient_123">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Host URL (WS / HTTP)</label>
|
|
<input type="text" class="form-input" id="inputHost" value="http://localhost:8000">
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
|
|
<button class="btn btn-primary btn-full" id="btnConnect">Connect WS</button>
|
|
<button class="btn btn-danger" id="btnDisconnect">Disconnect</button>
|
|
</div>
|
|
|
|
<div class="state-card" style="margin-top: auto;">
|
|
<div class="state-card-title">Session Metadata</div>
|
|
<div class="fact-item">
|
|
<span class="fact-key">Language:</span>
|
|
<span class="fact-val" id="metaLang">en-IN</span>
|
|
</div>
|
|
<div class="fact-item">
|
|
<span class="fact-key">Specialty:</span>
|
|
<span class="fact-val" id="metaCategory">GENERAL</span>
|
|
</div>
|
|
<div class="fact-item">
|
|
<span class="fact-key">WS Transport:</span>
|
|
<span class="fact-val" id="metaTransport">WebSocket</span>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Center Column: Real-Time Assistant Chat -->
|
|
<div class="panel">
|
|
<div class="panel-header">
|
|
<div class="panel-title">💬 Live Clinical Dialogue</div>
|
|
<span class="stage-badge" id="badgeStage">GREETING</span>
|
|
</div>
|
|
|
|
<div class="panel-body">
|
|
|
|
<!-- Messages Stream -->
|
|
<div class="chat-messages" id="chatMessages">
|
|
<div class="message-bubble system">
|
|
Click "Create REST Session" or enter an active Session ID to start real-time onboarding.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Typing Indicator -->
|
|
<div class="typing-indicator" id="typingIndicator">
|
|
<span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 600;">AI Assistant is typing</span>
|
|
<div class="typing-dot"></div>
|
|
<div class="typing-dot"></div>
|
|
<div class="typing-dot"></div>
|
|
</div>
|
|
|
|
<!-- Quick Prompts Chips -->
|
|
<div class="quick-chips">
|
|
<span class="chip" onclick="sendQuickPrompt('I have severe headache for three days')">Headache 3 days</span>
|
|
<span class="chip" onclick="sendQuickPrompt('I have crushing chest pain and shortness of breath')">⚠️ Chest Pain</span>
|
|
<span class="chip" onclick="sendQuickPrompt('I take metformin 500mg once daily')">Metformin 500mg</span>
|
|
<span class="chip" onclick="sendQuickPrompt('I am allergic to penicillin')">Penicillin Allergy</span>
|
|
<span class="chip" onclick="sendQuickPrompt('No other medical history')">No History</span>
|
|
</div>
|
|
|
|
<!-- Input Bar -->
|
|
<div style="display: flex; gap: 0.5rem;">
|
|
<input type="text" class="form-input" style="flex: 1;" id="inputMessage" placeholder="Type a response to the healthcare AI..." onkeypress="handleKeyPress(event)">
|
|
<button class="btn btn-primary" id="btnSend">Send ↵</button>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column: Live Clinical State & Event Inspector -->
|
|
<div class="panel">
|
|
<div class="panel-header">
|
|
<div class="panel-title">📊 Clinical State & Protocol Frames</div>
|
|
</div>
|
|
<div class="panel-body" style="display: grid; grid-template-rows: 1fr 1fr; gap: 1rem;">
|
|
|
|
<!-- State Inspector -->
|
|
<div class="state-card" style="display: flex; flex-direction: column;">
|
|
<div class="state-card-title">Extracted Clinical Facts</div>
|
|
<div id="factsContainer" style="overflow-y: auto; flex: 1;">
|
|
<div class="fact-item">
|
|
<span class="fact-key">Primary Symptom:</span>
|
|
<span class="fact-val" id="factSymptom">--</span>
|
|
</div>
|
|
<div class="fact-item">
|
|
<span class="fact-key">Duration:</span>
|
|
<span class="fact-val" id="factDuration">--</span>
|
|
</div>
|
|
<div class="fact-item">
|
|
<span class="fact-key">Severity:</span>
|
|
<span class="fact-val" id="factSeverity">--</span>
|
|
</div>
|
|
<div class="fact-item">
|
|
<span class="fact-key">Medications:</span>
|
|
<span class="fact-val" id="factMeds">None</span>
|
|
</div>
|
|
<div class="fact-item">
|
|
<span class="fact-key">Allergies:</span>
|
|
<span class="fact-val" id="factAllergies">None</span>
|
|
</div>
|
|
<div class="fact-item">
|
|
<span class="fact-key">Safety Risk:</span>
|
|
<span class="fact-val" id="factSafety" style="color: var(--emerald);">Normal</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- WS Protocol Frame Inspector -->
|
|
<div class="state-card" style="display: flex; flex-direction: column;">
|
|
<div class="state-card-title" style="display: flex; justify-content: space-between;">
|
|
<span>WS Event Stream</span>
|
|
<span style="color: var(--primary); cursor: pointer;" onclick="clearEventLog()">Clear</span>
|
|
</div>
|
|
<div class="event-log" id="eventLog">
|
|
<!-- Dynamic JSON frames inserted here -->
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<script>
|
|
let ws = null;
|
|
let currentSessionId = null;
|
|
let currentApiKey = 'dev_api_key_123';
|
|
let currentPatientId = 'patient_123';
|
|
|
|
// DOM Elements
|
|
const statusDot = document.getElementById('statusDot');
|
|
const statusText = document.getElementById('statusText');
|
|
const inputSessionId = document.getElementById('inputSessionId');
|
|
const inputApiKey = document.getElementById('inputApiKey');
|
|
const inputPatientId = document.getElementById('inputPatientId');
|
|
const inputHost = document.getElementById('inputHost');
|
|
const btnCreateSession = document.getElementById('btnCreateSession');
|
|
const btnConnect = document.getElementById('btnConnect');
|
|
const btnDisconnect = document.getElementById('btnDisconnect');
|
|
const chatMessages = document.getElementById('chatMessages');
|
|
const typingIndicator = document.getElementById('typingIndicator');
|
|
const inputMessage = document.getElementById('inputMessage');
|
|
const btnSend = document.getElementById('btnSend');
|
|
const badgeStage = document.getElementById('badgeStage');
|
|
const emergencyToast = document.getElementById('emergencyToast');
|
|
const emergencyText = document.getElementById('emergencyText');
|
|
const eventLog = document.getElementById('eventLog');
|
|
|
|
// Clinical Fact Display Elements
|
|
const factSymptom = document.getElementById('factSymptom');
|
|
const factDuration = document.getElementById('factDuration');
|
|
const factSeverity = document.getElementById('factSeverity');
|
|
const factMeds = document.getElementById('factMeds');
|
|
const factAllergies = document.getElementById('factAllergies');
|
|
const factSafety = document.getElementById('factSafety');
|
|
|
|
// Create New Session via REST
|
|
btnCreateSession.addEventListener('click', async () => {
|
|
const host = inputHost.value.trim().replace(/\/$/, '');
|
|
const apiKey = inputApiKey.value.trim();
|
|
const patientId = inputPatientId.value.trim();
|
|
|
|
btnCreateSession.disabled = true;
|
|
btnCreateSession.innerText = 'Creating Session...';
|
|
|
|
try {
|
|
const response = await fetch(`${host}/v1/onboarding/start`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'x-api-key': apiKey,
|
|
'x-patient-id': patientId
|
|
},
|
|
body: JSON.stringify({
|
|
patient: { id: patientId, name: 'John Doe', age: 35, gender: 'male' },
|
|
category: { type: 'specialty', code: 'GENERAL', name: 'General Consultation' },
|
|
language: 'en-IN'
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.session_id) {
|
|
currentSessionId = data.session_id;
|
|
inputSessionId.value = data.session_id;
|
|
|
|
appendSystemMessage(`New Session Created: ${data.session_id}`);
|
|
if (data.message) {
|
|
appendAssistantMessage(data.message);
|
|
}
|
|
|
|
// Auto connect to WS
|
|
connectWebSocket();
|
|
} else {
|
|
alert('Failed to create session: ' + (data.detail || JSON.stringify(data)));
|
|
}
|
|
} catch (err) {
|
|
alert('Error connecting to REST backend: ' + err.message);
|
|
} finally {
|
|
btnCreateSession.disabled = false;
|
|
btnCreateSession.innerText = '⚡ Create REST Session';
|
|
}
|
|
});
|
|
|
|
// Connect WebSocket
|
|
function connectWebSocket() {
|
|
const sessionId = inputSessionId.value.trim();
|
|
const apiKey = inputApiKey.value.trim();
|
|
const patientId = inputPatientId.value.trim();
|
|
let host = inputHost.value.trim().replace(/\/$/, '');
|
|
|
|
if (!sessionId) {
|
|
alert('Please enter or create a Session ID first.');
|
|
return;
|
|
}
|
|
|
|
// Convert http/https to ws/wss
|
|
const wsProtocol = host.startsWith('https') ? 'wss' : 'ws';
|
|
const cleanHost = host.replace(/^https?:\/\//, '');
|
|
const wsUrl = `${wsProtocol}://${cleanHost}/api/v1/ws/onboarding/${sessionId}?token=${apiKey}&patient_id=${patientId}`;
|
|
|
|
updateStatus('connecting', 'Connecting...');
|
|
|
|
if (ws) {
|
|
ws.close();
|
|
}
|
|
|
|
ws = new WebSocket(wsUrl);
|
|
|
|
ws.onopen = () => {
|
|
updateStatus('connected', 'Connected');
|
|
appendSystemMessage('WebSocket Connected to Onboarding Session');
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
const payload = JSON.parse(event.data);
|
|
logEvent(payload);
|
|
handleIncomingWSMessage(payload);
|
|
} catch (e) {
|
|
console.error('Error parsing WS frame:', e);
|
|
}
|
|
};
|
|
|
|
ws.onerror = (error) => {
|
|
console.error('WS Error:', error);
|
|
updateStatus('disconnected', 'Error');
|
|
};
|
|
|
|
ws.onclose = (event) => {
|
|
updateStatus('disconnected', 'Disconnected');
|
|
appendSystemMessage(`WebSocket Disconnected (Code: ${event.code})`);
|
|
typingIndicator.classList.remove('active');
|
|
};
|
|
}
|
|
|
|
btnConnect.addEventListener('click', connectWebSocket);
|
|
|
|
btnDisconnect.addEventListener('click', () => {
|
|
if (ws) {
|
|
ws.close();
|
|
ws = null;
|
|
}
|
|
});
|
|
|
|
// Handle Messages Dispatching
|
|
function sendPatientMessage(text) {
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
alert('WebSocket is not connected. Click "Connect WS" first.');
|
|
return;
|
|
}
|
|
|
|
const messageId = 'msg_' + Math.random().toString(36).substr(2, 9);
|
|
const frame = {
|
|
type: 'patient.message',
|
|
message_id: messageId,
|
|
text: text
|
|
};
|
|
|
|
ws.send(JSON.stringify(frame));
|
|
logEvent(frame);
|
|
appendPatientMessage(text);
|
|
inputMessage.value = '';
|
|
}
|
|
|
|
btnSend.addEventListener('click', () => {
|
|
const text = inputMessage.value.trim();
|
|
if (text) sendPatientMessage(text);
|
|
});
|
|
|
|
function handleKeyPress(e) {
|
|
if (e.key === 'Enter') {
|
|
const text = inputMessage.value.trim();
|
|
if (text) sendPatientMessage(text);
|
|
}
|
|
}
|
|
|
|
function sendQuickPrompt(promptText) {
|
|
inputMessage.value = promptText;
|
|
sendPatientMessage(promptText);
|
|
}
|
|
|
|
// Process Incoming Protocol Events
|
|
function handleIncomingWSMessage(msg) {
|
|
switch (msg.type) {
|
|
case 'connection.ack':
|
|
appendSystemMessage(`ACK Received: Connected to session ${msg.session_id}`);
|
|
break;
|
|
|
|
case 'typing.started':
|
|
typingIndicator.classList.add('active');
|
|
break;
|
|
|
|
case 'typing.stopped':
|
|
typingIndicator.classList.remove('active');
|
|
break;
|
|
|
|
case 'assistant.message':
|
|
typingIndicator.classList.remove('active');
|
|
appendAssistantMessage(msg.text);
|
|
break;
|
|
|
|
case 'state.updated':
|
|
if (msg.stage) {
|
|
badgeStage.innerText = msg.stage.toUpperCase();
|
|
}
|
|
if (msg.extracted_state) {
|
|
updateClinicalFacts(msg.extracted_state);
|
|
}
|
|
break;
|
|
|
|
case 'safety.alert':
|
|
triggerEmergencyAlert(msg.message || 'Critical medical emergency risk detected.');
|
|
break;
|
|
|
|
case 'onboarding.completed':
|
|
appendSystemMessage('🎉 Onboarding completed successfully!');
|
|
badgeStage.innerText = 'COMPLETED';
|
|
break;
|
|
|
|
case 'error':
|
|
typingIndicator.classList.remove('active');
|
|
appendSystemMessage(`❌ Error [${msg.code}]: ${msg.message}`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// UI Helpers
|
|
function updateStatus(stateClass, label) {
|
|
statusDot.className = `status-dot ${stateClass}`;
|
|
statusText.innerText = label;
|
|
}
|
|
|
|
function appendPatientMessage(text) {
|
|
const bubble = document.createElement('div');
|
|
bubble.className = 'message-bubble patient';
|
|
bubble.innerHTML = `
|
|
<div class="message-meta">You</div>
|
|
<div>${escapeHtml(text)}</div>
|
|
`;
|
|
chatMessages.appendChild(bubble);
|
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
}
|
|
|
|
function appendAssistantMessage(text) {
|
|
const bubble = document.createElement('div');
|
|
bubble.className = 'message-bubble assistant';
|
|
bubble.innerHTML = `
|
|
<div class="message-meta" style="color: var(--cyan);">AI Healthcare Agent</div>
|
|
<div>${escapeHtml(text)}</div>
|
|
`;
|
|
chatMessages.appendChild(bubble);
|
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
}
|
|
|
|
function appendSystemMessage(text) {
|
|
const bubble = document.createElement('div');
|
|
bubble.className = 'message-bubble system';
|
|
bubble.innerText = text;
|
|
chatMessages.appendChild(bubble);
|
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
}
|
|
|
|
function updateClinicalFacts(state) {
|
|
if (state.primary_symptom) {
|
|
factSymptom.innerText = state.primary_symptom.name || state.primary_symptom;
|
|
if (state.primary_symptom.duration) factDuration.innerText = state.primary_symptom.duration;
|
|
if (state.primary_symptom.severity) factSeverity.innerText = state.primary_symptom.severity;
|
|
}
|
|
|
|
if (state.medications && state.medications.length > 0) {
|
|
factMeds.innerText = state.medications.map(m => typeof m === 'string' ? m : m.name).join(', ');
|
|
}
|
|
|
|
if (state.allergies && state.allergies.length > 0) {
|
|
factAllergies.innerText = state.allergies.map(a => typeof a === 'string' ? a : a.substance).join(', ');
|
|
}
|
|
|
|
if (state.is_emergency || state.is_crisis) {
|
|
factSafety.innerText = 'CRITICAL';
|
|
factSafety.style.color = 'var(--rose)';
|
|
}
|
|
}
|
|
|
|
function triggerEmergencyAlert(msg) {
|
|
emergencyText.innerText = msg;
|
|
emergencyToast.classList.add('active');
|
|
setTimeout(() => {
|
|
emergencyToast.classList.remove('active');
|
|
}, 7000);
|
|
}
|
|
|
|
function logEvent(data) {
|
|
const item = document.createElement('div');
|
|
const eventType = data.type || 'unknown';
|
|
item.className = `event-item ${eventType.replace('.', '\\.')}`;
|
|
|
|
const timeStr = new Date().toLocaleTimeString();
|
|
item.innerHTML = `
|
|
<div class="event-header">
|
|
<span class="event-type">${eventType}</span>
|
|
<span class="event-time">${timeStr}</span>
|
|
</div>
|
|
<div class="event-json">${escapeHtml(JSON.stringify(data, null, 2))}</div>
|
|
`;
|
|
|
|
eventLog.prepend(item);
|
|
}
|
|
|
|
function clearEventLog() {
|
|
eventLog.innerHTML = '';
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|