dek924 commited on
Commit
8a101a4
Β·
1 Parent(s): b9a55c8

feat: page split & add simulation configuration

Browse files
Files changed (1) hide show
  1. app.py +271 -91
app.py CHANGED
@@ -62,6 +62,48 @@ def _patient_choices() -> list[tuple[str, str]]:
62
  # ---------------------------------------------------------------------------
63
  # HTML helpers
64
  # ---------------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  def _row(label: str, val) -> str:
66
  val = str(val) if val not in (None, "") else "N/A"
67
  return (
@@ -156,15 +198,36 @@ CUSTOM_CSS = """
156
  'Segoe UI', sans-serif !important;
157
  }
158
 
159
- /* ── Compact radio pills ────────────────────────────────────────── */
160
- .compact-radio .wrap {
161
- gap: 6px !important;
162
  }
163
- .compact-radio label {
164
- padding: 6px 14px !important;
165
- border-radius: 8px !important;
166
- font-size: 13px !important;
167
- min-width: unset !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  }
169
 
170
  /* ── Tooltip descriptions ───────────────────────────────────────── */
@@ -175,11 +238,40 @@ CUSTOM_CSS = """
175
  line-height: 1.4;
176
  }
177
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  /* ── Mode cards ─────────────────────────────────────────────────── */
179
  .mode-card {
180
- border: 1px solid var(--border-color-primary);
181
- border-radius: 12px;
182
- padding: 20px;
 
 
 
 
 
 
 
 
183
  }
