From e86c23846c9f3b71ea3c5e95a815a0c5e537c63b Mon Sep 17 00:00:00 2001 From: jkvision1101 Date: Wed, 15 Jul 2026 18:20:31 +0900 Subject: [PATCH 1/2] InputTOPP reactive parameter --- src/workflow/StreamlitUI.py | 55 ++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/workflow/StreamlitUI.py b/src/workflow/StreamlitUI.py index 9c24dc2c..12601343 100644 --- a/src/workflow/StreamlitUI.py +++ b/src/workflow/StreamlitUI.py @@ -819,7 +819,6 @@ def format_files(input: Any) -> List[str]: self.parameter_manager.save_parameters() - @st.fragment def input_TOPP( self, topp_tool_name: str, @@ -831,6 +830,7 @@ def input_TOPP( display_subsection_tabs: bool = False, custom_defaults: dict = {}, tool_instance_name: str = None, + reactive: bool = False, ) -> None: """ Generates input widgets for TOPP tool parameters dynamically based on the tool's @@ -842,6 +842,8 @@ def input_TOPP( num_cols (int, optional): Number of columns to use for the layout. Defaults to 3. exclude_parameters (List[str], optional): List of parameter names to exclude from the widget. Defaults to an empty list. include_parameters (List[str], optional): List of parameter names to include in the widget. Defaults to an empty list. + flag_parameters (List[str], optional): List of parameter names that should + be treated as no-value CLI flags during command construction. display_tool_name (bool, optional): Whether to display the TOPP tool name. Defaults to True. display_subsections (bool, optional): Whether to split parameters into subsections based on the prefix. Defaults to True. display_subsection_tabs (bool, optional): Whether to display main subsections in separate tabs (if more than one main section). Defaults to False. @@ -852,7 +854,58 @@ def input_TOPP( defaults to topp_tool_name. The instance name is used for session state keys and parameter storage, while topp_tool_name is used for the actual tool executable and ini file creation. + reactive (bool, optional): If True, widget changes trigger the parent + section to re-render, enabling conditional UI based on this widget's + value. Use when downstream UI depends on a parameter value (e.g., + TMT type driving channel count). Default is False. """ + if reactive: + self._input_TOPP_impl( + topp_tool_name, num_cols, exclude_parameters, include_parameters, + flag_parameters, display_tool_name, display_subsections, + display_subsection_tabs, custom_defaults, tool_instance_name, + ) + else: + self._input_TOPP_fragmented( + topp_tool_name, num_cols, exclude_parameters, include_parameters, + flag_parameters, display_tool_name, display_subsections, + display_subsection_tabs, custom_defaults, tool_instance_name, + ) + + @st.fragment + def _input_TOPP_fragmented( + self, + topp_tool_name: str, + num_cols: int = 4, + exclude_parameters: List[str] = [], + include_parameters: List[str] = [], + flag_parameters: List[str] = [], + display_tool_name: bool = True, + display_subsections: bool = True, + display_subsection_tabs: bool = False, + custom_defaults: dict = {}, + tool_instance_name: str = None, + ) -> None: + self._input_TOPP_impl( + topp_tool_name, num_cols, exclude_parameters, include_parameters, + flag_parameters, display_tool_name, display_subsections, + display_subsection_tabs, custom_defaults, tool_instance_name, + ) + + def _input_TOPP_impl( + self, + topp_tool_name: str, + num_cols: int = 4, + exclude_parameters: List[str] = [], + include_parameters: List[str] = [], + flag_parameters: List[str] = [], + display_tool_name: bool = True, + display_subsections: bool = True, + display_subsection_tabs: bool = False, + custom_defaults: dict = {}, + tool_instance_name: str = None, + ) -> None: + """Internal implementation of input_TOPP - contains all the widget logic.""" # Default instance name to the tool name when not provided if tool_instance_name is None: tool_instance_name = topp_tool_name From 886d34454b87b69bb1e9a6d34171bbbfeaee95d8 Mon Sep 17 00:00:00 2001 From: Tom David Mueller Date: Thu, 16 Jul 2026 13:38:27 +0200 Subject: [PATCH 2/2] docs: document reactive/conditional-UI pattern for workflow widgets Add developer + agent-facing documentation for the reactive=True option on input_widget, select_input_file and input_TOPP, which un-isolates a parameter widget from its st.fragment so a change reruns configure() and enables conditional UI. - docs/toppframework.py: "Reactive parameters (conditional UI)" block with a combined input_widget + input_TOPP example in the Parameter Input section - .claude/skills/create-workflow.md: UI-widget note, Conditional UI section, checklist item - CLAUDE.md: reactive note in the TOPP Workflows section - docs/build_app.md: pointer to the framework doc --- .claude/skills/create-workflow.md | 22 +++++++++++++++++++++ CLAUDE.md | 2 ++ docs/build_app.md | 2 ++ docs/toppframework.py | 32 +++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+) diff --git a/.claude/skills/create-workflow.md b/.claude/skills/create-workflow.md index 07856db2..38e981dc 100644 --- a/.claude/skills/create-workflow.md +++ b/.claude/skills/create-workflow.md @@ -120,10 +120,31 @@ The 4 pages call these methods respectively: - `self.ui.input_TOPP("ToolName", custom_defaults={})` — auto-generated TOPP parameter UI - `self.ui.input_python("script_name")` — auto-generated Python tool parameter UI - `self.ui.input_widget(key, default, label)` — single custom widget +- `select_input_file`, `input_TOPP` and `input_widget` accept `reactive=True` to rerun `configure()` when the widget changes (for conditional UI — see below) ### Logging - `self.logger.log("message")` — log progress during execution +## Conditional UI (reactive) + +Parameter widgets are isolated in an `st.fragment` by default, so a change reruns only that +widget and can't show/hide other widgets. Pass `reactive=True` to render the widget in the +parent scope instead — a change then reruns `configure()`. Read the live value from +`st.session_state` (not `self.params`, which is stale within the rerun) using +`self.parameter_manager.param_prefix` for custom-widget keys or `topp_param_prefix` for TOPP +keys of the form `":1:"`. + +```python +@st.fragment +def configure(self) -> None: + pm = self.parameter_manager + # changing the tool's `type` selectbox reruns configure() + self.ui.input_TOPP("IsobaricAnalyzer", reactive=True) + iso_type = st.session_state.get(f"{pm.topp_param_prefix}IsobaricAnalyzer:1:type", "") + if iso_type.startswith("tmt"): + self.ui.input_widget("tmt-channels", 10, "TMT channels", widget_type="number") +``` + ## Reference Files - Example workflow: `src/Workflow.py` @@ -144,6 +165,7 @@ The 4 pages call these methods respectively: - [ ] `__init__` calls `super().__init__("Name", st.session_state["workspace"])` - [ ] `upload()`, `configure()`, `execution()`, `results()` implemented - [ ] `@st.fragment` on `configure()` and `results()` +- [ ] `reactive=True` on any widget whose value controls other widgets' visibility - [ ] 4 content pages created in `content/` - [ ] All 4 pages registered as a group in `app.py` - [ ] Default parameters added to `default-parameters.json` if needed diff --git a/CLAUDE.md b/CLAUDE.md index 3eeb1927..e0e0a222 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,6 +89,8 @@ Each workflow gets 4 content pages (upload, configure, run, results) that call ` Decorate `configure()` and `results()` with `@st.fragment` for partial reruns. +For conditional UI (a widget that shows/hides other widgets), pass `reactive=True` to `input_widget`, `select_input_file`, or `input_TOPP` so a change reruns the parent `configure()` instead of only its isolated fragment. Read the changed value from `st.session_state` (not `self.params`, which is stale within the rerun) via `parameter_manager.param_prefix` for custom-widget keys or `topp_param_prefix` for TOPP keys (`":1:"`). + ### Python Tools Custom scripts in `src/python-tools/` define a `DEFAULTS` list for auto-generated UI: diff --git a/docs/build_app.md b/docs/build_app.md index 9a345745..08b7d245 100644 --- a/docs/build_app.md +++ b/docs/build_app.md @@ -133,3 +133,5 @@ Take a look at the example pages `Simple Workflow` or `Workflow with mzML files` ### Complex workflow using TOPP tools This template app features a module in `src/workflow` that allows for complex and long workflows to be built very efficiently. Check out the `TOPP Workflow Framework` page for more information (on the *sidebar*). + +For building **conditional parameter UI** (widgets that appear or disappear based on another parameter's value), see the *Reactive parameters* subsection of the `TOPP Workflow Framework` page's *Parameter Input* section. diff --git a/docs/toppframework.py b/docs/toppframework.py index 592fc5f7..a91b2be7 100644 --- a/docs/toppframework.py +++ b/docs/toppframework.py @@ -132,6 +132,38 @@ def content(): "💡 Access parameter widget values by their **key** in the `self.params` object, e.g. `self.params['mzML-files']` will give all selected mzML files." ) + st.markdown( + """ +### Reactive parameters (conditional UI) + +By default every parameter widget is rendered inside an isolated `st.fragment`: changing it reruns only that widget, which keeps large parameter sections fast but also means a widget **cannot** show or hide other widgets. Pass **`reactive=True`** to `self.ui.input_widget`, `self.ui.select_input_file` or `self.ui.input_TOPP` to render the widget directly in the parent `configure()` scope instead — a change then reruns `configure()`, so you can conditionally render downstream widgets based on the current value. + +Read the current value from `st.session_state` (**not** `self.params`, which is only loaded once per run and is stale within the same rerun) using the parameter-manager prefixes: `param_prefix` for `input_widget` / `select_input_file` keys, and `topp_param_prefix` for TOPP keys of the form `":1:"` (the `` matches the tool's ini, as shown for each widget in the panel). +""" + ) + st.code( + '''@st.fragment +def configure(self) -> None: + pm = self.parameter_manager + + # A custom widget that reveals more widgets when checked + self.ui.input_widget( + "advanced-mode", False, "Advanced mode", + widget_type="checkbox", reactive=True, + ) + if st.session_state.get(f"{pm.param_prefix}advanced-mode", False): + self.ui.input_widget("threads", 4, "Threads", widget_type="number") + + # A TOPP parameter driving conditional UI (e.g. TMT type -> channel count) + self.ui.input_TOPP("IsobaricAnalyzer", reactive=True) + iso_type = st.session_state.get(f"{pm.topp_param_prefix}IsobaricAnalyzer:1:type", "") + if iso_type.startswith("tmt"): + self.ui.input_widget("tmt-channels", 10, "TMT channels", widget_type="number")''' + ) + st.info( + "💡 `reactive=True` is opt-in per widget and defaults to `False`. On `input_TOPP` it un-isolates the whole tool panel, so **any** parameter change reruns `configure()` — leave it off unless another widget's visibility depends on a value in that panel." + ) + with st.expander("**Code documentation**", expanded=True): st.help(StreamlitUI.input_widget) st.help(StreamlitUI.select_input_file)