Skip to content

Commit 2636bdb

Browse files
committed
fix: use st.rerun() instead of full page reload for presets
Replace streamlit_js_eval page reload with st.rerun() for smoother UX: - No page flash/flicker - Scroll position preserved - Faster (no re-fetching static assets) Uses "delete-then-rerun" pattern: apply_preset() now deletes session_state keys instead of setting them, forcing widgets to re-initialize fresh from params.json. This avoids fragment caching issues. Changes: - ParameterManager.apply_preset(): delete keys instead of setting them - ParameterManager.clear_parameter_session_state(): new helper method - StreamlitUI: preset_buttons(), "Load default parameters", and "Import parameters" now use st.rerun() with st.toast() for feedback - Removed unused streamlit_js_eval import - Updated tests for delete-based behavior https://claude.ai/code/session_01BnipAqTf16J7kJVfnzah2U
1 parent 3d6d768 commit 2636bdb

File tree

3 files changed

+89
-24
lines changed

3 files changed

+89
-24
lines changed

src/workflow/ParameterManager.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,11 @@ def get_preset_description(self, preset_name: str) -> str:
220220

221221
def apply_preset(self, preset_name: str) -> bool:
222222
"""
223-
Apply a preset to the current session state and save to params.json.
223+
Apply a preset by updating params.json and clearing relevant session_state keys.
224224
225-
This method updates session state keys for both TOPP tool parameters and
226-
general workflow parameters based on the preset definition.
225+
Uses the "delete-then-rerun" pattern: instead of overwriting session_state
226+
values (which widgets may not reflect immediately due to fragment caching),
227+
we delete the keys so widgets re-initialize fresh from params.json on rerun.
227228
228229
Args:
229230
preset_name: Name of the preset to apply
@@ -239,6 +240,9 @@ def apply_preset(self, preset_name: str) -> bool:
239240
# Load existing parameters
240241
current_params = self.get_parameters_from_json()
241242

243+
# Collect keys to delete from session_state
244+
keys_to_delete = []
245+
242246
for key, value in preset.items():
243247
# Skip description key
244248
if key == "_description":
@@ -248,7 +252,7 @@ def apply_preset(self, preset_name: str) -> bool:
248252
# Handle general workflow parameters
249253
for param_name, param_value in value.items():
250254
session_key = f"{self.param_prefix}{param_name}"
251-
st.session_state[session_key] = param_value
255+
keys_to_delete.append(session_key)
252256
current_params[param_name] = param_value
253257
elif isinstance(value, dict) and not key.startswith("_"):
254258
# Handle TOPP tool parameters
@@ -257,11 +261,30 @@ def apply_preset(self, preset_name: str) -> bool:
257261
current_params[tool_name] = {}
258262
for param_name, param_value in value.items():
259263
session_key = f"{self.topp_param_prefix}{tool_name}:1:{param_name}"
260-
st.session_state[session_key] = param_value
264+
keys_to_delete.append(session_key)
261265
current_params[tool_name][param_name] = param_value
262266

263-
# Save updated parameters
267+
# Delete affected keys from session_state so widgets re-initialize fresh
268+
for session_key in keys_to_delete:
269+
if session_key in st.session_state:
270+
del st.session_state[session_key]
271+
272+
# Save updated parameters to file
264273
with open(self.params_file, "w", encoding="utf-8") as f:
265274
json.dump(current_params, f, indent=4)
266275

267-
return True
276+
return True
277+
278+
def clear_parameter_session_state(self) -> None:
279+
"""
280+
Clear all parameter-related keys from session_state.
281+
282+
This forces widgets to re-initialize from params.json or defaults
283+
on the next rerun, rather than using potentially stale session_state values.
284+
"""
285+
keys_to_delete = [
286+
key for key in list(st.session_state.keys())
287+
if key.startswith(self.param_prefix) or key.startswith(self.topp_param_prefix)
288+
]
289+
for key in keys_to_delete:
290+
del st.session_state[key]

src/workflow/StreamlitUI.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from io import BytesIO
1313
import zipfile
1414
from datetime import datetime
15-
from streamlit_js_eval import streamlit_js_eval
1615

1716

1817
from src.common.common import (
@@ -1092,8 +1091,8 @@ def preset_buttons(self, num_cols: int = 4) -> None:
10921091
use_container_width=True,
10931092
):
10941093
if self.parameter_manager.apply_preset(preset_name):
1095-
st.success(f"Applied preset: {preset_name}")
1096-
streamlit_js_eval(js_expressions="parent.window.location.reload()")
1094+
st.toast(f"Applied preset: {preset_name}")
1095+
st.rerun()
10971096
else:
10981097
st.error(f"Failed to apply preset: {preset_name}")
10991098
# Start new row if needed
@@ -1120,11 +1119,13 @@ def parameter_section(self, custom_parameter_function) -> None:
11201119
with cols[0]:
11211120
if st.button(
11221121
"⚠️ Load default parameters",
1123-
help="Reset paramter section to default.",
1122+
help="Reset parameter section to default.",
11241123
use_container_width=True,
11251124
):
11261125
self.parameter_manager.reset_to_default_parameters()
1127-
streamlit_js_eval(js_expressions="parent.window.location.reload()")
1126+
self.parameter_manager.clear_parameter_session_state()
1127+
st.toast("Parameters reset to defaults")
1128+
st.rerun()
11281129
with cols[1]:
11291130
if self.parameter_manager.params_file.exists():
11301131
with open(self.parameter_manager.params_file, "rb") as f:
@@ -1148,12 +1149,16 @@ def parameter_section(self, custom_parameter_function) -> None:
11481149

11491150
with cols[2]:
11501151
up = st.file_uploader(
1151-
"⬆️ Import parameters", help="Reset parameter section to default."
1152+
"⬆️ Import parameters",
1153+
help="Import previously exported parameters.",
1154+
key="param_import_uploader"
11521155
)
11531156
if up is not None:
11541157
with open(self.parameter_manager.params_file, "w") as f:
11551158
f.write(up.read().decode("utf-8"))
1156-
streamlit_js_eval(js_expressions="parent.window.location.reload()")
1159+
self.parameter_manager.clear_parameter_session_state()
1160+
st.toast("Parameters imported")
1161+
st.rerun()
11571162

11581163
def execution_section(
11591164
self,

tests/test_parameter_presets.py

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -182,33 +182,43 @@ def test_get_preset_description_empty_for_nonexistent_preset(self, temp_workflow
182182

183183
assert desc == ""
184184

185-
def test_apply_preset_updates_session_state(self, temp_workflow_dir, sample_presets, temp_cwd):
186-
"""Test that apply_preset correctly updates session state."""
185+
def test_apply_preset_deletes_session_state_keys(self, temp_workflow_dir, sample_presets, temp_cwd):
186+
"""Test that apply_preset deletes session_state keys instead of setting them."""
187187
with open("presets.json", "w") as f:
188188
json.dump(sample_presets, f)
189189

190190
pm = ParameterManager(temp_workflow_dir)
191+
192+
# Pre-populate session_state with old values
193+
mock_streamlit.session_state[f"{pm.topp_param_prefix}ToolA:1:param1"] = 999.0
194+
mock_streamlit.session_state[f"{pm.topp_param_prefix}ToolA:1:param2"] = "old_value"
195+
mock_streamlit.session_state[f"{pm.topp_param_prefix}ToolB:1:param3"] = 999
196+
191197
result = pm.apply_preset("Preset A")
192198

193199
assert result is True
194200

195-
# Check TOPP tool parameters in session state
196-
assert mock_streamlit.session_state[f"{pm.topp_param_prefix}ToolA:1:param1"] == 10.0
197-
assert mock_streamlit.session_state[f"{pm.topp_param_prefix}ToolA:1:param2"] == "value2"
198-
assert mock_streamlit.session_state[f"{pm.topp_param_prefix}ToolB:1:param3"] == 5
201+
# Keys should be DELETED (not set to new values) so widgets re-initialize fresh
202+
assert f"{pm.topp_param_prefix}ToolA:1:param1" not in mock_streamlit.session_state
203+
assert f"{pm.topp_param_prefix}ToolA:1:param2" not in mock_streamlit.session_state
204+
assert f"{pm.topp_param_prefix}ToolB:1:param3" not in mock_streamlit.session_state
199205

200-
def test_apply_preset_handles_general_params(self, temp_workflow_dir, sample_presets, temp_cwd):
201-
"""Test that apply_preset correctly handles _general parameters."""
206+
def test_apply_preset_deletes_general_param_keys(self, temp_workflow_dir, sample_presets, temp_cwd):
207+
"""Test that apply_preset deletes _general parameter keys from session_state."""
202208
with open("presets.json", "w") as f:
203209
json.dump(sample_presets, f)
204210

205211
pm = ParameterManager(temp_workflow_dir)
212+
213+
# Pre-populate session_state with old value
214+
mock_streamlit.session_state[f"{pm.param_prefix}general_param"] = "old_value"
215+
206216
result = pm.apply_preset("Preset B")
207217

208218
assert result is True
209219

210-
# Check general parameter in session state
211-
assert mock_streamlit.session_state[f"{pm.param_prefix}general_param"] == "general_value"
220+
# Key should be DELETED so widget re-initializes fresh
221+
assert f"{pm.param_prefix}general_param" not in mock_streamlit.session_state
212222

213223
def test_apply_preset_saves_to_params_file(self, temp_workflow_dir, sample_presets, temp_cwd):
214224
"""Test that apply_preset saves parameters to params.json."""
@@ -265,6 +275,33 @@ def test_apply_preset_preserves_existing_params(self, temp_workflow_dir, sample_
265275
# New params from preset should be added
266276
assert saved_params["ToolA"]["param1"] == 10.0
267277

278+
def test_clear_parameter_session_state(self, temp_workflow_dir):
279+
"""Test that clear_parameter_session_state removes all parameter keys."""
280+
pm = ParameterManager(temp_workflow_dir)
281+
282+
# Add various keys to session_state
283+
mock_streamlit.session_state[f"{pm.param_prefix}param1"] = "value1"
284+
mock_streamlit.session_state[f"{pm.param_prefix}param2"] = "value2"
285+
mock_streamlit.session_state[f"{pm.topp_param_prefix}Tool:1:param"] = 10.0
286+
mock_streamlit.session_state["unrelated_key"] = "should_remain"
287+
288+
pm.clear_parameter_session_state()
289+
290+
# Parameter keys should be deleted
291+
assert f"{pm.param_prefix}param1" not in mock_streamlit.session_state
292+
assert f"{pm.param_prefix}param2" not in mock_streamlit.session_state
293+
assert f"{pm.topp_param_prefix}Tool:1:param" not in mock_streamlit.session_state
294+
295+
# Unrelated keys should remain
296+
assert mock_streamlit.session_state["unrelated_key"] == "should_remain"
297+
298+
def test_clear_parameter_session_state_empty(self, temp_workflow_dir):
299+
"""Test that clear_parameter_session_state handles empty session_state."""
300+
pm = ParameterManager(temp_workflow_dir)
301+
302+
# Should not raise even with no matching keys
303+
pm.clear_parameter_session_state()
304+
268305

269306
class TestWorkflowNameParameter:
270307
"""Tests for the workflow_name parameter in ParameterManager."""

0 commit comments

Comments
 (0)