Hackathon Builder
Emergency Intelligence Upgrade: Multi-file synthesis, demographic preservation, and senior physician-grade reasoning
3024450
Raw History Blame Contribute Delete
10.8 kB
import { useState, useEffect, useCallback } from 'react';
import { Activity } from 'lucide-react';
import Welcome from './Welcome';
import Tutorial from './Tutorial';
import { PatientPanel } from './PatientPanel';
import { AgentPanel } from './AgentPanel';
import { BriefPanel } from './BriefPanel';
import { ApiKeyModal } from './ApiKeyModal';
import ConsultPanel from './ConsultPanel';
const API = import.meta.env.VITE_API_BASE || '';
const INITIAL_STATUSES = {
triage: 'idle', contraindication: 'idle',
labs: 'idle', differentials: 'idle', action_protocol: 'idle',
};
// Staggered agent activation for visual drama
const AGENT_ORDER = ['triage', 'contraindication', 'labs', 'differentials', 'action_protocol'];
export default function App() {
const [showWelcome, setShowWelcome] = useState(true);
const [showTutorial, setShowTutorial] = useState(false);
const [showApiModal, setShowApiModal] = useState(false);
const [patients, setPatients] = useState([]);
const [patientsLoading, setPatientsLoading] = useState(false);
const [selected, setSelected] = useState(null);
const [running, setRunning] = useState(false);
const [agentStatuses, setAgentStatuses] = useState(INITIAL_STATUSES);
const [agentResults, setAgentResults] = useState({});
const [briefData, setBriefData] = useState(null);
const [activeTab, setActiveTab] = useState('orchestrate');
const [focus, setFocus] = useState('General');
// Load patients
const fetchPatients = useCallback(async () => {
setPatientsLoading(true);
try {
const res = await fetch(`${API}/patients`);
const data = await res.json();
setPatients(data.patients || []);
} catch {
setPatients([]);
} finally {
setPatientsLoading(false);
}
}, []);
useEffect(() => { fetchPatients(); }, [fetchPatients]);
const handleSelectPatient = (patient) => {
setSelected(patient);
setBriefData(null);
setAgentStatuses(INITIAL_STATUSES);
setAgentResults({});
setActiveTab('orchestrate');
};
// Upload patient JSON/PDF (Multi-file support)
const handleUpload = async (e) => {
const fileList = Array.from(e.target.files);
if (fileList.length === 0) return;
const form = new FormData();
fileList.forEach(f => form.append('files', f)); // 'files' is plural now
try {
const geminiKey = localStorage.getItem('gemini_key');
const headers = geminiKey ? { 'x-gemini-key': geminiKey } : {};
const res = await fetch(`${API}/patients/upload`, { method: 'POST', body: form, headers });
if (!res.ok) {
const err = await res.json();
alert(`Upload failed: ${err.detail || 'Internal Server Error'}`);
} else {
fetchPatients();
}
} catch (err) {
alert(`Network error during upload: ${err.message}`);
}
e.target.value = '';
};
// Run 5-agent orchestration with staggered UI animation
const runOrchestration = async (targetPatient = null) => {
// If targetPatient is an event object from a click handler, ignore it
const isEvent = targetPatient && targetPatient.nativeEvent;
const p = (targetPatient && !isEvent) ? targetPatient : selected;
if (!p || running) return;
setRunning(true);
setBriefData(null);
setAgentResults({});
// Light up agents one by one with a delay for UX drama
const activateAgents = async () => {
for (let i = 0; i < AGENT_ORDER.length; i++) {
const key = AGENT_ORDER[i];
setAgentStatuses(prev => ({ ...prev, [key]: 'working' }));
await new Promise(r => setTimeout(r, 180));
}
};
activateAgents(); // fire and don't await — let it animate while API call runs
try {
const geminiKey = localStorage.getItem('gemini_key');
const headers = { 'Content-Type': 'application/json' };
if (geminiKey) headers['x-gemini-key'] = geminiKey;
const res = await fetch(`${API}/orchestrate/${p.id}`, {
method: 'POST',
headers
});
if (!res.ok) throw new Error('API error');
const data = await res.json();
// Mark all complete with slight stagger
for (let i = 0; i < AGENT_ORDER.length; i++) {
const key = AGENT_ORDER[i];
await new Promise(r => setTimeout(r, 120));
setAgentStatuses(prev => ({ ...prev, [key]: 'complete' }));
setAgentResults(prev => ({ ...prev, [key]: data.agents?.[key] }));
}
setBriefData(data);
// Update the patient risk in sidebar from triage
if (data.agents?.triage?.severity) {
const sev = data.agents.triage.severity;
const risk = sev <= 2 ? 'critical' : sev === 3 ? 'moderate' : 'stable';
setSelected(prev => prev ? { ...prev, risk_level: risk } : prev);
}
} catch {
AGENT_ORDER.forEach(key => {
setAgentStatuses(prev => ({ ...prev, [key]: 'error' }));
});
} finally {
setRunning(false);
}
};
if (showWelcome) return <Welcome onStart={() => { setShowWelcome(false); setShowTutorial(true); }} />;
if (showTutorial) return <Tutorial onComplete={() => setShowTutorial(false)} />;
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' }}>
{/* Header */}
<header style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '0 20px', height: 48, flexShrink: 0,
background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-subtle)',
zIndex: 10,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ animation: 'heartbeat 1.4s infinite', color: 'var(--red-critical)' }}>
<Activity size={20} />
</div>
<span style={{ fontWeight: 800, fontSize: 15, letterSpacing: '-0.02em' }}>CodeBlue Context</span>
<span style={{
fontSize: 10, fontWeight: 600, padding: '2px 8px', borderRadius: 99,
background: 'rgba(255,45,85,0.15)', color: 'var(--red-critical)',
border: '1px solid rgba(255,45,85,0.3)', letterSpacing: '0.05em',
}}>
EMERGENCY INTELLIGENCE
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 16, fontSize: 11, color: 'var(--text-tertiary)' }}>
<button
onClick={() => setShowApiModal(true)}
className="btn-ghost"
style={{ color: 'var(--purple-ai)', borderColor: 'rgba(191,90,242,0.3)', padding: '6px 12px' }}
>
🔑 Provide API Key
</button>
<button
className="btn-demo"
onClick={() => {
const demoPatient = patients.find(p => p.id === 'CB-001') || patients[0];
if (demoPatient) {
handleSelectPatient(demoPatient);
setTimeout(() => runOrchestration(demoPatient), 50); // slight delay to let UI select it
}
}}
style={{
padding: '6px 14px', borderRadius: 6, fontWeight: 700, cursor: 'pointer',
background: 'var(--blue-accent)', color: '#fff', border: 'none',
transition: 'all 0.2s',
display: 'flex', alignItems: 'center', gap: '6px'
}}
onMouseOver={(e) => e.currentTarget.style.filter = 'brightness(1.1)'}
onMouseOut={(e) => e.currentTarget.style.filter = 'brightness(1)'}
>
<Activity size={14} /> Run Live Demo
</button>
<span>
<span style={{ color: 'var(--green-safe)', fontWeight: 700 }}>●</span> System Online
</span>
<span>{patients.length} patients</span>
</div>
</header>
{/* 3-column layout */}
<div style={{ flex: 1, display: 'grid', gridTemplateColumns: '240px 420px 1fr', overflow: 'hidden' }}>
{/* Left: Patient List */}
<div style={{ borderRight: '1px solid var(--border-subtle)', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<PatientPanel
patients={patients}
selected={selected}
onSelect={handleSelectPatient}
onUpload={handleUpload}
loading={patientsLoading}
/>
</div>
{/* Center: Tabs + Content */}
<div style={{ borderRight: '1px solid var(--border-subtle)', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', background: 'var(--bg-base)', padding: '4px 4px 0', borderBottom: '1px solid var(--border-subtle)' }}>
<button
onClick={() => setActiveTab('orchestrate')}
style={{
flex: 1, padding: '10px', fontSize: 11, fontWeight: 700, cursor: 'pointer', border: 'none',
background: activeTab === 'orchestrate' ? 'var(--bg-surface)' : 'transparent',
color: activeTab === 'orchestrate' ? 'var(--blue-accent)' : 'var(--text-secondary)',
borderRadius: '8px 8px 0 0', borderBottom: activeTab === 'orchestrate' ? '2px solid var(--blue-accent)' : 'none'
}}
>
ORCHESTRATION
</button>
<button
onClick={() => setActiveTab('consult')}
style={{
flex: 1, padding: '10px', fontSize: 11, fontWeight: 700, cursor: 'pointer', border: 'none',
background: activeTab === 'consult' ? 'var(--bg-surface)' : 'transparent',
color: activeTab === 'consult' ? 'var(--purple-ai)' : 'var(--text-secondary)',
borderRadius: '8px 8px 0 0', borderBottom: activeTab === 'consult' ? '2px solid var(--purple-ai)' : 'none'
}}
>
ADVANCED CONSULT
</button>
</div>
<div style={{ flex: 1, overflow: 'hidden' }}>
{activeTab === 'orchestrate' ? (
<AgentPanel
patient={selected}
agentStatuses={agentStatuses}
agentResults={agentResults}
onRun={runOrchestration}
running={running}
focus={focus}
setFocus={setFocus}
/>
) : (
<ConsultPanel
patientId={selected?.id}
previousResults={agentResults}
/>
)}
</div>
</div>
{/* Right: Emergency Brief */}
<div style={{ overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<BriefPanel data={briefData} patient={selected} />
</div>
</div>
{showApiModal && <ApiKeyModal onClose={() => setShowApiModal(false)} />}
</div>
);
}