fulviodeo commited on
Commit
facae1f
Β·
1 Parent(s): 9b364cf

Updated colours.

Browse files

Updated query box height and behaviour.

notebooks/PopulationHealthScreener.ipynb CHANGED
@@ -11,7 +11,7 @@
11
  }
12
  },
13
  "outputs": [],
14
- "source": "# imports\nimport base64\nfrom datetime import date\nfrom dateutil.relativedelta import relativedelta\nimport ipywidgets as widgets\nfrom IPython.utils.capture import capture_output\nfrom src.utils.app_utils import silence_libraries, exec_script\nfrom src.handlers.prediction_handler import PredictionHandler\nfrom src.handlers.error_handler import AppError\nfrom src.utils.ui_elements import (\n inject_styles, BOX_WIDTH, BUTTON_WIDTH, QUERY_WIDTH, make_separator, make_spacer,\n make_section_header_with_info, Tutorial, RunButton, error_html, AppColour,\n download_link, download_combined_link, download_predictions_link,\n)\nsilence_libraries()\nwith capture_output():\n from src.handlers.model_wrapper import available_models\ninject_styles()\ntutorial = Tutorial()\n\n_DARK_GREY = AppColour.DARK_GREY.value\n_GREY = AppColour.GREY.value\n_LIGHT_GREY = AppColour.LIGHT_GREY.value\n_ORANGE = AppColour.ORANGE.value\n_GREEN = AppColour.GREEN.value\n\n_SP_XS = '4px' # between target recall and expected, and between date pickers\n_SP_S = '8px' # everywhere else within a section\n_SP_M = '12px' # end of section β†’ separator\n_SP_L = '16px' # title β†’ first section, and separator β†’ next section\n\n# app state\narticles_table = None; selected_model = None; model_loaded = False\n\n# model section\nmodel_dropdown = widgets.Dropdown(\n options=[m.name for m in available_models], value=available_models[0].name,\n description='', layout=widgets.Layout(width='262px'))\n\n# query section\n_PAPER_URL = 'https://www.thelancet.com/journals/lancet/article/PIIS0140-6736(16)30054-X/abstract'\n_setting_paper_query = False # flag to suppress uncheck when we programmatically set the query\n_use_paper_query_btn = widgets.Checkbox(\n value=False, description='Use the query from the 2016 paper by the NCD-RisC',\n indent=False, layout=widgets.Layout(width='auto'))\n_use_paper_query_btn.add_class('grey-checkbox')\n_paper_link = widgets.HTML(\n value=f\"<a href='{_PAPER_URL}' target='_blank' style='font-size:13px;color:{_DARK_GREY};text-decoration:underline;'>&#8599;</a>\",\n layout=widgets.Layout(margin='0 0 0 4px'))\n_qs_row = widgets.HBox([_use_paper_query_btn, _paper_link], layout=widgets.Layout(align_items='center'))\nquery_input = widgets.Textarea(\n placeholder='Enter PubMed query...',\n layout=widgets.Layout(width=QUERY_WIDTH, height='145px'))\n\n# date pickers\n_lbl = f\"text-align:right;display:inline-block;width:75px;font-size:13px;line-height:28px;padding-right:6px;color:{_DARK_GREY};\"\nstart_date = widgets.DatePicker(value=date.today() - relativedelta(months=1), description='', layout=widgets.Layout(width='144px'))\nend_date = widgets.DatePicker(value=date.today(), description='', layout=widgets.Layout(width='144px'))\n\n# action buttons\nrecall_target = widgets.Dropdown(\n options=[('95%', 95), ('90%', 90), ('80%', 80), ('70%', 70), ('60%', 60)],\n value=95, description='', layout=widgets.Layout(width='140px'))\n_WORKS_SAVED = {60: 85.5, 70: 82.2, 80: 78.5, 90: 71.3, 95: 65.7}\n_PRECISION = {60: 83.4, 70: 79.4, 80: 75.5, 90: 63.5, 95: 56.1}\n_recall_stats = widgets.HTML()\n_upload_success = widgets.HTML()\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(\n description='\\u21ba', layout=widgets.Layout(width='28px', height='28px', display='none'))\n_reset_btn.add_class('circular-btn')\nunload_btn = widgets.Button(description='β†Ί', layout=widgets.Layout(width='28px', height='28px', display='none'))\nunload_btn.add_class('circular-btn')\n_upload_refresh_btn = widgets.Button(description='β†Ί', layout=widgets.Layout(width='28px', height='28px', display='none'))\n_upload_refresh_btn.add_class('circular-btn')\n_upload_err_btn = widgets.Button(description='β†Ί', layout=widgets.Layout(width='28px', height='28px', display='none'))\n_upload_err_btn.add_class('circular-btn')\n_upload_err_btn.add_class('error-reload-btn')\n\n# upload β€” custom HTML file button\n# Strategy: JS dispatches a native DOM 'change' event on a hidden Text widget's <input>.\n# ipywidgets' Text frontend listens to 'change' on its <input> and syncs to Python.\n# No require(), no widget manager access needed.\n_upload_pipe = widgets.Text(value='')\n_pipe_box = widgets.VBox(\n [_upload_pipe],\n layout=widgets.Layout(height='0px', overflow='hidden', margin='0px', padding='0px'))\n_pipe_box.add_class('_up_pipe')\n\n_UID = str(id(_upload_pipe)) # unique int for stable DOM element IDs\n_BTN = (f\"width:{BUTTON_WIDTH};font-weight:bold;font-size:15px;padding-left:12px;\"\n f\"border:none;text-align:left;height:32px;border-radius:2px;\")\n\ndef _make_upload_html(state='idle'):\n if state == 'done':\n return (f\"<button style='{_BTN}background:{_GREEN};color:white;cursor:default;'>\"\n f\"\\u2713 Upload completed</button>\")\n if state == 'disabled':\n return (f\"<button style='{_BTN}background:{_LIGHT_GREY};color:{_GREY};cursor:not-allowed;'>\"\n f\"Upload</button>\")\n # idle: file input + button; JS dispatches change event on the hidden Text widget's <input>\n return (\n f\"<input type='file' id='_fi{_UID}' accept='.csv' style='display:none'\"\n f\" onchange=\\\"(function(inp){{\"\n f\"if(!inp.files.length)return;\"\n f\"var r=new FileReader();\"\n f\"r.onload=function(e){{\"\n f\"var b64=e.target.result.split(',')[1];\"\n f\"inp.value='';\"\n f\"var pi=document.querySelector('._up_pipe input[type=text]');\"\n f\"if(pi){{pi.value=b64;pi.dispatchEvent(new Event('change',{{bubbles:true}}));}}\"\n f\"else{{console.error('pipe not found');}}\"\n f\"}};\"\n f\"r.readAsDataURL(inp.files[0]);\"\n f\"}})(this)\\\">\"\n f\"<button onclick=\\\"document.getElementById('_fi{_UID}').click()\\\"\"\n f\" style='{_BTN}background:{_ORANGE};color:white;cursor:pointer;'>Upload</button>\"\n )\n\nupload_widget = widgets.HTML(value=_make_upload_html())\n\n# status / progress\nload_file_error = widgets.HTML(value='')\n_screen_progress = widgets.IntProgress(value=0, min=0, max=1)\n_screen_progress.observe(\n lambda c: predict_run.btn.disabled and setattr(\n predict_run.btn, 'description', f'Screening... {c.new}/{_screen_progress.max}'),\n names='value')\n\n# criteria link for screen articles section\n_CRITERIA_URL = 'https://github.com/NCD-RisC/ncdrisc-methods/blob/main/Inclusion%20Exclusion%20criteria%20Protocol.pdf'\n_criteria_link = widgets.HTML(\n value=f\"<a href='{_CRITERIA_URL}' target='_blank' style='font-size:13px;color:{_DARK_GREY};text-decoration:underline;margin-left:8px;'>Criteria &#8599;</a>\",\n layout=widgets.Layout(margin='0 0 0 4px'))\n\n# state helpers\n\ndef _set_search_disabled(d):\n _use_paper_query_btn.disabled = d\n _paper_link.value = (f\"<a href='{_PAPER_URL}' target='_blank' style='font-size:13px;color:{_DARK_GREY};\"\n f\"text-decoration:underline;{'opacity:0.4;pointer-events:none;' if d else ''}'>&#8599;</a>\")\n query_input.disabled = d\n start_date.disabled = end_date.disabled = search_run.btn.disabled = d\n\ndef _set_load_disabled(d):\n upload_widget.value = _make_upload_html('disabled' if d else 'idle')\n\ndef _update_predict():\n r = model_loaded and articles_table is not None\n predict_run.btn.disabled = not r\n\ndef _update_recall_stats(change=None):\n v = recall_target.value\n ws = round(_WORKS_SAVED[v])\n pr = round(_PRECISION[v])\n _recall_stats.value = (\n f\"<div style='font-size:12px;color:{_DARK_GREY};line-height:1.8;'>\"\n f\"Expected <b>{ws}%</b> reduction in number of articles needing human review<br>\"\n f\"Expected <b>{pr}%</b> of screened articles to be relevant\"\n f\"</div>\"\n )\nrecall_target.observe(_update_recall_stats, names='value')\n_update_recall_stats()\n\ndef _set_articles(table):\n global articles_table\n articles_table = table\n _update_predict()\n\ndef _unset_articles():\n global articles_table\n articles_table = None\n unload_btn.layout.display = 'none'; _upload_refresh_btn.layout.display = 'none'\n _update_predict()\n\n# event handlers\n\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)\n\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_input': query_input,\n 'start_date': start_date, 'end_date': end_date,\n })\n search_run.done('\\u2713 Search completed'); _set_load_disabled(True)\n _set_articles(ns['articles_table'])\n search_run.out.value = (\n download_link(ns['articles_table'], 'Articles') +\n f\"<div style='font-size:12px;color:{_DARK_GREY};margin-left:12px;'>\"\n \"Articles loaded and ready to be screened</div>\")\n unload_btn.layout.display = ''\n except Exception as e:\n _set_search_disabled(False); search_run.fail(e)\n\ndef on_file_uploaded(change):\n if not change.new: return\n _upload_pipe.value = '' # reset so same file can be re-uploaded\n load_file_error.value = ''; _upload_success.value = ''; _upload_err_btn.layout.display = 'none'\n try:\n raw = base64.b64decode(change.new)\n table = PredictionHandler.from_bytes(raw)\n upload_widget.value = _make_upload_html('done')\n _set_articles(table); _set_search_disabled(True)\n _upload_refresh_btn.layout.display = ''\n _upload_success.value = (f\"<div style='font-size:12px;color:{_DARK_GREY};'>\"\n \"Articles loaded and ready to be screened</div>\")\n except BaseException as e:\n upload_widget.value = _make_upload_html('idle')\n load_file_error.value = error_html(e)\n _upload_err_btn.layout.display = ''\n_upload_pipe.observe(on_file_uploaded, names='value')\n\ndef on_unload(b):\n _unset_articles()\n saved = (start_date.value, end_date.value, query_input.value)\n _set_search_disabled(False)\n start_date.value, end_date.value, query_input.value = saved\n _set_load_disabled(False)\n search_run.reset(); _reset_btn.layout.display = 'none'\n load_file_error.value = ''; _upload_err_btn.layout.display = 'none'\n _upload_success.value = ''\n\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 unload_btn.disabled = True; _upload_refresh_btn.disabled = True\n predict_run.out.value = (\n download_combined_link(PredictionHandler(ns['review_data']), 'Articles identified after screening') +\n download_predictions_link(PredictionHandler(ns['data']), 'Results for all articles'))\n except Exception as e:\n predict_run.fail(e)\n\ndef on_predict_reset(b):\n predict_run.reset(); _reset_btn.layout.display = 'none'\n unload_btn.disabled = False; _upload_refresh_btn.disabled = False\n _update_predict()\n\ndef on_use_paper_query(change):\n global _setting_paper_query\n if not change['new']: return\n wrapper = next(m for m in available_models if m.name == model_dropdown.value)\n _setting_paper_query = True\n query_input.value = wrapper.default_query\n _setting_paper_query = False\n\ndef on_query_changed(change):\n if _setting_paper_query: return\n if _use_paper_query_btn.value:\n _use_paper_query_btn.value = False\n\n_use_paper_query_btn.observe(on_use_paper_query, names='value')\nquery_input.observe(on_query_changed, names='value')\n\n# wire up\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); _upload_refresh_btn.on_click(on_unload)\n_upload_err_btn.on_click(lambda b: (setattr(load_file_error, 'value', ''),\n setattr(_upload_err_btn.layout, 'display', 'none')))\n\n# layout\n_M = widgets.Layout(margin='0 0 0 12px')\ndisplay(\n tutorial.overlay,\n widgets.HTML(\n f\"<h2 style='margin:0 0 0 12px;font-size:22px;font-weight:bold;'>\"\n f\"<span style='color:{_DARK_GREY};'>PopulationHealth</span>\"\n f\"<span style='color:{_ORANGE};'>Screener</span>\"\n f\"</h2>\"\n ),\n make_spacer(_SP_L), # L: title β†’ first section\n widgets.VBox([\n make_section_header_with_info('Choose a model', tutorial, 0),\n make_spacer(_SP_S),\n model_dropdown,\n make_spacer(_SP_S),\n load_run.row,\n ], layout=_M),\n make_spacer(_SP_M), # M: end of section β†’ separator\n make_separator(),\n make_spacer(_SP_L), # L: separator β†’ next section\n widgets.VBox([\n widgets.HBox([\n widgets.VBox([\n make_section_header_with_info('Search PubMed', tutorial, 1),\n make_spacer(_SP_S),\n query_input,\n make_spacer(_SP_XS),\n _qs_row,\n make_spacer(_SP_S),\n widgets.HBox([widgets.HTML(f\"<span style='{_lbl}'>Start date:</span>\"), start_date]),\n make_spacer(_SP_XS),\n widgets.HBox([widgets.HTML(f\"<span style='{_lbl}'>End date:</span>\"), end_date]),\n make_spacer(_SP_S),\n widgets.VBox([widgets.HBox([search_run.btn, search_run.err_reload, unload_btn]), search_run.out]),\n ]),\n widgets.HBox([\n widgets.HTML(f\"<span style='font-size:16px;color:{_DARK_GREY};'>or</span>\"),\n widgets.VBox([\n make_section_header_with_info('Load articles from a file', tutorial, 1),\n make_spacer(_SP_S),\n widgets.VBox([\n widgets.HBox([upload_widget, _upload_refresh_btn, _upload_err_btn]),\n _pipe_box,\n load_file_error,\n _upload_success,\n ]),\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 ], layout=_M),\n make_spacer(_SP_M), # M: end of section β†’ separator\n make_separator(),\n make_spacer(_SP_L), # L: separator β†’ next section\n widgets.VBox([\n widgets.HBox([make_section_header_with_info('Screen articles', tutorial, 2), _criteria_link],\n layout=widgets.Layout(align_items='center')),\n make_spacer(_SP_S),\n widgets.VBox([\n widgets.HBox([\n widgets.HTML(f\"<span style='font-size:13px;line-height:28px;color:{_DARK_GREY};margin-right:8px;'>Target recall</span>\"),\n recall_target,\n ], layout=widgets.Layout(align_items='center')),\n make_spacer(_SP_XS),\n _recall_stats,\n ]),\n make_spacer(_SP_S),\n widgets.VBox([widgets.HBox([predict_run.btn, predict_run.err_reload, _reset_btn]), predict_run.out]),\n ], layout=_M),\n)\ntutorial.show()"
15
  },
