fulviodeo commited on
Commit
e78ee02
·
1 Parent(s): ead64a0

Fixed custom query box bug

Browse files
notebooks/PopulationHealthScreener.ipynb CHANGED
@@ -11,7 +11,7 @@
11
  }
12
  },
13
  "outputs": [],
14
- "source": "import base64\nfrom datetime import date\nfrom dateutil.relativedelta import relativedelta\nimport ipywidgets as widgets\nfrom IPython.display import Javascript\nfrom IPython.utils.capture import capture_output\nfrom src.utils.app_utils import silence_libraries, exec_script\nfrom src.utils.prediction_table import PredictionTable\nfrom src.utils.ui_elements import (\n inject_styles, BOX_WIDTH, QUERY_WIDTH, make_button, make_separator, make_spacer,\n make_section_header, RunButton, error_html, QuerySelectorUI,\n upload_button_html, make_upload_widgets, make_upload_js,\n download_link, download_link_combined, download_link_predictions,\n)\nsilence_libraries()\nwith capture_output():\n from src.handlers.model_wrapper import available_models\ninject_styles()\narticles_table = None; selected_model = None; model_loaded = False\nmodel_dropdown = widgets.Dropdown(options=[m.name for m in available_models], value=available_models[0].name, description='', layout=widgets.Layout(width=BOX_WIDTH))\nquery_selector = widgets.Text(value='paper', layout=widgets.Layout(display='none'))\n_qs_ui = QuerySelectorUI(query_selector)\ndisplay(Javascript(_qs_ui.make_init_js()))\nquery_input = widgets.Textarea(placeholder='Enter PubMed query...', disabled=True,\n layout=widgets.Layout(width=QUERY_WIDTH, height='60px'))\nquery_selector.observe(lambda c: setattr(query_input, 'disabled', c.new == 'paper'), names='value')\n_lbl = \"text-align:right;display:inline-block;width:75px;font-size:13px;line-height:28px;padding-right:6px;\"\nstart_date = widgets.DatePicker(value=date.today() - relativedelta(months=1), description='')\nend_date = widgets.DatePicker(value=date.today(), description='')\nrecall_target = widgets.Dropdown(options=[('95%', 95), ('90%', 90), ('80%', 80), ('70%', 70), ('60%', 60)],\n value=95, description='', disabled=True, layout=widgets.Layout(width='120px'))\nload_run = RunButton('Load', executing_msg='Loading...', error_label='Model')\nsearch_run = RunButton('Search', executing_msg='Searching...', error_label='Search')\npredict_run = RunButton('Screen articles', executing_msg='Screening...', error_label='Screening')\npredict_run.btn.disabled = True\n_reset_btn = widgets.Button(description='\\u21ba',\n layout=widgets.Layout(width='32px', height='28px', display='none'))\nunload_btn = make_button('Unload', width='100px'); unload_btn.layout.display = 'none'\n_upload_uid, _upload_data, upload_btn_widget = make_upload_widgets()\ndisplay(Javascript(make_upload_js(_upload_uid, _upload_data)))\narticles_status = widgets.HTML(value=''); load_file_error = widgets.HTML(value='')\n_screen_progress = widgets.IntProgress(value=0, min=0, max=1)\n_screen_progress.observe(lambda c: predict_run.btn.disabled and setattr(\n predict_run.btn, 'description', f'Screening... {c.new}/{_screen_progress.max}'), names='value')\ndef _set_search_disabled(d):\n query_selector.disabled = d; _qs_ui.refresh(d)\n query_input.disabled = d or (query_selector.value == 'paper')\n start_date.disabled = end_date.disabled = search_run.btn.disabled = d\ndef _set_load_disabled(d):\n upload_btn_widget.value = upload_button_html('disabled' if d else 'upload', _upload_uid)\ndef _update_predict():\n r = model_loaded and articles_table is not None\n recall_target.disabled = predict_run.btn.disabled = not r\ndef _set_articles(table, name):\n global articles_table\n articles_table = table\n articles_status.value = f\"<span style='color:#28a745;font-size:13px;line-height:28px;margin-right:10px;'>Loaded {name}</span>\"\n unload_btn.layout.display = ''; _update_predict()\ndef _unset_articles():\n global articles_table\n articles_table = None; articles_status.value = ''\n unload_btn.layout.display = 'none'; _update_predict()\ndef run_load_model(b):\n global model_loaded, selected_model\n load_run.start()\n try:\n with capture_output():\n ns = exec_script('src/choose_model.py', {'model_dropdown': model_dropdown})\n selected_model = ns['selected_model']; model_loaded = True\n load_run.done('\\u2713 Model loaded'); _update_predict()\n except Exception as e:\n model_loaded = False; load_run.fail(e)\ndef run_search(b):\n search_run.start(); saved = (start_date.value, end_date.value)\n _set_search_disabled(True); start_date.value, end_date.value = saved\n try:\n with capture_output():\n ns = exec_script('src/search_pubmed.py', {\n 'query_selector': query_selector, 'query_input': query_input,\n 'start_date': start_date, 'end_date': end_date, 'model_dropdown': model_dropdown,\n })\n search_run.done('\\u2713 Search completed'); _set_load_disabled(True)\n _set_articles(ns['articles_table'], 'Articles.csv')\n search_run.out.value = download_link(ns['articles_table'].df, 'Articles')\n except Exception as e:\n _set_search_disabled(False); search_run.fail(e)\ndef on_file_uploaded(change):\n if not change.new: return\n load_file_error.value = ''; upload_btn_widget.value = upload_button_html('reading', _upload_uid)\n try:\n name, b64 = change.new.split('|||', 1); content = base64.b64decode(b64)\n if not content: raise ValueError(\"File content is empty \u2014 the upload may not have completed.\")\n table = PredictionTable.from_bytes(content)\n upload_btn_widget.value = upload_button_html('done', _upload_uid)\n _set_articles(table, name); _set_search_disabled(True)\n except Exception as e:\n upload_btn_widget.value = upload_button_html('upload', _upload_uid)\n load_file_error.value = error_html(e)\n finally:\n _upload_data.value = ''\n_upload_data.observe(on_file_uploaded, names='value')\ndef on_unload(b):\n _unset_articles(); _set_search_disabled(False); _set_load_disabled(False)\n search_run.reset(); _reset_btn.layout.display = 'none'; load_file_error.value = ''\ndef run_screen_articles(b):\n predict_run.start(); _reset_btn.layout.display = 'none'; _screen_progress.value = 0\n try:\n with capture_output():\n ns = exec_script('src/screen_articles.py', {\n 'selected_model': selected_model, 'recall_target': recall_target,\n 'articles_table': articles_table, '_screen_progress': _screen_progress,\n '_screen_progress_label': None,\n })\n predict_run.done('\\u2713 Screening completed'); _reset_btn.layout.display = ''\n predict_run.out.value = (\n download_link_combined(ns['review_data'], 'Screened articles') + '<br>' +\n download_link_predictions(ns['data'], 'Screened articles'))\n except Exception as e:\n predict_run.fail(e)\ndef on_predict_reset(b):\n predict_run.reset(); _reset_btn.layout.display = 'none'; _update_predict()\n_reset_btn.on_click(on_predict_reset); load_run.on_click(run_load_model); search_run.on_click(run_search)\npredict_run.on_click(run_screen_articles); unload_btn.on_click(on_unload)\n_M = widgets.Layout(margin='0 0 0 12px')\ndisplay(\n widgets.HTML(\"<h2 style='margin:8px 0 12px 12px;font-size:22px;font-weight:bold;'>PopulationHealthScreener</h2>\"),\n widgets.VBox([make_section_header('Choose a model'), model_dropdown, make_spacer('20px'), load_run.row], layout=_M),\n make_separator(),\n widgets.VBox([widgets.HBox([\n widgets.VBox([make_section_header('Search PubMed'), make_spacer('8px'),\n query_selector, _qs_ui.widget, make_spacer('2px'), query_input, make_spacer('10px'),\n widgets.HBox([widgets.HTML(f\"<span style='{_lbl}'>Start date:</span>\"), start_date]),\n widgets.HBox([widgets.HTML(f\"<span style='{_lbl}'>End date:</span>\"), end_date]),\n make_spacer('12px'), search_run.row]),\n widgets.HBox([widgets.HTML(\"<span style='font-size:16px;'>or</span>\"),\n widgets.VBox([make_section_header('Load articles from a file'), make_spacer('8px'),\n widgets.HBox([upload_btn_widget, load_file_error]), _upload_data],\n layout=widgets.Layout(margin='0 0 0 20px'))],\n layout=widgets.Layout(align_items='flex-start', margin='0 0 0 24px'))],\n layout=widgets.Layout(align_items='flex-start')),\n make_spacer('12px'), widgets.HBox([articles_status, unload_btn])], layout=_M),\n make_separator(),\n widgets.VBox([\n widgets.HBox([make_section_header('Screen articles'), _reset_btn], layout=widgets.Layout(align_items='center')),\n make_spacer('8px'),\n widgets.HBox([widgets.HTML(\"<span style='font-size:13px;line-height:28px;margin-right:8px;'>Target recall</span>\"),\n recall_target,\n widgets.HTML(\"<span style='font-size:13px;color:gray;margin-left:12px;line-height:28px;'>Based on the recall achieved in previous testing; the higher the recall, the more inclusive the model</span>\")],\n layout=widgets.Layout(align_items='center')),\n make_spacer('12px'), predict_run.row], layout=_M),\n)\n"
15
  },
