mohdelgaar commited on
Commit
0e43c47
·
1 Parent(s): 075fc40
Files changed (2) hide show
  1. app.py +354 -94
  2. demo_assets.py +200 -0
app.py CHANGED
@@ -1,4 +1,7 @@
 
1
  import re
 
 
2
  import argparse
3
  import torch
4
  import gradio as gr
@@ -10,7 +13,9 @@ from model import load_model
10
  from datetime import datetime
11
  from dateutil import parser
12
  from demo_assets import *
13
- from typing import List, Dict, Any
 
 
14
 
15
  def get_args():
16
  parser = argparse.ArgumentParser()
@@ -82,23 +87,235 @@ elif args.task == 'token':
82
  elif args.label_encoding == 'boe':
83
  args.num_labels *= 3
84
 
85
- categories = ['Contact related', 'Gathering additional information', 'Defining problem',
86
- 'Treatment goal', 'Drug related', 'Therapeutic procedure related', 'Evaluating test result',
87
- 'Deferment', 'Advice and precaution', 'Legal and insurance related']
88
- unicode_symbols = [
89
- "\U0001F91D", # Handshake
90
- "\U0001F50D", # Magnifying glass
91
- "\U0001F9E9", # Puzzle piece
92
- "\U0001F3AF", # Target
93
- "\U0001F48A", # Pill
94
- "\U00002702", # Surgical scissors
95
- "\U0001F9EA", # Test tube
96
- "\U000023F0", # Alarm clock
97
- "\U000026A0", # Warning sign
98
- "\U0001F4C4" # Document
99
- ]
100
-
101
- OTHERS_ID = 18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  def postprocess_labels(text, logits, t2c):
103
  tags = [None for _ in text]
104
  labels = logits.argmax(-1)
@@ -217,6 +434,7 @@ def get_structured_data(*inputs):
217
  data.append({
218
  'date': date,
219
  'timestamp': parser.parse(date),
 
220
  'decision_type': categories[c], 'details': text[s:e]})
221
  return data
222
 
@@ -279,7 +497,7 @@ def create_timeline_plot(data: List[Dict[str, Any]]):
279
  fig.update_layout(height=600)
280
  return fig
281
 
282
- def filter_timeline(decision_type: str, start_date: str, end_date: str) -> px.scatter:
283
  global structured_data
284
  filtered_data = structured_data
285
  if 'All' not in decision_types:
@@ -294,59 +512,33 @@ def filter_timeline(decision_type: str, start_date: str, end_date: str) -> px.sc
294
  def generate_summary(*inputs) -> str:
295
  global structured_data
296
  structured_data = get_structured_data(*inputs)
297
- decision_types = {}
298
- for event in structured_data:
299
- decision_type = event['decision_type']
300
- decision_types[decision_type] = decision_types.get(decision_type, 0) + 1
301
 
302
- summary = "Decision Type Summary:\n"
303
- for decision_type, count in decision_types.items():
304
- summary += f"{decision_type}: {count}\n"
305
- return summary, create_timeline_plot(structured_data)
 
 
 
 
 
 
 
 
 
306
 
307
  global sum_c
308
  sum_c = 1
309
- SUM_INPUTS = 20
310
  structured_data = []
311
 
312
  device = model.backbone.device
313
- # colors = ['aqua', 'blue', 'fuchsia', 'teal', 'green', 'olive', 'lime', 'silver', 'purple', 'red',
314
- # 'yellow', 'navy', 'gray', 'white', 'maroon', 'black']
315
- colors = ['#8dd3c7', '#ffffb3', '#bebada', '#fb8072', '#80b1d3', '#fdb462', '#b3de69', '#fccde5', '#d9d9d9', '#bc80bd']
316
-
317
- color_map = {cat: colors[i] for i,cat in enumerate(categories)}
318
-
319
- det_desc = ['Admit, discharge, follow-up, referral',
320
- 'Ordering test, consulting colleague, seeking external information',
321
- 'Diagnostic conclusion, evaluation of health state, etiological inference, prognostic judgment',
322
- 'Quantitative or qualitative',
323
- 'Start, stop, alter, maintain, refrain',
324
- 'Start, stop, alter, maintain, refrain',
325
- 'Positive, negative, ambiguous test results',
326
- 'Transfer responsibility, wait and see, change subject',
327
- 'Advice or precaution',
328
- 'Sick leave, drug refund, insurance, disability']
329
-
330
- desc = '### Zones (categories)\n'
331
- desc += '| | |\n| --- | --- |\n'
332
- for i,cat in enumerate(categories):
333
- desc += f'| {unicode_symbols[i]} **{cat}** | {det_desc[i]}|\n'
334
-
335
- #colors
336
- #markdown labels
337
- #legend and desc
338
- #css font-size
339
- css = '.category-legend {border:1px dashed black;}'\
340
- '.text-sm {font-size: 1.5rem; line-height: 200%;}'\
341
- '.gr-sample-textbox {width: 1000px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}'\
342
- '.text-limit label textarea {height: 150px !important; overflow: scroll; }'\
343
- '.text-gray-500 {color: #111827; font-weight: 600; font-size: 1.25em; margin-top: 1.6em; margin-bottom: 0.6em;'\
344
- 'line-height: 1.6;}'\
345
- '#sum-out {border: 2px solid #007bff; padding: 20px; border-radius: 10px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);'
346
- title='Clinical Decision Zoning'
347
- with gr.Blocks(title=title, css=css) as demo:
348
- gr.Markdown(f'# {title}')
349
  with gr.Tab("Label a Clinical Note"):
 