184
  """
185
 
@@ -215,21 +307,28 @@ def _make_tip_html(tips: dict, selected: str) -> str:
215
  # ---------------------------------------------------------------------------
216
  # Callbacks β€” Setup
217
  # ---------------------------------------------------------------------------
 
 
 
 
218
  def start_simulation(
219
  hadm_id: str,
220
  api_key_input: str,
221
  model: str,
222
  cefr: str,
223
  personality: str,
224
- recall: str,
225
- confusion: str,
226
  ):
 
 
227
  def _no_change():
228
  return (
229
  gr.update(), # patient_agent_state
230
  gr.update(), # sim_config_state
231
  gr.update(visible=True), # setup_section
232
  gr.update(visible=False), # mode_section
 
233
  )
234
 
235
  if not hadm_id:
@@ -277,10 +376,13 @@ def start_simulation(
277
  gr.Warning(f"Failed to initialize patient agent: {e}")
278
  return _no_change()
279
 
 
 
280
  sim_config = {
281
  "patient": patient,
282
  "api_key": api_key,
283
  "model": model,
 
284
  }
285
 
286
  return (
@@ -288,6 +390,7 @@ def start_simulation(
288
  sim_config,
289
  gr.update(visible=False), # hide setup_section
290
  gr.update(visible=True), # show mode_section
 
291
  )
292
 
293
 
@@ -299,6 +402,7 @@ def back_to_setup():
299
  gr.update(visible=False), # hide mode_section
300
  gr.update(visible=False), # hide chat_section
301
  gr.update(visible=False), # hide auto_section
 
302
  )
303
 
304
 
@@ -309,7 +413,7 @@ def start_manual(profile_mode: str, agent, sim_config: dict):
309
  if agent is None or sim_config is None:
310
  gr.Warning("Session expired. Please restart.")
311
  return (
312
- gr.update(), gr.update(),
313
  gr.update(visible=True), gr.update(visible=False), gr.update(visible=False),
314
  )
315
 
@@ -322,6 +426,7 @@ def start_manual(profile_mode: str, agent, sim_config: dict):
322
  )
323
  return (
324
  profile_html,
 
325
  [], # empty chat history
326
  gr.update(visible=False), # hide mode_section
327
  gr.update(visible=False), # hide auto_section
@@ -353,8 +458,9 @@ def back_to_setup_from_chat(agent):
353
  if agent is not None:
354
  agent.reset_history(verbose=False)
355
  return (
356
- gr.update(), # keep chat log
357
  "", # clear profile html
 
358
  None, # clear patient_agent_state
359
  None, # clear sim_config_state
360
  gr.update(visible=True), # show setup_section
@@ -374,6 +480,7 @@ _AUTO_OUTPUTS_UPDATE = (
374
  gr.update(visible=True), # mode_section
375
  gr.update(visible=False), # auto_section
376
  gr.update(visible=False), # chat_section
 
377
  )
378
 
379
 
@@ -400,18 +507,22 @@ def start_auto(agent, sim_config: dict):
400
  yield _AUTO_OUTPUTS_UPDATE
401
  return
402
 
403
- # Switch to auto_section immediately with empty chatbot
404
  chat_history = []
405
- yield chat_history, gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
 
406
 
407
  def _append(role: str, content: str):
408
  chat_history.append({"role": role, "content": content})
409
 
 
 
 
410
  try:
411
  # Doctor greets first
412
  doctor_greet = doctor.doctor_greet
413
  _append("user", doctor_greet)
414
- yield chat_history, gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
415
 
416
  for inference_idx in range(MAX_AUTO_INFERENCES):
417
  is_last = inference_idx == MAX_AUTO_INFERENCES - 1
@@ -423,7 +534,7 @@ def start_auto(agent, sim_config: dict):
423
  verbose=False,
424
  )
425
  _append("assistant", patient_response)
426
- yield chat_history, gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
427
 
428
  # Doctor responds
429
  doctor_input = chat_history[-1]["content"]
@@ -435,21 +546,45 @@ def start_auto(agent, sim_config: dict):
435
  verbose=False,
436
  )
437
  _append("user", doctor_response)
438
- yield chat_history, gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
439
 
440
  if detect_ed_termination(doctor_response):
441
  break
442
 
443
  except Exception as e:
444
  gr.Warning(f"Simulation error: {e}")
445
- yield chat_history, gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
 
447
 
448
  def back_to_setup_from_auto(agent):
449
  if agent is not None:
450
  agent.reset_history(verbose=False)
451
  return (
452
- gr.update(), # keep auto chatbot log
 
453
  None, # clear patient_agent_state
454
  None, # clear sim_config_state
455
  gr.update(visible=True), # show setup_section
@@ -466,25 +601,47 @@ with gr.Blocks(title="PatientSim", theme=gr.themes.Soft(), css=CUSTOM_CSS) as de
466
  patient_agent_state = gr.State(None)
467
  sim_config_state = gr.State(None)
468
 
469
- # ── Header ──────────────────────────────────────────────────────────────
470
- gr.Markdown(
471
- "# πŸ₯ PatientSim β€” ED Consultation Demo\n"
472
- "An interactive simulator for realistic doctor–patient interactions "
473
- "([NeurIPS 2025](https://openreview.net/forum?id=1THAjdP4QJ)). "
474
- "Configure a patient and persona below, then choose your simulation mode."
475
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
  # ── Setup section ────────────────────────────────────────────────────────
478
- with gr.Column(visible=True) as setup_section:
 
 
 
 
 
 
479
 
480
- with gr.Group():
481
- gr.Markdown("#### πŸ”‘ Connection")
482
  with gr.Row():
483
  api_key_input = gr.Textbox(
484
  label="API Key",
485
  placeholder="Google AI or OpenAI API key (or set via env var)",
486
  type="password",
487
- scale=2,
488
  )
489
  model_dd = gr.Dropdown(
490
  choices=BACKEND_MODELS,
@@ -493,25 +650,25 @@ with gr.Blocks(title="PatientSim", theme=gr.themes.Soft(), css=CUSTOM_CSS) as de
493
  scale=1,
494
  )
495
 
496
- with gr.Group():
497
- gr.Markdown("#### 🩺 Patient Case")
498
  patient_dd = gr.Dropdown(
499
  choices=_patient_choices(),
500
  label="Select a patient case",
501
  )
502
 
503
- with gr.Group():
504
- gr.Markdown("#### 🎭 Patient Persona")
505
  with gr.Row():
506
  with gr.Column(min_width=200):
507
- personality_radio = gr.Radio(
508
  choices=PERSONALITY_CHOICES,
509
  value="plain",
510
  label="Personality",
511
- elem_classes=["compact-radio"],
512
  )
513
  personality_tip = gr.HTML(
514
- value=_make_tip_html(PERSONALITY_TIPS, "plain")
 
515
  )
516
  with gr.Column(min_width=200):
517
  cefr_radio = gr.Radio(
@@ -521,35 +678,35 @@ with gr.Blocks(title="PatientSim", theme=gr.themes.Soft(), css=CUSTOM_CSS) as de
521
  elem_classes=["compact-radio"],
522
  )
523
  cefr_tip = gr.HTML(
524
- value=_make_tip_html(CEFR_TIPS, "C")
 
525
  )
526
  with gr.Row():
527
  with gr.Column(min_width=200):
528
- recall_radio = gr.Radio(
529
- choices=RECALL_CHOICES,
530
- value="high",
531
- label="Medical History Recall",
532
- elem_classes=["compact-radio"],
533
- )
534
- recall_tip = gr.HTML(
535
- value=_make_tip_html(RECALL_TIPS, "high")
536
  )
537
  with gr.Column(min_width=200):
538
- confusion_radio = gr.Radio(
539
- choices=CONFUSION_CHOICES,
540
- value="normal",
541
- label="Cognitive Confusion",
542
- elem_classes=["compact-radio"],
543
- )
544
- confusion_tip = gr.HTML(
545
- value=_make_tip_html(CONFUSION_TIPS, "normal")
546
  )
547
 
548
- start_btn = gr.Button("β–Ά Start Simulation", variant="primary", size="lg")
549
 
550
  # ── Mode selection section ───────────────────────────────────────────────
551
  with gr.Column(visible=False) as mode_section:
552
- gr.Markdown("## Choose Simulation Mode")
 
 
 
 
 
 
 
553
  with gr.Row(equal_height=True):
554
 
555
  # Auto simulation card
@@ -586,9 +743,16 @@ with gr.Blocks(title="PatientSim", theme=gr.themes.Soft(), css=CUSTOM_CSS) as de
586
  # ── Auto simulation section ──────────────────────────────────────────────
587
  with gr.Column(visible=False) as auto_section:
588
  with gr.Row():
 
589
  back_from_auto_btn = gr.Button("← Back to Setup", scale=0)
590
 
591
- gr.Markdown("### Auto Simulation Result")
 
 
 
 
 
 
592
  auto_chatbot = gr.Chatbot(
593
  label="Doctor–Patient Dialogue",
594
  height=560,
@@ -602,13 +766,21 @@ with gr.Blocks(title="PatientSim", theme=gr.themes.Soft(), css=CUSTOM_CSS) as de
602
 
603
  # ── Manual chat section ──────────────────────────────────────────────────
604
  with gr.Column(visible=False) as chat_section:
 
 
 
 
 
 
605
  with gr.Row():
 
606
  back_from_chat_btn = gr.Button("← Back to Setup", scale=1)
607
  reset_btn = gr.Button("β†Ί Reset Conversation", scale=1)
608
 
609
  with gr.Row(equal_height=False):
610
  with gr.Column(scale=1, min_width=280):
611
  profile_display = gr.HTML()
 
612
 
613
  with gr.Column(scale=2):
614
  chatbot = gr.Chatbot(
@@ -639,10 +811,16 @@ with gr.Blocks(title="PatientSim", theme=gr.themes.Soft(), css=CUSTOM_CSS) as de
639
 
640
  # ── Event wiring ─────────────────────────────────────────────────────────
641
 
 
 
 
 
 
 
642
  # Tooltip updates
643
- personality_radio.change(
644
  fn=lambda v: _make_tip_html(PERSONALITY_TIPS, v),
645
- inputs=[personality_radio],
646
  outputs=[personality_tip],
647
  )
648
  cefr_radio.change(
@@ -650,64 +828,66 @@ with gr.Blocks(title="PatientSim", theme=gr.themes.Soft(), css=CUSTOM_CSS) as de
650
  inputs=[cefr_radio],
651
  outputs=[cefr_tip],
652
  )
653
- recall_radio.change(
654
- fn=lambda v: _make_tip_html(RECALL_TIPS, v),
655
- inputs=[recall_radio],
656
- outputs=[recall_tip],
657
- )
658
- confusion_radio.change(
659
- fn=lambda v: _make_tip_html(CONFUSION_TIPS, v),
660
- inputs=[confusion_radio],
661
- outputs=[confusion_tip],
662
- )
663
 
664
  # Start simulation β†’ mode selection
665
  start_btn.click(
666
  fn=start_simulation,
667
- inputs=[patient_dd, api_key_input, model_dd, cefr_radio, personality_radio, recall_radio, confusion_radio],
668
- outputs=[patient_agent_state, sim_config_state, setup_section, mode_section],
669
  )
670
 
671
  # Back to setup from mode selection
672
  back_from_mode_btn.click(
673
  fn=back_to_setup,
674
- outputs=[patient_agent_state, sim_config_state, setup_section, mode_section, chat_section, auto_section],
675
  )
676
 
677
  # Auto simulation
678
- auto_btn.click(
679
  fn=start_auto,
680
  inputs=[patient_agent_state, sim_config_state],
681
- outputs=[auto_chatbot, mode_section, auto_section, chat_section],
 
 
 
 
 
 
682
  )
683
  back_from_auto_btn.click(
684
  fn=back_to_setup_from_auto,
685
- outputs=[auto_chatbot, patient_agent_state, sim_config_state, setup_section, mode_section, auto_section, chat_section],
 
 
686
  )
687
 
688
  # Manual practice
689
  manual_btn.click(
690
  fn=start_manual,
691
  inputs=[profile_mode_radio, patient_agent_state, sim_config_state],
692
- outputs=[profile_display, chatbot, mode_section, auto_section, chat_section],
693
  )
694
- back_from_chat_btn.click(
695
- fn=back_to_setup_from_chat,
696
- inputs=[patient_agent_state],
697
- outputs=[chatbot, profile_display, patient_agent_state, sim_config_state, setup_section, mode_section, auto_section, chat_section],
698
- )
699
-
700
- # Chat interactions
701
- _chat_outputs = [chatbot, msg_box]
702
- send_btn.click(
703
  fn=chat,
704
  inputs=[msg_box, chatbot, patient_agent_state],
705
- outputs=_chat_outputs,
706
  )
707
- msg_box.submit(
708
  fn=chat,
709
  inputs=[msg_box, chatbot, patient_agent_state],
710
- outputs=_chat_outputs,
 
 
 
 
 
 
 
 
 
 
 
 
711
  )
712
  reset_btn.click(
713
  fn=reset_chat,
 
62
  # ---------------------------------------------------------------------------
63
  # HTML helpers
64
  # ---------------------------------------------------------------------------
65
+ def build_recap_html(hadm_id: str, model: str, cefr: str, personality: str, recall: str, confusion: str) -> str:
66
+ patient = PATIENT_DICT.get(hadm_id, {})
67
+ patient_label = (
68
+ f"Age {patient.get('age')} Β· {patient.get('gender')} Β· {patient.get('diagnosis', 'Unknown')}"
69
+ if patient else "β€”"
70
+ )
71
+ personality_label = next((l for l, v in PERSONALITY_CHOICES if v == personality), personality)
72
+ cefr_label = next((l for l, v in CEFR_CHOICES if v == cefr), cefr)
73
+ recall_label = next((l for l, v in RECALL_CHOICES if v == recall), recall)
74
+ confusion_label = next((l for l, v in CONFUSION_CHOICES if v == confusion), confusion)
75
+
76
+ def _card(label, value):
77
+ return (
78
+ "<div style='padding:10px 14px;background:var(--background-fill-primary,#fff);"
79
+ "border:1px solid var(--border-color-primary);border-radius:8px'>"
80
+ f"<div style='font-size:11px;font-weight:600;letter-spacing:0.06em;"
81
+ f"text-transform:uppercase;color:var(--body-text-color-subdued);margin-bottom:4px'>{label}</div>"
82
+ f"<div style='font-size:13px;font-weight:500;line-height:1.4'>{value}</div>"
83
+ "</div>"
84
+ )
85
+
86
+ items = [
87
+ _card("Patient", patient_label),
88
+ _card("Personality", personality_label),
89
+ _card("Model", model),
90
+ _card("Language Proficiency", cefr_label),
91
+ _card("Medical History Recall", recall_label),
92
+ _card("Cognitive Confusion", confusion_label),
93
+ ]
94
+
95
+ grid_items = "".join(items)
96
+ return (
97
+ "<div style='background:var(--color-accent-soft,#f0f7ff);"
98
+ "border:1px solid var(--border-color-primary);border-radius:10px;"
99
+ "padding:16px 20px;margin-bottom:8px'>"
100
+ "<div style='font-weight:600;font-size:14px;margin-bottom:12px'>πŸ“‹ Simulation Configuration</div>"
101
+ f"<div style='display:grid;grid-template-columns:1fr 1fr;gap:8px'>{grid_items}</div>"
102
+ "</div>"
103
+ )
104
+
105
+
106
+
107
  def _row(label: str, val) -> str:
108
  val = str(val) if val not in (None, "") else "N/A"
109
  return (
 
198
  'Segoe UI', sans-serif !important;
199
  }
200
 
201
+ /* ── Page background ────────────────────────────────────────────── */
202
+ body, .gradio-container, .main, footer {
203
+ background-color: #f3f4f6 !important;
204
  }
205
+
206
+ /* ── White shadow cards for form sections ───────────────────────── */
207
+ .form-card {
208
+ background: #ffffff !important;
209
+ border-radius: 14px !important;
210
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 0 0 1px rgba(0,0,0,0.05) !important;
211
+ border: none !important;
212
+ padding: 20px 24px !important;
213
+ }
214
+ .form-card > .gap,
215
+ .form-card > div {
216
+ background: transparent !important;
217
+ border: none !important;
218
+ gap: 12px !important;
219
+ }
220
+
221
+ /* ── Card section title (replaces gray header bar) ──────────────── */
222
+ .card-title {
223
+ font-size: 14px;
224
+ font-weight: 600;
225
+ color: #374151;
226
+ padding-bottom: 10px;
227
+ margin-bottom: 2px;
228
+ border-bottom: 1px solid #e5e7eb;
229
+ display: block;
230
+ text-align: center;
231
  }
232
 
233
  /* ── Tooltip descriptions ───────────────────────────────────────── */
 
238
  line-height: 1.4;
239
  }
240
 
241
+ /* ── Transparent HTML tip containers ────────────────────────────── */
242
+ .tip-html,
243
+ .tip-html > div,
244
+ .tip-html > .prose {
245
+ background: transparent !important;
246
+ border: none !important;
247
+ padding: 0 !important;
248
+ margin: 0 !important;
249
+ }
250
+
251
+ /* ── CEFR radio β€” clean pill style ─────────────────────────────── */
252
+ .compact-radio .wrap {
253
+ gap: 6px !important;
254
+ }
255
+ .compact-radio label {
256
+ padding: 5px 14px !important;
257
+ border-radius: 8px !important;
258
+ font-size: 13px !important;
259
+ min-width: unset !important;
260
+ }
261
+
262
  /* ── Mode cards ─────────────────────────────────────────────────── */
263
  .mode-card {
264
+ background: #ffffff !important;
265
+ border: 1px solid #e5e7eb !important;
266
+ border-radius: 14px !important;
267
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06) !important;
268
+ padding: 20px !important;
269
+ }
270
+
271
+ /* ── Start simulation button shadow ─────────────────────────────── */
272
+ #start-btn > button {
273
+ box-shadow: 0 4px 14px rgba(59, 130, 246, 0.30) !important;
274
+ font-size: 15px !important;
275
  }
276
  """