16
  {
17
  "cell_type": "code",
 
11
  }
12
  },
13
  "outputs": [],
14
+ "source": "import base64\nfrom datetime import date\nfrom dateutil.relativedelta import relativedelta\nimport ipywidgets as widgets\nfrom IPython.display import Javascript\nfrom IPython.utils.capture import capture_output\nfrom src.utils.app_utils import silence_libraries, exec_script\nfrom src.utils.prediction_table import PredictionTable\nfrom src.utils.ui_elements import (\n inject_styles, BOX_WIDTH, QUERY_WIDTH, make_button, make_separator, make_spacer,\n make_section_header, RunButton, error_html,\n upload_button_html, make_upload_widgets, make_upload_js,\n download_link, download_link_combined, download_link_predictions,\n)\nsilence_libraries()\nwith capture_output():\n from src.handlers.model_wrapper import available_models\ninject_styles()\narticles_table = None; selected_model = None; model_loaded = False\nmodel_dropdown = widgets.Dropdown(options=[m.name for m in available_models], value=available_models[0].name, description='', layout=widgets.Layout(width=BOX_WIDTH))\n_PAPER_URL = 'https://www.thelancet.com/journals/lancet/article/PIIS0140-6736(16)30054-X/abstract'\nquery_selector = widgets.RadioButtons(\n options=[('Use the query from the 2016 paper by the NCD-RisC', 'paper'),\n ('Use a custom query', 'custom')],\n value='paper', description='', style={'description_width': '0px'},\n layout=widgets.Layout(width='auto'))\n_paper_link = widgets.HTML(\n value=f\"<a href='{_PAPER_URL}' target='_blank' style='font-size:13px;color:inherit;text-decoration:underline;'>&#8599;</a>\",\n layout=widgets.Layout(margin='4px 0 0 4px'))\n_qs_widget = widgets.HBox([query_selector, _paper_link], layout=widgets.Layout(align_items='flex-start'))\nquery_input = widgets.Textarea(placeholder='Enter PubMed query...', disabled=True,\n layout=widgets.Layout(width=QUERY_WIDTH, height='60px'))\nquery_selector.observe(lambda c: setattr(query_input, 'disabled', c.new == 'paper'), names='value')\n_lbl = \"text-align:right;display:inline-block;width:75px;font-size:13px;line-height:28px;padding-right:6px;\"\nstart_date = widgets.DatePicker(value=date.today() - relativedelta(months=1), description='')\nend_date = widgets.DatePicker(value=date.today(), description='')\nrecall_target = widgets.Dropdown(options=[('95%', 95), ('90%', 90), ('80%', 80), ('70%', 70), ('60%', 60)],\n value=95, description='', disabled=True, layout=widgets.Layout(width='120px'))\nload_run = RunButton('Load', executing_msg='Loading...', error_label='Model')\nsearch_run = RunButton('Search', executing_msg='Searching...', error_label='Search')\npredict_run = RunButton('Screen articles', executing_msg='Screening...', error_label='Screening')\npredict_run.btn.disabled = True\n_reset_btn = widgets.Button(description='\\u21ba',\n layout=widgets.Layout(width='32px', height='28px', display='none'))\nunload_btn = make_button('Unload', width='100px'); unload_btn.layout.display = 'none'\n_upload_uid, _upload_data, upload_btn_widget = make_upload_widgets()\ndisplay(Javascript(make_upload_js(_upload_uid, _upload_data)))\narticles_status = widgets.HTML(value=''); load_file_error = widgets.HTML(value='')\n_screen_progress = widgets.IntProgress(value=0, min=0, max=1)\n_screen_progress.observe(lambda c: predict_run.btn.disabled and setattr(\n predict_run.btn, 'description', f'Screening... {c.new}/{_screen_progress.max}'), names='value')\ndef _set_search_disabled(d):\n query_selector.disabled = d\n _paper_link.value = (f\"<a href='{_PAPER_URL}' target='_blank' style='font-size:13px;\"\n f\"color:inherit;text-decoration:underline;{'opacity:0.6;pointer-events:none;' if d else ''}'>&#8599;</a>\")\n query_input.disabled = d or (query_selector.value == 'paper')\n start_date.disabled = end_date.disabled = search_run.btn.disabled = d\ndef _set_load_disabled(d):\n upload_btn_widget.value = upload_button_html('disabled' if d else 'upload', _upload_uid)\ndef _update_predict():\n r = model_loaded and articles_table is not None\n recall_target.disabled = predict_run.btn.disabled = not r\ndef _set_articles(table, name):\n global articles_table\n articles_table = table\n articles_status.value = f\"<span style='color:#28a745;font-size:13px;line-height:28px;margin-right:10px;'>Loaded {name}</span>\"\n unload_btn.layout.display = ''; _update_predict()\ndef _unset_articles():\n global articles_table\n articles_table = None; articles_status.value = ''\n unload_btn.layout.display = 'none'; _update_predict()\ndef run_load_model(b):\n global model_loaded, selected_model\n load_run.start()\n try:\n with capture_output():\n ns = exec_script('src/choose_model.py', {'model_dropdown': model_dropdown})\n selected_model = ns['selected_model']; model_loaded = True\n load_run.done('\\u2713 Model loaded'); _update_predict()\n except Exception as e:\n model_loaded = False; load_run.fail(e)\ndef run_search(b):\n search_run.start(); saved = (start_date.value, end_date.value)\n _set_search_disabled(True); start_date.value, end_date.value = saved\n try:\n with capture_output():\n ns = exec_script('src/search_pubmed.py', {\n 'query_selector': query_selector, 'query_input': query_input,\n 'start_date': start_date, 'end_date': end_date, 'model_dropdown': model_dropdown,\n })\n search_run.done('\\u2713 Search completed'); _set_load_disabled(True)\n _set_articles(ns['articles_table'], 'Articles.csv')\n search_run.out.value = download_link(ns['articles_table'].df, 'Articles')\n except Exception as e:\n _set_search_disabled(False); search_run.fail(e)\ndef on_file_uploaded(change):\n if not change.new: return\n load_file_error.value = ''; upload_btn_widget.value = upload_button_html('reading', _upload_uid)\n try:\n name, b64 = change.new.split('|||', 1); content = base64.b64decode(b64)\n if not content: raise ValueError(\"File content is empty \u2014 the upload may not have completed.\")\n table = PredictionTable.from_bytes(content)\n upload_btn_widget.value = upload_button_html('done', _upload_uid)\n _set_articles(table, name); _set_search_disabled(True)\n except Exception as e:\n upload_btn_widget.value = upload_button_html('upload', _upload_uid)\n load_file_error.value = error_html(e)\n finally:\n _upload_data.value = ''\n_upload_data.observe(on_file_uploaded, names='value')\ndef on_unload(b):\n _unset_articles(); _set_search_disabled(False); _set_load_disabled(False)\n search_run.reset(); _reset_btn.layout.display = 'none'; load_file_error.value = ''\ndef run_screen_articles(b):\n predict_run.start(); _reset_btn.layout.display = 'none'; _screen_progress.value = 0\n try:\n with capture_output():\n ns = exec_script('src/screen_articles.py', {\n 'selected_model': selected_model, 'recall_target': recall_target,\n 'articles_table': articles_table, '_screen_progress': _screen_progress,\n '_screen_progress_label': None,\n })\n predict_run.done('\\u2713 Screening completed'); _reset_btn.layout.display = ''\n predict_run.out.value = (\n download_link_combined(ns['review_data'], 'Screened articles') + '<br>' +\n download_link_predictions(ns['data'], 'Screened articles'))\n except Exception as e:\n predict_run.fail(e)\ndef on_predict_reset(b):\n predict_run.reset(); _reset_btn.layout.display = 'none'; _update_predict()\n_reset_btn.on_click(on_predict_reset); load_run.on_click(run_load_model); search_run.on_click(run_search)\npredict_run.on_click(run_screen_articles); unload_btn.on_click(on_unload)\n_M = widgets.Layout(margin='0 0 0 12px')\ndisplay(\n widgets.HTML(\"<h2 style='margin:8px 0 12px 12px;font-size:22px;font-weight:bold;'>PopulationHealthScreener</h2>\"),\n widgets.VBox([make_section_header('Choose a model'), model_dropdown, make_spacer('20px'), load_run.row], layout=_M),\n make_separator(),\n widgets.VBox([widgets.HBox([\n widgets.VBox([make_section_header('Search PubMed'), make_spacer('8px'),\n _qs_widget, make_spacer('2px'), query_input, make_spacer('10px'),\n widgets.HBox([widgets.HTML(f\"<span style='{_lbl}'>Start date:</span>\"), start_date]),\n widgets.HBox([widgets.HTML(f\"<span style='{_lbl}'>End date:</span>\"), end_date]),\n make_spacer('12px'), search_run.row]),\n widgets.HBox([widgets.HTML(\"<span style='font-size:16px;'>or</span>\"),\n widgets.VBox([make_section_header('Load articles from a file'), make_spacer('8px'),\n widgets.HBox([upload_btn_widget, load_file_error]), _upload_data],\n layout=widgets.Layout(margin='0 0 0 20px'))],\n layout=widgets.Layout(align_items='flex-start', margin='0 0 0 24px'))],\n layout=widgets.Layout(align_items='flex-start')),\n make_spacer('12px'), widgets.HBox([articles_status, unload_btn])], layout=_M),\n make_separator(),\n widgets.VBox([\n widgets.HBox([make_section_header('Screen articles'), _reset_btn], layout=widgets.Layout(align_items='center')),\n make_spacer('8px'),\n widgets.HBox([widgets.HTML(\"<span style='font-size:13px;line-height:28px;margin-right:8px;'>Target recall</span>\"),\n recall_target,\n widgets.HTML(\"<span style='font-size:13px;color:gray;margin-left:12px;line-height:28px;'>Based on the recall achieved in previous testing; the higher the recall, the more inclusive the model</span>\")],\n layout=widgets.Layout(align_items='center')),\n make_spacer('12px'), predict_run.row], layout=_M),\n)\n"
15
  },
