CodeBlue-Context / frontend /src /AgentPanel.jsx
Hackathon Builder
Fix AgentPanel props for clinical focus
6edc70c
Raw History Blame Contribute Delete
13.5 kB
// AgentPanel.jsx β€” Center orchestration column
import { useState } from 'react';
import { ShieldAlert, FlaskConical, Stethoscope, ListChecks, Zap, CheckCircle, Loader, Circle, ChevronDown, ChevronUp } from 'lucide-react';
const AGENTS = [
{ key: 'triage', label: 'Triage Agent', icon: <Zap size={14}/>, desc: 'Severity classification & chief complaint' },
{ key: 'contraindication', label: 'Contraindication Guardian', icon: <ShieldAlert size={14}/>, desc: 'Drug interactions & allergy risks' },
{ key: 'labs', label: 'Lab Interpreter', icon: <FlaskConical size={14}/>, desc: 'Critical lab findings & patterns' },
{ key: 'differentials', label: 'Differential Diagnosis', icon: <Stethoscope size={14}/>, desc: 'Top 3 differentials from evidence' },
{ key: 'action_protocol', label: 'Action Protocol', icon: <ListChecks size={14}/>, desc: 'Immediate ordered steps & disposition' },
];
const THINKING_MESSAGES = {
triage: "Analyzing vitals and identifying life-threats...",
contraindication: "Scanning history for high-risk medication overlaps...",
labs: "Interpreting metabolic panels and blood gases...",
differentials: "Cross-referencing symptoms with clinical evidence...",
action_protocol: "Synthesizing final emergency stabilization steps...",
};
function StatusIcon({ status }) {
if (status === 'working') return <div className="spinner" />;
if (status === 'complete') return <CheckCircle size={15} color="var(--green-safe)" />;
if (status === 'error') return <Circle size={15} color="var(--red-critical)" />;
return <Circle size={15} color="var(--text-tertiary)" />;
}
function AgentRow({ agent, status, result, expanded, onToggle }) {
const cls = status === 'idle' ? 'agent-idle'
: status === 'working' ? 'agent-working'
: status === 'complete' ? 'agent-complete'
: 'agent-error';
const hasResult = result && status === 'complete';
return (
<div className={`card ${cls}`} style={{ marginBottom: 8, transition: 'all 0.3s' }}>
<div
onClick={hasResult ? onToggle : undefined}
style={{
display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',
cursor: hasResult ? 'pointer' : 'default',
}}
>
<div style={{
width: 28, height: 28, borderRadius: 7,
background: 'rgba(255,255,255,0.04)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: status === 'complete' ? 'var(--green-safe)' : status === 'working' ? 'var(--blue-accent)' : 'var(--text-tertiary)',
}}>
{agent.icon}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: status === 'idle' ? 'var(--text-secondary)' : 'var(--text-primary)' }}>
{agent.label}
</div>
<div style={{ fontSize: 10, color: status === 'working' ? 'var(--blue-accent)' : 'var(--text-tertiary)', fontWeight: status === 'working' ? 600 : 400 }}>
{status === 'working' ? THINKING_MESSAGES[agent.key] : agent.desc}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<StatusIcon status={status} />
{hasResult && (expanded ? <ChevronUp size={12} color="var(--text-tertiary)"/> : <ChevronDown size={12} color="var(--text-tertiary)"/>)}
</div>
</div>
{hasResult && expanded && (
<div style={{ padding: '0 14px 12px', borderTop: '1px solid var(--border-subtle)', paddingTop: 10 }} className="fade-in">
<AgentResultPreview agentKey={agent.key} result={result} />
</div>
)}
</div>
);
}
function AgentResultPreview({ agentKey, result }) {
const style = { fontSize: 11, color: 'var(--text-secondary)', lineHeight: 1.6 };
const tag = (text, color) => (
<span style={{ background: color + '20', color, border: `1px solid ${color}40`, borderRadius: 4, padding: '1px 6px', fontSize: 10, fontWeight: 700, marginRight: 4 }}>
{text}
</span>
);
if (agentKey === 'triage') return (
<div style={style}>
{tag(`ESI ${result.severity}: ${result.severity_label}`,
result.severity <= 2 ? 'var(--red-critical)' : result.severity === 3 ? 'var(--amber-warning)' : 'var(--green-safe)')}
<p style={{ marginTop: 6 }}>{result.chief_complaint}</p>
{(result.time_critical_flags || []).map((f, i) => <p key={i}>⚑ {f}</p>)}
{result.agent_reasoning && (
<div style={{ marginTop: 8, padding: 8, background: 'var(--bg-base)', borderRadius: 6, borderLeft: '3px solid var(--blue-accent)' }}>
<p style={{ fontSize: 10, fontWeight: 700, marginBottom: 4, color: 'var(--text-primary)' }}>AGENT REASONING</p>
{result.agent_reasoning.map((r, i) => <p key={i} style={{ fontSize: 10 }}>β€’ {r}</p>)}
</div>
)}
</div>
);
if (agentKey === 'contraindication') return (
<div style={style}>
{tag(result.treatment_verdict,
result.treatment_verdict === 'SAFE' ? 'var(--green-safe)'
: result.treatment_verdict === 'CONTRAINDICATED' ? 'var(--red-critical)'
: 'var(--amber-warning)')}
<p style={{ marginTop: 6 }}>{result.verdict_reason}</p>
{(result.allergy_risks || []).slice(0, 2).map((r, i) => (
<p key={i} style={{ color: 'var(--red-critical)' }}>⚠ Allergy: {r.substance} β†’ {r.risk}</p>
))}
{(result.drug_interactions || []).slice(0, 2).map((d, i) => (
<p key={i} style={{ color: 'var(--amber-warning)' }}>πŸ’Š {d.drug_a} + {d.drug_b}: {d.interaction}</p>
))}
{result.agent_reasoning && (
<div style={{ marginTop: 8, padding: 8, background: 'var(--bg-base)', borderRadius: 6, borderLeft: '3px solid var(--blue-accent)' }}>
<p style={{ fontSize: 10, fontWeight: 700, marginBottom: 4, color: 'var(--text-primary)' }}>AGENT REASONING</p>
{result.agent_reasoning.map((r, i) => <p key={i} style={{ fontSize: 10 }}>β€’ {r}</p>)}
</div>
)}
</div>
);
if (agentKey === 'labs') return (
<div style={style}>
{(result.critical_labs || []).map((l, i) => (
<p key={i} style={{ color: 'var(--red-critical)' }}>πŸ”΄ {l.name}: {l.value} β€” {l.significance}</p>
))}
{(result.abnormal_labs || []).map((l, i) => (
<p key={i} style={{ color: 'var(--amber-warning)' }}>🟑 {l.name}: {l.value} β€” {l.significance}</p>
))}
{(result.immediate_concerns || []).slice(0, 2).map((c, i) => <p key={i}>β†’ {c}</p>)}
{result.agent_reasoning && (
<div style={{ marginTop: 8, padding: 8, background: 'var(--bg-base)', borderRadius: 6, borderLeft: '3px solid var(--blue-accent)' }}>
<p style={{ fontSize: 10, fontWeight: 700, marginBottom: 4, color: 'var(--text-primary)' }}>AGENT REASONING</p>
{result.agent_reasoning.map((r, i) => <p key={i} style={{ fontSize: 10 }}>β€’ {r}</p>)}
</div>
)}
</div>
);
if (agentKey === 'differentials') return (
<div style={style}>
{(result.differentials || []).map((d, i) => (
<div key={i} style={{ marginBottom: 6 }}>
{tag(d.likelihood, d.likelihood === 'HIGH' ? 'var(--red-critical)' : d.likelihood === 'MODERATE' ? 'var(--amber-warning)' : 'var(--text-secondary)')}
<span style={{ fontWeight: 600, color: 'var(--text-primary)' }}>{d.diagnosis}</span>
{d.must_rule_out && <span style={{ color: 'var(--red-critical)', fontSize: 10, marginLeft: 6 }}>MUST RULE OUT</span>}
</div>
))}
{result.agent_reasoning && (
<div style={{ marginTop: 8, padding: 8, background: 'var(--bg-base)', borderRadius: 6, borderLeft: '3px solid var(--blue-accent)' }}>
<p style={{ fontSize: 10, fontWeight: 700, marginBottom: 4, color: 'var(--text-primary)' }}>AGENT REASONING</p>
{result.agent_reasoning.map((r, i) => <p key={i} style={{ fontSize: 10 }}>β€’ {r}</p>)}
</div>
)}
</div>
);
if (agentKey === 'action_protocol') return (
<div style={style}>
{(result.immediate_actions || []).map((a, i) => <p key={i}>{i + 1}. {a}</p>)}
{result.disposition && (
<p style={{ marginTop: 6, fontWeight: 700, color: 'var(--blue-accent)' }}>
Disposition β†’ {result.disposition}
</p>
)}
{result.agent_reasoning && (
<div style={{ marginTop: 8, padding: 8, background: 'var(--bg-base)', borderRadius: 6, borderLeft: '3px solid var(--blue-accent)' }}>
<p style={{ fontSize: 10, fontWeight: 700, marginBottom: 4, color: 'var(--text-primary)' }}>AGENT REASONING</p>
{result.agent_reasoning.map((r, i) => <p key={i} style={{ fontSize: 10 }}>β€’ {r}</p>)}
</div>
)}
</div>
);
return null;
}
export function AgentPanel({ agentStatuses, agentResults, onRun, patient, running, focus, setFocus }) {
const [expanded, setExpanded] = useState({});
const toggle = key => setExpanded(e => ({ ...e, [key]: !e[key] }));
const doneCount = AGENTS.filter(a => agentStatuses[a.key] === 'complete').length;
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ padding: '14px 16px', borderBottom: '1px solid var(--border-subtle)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontWeight: 700, fontSize: 13, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
Agent Orchestration
</span>
{running && (
<span style={{ fontSize: 11, color: 'var(--blue-accent)', display: 'flex', alignItems: 'center', gap: 5 }}>
<div className="spinner" style={{ width: 12, height: 12 }} /> {doneCount}/5 complete
</span>
)}
</div>
</div>
<div style={{ padding: '12px 14px', flex: 1, overflowY: 'auto' }}>
{!patient && (
<div style={{ textAlign: 'center', padding: '40px 20px', color: 'var(--text-tertiary)', fontSize: 13 }}>
Select a patient to run emergency analysis
</div>
)}
{patient && (
<>
{/* Clinical Focus Selector */}
<div style={{ marginBottom: 14 }}>
<p style={{ fontSize: 9, fontWeight: 800, color: 'var(--text-tertiary)', textTransform: 'uppercase', marginBottom: 6 }}>Clinical Focus</p>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{['General', 'Cardiac', 'Trauma', 'Respiratory', 'Neuro'].map(f => (
<div
key={f}
onClick={() => !running && setFocus?.(f)}
style={{
padding: '4px 10px', borderRadius: 20, fontSize: 10, fontWeight: 700, cursor: 'pointer',
background: focus === f ? 'var(--blue-accent)' : 'var(--bg-surface)',
color: focus === f ? '#fff' : 'var(--text-secondary)',
border: focus === f ? 'none' : '1px solid var(--border-subtle)',
transition: 'all 0.2s'
}}
>
{f}
</div>
))}
</div>
</div>
{/* Agent rows */}
{AGENTS.map(a => (
<AgentRow
key={a.key} agent={a}
status={agentStatuses[a.key] || 'idle'}
result={agentResults[a.key]}
expanded={expanded[a.key]}
onToggle={() => toggle(a.key)}
/>
))}
{/* Run button */}
<button className="btn-danger pulse-on-hover" style={{ marginTop: 10, width: '100%' }} onClick={onRun} disabled={running}>
{running
? <><div className="spinner" /> AI Agents Orchestrating...</>
: <><Zap size={15} /> Analyze Patient Record</>}
</button>
{/* Collaboration Log */}
{running || Object.keys(agentResults).length > 0 ? (
<div style={{ marginTop: 20, borderTop: '1px solid var(--border-subtle)', paddingTop: 16 }}>
<p style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', marginBottom: 10, letterSpacing: '0.05em' }}>
Collaboration Thread
</p>
<div style={{
background: 'var(--bg-surface)', padding: 12, borderRadius: 8, border: '1px solid var(--border-subtle)',
fontFamily: 'var(--font-mono)', fontSize: 10, lineHeight: 1.5, maxHeight: 120, overflowY: 'auto'
}}>
{AGENTS.filter(a => agentResults[a.key] || agentStatuses[a.key] === 'working').map((a, i) => (
<div key={i} style={{ marginBottom: 6 }} className="fade-in">
<span style={{ color: 'var(--blue-accent)', fontWeight: 700 }}>[{a.label}]: </span>
<span style={{ color: 'var(--text-secondary)' }}>
{agentStatuses[a.key] === 'working' ? 'Transmitting findings...' : (agentResults[a.key]?.agent_reasoning?.[0] || 'Processed record successfully.')}
</span>
</div>
))}
</div>
</div>
) : null}
</>
)}
</div>
</div>
);
}