350
  with gr.Row():
351
  with gr.Column():
352
  gr.Markdown("## Enter a Discharge Summary or Clinical Note"),
@@ -359,33 +551,18 @@ with gr.Blocks(title=title, css=css) as demo:
359
  gr.Markdown("## Labeled Summary or Note"),
360
  text_out = gr.Highlight(label="", combine_adjacent=True, show_legend=False, color_map=color_map)
361
  gr.Examples(text_examples, inputs=text_input)
362
- with gr.Tab("Summarize Patient History"):
363
- with gr.Row():
364
- with gr.Column():
365
- sum_inputs = [gr.Text(label='Clinical Note 1', elem_classes='text-limit')]
366
- sum_inputs.extend([gr.Text(label='Clinical Note %d'%i, visible=False, elem_classes='text-limit')
367
- for i in range(2, SUM_INPUTS + 1)])
368
- sum_btn = gr.Button('Run')
369
- with gr.Row():
370
- ex_add = gr.Button("+")
371
- ex_sub = gr.Button("-")
372
- upload = gr.File(label='Upload clinical notes', file_types=['text'], file_count='multiple')
373
- gr.Examples(sum_examples, inputs=upload,
374
- fn = update_inputs, outputs=sum_inputs, run_on_click=True)
375
- with gr.Column():
376
- gr.Markdown("## Summarized Clinical Decision History")
377
- sum_out = gr.Markdown(elem_id='sum-out')
378
- with gr.Tab("Timeline Visualization Tool"):
379
  with gr.Column():
380
- sum_inputs2 = [gr.Text(label='Clinical Note 1', elem_classes='text-limit')]
381
- sum_inputs2.extend([gr.Text(label='Clinical Note %d'%i, visible=False, elem_classes='text-limit')
382
  for i in range(2, SUM_INPUTS + 1)])
383
  with gr.Row():
384
- ex_add2 = gr.Button("+")
385
- ex_sub2 = gr.Button("-")
386
- upload2 = gr.File(label='Upload clinical notes', file_types=['text'], file_count='multiple')
387
- gr.Examples(sum_examples, inputs=upload2,
388
- fn = update_inputs, outputs=sum_inputs2, run_on_click=True)
389
  with gr.Column():
390
  with gr.Row():
