Spaces:
Sleeping
Sleeping
File size: 12,704 Bytes
7d05933 3cd0ccc c066809 9b364cf c066809 0f20848 f6dbff2 fe0e3bd 3cd0ccc c066809 9b364cf c066809 3cd0ccc c066809 f6dbff2 c066809 9b364cf c066809 9b364cf c066809 9b364cf c066809 f6dbff2 7c39af1 9b364cf c066809 9b364cf c066809 7ff0050 9b364cf 7ff0050 c066809 9b364cf c066809 181d28a c066809 181d28a c066809 181d28a c066809 9b364cf c066809 9b364cf facae1f 9b364cf c066809 181d28a 9b364cf 181d28a c066809 3cd0ccc 7d05933 f6dbff2 3cd0ccc 0f20848 7d05933 3cd0ccc 0f20848 7d05933 9b364cf 0f20848 7d05933 0f20848 7d05933 9b364cf 0f20848 3cd0ccc 7d05933 3cd0ccc 6cc1523 3cd0ccc 7d05933 3cd0ccc f6dbff2 191e400 7d05933 c066809 7d05933 c066809 191e400 7d05933 191e400 c066809 191e400 7d05933 191e400 c066809 191e400 c066809 191e400 eeb280c c066809 eeb280c c066809 191e400 7d05933 191e400 c066809 191e400 7d05933 9b364cf 7d05933 c066809 7d05933 766aeb2 7d05933 c066809 7d05933 c066809 7d05933 74aa4e3 7d05933 74aa4e3 c066809 7d05933 c066809 74aa4e3 7d05933 42309ae 7d05933 181d28a c066809 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 | import base64
import uuid
import ipywidgets as widgets
from IPython.display import HTML, display
from src.utils.colours import AppColour # noqa: F401 — re-exported for notebook
from src.handlers.error_handler import ErrorHandler
from src.handlers.tutorial_handler import Tutorial # noqa: F401 — re-exported for notebook
_DARK_GREY = AppColour.DARK_GREY.value
_GREY = AppColour.GREY.value
_LIGHT_GREY = AppColour.LIGHT_GREY.value
_ORANGE = AppColour.ORANGE.value
_RED = AppColour.RED.value
_GREEN = AppColour.GREEN.value
BOX_WIDTH = '300px'
BUTTON_WIDTH = '225px'
QUERY_WIDTH = '396px'
_STYLES_CSS = f"""<style>
/* Zero out widget HTML container margins so spacers have full control */
.widget-html, .widget-html-content {{
margin: 0 !important;
padding: 0 !important;
}}
/* Widget label/description text: dark grey */
.widget-label, .widget-label-basic {{
color: {_DARK_GREY} !important;
}}
/* All enabled buttons: Orange bg, white text */
.widget-button {{
justify-content: flex-start !important;
text-align: left !important;
font-weight: bold !important;
font-size: 15px !important;
padding-left: 12px !important;
background-color: {_ORANGE} !important;
color: white !important;
border: none !important;
}}
/* Disabled (not runnable / already ran): LG bg, dark G text */
.widget-button:disabled {{
background-color: {_LIGHT_GREY} !important;
color: {_GREY} !important;
opacity: 1 !important;
}}
/* done-button: green bg, white text */
.done-button.widget-button {{
background-color: {_GREEN} !important;
color: white !important;
border: none !important;
}}
/* Error reload: red outline, white bg */
.error-reload-btn.widget-button {{
background-color: white !important;
color: {_RED} !important;
border: 1px solid {_RED} !important;
}}
/* circular-btn centres its icon; padding-left must be 0 */
.circular-btn.widget-button {{
border-radius: 50% !important;
padding: 0 !important;
padding-left: 0 !important;
justify-content: center !important;
text-align: center !important;
align-items: center !important;
font-size: 14px !important;
line-height: 1 !important;
min-width: unset !important;
}}
/* Tutorial nav buttons (Back / Next): transparent bg, dark grey text */
.tut-nav-btn.widget-button {{
background-color: transparent !important;
color: {_DARK_GREY} !important;
border: none !important;
box-shadow: none !important;
}}
.tut-nav-btn.widget-button:disabled {{
background-color: transparent !important;
color: {_GREY} !important;
}}
.widget-textarea textarea {{
resize: none !important;
overflow-y: auto !important;
border: 1px solid {_DARK_GREY} !important;
color: {_DARK_GREY} !important;
}}
.widget-datepicker input {{
border: 1px solid {_DARK_GREY} !important;
color: {_DARK_GREY} !important;
}}
.widget-datepicker input:disabled {{
background-color: #f0f0f0 !important;
color: {_GREY} !important;
cursor: not-allowed !important;
}}
.widget-dropdown select {{
border: 1px solid {_DARK_GREY} !important;
color: {_DARK_GREY} !important;
}}
/* Tutorial overlay */
.tut-overlay {{
position: fixed !important;
top: 0 !important; left: 0 !important;
right: 0 !important; bottom: 0 !important;
background: rgba(0,0,0,0.55) !important;
z-index: 9999 !important;
}}
.tut-card {{
background: white;
border-radius: 8px;
padding: 28px 32px 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
z-index: 9999;
}}
.grey-checkbox input[type="checkbox"] {{
appearance: none;
-webkit-appearance: none;
width: 13px;
height: 13px;
border: 2px solid {_DARK_GREY};
border-radius: 2px;
background: transparent;
cursor: pointer;
vertical-align: middle;
position: relative;
}}
.grey-checkbox input[type="checkbox"]:checked {{
background: transparent;
border: 2px solid {_DARK_GREY};
}}
.grey-checkbox input[type="checkbox"]:checked::after {{
content: '';
position: absolute;
left: 2px;
top: 1px;
width: 3px;
height: 5px;
border: 2px solid {_DARK_GREY};
border-top: none;
border-left: none;
transform: rotate(45deg);
}}
.tut-info-btn {{
background: none !important;
border: none !important;
box-shadow: none !important;
color: {_DARK_GREY} !important;
font-size: 15px !important;
font-weight: normal !important;
padding: 0 2px !important;
min-width: unset !important;
width: auto !important;
height: auto !important;
line-height: inherit !important;
justify-content: center !important;
cursor: pointer !important;
}}
.tut-info-btn:hover {{ color: {_GREY} !important; }}
/* ✕ top-right of every card: transparent bg, red icon */
.tut-x-btn.widget-button {{
background-color: transparent !important;
color: {_RED} !important;
border: none !important;
box-shadow: none !important;
padding-left: 0 !important;
justify-content: center !important;
}}
/* "Close" button in bottom-nav of step 3/3: transparent bg, red text */
.tut-close-btn.widget-button {{
background-color: transparent !important;
color: {_RED} !important;
border: none !important;
box-shadow: none !important;
}}
.tut-close-btn.widget-button:disabled {{
background-color: transparent !important;
color: #e07070 !important;
}}
</style>
"""
def inject_styles():
"""
Push the app's global CSS into the page output.
Call once at the top of the notebook before any widgets are displayed.
"""
display(HTML(_STYLES_CSS))
def make_button(description, width=BUTTON_WIDTH):
"""Return a bold ipywidgets Button with the given label and width."""
return widgets.Button(
description=description,
layout=widgets.Layout(width=width),
style=widgets.ButtonStyle(font_weight='bold')
)
def make_separator():
"""Return a thin horizontal rule widget for use between app sections."""
return widgets.HTML("<hr style='margin: 0 12px; border: none; border-top: 1px solid #ccc;'>")
def make_spacer(height='15px'):
"""Return an invisible HTML widget that occupies the given vertical height."""
return widgets.HTML(f"<div style='height:{height};'></div>")
def make_section_header(text):
"""Return a bold 16 px HTML widget suitable for labelling an app section."""
return widgets.HTML(f"<b style='font-size:16px;color:{_DARK_GREY};'>{text}</b>")
def set_executing(btn, message='Running...'):
"""
Put a Button into the executing state.
Sets the description to message, disables the button, and removes the
done-button CSS class so it reverts to the default style.
"""
btn.description = message
btn.disabled = True
btn.remove_class('done-button')
def set_done(btn, message):
"""
Put a Button into the completed state.
Sets the description to message, keeps the button disabled, and adds the
done-button CSS class (green border and text).
"""
btn.description = message
btn.disabled = True
btn.add_class('done-button')
def error_html(exc):
"""
Return an HTML string that displays exc in red below a button row.
Newlines in the message are rendered as line breaks.
Assign the result to an HTML widget's value to surface errors in the UI.
"""
return ErrorHandler.html(exc)
class RunButton:
"""
A button paired with an inline HTML widget for status and error display.
Use start() when an operation begins, done(msg) on success, and fail(exc)
on failure. Call reset() to restore the button to its initial clickable
state (e.g. after an Unload).
run = RunButton('Search', executing_msg='Searching...', error_label='Search')
display(run.row)
run.on_click(my_handler)
"""
def __init__(self, label, executing_msg='Running...', error_label=None, width=BUTTON_WIDTH):
self._label = label
self._executing_msg = executing_msg
self._error_label = error_label or label
self.btn = make_button(label, width=width)
self.out = widgets.HTML(value='')
self.err_reload = widgets.Button(
description='↺',
layout=widgets.Layout(width='28px', height='28px', display='none'))
self.err_reload.add_class('circular-btn')
self.err_reload.add_class('error-reload-btn')
self.err_reload.on_click(lambda b: self.reset())
self.row = widgets.VBox([
widgets.HBox([self.btn, self.err_reload]),
self.out,
])
def on_click(self, handler):
self.btn.on_click(handler)
def start(self):
"""
Clear inline output and put the button into the executing state.
Call at the very beginning of an on_click handler, before any async work.
"""
self.out.value = ''
self.err_reload.layout.display = 'none'
set_executing(self.btn, self._executing_msg)
def done(self, msg=None):
self.err_reload.layout.display = 'none'
set_done(self.btn, msg if msg is not None else f'\u2713 {self._label}')
def fail(self, exc):
self.out.value = error_html(exc)
self.btn.description = self._label
self.btn.disabled = True
self.btn.remove_class('done-button')
self.err_reload.layout.display = ''
def reset(self):
"""
Restore the button and output to their initial state.
Call when the user unloads data or explicitly resets the section so the
button becomes clickable again with its original label.
"""
self.btn.description = self._label
self.btn.disabled = False
self.btn.remove_class('done-button')
self.err_reload.layout.display = 'none'
self.out.value = ''
_DL_SPAN = "display:inline-flex;align-items:center;margin-left:12px;line-height:28px;"
_DL_LINK = f"font-size:12px;color:{_DARK_GREY};text-decoration:underline;line-height:28px;margin-right:3px;"
_DL_BOX = "font-size:12px;border:1px solid #ccc;border-radius:3px;padding:1px 5px;height:20px;line-height:18px;width:130px;margin:0 2px;"
_DL_EXT = "font-size:12px;line-height:28px;"
def download_link(table, stem):
n = len(table.df)
b64 = table.to_csv_b64()
return (
f"<span style='{_DL_SPAN}'>"
f"<a href='data:text/csv;base64,{b64}' download='{stem}.csv' style='{_DL_LINK}'>Download {n} articles</a>"
f"</span>"
)
def download_combined_link(table, stem):
"""
Return an HTML snippet whose Download link saves both a .csv and a .ris file.
The two saves are triggered sequentially (150 ms apart) so the browser
handles them as distinct downloads. Suitable for the screened-articles section
where researchers need both formats for their reference manager.
"""
csv_b64 = table.to_csv_b64()
ris_b64 = table.to_ris_b64()
uid = uuid.uuid4().hex[:8]
return (
f"<span style='{_DL_SPAN}'>"
f"<a id='csv-{uid}' href='data:text/csv;base64,{csv_b64}' download='{stem}.csv' style='display:none'></a>"
f"<a id='ris-{uid}' href='data:application/x-research-info-systems;base64,{ris_b64}' download='{stem}.ris' style='display:none'></a>"
f"<a href='#' style='{_DL_LINK}'"
f" onclick=\"document.getElementById('csv-{uid}').click();"
f"setTimeout(function(){{document.getElementById('ris-{uid}').click();}},150);"
f"return false;\">Download {len(table.df)} articles identified after screening</a>"
f"</span>"
)
def download_predictions_link(table, stem):
n = len(table.df)
df = table.df.rename(columns={
'y_prob': 'Predicted probability of meeting inclusion criteria',
'y_pred': 'Prediction (1=meets inclusion criteria; 0=does not meet inclusion criteria)',
})
b64 = base64.b64encode(df.to_csv(index=False).encode()).decode()
return (
f"<span style='{_DL_SPAN}'>"
f"<a href='data:text/csv;base64,{b64}' download='{stem}.csv' style='{_DL_LINK}'>Download results for all {n} articles</a>"
f"</span>"
)
def make_section_header_with_info(text, tutorial, step):
"""Section header with an ⓘ button that reopens the tutorial at `step`."""
btn = widgets.Button(
description='ⓘ',
layout=widgets.Layout(width='auto', height='auto', min_width='unset', padding='0',
margin='0 0 0 6px'))
btn.add_class('tut-info-btn')
btn.on_click(lambda b: tutorial.show(step))
return widgets.HBox([make_section_header(text), btn],
layout=widgets.Layout(align_items='center'))
|