16
  {
17
  "cell_type": "code",
notebooks/src/utils/ui_elements.py CHANGED
@@ -160,84 +160,6 @@ class RunButton:
160
  self.out.value = ''
161
 
162
 
163
- class QuerySelectorUI:
164
- """
165
- Styled HTML radio group that mirrors a hidden Dropdown widget.
166
-
167
- The hidden Dropdown is the Python source of truth. JS syncs radio clicks
168
- back into the widget model via the comm channel (reliable in Voila).
169
-
170
- Usage:
171
- qs_ui = QuerySelectorUI(query_selector, radio_name)
172
- display(qs_ui.widget)
173
- qs_ui.refresh(disabled=True) # re-render after state change
174
- """
175
-
176
- _PAPER_URL = (
177
- 'https://www.thelancet.com/journals/lancet/article/'
178
- 'PIIS0140-6736(16)30054-X/abstract'
179
- )
180
-
181
- def __init__(self, query_selector, radio_name=None):
182
- self._qs = query_selector
183
- # Each radio group on the page needs a unique name so the browser keeps them independent.
184
- # Generate one automatically if the caller doesn't supply it.
185
- self._rname = radio_name or f"qs_{uuid.uuid4().hex[:8]}"
186
- self._qid = query_selector.model_id
187
- self.widget = widgets.HTML(value=self._render())
188
-
189
- def make_init_js(self):
190
- """Return JS that registers window._lsa_qs_<rname> as a global function."""
191
- return f"""
192
- window._lsa_qs_{self._rname} = function(val) {{
193
- requirejs(['@jupyter-widgets/base'], function(base) {{
194
- var mgr = Object.values(base.ManagerBase._managers || {{}})[0];
195
- if (!mgr) return;
196
- mgr.get_model('{self._qid}').then(function(model) {{
197
- model.set('value', val);
198
- model.save_changes();
199
- }}).catch(function(e) {{ console.error('lsa_qs error', e); }});
200
- }});
201
- }};
202
- """
203
-
204
- def _js_sync(self, val):
205
- """Inline JS that calls the pre-registered global handler."""
206
- return f"window._lsa_qs_{self._rname}('{val}')"
207
-
208
- def _render(self, disabled=False):
209
- dis = 'disabled' if disabled else ''
210
- opacity = 'opacity:0.6;' if disabled else ''
211
- cursor = 'default' if disabled else 'pointer'
212
- p_chk = 'checked' if self._qs.value == 'paper' else ''
213
- c_chk = 'checked' if self._qs.value == 'custom' else ''
214
- return (
215
- f"<div style='font-size:13px;line-height:2.2;{opacity}'>"
216
- f"<label style='cursor:{cursor};display:block;'>"
217
- f"<input type='radio' name='{self._rname}' value='paper' {p_chk} {dis}"
218
- f" onchange=\"{self._js_sync('paper')}\">"
219
- f"&nbsp;Use the query from the "
220
- f"<a href='{self._PAPER_URL}' target='_blank'"
221
- f" style='color:inherit;text-decoration:underline;'>"
222
- f"2016 paper by the NCD-RisC</a>"
223
- f"</label>"
224
- f"<label style='cursor:{cursor};display:block;'>"
225
- f"<input type='radio' name='{self._rname}' value='custom' {c_chk} {dis}"
226
- f" onchange=\"{self._js_sync('custom')}\">"
227
- f"&nbsp;Use a custom query"
228
- f"</label>"
229
- f"</div>"
230
- )
231
-
232
- def refresh(self, disabled=False):
233
- """
234
- Re-render the radio group to reflect the current dropdown value.
235
-
236
- Pass disabled=True to grey out the radios after an operation locks the
237
- query source (e.g. while a search is running or after one completes).
238
- """
239
- self.widget.value = self._render(disabled)
240
-
241
 
242
  _BTN_BASE = (
243
  f"display:inline-flex;align-items:center;"
 
160
  self.out.value = ''
161
 
162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
  _BTN_BASE = (
165
  f"display:inline-flex;align-items:center;"