277
 
 
307
  # ---------------------------------------------------------------------------
308
  # Callbacks β€” Setup
309
  # ---------------------------------------------------------------------------
310
+ def go_to_setup():
311
+ return gr.update(visible=False), gr.update(visible=True)
312
+
313
+
314
  def start_simulation(
315
  hadm_id: str,
316
  api_key_input: str,
317
  model: str,
318
  cefr: str,
319
  personality: str,
320
+ recall_high: bool,
321
+ confusion_high: bool,
322
  ):
323
+ recall = "high" if recall_high else "low"
324
+ confusion = "high" if confusion_high else "normal"
325
  def _no_change():
326
  return (
327
  gr.update(), # patient_agent_state
328
  gr.update(), # sim_config_state
329
  gr.update(visible=True), # setup_section
330
  gr.update(visible=False), # mode_section
331
+ gr.update(value=""), # recap_display
332
  )
333
 
334
  if not hadm_id:
 
376
  gr.Warning(f"Failed to initialize patient agent: {e}")
377
  return _no_change()
378
 
379
+ recap = build_recap_html(hadm_id, model, cefr, personality, recall, confusion)
380
+
381
  sim_config = {
382
  "patient": patient,
383
  "api_key": api_key,
384
  "model": model,
385
+ "recap_html": recap,
386
  }
