feat(review): inline file uploader instead of redirect home
When a user lands on Review without an upload, show a file uploader on the page itself and auto-run the analyzer once a file is picked, rather than bouncing them to the home page with a "Back to home" button. Auto-analyze is the right default here: the user is already on the Review page, so they've implicitly committed to a scan. Stashing the bytes in the same session-state keys the home page uses keeps the rest of the flow (encoding picker, gate, tool pages) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -286,15 +286,51 @@ st.caption(
|
|||||||
"the rest before applying."
|
"the rest before applying."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Pre-flight: nothing to review without an upload.
|
# Pre-flight: if nothing has been uploaded yet, let the user upload
|
||||||
|
# directly from this page instead of bouncing them back to the home
|
||||||
|
# screen. Once a file is picked, we auto-run the analyzer (the user is
|
||||||
|
# already on the Review page — they've implicitly committed to a scan),
|
||||||
|
# stash the result, and rerun so the rest of the page picks it up.
|
||||||
findings: list[Finding] = st.session_state.get("home_findings") or []
|
findings: list[Finding] = st.session_state.get("home_findings") or []
|
||||||
upload_name = st.session_state.get("home_uploaded_name")
|
upload_name = st.session_state.get("home_uploaded_name")
|
||||||
|
|
||||||
if not upload_name:
|
if not upload_name:
|
||||||
st.warning("No file uploaded. Go back to the home page and upload a CSV or Excel file first.")
|
st.info(
|
||||||
if st.button("Back to home"):
|
"Upload a CSV or Excel file to begin reviewing. The analyzer runs "
|
||||||
st.switch_page("app.py")
|
"locally and your data never leaves this computer."
|
||||||
st.stop()
|
)
|
||||||
|
review_upload = st.file_uploader(
|
||||||
|
"Choose a file",
|
||||||
|
type=["csv", "tsv", "xlsx", "xls"],
|
||||||
|
key="review_upload",
|
||||||
|
help="Drag-and-drop or browse for a CSV, TSV, or Excel file.",
|
||||||
|
)
|
||||||
|
if review_upload is None:
|
||||||
|
st.stop()
|
||||||
|
|
||||||
|
# New file → stash bytes + size + name, drop any stale state, then
|
||||||
|
# run the analyzer. The rerun at the bottom lets the rest of this
|
||||||
|
# page render with the upload in place.
|
||||||
|
same_file = (
|
||||||
|
st.session_state.get("home_uploaded_name") == review_upload.name
|
||||||
|
and st.session_state.get("home_uploaded_size") == review_upload.size
|
||||||
|
)
|
||||||
|
if not same_file:
|
||||||
|
st.session_state["home_uploaded_name"] = review_upload.name
|
||||||
|
st.session_state["home_uploaded_size"] = review_upload.size
|
||||||
|
st.session_state["home_uploaded_bytes"] = review_upload.getvalue()
|
||||||
|
st.session_state.pop("home_findings", None)
|
||||||
|
st.session_state.pop("home_skipped", None)
|
||||||
|
st.session_state.pop("review_decisions", None)
|
||||||
|
st.session_state.pop("normalization_result", None)
|
||||||
|
st.session_state.pop("normalization_for", None)
|
||||||
|
st.session_state.pop("encoding_override", None)
|
||||||
|
|
||||||
|
if st.session_state.get("home_findings") is None:
|
||||||
|
with st.spinner("Analyzing…"):
|
||||||
|
st.session_state["home_findings"] = _run_analysis_with_override(None)
|
||||||
|
st.session_state["home_skipped"] = False
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
# ---- Encoding picker --------------------------------------------------------
|
# ---- Encoding picker --------------------------------------------------------
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -156,16 +156,24 @@ class TestPipelineRunnerWorkflow:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class TestReviewWorkflow:
|
class TestReviewWorkflow:
|
||||||
"""The Review page is the gate-fixer. Without an upload it shows a
|
"""The Review page is the gate-fixer. Without an upload it shows
|
||||||
'go back to home' message. With an upload it runs the analyzer and
|
its own file uploader so the user can start the flow from this
|
||||||
shows findings."""
|
page directly. With an upload it runs the analyzer and shows
|
||||||
|
findings."""
|
||||||
|
|
||||||
def test_no_upload_shows_back_to_home(self, app_factory):
|
def test_no_upload_shows_inline_uploader(self, app_factory):
|
||||||
app = app_factory("0_Review")
|
app = app_factory("0_Review")
|
||||||
app.run()
|
app.run()
|
||||||
text = collected_text(app)
|
text = collected_text(app)
|
||||||
# Page shows ``No file uploaded`` + ``Back to home``.
|
# Page should invite the user to upload, not redirect home.
|
||||||
assert "No file uploaded" in text or "uploaded" in text.lower()
|
assert "Upload" in text or "Choose a file" in text, (
|
||||||
|
f"Review page should expose an inline uploader; got:\n{text[:400]}"
|
||||||
|
)
|
||||||
|
# The 'Back to home' button is gone — the page is self-contained now.
|
||||||
|
labels = [b.label for b in app.button]
|
||||||
|
assert not any("Back to home" in lbl for lbl in labels), (
|
||||||
|
f"Back-to-home button should be removed; got buttons: {labels}"
|
||||||
|
)
|
||||||
|
|
||||||
def test_with_upload_shows_review_content(
|
def test_with_upload_shows_review_content(
|
||||||
self, app_factory, small_csv_bytes,
|
self, app_factory, small_csv_bytes,
|
||||||
|
|||||||
Reference in New Issue
Block a user