feat: multi-row survivor support in match group review

Replace radio + Merge/Keep Both buttons with per-row checkboxes
and a single Confirm button. Users can now:

- Keep all rows (not duplicates) — check all, confirm
- Merge to one row — uncheck all but one, optionally customize columns
- Split a group — keep some rows, remove others (new capability)

Decision format changed from {action, survivor_idx, overrides} to
{keep_indices, overrides}. apply_review_decisions() updated to handle
all three modes. Batch actions updated accordingly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 23:52:45 +00:00
parent debb0cb516
commit 863fe89f2c
2 changed files with 148 additions and 113 deletions

View File

@@ -202,16 +202,14 @@ if uploaded is not None:
def _accept_all():
for g in result.match_groups:
st.session_state["review_decisions"][g.group_id] = {
"action": True,
"survivor_idx": g.survivor_index,
"keep_indices": [g.survivor_index],
"overrides": {},
}
def _reject_all():
for g in result.match_groups:
st.session_state["review_decisions"][g.group_id] = {
"action": False,
"survivor_idx": g.survivor_index,
"keep_indices": list(g.row_indices),
"overrides": {},
}
@@ -234,27 +232,46 @@ if uploaded is not None:
# Show decision summary
if decisions:
st.divider()
accepted = sum(
1 for v in decisions.values()
if isinstance(v, dict) and v.get("action") is True
)
customized = sum(
1 for v in decisions.values()
if isinstance(v, dict) and v.get("action") is True
and v.get("overrides")
)
rejected = sum(
1 for v in decisions.values()
if isinstance(v, dict) and v.get("action") is False
)
pending = len(result.match_groups) - len(decisions)
merged = 0
customized = 0
split = 0
kept_all = 0
for v in decisions.values():
if not isinstance(v, dict):
continue
ki = v.get("keep_indices", [])
# Find the matching group size
gid_for_v = next(
(gid for gid, d in decisions.items() if d is v),
None,
)
group_size = next(
(len(g.row_indices) for g in result.match_groups
if g.group_id == gid_for_v),
0,
)
if len(ki) == group_size:
kept_all += 1
elif len(ki) == 1:
if v.get("overrides"):
customized += 1
else:
merged += 1
else:
split += 1
summary_parts = [f"{accepted} merged"]
pending = len(result.match_groups) - len(decisions)
parts = []
if merged:
parts.append(f"{merged} merged")
if customized:
summary_parts.append(f"{customized} customized")
summary_parts.append(f"{rejected} kept both")
summary_parts.append(f"{pending} pending")
st.caption("Decisions: " + ", ".join(summary_parts))
parts.append(f"{customized} customized")
if split:
parts.append(f"{split} split")
if kept_all:
parts.append(f"{kept_all} kept all")
parts.append(f"{pending} pending")
st.caption("Decisions: " + ", ".join(parts))
# Apply decisions and offer download
if st.button(