""" Verify insertions tab โ€” step through CR/TS screenshot pairs from a review package (produced locally by the `cr-visual-pair` skill) and record OK / Not OK / Wrong match verdicts. Verdicts live in st.session_state only; the user downloads a text log at the end. """ import base64 import io import json import mimetypes import zipfile from pathlib import Path import streamlit as st VERDICT_LABELS = { "ok": "OK", "not_ok": "NOT OK", "wrong_match": "WRONG MATCH (page shown did not correspond)", } SKILL_PATH = Path(__file__).parent / "skills" / "cr-visual-pair" / "SKILL.md" _ZOOM_CSS = """ """ def _img_data_uri(path: Path) -> str: mime = mimetypes.guess_type(path.name)[0] or "image/png" data = base64.b64encode(path.read_bytes()).decode() return f"data:{mime};base64,{data}" def _render_zoomable_image(img_path: Path, anchor_id: str) -> None: anchor_id = "".join(c if c.isalnum() else "-" for c in anchor_id) data_uri = _img_data_uri(img_path) st.markdown( f"""
×
""", unsafe_allow_html=True, ) def _render_skill_card() -> None: with st.expander("๐ŸŸข cr-visual-pair"): if not SKILL_PATH.exists(): st.error(f"Skill file not found at {SKILL_PATH}") return content = SKILL_PATH.read_text() st.caption( "Run this skill locally (Windows + Word) to produce a `review_package.zip` " "from an ApplyCRs run. Save it as `~/.claude/skills/cr-visual-pair/SKILL.md`, " "or download it below." ) st.download_button( "โฌ‡ Download SKILL.md", data=content, file_name="SKILL.md", mime="text/markdown", key="verify_skill_download", ) st.code(content, language="markdown") def _extract_package(uploaded, dest_dir: Path) -> dict | None: dest_dir.mkdir(parents=True, exist_ok=True) zip_bytes = uploaded.getbuffer() with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: zf.extractall(dest_dir) manifest_path = dest_dir / "manifest.json" if not manifest_path.exists(): return None return json.loads(manifest_path.read_text()) def _build_log(manifest: dict, verdicts: dict, review_dir: Path) -> str: lines = [] for pair in manifest.get("pairs", []): pid = pair["id"] verdict = verdicts.get(pid) verdict_label = VERDICT_LABELS.get(verdict, "UNREVIEWED") ts_spec = pair.get("ts_spec", "?") ts_version = pair.get("ts_version", "?") section = pair.get("section_number", "?") match_note = "" if pair.get("matched", True) else " [auto-flagged: low-confidence match]" lines.append( f"TS {ts_spec} v{ts_version} [section {section}] : {verdict_label}{match_note}" ) for change in pair.get("changes", []): change_note = "" if change.get("matched", True) else " [low-confidence match]" lines.append( f" - CR {change.get('cr_uid', '?')} ({change.get('change_type', '?')})" f"{change_note}" ) return "\n".join(lines) + "\n" def render_verify_tab(session_dir_fn, sid: str) -> None: st.subheader("Verify insertions") st.caption( "Upload a review package (generated locally with the `cr-visual-pair` skill) " "and step through each CR โ†” TS screenshot pair to confirm the insertion is correct." ) _render_skill_card() st.markdown(_ZOOM_CSS, unsafe_allow_html=True) review_dir = session_dir_fn(sid) / "review" manifest_key = "verify_manifest" review_dir_key = "verify_review_dir" if manifest_key not in st.session_state: uploaded = st.file_uploader( "Upload review package (.zip)", type=["zip"], key="verify_upload" ) if uploaded is not None: manifest = _extract_package(uploaded, review_dir) if manifest is None: st.error("This zip does not contain a manifest.json โ€” not a valid review package.") return st.session_state[manifest_key] = manifest st.session_state[review_dir_key] = str(review_dir) st.session_state["verify_idx"] = 0 st.session_state["verify_verdicts"] = {} st.rerun() return manifest = st.session_state[manifest_key] pairs = manifest.get("pairs", []) review_dir = Path(st.session_state[review_dir_key]) if not pairs: st.warning("This review package has no pairs to review.") if st.button("Upload a different package"): for k in (manifest_key, review_dir_key, "verify_idx", "verify_verdicts"): st.session_state.pop(k, None) st.rerun() return idx = st.session_state.setdefault("verify_idx", 0) verdicts = st.session_state.setdefault("verify_verdicts", {}) idx = max(0, min(idx, len(pairs) - 1)) st.session_state["verify_idx"] = idx pair = pairs[idx] pid = pair["id"] n_ok = sum(1 for v in verdicts.values() if v == "ok") n_not_ok = sum(1 for v in verdicts.values() if v == "not_ok") n_wrong = sum(1 for v in verdicts.values() if v == "wrong_match") n_reviewed = len(verdicts) st.caption( f"Pair {idx + 1} / {len(pairs)} ยท " f"Reviewed: {n_reviewed}/{len(pairs)} ยท โœ… {n_ok} โŒ {n_not_ok} โš  {n_wrong}" ) st.markdown( f"**TS** `{pair.get('ts_spec', '?')}` v{pair.get('ts_version', '?')} ยท " f"section `{pair.get('section_number', '?')}`" ) if not pair.get("matched", True): st.warning( "โš  The pairing tool could not confidently locate this section's page in the " "TS โ€” double-check whether it's the right page before judging the insertion." ) existing_verdict = verdicts.get(pid) if existing_verdict: st.info(f"Current verdict: **{VERDICT_LABELS[existing_verdict]}**") col_cr, col_ts = st.columns(2) with col_cr: st.markdown("**CR (source change)**") changes = pair.get("changes", []) if changes: for change in changes: label = f"`{change.get('cr_uid', '?')}` ยท {change.get('change_type', '?')}" if not change.get("matched", True): label += " โš  low-confidence" st.caption(label) if change.get("summary"): st.caption(change["summary"]) cr_images = change.get("cr_images", []) if cr_images: for img_idx, img in enumerate(cr_images): img_path = review_dir / img if img_path.exists(): _render_zoomable_image( img_path, f"cr-zoom-{pid}-{change.get('cr_uid', '?')}-{img_idx}" ) else: st.error(f"Missing image: {img}") else: st.warning("No CR image for this change.") else: st.warning("No CR changes recorded for this pair.") with col_ts: st.markdown("**TS (applied output)**") ts_images = pair.get("ts_images", []) if ts_images: for img_idx, img in enumerate(ts_images): img_path = review_dir / img if img_path.exists(): _render_zoomable_image(img_path, f"ts-zoom-{pid}-{img_idx}") else: st.error(f"Missing image: {img}") else: st.warning("No TS image for this pair โ€” pairing tool did not find a candidate page.") def _advance(): if st.session_state["verify_idx"] < len(pairs) - 1: st.session_state["verify_idx"] += 1 col1, col2, col3, col4, col5 = st.columns(5) with col1: if st.button("โœ… OK", type="primary", key=f"ok_{pid}"): verdicts[pid] = "ok" _advance() st.rerun() with col2: if st.button("โŒ Not OK", key=f"not_ok_{pid}"): verdicts[pid] = "not_ok" _advance() st.rerun() with col3: if st.button("โš  Wrong match", key=f"wrong_{pid}"): verdicts[pid] = "wrong_match" _advance() st.rerun() with col4: if st.button("โ† Prev", disabled=idx == 0, key=f"prev_{pid}"): st.session_state["verify_idx"] -= 1 st.rerun() with col5: if st.button("Next โ†’", disabled=idx == len(pairs) - 1, key=f"next_{pid}"): st.session_state["verify_idx"] += 1 st.rerun() if idx == len(pairs) - 1 and pid in verdicts: st.success("Review complete โ€” all pairs have a verdict. Download the log below.") st.divider() log_text = _build_log(manifest, verdicts, review_dir) st.download_button( "โฌ‡ Download review log", data=log_text, file_name=f"cr_review_{sid[:8]}.log", mime="text/plain", ) if st.button("Upload a different package", key="verify_reset"): for k in (manifest_key, review_dir_key, "verify_idx", "verify_verdicts"): st.session_state.pop(k, None) st.rerun()