fix: prevent match group expanders from collapsing on button click

Replace st.rerun() with on_click callbacks so decisions write to
session state before the natural rerun. Decided groups auto-collapse
with status in the label; undecided groups stay expanded. Added undo
button on decided groups.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 23:25:12 +00:00
parent b871ab24fc
commit 39e139d777
2 changed files with 42 additions and 31 deletions

View File

@@ -213,11 +213,7 @@ if uploaded is not None:
# Individual group cards # Individual group cards
decisions = st.session_state["review_decisions"] decisions = st.session_state["review_decisions"]
for i, group in enumerate(result.match_groups): for i, group in enumerate(result.match_groups):
decision = match_group_card(group, df, group_num=i + 1) match_group_card(group, df, group_num=i + 1)
if decision is not None:
decisions[group.group_id] = decision
st.session_state["review_decisions"] = decisions
st.rerun()
# Show decision summary # Show decision summary
if decisions: if decisions:

View File

@@ -262,26 +262,37 @@ def match_group_card(
group: MatchResult, group: MatchResult,
df: pd.DataFrame, df: pd.DataFrame,
group_num: int, group_num: int,
) -> Optional[bool]: ) -> None:
"""Render an expandable match group card with side-by-side diff. """Render an expandable match group card with side-by-side diff.
Returns: Decisions are stored directly in ``st.session_state["review_decisions"]``
True — user clicked Merge (accept match) via ``on_click`` callbacks so that other expanders keep their state on
False — user clicked Keep Both (reject match) rerun.
None — no decision yet
""" """
confidence = group.confidence confidence = group.confidence
auto_expand = confidence < 95.0
matched_on = ", ".join(group.matched_on) matched_on = ", ".join(group.matched_on)
n_rows = len(group.row_indices) n_rows = len(group.row_indices)
gid = group.group_id
decisions = st.session_state.get("review_decisions", {})
has_decision = gid in decisions
decision_val = decisions.get(gid)
# Build label — append decision status if already decided
label = ( label = (
f"Group {group_num}: {n_rows} rows " f"Group {group_num}: {n_rows} rows "
f"(confidence: {confidence:.0f}%) " f"(confidence: {confidence:.0f}%) "
f"[{matched_on}]" f"[{matched_on}]"
) )
if decision_val is True:
label += " — Merged"
elif decision_val is False:
label += " — Kept Both"
with st.expander(label, expanded=auto_expand): # Decided groups collapse; undecided groups stay open
expanded = not has_decision
with st.expander(label, expanded=expanded):
# Build comparison DataFrame # Build comparison DataFrame
display_cols = [c for c in df.columns if not str(c).startswith("_norm_")] display_cols = [c for c in df.columns if not str(c).startswith("_norm_")]
rows_data = [] rows_data = []
@@ -312,28 +323,32 @@ def match_group_card(
styled = compare_df.style.apply(_highlight_diffs, axis=0) styled = compare_df.style.apply(_highlight_diffs, axis=0)
st.dataframe(styled, use_container_width=True) st.dataframe(styled, use_container_width=True)
# Action buttons if has_decision:
btn_left, btn_mid, btn_right = st.columns(3) # Show current decision with option to undo
merge_key = f"merge_{group.group_id}" if decision_val is True:
keep_key = f"keep_{group.group_id}"
with btn_left:
if st.button("Merge", key=merge_key, type="primary"):
return True
with btn_mid:
if st.button("Keep Both", key=keep_key):
return False
# Check session state for previous decisions
decisions = st.session_state.get("review_decisions", {})
if group.group_id in decisions:
decision = decisions[group.group_id]
if decision is True:
st.success("Decision: Merge") st.success("Decision: Merge")
elif decision is False: else:
st.info("Decision: Keep Both") st.info("Decision: Keep Both")
return None def _undo(g=gid):
st.session_state["review_decisions"].pop(g, None)
st.button("Undo", key=f"undo_{gid}", on_click=_undo)
else:
# Action buttons — on_click writes to session state before rerun
def _on_merge(g=gid):
st.session_state["review_decisions"][g] = True
def _on_keep(g=gid):
st.session_state["review_decisions"][g] = False
btn_left, btn_mid, _btn_right = st.columns(3)
with btn_left:
st.button("Merge", key=f"merge_{gid}",
type="primary", on_click=_on_merge)
with btn_mid:
st.button("Keep Both", key=f"keep_{gid}",
on_click=_on_keep)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------