387
 
388
  return (
 
390
  sim_config,
391
  gr.update(visible=False), # hide setup_section
392
  gr.update(visible=True), # show mode_section
393
+ gr.update(value=recap), # recap_display
394
  )
395
 
396
 
 
402
  gr.update(visible=False), # hide mode_section
403
  gr.update(visible=False), # hide chat_section
404
  gr.update(visible=False), # hide auto_section
405
+ gr.update(value=""), # clear recap_display
406
  )
407
 
408
 
 
413
  if agent is None or sim_config is None:
414
  gr.Warning("Session expired. Please restart.")
415
  return (
416
+ gr.update(), gr.update(), gr.update(),
417
  gr.update(visible=True), gr.update(visible=False), gr.update(visible=False),
418
  )
419
 
 
426
  )
427
  return (
428
  profile_html,
429
+ sim_config.get("recap_html", ""), # show recap in left panel
430
  [], # empty chat history
431
  gr.update(visible=False), # hide mode_section
432
  gr.update(visible=False), # hide auto_section
 
458
  if agent is not None:
459
  agent.reset_history(verbose=False)
460
  return (
461
+ [], # clear chat log
462
  "", # clear profile html
463
+ "", # clear chat recap
464
  None, # clear patient_agent_state
465
  None, # clear sim_config_state
466
  gr.update(visible=True), # show setup_section
 
480
  gr.update(visible=True), # mode_section
481
  gr.update(visible=False), # auto_section
482
  gr.update(visible=False), # chat_section
483
+ gr.update(), # auto_recap
484
  )
485
 
486
 
 
507
  yield _AUTO_OUTPUTS_UPDATE
508
  return
509
 
510
+ # Switch to auto_section immediately with empty chatbot; set recap once
511
  chat_history = []
512
+ recap_html = sim_config.get("recap_html", "")
513
+ yield chat_history, gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), recap_html
514
 
