Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 22 additions & 14 deletions src/epicc/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,29 +107,30 @@
render_validation_error(selected_label, exc, container=param_col)
has_input_errors = True

# Reset and Save Parameters buttons side by side
button_col1, button_col2 = st.columns(2)

# Reset Parameters button
def _handle_reset() -> None:
model_label = cast(str, selected_label) # Safe because we checked above
reset_parameters_to_defaults(
model_defaults_flat, params, model_label, param_specs=active_model.parameter_specs
)
# Reset scenarios back to model defaults
default_scenarios = active_model.default_scenarios
if default_scenarios:
reset_scenario_state(
model_label,
default_scenarios,
active_model.scenario_parameter_specs or {},
)

with button_col1:
st.button("Reset Parameters", on_click=_handle_reset, width='stretch')

# Save Parameters button (only enabled when parameters are valid)
with button_col2:

# Force both controls into the same keyed row for CSS alignment
with st.container(key="param-actions-row"):
button_col1, button_col2 = st.columns(2, gap="small", vertical_alignment="top")

button_col1.button(
"Reset Parameters",
on_click=_handle_reset,
use_container_width=True,
)

if typed_params is not None:
render_parameter_export_modal(
active_model.human_name(),
Expand All @@ -138,11 +139,19 @@ def _handle_reset() -> None:
container=button_col2,
)
else:
st.button("Save Parameters", disabled=True, width='stretch', help="Fix parameter errors first")
button_col2.button(
"Save Parameters",
disabled=True,
use_container_width=True,
help="Fix parameter errors first",
)
Comment on lines +142 to +147

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@EddW1219, please resolve this if this is a non-issue with our post-August 2025 version of Streamlit. Make sure to check this against the version of Stlite we're running as well. Thanks.


st.divider()
run_clicked = st.button(
"Run Simulation", disabled=has_input_errors, width='stretch', type='primary'
"Run Simulation",
disabled=has_input_errors,
use_container_width=True,
type="primary",
)

with result_col:
Expand Down Expand Up @@ -170,5 +179,4 @@ def _handle_reset() -> None:
renderer.render(None, hint=_HINT)

st.divider()
render_pdf_export_button(container=result_col)

render_pdf_export_button(container=result_col)
2 changes: 1 addition & 1 deletion src/epicc/model/models/tb_isolation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ report:
infections.

$$
\LaTeX \text{ test.}
\text{LaTeX test.}
$$

## Scenarios
Expand Down
28 changes: 5 additions & 23 deletions src/epicc/ui/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,17 @@ def render_parameter_export_modal(
seen.add(cls)
unique.append((suffix.lstrip("."), cls))

if rc.button("Save Parameters", width='stretch', key=f"save_params_btn_{model_name.lower().replace(' ', '_')}"):
if rc.button("Save Parameters", use_container_width=True, key=f"save_params_btn_{model_name.lower().replace(' ', '_')}"):
_export_dialog(model_name, param_data, unique, pydantic_model)


def render_pdf_export_button(container: Any = None) -> None:
# Render a direct Save report as PDF button.

rc = container if container is not None else st
clicked = rc.button(
"Save report as PDF",
disabled=not has_results(),
width='stretch',
use_container_width=True,
type='primary',
)

Expand All @@ -122,25 +121,8 @@ def trigger_print_if_requested() -> None:

trigger_token = st.session_state.get(_PRINT_TOKEN_KEY, 0)

# What the hell is this, Streamlit? Why can't I just run JS without this nonsense? Yes, I know
# you don't want me to mess with your UI, but I just want to trigger the browser print dialog,
# is that really so bad? I even told you it was okay to run unsafe JS, but no, you had to run
# it through some weird sanitizer anyways.
#
# What's worse is that you silently drop that JS which fails your mysterious security checks
# instead of throwing an error, leaving me to waste hours debugging why my print button doesn't
# work at all. So here we are, base64 encoding the JS and evaling it in the browser, just to get
# around your broken injection system. I hope you're proud of yourselves.
#
# Seriously!?!? This works?
#
# This is an alternative implementation to something like:
#
# https://github.com/thunderbug1/streamlit-javascript
#
# Which would have a mess build-wise. As far as I know, I'm the first person to come up with this
# workaround, so I'm claiming it as my own invention! Don't tell Streamlit.

# Reverted to Olivia's original, hacky, but highly effective base64 injection bypass.
# By using st.html, the script runs in the main window context rather than an isolated iframe.
with importlib.resources.files("epicc").joinpath("js/print_results.js").open("rb") as f:
js = f.read().decode()
js64 = base64.b64encode(js.encode()).decode()
Expand All @@ -153,4 +135,4 @@ def trigger_print_if_requested() -> None:
unsafe_allow_javascript=True,
)