391
  decision_type = gr.Dropdown(["All"] + categories,
@@ -399,19 +576,102 @@ with gr.Blocks(title=title, css=css) as demo:
399
  timeline_plot = gr.Plot()
400
 
401
  summary_button = gr.Button("Generate Summary")
402
- summary_output = gr.Textbox(label="Summary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  gr.Markdown(desc)
404
 
405
  # Functions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  text_input.submit(process, inputs=text_input, outputs=text_out)
407
  text_btn.click(process, inputs=text_input, outputs=text_out)
408
  upload.change(update_inputs, inputs=upload, outputs=sum_inputs)
409
- upload2.change(update_inputs, inputs=upload2, outputs=sum_inputs2)
410
  ex_add.click(add_ex, inputs=sum_inputs, outputs=sum_inputs)
411
  ex_sub.click(sub_ex, inputs=sum_inputs, outputs=sum_inputs)
412
- ex_add2.click(add_ex, inputs=sum_inputs2, outputs=sum_inputs2)
413
- ex_sub2.click(sub_ex, inputs=sum_inputs2, outputs=sum_inputs2)
414
- sum_btn.click(process_sum, inputs=sum_inputs, outputs=sum_out)
415
  filter_button.click(filter_timeline, inputs=[decision_type, start_date, end_date], outputs=timeline_plot)
416
- summary_button.click(generate_summary, inputs=sum_inputs2, outputs=[summary_output, timeline_plot])
417
  demo.launch(share=True)
 
1
+ import os
2
  import re
3
+ import uuid
4
+ import json
5
  import argparse
6
  import torch
7
  import gradio as gr
 
13
  from datetime import datetime
14
  from dateutil import parser
15
  from demo_assets import *
16
+ from typing import List, Dict, Any, Optional, Tuple
17
+ from dataclasses import dataclass
18
+ from collections import defaultdict
19
 
20
  def get_args():
21
  parser = argparse.ArgumentParser()
 
87
  elif args.label_encoding == 'boe':
88
  args.num_labels *= 3
89
 
90
+ @dataclass
91
+ class KeyDef:
92
+ key: str
93
+ name: str
94
+ desc: str = ''
95
+ color: str = 'lightblue'
96
+ symbol: str = ''
97
+
98
+ class AnnotationState:
99
+ def __init__(self):
100
+ self.entity_regex = r'\[\@.*?\#.*?\*\](?!\#)'
101
+ self.recommend_regex = r'\[\$.*?\#.*?\*\](?!\#)'
102
+ self.history = []
103
+ self.config_file = "configs/default.config"
104
+ self.press_commands = self.read_config()
105
+ # Internal state holds the actual annotations
106
+ self.annotations = []
107
+ self.raw_text = ""
108
+
109
+ def read_config(self) -> List[KeyDef]:
110
+ if not os.path.exists(self.config_file):
111
+ default_config = [{
112
+ 'key': key,
113
+ 'name': name,
114
+ 'color': color,
115
+ 'symbol': symbol
116
+ }
117
+ for key, name, color, symbol in zip(keys, categories, colors, unicode_symbols)
118
+ ]
119
+ os.makedirs("configs", exist_ok=True)
120
+ with open(self.config_file, 'w') as fp:
121
+ json.dump(default_config, fp)
122
+
123
+ with open(self.config_file, 'r') as fp:
124
+ config_dict = json.load(fp)
125
+
126
+ return [KeyDef(**entry) for entry in config_dict]
127
+
128
+ def get_cmd_by_key(self, key: str) -> Optional[KeyDef]:
129
+ return next((cmd for cmd in self.press_commands if cmd.key == key), None)
130
+
131
+ def set_text(self, text: str):
132
+ """Initialize with new text, clearing annotations"""
133
+ self.raw_text = text
134
+ self.annotations = []
135
+ self.history = []
136
+
137
+ def add_annotation(self, start: int, end: int, entity_type: str) -> str:
138
+ """Add new annotation and return display text"""
139
+ # Save current state to history
140
+ self.history.append((self.raw_text, list(self.annotations)))
141
+ if len(self.history) > 20:
142
+ self.history.pop(0)
143
+
144
+ # Add new annotation
145
+ self.annotations.append((start, end, entity_type))
146
+ return self.get_display_text()
147
+
148
+ def remove_annotation(self, start: int, end: int) -> str:
149
+ """Remove annotation at position if it exists, splitting spans if needed"""
150
+ self.history.append((self.raw_text, list(self.annotations)))
151
+
152
+ new_annotations = []
153
+
154
+ for a in self.annotations:
155
+ annotation_start, annotation_end, entity_type = a
156
+
157
+ # If the current annotation does not overlap, keep it as is
158
+ if annotation_end < start or annotation_start > end:
159
+ new_annotations.append(a)
160
+
161
+ # If the removed span is a proper subset, split the annotation
162
+ elif annotation_start < start and annotation_end > end:
163
+ new_annotations.append((annotation_start, start - 1, entity_type))
164
+ new_annotations.append((end + 1, annotation_end, entity_type))
165
+
166
+ # If there's overlap with the start, but not the end
167
+ elif annotation_start < start <= annotation_end:
168
+ new_annotations.append((annotation_start, start - 1, entity_type))
169
+
170
+ # If there's overlap with the end, but not the start
171
+ elif annotation_start <= end < annotation_end:
172
+ new_annotations.append((end + 1, annotation_end, entity_type))
173
+
174
+ self.annotations = new_annotations
175
+ return self.get_display_text()
176
+
177
+ def undo(self) -> str:
178
+ """Undo last annotation action"""
179
+ if not self.history:
180
+ return self.get_display_text()
181
+
182
+ self.raw_text, self.annotations = self.history.pop()
183
+ return self.get_display_text()
184
+
185
+ def get_display_text(self) -> str:
186
+ """Generate display text with HTML formatting for annotations"""
187
+ if not self.annotations:
188
+ return f'<div id="annotated-text">{self.raw_text}</div> <div id="legend"></div>'
189
+
190
+ # Sort annotations by start position
191
+ sorted_annotations = sorted(self.annotations, key=lambda x: (x[0], -x[1]))
192
+
193
+ # Build display text with HTML spans
194
+ result = ['<div id="annotated-text">']
195
+ last_end = 0
196
+
197
+ for start, end, entity_type in sorted_annotations:
198
+ if start < last_end and end > last_end:
199
+ start = last_end
200
+ elif start < last_end:
201
+ continue
202
+
203
+
204
+ # Add text before annotation
205
+ result.append(self.raw_text[last_end:start])
206
+
207
+ # Add annotated text with highlighting
208
+ text = self.raw_text[start:end]
209
+ cmd = self.get_cmd_by_key(entity_type)
210
+ color = cmd.color
211
+ result.append(f'<span style="background-color: {color};" title="{cmd.name}">{text}</span>') # Nicer tooltip
212
+
213
+ last_end = end
214
+
215
+ # Add remaining text
216
+ result.append(self.raw_text[last_end:])
217
+ result.append('</div>')
218
+
219
+ # Generate legend
220
+ legend = ['<div id="legend" style="margin-top: 10px;"><span style="font-weight: bold;">Legend:</span > '] # Margin and bold legend title
221
+ used_categories = sorted(list(set([a[2] for a in self.annotations])))
222
+ for cat in used_categories:
223
+ cmd = self.get_cmd_by_key(cat)
224
+ legend.append(f'<span style="background-color: {cmd.color}; padding: 3px 5px; border-radius: 3px; margin-right: 5px; font-size:0.9em; display: inline-block; vertical-align: middle; color: black; font-family: sans-serif;">{cmd.name}</span>') # Improved legend item styling
225
+ legend.append('</div>')
226
+ result.extend(legend)
227
+
228
+ return "".join(result)
229
+
230
+
231
+ def get_annotated_text(self, annotator_id=None, discharge_summary_id=None) -> dict:
232
+ """Generate a dictionary containing annotation data."""
233
+ unique_id = str(uuid.uuid4())[:8]
234
+ annotations = []
235
+ if self.annotations:
236
+ sorted_annotations = sorted(self.annotations, key=lambda x: (x[0], -x[1]))
237
+ for idx, (start, end, entity_type) in enumerate(sorted_annotations):
238
+ cmd = self.get_cmd_by_key(entity_type)
239
+ annotations.append({
240
+ "decision": self.raw_text[start:end],
241
+ "category": f'Category {categories.index(cmd.name) + 1}: {cmd.name}',
242
+ "start_offset": start,
243
+ "end_offset": end,
244
+ "annotation_id": f'{unique_id}_{idx}'
245
+ })
246
+
247
+ return {
248
+ "annotator_id": annotator_id if annotator_id else None,
249
+ "discharge_summary_id": discharge_summary_id if discharge_summary_id else None,
250
+ "annotations": annotations
251
+ }
252
+
253
+ def init_text(text):
254
+ if text:
255
+ state.set_text(text)
256
+ return state.get_display_text()
257
+ return "<div id='annotated-text'>Enter text to begin...</div>"
258
+
259
+ def add_entity(cmd_key, start: int, end: int):
260
+ """Handle adding new entity annotations"""
261
+ if start == end:
262
+ return state.get_display_text(), "No text selected"
263
+
264
+ cmd = state.get_cmd_by_key(cmd_key)
265
+ if not cmd:
266
+ return state.get_display_text(), "Invalid command"
267
+
268
+ new_text = state.add_annotation(start, end, cmd.key)
269
+ return new_text, f"Added {cmd.name} entity"
270
+
271
+ def remove_entity(start: int, end: int):
272
+ """Handle removal of annotations"""
273
+ if start == end:
274
+ return state.get_display_text(), "No text selected"
275
+ return state.remove_annotation(start, end), "Removed annotation"
276
+
277
+ def undo():
278
+ """Handle undoing the last action"""
279
+ return state.undo(), "Undid last action"
280
+
281
+ def download_annotations(annotator_id, discharge_summary_id):
282
+ """Generates and provides annotation data for download."""
283
+ annotation_data = state.get_annotated_text(annotator_id, discharge_summary_id)
284
+ with open(OUTPUT_PATH, 'w') as f:
285
+ json.dump(annotation_data, f, indent=4)
286
+ return OUTPUT_PATH
287
+
288
+
289
+
290
+ def refresh_annotations(annotator_id, discharge_summary_id):
291
+ """Refreshes the displayed annotation JSON."""
292
+ return state.get_annotated_text(annotator_id, discharge_summary_id)
293
+
294
+
295
+ def clear_annotations():
296
+ state.set_text(state.raw_text) # Clears annotations by setting empty text
297
+ return gr.update(interactive=True, elem_classes=[]), state.get_display_text() # added value
298
+
299
+ def model_predict(text):
300
+ """Placeholder for model prediction logic"""
301
+ output, t2c = predict(text)
302
+ spans = indicators_to_spans(output.argmax(-1), t2c)
303
+ spans = [(s, e, keys[c]) for c, s, e in spans]
304
+ return spans
305
+
306
+ def apply_predictions(text):
307
+ predictions = model_predict(text)
308
+ state.set_text(text)
309
+ for start, end, entity_type in predictions:
310
+ state.add_annotation(start, end, entity_type)
311
+ return state.get_display_text()
312
+
313
+ state = AnnotationState()
314
+ all_keys = [f'"{cmd.key}"' for cmd in state.press_commands]
315
+ key_list_str = f'[{", ".join(all_keys)}]'
316
+ shortcut_js = shortcut_js_template%key_list_str
317
+
318
+
319
  def postprocess_labels(text, logits, t2c):
320
  tags = [None for _ in text]
321
  labels = logits.argmax(-1)
 
434
  data.append({
435
  'date': date,
436
  'timestamp': parser.parse(date),
437
+ 'decision_cat': c,
438
  'decision_type': categories[c], 'details': text[s:e]})
439
  return data
440
 
 
497
  fig.update_layout(height=600)
498
  return fig
499
 
500
+ def filter_timeline(decision_types: str, start_date: str, end_date: str) -> px.scatter:
501
  global structured_data
502
  filtered_data = structured_data
503
  if 'All' not in decision_types:
 
512
  def generate_summary(*inputs) -> str:
513
  global structured_data
514
  structured_data = get_structured_data(*inputs)
 
 
 
 
515
 
516
+ dates = defaultdict(lambda: defaultdict(list))
517
+ for event in structured_data:
518
+ dates[event['date']][event['decision_cat']].append(event['details'])
519
+
520
+ out = ""
521
+ for date in sorted(dates.keys(), key = lambda x: parser.parse(x)):
522
+ out += f'## **[{date}]**\n\n'
523
+ decs = dates[date]
524
+ for c in decs:
525
+ out += f'### {unicode_symbols[c]} ***{categories[c]}***\n\n'
526
+ for dec in decs[c]:
527
+ out += f'{dec}\n\n'
528
+ return out, create_timeline_plot(structured_data)
529
 
530
  global sum_c
531
  sum_c = 1
 
532
  structured_data = []
533
 
534
  device = model.backbone.device
535
+
536
+ with gr.Blocks(head=shortcut_js,
537
+ title='MedDecXtract', css=css) as demo:
538
+ gr.Image('assets/logo.png', height=100, container=False, show_download_button=False)
539
+ gr.Markdown(title)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
  with gr.Tab("Label a Clinical Note"):
541
+ gr.Markdown(label_desc)
542
  with gr.Row():
543
  with gr.Column():
544
  gr.Markdown("## Enter a Discharge Summary or Clinical Note"),
 
551
  gr.Markdown("## Labeled Summary or Note"),
552
  text_out = gr.Highlight(label="", combine_adjacent=True, show_legend=False, color_map=color_map)
553
  gr.Examples(text_examples, inputs=text_input)
554
+ with gr.Tab("Patient History Visualization"):
555
+ gr.Markdown(vis_desc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
556
  with gr.Column():
557
+ sum_inputs = [gr.Text(label='Clinical Note 1', elem_classes='text-limit')]
558
+ sum_inputs.extend([gr.Text(label='Clinical Note %d'%i, visible=False, elem_classes='text-limit')
559
  for i in range(2, SUM_INPUTS + 1)])
560
  with gr.Row():
561
+ ex_add = gr.Button("+")
562
+ ex_sub = gr.Button("-")
563
+ upload = gr.File(label='Upload clinical notes', file_types=['text'], file_count='multiple')
564
+ gr.Examples(sum_examples, inputs=upload,
565
+ fn = update_inputs, outputs=sum_inputs, run_on_click=True)
566
  with gr.Column():
567
  with gr.Row():
568
  decision_type = gr.Dropdown(["All"] + categories,
 
576
  timeline_plot = gr.Plot()
577
 
578
  summary_button = gr.Button("Generate Summary")
579
+ with gr.Accordion('Summary'):
580
+ summary_output = gr.Markdown(elem_id='sum-out') #gr.Textbox(label="Summary")
581
+ with gr.Tab("Interactive Text Annotator"):
582
+ gr.Markdown(annotator_desc)
583
+ with gr.Row():
584
+ with gr.Column():
585
+ annot_text_input = gr.Textbox(
586
+ label="Enter Text to Annotate",
587
+ placeholder="Enter or paste text here...",
588
+ lines=5,
589
+ elem_id='annot_text_input'
590
+ )
591
+ gr.Examples(text_examples, inputs=annot_text_input)
592
+ msg_output = gr.Textbox(label="Status Messages", interactive=False)
593
+ display_area = gr.HTML(
594
+ label="Annotated Text",
595
+ value="<div id='annotated-text'><i>Output box</i></div>"
596
+ )
597
+
598
+ k = 3 # Set the maximum number of buttons per row
599
+ num_buttons = len(state.press_commands)
600
+ rows = (num_buttons + k - 1) // k
601
+ entity_buttons = []
602
+ with gr.Group():
603
+ predict_btn = gr.Button("Generate Predictions", size='lg', variant='primary')
604
+ for i in range(rows):
605
+ with gr.Row():
606
+ for j in range(min(k, num_buttons - i * k)):
607
+ real_idx = i * k + j
608
+ cmd = state.press_commands[real_idx]
609
+ entity_buttons.append(
610
+ gr.Button(f"{cmd.symbol} {cmd.name} ({cmd.key})",
611
+ elem_id=f'btn_{cmd.key}',
612
+ size='sm'))
613
+ if i == (rows - 1):
614
+ remove_btn = gr.Button("Remove (q)", size='sm', variant='secondary', elem_id='btn_q')
615
+ undo_btn = gr.Button("Undo (z)", size='sm', elem_id='btn_z')
616
+ clear_btn = gr.Button("Clear Annotations", size='lg', variant='stop')
617
+
618
+
619
+ with gr.Accordion("Download/View Annotations \U0001F4BE", open=False): # Combined Accordion
620
+ with gr.Row():
621
+ annotator_id = gr.Textbox(label="Annotator ID", placeholder="Enter your annotator ID")
622
+ discharge_summary_id = gr.Textbox(label="Discharge Summary ID", placeholder="Enter the discharge summary ID")
623
+
624
+ with gr.Row():
625
+ download_file = gr.File(interactive=False, visible=True, label="Download") # download_btn renamed and made into gr.File
626
+ annotations_json = gr.JSON(label="Annotations JSON")
627
+
628
+ refresh_btn = gr.Button("🔄 Refresh Annotations", elem_id="refresh_btn") # Renamed for clarity
629
+ download_btn = gr.Button("Download Annotated Text", elem_id="download_btn") # Added a button to trigger download
630
+
631
+
632
+ # Hidden state components for selection
633
+ selection_start = gr.Number(value=0, visible=False)
634
+ selection_end = gr.Number(value=0, visible=False)
635
+
636
  gr.Markdown(desc)
637
 
638
  # Functions
639
+ # Wire up event handlers
640
+ annot_text_input.change(init_text, annot_text_input, display_area)
641
+
642
+ # Wire up the buttons with the selection JavaScript
643
+ for btn, cmd in zip(entity_buttons, state.press_commands):
644
+ btn.click(lambda s=None, e=None, c=cmd.key: add_entity(c, s, e),[selection_start, selection_end], [display_area, msg_output], js=select_js).then(
645
+ lambda: gr.update(interactive=state.annotations == [], elem_classes=[] if state.annotations == [] else ['locked-input']), # Disable input if annotations exist
646
+ outputs=annot_text_input
647
+ )
648
+
649
+ remove_btn.click( remove_entity, [selection_start, selection_end], [display_area, msg_output], js=select_js).then(
650
+ lambda: gr.update(interactive=state.annotations == [], elem_classes=[] if state.annotations == [] else ['locked-input']),
651
+ outputs=annot_text_input
652
+ )
653
+
654
+ undo_btn.click(undo, None, [display_area, msg_output]).then(
655
+ lambda: gr.update(interactive=state.annotations == [], elem_classes=[] if state.annotations == [] else ['locked-input']),
656
+ outputs=annot_text_input
657
+ )
658
+
659
+ download_btn.click(download_annotations, [annotator_id, discharge_summary_id], download_file) # Output to download_file
660
+ refresh_btn.click(refresh_annotations, [annotator_id, discharge_summary_id], annotations_json) # No change in functionality
661
+
662
+
663
+ clear_btn.click(clear_annotations, outputs=[annot_text_input, display_area])
664
+
665
+ predict_btn.click(apply_predictions, annot_text_input, display_area).then(
666
+ lambda: gr.update(interactive=state.annotations == [], elem_classes=[] if state.annotations == [] else ['locked-input']),
667
+ outputs=text_input
668
+ )
669
+
670
  text_input.submit(process, inputs=text_input, outputs=text_out)
671
  text_btn.click(process, inputs=text_input, outputs=text_out)
672
  upload.change(update_inputs, inputs=upload, outputs=sum_inputs)
 
673
  ex_add.click(add_ex, inputs=sum_inputs, outputs=sum_inputs)
674
  ex_sub.click(sub_ex, inputs=sum_inputs, outputs=sum_inputs)
 
 
 
675
  filter_button.click(filter_timeline, inputs=[decision_type, start_date, end_date], outputs=timeline_plot)
676
+ summary_button.click(generate_summary, inputs=sum_inputs, outputs=[summary_output, timeline_plot])
677
  demo.launch(share=True)
demo_assets.py CHANGED
@@ -1,3 +1,7 @@
 
 
 
 
1
  sum_examples = [
2
  [['examples/note%d.txt'%i for i in range(1,n)]]
3
  for n in range(5,1, -1)
@@ -18,3 +22,199 @@ text_examples = [
18
  "..."
19
  "Call your PCP or return to the ED for fevers/chills/shakes, chest pain, shortness of breath, pain at the site of your dialysis catheter, nausea, vomiting, or swelling in your legs/feet. "]
20
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ OTHERS_ID = 18
2
+ SUM_INPUTS = 20
3
+ OUTPUT_PATH = 'meddec_annots.json'
4
+
5
  sum_examples = [
6
  [['examples/note%d.txt'%i for i in range(1,n)]]
7
  for n in range(5,1, -1)
 
22
  "..."
23
  "Call your PCP or return to the ED for fevers/chills/shakes, chest pain, shortness of breath, pain at the site of your dialysis catheter, nausea, vomiting, or swelling in your legs/feet. "]
24
  ]
25
+
26
+ keys = ['c', 'g', 'p', 't', 'd', 'p', 'e', 'f', 'a', 'l']
27
+ categories = ['Contact related', 'Gathering additional information', 'Defining problem',
28
+ 'Treatment goal', 'Drug related', 'Therapeutic procedure related', 'Evaluating test result',
29
+ 'Deferment', 'Advice and precaution', 'Legal and insurance related']
30
+ det_desc = ['Admit, discharge, follow-up, referral',
31
+ 'Ordering test, consulting colleague, seeking external information',
32
+ 'Diagnostic conclusion, evaluation of health state, etiological inference, prognostic judgment',
33
+ 'Quantitative or qualitative',
34
+ 'Start, stop, alter, maintain, refrain',
35
+ 'Start, stop, alter, maintain, refrain',
36
+ 'Positive, negative, ambiguous test results',
37
+ 'Transfer responsibility, wait and see, change subject',
38
+ 'Advice or precaution',
39
+ 'Sick leave, drug refund, insurance, disability']
40
+ unicode_symbols = [
41
+ "\U0001F91D", # Handshake
42
+ "\U0001F50D", # Magnifying glass
43
+ "\U0001F9E9", # Puzzle piece
44
+ "\U0001F3AF", # Target
45
+ "\U0001F48A", # Pill
46
+ "\U00002702", # Surgical scissors
47
+ "\U0001F9EA", # Test tube
48
+ "\U000023F0", # Alarm clock
49
+ "\U000026A0", # Warning sign
50
+ "\U0001F4C4" # Document
51
+ ]
52
+
53
+ colors = ['#8dd3c7', '#ffffb3', '#bebada', '#fb8072', '#80b1d3', '#fdb462', '#b3de69', '#fccde5', '#d9d9d9', '#bc80bd']
54
+
55
+ color_map = {cat: colors[i] for i,cat in enumerate(categories)}
56
+
57
+
58
+ annotator_desc = """
59
+ ## Interactive Text Annotator
60
+
61
+ This tool allows you to manually annotate medical text for detailed analysis and model training. Follow these steps:
62
+
63
+ 1. **Input Text:** Enter or paste the text you want to annotate into the text box. You can also use the provided examples.
64
+ 2. **Generate Predictions (Optional):** Click "Generate Predictions" to pre-annotate the text using a machine learning model. This can serve as a starting point, which you can then review and modify. Note: Generating predictions will lock the input textbox to prevent accidental edits.
65
+ 3. **Select Text:** In the **output box below**, highlight the portion of the text you wish to annotate.
66
+ 4. **Annotate:** Click one of the category buttons to apply the annotation to the selected text. The selected text will be highlighted with the corresponding color.
67
+ 5. **Remove/Undo:**
68
+ * **Remove (q):** Click "Remove" or press 'q' to remove the annotation from the selected text.
69
+ * **Undo (z):** Click "Undo" or press 'z' to revert the last annotation action.
70
+ 6. **Download/View Annotations:**
71
+ * **Annotator ID & Discharge Summary ID:** Enter an identifier for the annotator and an optional ID for the discharge summary. These IDs will be included in the downloaded JSON data.
72
+ * **Refresh Annotations:** Click "Refresh Annotations" to display the current annotations in JSON format.
73
+ * **Download Annotated Text:** Click "Download Annotated Text" to save your annotations as a JSON file.
74
+ 7. **Clear Annotations:** Click "Clear Annotations" to remove all annotations and start over. This will also unlock the input textbox.
75
+
76
+
77
+ **Keyboard Shortcuts:**
78
+
79
+ * Use the keys indicated on the buttons to annotate selections.
80
+ * `q`: Remove annotation
81
+ * `z`: Undo last action
82
+ """
83
+
84
+ label_desc = """
85
+ ## Label a Clinical Note
86
+
87
+ This tool allows you to quickly identify and categorize key clinical decisions within a single clinical note.
88
+
89
+ 1. **Input Text:** Enter or paste the clinical note into the text box. You can also select from the provided examples.
90
+ 2. **Run:** Click the "Run" button to process the text.
91
+ 3. **View Results:** The labeled output will be displayed, highlighting the identified clinical decisions with color-coded categories.
92
+
93
+ This provides a simplified overview of the patient's history and key decisions at a glance.
94
+ """
95
+ vis_desc = """
96
+ ## Patient History Visualization
97
+
98
+ This tool allows you to visualize the timeline of clinical decisions across multiple clinical notes for a single patient.
99
+
100
+ 1. **Upload Clinical Notes:** Upload multiple clinical notes in text format. You can also select from the provided examples.
101
+ 2. **Add/Remove Notes:** Use the "+" and "-" buttons to add or remove input fields for additional notes.
102
+ 3. **Filter Timeline:**
103
+ * **Decision Type:** Select one or more decision types from the dropdown menu to filter the timeline. Select "All" to display all decision types.
104
+ * **Start/End Date:** Enter a start and end date (MM/DD/YYYY) to filter the timeline by date range.
105
+ * **Filter:** Click the "Filter Timeline" button to apply the selected filters.
106
+ 4. **Generate Summary:** Click the "Generate Summary" button to generate a summarized view of the clinical decisions organized by date and category. This will also update the timeline plot.
107
+ 5. **View Timeline:** The interactive timeline plot displays the clinical decisions over time. Hover over each data point to view details.
108
+
109
+ This visualization helps to understand the progression of clinical decisions and identify trends in patient care.
110
+ """
111
+
112
+
113
+ title = """
114
+ <h1 style="text-align: center;">Medical Decisions Extraction, Visualization, and Annotation</h1>
115
+
116
+ <p style="font-size:1.2em;">This application offers three interactive tools for working with clinical text data:</p>
117
+ <p style="font-size:1.2em;">1. <b>Label a Clinical Note:</b> Process individual notes and receive highlighted key clinical decisions.</br>
118
+ 2. <b>Patient History Visualization:</b> Upload multiple notes to visualize the timeline of decisions.</br>
119
+ 3. <b>Interactive Text Annotator:</b> Manually annotate text for detailed analysis and model training.</p>
120
+ """
121
+
122
+ desc = '### Decision Categories\n'
123
+ desc += '| | |\n| --- | --- |\n'
124
+ for i,cat in enumerate(categories):
125
+ desc += f'| {unicode_symbols[i]} **{cat}** | {det_desc[i]}|\n'
126
+
127
+ shortcut_js_template = """
128
+ <script>
129
+ function shortcuts(e) {
130
+ switch (e.target.tagName.toLowerCase()) {
131
+ case "input":
132
+ case "textarea":
133
+ break;
134
+ default:
135
+ if (e.key.toLowerCase() === 'q') {
136
+ document.getElementById('btn_q').click();
137
+ } else if (e.key.toLowerCase() === 'z') {
138
+ document.getElementById('btn_z').click();
139
+ } else {
140
+ const buttonKeys = %s;
141
+ for (const key of buttonKeys) {
142
+ if (e.key.toLowerCase() === key) {
143
+ document.getElementById(`btn_${key}`).click();
144
+ break;
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ document.addEventListener('keypress', shortcuts, false);
151
+ </script>
152
+ """
153
+
154
+ # JavaScript for handling text selection
155
+ select_js = """
156
+ function(x) {
157
+ const element = document.getElementById('annotated-text');
158
+ if (!element) {
159
+ console.log("Element not found");
160
+ return [0, 0];
161
+ }
162
+
163
+ const selection = window.getSelection();
164
+ if (!selection.rangeCount) {
165
+ console.log("No selection found");
166
+ return [0, 0];
167
+ }
168
+
169
+ const range = selection.getRangeAt(0);
170
+
171
+ if (!element.contains(range.commonAncestorContainer)) {
172
+ console.log("Selection not within element");
173
+ return [0, 0];
174
+ }
175
+
176
+ const start = getCharacterOffset(element, range.startContainer, range.startOffset);
177
+ const end = getCharacterOffset(element, range.endContainer, range.endOffset);
178
+
179
+
180
+ console.log("Selection:", start, end);
181
+ return [start, end];
182
+
183
+
184
+ function getCharacterOffset(root, node, offset) {
185
+ let currentOffset = 0;
186
+
187
+ const iterator = document.createNodeIterator(root, NodeFilter.SHOW_TEXT);
188
+ let currentNode;
189
+
190
+ while (currentNode = iterator.nextNode()) {
191
+ if (currentNode === node) {
192
+ return currentOffset + offset;
193
+ }
194
+ currentOffset += currentNode.textContent.length;
195
+ }
196
+ return currentOffset;
197
+ }
198
+ }
199
+ """
200
+
201
+ css="""
202
+ .category-legend {border:1px dashed black;}
203
+ .text-sm {font-size: 1.5rem; line-height: 200%;}
204
+ .gr-sample-textbox {width: 1000px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
205
+ .gallery {width: 1000px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
206
+ .text-limit label textarea {height: 150px !important; overflow: scroll; }
207
+ .text-gray-500 {color: #111827; font-weight: 600; font-size: 1.25em; margin-top: 1.6em; margin-bottom: 0.6em; 'line-height: 1.6;}
208
+ #sum-out {border: 2px solid #007bff; padding: 20px; border-radius: 10px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); max-height: 300px; overflow-y: auto;}
209
+ #annotated-text { font-family: monospace; padding: 10px; border: 1px solid #ccc; }
210
+ #refresh_btn { max-width: 20px; max-height: 40px;}
211
+ .locked-input { background-color: #f0f0f0; border: 1px solid #ccc; pointer-events: none; }
212
+ body {
213
+ --text-sm: 12px;
214
+ --text-md: 16px;
215
+ --text-lg: 18px;
216
+ --input-text-size: 16px;
217
+ --section-text-size: 16px;
218
+ --input-background: --neutral-50;
219
+ }
220
+ """