"""Gradio UI layout for Diagnostic Devil's Advocate."""
import gradio as gr
from config import ENABLE_MEDASR
def build_ui(analyze_fn, load_demo_fn, transcribe_fn=None):
"""
Build the Gradio Blocks UI.
Args:
analyze_fn: generator(image, diagnosis, context, modality) -> yields HTML
load_demo_fn: callback(demo_name) -> (image, diagnosis, context, modality)
transcribe_fn: callback(audio, existing_context) -> yields (context, status_html) (optional)
"""
with gr.Blocks(title="Diagnostic Devil's Advocate") as demo:
# ── Hero Banner ──
gr.HTML(
"""
MedGemma Impact Challenge
Diagnostic Devil's Advocate
AI-Powered Cognitive Debiasing for Medical Image Interpretation
Upload a medical image with the working diagnosis.
Four AI agents will independently analyze it, detect cognitive biases,
challenge the diagnosis, and synthesize a debiasing report.
MedGemma 1.5
MedSigLIP
LangGraph
MedASR
"""
)
# ── Demo Cases Row (3 clickable cards) ──
gr.HTML('SELECT A DEMO CASE
')
with gr.Row(elem_classes=["case-row"]):
demo_btn_1 = gr.Button(
value="",
elem_id="case-btn-1",
elem_classes=["case-card-btn"],
)
demo_btn_2 = gr.Button(
value="",
elem_id="case-btn-2",
elem_classes=["case-card-btn"],
)
demo_btn_3 = gr.Button(
value="",
elem_id="case-btn-3",
elem_classes=["case-card-btn"],
)
# Overlay HTML on top of buttons for card visuals
gr.HTML("""
🫁
TRAUMA
Missed Pneumothorax
32-year-old Male
Motorcycle collision · Left chest pain · HR 104 · SpO₂ 96%
Initial Dx:
Rib contusion
🫀
VASCULAR
Aortic Dissection
58-year-old Male
Sudden chest→back pain · BP asymmetry 32mmHg · D-dimer 4850
Initial Dx:
GERD / Reflux
🩸
POSTPARTUM
Pulmonary Embolism
29-year-old Female
5 days post C-section · HR 118 · SpO₂ 91% · pO₂ 68
Initial Dx:
Postpartum anxiety
""")
# ── Main Content: Input + Output ──
with gr.Row(equal_height=False):
# ═══════════ Left Column: Input ═══════════
with gr.Column(scale=4, min_width=340):
gr.HTML('CLINICAL INPUT
')
image_input = gr.Image(
type="pil",
label="Medical Image",
height=240,
)
modality_input = gr.Radio(
choices=["CXR", "CT", "Other"],
value="CXR",
label="Imaging Modality",
)
diagnosis_input = gr.Textbox(
label="Doctor's Working Diagnosis",
placeholder="e.g., Left rib contusion with musculoskeletal chest wall pain",
)
context_input = gr.Textbox(
label="Clinical Context (history, vitals, labs, exam)",
placeholder=(
"e.g., 32M, motorcycle accident, left-sided chest pain, "
"HR 104, SpO2 96%, WBC 11.2..."
),
lines=5,
)
# ── Voice Input (MedASR) ──
if ENABLE_MEDASR and transcribe_fn:
gr.HTML("""
Record clinical context with your microphone.
Text will be appended to the context field above.
""")
with gr.Row(elem_classes=["voice-row"]):
audio_input = gr.Audio(
sources=["microphone"],
type="numpy",
label="",
show_label=False,
elem_classes=["voice-audio"],
)
with gr.Column(scale=1, min_width=160):
transcribe_btn = gr.Button(
"Transcribe",
size="sm",
elem_classes=["transcribe-btn"],
)
voice_status = gr.HTML(
value='Ready to record
',
)
else:
gr.HTML(
'Voice input disabled (MedASR)
'
)
analyze_btn = gr.Button(
"Analyze & Challenge Diagnosis",
variant="primary",
size="lg",
elem_classes=["analyze-btn"],
)
# ═══════════ Right Column: Pipeline Output ═══════════
with gr.Column(scale=6, min_width=500):
gr.HTML('PIPELINE OUTPUT
')
pipeline_output = gr.HTML(
value=_initial_progress_html(),
)
# ── Footer ──
gr.HTML(
"""
"""
)
# ═══════════ Wire Callbacks ═══════════
analyze_btn.click(
fn=analyze_fn,
inputs=[image_input, diagnosis_input, context_input, modality_input],
outputs=[pipeline_output],
)
demo_btn_1.click(
fn=lambda: load_demo_fn("Case 1: Missed Pneumothorax"),
inputs=[],
outputs=[image_input, diagnosis_input, context_input, modality_input],
)
demo_btn_2.click(
fn=lambda: load_demo_fn("Case 2: Aortic Dissection"),
inputs=[],
outputs=[image_input, diagnosis_input, context_input, modality_input],
)
demo_btn_3.click(
fn=lambda: load_demo_fn("Case 3: Pulmonary Embolism"),
inputs=[],
outputs=[image_input, diagnosis_input, context_input, modality_input],
)
# Voice transcription — outputs to context field + status indicator
if ENABLE_MEDASR and transcribe_fn:
transcribe_btn.click(
fn=transcribe_fn,
inputs=[audio_input, context_input],
outputs=[context_input, voice_status],
)
return demo
def _initial_progress_html() -> str:
"""Static initial progress bar HTML."""
return _build_progress_html([], None, None, {})
def _build_progress_html(
completed: list[str],
active: str | None,
error: str | None,
agent_outputs: dict[str, str] | None = None,
) -> str:
"""Build pipeline output: progress bar + each agent's result inline.
Args:
agent_outputs: {agent_id: html_content} for completed agents.
"""
if agent_outputs is None:
agent_outputs = {}
agents = [
("diagnostician", "Diagnostician", "Independent image analysis"),
("bias_detector", "Bias Detector", "Cognitive bias identification"),
("devil_advocate", "Devil's Advocate", "Adversarial challenge"),
("consultant", "Consultant", "Consultation synthesis"),
]
n_done = len(completed)
pct = int(n_done / len(agents) * 100)
bar_color = "#ef4444" if error else "#3b82f6"
html = f"""
"""
for agent_id, name, desc in agents:
if agent_id == error:
cls = "step-error"
icon = "✗"
status = "Failed"
elif agent_id in completed:
cls = "step-done"
icon = "✓"
status = "Complete"
elif agent_id == active:
cls = "step-active"
icon = "⟳"
status = desc
else:
cls = "step-waiting"
icon = "○"
status = "Waiting"
content = agent_outputs.get(agent_id, "")
if content:
# Collapsible:
with header as
html += f"""
{content}
"""
else:
# No output yet — just show the header (not collapsible)
html += f"""
"""
html += "
"
return html