"""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("""
🎙️ Voice Input MedASR
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"""
{pct}%
""" 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"""
{icon} {name} {status}
{content}
""" else: # No output yet — just show the header (not collapsible) html += f"""
{icon} {name} {status}
""" html += "
" return html