/** * SimulationInterface Page * Main interface for AI Patient Simulation */ import React, { useState, useEffect, useRef } from 'react'; import { useLocation } from 'react-router-dom'; import { simulationApi } from '../services/simulationApi'; import { profileApi } from '../services/profileApi'; import { biasApi } from '../services/biasApi'; import type { StudentProfile } from '../components/ProfileModal'; import ChatMessage from '../components/ChatMessage'; import PatientCard from '../components/PatientCard'; import AITutorPanel from '../components/AITutorPanel'; import { BiasInterruptModal } from '../components/BiasInterruptModal'; import type { SimulationMessage, TutorFeedback, EmotionalState, RapportLevel, PatientInfo, } from '../types/simulation'; const SimulationInterface: React.FC = () => { const location = useLocation(); // Simulation state const [caseId, setCaseId] = useState(null); const [patientInfo, setPatientInfo] = useState(null); const [settingContext, setSettingContext] = useState(''); const [avatarPath, setAvatarPath] = useState(''); const [emotionalState, setEmotionalState] = useState('concerned'); const [rapportLevel, setRapportLevel] = useState(3); // Case selection from profile const [selectedSpecialty, setSelectedSpecialty] = useState('general_medicine'); const [selectedDifficulty, setSelectedDifficulty] = useState('intermediate'); const [caseSelectionMessage, setCaseSelectionMessage] = useState(''); // Messages & feedback const [messages, setMessages] = useState([]); const [tutorFeedback, setTutorFeedback] = useState([]); // UI state const [loading, setLoading] = useState(false); const [inputMessage, setInputMessage] = useState(''); const [isSimulationStarted, setIsSimulationStarted] = useState(false); const [error, setError] = useState(null); // Bias detection state const [biasModalOpen, setBiasModalOpen] = useState(false); const [detectedBiasType, setDetectedBiasType] = useState(''); const [biasInterventionMessage, setBiasInterventionMessage] = useState(''); const [biasReflectionQuestions, setBiasReflectionQuestions] = useState([]); const [messageCountSinceLastCheck, setMessageCountSinceLastCheck] = useState(0); // Refs const messagesEndRef = useRef(null); const inputRef = useRef(null); // Auto-scroll to bottom when new messages arrive useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); // Check for profile-based case selection on mount useEffect(() => { const searchParams = new URLSearchParams(location.search); const isNew = searchParams.get('new') === 'true'; if (isNew) { // Read profile from localStorage const profileStr = localStorage.getItem('studentProfile'); if (profileStr) { try { const profile: StudentProfile = JSON.parse(profileStr); // Call profile API to select case profileApi .selectCase({ profile: { yearLevel: profile.yearLevel, comfortableSpecialties: profile.comfortableSpecialties, setting: profile.setting, }, feature: 'simulation', }) .then((result) => { setSelectedSpecialty(result.specialty); setSelectedDifficulty(result.difficulty); setCaseSelectionMessage(result.why_selected); }) .catch((err) => { console.error('Failed to select case from profile:', err); // Fall back to defaults if API fails }); } catch (err) { console.error('Failed to parse profile:', err); } } } }, [location]); /** * Start a new simulation */ const handleStartSimulation = async () => { setLoading(true); setError(null); try { const response = await simulationApi.startSimulation({ specialty: selectedSpecialty, difficulty: selectedDifficulty, year_level: 'final_year', // This can be derived from profile if needed }); setCaseId(response.case_id); setPatientInfo(response.patient_info); setSettingContext(response.setting_context); setAvatarPath(response.avatar_path); // Add initial patient message setMessages([ { role: 'patient', content: response.initial_message, timestamp: new Date().toISOString(), emotional_state: emotionalState, }, ]); setIsSimulationStarted(true); // Focus input setTimeout(() => inputRef.current?.focus(), 100); } catch (err: any) { setError(err.message || 'Failed to start simulation'); } finally { setLoading(false); } }; /** * Send student message */ const handleSendMessage = async (e: React.FormEvent) => { e.preventDefault(); if (!inputMessage.trim() || !caseId) return; const studentMessage = inputMessage.trim(); setInputMessage(''); setLoading(true); setError(null); // Add student message immediately (optimistic update) const newStudentMessage: SimulationMessage = { role: 'student', content: studentMessage, timestamp: new Date().toISOString(), }; setMessages((prev) => [...prev, newStudentMessage]); try { const response = await simulationApi.sendMessage({ case_id: caseId, student_message: studentMessage, }); // Update state setEmotionalState(response.emotional_state); setRapportLevel(response.rapport_level); setAvatarPath(response.avatar_path); // Add patient response const patientMessage: SimulationMessage = { role: 'patient', content: response.patient_response, timestamp: new Date().toISOString(), emotional_state: response.emotional_state, }; setMessages((prev) => { const updatedMessages = [...prev, patientMessage]; // Check for bias every 3 messages setMessageCountSinceLastCheck((count) => { const newCount = count + 1; if (newCount >= 3) { // Run bias check asynchronously checkForBias(updatedMessages); return 0; } return newCount; }); return updatedMessages; }); // Add tutor feedback setTutorFeedback((prev) => [...prev, ...response.tutor_feedback]); } catch (err: any) { setError(err.message || 'Failed to send message'); } finally { setLoading(false); } }; /** * Complete simulation (for later implementation) */ const handleCompleteSimulation = () => { // TODO: Navigate to diagnosis page console.log('Complete simulation'); }; /** * Check for cognitive bias in student's conversation */ const checkForBias = async (conversationHistory: SimulationMessage[]) => { if (!caseId || !patientInfo) return; try { // Convert messages to API format const conversationMessages = conversationHistory.map((msg) => ({ role: msg.role, content: msg.content, timestamp: msg.timestamp, })); const response = await biasApi.detectBias({ case_id: caseId, conversation_history: conversationMessages, patient_profile: { name: patientInfo.name, chief_complaint: patientInfo.chief_complaint, actual_diagnosis: 'Withheld', // Not revealed during simulation }, }); if (response.bias_detected && response.intervention_message && response.reflection_questions) { setDetectedBiasType(response.bias_type || 'cognitive_bias'); setBiasInterventionMessage(response.intervention_message); setBiasReflectionQuestions(response.reflection_questions); setBiasModalOpen(true); } } catch (err) { console.error('Bias detection failed:', err); // Don't block simulation on bias detection errors } }; /** * Handle student's reflection on detected bias */ const handleBiasReflection = (reflections: string[]) => { console.log('Student reflections:', reflections); // TODO: Log reflections to backend for analytics setBiasModalOpen(false); setMessageCountSinceLastCheck(0); }; // Render loading state if (!isSimulationStarted) { return (

AI Patient Simulation

Practice clinical communication with realistic AI patients

{caseSelectionMessage && (

Case Selected for You:

{caseSelectionMessage}

)} {error && (
{error}
)}
); } // Render simulation interface return (
{/* Header */}

AI Patient Simulation

{patientInfo && (

Case: {patientInfo.chief_complaint}

)}
{/* Main Layout */}
{/* Left: Patient Card (1/3) */}
{patientInfo && (
)}
{/* Middle: Chat (1/3) */}
{/* Messages */}
{messages.map((msg, index) => ( ))}
{/* Input */}
setInputMessage(e.target.value)} placeholder="Type your message to the patient..." disabled={loading} className="flex-1 px-4 py-3 bg-warm-gray-50 border border-warm-gray-200 rounded-xl text-text-primary placeholder-text-tertiary focus:outline-none focus:ring-2 focus:ring-forest-green focus:border-transparent transition-all duration-300" />
{/* Right: AI Tutor (1/3) */}
{/* Error Toast */} {error && (
{error}
)} {/* Bias Interrupt Modal */}
); }; export default SimulationInterface;