515
  def _append(role: str, content: str):
516
  chat_history.append({"role": role, "content": content})
517
 
518
+ def _yield():
519
+ return chat_history, gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), gr.update()
520
+
521
  try:
522
  # Doctor greets first
523
  doctor_greet = doctor.doctor_greet
524
  _append("user", doctor_greet)
525
+ yield _yield()
526
 
527
  for inference_idx in range(MAX_AUTO_INFERENCES):
528
  is_last = inference_idx == MAX_AUTO_INFERENCES - 1
 
534
  verbose=False,
535
  )
536
  _append("assistant", patient_response)
537
+ yield _yield()
538
 
539
  # Doctor responds
540
  doctor_input = chat_history[-1]["content"]
 
546
  verbose=False,
547
  )
548
  _append("user", doctor_response)
549
+ yield _yield()
550
 
551
  if detect_ed_termination(doctor_response):
552
  break
553
 
554
  except Exception as e:
555
  gr.Warning(f"Simulation error: {e}")
556
+ yield _yield()
557
+
558
+
559
+ def back_to_mode_from_auto(agent):
560
+ if agent is not None:
561
+ agent.reset_history(verbose=False)
562
+ return (
563
+ [], # clear auto chatbot log
564
+ "", # clear auto recap
565
+ gr.update(visible=True), # show mode_section
566
+ gr.update(visible=False), # hide auto_section
567
+ )
568
+
569
+
570
+ def back_to_mode_from_chat(agent):
571
+ if agent is not None:
572
+ agent.reset_history(verbose=False)
573
+ return (
574
+ [], # clear chat log
575
+ "", # clear profile html
576
+ "", # clear chat recap
577
+ gr.update(visible=True), # show mode_section
578
+ gr.update(visible=False), # hide chat_section
579
+ )
580
 