16
  {
17
  "cell_type": "code",
 
11
  }
12
  },
13
  "outputs": [],
14
+ "source": "# imports\nimport base64\nfrom datetime import date\nfrom dateutil.relativedelta import relativedelta\nimport ipywidgets as widgets\nfrom IPython.utils.capture import capture_output\nfrom src.utils.app_utils import silence_libraries, exec_script\nfrom src.handlers.prediction_handler import PredictionHandler\nfrom src.handlers.error_handler import AppError\nfrom src.utils.ui_elements import (\n inject_styles, BOX_WIDTH, BUTTON_WIDTH, QUERY_WIDTH, make_separator, make_spacer,\n make_section_header_with_info, Tutorial, RunButton, error_html, AppColour,\n download_link, download_combined_link, download_predictions_link,\n)\nsilence_libraries()\nwith capture_output():\n from src.handlers.model_wrapper import available_models\ninject_styles()\ntutorial = Tutorial()\n\n_DARK_GREY = AppColour.DARK_GREY.value\n_GREY = AppColour.GREY.value\n_LIGHT_GREY = AppColour.LIGHT_GREY.value\n_ORANGE = AppColour.ORANGE.value\n_GREEN = AppColour.GREEN.value\n\n_SP_XS = '4px' # between target recall and expected, and between date pickers\n_SP_S = '8px' # everywhere else within a section\n_SP_M = '12px' # end of section β†’ separator\n_SP_L = '16px' # title β†’ first section, and separator β†’ next section\n\n# app state\narticles_table = None; selected_model = None; model_loaded = False\n\n# model section\nmodel_dropdown = widgets.Dropdown(\n options=[m.name for m in available_models], value=available_models[0].name,\n description='', layout=widgets.Layout(width='262px'))\n\n# query section\n_PAPER_URL = 'https://www.thelancet.com/journals/lancet/article/PIIS0140-6736(16)30054-X/abstract'\n_setting_paper_query = False # flag to suppress uncheck when we programmatically set the query\n_use_paper_query_btn = widgets.Checkbox(\n value=False, description='Use the query from the 2016 paper by the NCD-RisC',\n indent=False, layout=widgets.Layout(width='auto'))\n_use_paper_query_btn.add_class('grey-checkbox')\n_paper_link = widgets.HTML(\n value=f\"<a href='{_PAPER_URL}' target='_blank' style='font-size:13px;color:{_DARK_GREY};text-decoration:underline;'>&#8599;</a>\",\n layout=widgets.Layout(margin='0 0 0 4px'))\n_qs_row = widgets.HBox([_use_paper_query_btn, _paper_link], layout=widgets.Layout(align_items='center'))\nquery_input = widgets.Textarea(\n placeholder='Enter PubMed query...',\n layout=widgets.Layout(width=QUERY_WIDTH, height='145px'))\n\n# date pickers\n_lbl = f\"text-align:right;display:inline-block;width:75px;font-size:13px;line-height:28px;padding-right:6px;color:{_DARK_GREY};\"\nstart_date = widgets.DatePicker(value=date.today() - relativedelta(months=1), description='', layout=widgets.Layout(width='144px'))\nend_date = widgets.DatePicker(value=date.today(), description='', layout=widgets.Layout(width='144px'))\n\n# action buttons\nrecall_target = widgets.Dropdown(\n options=[('95%', 95), ('90%', 90), ('80%', 80), ('70%', 70), ('60%', 60)],\n value=95, description='', layout=widgets.Layout(width='140px'))\n_WORKS_SAVED = {60: 85.5, 70: 82.2, 80: 78.5, 90: 71.3, 95: 65.7}\n_PRECISION = {60: 83.4, 70: 79.4, 80: 75.5, 90: 63.5, 95: 56.1}\n_recall_stats = widgets.HTML()\n_upload_success = widgets.HTML()\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(\n description='\\u21ba', layout=widgets.Layout(width='28px', height='28px', display='none'))\n_reset_btn.add_class('circular-btn')\nunload_btn = widgets.Button(description='β†Ί', layout=widgets.Layout(width='28px', height='28px', display='none'))\nunload_btn.add_class('circular-btn')\n_upload_refresh_btn = widgets.Button(description='β†Ί', layout=widgets.Layout(width='28px', height='28px', display='none'))\n_upload_refresh_btn.add_class('circular-btn')\n_upload_err_btn = widgets.Button(description='β†Ί', layout=widgets.Layout(width='28px', height='28px', display='none'))\n_upload_err_btn.add_class('circular-btn')\n_upload_err_btn.add_class('error-reload-btn')\n\n# upload β€” custom HTML file button\n# Strategy: JS dispatches a native DOM 'change' event on a hidden Text widget's <input>.\n# ipywidgets' Text frontend listens to 'change' on its <input> and syncs to Python.\n# No require(), no widget manager access needed.\n_upload_pipe = widgets.Text(value='')\n_pipe_box = widgets.VBox(\n [_upload_pipe],\n layout=widgets.Layout(height='0px', overflow='hidden', margin='0px', padding='0px'))\n_pipe_box.add_class('_up_pipe')\n\n_UID = str(id(_upload_pipe)) # unique int for stable DOM element IDs\n_BTN = (f\"width:{BUTTON_WIDTH};font-weight:bold;font-size:15px;padding-left:12px;\"\n f\"border:none;text-align:left;height:32px;border-radius:2px;\")\n\ndef _make_upload_html(state='idle'):\n if state == 'done':\n return (f\"<button style='{_BTN}background:{_GREEN};color:white;cursor:default;'>\"\n f\"\\u2713 Upload completed</button>\")\n if state == 'disabled':\n return (f\"<button style='{_BTN}background:{_LIGHT_GREY};color:{_GREY};cursor:not-allowed;'>\"\n f\"Upload</button>\")\n # idle: file input + button; JS dispatches change event on the hidden Text widget's <input>\n return (\n f\"<input type='file' id='_fi{_UID}' accept='.csv' style='display:none'\"\n f\" onchange=\\\"(function(inp){{\"\n f\"if(!inp.files.length)return;\"\n f\"var r=new FileReader();\"\n f\"r.onload=function(e){{\"\n f\"var b64=e.target.result.split(',')[1];\"\n f\"inp.value='';\"\n f\"var pi=document.querySelector('._up_pipe input[type=text]');\"\n f\"if(pi){{pi.value=b64;pi.dispatchEvent(new Event('change',{{bubbles:true}}));}}\"\n f\"else{{console.error('pipe not found');}}\"\n f\"}};\"\n f\"r.readAsDataURL(inp.files[0]);\"\n f\"}})(this)\\\">\"\n f\"<button onclick=\\\"document.getElementById('_fi{_UID}').click()\\\"\"\n f\" style='{_BTN}background:{_ORANGE};color:white;cursor:pointer;'>Upload</button>\"\n )\n\nupload_widget = widgets.HTML(value=_make_upload_html())\n\n# status / progress\nload_file_error = widgets.HTML(value='')\n_screen_progress = widgets.IntProgress(value=0, min=0, max=1)\n_screen_progress.observe(\n lambda c: predict_run.btn.disabled and setattr(\n predict_run.btn, 'description', f'Screening... {c.new}/{_screen_progress.max}'),\n names='value')\n\n# criteria link for screen articles section\n_CRITERIA_URL = 'https://github.com/NCD-RisC/ncdrisc-methods/blob/main/Inclusion%20Exclusion%20criteria%20Protocol.pdf'\n_criteria_link = widgets.HTML(\n value=f\"<a href='{_CRITERIA_URL}' target='_blank' style='font-size:13px;color:{_DARK_GREY};text-decoration:underline;margin-left:8px;'>Criteria &#8599;</a>\",\n layout=widgets.Layout(margin='0 0 0 4px'))\n\n# state helpers\n\ndef _set_search_disabled(d):\n _use_paper_query_btn.disabled = d\n _paper_link.value = (f\"<a href='{_PAPER_URL}' target='_blank' style='font-size:13px;color:{_DARK_GREY};\"\n f\"text-decoration:underline;{'opacity:0.4;pointer-events:none;' if d else ''}'>&#8599;</a>\")\n query_input.disabled = d\n start_date.disabled = end_date.disabled = search_run.btn.disabled = d\n\ndef _set_load_disabled(d):\n upload_widget.value = _make_upload_html('disabled' if d else 'idle')\n\ndef _update_predict():\n r = model_loaded and articles_table is not None\n predict_run.btn.disabled = not r\n\ndef _update_recall_stats(change=None):\n v = recall_target.value\n ws = round(_WORKS_SAVED[v])\n pr = round(_PRECISION[v])\n _recall_stats.value = (\n f\"<div style='font-size:12px;color:{_DARK_GREY};line-height:1.8;'>\"\n f\"Expected <b>{ws}%</b> reduction in number of articles needing human review<br>\"\n f\"Expected <b>{pr}%</b> of screened articles to be relevant\"\n f\"</div>\"\n )\nrecall_target.observe(_update_recall_stats, names='value')\n_update_recall_stats()\n\ndef _set_articles(table):\n global articles_table\n articles_table = table\n _update_predict()\n\ndef _unset_articles():\n global articles_table\n articles_table = None\n unload_btn.layout.display = 'none'; _upload_refresh_btn.layout.display = 'none'\n _update_predict()\n\n# event handlers\n\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)\n\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_input': query_input,\n 'start_date': start_date, 'end_date': end_date,\n })\n t = ns['articles_table']; n = len(t.df)\n search_run.done('\\u2713 Search completed'); _set_load_disabled(True)\n _set_articles(t)\n search_run.out.value = (\n download_link(t, 'Articles') +\n f\"<div style='font-size:12px;color:{_DARK_GREY};margin-left:12px;'>\"\n f\"{n:,} articles loaded and ready to be screened</div>\")\n unload_btn.layout.display = ''\n except Exception as e:\n _set_search_disabled(False); search_run.fail(e)\n\ndef on_file_uploaded(change):\n if not change.new: return\n _upload_pipe.value = '' # reset so same file can be re-uploaded\n load_file_error.value = ''; _upload_success.value = ''; _upload_err_btn.layout.display = 'none'\n try:\n raw = base64.b64decode(change.new)\n table = PredictionHandler.from_bytes(raw)\n n = len(table.df)\n upload_widget.value = _make_upload_html('done')\n _set_articles(table); _set_search_disabled(True)\n _upload_refresh_btn.layout.display = ''\n _upload_success.value = (f\"<div style='font-size:12px;color:{_DARK_GREY};'>\"\n f\"{n:,} articles loaded and ready to be screened</div>\")\n except BaseException as e:\n upload_widget.value = _make_upload_html('idle')\n load_file_error.value = error_html(e)\n _upload_err_btn.layout.display = ''\n_upload_pipe.observe(on_file_uploaded, names='value')\n\ndef on_unload(b):\n _unset_articles()\n saved = (start_date.value, end_date.value, query_input.value)\n _set_search_disabled(False)\n start_date.value, end_date.value, query_input.value = saved\n _set_load_disabled(False)\n search_run.reset(); _reset_btn.layout.display = 'none'\n load_file_error.value = ''; _upload_err_btn.layout.display = 'none'\n _upload_success.value = ''\n\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 unload_btn.disabled = True; _upload_refresh_btn.disabled = True\n predict_run.out.value = (\n download_combined_link(PredictionHandler(ns['review_data']), 'Articles identified after screening') +\n download_predictions_link(PredictionHandler(ns['data']), 'Results for all articles'))\n except Exception as e:\n predict_run.fail(e)\n\ndef on_predict_reset(b):\n predict_run.reset(); _reset_btn.layout.display = 'none'\n unload_btn.disabled = False; _upload_refresh_btn.disabled = False\n _update_predict()\n\ndef on_use_paper_query(change):\n global _setting_paper_query\n if not change['new']: return\n wrapper = next(m for m in available_models if m.name == model_dropdown.value)\n _setting_paper_query = True\n query_input.value = wrapper.default_query\n _setting_paper_query = False\n\ndef on_query_changed(change):\n if _setting_paper_query: return\n if _use_paper_query_btn.value:\n _use_paper_query_btn.value = False\n\n_use_paper_query_btn.observe(on_use_paper_query, names='value')\nquery_input.observe(on_query_changed, names='value')\n\n# wire up\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); _upload_refresh_btn.on_click(on_unload)\n_upload_err_btn.on_click(lambda b: (setattr(load_file_error, 'value', ''),\n setattr(_upload_err_btn.layout, 'display', 'none')))\n\n# layout\n_M = widgets.Layout(margin='0 0 0 12px')\ndisplay(\n tutorial.overlay,\n widgets.HTML(\n f\"<h2 style='margin:0 0 0 12px;font-size:22px;font-weight:bold;'>\"\n f\"<span style='color:{_DARK_GREY};'>PopulationHealth</span>\"\n f\"<span style='color:{_ORANGE};'>Screener</span>\"\n f\"</h2>\"\n ),\n make_spacer(_SP_L), # L: title β†’ first section\n widgets.VBox([\n make_section_header_with_info('Choose a model', tutorial, 0),\n make_spacer(_SP_S),\n model_dropdown,\n make_spacer(_SP_S),\n load_run.row,\n ], layout=_M),\n make_spacer(_SP_M), # M: end of section β†’ separator\n make_separator(),\n make_spacer(_SP_L), # L: separator β†’ next section\n widgets.VBox([\n widgets.HBox([\n widgets.VBox([\n make_section_header_with_info('Search PubMed', tutorial, 1),\n make_spacer(_SP_S),\n query_input,\n make_spacer(_SP_XS),\n _qs_row,\n make_spacer(_SP_S),\n widgets.HBox([widgets.HTML(f\"<span style='{_lbl}'>Start date:</span>\"), start_date]),\n make_spacer(_SP_XS),\n widgets.HBox([widgets.HTML(f\"<span style='{_lbl}'>End date:</span>\"), end_date]),\n make_spacer(_SP_S),\n widgets.VBox([widgets.HBox([search_run.btn, search_run.err_reload, unload_btn]), search_run.out]),\n ]),\n widgets.HBox([\n widgets.HTML(f\"<span style='font-size:16px;color:{_DARK_GREY};'>or</span>\"),\n widgets.VBox([\n make_section_header_with_info('Load articles from a file', tutorial, 1),\n make_spacer(_SP_S),\n widgets.VBox([\n widgets.HBox([upload_widget, _upload_refresh_btn, _upload_err_btn]),\n _pipe_box,\n load_file_error,\n _upload_success,\n ]),\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 ], layout=_M),\n make_spacer(_SP_M), # M: end of section β†’ separator\n make_separator(),\n make_spacer(_SP_L), # L: separator β†’ next section\n widgets.VBox([\n widgets.HBox([make_section_header_with_info('Screen articles', tutorial, 2), _criteria_link],\n layout=widgets.Layout(align_items='center')),\n make_spacer(_SP_S),\n widgets.VBox([\n widgets.HBox([\n widgets.HTML(f\"<span style='font-size:13px;line-height:28px;color:{_DARK_GREY};margin-right:8px;'>Target recall</span>\"),\n recall_target,\n ], layout=widgets.Layout(align_items='center')),\n make_spacer(_SP_XS),\n _recall_stats,\n ]),\n make_spacer(_SP_S),\n widgets.VBox([widgets.HBox([predict_run.btn, predict_run.err_reload, _reset_btn]), predict_run.out]),\n ], layout=_M),\n)\ntutorial.show()"
15
  },
16
  {
17
  "cell_type": "code",
notebooks/src/utils/ui_elements.py CHANGED
@@ -134,9 +134,9 @@ _STYLES_CSS = f"""<style>
134
  content: '';
135
  position: absolute;
136
  left: 2px;
137
- top: -1px;
138
- width: 4px;
139
- height: 8px;
140
  border: 2px solid {_DARK_GREY};
141
  border-top: none;
142
  border-left: none;
 
134
  content: '';
135
  position: absolute;
136
  left: 2px;
137
+ top: 1px;
138
+ width: 3px;
139
+ height: 5px;
140
  border: 2px solid {_DARK_GREY};
141
  border-top: none;
142
  border-left: none;