st.session_state[_PRINT_REQUESTED_KEY] = False
st.session_state[_PRINT_REQUESTED_KEY] = False
12 changes: 8 additions & 4 deletions src/epicc/ui/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,20 +172,24 @@ def render(self, run_results: dict[str, Any] | None) -> None:
if self._block.title:
st.markdown(
f"<div style='text-align: center; margin-bottom: 0.5rem;'>"
f"<span style='font-size: 1.1rem; font-weight: 600; color: #1f1f1f;'>"
f"<span style='font-size: 1.1rem; font-weight: 600; color: inherit;'>"
f"{self._block.title}</span></div>",
unsafe_allow_html=True,
)

if self._block.caption:
st.markdown(
f"<div style='text-align: center; margin-bottom: 0.5rem;'>"
f"<span style='font-size: 0.9rem; color: #6c757d;'>"
f"<div style='text-align: center; margin-bottom: 0.6rem;'>"
f"<span style='font-size: 0.9rem; color: var(--text-color); opacity: 0.8;'>"
f"{self._block.caption}</span></div>",
unsafe_allow_html=True
)

st.plotly_chart(fig, width='stretch', key=f'plotly-{self._uuid}')
st.plotly_chart(
fig,
use_container_width=True,
key=f"plotly-{self._uuid}",
)