581
 
582
  def back_to_setup_from_auto(agent):
583
  if agent is not None:
584
  agent.reset_history(verbose=False)
585
  return (
586
+ [], # clear auto chatbot log
587
+ "", # clear auto recap
588
  None, # clear patient_agent_state
589
  None, # clear sim_config_state
590
  gr.update(visible=True), # show setup_section
 
601
  patient_agent_state = gr.State(None)
602
  sim_config_state = gr.State(None)
603
 
604
+ # ── Intro section ────────────────────────────────────────────────────────
605
+ with gr.Column(visible=True) as intro_section:
606
+ gr.Markdown(
607
+ "# πŸ₯ PatientSim β€” ED Consultation Demo\n\n"
608
+ "**PatientSim** is a research framework for simulating realistic emergency department "
609
+ "doctor–patient interactions, presented at "
610
+ "[NeurIPS 2025 (Datasets & Benchmarks)](https://openreview.net/forum?id=1THAjdP4QJ).\n\n"
611
+ "Large language models act as patients with **controllable personas** β€” you choose their "
612
+ "personality, language proficiency, medical history recall, and cognitive state β€” producing "
613
+ "diverse and realistic consultation scenarios for training and evaluation.\n\n"
614
+ "---\n\n"
615
+ "### What you can do here\n\n"
616
+ "| Mode | Description |\n"
617
+ "|------|-------------|\n"
618
+ "| πŸ€– **Auto Simulation** | Watch an AI doctor conduct a full consultation with the simulated patient and arrive at a differential diagnosis β€” no input required. |\n"
619
+ "| 🩺 **Practice Mode** | You play the doctor. Consult the simulated patient, gather medical history, and work toward a diagnosis yourself. |\n\n"
620
+ "---\n\n"
621
+ "### How to get started\n"
622
+ "1. Click **Get Started** below.\n"
623
+ "2. Enter your API key and select a patient case and persona.\n"
624
+ "3. Choose a simulation mode and begin."
625
+ )
626
+ get_started_btn = gr.Button("Get Started β†’", variant="primary", size="lg")
627
 
628
  # ── Setup section ────────────────────────────────────────────────────────
629
+ with gr.Column(visible=False) as setup_section:
630
+ gr.Markdown(
631
+ "# πŸ₯ PatientSim β€” Setup\n\n"
632
+ "Configure your session below. Select the patient case you want to simulate, "
633
+ "choose the AI model to power the patient, and define the patient's persona across "
634
+ "four behavioral axes. When ready, click **Start Simulation**."
635
+ )
636
 
637
+ with gr.Group(elem_classes=["form-card"]):
638
+ gr.HTML("<span class='card-title'>πŸ”‘ Connection</span>")
639
  with gr.Row():
640
  api_key_input = gr.Textbox(
641
  label="API Key",
642
  placeholder="Google AI or OpenAI API key (or set via env var)",
643
  type="password",
644
+ scale=3,
645
  )
646
  model_dd = gr.Dropdown(
647
  choices=BACKEND_MODELS,
 
650
  scale=1,
651
  )
652
 
653
+ with gr.Group(elem_classes=["form-card"]):
654
+ gr.HTML("<span class='card-title'>🩺 Patient Case</span>")
655
  patient_dd = gr.Dropdown(
656
  choices=_patient_choices(),
657
  label="Select a patient case",
658
  )
659
 
660
+ with gr.Group(elem_classes=["form-card"]):
661
+ gr.HTML("<span class='card-title'>🎭 Patient Persona</span>")
662
  with gr.Row():
663
  with gr.Column(min_width=200):
664
+ personality_dd = gr.Dropdown(
665
  choices=PERSONALITY_CHOICES,
666
  value="plain",
667
  label="Personality",
 
668
  )
669
  personality_tip = gr.HTML(
670
+ value=_make_tip_html(PERSONALITY_TIPS, "plain"),
671
+ elem_classes=["tip-html"],
672
  )
673
  with gr.Column(min_width=200):
674
  cefr_radio = gr.Radio(
 
678
  elem_classes=["compact-radio"],
679
  )
680
  cefr_tip = gr.HTML(
681
+ value=_make_tip_html(CEFR_TIPS, "C"),
682
+ elem_classes=["tip-html"],
683
  )
684
  with gr.Row():
685
  with gr.Column(min_width=200):
686
+ recall_toggle = gr.Checkbox(
687
+ label="High Medical History Recall",
688
+ value=True,
689
+ info="Enabled: patient usually recalls events accurately. Disabled: often forgets key medical events.",
 
 
 
 
690
  )
691
  with gr.Column(min_width=200):
692
+ confusion_toggle = gr.Checkbox(
693
+ label="High Cognitive Confusion",
694
+ value=False,
695
+ info="Enabled: patient is highly dazed and disoriented. Disabled: clear mental status.",
 
 
 
 
696
  )
697
 
698
+ start_btn = gr.Button("β–Ά Start Simulation", variant="primary", size="lg", elem_id="start-btn")
699
 
700
  # ── Mode selection section ───────────────────────────────────────────────
701
  with gr.Column(visible=False) as mode_section:
702
+ gr.Markdown(
703
+ "## Choose Simulation Mode\n\n"
704
+ "Your patient agent is ready. Review your configuration below, then pick a mode. "
705
+ "**Auto Simulation** runs a fully automated consultation so you can observe the "
706
+ "system in action. **Practice Mode** puts you in the doctor's seat for hands-on "
707
+ "training."
708
+ )
709
+ recap_display = gr.HTML()
710
  with gr.Row(equal_height=True):
711
 
712
  # Auto simulation card
 
743
  # ── Auto simulation section ──────────────────────────────────────────────
744
  with gr.Column(visible=False) as auto_section:
745
  with gr.Row():
746
+ back_from_auto_to_mode_btn = gr.Button("← Mode Selection", scale=0)
747
  back_from_auto_btn = gr.Button("← Back to Setup", scale=0)
748
 
749
+ gr.Markdown(
750
+ "### πŸ€– Auto Simulation\n\n"
751
+ "An AI doctor is conducting the consultation. The doctor will ask questions, "
752
+ "gather medical history, and conclude with a differential diagnosis. "
753
+ "**Doctor** messages appear on the left; **Patient** responses on the right."
754
+ )
755
+ auto_recap = gr.HTML()
756
  auto_chatbot = gr.Chatbot(
757
  label="Doctor–Patient Dialogue",
758
  height=560,
 
766
 
767
  # ── Manual chat section ──────────────────────────────────────────────────
768
  with gr.Column(visible=False) as chat_section:
769
+ gr.Markdown(
770
+ "### 🩺 Practice Mode\n\n"
771
+ "You are the attending physician. Type your questions and responses in the box "
772
+ "below to consult the simulated patient. Use the patient profile panel on the "
773
+ "left for reference. Try to gather sufficient history to reach a differential diagnosis."
774
+ )
775
  with gr.Row():
776
+ back_from_chat_to_mode_btn = gr.Button("← Mode Selection", scale=1)
777
  back_from_chat_btn = gr.Button("← Back to Setup", scale=1)
778
  reset_btn = gr.Button("β†Ί Reset Conversation", scale=1)
779
 
780
  with gr.Row(equal_height=False):
781
  with gr.Column(scale=1, min_width=280):
782
  profile_display = gr.HTML()
783
+ chat_recap = gr.HTML()
784
 
785
  with gr.Column(scale=2):
786
  chatbot = gr.Chatbot(
 
811
 
812
  # ── Event wiring ─────────────────────────────────────────────────────────
813
 
814
+ # Intro β†’ Setup
815
+ get_started_btn.click(
816
+ fn=go_to_setup,
817
+ outputs=[intro_section, setup_section],
818
+ )
819
+
820
  # Tooltip updates
821
+ personality_dd.change(
822
  fn=lambda v: _make_tip_html(PERSONALITY_TIPS, v),
823
+ inputs=[personality_dd],
824
  outputs=[personality_tip],
825
  )
826
  cefr_radio.change(
 
828
  inputs=[cefr_radio],
829
  outputs=[cefr_tip],
830
  )
 
 
 
 
 
 
 
 
 
 
831
 
832
  # Start simulation β†’ mode selection
833
  start_btn.click(
834
  fn=start_simulation,
835
+ inputs=[patient_dd, api_key_input, model_dd, cefr_radio, personality_dd, recall_toggle, confusion_toggle],
836
+ outputs=[patient_agent_state, sim_config_state, setup_section, mode_section, recap_display],
837
  )
838
 
839
  # Back to setup from mode selection
840
  back_from_mode_btn.click(
841
  fn=back_to_setup,
842
+ outputs=[patient_agent_state, sim_config_state, setup_section, mode_section, chat_section, auto_section, recap_display],
843
  )
844
 
845
  # Auto simulation
846
+ auto_event = auto_btn.click(
847
  fn=start_auto,
848
  inputs=[patient_agent_state, sim_config_state],
849
+ outputs=[auto_chatbot, mode_section, auto_section, chat_section, auto_recap],
850
+ )
851
+ back_from_auto_to_mode_btn.click(
852
+ fn=back_to_mode_from_auto,
853
+ inputs=[patient_agent_state],
854
+ outputs=[auto_chatbot, auto_recap, mode_section, auto_section],
855
+ cancels=[auto_event],
856
  )
857
  back_from_auto_btn.click(
858
  fn=back_to_setup_from_auto,
859
+ inputs=[patient_agent_state],
860
+ outputs=[auto_chatbot, auto_recap, patient_agent_state, sim_config_state, setup_section, mode_section, auto_section, chat_section],
861
+ cancels=[auto_event],
862
  )
863
 
864
  # Manual practice
865
  manual_btn.click(
866
  fn=start_manual,
867
  inputs=[profile_mode_radio, patient_agent_state, sim_config_state],
868
+ outputs=[profile_display, chat_recap, chatbot, mode_section, auto_section, chat_section],
869
  )
870
+ chat_event_send = send_btn.click(
 
 
 
 
 
 
 
 
871
  fn=chat,
872
  inputs=[msg_box, chatbot, patient_agent_state],
873
+ outputs=[chatbot, msg_box],
874
  )
875
+ chat_event_submit = msg_box.submit(
876
  fn=chat,
877
  inputs=[msg_box, chatbot, patient_agent_state],
878
+ outputs=[chatbot, msg_box],
879
+ )
880
+ back_from_chat_to_mode_btn.click(
881
+ fn=back_to_mode_from_chat,
882
+ inputs=[patient_agent_state],
883
+ outputs=[chatbot, profile_display, chat_recap, mode_section, chat_section],
884
+ cancels=[chat_event_send, chat_event_submit],
885
+ )
886
+ back_from_chat_btn.click(
887
+ fn=back_to_setup_from_chat,
888
+ inputs=[patient_agent_state],
889
+ outputs=[chatbot, profile_display, chat_recap, patient_agent_state, sim_config_state, setup_section, mode_section, auto_section, chat_section],
890
+ cancels=[chat_event_send, chat_event_submit],
891
  )
892
  reset_btn.click(
893
  fn=reset_chat,