Files
cureselect-voice-onboarding…/src/App.jsx
T

259 lines
9.2 KiB
React

import React, { useState, useEffect } from 'react';
import Header from './components/Header';
import PatientSetupForm from './components/PatientSetupForm';
import VoiceChat from './components/VoiceChat';
import DocumentUploader from './components/DocumentUploader';
import SummaryModal from './components/SummaryModal';
import {
checkHealth,
startVoiceSession,
startTextSession,
sendVoiceTurn,
sendTextMessage,
} from './services/api';
export default function App() {
const [healthStatus, setHealthStatus] = useState(null);
const [activeSession, setActiveSession] = useState(null);
const [isVoiceMode, setIsVoiceMode] = useState(true);
const [isLoadingSession, setIsLoadingSession] = useState(false);
const [isProcessingTurn, setIsProcessingTurn] = useState(false);
// Modals visibility
const [showDocumentUploader, setShowDocumentUploader] = useState(false);
const [showSummaryModal, setShowSummaryModal] = useState(false);
const [uploadedDocuments, setUploadedDocuments] = useState([]);
// Check Backend Health Status on Mount and periodically
useEffect(() => {
async function pingHealth() {
const health = await checkHealth();
setHealthStatus(health);
}
pingHealth();
const interval = setInterval(pingHealth, 15000);
return () => clearInterval(interval);
}, []);
// Ingest Patient Data from URL Search Params or Parent postMessage (WebView Bridge)
useEffect(() => {
if (typeof window === 'undefined') return;
// Method 1: Check URL Query Parameters (e.g. ?first_name=Ramesh&phone=9876543210&autostart=true)
const urlParams = new URLSearchParams(window.location.search);
const autostart = urlParams.get('autostart') === 'true' || urlParams.get('autoStart') === 'true';
const firstName = urlParams.get('first_name') || urlParams.get('firstName') || urlParams.get('name');
if (autostart && firstName) {
const patient = {
first_name: firstName,
last_name: urlParams.get('last_name') || urlParams.get('lastName') || '',
age: urlParams.get('age') || '30',
gender: urlParams.get('gender') || 'Male',
phone: urlParams.get('phone') || '',
email: urlParams.get('email') || '',
};
const category = {
type: 'general',
code: urlParams.get('category') || 'general_medicine',
name: urlParams.get('category_name') || 'General Medicine / Primary Care',
};
const language = urlParams.get('language') || urlParams.get('lang') || 'ta-IN';
const mode = urlParams.get('mode') || 'voice';
handleStartSession({ patient, category, language, mode });
}
// Method 2: Listen for postMessage from Parent App (Web iFrame / WebView)
const handleParentBridgeMessage = (event) => {
if (!event.data) return;
let payload = event.data;
if (typeof payload === 'string') {
try { payload = JSON.parse(payload); } catch (_) { return; }
}
if (payload.type === 'INIT_PATIENT_ONBOARDING' && payload.data) {
const { patient, category, language, mode } = payload.data;
if (patient) {
handleStartSession({
patient,
category: category || { type: 'general', code: 'general_medicine', name: 'General Medicine / Primary Care' },
language: language || 'ta-IN',
mode: mode || 'voice',
});
}
}
};
window.addEventListener('message', handleParentBridgeMessage);
return () => window.removeEventListener('message', handleParentBridgeMessage);
}, []);
// Handle Session Initialization (Start Voice / Text Session)
const handleStartSession = async ({ patient, category, language, mode }) => {
// Prime audio context synchronously inside user click gesture event
if (typeof window !== 'undefined') {
try {
const silentAudio = new Audio("data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=");
silentAudio.play().then(() => silentAudio.pause()).catch(() => {});
} catch (_) {}
}
setIsLoadingSession(true);
const useVoice = mode ? mode === 'voice' : true;
setIsVoiceMode(useVoice);
try {
let sessionRes;
if (useVoice) {
sessionRes = await startVoiceSession({ patient, category, language });
} else {
sessionRes = await startTextSession({ patient, category, language });
}
setActiveSession({
...sessionRes,
patient,
category: sessionRes?.category || category,
language: sessionRes?.language || language,
greeting_text: sessionRes?.greeting_text || `Hello ${patient.first_name || patient.name || 'Patient'}, welcome to CureSelect Healthcare. I am your AI clinical assistant. What symptoms or medical concerns bring you in today?`,
});
} catch (err) {
console.error('Failed to start session:', err);
// Fallback mock session if backend server is unreachable
setActiveSession({
success: true,
session_id: `VOICE-${Math.random().toString(36).substring(2, 9).toUpperCase()}`,
patient_id: patient.id,
patient,
category,
language,
greeting_text: `Hello ${patient.first_name || patient.name || 'Patient'}, welcome to CureSelect Healthcare. I am your AI clinical assistant. What symptoms or medical concerns bring you in today?`,
status: 'in_progress',
stage: 'greeting',
completed: false,
});
} finally {
setIsLoadingSession(false);
}
};
// Handle Turn Submission (Voice Blob or Text)
const handleSendTurn = async ({ audioBlob, textFallback }) => {
if (!activeSession) return null;
setIsProcessingTurn(true);
try {
let turnRes;
if (isVoiceMode) {
turnRes = await sendVoiceTurn(activeSession.session_id, audioBlob, textFallback);
} else {
turnRes = await sendTextMessage(activeSession.session_id, textFallback);
}
if (turnRes) {
setActiveSession((prev) => ({
...prev,
stage: turnRes.stage || prev.stage,
completed: Boolean(turnRes.completed || turnRes.stage === 'completed' || turnRes.stage === 'finished'),
}));
}
return turnRes;
} catch (err) {
console.error('Error submitting turn:', err);
// Fallback local mock response for seamless UI demo if network error
return {
success: true,
session_id: activeSession.session_id,
patient_transcript: textFallback || 'Audio turn recorded',
ai_text: 'Thank you for providing that detail. Do you currently experience any shortness of breath or radiating pain?',
stage: 'symptoms_inquiry',
completed: false,
audio_url: null,
};
} finally {
setIsProcessingTurn(false);
}
};
// Document Upload Callback
const handleDocumentUploaded = (docResult) => {
setUploadedDocuments((prev) => [...prev, docResult]);
};
// Reset Session
const handleResetSession = () => {
if (window.confirm('Are you sure you want to end this session and start a new patient onboarding?')) {
setActiveSession(null);
setUploadedDocuments([]);
setShowDocumentUploader(false);
setShowSummaryModal(false);
}
};
return (
<div className="min-h-screen flex flex-col justify-between bg-gradient-to-br from-purple-50/50 via-slate-50 to-indigo-50/50 text-slate-900 selection:bg-purple-500 selection:text-white overflow-x-hidden">
{/* Header Bar */}
<Header
healthStatus={healthStatus}
activeSession={activeSession}
onResetSession={handleResetSession}
/>
{/* Main Content Area */}
<main className="flex-1 flex flex-col justify-center py-2 sm:py-4">
{!activeSession ? (
<PatientSetupForm
onStartSession={handleStartSession}
isLoading={isLoadingSession}
/>
) : (
<VoiceChat
session={activeSession}
isVoiceMode={isVoiceMode}
onSendTurn={handleSendTurn}
onOpenDocumentUploader={() => setShowDocumentUploader(true)}
onCompleteSession={() => setShowSummaryModal(true)}
isProcessingTurn={isProcessingTurn}
documentsCount={uploadedDocuments.length}
/>
)}
</main>
{/* Prescription & Medical Document Uploader Modal */}
{showDocumentUploader && activeSession && (
<DocumentUploader
sessionId={activeSession.session_id}
onClose={() => setShowDocumentUploader(false)}
onDocumentUploaded={handleDocumentUploaded}
/>
)}
{/* Clinical Summary & EHR 2-Step Confirmation Modal */}
{showSummaryModal && activeSession && (
<SummaryModal
sessionId={activeSession.session_id}
isVoiceMode={isVoiceMode}
onClose={() => setShowSummaryModal(false)}
onSubmittedSuccessfully={() => {
// Keep modal open on success view
}}
/>
)}
{/* Footer */}
<footer className="py-2.5 px-4 border-t border-slate-200/80 text-center text-[11px] sm:text-xs text-slate-500 font-medium bg-white/70 backdrop-blur-md shrink-0">
CureSelect Healthcare AI Engine
</footer>
</div>
);
}