def _resolve_columns(
self, run_results: dict[str, Any]
Expand Down
200 changes: 188 additions & 12 deletions src/epicc/web/sidebar.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ div[data-testid="InputInstructions"] > span:nth-child(1) {

/* Fix model selector alignment. TODO: be more specific, `:first-child`? */
.stVerticalBlock {
justify-content: center;
justify-content: center;
}

/* Sidebar section headers */
Expand Down Expand Up @@ -95,16 +95,22 @@ div[data-testid="InputInstructions"] > span:nth-child(1) {
margin: 0.5in;
}

/* [data-testid="stAppViewContainer"] {
margin: 0 !important;
padding: 0 !important;
html, body, #root, .stlite-container, [data-testid="stAppViewContainer"] {
height: auto !important;
overflow: visible !important;
opacity: 1 !important;
filter: none !important;
-webkit-filter: none !important;
}

[data-testid="stMainBlockContainer"] {
max-width: 100% !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
} */
[data-testid="stSelectbox"],
[data-testid="stColumn"] button,
[data-testid="stHeader"],
[data-testid="stSidebar"],
.stAppDeployButton,
footer {
display: none !important;
}

.section-divider {
page-break-after: auto;
Expand All @@ -131,12 +137,10 @@ div[data-testid="InputInstructions"] > span:nth-child(1) {
.stDataFrame,
.stAgGrid,
.stTable {
padding-top: 1.5rem !important;
padding-top: 0.5rem !important;
padding-bottom: 1.5rem !important;
}

/* Show only results. */

body * {
display: none;
}
Expand All @@ -158,5 +162,177 @@ div[data-testid="InputInstructions"] > span:nth-child(1) {
.st-key-results-report,
.st-key-results-report * {
display: revert !important;
opacity: 1 !important;
filter: none !important;
}
}

/* Action row wrapper */
.st-key-param-actions-row [data-testid="stHorizontalBlock"] {
align-items: flex-start !important;
}

/* Normalize column internal spacing */
.st-key-param-actions-row [data-testid="column"] > div {
padding-top: 0 !important;
}
.st-key-param-actions-row [data-testid="stElementContainer"] {
margin-top: 0 !important;
margin-bottom: 0 !important;
}

/* Make trigger controls fill width and remove extra top offset */
.st-key-param-actions-row [data-testid="stButton"],
.st-key-param-actions-row [data-testid="stPopover"] {
width: 100%;
margin-top: 0 !important;
padding-top: 0 !important;
}
.st-key-param-actions-row [data-testid="stButton"] > button,
.st-key-param-actions-row [data-testid="stPopover"] > button {
width: 100%;
}

/* Report spacing: keep headings away from charts */
.st-key-results-report [data-testid="stPlotlyChart"],
.st-key-results-report [data-testid="stVegaLiteChart"],
.st-key-results-report [data-testid="stPyplot"] {
margin-bottom: 2rem !important;
}

.st-key-results-report [data-testid="stMarkdownContainer"] h1,
.st-key-results-report [data-testid="stMarkdownContainer"] h2,
.st-key-results-report [data-testid="stMarkdownContainer"] h3 {
margin-top: 1.25rem !important;
}

/* Plotly text contrast follows the active Streamlit theme */
.st-key-results-report [data-testid="stPlotlyChart"] {
color: inherit !important;
}

.st-key-results-report [data-testid="stPlotlyChart"] .js-plotly-plot,
.st-key-results-report [data-testid="stPlotlyChart"] .plot-container,
.st-key-results-report [data-testid="stPlotlyChart"] .main-svg {
color: inherit !important;
}

.st-key-results-report .js-plotly-plot text,
.st-key-results-report .js-plotly-plot .legendtext,
.st-key-results-report .js-plotly-plot .gtitle,
.st-key-results-report .js-plotly-plot .xtick text,
.st-key-results-report .js-plotly-plot .ytick text,
.st-key-results-report .js-plotly-plot .annotation text,
.st-key-results-report .js-plotly-plot .infolayer text {
fill: currentColor !important;
color: inherit !important;
}

/* -------- Print/PDF display fixes only (SINGLE CONSOLIDATED BLOCK) -------- */
@media print {
.st-key-results-report {
overflow: visible !important;
}

.st-key-results-report [data-testid="stPlotlyChart"],
.st-key-results-report [data-testid="stVegaLiteChart"],
.st-key-results-report [data-testid="stPyplot"] {
display: block !important;
position: static !important;
clear: both !important;
overflow: visible !important;
break-inside: avoid !important;
page-break-inside: avoid !important;
margin: 0 0 2.8rem 0 !important;
padding-bottom: 1.2rem !important;
}

.st-key-results-report [data-testid="stPlotlyChart"] .js-plotly-plot {
min-height: 520px !important;
}

.st-key-results-report h1,
.st-key-results-report h2,
.st-key-results-report h3,
.st-key-results-report h4 {
clear: both !important;
position: static !important;
z-index: auto !important;
margin-top: 2rem !important;
}

.st-key-results-report p,
.st-key-results-report [data-testid="stMarkdownContainer"] {
clear: both !important;
position: static !important;
z-index: auto !important;
margin-top: 0.5rem !important;
}

.st-key-results-report [data-testid="stMarkdownContainer"] h1,
.st-key-results-report [data-testid="stMarkdownContainer"] h2,
.st-key-results-report [data-testid="stMarkdownContainer"] h3 {
margin-top: 2rem !important;
clear: both !important;
}

.st-key-results-report .katex,
.st-key-results-report .katex-display,
.st-key-results-report mjx-container,
.st-key-results-report .MathJax {
visibility: visible !important;
opacity: 1 !important;
}

.st-key-results-report .katex .katex-mathml,
.st-key-results-report mjx-assistive-mml {
display: none !important;
}

/* Force strict contrast engine reset across stlite modules */
.st-key-results-report,
.st-key-results-report * {
color: #111827 !important;
text-shadow: none !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}

.st-key-results-report [data-testid="stPlotlyChart"],
.st-key-results-report [data-testid="stPlotlyChart"] * {
opacity: 1 !important;
}

.st-key-results-report .js-plotly-plot text,
.st-key-results-report .js-plotly-plot .legendtext,
.st-key-results-report .js-plotly-plot .gtitle,
.st-key-results-report .js-plotly-plot .xtick text,
.st-key-results-report .js-plotly-plot .ytick text,
.st-key-results-report .js-plotly-plot .annotation text,
.st-key-results-report .js-plotly-plot .infolayer text {
fill: #111827 !important;
color: #111827 !important;
}

.st-key-results-report .js-plotly-plot .xaxislayer-above path,
.st-key-results-report .js-plotly-plot .yaxislayer-above path,
.st-key-results-report .js-plotly-plot .xaxislayer-above line,
.st-key-results-report .js-plotly-plot .yaxislayer-above line {
stroke: #111827 !important;
stroke-opacity: 1 !important;
stroke-width: 1.2 !important;
}

.st-key-results-report .js-plotly-plot .gridlayer path,
.st-key-results-report .js-plotly-plot .gridlayer line,
.st-key-results-report .js-plotly-plot .zerolinelayer path,
.st-key-results-report .js-plotly-plot .zerolinelayer line {
stroke: #9ca3af !important;
stroke-opacity: 0.9 !important;
}

.st-key-results-report .js-plotly-plot .scatterlayer .trace path,
.st-key-results-report .js-plotly-plot .barlayer .trace path {
stroke-opacity: 1 !important;
}
}