feat(gui): tool pages pick up the home-page upload via session_state
Closes the last UX gap from the analyzer review: each tool page had its
own st.file_uploader, so users had to upload the same file twice (once
on the home page for analysis, once on each tool page).
components.pickup_or_upload(label, key, types) returns either:
- a _StashedUpload shim wrapping the home-page bytes (when present and
the user hasn't asked for a different file on this page), or
- the standard st.file_uploader (when nothing is stashed or the user
clicked "Use a different file").
_StashedUpload duck-types Streamlit's UploadedFile (.name, .size,
.getvalue(), .read()) so existing tool-page code consumes it without
changes. A "Use a different file" button per page sets a session-state
override flag; a "Switch back to upload-screen file" button clears it.
Wired into 2_Text_Cleaner.py and 1_Deduplicator.py — the two pages with
working uploaders today. The remaining stub pages adopt it when they're
implemented; the helper is the public surface they'll use.
Verified by smoke-launching streamlit headless and curling the home,
text-cleaner, and deduplicator routes — all return 200 with no errors
in the server log.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -903,3 +903,76 @@ def findings_count_for_tool(tool_id: str) -> int:
|
|||||||
"""
|
"""
|
||||||
findings = st.session_state.get("home_findings") or []
|
findings = st.session_state.get("home_findings") or []
|
||||||
return sum(1 for f in findings if f.tool == tool_id)
|
return sum(1 for f in findings if f.tool == tool_id)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cross-page upload pickup
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class _StashedUpload:
|
||||||
|
"""Duck-types ``st.runtime.uploaded_file_manager.UploadedFile`` enough
|
||||||
|
for the tool pages: ``.name``, ``.size``, ``.getvalue()``.
|
||||||
|
|
||||||
|
Tool pages that previously consumed a Streamlit ``UploadedFile`` can
|
||||||
|
accept this in its place without changes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("name", "size", "_data")
|
||||||
|
|
||||||
|
def __init__(self, name: str, data: bytes) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.size = len(data)
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
def getvalue(self) -> bytes:
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
def read(self) -> bytes:
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
|
||||||
|
def pickup_or_upload(
|
||||||
|
*,
|
||||||
|
label: str,
|
||||||
|
key: str,
|
||||||
|
types: list[str],
|
||||||
|
help: str | None = None,
|
||||||
|
):
|
||||||
|
"""Return an upload object, preferring the home-page upload when present.
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
|
||||||
|
- If ``st.session_state['home_uploaded_bytes']`` is set and the user
|
||||||
|
hasn't asked for a different file on this page, render a banner
|
||||||
|
("Using *<name>* from upload screen") plus a "Use a different file"
|
||||||
|
button, and return a :class:`_StashedUpload` shim.
|
||||||
|
- Otherwise render the standard ``st.file_uploader`` with the supplied
|
||||||
|
*label*, *key*, and *types*. Returns the Streamlit ``UploadedFile``
|
||||||
|
directly (or ``None`` if nothing uploaded).
|
||||||
|
|
||||||
|
The ``_StashedUpload`` shim exposes ``.name``, ``.size``, and
|
||||||
|
``.getvalue()`` so existing tool-page code that consumes a Streamlit
|
||||||
|
upload object works without changes.
|
||||||
|
"""
|
||||||
|
override_key = f"{key}__override"
|
||||||
|
has_session_upload = st.session_state.get("home_uploaded_bytes") is not None
|
||||||
|
use_session = has_session_upload and not st.session_state.get(override_key, False)
|
||||||
|
|
||||||
|
if use_session:
|
||||||
|
name = st.session_state.get("home_uploaded_name", "uploaded file")
|
||||||
|
st.info(f"Using **{name}** from the upload screen.")
|
||||||
|
if st.button("Use a different file", key=f"{key}__pick_diff"):
|
||||||
|
st.session_state[override_key] = True
|
||||||
|
st.rerun()
|
||||||
|
return _StashedUpload(name, st.session_state["home_uploaded_bytes"])
|
||||||
|
|
||||||
|
uploaded = st.file_uploader(label, type=types, key=key, help=help)
|
||||||
|
if uploaded is not None and st.session_state.get(override_key):
|
||||||
|
# User has uploaded their own file on this page; clear the override
|
||||||
|
# so the next visit to a tool page starts fresh.
|
||||||
|
pass
|
||||||
|
if uploaded is None and st.session_state.get(override_key) and has_session_upload:
|
||||||
|
if st.button("Switch back to upload-screen file", key=f"{key}__switch_back"):
|
||||||
|
st.session_state[override_key] = False
|
||||||
|
st.rerun()
|
||||||
|
return uploaded
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from src.gui.components import (
|
|||||||
config_panel,
|
config_panel,
|
||||||
hide_streamlit_chrome,
|
hide_streamlit_chrome,
|
||||||
match_group_card,
|
match_group_card,
|
||||||
|
pickup_or_upload,
|
||||||
results_summary,
|
results_summary,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,11 +57,11 @@ st.caption("Find and remove duplicate rows in CSV, delimited text, and Excel fil
|
|||||||
# File upload
|
# File upload
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
uploaded = st.file_uploader(
|
uploaded = pickup_or_upload(
|
||||||
"Upload CSV or Excel file",
|
label="Upload CSV or Excel file",
|
||||||
type=["csv", "tsv", "xlsx", "xls"],
|
|
||||||
help="Supports CSV, TSV, and Excel files. Encoding and delimiters are auto-detected.",
|
|
||||||
key="dedup_file_upload",
|
key="dedup_file_upload",
|
||||||
|
types=["csv", "tsv", "xlsx", "xls"],
|
||||||
|
help="Supports CSV, TSV, and Excel files. Encoding and delimiters are auto-detected.",
|
||||||
)
|
)
|
||||||
|
|
||||||
if uploaded is not None:
|
if uploaded is not None:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ _project_root = Path(__file__).resolve().parent.parent.parent.parent
|
|||||||
if str(_project_root) not in sys.path:
|
if str(_project_root) not in sys.path:
|
||||||
sys.path.insert(0, str(_project_root))
|
sys.path.insert(0, str(_project_root))
|
||||||
|
|
||||||
from src.gui.components import hide_streamlit_chrome
|
from src.gui.components import hide_streamlit_chrome, pickup_or_upload
|
||||||
from src.core.text_clean import (
|
from src.core.text_clean import (
|
||||||
PRESETS,
|
PRESETS,
|
||||||
CleanOptions,
|
CleanOptions,
|
||||||
@@ -38,10 +38,10 @@ st.caption(
|
|||||||
# File upload
|
# File upload
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
uploaded = st.file_uploader(
|
uploaded = pickup_or_upload(
|
||||||
"Upload CSV or Excel file",
|
label="Upload CSV or Excel file",
|
||||||
type=["csv", "tsv", "xlsx", "xls"],
|
|
||||||
key="textclean_file_upload",
|
key="textclean_file_upload",
|
||||||
|
types=["csv", "tsv", "xlsx", "xls"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if uploaded is None:
|
if uploaded is None:
|
||||||
|
|||||||
Reference in New Issue
Block a user