Hackathon Builder commited on
Commit
7285ff3
·
1 Parent(s): d6b9b71

Redesign for Clinical Speed and implement Advanced Consultation Mode

Browse files
backend/main.py CHANGED
@@ -152,7 +152,8 @@ CURRENT MEDICATIONS: {json.dumps(record.get('medications', []))}
152
 
153
  Return JSON with "agent_reasoning".
154
  CLINICAL CORRELATION: Check if the Triage severity suggests a need for aggressive drugs that might interact with current meds.
155
- """{{
 
156
  "treatment_verdict": "SAFE",
157
  "verdict_reason": "one-line verdict explanation",
158
  "allergy_risks": [{{"substance": "", "risk": "", "severity": "HIGH"}}],
@@ -191,7 +192,8 @@ LABS: {json.dumps(labs)}
191
 
192
  Return JSON with "agent_reasoning".
193
  CLINICAL CORRELATION: Correlate lab abnormalities with the Triage chief complaint.
194
- """{{
 
195
  "critical_labs": [{{"name": "", "value": "", "significance": "", "immediate_action": ""}}],
196
  "abnormal_labs": [{{"name": "", "value": "", "significance": ""}}],
197
  "lab_patterns": ["pattern across multiple results"],
@@ -213,7 +215,8 @@ VITALS: {json.dumps(record.get('vitals', {}))}
213
 
214
  Return JSON with "agent_reasoning".
215
  CLINICAL CORRELATION: Synthesize the Triage severity and Lab abnormalities into a cohesive differential diagnosis.
216
- """{{
 
217
  "differentials": [
218
  {{
219
  "diagnosis": "",
@@ -468,6 +471,38 @@ def check_treatment(patient_id: str, body: Dict[str, Any] = Body(...), x_gemini_
468
  return {"patient_id": patient_id, "proposed_treatment": treatment, "result": result}
469
 
470
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  # ── MCP Tool Manifest ─────────────────────────────────────────────────────────
472
 
473
  @app.get("/mcp/tools")
 
152
 
153
  Return JSON with "agent_reasoning".
154
  CLINICAL CORRELATION: Check if the Triage severity suggests a need for aggressive drugs that might interact with current meds.
155
+
156
+ {{
157
  "treatment_verdict": "SAFE",
158
  "verdict_reason": "one-line verdict explanation",
159
  "allergy_risks": [{{"substance": "", "risk": "", "severity": "HIGH"}}],
 
192
 
193
  Return JSON with "agent_reasoning".
194
  CLINICAL CORRELATION: Correlate lab abnormalities with the Triage chief complaint.
195
+
196
+ {{
197
  "critical_labs": [{{"name": "", "value": "", "significance": "", "immediate_action": ""}}],
198
  "abnormal_labs": [{{"name": "", "value": "", "significance": ""}}],
199
  "lab_patterns": ["pattern across multiple results"],
 
215
 
216
  Return JSON with "agent_reasoning".
217
  CLINICAL CORRELATION: Synthesize the Triage severity and Lab abnormalities into a cohesive differential diagnosis.
218
+
219
+ {{
220
  "differentials": [
221
  {{
222
  "diagnosis": "",
 
471
  return {"patient_id": patient_id, "proposed_treatment": treatment, "result": result}
472
 
473
 
474
+ @app.post("/consult/{patient_id}")
475
+ def consult_team(patient_id: str, body: Dict[str, Any] = Body(...), x_gemini_key: Optional[str] = Header(None)):
476
+ """
477
+ Advanced Consultation: Ask the 5-agent team a specific clinical question.
478
+ """
479
+ record = next((p for p in PATIENTS if p.get("id") == patient_id), None)
480
+ if not record:
481
+ raise HTTPException(404, f"Patient '{patient_id}' not found")
482
+
483
+ question = body.get("question", "")
484
+ context = body.get("previous_results", {}) # Optional context from previous run
485
+
486
+ prompt = f"""You are the lead ED Attending. The doctor has asked the team a question about this patient.
487
+ PATIENT: {json.dumps(record)}
488
+ PREVIOUS FINDINGS: {json.dumps(context)}
489
+ DOCTOR'S QUESTION: "{question}"
490
+
491
+ Consult your team (Triage, Guardian, Pathologist, Physician) and provide a concise, expert clinical response.
492
+ Include 'team_consensus' and 'specialist_notes' for each relevant agent.
493
+ Return ONLY raw JSON:
494
+ {{
495
+ "answer": "Expert answer...",
496
+ "team_consensus": "agreement/disagreement summary",
497
+ "specialist_insights": [
498
+ {{"agent": "Guardian", "note": "..."}},
499
+ {{"agent": "Pathologist", "note": "..."}}
500
+ ]
501
+ }}"""
502
+ r = _gemini(prompt, override_key=x_gemini_key)
503
+ return {"result": r}
504
+
505
+
506
  # ── MCP Tool Manifest ─────────────────────────────────────────────────────────
507
 
508
  @app.get("/mcp/tools")
frontend/dist/assets/index-CHty_9PA.js DELETED
The diff for this file is too large to render. See raw diff
 
frontend/dist/assets/index-DJox2Dzs.js ADDED
The diff for this file is too large to render. See raw diff
 
frontend/dist/assets/index-DKXwe-v6.css ADDED
@@ -0,0 +1 @@
 
 
1
+ @import"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;700&display=swap";:root{--bg-base: #f0f4f8;--bg-surface: #ffffff;--bg-elevated: #ffffff;--bg-card: #ffffff;--border-subtle: rgba(15, 23, 42, .08);--border-default: rgba(15, 23, 42, .12);--border-accent: rgba(14, 165, 233, .3);--red-critical: #e11d48;--red-glow: rgba(225, 29, 72, .2);--amber-warning: #f59e0b;--amber-glow: rgba(245, 158, 11, .2);--green-safe: #10b981;--green-glow: rgba(16, 185, 129, .2);--blue-accent: #0284c7;--blue-glow: rgba(2, 132, 199, .2);--purple-ai: #8b5cf6;--text-primary: #0f172a;--text-secondary: #475569;--text-tertiary: #64748b;--font-sans: "Inter", system-ui, sans-serif;--font-mono: "JetBrains Mono", monospace}@keyframes pulse-red{0%{box-shadow:0 0 #e11d4866}70%{box-shadow:0 0 0 10px #e11d4800}to{box-shadow:0 0 #e11d4800}}@keyframes directive-glow{0%{border-color:var(--blue-accent)}50%{border-color:#fff}to{border-color:var(--blue-accent)}}@keyframes log-slide{0%{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}.pulse-critical{animation:pulse-red 2s infinite;border:1.5px solid var(--red-critical)!important}.directive-banner{background:var(--blue-accent);color:#fff;border:2px solid var(--blue-accent);animation:directive-glow 3s infinite}.log-entry{animation:log-slide .3s ease-out forwards}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%;background:var(--bg-base);color:var(--text-primary);font-family:var(--font-sans);font-size:14px;-webkit-font-smoothing:antialiased}::-webkit-scrollbar{width:4px;height:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border-default);border-radius:99px}@keyframes ekg{0%{stroke-dashoffset:1000}to{stroke-dashoffset:0}}@keyframes pulse-red{0%,to{opacity:1;box-shadow:0 0 0 0 var(--red-glow)}50%{opacity:.8;box-shadow:0 0 0 8px transparent}}@keyframes pulse-amber{0%,to{box-shadow:0 0 0 0 var(--amber-glow)}50%{box-shadow:0 0 0 6px transparent}}@keyframes fade-up{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes glow-blue{0%,to{box-shadow:0 0 12px var(--blue-glow)}50%{box-shadow:0 0 24px var(--blue-glow),0 0 4px var(--blue-accent)}}@keyframes pulse-blue-btn{0%{box-shadow:0 0 #0a84ff99}70%{box-shadow:0 0 0 10px #0a84ff00}to{box-shadow:0 0 #0a84ff00}}@keyframes slide-in-right{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}@keyframes heartbeat{0%,to{transform:scale(1)}14%{transform:scale(1.15)}28%{transform:scale(1)}42%{transform:scale(1.1)}70%{transform:scale(1)}}.card{background:var(--bg-card);border:1px solid var(--border-subtle);border-radius:12px}.card-elevated{background:var(--bg-elevated);border:1px solid var(--border-default);border-radius:12px}.glass{background:#0d1b2eb3;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--border-default);border-radius:12px}.badge{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:99px;font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.badge-critical{background:#ff2d5526;color:var(--red-critical);border:1px solid rgba(255,45,85,.3);animation:pulse-red 2s infinite}.badge-moderate{background:#ff9f0a1f;color:var(--amber-warning);border:1px solid rgba(255,159,10,.3)}.badge-stable{background:#30d1581a;color:var(--green-safe);border:1px solid rgba(48,209,88,.25)}.badge-unknown{background:#8e8e931a;color:var(--text-secondary);border:1px solid rgba(142,142,147,.2)}.verdict-safe{background:#30d15826;color:var(--green-safe);border:1px solid rgba(48,209,88,.3)}.verdict-caution{background:#ff9f0a26;color:var(--amber-warning);border:1px solid rgba(255,159,10,.3);animation:pulse-amber 2s infinite}.verdict-contraindicated{background:#ff2d5526;color:var(--red-critical);border:1px solid rgba(255,45,85,.4);animation:pulse-red 1.5s infinite}.verdict-not_evaluated{background:#8e8e931a;color:var(--text-secondary);border:1px solid rgba(142,142,147,.2)}.agent-idle{border-color:var(--border-subtle)}.agent-working{border-color:var(--blue-accent);animation:glow-blue 1.2s infinite}.agent-complete{border-color:var(--green-safe);box-shadow:0 0 10px var(--green-glow)}.agent-error{border-color:var(--red-critical)}.btn-primary{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 20px;border-radius:8px;border:none;cursor:pointer;font-family:var(--font-sans);font-weight:600;font-size:13px;background:var(--blue-accent);color:#fff;transition:opacity .15s,transform .1s}.btn-primary:hover:not(:disabled){opacity:.85;transform:translateY(-1px)}.btn-primary:active{transform:translateY(0)}.btn-primary:disabled{opacity:.35;cursor:not-allowed}.btn-danger{background:linear-gradient(135deg,#c0392b,var(--red-critical));color:#fff;display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:12px 24px;border-radius:8px;border:none;cursor:pointer;font-family:var(--font-sans);font-weight:700;font-size:14px;letter-spacing:.02em;box-shadow:0 4px 20px #ff2d554d;transition:box-shadow .2s,transform .1s;width:100%}.btn-danger:hover:not(:disabled){box-shadow:0 6px 30px #ff2d5580;transform:translateY(-1px)}.btn-danger:disabled{opacity:.3;cursor:not-allowed;transform:none}.btn-ghost{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border-radius:7px;border:1px solid var(--border-default);background:transparent;color:var(--text-secondary);cursor:pointer;font-family:var(--font-sans);font-size:12px;font-weight:500;transition:border-color .15s,color .15s,background .15s}.btn-ghost:hover{border-color:var(--blue-accent);color:var(--blue-accent);background:var(--blue-glow)}.btn-demo{animation:pulse-blue-btn 2s infinite}.spinner{width:16px;height:16px;border:2px solid rgba(255,255,255,.2);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite}.fade-up{animation:fade-up .35s ease both}.fade-in{animation:fade-in .3s ease both}.slide-in{animation:slide-in-right .35s ease both}.mono{font-family:var(--font-mono)}.input-dark{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:8px;color:var(--text-primary);padding:9px 12px;font-family:var(--font-sans);font-size:13px;width:100%;outline:none;transition:border-color .15s}.input-dark:focus{border-color:var(--blue-accent);box-shadow:0 0 0 3px #0a84ff26}.input-dark::-moz-placeholder{color:var(--text-tertiary)}.input-dark::placeholder{color:var(--text-tertiary)}.divider{height:1px;background:var(--border-subtle)}.welcome-screen{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;background:radial-gradient(ellipse at 50% 0%,rgba(10,132,255,.08) 0%,transparent 70%),var(--bg-base);text-align:center;padding:40px 20px}
frontend/dist/assets/index-kcxpakQ_.css DELETED
@@ -1 +0,0 @@
1
- @import"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;700&display=swap";:root{--bg-base: #f0f4f8;--bg-surface: #ffffff;--bg-elevated: #ffffff;--bg-card: #ffffff;--border-subtle: rgba(15, 23, 42, .08);--border-default: rgba(15, 23, 42, .12);--border-accent: rgba(14, 165, 233, .3);--red-critical: #e11d48;--red-glow: rgba(225, 29, 72, .2);--amber-warning: #f59e0b;--amber-glow: rgba(245, 158, 11, .2);--green-safe: #10b981;--green-glow: rgba(16, 185, 129, .2);--blue-accent: #0284c7;--blue-glow: rgba(2, 132, 199, .2);--purple-ai: #8b5cf6;--text-primary: #0f172a;--text-secondary: #475569;--text-tertiary: #64748b;--font-sans: "Inter", system-ui, sans-serif;--font-mono: "JetBrains Mono", monospace}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%;background:var(--bg-base);color:var(--text-primary);font-family:var(--font-sans);font-size:14px;-webkit-font-smoothing:antialiased}::-webkit-scrollbar{width:4px;height:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border-default);border-radius:99px}@keyframes ekg{0%{stroke-dashoffset:1000}to{stroke-dashoffset:0}}@keyframes pulse-red{0%,to{opacity:1;box-shadow:0 0 0 0 var(--red-glow)}50%{opacity:.8;box-shadow:0 0 0 8px transparent}}@keyframes pulse-amber{0%,to{box-shadow:0 0 0 0 var(--amber-glow)}50%{box-shadow:0 0 0 6px transparent}}@keyframes fade-up{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes glow-blue{0%,to{box-shadow:0 0 12px var(--blue-glow)}50%{box-shadow:0 0 24px var(--blue-glow),0 0 4px var(--blue-accent)}}@keyframes pulse-blue-btn{0%{box-shadow:0 0 #0a84ff99}70%{box-shadow:0 0 0 10px #0a84ff00}to{box-shadow:0 0 #0a84ff00}}@keyframes slide-in-right{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}@keyframes heartbeat{0%,to{transform:scale(1)}14%{transform:scale(1.15)}28%{transform:scale(1)}42%{transform:scale(1.1)}70%{transform:scale(1)}}.card{background:var(--bg-card);border:1px solid var(--border-subtle);border-radius:12px}.card-elevated{background:var(--bg-elevated);border:1px solid var(--border-default);border-radius:12px}.glass{background:#0d1b2eb3;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--border-default);border-radius:12px}.badge{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:99px;font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.badge-critical{background:#ff2d5526;color:var(--red-critical);border:1px solid rgba(255,45,85,.3);animation:pulse-red 2s infinite}.badge-moderate{background:#ff9f0a1f;color:var(--amber-warning);border:1px solid rgba(255,159,10,.3)}.badge-stable{background:#30d1581a;color:var(--green-safe);border:1px solid rgba(48,209,88,.25)}.badge-unknown{background:#8e8e931a;color:var(--text-secondary);border:1px solid rgba(142,142,147,.2)}.verdict-safe{background:#30d15826;color:var(--green-safe);border:1px solid rgba(48,209,88,.3)}.verdict-caution{background:#ff9f0a26;color:var(--amber-warning);border:1px solid rgba(255,159,10,.3);animation:pulse-amber 2s infinite}.verdict-contraindicated{background:#ff2d5526;color:var(--red-critical);border:1px solid rgba(255,45,85,.4);animation:pulse-red 1.5s infinite}.verdict-not_evaluated{background:#8e8e931a;color:var(--text-secondary);border:1px solid rgba(142,142,147,.2)}.agent-idle{border-color:var(--border-subtle)}.agent-working{border-color:var(--blue-accent);animation:glow-blue 1.2s infinite}.agent-complete{border-color:var(--green-safe);box-shadow:0 0 10px var(--green-glow)}.agent-error{border-color:var(--red-critical)}.btn-primary{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 20px;border-radius:8px;border:none;cursor:pointer;font-family:var(--font-sans);font-weight:600;font-size:13px;background:var(--blue-accent);color:#fff;transition:opacity .15s,transform .1s}.btn-primary:hover:not(:disabled){opacity:.85;transform:translateY(-1px)}.btn-primary:active{transform:translateY(0)}.btn-primary:disabled{opacity:.35;cursor:not-allowed}.btn-danger{background:linear-gradient(135deg,#c0392b,var(--red-critical));color:#fff;display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:12px 24px;border-radius:8px;border:none;cursor:pointer;font-family:var(--font-sans);font-weight:700;font-size:14px;letter-spacing:.02em;box-shadow:0 4px 20px #ff2d554d;transition:box-shadow .2s,transform .1s;width:100%}.btn-danger:hover:not(:disabled){box-shadow:0 6px 30px #ff2d5580;transform:translateY(-1px)}.btn-danger:disabled{opacity:.3;cursor:not-allowed;transform:none}.btn-ghost{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border-radius:7px;border:1px solid var(--border-default);background:transparent;color:var(--text-secondary);cursor:pointer;font-family:var(--font-sans);font-size:12px;font-weight:500;transition:border-color .15s,color .15s,background .15s}.btn-ghost:hover{border-color:var(--blue-accent);color:var(--blue-accent);background:var(--blue-glow)}.btn-demo{animation:pulse-blue-btn 2s infinite}.spinner{width:16px;height:16px;border:2px solid rgba(255,255,255,.2);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite}.fade-up{animation:fade-up .35s ease both}.fade-in{animation:fade-in .3s ease both}.slide-in{animation:slide-in-right .35s ease both}.mono{font-family:var(--font-mono)}.input-dark{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:8px;color:var(--text-primary);padding:9px 12px;font-family:var(--font-sans);font-size:13px;width:100%;outline:none;transition:border-color .15s}.input-dark:focus{border-color:var(--blue-accent);box-shadow:0 0 0 3px #0a84ff26}.input-dark::-moz-placeholder{color:var(--text-tertiary)}.input-dark::placeholder{color:var(--text-tertiary)}.divider{height:1px;background:var(--border-subtle)}.welcome-screen{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;background:radial-gradient(ellipse at 50% 0%,rgba(10,132,255,.08) 0%,transparent 70%),var(--bg-base);text-align:center;padding:40px 20px}
 
 
frontend/dist/index.html CHANGED
@@ -7,8 +7,8 @@
7
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
8
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
9
  <title>CodeBlue Context</title>
10
- <script type="module" crossorigin src="/assets/index-CHty_9PA.js"></script>
11
- <link rel="stylesheet" crossorigin href="/assets/index-kcxpakQ_.css">
12
  </head>
13
  <body>
14
  <div id="root"></div>
 
7
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
8
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
9
  <title>CodeBlue Context</title>
10
+ <script type="module" crossorigin src="/assets/index-DJox2Dzs.js"></script>
11
+ <link rel="stylesheet" crossorigin href="/assets/index-DKXwe-v6.css">
12
  </head>
13
  <body>
14
  <div id="root"></div>
frontend/src/AgentPanel.jsx CHANGED
@@ -204,17 +204,26 @@ export function AgentPanel({ agentStatuses, agentResults, onRun, patient, runnin
204
 
205
  {patient && (
206
  <>
207
- {/* Patient summary strip */}
208
- <div style={{
209
- padding: '8px 12px', borderRadius: 8, marginBottom: 14,
210
- background: 'var(--bg-surface)', border: '1px solid var(--border-subtle)',
211
- fontSize: 12, color: 'var(--text-secondary)', display: 'flex', gap: 12,
212
- }}>
213
- <span style={{ fontWeight: 700, color: 'var(--text-primary)' }}>{patient.display_name}</span>
214
- <span>{patient.age}y {patient.gender}</span>
215
- <span style={{ marginLeft: 'auto' }}>
216
- <span className={`badge badge-${patient.risk_level || 'stable'}`}>{patient.risk_level}</span>
217
- </span>
 
 
 
 
 
 
 
 
 
218
  </div>
219
 
220
  {/* Agent rows */}
@@ -229,10 +238,10 @@ export function AgentPanel({ agentStatuses, agentResults, onRun, patient, runnin
229
  ))}
230
 
231
  {/* Run button */}
232
- <button className="btn-danger" style={{ marginTop: 10 }} onClick={onRun} disabled={running}>
233
  {running
234
- ? <><div className="spinner" /> Analyzing...</>
235
- : <><Zap size={15} /> Generate Emergency Snapshot</>}
236
  </button>
237
 
238
  {/* Collaboration Log */}
 
204
 
205
  {patient && (
206
  <>
207
+ {/* Clinical Focus Selector */}
208
+ <div style={{ marginBottom: 14 }}>
209
+ <p style={{ fontSize: 9, fontWeight: 800, color: 'var(--text-tertiary)', textTransform: 'uppercase', marginBottom: 6 }}>Clinical Focus</p>
210
+ <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
211
+ {['General', 'Cardiac', 'Trauma', 'Respiratory', 'Neuro'].map(f => (
212
+ <div
213
+ key={f}
214
+ onClick={() => !running && setFocus?.(f)}
215
+ style={{
216
+ padding: '4px 10px', borderRadius: 20, fontSize: 10, fontWeight: 700, cursor: 'pointer',
217
+ background: focus === f ? 'var(--blue-accent)' : 'var(--bg-surface)',
218
+ color: focus === f ? '#fff' : 'var(--text-secondary)',
219
+ border: focus === f ? 'none' : '1px solid var(--border-subtle)',
220
+ transition: 'all 0.2s'
221
+ }}
222
+ >
223
+ {f}
224
+ </div>
225
+ ))}
226
+ </div>
227
  </div>
228
 
229
  {/* Agent rows */}
 
238
  ))}
239
 
240
  {/* Run button */}
241
+ <button className="btn-danger pulse-on-hover" style={{ marginTop: 10, width: '100%' }} onClick={onRun} disabled={running}>
242
  {running
243
+ ? <><div className="spinner" /> AI Agents Orchestrating...</>
244
+ : <><Zap size={15} /> Analyze Patient Record</>}
245
  </button>
246
 
247
  {/* Collaboration Log */}
frontend/src/App.jsx CHANGED
@@ -6,6 +6,7 @@ import { PatientPanel } from './PatientPanel';
6
  import { AgentPanel } from './AgentPanel';
7
  import { BriefPanel } from './BriefPanel';
8
  import { ApiKeyModal } from './ApiKeyModal';
 
9
 
10
  const API = import.meta.env.VITE_API_BASE || '';
11
 
@@ -29,6 +30,9 @@ export default function App() {
29
  const [agentResults, setAgentResults] = useState({});
30
  const [briefData, setBriefData] = useState(null);
31
 
 
 
 
32
  // Load patients
33
  const fetchPatients = useCallback(async () => {
34
  setPatientsLoading(true);
@@ -50,6 +54,7 @@ export default function App() {
50
  setBriefData(null);
51
  setAgentStatuses(INITIAL_STATUSES);
52
  setAgentResults({});
 
53
  };
54
 
55
  // Upload patient JSON
@@ -181,19 +186,11 @@ export default function App() {
181
  <span style={{ color: 'var(--green-safe)', fontWeight: 700 }}>●</span> System Online
182
  </span>
183
  <span>{patients.length} patients</span>
184
- <div style={{
185
- padding: '4px 12px', borderRadius: 99,
186
- background: 'rgba(48,209,88,0.08)',
187
- border: '1px solid rgba(48,209,88,0.2)',
188
- color: 'var(--green-safe)', fontWeight: 700,
189
- }}>
190
- 15 min → &lt;10 sec
191
- </div>
192
  </div>
193
  </header>
194
 
195
  {/* 3-column layout */}
196
- <div style={{ flex: 1, display: 'grid', gridTemplateColumns: '240px 1fr 1fr', overflow: 'hidden' }}>
197
 
198
  {/* Left: Patient List */}
199
  <div style={{ borderRight: '1px solid var(--border-subtle)', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
@@ -206,15 +203,50 @@ export default function App() {
206
  />
207
  </div>
208
 
209
- {/* Center: Agent Orchestration */}
210
  <div style={{ borderRight: '1px solid var(--border-subtle)', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
211
- <AgentPanel
212
- patient={selected}
213
- agentStatuses={agentStatuses}
214
- agentResults={agentResults}
215
- onRun={runOrchestration}
216
- running={running}
217
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  </div>
219
 
220
  {/* Right: Emergency Brief */}
 
6
  import { AgentPanel } from './AgentPanel';
7
  import { BriefPanel } from './BriefPanel';
8
  import { ApiKeyModal } from './ApiKeyModal';
9
+ import ConsultPanel from './ConsultPanel';
10
 
11
  const API = import.meta.env.VITE_API_BASE || '';
12
 
 
30
  const [agentResults, setAgentResults] = useState({});
31
  const [briefData, setBriefData] = useState(null);
32
 
33
+ const [activeTab, setActiveTab] = useState('orchestrate');
34
+ const [focus, setFocus] = useState('General');
35
+
36
  // Load patients
37
  const fetchPatients = useCallback(async () => {
38
  setPatientsLoading(true);
 
54
  setBriefData(null);
55
  setAgentStatuses(INITIAL_STATUSES);
56
  setAgentResults({});
57
+ setActiveTab('orchestrate');
58
  };
59
 
60
  // Upload patient JSON
 
186
  <span style={{ color: 'var(--green-safe)', fontWeight: 700 }}>●</span> System Online
187
  </span>
188
  <span>{patients.length} patients</span>
 
 
 
 
 
 
 
 
189
  </div>
190
  </header>
191
 
192
  {/* 3-column layout */}
193
+ <div style={{ flex: 1, display: 'grid', gridTemplateColumns: '240px 420px 1fr', overflow: 'hidden' }}>
194
 
195
  {/* Left: Patient List */}
196
  <div style={{ borderRight: '1px solid var(--border-subtle)', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
 
203
  />
204
  </div>
205
 
206
+ {/* Center: Tabs + Content */}
207
  <div style={{ borderRight: '1px solid var(--border-subtle)', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
208
+ <div style={{ display: 'flex', background: 'var(--bg-base)', padding: '4px 4px 0', borderBottom: '1px solid var(--border-subtle)' }}>
209
+ <button
210
+ onClick={() => setActiveTab('orchestrate')}
211
+ style={{
212
+ flex: 1, padding: '10px', fontSize: 11, fontWeight: 700, cursor: 'pointer', border: 'none',
213
+ background: activeTab === 'orchestrate' ? 'var(--bg-surface)' : 'transparent',
214
+ color: activeTab === 'orchestrate' ? 'var(--blue-accent)' : 'var(--text-secondary)',
215
+ borderRadius: '8px 8px 0 0', borderBottom: activeTab === 'orchestrate' ? '2px solid var(--blue-accent)' : 'none'
216
+ }}
217
+ >
218
+ ORCHESTRATION
219
+ </button>
220
+ <button
221
+ onClick={() => setActiveTab('consult')}
222
+ style={{
223
+ flex: 1, padding: '10px', fontSize: 11, fontWeight: 700, cursor: 'pointer', border: 'none',
224
+ background: activeTab === 'consult' ? 'var(--bg-surface)' : 'transparent',
225
+ color: activeTab === 'consult' ? 'var(--purple-ai)' : 'var(--text-secondary)',
226
+ borderRadius: '8px 8px 0 0', borderBottom: activeTab === 'consult' ? '2px solid var(--purple-ai)' : 'none'
227
+ }}
228
+ >
229
+ ADVANCED CONSULT
230
+ </button>
231
+ </div>
232
+ <div style={{ flex: 1, overflow: 'hidden' }}>
233
+ {activeTab === 'orchestrate' ? (
234
+ <AgentPanel
235
+ patient={selected}
236
+ agentStatuses={agentStatuses}
237
+ agentResults={agentResults}
238
+ onRun={runOrchestration}
239
+ running={running}
240
+ focus={focus}
241
+ setFocus={setFocus}
242
+ />
243
+ ) : (
244
+ <ConsultPanel
245
+ patientId={selected?.id}
246
+ previousResults={agentResults}
247
+ />
248
+ )}
249
+ </div>
250
  </div>
251
 
252
  {/* Right: Emergency Brief */}
frontend/src/BriefPanel.jsx CHANGED
@@ -166,43 +166,48 @@ export function BriefPanel({ data, patient }) {
166
  </div>
167
 
168
  <div style={{ flex: 1, overflowY: 'auto', padding: '12px 14px' }}>
169
- {/* Triage Banner */}
170
- {triage.severity_label && (
171
- <div className="fade-up" style={{
172
- padding: '10px 14px', borderRadius: 10, marginBottom: 14,
173
- background: severityColor + '12', border: `2px solid ${severityColor}40`,
174
- display: 'flex', alignItems: 'center', gap: 10,
175
- }}>
176
- <div style={{ fontSize: 22, fontWeight: 900, color: severityColor, fontFamily: 'var(--font-mono)' }}>
177
- ESI {triage.severity}
178
- </div>
179
- <div>
180
- <div style={{ fontSize: 12, fontWeight: 700, color: severityColor }}>{triage.severity_label}</div>
181
- <div style={{ fontSize: 11, color: 'var(--text-secondary)' }}>{triage.chief_complaint}</div>
182
- </div>
183
- </div>
184
- )}
185
-
186
- {/* Primary Objective Banner */}
187
  {action.primary_objective && (
188
- <div className="fade-up" style={{
189
- padding: '12px 14px', borderRadius: 10, marginBottom: 14,
190
- background: 'var(--blue-accent)', border: 'none',
191
- color: '#fff', boxShadow: '0 4px 12px rgba(10, 132, 255, 0.3)'
192
  }}>
193
- <div style={{ fontSize: 9, fontWeight: 800, textTransform: 'uppercase', opacity: 0.8, letterSpacing: '0.08em', marginBottom: 4 }}>
194
- Primary Clinical Objective
195
  </div>
196
- <div style={{ fontSize: 14, fontWeight: 700, lineHeight: 1.4 }}>
197
  {action.primary_objective}
198
  </div>
 
199
  </div>
200
  )}
201
 
202
- {/* Critical Alerts */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  {(triage.time_critical_flags?.length > 0 || triage.do_not_delay?.length > 0) && (
204
- <Section title="Time-Critical Findings" icon={<AlertTriangle size={12}/>} accent="var(--red-critical)">
205
- {(triage.time_critical_flags || []).map((f, i) => <AlertBox key={i} text={f} severity="critical" />)}
 
 
206
  {(triage.do_not_delay || []).map((f, i) => <AlertBox key={i} text={`⚡ NOW: ${f}`} severity="warning" />)}
207
  </Section>
208
  )}
@@ -222,21 +227,19 @@ export function BriefPanel({ data, patient }) {
222
  </Section>
223
  )}
224
 
225
- {/* Multi-Agent Correlation Alerts */}
226
  {action.agent_reasoning && (
227
- <Section title="Team Collaboration Insights" icon={<Zap size={12}/>} accent="var(--purple-ai)">
228
- <div style={{ padding: '8px 12px', borderRadius: 8, background: 'var(--bg-card)', border: '1px solid var(--purple-ai)30' }}>
229
- <div style={{ fontSize: 9, fontWeight: 800, color: 'var(--purple-ai)', textTransform: 'uppercase', marginBottom: 6 }}>
230
- Orchestration Consensus
231
- </div>
232
- <p style={{ fontSize: 11, color: 'var(--text-primary)', lineHeight: 1.5, fontStyle: 'italic' }}>
233
- "{action.agent_reasoning[0]}"
234
- </p>
235
- {action.agent_reasoning.length > 1 && (
236
- <p style={{ fontSize: 11, color: 'var(--text-secondary)', marginTop: 4, borderTop: '1px solid var(--border-subtle)', paddingTop: 4 }}>
237
- Hand-off: {action.agent_reasoning[1]}
238
- </p>
239
- )}
240
  </div>
241
  </Section>
242
  )}
 
166
  </div>
167
 
168
  <div style={{ flex: 1, overflowY: 'auto', padding: '12px 14px' }}>
169
+ {/* Primary Clinical Directive */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  {action.primary_objective && (
171
+ <div className="fade-up directive-banner" style={{
172
+ padding: '14px 18px', borderRadius: 12, marginBottom: 20,
173
+ boxShadow: '0 8px 24px rgba(2, 132, 199, 0.25)',
174
+ position: 'relative', overflow: 'hidden'
175
  }}>
176
+ <div style={{ fontSize: 9, fontWeight: 900, textTransform: 'uppercase', opacity: 0.9, letterSpacing: '0.12em', marginBottom: 6 }}>
177
+ Primary Clinical Directive
178
  </div>
179
+ <div style={{ fontSize: 16, fontWeight: 800, lineHeight: 1.3 }}>
180
  {action.primary_objective}
181
  </div>
182
+ <Zap size={40} style={{ position: 'absolute', right: -10, bottom: -10, opacity: 0.1, transform: 'rotate(15deg)' }} />
183
  </div>
184
  )}
185
 
186
+ {/* ESI & Status Summary */}
187
+ <div style={{ display: 'flex', gap: 10, marginBottom: 20 }}>
188
+ <div style={{
189
+ flex: 1, background: severityColor + '15', border: `2px solid ${severityColor}`,
190
+ borderRadius: 12, padding: '12px', textAlign: 'center'
191
+ }}>
192
+ <div style={{ fontSize: 9, fontWeight: 800, color: severityColor, textTransform: 'uppercase' }}>ESI Severity</div>
193
+ <div style={{ fontSize: 28, fontWeight: 900, color: severityColor }}>{triage.severity || '?'}</div>
194
+ <div style={{ fontSize: 10, fontWeight: 700, color: severityColor }}>{triage.severity_label}</div>
195
+ </div>
196
+ <div style={{
197
+ flex: 1, background: 'var(--bg-base)', border: '1px solid var(--border-subtle)',
198
+ borderRadius: 12, padding: '12px', display: 'flex', flexDirection: 'column', justifyContent: 'center'
199
+ }}>
200
+ <div style={{ fontSize: 9, fontWeight: 800, color: 'var(--text-tertiary)', textTransform: 'uppercase' }}>Disposition</div>
201
+ <div style={{ fontSize: 14, fontWeight: 800, color: 'var(--blue-accent)' }}>{action.disposition || 'PENDING'}</div>
202
+ </div>
203
+ </div>
204
+
205
+ {/* Threat Detection */}
206
  {(triage.time_critical_flags?.length > 0 || triage.do_not_delay?.length > 0) && (
207
+ <Section title="Immediate Threats" icon={<ShieldAlert size={14}/>} accent="var(--red-critical)">
208
+ <div className="pulse-critical" style={{ borderRadius: 8, padding: 4 }}>
209
+ {(triage.time_critical_flags || []).map((f, i) => <AlertBox key={i} text={f} severity="critical" />)}
210
+ </div>
211
  {(triage.do_not_delay || []).map((f, i) => <AlertBox key={i} text={`⚡ NOW: ${f}`} severity="warning" />)}
212
  </Section>
213
  )}
 
227
  </Section>
228
  )}
229
 
230
+ {/* Team Insights */}
231
  {action.agent_reasoning && (
232
+ <Section title="Expert Correlation" icon={<Zap size={14}/>} accent="var(--purple-ai)">
233
+ <div style={{
234
+ padding: '12px', borderRadius: 10, background: 'var(--purple-ai)05',
235
+ border: '1px dashed var(--purple-ai)40', fontSize: 11, lineHeight: 1.5
236
+ }}>
237
+ <p style={{ fontWeight: 700, color: 'var(--purple-ai)', marginBottom: 4 }}>TEAM CONSENSUS</p>
238
+ {action.agent_reasoning.map((r, i) => (
239
+ <p key={i} style={{ marginBottom: 4, display: 'flex', gap: 6 }}>
240
+ <span style={{ color: 'var(--purple-ai)' }}>•</span> {r}
241
+ </p>
242
+ ))}
 
 
243
  </div>
244
  </Section>
245
  )}
frontend/src/ConsultPanel.jsx ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { MessageSquare, Send, Bot, User, Zap, ShieldAlert, FlaskConical, Stethoscope } from 'lucide-react';
3
+
4
+ const API_BASE = import.meta.env.VITE_API_BASE || '';
5
+
6
+ export default function ConsultPanel({ patientId, previousResults }) {
7
+ const [messages, setMessages] = useState([
8
+ { role: 'assistant', content: "Hello Doctor. I've synthesized the team's findings. You can now ask us specific clinical questions about this case.", type: 'initial' }
9
+ ]);
10
+ const [input, setInput] = useState('');
11
+ const [loading, setLoading] = useState(false);
12
+
13
+ const sendMessage = async () => {
14
+ if (!input.trim() || loading || !patientId) return;
15
+
16
+ const userMsg = input.trim();
17
+ setMessages(prev => [...prev, { role: 'user', content: userMsg }]);
18
+ setInput('');
19
+ setLoading(true);
20
+
21
+ try {
22
+ const geminiKey = localStorage.getItem('gemini_key');
23
+ const headers = { 'Content-Type': 'application/json' };
24
+ if (geminiKey) headers['x-gemini-key'] = geminiKey;
25
+
26
+ const res = await fetch(`${API_BASE}/consult/${patientId}`, {
27
+ method: 'POST',
28
+ headers,
29
+ body: JSON.stringify({
30
+ question: userMsg,
31
+ previous_results: previousResults
32
+ }),
33
+ });
34
+
35
+ const data = await res.json();
36
+ const result = data.result;
37
+
38
+ if (result) {
39
+ setMessages(prev => [...prev, {
40
+ role: 'assistant',
41
+ content: result.answer,
42
+ insights: result.specialist_insights,
43
+ consensus: result.team_consensus
44
+ }]);
45
+ } else {
46
+ throw new Error('No result');
47
+ }
48
+ } catch (err) {
49
+ setMessages(prev => [...prev, { role: 'assistant', content: "I'm sorry, I couldn't reach the agent team. Please verify your API key." }]);
50
+ } finally {
51
+ setLoading(false);
52
+ }
53
+ };
54
+
55
+ return (
56
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', background: 'var(--bg-surface)' }}>
57
+ <div style={{ padding: '14px 16px', borderBottom: '1px solid var(--border-subtle)', background: 'var(--bg-surface)' }}>
58
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
59
+ <Bot size={18} color="var(--purple-ai)" />
60
+ <span style={{ fontWeight: 700, fontSize: 13, color: 'var(--text-primary)' }}>Multi-Agent Consultation</span>
61
+ </div>
62
+ </div>
63
+
64
+ <div style={{ flex: 1, overflowY: 'auto', padding: '16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
65
+ {messages.map((m, i) => (
66
+ <div key={i} style={{
67
+ alignSelf: m.role === 'user' ? 'flex-end' : 'flex-start',
68
+ maxWidth: '85%',
69
+ display: 'flex', gap: 10,
70
+ flexDirection: m.role === 'user' ? 'row-reverse' : 'row'
71
+ }}>
72
+ <div style={{
73
+ width: 28, height: 28, borderRadius: '50%',
74
+ background: m.role === 'user' ? 'var(--blue-accent)' : 'var(--purple-ai)15',
75
+ display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
76
+ color: m.role === 'user' ? '#fff' : 'var(--purple-ai)'
77
+ }}>
78
+ {m.role === 'user' ? <User size={14} /> : <Bot size={14} />}
79
+ </div>
80
+
81
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
82
+ <div style={{
83
+ padding: '10px 14px', borderRadius: 12,
84
+ background: m.role === 'user' ? 'var(--blue-accent)' : 'var(--bg-base)',
85
+ color: m.role === 'user' ? '#fff' : 'var(--text-primary)',
86
+ fontSize: 12, lineHeight: 1.5,
87
+ boxShadow: m.role === 'user' ? '0 2px 8px rgba(2, 132, 199, 0.2)' : 'none'
88
+ }}>
89
+ {m.content}
90
+ </div>
91
+
92
+ {m.insights && (
93
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4, marginTop: 4 }}>
94
+ {m.insights.map((ins, j) => (
95
+ <div key={j} style={{
96
+ fontSize: 10, padding: '6px 8px', borderRadius: 6,
97
+ background: 'var(--bg-surface)', border: '1px solid var(--border-subtle)',
98
+ color: 'var(--text-secondary)', display: 'flex', gap: 6
99
+ }}>
100
+ <span style={{ fontWeight: 800, color: 'var(--purple-ai)' }}>{ins.agent}:</span>
101
+ <span>{ins.note}</span>
102
+ </div>
103
+ ))}
104
+ </div>
105
+ )}
106
+ </div>
107
+ </div>
108
+ ))}
109
+ {loading && (
110
+ <div style={{ alignSelf: 'flex-start', display: 'flex', gap: 10 }}>
111
+ <div className="spinner" style={{ width: 14, height: 14 }} />
112
+ <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Consulting agents...</span>
113
+ </div>
114
+ )}
115
+ </div>
116
+
117
+ <div style={{ padding: 16, borderTop: '1px solid var(--border-subtle)' }}>
118
+ <div style={{ position: 'relative' }}>
119
+ <input
120
+ type="text"
121
+ placeholder="Ask the team a question..."
122
+ value={input}
123
+ onChange={e => setInput(e.target.value)}
124
+ onKeyDown={e => e.key === 'Enter' && sendMessage()}
125
+ style={{
126
+ width: '100%', padding: '12px 45px 12px 14px', borderRadius: 10,
127
+ border: '1px solid var(--border-default)', outline: 'none',
128
+ fontSize: 12, transition: 'all 0.2s',
129
+ background: 'var(--bg-base)'
130
+ }}
131
+ />
132
+ <button
133
+ onClick={sendMessage}
134
+ disabled={loading || !input.trim()}
135
+ style={{
136
+ position: 'absolute', right: 8, top: '50%', transform: 'translateY(-50%)',
137
+ background: 'var(--blue-accent)', color: '#fff', border: 'none',
138
+ width: 28, height: 28, borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center',
139
+ cursor: 'pointer', opacity: (loading || !input.trim()) ? 0.5 : 1
140
+ }}
141
+ >
142
+ <Send size={14} />
143
+ </button>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ );
148
+ }