2323)
2424from epicc .utils .section_renderer import render_sections
2525
26+ # ---------------------------------------------------------------------------
27+ # Export / print state helpers (inlined from epicc.utils.export)
28+ # ---------------------------------------------------------------------------
29+
30+ RESULTS_PAYLOAD_KEY = "results_payload"
31+ PRINT_REQUESTED_KEY = "print_requested"
32+ PRINT_TRIGGER_TOKEN_KEY = "print_trigger_token"
33+
34+
35+ def initialize_export_state () -> None :
36+ if RESULTS_PAYLOAD_KEY not in st .session_state :
37+ st .session_state [RESULTS_PAYLOAD_KEY ] = None
38+
39+ if PRINT_REQUESTED_KEY not in st .session_state :
40+ st .session_state [PRINT_REQUESTED_KEY ] = False
41+
42+ if PRINT_TRIGGER_TOKEN_KEY not in st .session_state :
43+ st .session_state [PRINT_TRIGGER_TOKEN_KEY ] = 0
44+
45+
46+ def clear_export_state () -> None :
47+ st .session_state [RESULTS_PAYLOAD_KEY ] = None
48+ st .session_state [PRINT_REQUESTED_KEY ] = False
49+ st .session_state [PRINT_TRIGGER_TOKEN_KEY ] = 0
50+
51+
52+ def has_results () -> bool :
53+ return st .session_state .get (RESULTS_PAYLOAD_KEY ) is not None
54+
55+
56+ def get_results_payload () -> dict [str , Any ] | None :
57+ payload = st .session_state .get (RESULTS_PAYLOAD_KEY )
58+ if payload is None :
59+ return None
60+
61+ return payload
62+
63+
64+ def set_results_payload (payload : dict [str , Any ] | None ) -> None :
65+ st .session_state [RESULTS_PAYLOAD_KEY ] = payload
66+
67+
68+ def render_export_button () -> None :
69+ export_clicked = st .sidebar .button (
70+ "Export Results as PDF" , disabled = not has_results ()
71+ )
72+
73+ if export_clicked and has_results ():
74+ st .session_state [PRINT_REQUESTED_KEY ] = True
75+ st .session_state [PRINT_TRIGGER_TOKEN_KEY ] = (
76+ st .session_state .get (PRINT_TRIGGER_TOKEN_KEY , 0 ) + 1
77+ )
78+
79+
80+ def trigger_print_if_requested () -> None :
81+ if not st .session_state .get (PRINT_REQUESTED_KEY ):
82+ return
83+
84+ if not has_results ():
85+ st .session_state [PRINT_REQUESTED_KEY ] = False
86+ return
87+
88+ trigger_token = st .session_state .get (PRINT_TRIGGER_TOKEN_KEY , 0 )
89+ st .html (
90+ (
91+ "<script>"
92+ f"window.__epiccPrintToken = { trigger_token } ;"
93+ "setTimeout(function(){ window.parent.print(); }, 0);"
94+ "</script>"
95+ ),
96+ unsafe_allow_javascript = True ,
97+ )
98+ st .session_state [PRINT_REQUESTED_KEY ] = False
99+
26100
27101def _load_styles () -> None :
28102 with importlib .resources .files ("epicc" ).joinpath ("web/sidebar.css" ).open ("rb" ) as f :
@@ -35,13 +109,20 @@ def _sync_active_model(model_key: str) -> dict[str, Any]:
35109 if active_model_key != model_key :
36110 st .session_state .active_model_key = model_key
37111 st .session_state .params = {}
112+ clear_export_state ()
38113
39114 if "params" not in st .session_state :
40115 st .session_state .params = {}
41116
42117 return st .session_state .params
43118
44119
120+ def _render_results_panel (results_payload : dict [str , Any ]) -> None :
121+ st .title (results_payload .get ("title" , CONFIG .app .title ))
122+ st .write (results_payload .get ("description" , "" ))
123+ render_sections (results_payload .get ("sections" , []))
124+
125+
45126def _render_excel_parameter_inputs (
46127 params : dict [str , Any ],
47128) -> tuple [dict [str , Any ], dict [str , str ]]:
@@ -61,6 +142,7 @@ def _render_excel_parameter_inputs(
61142 if st .session_state .get ("excel_active_identity" ) != excel_identity :
62143 st .session_state .excel_active_identity = excel_identity
63144 st .session_state .params = {}
145+ clear_export_state ()
64146 params = st .session_state .params
65147 should_refresh_params = True
66148
@@ -135,6 +217,7 @@ def _render_python_parameter_inputs(
135217 if st .session_state .get ("active_param_identity" ) != param_identity :
136218 st .session_state .active_param_identity = param_identity
137219 st .session_state .params = {}
220+ clear_export_state ()
138221 params = st .session_state .params
139222 should_refresh_params = True
140223
@@ -281,11 +364,11 @@ def _render_validation_error_details(
281364
282365def _run_excel_simulation (
283366 params : dict [str , Any ], label_overrides : dict [str , str ]
284- ) -> None :
367+ ) -> dict [ str , Any ] | None :
285368 uploaded_excel_model = st .session_state .get ("excel_model_uploader" )
286369 if not uploaded_excel_model :
287370 st .error ("Please upload an Excel model file first." )
288- st . stop ()
371+ return None
289372
290373 with st .spinner (f"Running Excel-driven model: { uploaded_excel_model .name } ..." ):
291374 results = run_excel_driven_model (
@@ -295,22 +378,32 @@ def _run_excel_simulation(
295378 sheet_name = None ,
296379 label_overrides = label_overrides ,
297380 )
298- st .title (results .get ("model_title" , "Excel Driven Model" ))
299- st .write (results .get ("model_description" , "" ))
300- render_sections (results ["sections" ])
381+ return {
382+ "title" : results .get ("model_title" , "Excel Driven Model" ),
383+ "description" : results .get ("model_description" , "" ),
384+ "sections" : results .get ("sections" , []),
385+ }
301386
302387
303388def _run_python_simulation (
304389 selected_label : str ,
305390 model : BaseSimulationModel ,
306391 typed_params : BaseModel ,
307392 label_overrides : dict [str , str ],
308- ) -> None :
393+ ) -> dict [str , Any ]:
394+ # NOTE: Previously this function rendered results directly with st.* calls and
395+ # returned None implicitly. That meant set_results_payload(None) was always
396+ # called, has_results() was always False, and the PDF export button was
397+ # permanently disabled. It now returns a payload dict stored in session state;
398+ # rendering is deferred to _render_results_panel after st.rerun().
309399 with st .spinner (f"Running { selected_label } ..." ):
310- st .title (model .model_title or CONFIG .app .title )
311- st .write (model .model_description or CONFIG .app .description )
312400 results = model .run (typed_params , label_overrides = label_overrides )
313- render_sections (model .build_sections (results ))
401+ sections = model .build_sections (results )
402+ return {
403+ "title" : model .model_title or CONFIG .app .title ,
404+ "description" : model .model_description or CONFIG .app .description ,
405+ "sections" : sections ,
406+ }
314407
315408
316409_load_styles ()
@@ -328,6 +421,7 @@ def _run_python_simulation(
328421model_key = selected_label
329422
330423params = _sync_active_model (model_key )
424+ initialize_export_state ()
331425
332426st .sidebar .subheader ("Input Parameters" )
333427
@@ -355,20 +449,41 @@ def _run_python_simulation(
355449 _render_validation_error_details (selected_label , exc , sidebar = True )
356450 has_input_errors = True
357451
358- if not st .sidebar .button ("Run Simulation" , disabled = has_input_errors ):
359- st . stop ()
452+ run_clicked = st .sidebar .button ("Run Simulation" , disabled = has_input_errors )
453+ render_export_button ()
360454
361- if is_excel_model :
362- _run_excel_simulation (params , label_overrides )
455+ # For Excel models typed_params is never set (not needed by that path).
456+ # Only block execution for Python models when parameter validation has failed.
457+ if not is_excel_model and typed_params is None :
458+ st .error ("Cannot run simulation until parameter validation errors are fixed." )
363459 st .stop ()
364460
365- if typed_params is None :
366- st .error ("Cannot run simulation until parameter validation errors are fixed." )
461+ if run_clicked :
462+ if is_excel_model :
463+ set_results_payload (_run_excel_simulation (params , label_overrides ))
464+ else :
465+ assert typed_params is not None # guaranteed by the st.stop() guard above
466+ set_results_payload (
467+ _run_python_simulation (
468+ selected_label ,
469+ model_registry [selected_label ],
470+ typed_params ,
471+ label_overrides ,
472+ )
473+ )
474+
475+ # Always rerun after a successful run so the export button reflects the new
476+ # state (has_results() == True) and _render_results_panel is reached below.
477+ if has_results ():
478+ st .rerun ()
479+
480+ elif not has_results ():
481+ # No run was clicked and no stored results exist yet; nothing to display.
367482 st .stop ()
368483
369- _run_python_simulation (
370- selected_label ,
371- model_registry [ selected_label ],
372- typed_params ,
373- label_overrides ,
374- )
484+
485+ results_payload = get_results_payload ()
486+ if results_payload :
487+ _render_results_panel ( results_payload )
488+
489+ trigger_print_if_requested ( )
0 commit comments