diff --git a/src/gui/app.py b/src/gui/app.py index 7d48789..6f95ba1 100644 --- a/src/gui/app.py +++ b/src/gui/app.py @@ -213,11 +213,7 @@ if uploaded is not None: # Individual group cards decisions = st.session_state["review_decisions"] for i, group in enumerate(result.match_groups): - decision = 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() + match_group_card(group, df, group_num=i + 1) # Show decision summary if decisions: diff --git a/src/gui/components.py b/src/gui/components.py index 3d5958f..3504644 100644 --- a/src/gui/components.py +++ b/src/gui/components.py @@ -262,26 +262,37 @@ def match_group_card( group: MatchResult, df: pd.DataFrame, group_num: int, -) -> Optional[bool]: +) -> None: """Render an expandable match group card with side-by-side diff. - Returns: - True — user clicked Merge (accept match) - False — user clicked Keep Both (reject match) - None — no decision yet + Decisions are stored directly in ``st.session_state["review_decisions"]`` + via ``on_click`` callbacks so that other expanders keep their state on + rerun. """ confidence = group.confidence - auto_expand = confidence < 95.0 matched_on = ", ".join(group.matched_on) 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 = ( f"Group {group_num}: {n_rows} rows " f"(confidence: {confidence:.0f}%) " 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 display_cols = [c for c in df.columns if not str(c).startswith("_norm_")] rows_data = [] @@ -312,28 +323,32 @@ def match_group_card( styled = compare_df.style.apply(_highlight_diffs, axis=0) st.dataframe(styled, use_container_width=True) - # Action buttons - btn_left, btn_mid, btn_right = st.columns(3) - merge_key = f"merge_{group.group_id}" - 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: + if has_decision: + # Show current decision with option to undo + if decision_val is True: st.success("Decision: Merge") - elif decision is False: + else: 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) # ---------------------------------------------------------------------------