File size: 12,736 Bytes
c0fff99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336da73
c0fff99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
"""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(
            """
            <div class="hero-banner">
                <div class="hero-badge">MedGemma Impact Challenge</div>
                <h1>Diagnostic Devil's Advocate</h1>
                <p class="hero-sub">AI-Powered Cognitive Debiasing for Medical Image Interpretation</p>
                <p class="hero-desc">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.</p>
                <div class="hero-models">
                    <span class="model-chip">MedGemma 1.5</span>
                    <span class="model-chip">MedSigLIP</span>
                    <span class="model-chip">LangGraph</span>
                    <span class="model-chip">MedASR</span>
                </div>
            </div>
            """
        )

        # ── Demo Cases Row (3 clickable cards) ──
        gr.HTML('<div class="section-label">SELECT A DEMO CASE</div>')

        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("""
        <div class="case-cards-overlay">
            <div class="case-card case-card-pneumo" onclick="document.querySelector('#case-btn-1').click()">
                <div class="card-top">
                    <span class="case-icon">🫁</span>
                    <span class="case-tag tag-blue">TRAUMA</span>
                </div>
                <div class="case-title">Missed Pneumothorax</div>
                <div class="case-meta">32-year-old Male</div>
                <div class="case-desc">Motorcycle collision · Left chest pain · HR 104 · SpO₂ 96%</div>
                <div class="case-misdiag">
                    <span class="misdiag-label">Initial Dx:</span>
                    <span class="misdiag-value">Rib contusion</span>
                </div>
            </div>
            <div class="case-card case-card-aorta" onclick="document.querySelector('#case-btn-2').click()">
                <div class="card-top">
                    <span class="case-icon">🫀</span>
                    <span class="case-tag tag-red">VASCULAR</span>
                </div>
                <div class="case-title">Aortic Dissection</div>
                <div class="case-meta">58-year-old Male</div>
                <div class="case-desc">Sudden chest→back pain · BP asymmetry 32mmHg · D-dimer 4850</div>
                <div class="case-misdiag">
                    <span class="misdiag-label">Initial Dx:</span>
                    <span class="misdiag-value">GERD / Reflux</span>
                </div>
            </div>
            <div class="case-card case-card-pe" onclick="document.querySelector('#case-btn-3').click()">
                <div class="card-top">
                    <span class="case-icon">🩸</span>
                    <span class="case-tag tag-purple">POSTPARTUM</span>
                </div>
                <div class="case-title">Pulmonary Embolism</div>
                <div class="case-meta">29-year-old Female</div>
                <div class="case-desc">5 days post C-section · HR 118 · SpO₂ 91% · pO₂ 68</div>
                <div class="case-misdiag">
                    <span class="misdiag-label">Initial Dx:</span>
                    <span class="misdiag-value">Postpartum anxiety</span>
                </div>
            </div>
        </div>
        """)

        # ── Main Content: Input + Output ──
        with gr.Row(equal_height=False):

            # ═══════════ Left Column: Input ═══════════
            with gr.Column(scale=4, min_width=340):
                gr.HTML('<div class="section-label">CLINICAL INPUT</div>')

                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("""
                    <div class="voice-section">
                        <div class="voice-header">
                            <span class="voice-icon">🎙️</span>
                            <span class="voice-title">Voice Input</span>
                            <span class="voice-badge">MedASR</span>
                        </div>
                        <div class="voice-hint">Record clinical context with your microphone.
                            Text will be appended to the context field above.</div>
                    </div>
                    """)
                    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='<div class="voice-status voice-idle">Ready to record</div>',
                            )
                else:
                    gr.HTML(
                        '<div class="voice-status voice-idle">Voice input disabled (MedASR)</div>'
                    )

                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('<div class="section-label">PIPELINE OUTPUT</div>')
                pipeline_output = gr.HTML(
                    value=_initial_progress_html(),
                )

        # ── Footer ──
        gr.HTML(
            """
            <div class="footer-text">
                <span>Built with</span>
                <span class="footer-chip">MedGemma</span>
                <span class="footer-chip">MedSigLIP</span>
                <span class="footer-chip">LangGraph</span>
                <span class="footer-chip">Gradio</span>
                <span class="footer-sep">|</span>
                <span>MedGemma Impact Challenge 2025</span>
                <span class="footer-sep">|</span>
                <span>Research & educational use only</span>
            </div>
            """
        )

        # ═══════════ 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"""
    <div class="progress-container">
        <div class="progress-bar-track">
            <div class="progress-bar-fill" style="width:{pct}%;background:{bar_color};">
                {pct}%
            </div>
        </div>
        <div class="pipeline-agents">
    """

    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: <details open> with header as <summary>
            html += f"""
        <details class="agent-block {cls}" open>
            <summary class="agent-header">
                <span class="step-icon">{icon}</span>
                <span class="step-name">{name}</span>
                <span class="step-status">{status}</span>
            </summary>
            <div class="agent-output">{content}</div>
        </details>"""
        else:
            # No output yet — just show the header (not collapsible)
            html += f"""
        <div class="agent-block {cls}">
            <div class="agent-header">
                <span class="step-icon">{icon}</span>
                <span class="step-name">{name}</span>
                <span class="step-status">{status}</span>
            </div>
        </div>"""

    html += "</div></div>"
    return html