StrongCapybara commited on
Commit
f503df8
Β·
1 Parent(s): 140fc55

feat(ui): implement modern clinical dashboard interface using custom gradio blocks

Browse files
Files changed (2) hide show
  1. server/app.py +3 -0
  2. server/ui.py +229 -0
server/app.py CHANGED
@@ -33,6 +33,8 @@ except (ImportError, ModuleNotFoundError):
33
  from server.triage_flow_environment import TriageEnvironment
34
 
35
 
 
 
36
  # Create the app with web interface
37
  app = create_app(
38
  TriageEnvironment,
@@ -40,6 +42,7 @@ app = create_app(
40
  TriageObservation,
41
  env_name="triage_flow",
42
  max_concurrent_envs=1,
 
43
  )
44
 
45
  from fastapi.responses import RedirectResponse
 
33
  from server.triage_flow_environment import TriageEnvironment
34
 
35
 
36
+ from .ui import build_triage_ui
37
+
38
  # Create the app with web interface
39
  app = create_app(
40
  TriageEnvironment,
 
42
  TriageObservation,
43
  env_name="triage_flow",
44
  max_concurrent_envs=1,
45
+ gradio_builder=build_triage_ui
46
  )
47
 
48
  from fastapi.responses import RedirectResponse
server/ui.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import json
3
+
4
+ CUSTOM_CSS = """
5
+ .metric-card {
6
+ background: rgba(255, 255, 255, 0.85);
7
+ backdrop-filter: blur(10px);
8
+ border: 1px solid rgba(226, 232, 240, 0.8);
9
+ border-radius: 12px;
10
+ padding: 1.5rem;
11
+ margin-bottom: 1rem;
12
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
13
+ }
14
+ .dark .metric-card {
15
+ background: rgba(30, 41, 59, 0.85);
16
+ border-color: rgba(51, 65, 85, 0.8);
17
+ }
18
+ .status-pill {
19
+ display: inline-block;
20
+ padding: 0.25rem 0.75rem;
21
+ border-radius: 9999px;
22
+ font-size: 0.875rem;
23
+ font-weight: 600;
24
+ }
25
+ .status-pill.success { background: #dcfce7; color: #166534; }
26
+ .status-pill.error { background: #fee2e2; color: #991b1b; }
27
+ .status-pill.info { background: #dbeafe; color: #1e3a8a; }
28
+ .dark .status-pill.success { background: #14532d; color: #bbf7d0; }
29
+ .dark .status-pill.error { background: #7f1d1d; color: #fecaca; }
30
+ .dark .status-pill.info { background: #1e3a8a; color: #bfdbfe; }
31
+ """
32
+
33
+ def extract_patient_display(obs, custom_error=None):
34
+ pt = obs.get("current_patient", {})
35
+ if pt is None:
36
+ pt = {}
37
+
38
+ pt_id = pt.get("patient_id", "N/A")
39
+ age = pt.get("age", "N/A")
40
+ pt_display = f"**ID:** {pt_id} | **Age:** {age}"
41
+
42
+ # Missing fields indicator
43
+ missing = obs.get("missing_fields", [])
44
+ if missing:
45
+ cc_disp = f"{pt.get('chief_complaint', 'N/A')} *({len(missing)} fields missing)*"
46
+ else:
47
+ cc_disp = pt.get("chief_complaint", "N/A")
48
+
49
+ symptoms = pt.get("symptoms", [])
50
+ symp_disp = ", ".join(symptoms) if symptoms else "None"
51
+
52
+ vitals = pt.get("vitals", {})
53
+ v_disp = ", ".join([f"**{k}**: {v}" for k, v in vitals.items()]) if vitals else "None"
54
+
55
+ hist = pt.get("history", [])
56
+ meds = pt.get("medications", [])
57
+ h_disp = f"**History**: {', '.join(hist) if hist else 'None'}\n\n**Meds**: {', '.join(meds) if meds else 'None'}"
58
+
59
+ q_len = obs.get("queue_length", 0)
60
+ q_pos = obs.get("queue_position", 0)
61
+ if not pt_id or pt_id == "N/A":
62
+ q_display = "πŸ“­ Empty Queue β€” No patients waiting."
63
+ else:
64
+ q_display = f"πŸ‘₯ **Queue Length:** {q_len} | **Viewing Position:** {q_pos + 1}"
65
+
66
+ fb = obs.get("previous_action_feedback", "")
67
+ if custom_error:
68
+ fb_disp = f"<span class='status-pill error'>❌ Error</span> {custom_error}"
69
+ elif fb:
70
+ if "Correct" in fb or "successful" in fb.lower():
71
+ fb_disp = f"<span class='status-pill success'>βœ… Applied</span> {fb}"
72
+ elif "Incorrect" in fb or "Penalty" in fb:
73
+ fb_disp = f"<span class='status-pill error'>⚠️ Penalty</span> {fb}"
74
+ else:
75
+ fb_disp = f"<span class='status-pill info'>ℹ️ Update</span> {fb}"
76
+ else:
77
+ fb_disp = "No recent interactions."
78
+
79
+ return pt_display, cc_disp, symp_disp, v_disp, h_disp, q_display, fb_disp
80
+
81
+ def build_triage_ui(web_manager, action_fields, metadata, is_chat_env, title, quick_start_md):
82
+ with gr.Blocks(css=CUSTOM_CSS) as blocks:
83
+ gr.Markdown(f"# πŸ₯ {title} β€” Clinical Dashboard")
84
+ gr.Markdown("Interactive medical triage environment. Monitor physiological vitals, issue requests for missing lab data, and assign clinical urgency strictly adhering to protocol.")
85
+
86
+ current_state = gr.State({})
87
+
88
+ with gr.Row():
89
+ task_dropdown = gr.Dropdown(
90
+ choices=["basic-triage", "incomplete-records-triage", "mass-casualty-triage"],
91
+ label="Environment Scenario",
92
+ value="basic-triage",
93
+ interactive=True
94
+ )
95
+ reset_btn = gr.Button("πŸ”„ Initialize Scenario", variant="primary")
96
+
97
+ with gr.Row():
98
+ with gr.Column(scale=2):
99
+ with gr.Group(elem_classes="metric-card"):
100
+ gr.Markdown("### πŸ«€ Active Patient Vitals & Presentation")
101
+ patient_id_md = gr.Markdown("Loading...")
102
+
103
+ with gr.Row():
104
+ with gr.Column():
105
+ gr.Markdown("**Chief Complaint:**")
106
+ cc_md = gr.Markdown("β€”")
107
+ with gr.Column():
108
+ gr.Markdown("**Symptoms:**")
109
+ symp_md = gr.Markdown("β€”")
110
+
111
+ gr.Markdown("---")
112
+ gr.Markdown("**Latest Vitals:**")
113
+ vitals_md = gr.Markdown("β€”")
114
+
115
+ gr.Markdown("---")
116
+ history_md = gr.Markdown("β€”")
117
+
118
+ with gr.Group(elem_classes="metric-card"):
119
+ gr.Markdown("### πŸ“Š Admin Queue Status")
120
+ queue_md = gr.Markdown("β€”")
121
+ feedback_md = gr.Markdown("β€”")
122
+
123
+ with gr.Column(scale=1):
124
+ with gr.Group(elem_classes="metric-card"):
125
+ gr.Markdown("### ⚑ Triage Actions")
126
+
127
+ action_type = gr.Dropdown(
128
+ choices=["assign_priority", "request_info", "escalate", "defer", "advance_queue"],
129
+ label="Action",
130
+ value="assign_priority"
131
+ )
132
+
133
+ priority = gr.Dropdown(
134
+ choices=["immediate", "urgent", "less_urgent", "non_urgent"],
135
+ label="Urgency Level",
136
+ value="urgent"
137
+ )
138
+
139
+ info_field = gr.Dropdown(
140
+ choices=["vitals", "history", "symptoms", "allergies", "medications"],
141
+ label="Request Missing Record Data",
142
+ visible=False
143
+ )
144
+
145
+ def update_visibility(act):
146
+ return [
147
+ gr.update(visible=act == "assign_priority"),
148
+ gr.update(visible=act == "request_info")
149
+ ]
150
+
151
+ action_type.change(
152
+ fn=update_visibility,
153
+ inputs=[action_type],
154
+ outputs=[priority, info_field]
155
+ )
156
+
157
+ submit_btn = gr.Button("πŸ“‹ Submit Order", variant="primary")
158
+
159
+ with gr.Group(elem_classes="metric-card"):
160
+ gr.Markdown("### πŸ“œ Event Log")
161
+ log_md = gr.Markdown("No actions taken during this scenario.")
162
+
163
+ async def ui_reset(task_name):
164
+ try:
165
+ res = await web_manager.reset_environment({"task_name": task_name})
166
+ return update_dashboard(res)
167
+ except Exception as e:
168
+ return update_dashboard({}, custom_error=str(e))
169
+
170
+ async def ui_step(act_type, prio, info_f, pt_state):
171
+ obs = pt_state.get("observation", {})
172
+ pt = obs.get("current_patient", {})
173
+ pt_id = pt.get("patient_id", "") if pt else ""
174
+
175
+ payload = {
176
+ "action_type": act_type,
177
+ "patient_id": pt_id
178
+ }
179
+ if act_type == "assign_priority":
180
+ payload["priority_level"] = prio
181
+ elif act_type == "request_info":
182
+ payload["info_field"] = info_f
183
+
184
+ try:
185
+ res = await web_manager.step_environment(payload)
186
+ return update_dashboard(res)
187
+ except Exception as e:
188
+ return update_dashboard(pt_state, custom_error=str(e))
189
+
190
+ def update_dashboard(res, custom_error=None):
191
+ obs = res.get("observation", {})
192
+
193
+ p_id, cc, syp, vit, hst, q_inf, fb = extract_patient_display(obs, custom_error)
194
+
195
+ # Formulate logs
196
+ logs = getattr(web_manager.episode_state, "action_logs", [])
197
+ state = web_manager.get_state()
198
+ reward_sum = state.get("task_state", {}).get("total_reward", 0.0) if state else 0.0
199
+
200
+ log_str = f"**Current Score:** {reward_sum:.2f}\n\n"
201
+ if not logs:
202
+ log_str += "No actions yet."
203
+ else:
204
+ for log in reversed(logs[-8:]):
205
+ d = log.done
206
+ r = log.reward
207
+ a = log.action.get("action_type", "?")
208
+ tar = log.action.get("priority_level") or log.action.get("info_field") or ""
209
+ tar = f"({tar})" if tar else ""
210
+ pt_ref = log.action.get("patient_id", "?")
211
+ st = "🏁 Task Complete" if d else f"Reward {r:.2f}"
212
+ log_str += f"- **{a}** {tar} to `{pt_ref}` β†’ {st}\n"
213
+
214
+ return [
215
+ res, p_id, cc, syp, vit, hst, q_inf, fb, log_str
216
+ ]
217
+
218
+ outputs = [
219
+ current_state, patient_id_md, cc_md, symp_md, vitals_md, history_md,
220
+ queue_md, feedback_md, log_md
221
+ ]
222
+
223
+ reset_btn.click(fn=ui_reset, inputs=[task_dropdown], outputs=outputs)
224
+ submit_btn.click(fn=ui_step, inputs=[action_type, priority, info_field, current_state], outputs=outputs)
225
+
226
+ # Trigger Reset initially when loading Gradio Dashboard (requires client to mount it)
227
+ # blocks.load(fn=ui_reset, inputs=[task_dropdown], outputs=outputs)
228
+
229
+ return blocks