CodeBlue-Context / frontend /src /BriefPanel.jsx
Hackathon Builder
Fix: Resolved syntax error in BriefPanel.jsx and finalized demo UI polish
e35cb47
Raw History Blame Contribute Delete
15.1 kB
// BriefPanel.jsx — Right column: Emergency Brief + Treatment Checker
import { useState } from 'react';
import { ShieldAlert, FlaskConical, Stethoscope, ListChecks, AlertTriangle, Clock, Send, CheckCircle, XCircle, AlertCircle, Zap, FileText, Activity, Shield, User } from 'lucide-react';
const API_BASE = import.meta.env.VITE_API_BASE || '';
const renderText = (val) => {
if (val === null || val === undefined) return '';
if (typeof val === 'string') return val;
if (typeof val === 'object') return JSON.stringify(val);
return String(val);
};
function Section({ title, icon, children, accent }) {
return (
<div className="fade-up" style={{
marginBottom: 14, borderRadius: 10,
background: 'var(--bg-surface)',
border: `1px solid ${accent ? accent + '30' : 'var(--border-subtle)'}`,
overflow: 'hidden',
}}>
<div style={{
padding: '8px 14px', display: 'flex', alignItems: 'center', gap: 8,
borderBottom: '1px solid var(--border-subtle)',
background: accent ? accent + '08' : 'transparent',
}}>
<span style={{ color: accent || 'var(--text-secondary)' }}>{icon}</span>
<span style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.06em', color: accent || 'var(--text-secondary)' }}>
{title}
</span>
</div>
<div style={{ padding: '10px 14px' }}>{children}</div>
</div>
);
}
function VerdictBadge({ verdict }) {
const cls = `badge verdict-${(verdict || 'not_evaluated').toLowerCase()}`;
const labels = { SAFE: '✓ SAFE', CAUTION: '⚠ CAUTION', CONTRAINDICATED: '✕ CONTRAINDICATED', NOT_EVALUATED: '? NOT EVALUATED' };
return <span className={cls}>{labels[verdict] || verdict}</span>;
}
function TreatmentChecker({ patientId }) {
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const [result, setResult] = useState(null);
const check = async () => {
if (!input.trim() || !patientId) return;
setLoading(true); setResult(null);
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_BASE}/check-treatment/${patientId}`, {
method: 'POST',
headers,
body: JSON.stringify({ treatment: input.trim() }),
});
const data = await res.json();
setResult(data.result);
} catch {
setResult({ treatment_verdict: 'NOT_EVALUATED', verdict_reason: 'Safety service unavailable.' });
} finally {
setLoading(false);
}
};
const accent = result?.treatment_verdict === 'SAFE' ? 'var(--green-safe)'
: result?.treatment_verdict === 'CONTRAINDICATED' ? 'var(--red-critical)'
: result?.treatment_verdict === 'CAUTION' ? 'var(--amber-warning)'
: 'var(--border-default)';
return (
<div style={{
padding: 14, borderRadius: 10, background: 'var(--bg-surface)',
border: '1px solid var(--border-default)',
}}>
<div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--purple-ai)', marginBottom: 10, display: 'flex', alignItems: 'center', gap: 6 }}>
<ShieldAlert size={12} /> Treatment Safety Check
</div>
<div style={{ display: 'flex', gap: 8 }}>
<input
className="input-dark"
placeholder='e.g. "morphine 4mg IV"'
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && check()}
/>
<button className="btn-primary" onClick={check} disabled={loading || !input.trim() || !patientId} style={{ flexShrink: 0, padding: '9px 14px' }}>
{loading ? <div className="spinner"/> : <Send size={13}/>}
</button>
</div>
{result && (
<div className="fade-up" style={{ marginTop: 12, padding: 12, borderRadius: 8, border: `1px solid ${accent}40`, background: accent + '08' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<VerdictBadge verdict={result.treatment_verdict} />
</div>
<p style={{ fontSize: 12, color: 'var(--text-primary)', lineHeight: 1.6, marginBottom: 8 }}>{result.verdict_reason}</p>
{(result.allergy_risks || []).map((r, i) => (
<p key={i} style={{ fontSize: 11, color: 'var(--red-critical)', marginBottom: 3 }}>⚠ Allergy: {r.substance} — {r.risk}</p>
))}
{(result.drug_interactions || []).map((d, i) => (
<p key={i} style={{ fontSize: 11, color: 'var(--amber-warning)', marginBottom: 3 }}>💊 Interaction: {d.drug_a} + {d.drug_b} → {d.interaction}</p>
))}
</div>
)}
</div>
);
}
export function BriefPanel({ data, patient }) {
if (!data) {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ padding: '14px 16px', borderBottom: '1px solid var(--border-subtle)' }}>
<span style={{ fontWeight: 700, fontSize: 13, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
Emergency Brief
</span>
</div>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 30, gap: 12, color: 'var(--text-tertiary)', textAlign: 'center' }}>
<AlertCircle size={36} style={{ opacity: 0.4 }} />
<p style={{ fontSize: 13 }}>Run emergency snapshot to generate brief</p>
{patient && <TreatmentChecker patientId={patient.id} />}
</div>
</div>
);
}
const { agents, meta } = data;
const triage = agents?.triage || {};
const action = agents?.action_protocol || {};
// Build collective reasoning log
const collectiveReasoning = [
...(triage.agent_reasoning || []).map(r => ({ agent: 'Triage', text: r })),
...(agents?.contraindication?.agent_reasoning || []).map(r => ({ agent: 'Guardian', text: r })),
...(agents?.labs?.agent_reasoning || []).map(r => ({ agent: 'Pathologist', text: r })),
...(agents?.differentials?.agent_reasoning || []).map(r => ({ agent: 'Resident', text: r })),
...(action.agent_reasoning || []).map(r => ({ agent: 'Attending', text: r })),
];
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: 'var(--bg-base)' }}>
<div style={{ padding: '14px 16px', borderBottom: '1px solid var(--border-subtle)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontWeight: 800, fontSize: 13, letterSpacing: '0.05em', color: 'var(--text-primary)', textTransform: 'uppercase' }}>
Emergency Brief
</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 10, color: 'var(--text-tertiary)' }}>
<Clock size={10} /> {meta?.processing_ms}ms
{meta?.gemini_enabled && <span style={{ color: 'var(--purple-ai)', fontWeight: 700 }}>· AI ACTIVE</span>}
</div>
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '20px 24px' }}>
{/* Patient Hero Header */}
<div style={{
marginBottom: 20, paddingBottom: 16, borderBottom: '1px solid var(--border-subtle)',
display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start'
}}>
<div>
<div style={{ fontSize: 28, fontWeight: 900, color: 'var(--text-primary)', letterSpacing: '-0.03em', marginBottom: 4 }}>
{patient?.display_name || 'Anonymous Patient'}
</div>
<div style={{ fontSize: 13, color: 'var(--text-secondary)', fontWeight: 600, display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, background: 'var(--border-subtle)', padding: '2px 8px', borderRadius: 4 }}>
<User size={12} />
<span>{patient?.age || '??'}y {patient?.gender || 'Unknown'}</span>
</div>
<span style={{ fontSize: 11, color: 'var(--text-tertiary)', fontFamily: 'var(--font-mono)' }}>{patient?.id}</span>
{(!patient?.age || patient?.age === '??') && (
<span style={{ fontSize: 10, color: 'var(--amber-warning)', background: 'var(--amber-bg)', padding: '2px 8px', borderRadius: 4 }}>
Demographics Unverified
</span>
)}
</div>
</div>
<div className={`badge badge-${patient?.risk_level || 'stable'}`} style={{ padding: '6px 12px' }}>
{patient?.risk_level?.toUpperCase()}
</div>
</div>
{/* Primary Clinical Directive */}
{action.primary_directive && (
<div className="fade-up directive-banner" style={{
padding: '20px', borderRadius: 16, marginBottom: 24,
boxShadow: 'var(--shadow-lg)',
position: 'relative', overflow: 'hidden'
}}>
<div style={{ fontSize: 10, fontWeight: 900, textTransform: 'uppercase', opacity: 0.9, letterSpacing: '0.15em', marginBottom: 10, display: 'flex', alignItems: 'center', gap: 6 }}>
<Zap size={12} fill="white" /> IMMEDIATE CLINICAL DIRECTIVE
</div>
<div style={{ fontSize: 20, fontWeight: 900, lineHeight: 1.25, letterSpacing: '-0.01em' }}>
{renderText(action.primary_directive)}
</div>
</div>
)}
{/* Immediate Critical Risks (Condition-First) */}
{triage.critical_risks?.length > 0 && (
<SectionBlock title="Physiological Threats" icon={<ShieldAlert size={14}/>} accent="var(--red-critical)">
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{triage.critical_risks.map((r, i) => (
<div key={i} className="pulse-critical" style={{
background: 'var(--red-bg)', border: '1px solid var(--red-glow)',
borderRadius: 12, padding: '12px 16px'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
<AlertTriangle size={14} color="var(--red-critical)" />
<span style={{ fontWeight: 800, fontSize: 13, color: 'var(--red-critical)' }}>
{renderText(r.finding || r.condition)}
</span>
</div>
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginLeft: 22, lineHeight: 1.4 }}>
<span style={{ color: 'var(--text-tertiary)' }}>Risk:</span> <span style={{ fontWeight: 700, color: 'var(--text-primary)' }}>{renderText(r.risk)}</span>
<br/>
<span style={{ fontSize: 11, opacity: 0.7 }}>Mechanism: {renderText(r.danger)}</span>
</div>
</div>
))}
</div>
</SectionBlock>
)}
{/* Action Mapping */}
{action.action_mapping?.length > 0 && (
<SectionBlock title="Action Mapping" icon={<Zap size={14}/>} accent="var(--blue-accent)">
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{action.action_mapping.map((m, i) => (
<div key={i} style={{ display: 'flex', gap: 14, alignItems: 'flex-start' }}>
<div style={{
width: 22, height: 22, borderRadius: '6px', background: 'var(--blue-accent)',
display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff',
fontSize: 11, fontWeight: 900, flexShrink: 0, marginTop: 1
}}>
{i + 1}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text-primary)' }}>{renderText(m.action)}</div>
<div style={{ fontSize: 11, color: 'var(--text-tertiary)', fontWeight: 500 }}>Objective: {renderText(m.condition)}</div>
</div>
</div>
))}
</div>
</SectionBlock>
)}
{/* Clinical Synthesis */}
{action.clinical_summary && (
<SectionBlock title="Clinical Synthesis" icon={<FileText size={14}/>} accent="var(--purple-ai)">
<div style={{
fontSize: 12.5, lineHeight: 1.6, color: 'var(--text-secondary)',
background: 'var(--purple-bg)', padding: '14px 18px', borderRadius: 12, border: '1px solid var(--border-subtle)',
boxShadow: 'var(--shadow-sm)'
}}>
"{renderText(action.clinical_summary)}"
</div>
</SectionBlock>
)}
{/* Safety Checker */}
<div style={{ marginTop: 24 }}>
<TreatmentChecker patientId={patient?.id} />
</div>
{/* Reasoning Pipeline */}
{collectiveReasoning.length > 0 && (
<div style={{ marginTop: 32, paddingTop: 20, borderTop: '1px solid var(--border-subtle)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<div style={{ fontSize: 10, fontWeight: 800, color: 'var(--text-tertiary)', textTransform: 'uppercase', display: 'flex', alignItems: 'center', gap: 6 }}>
<Activity size={10} /> Agent Reasoning Pipeline
</div>
<div style={{ fontSize: 9, fontWeight: 700, color: 'var(--green-safe)', background: 'var(--green-bg)', padding: '2px 6px', borderRadius: 4 }}>
CERTIFIED SYSTEM LOGIC
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{collectiveReasoning.map((r, i) => (
<div key={i} style={{ fontSize: 10.5, color: 'var(--text-secondary)', display: 'flex', gap: 12 }}>
<span style={{ fontWeight: 800, color: 'var(--purple-ai)', minWidth: 65, textTransform: 'uppercase', fontSize: 9, letterSpacing: '0.02em' }}>{r.agent}</span>
<span style={{ lineHeight: 1.5, opacity: 0.8 }}>{renderText(r.text)}</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}
function SectionBlock({ title, icon, children, accent = 'var(--blue-accent)' }) {
return (
<div style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12, borderBottom: `1px solid ${accent}20`, paddingBottom: 6 }}>
<div style={{ color: accent }}>{icon}</div>
<span style={{ fontSize: 11, fontWeight: 900, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--text-primary)' }}>{title}</span>
</div>
{children}
</div>
);
}