Spaces:
Sleeping
Sleeping
| """ | |
| 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 = """ | |
| <style> | |
| .cr-zoom-thumb { cursor: zoom-in; display: block; } | |
| .cr-zoom-thumb img { max-width: 100%; border-radius: 4px; display: block; } | |
| .cr-zoom-overlay { | |
| display: none; | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0, 0, 0, 0.9); | |
| z-index: 999999; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .cr-zoom-overlay:target { display: flex; } | |
| .cr-zoom-overlay img { | |
| max-width: 95vw; | |
| max-height: 95vh; | |
| object-fit: contain; | |
| cursor: zoom-out; | |
| } | |
| .cr-zoom-close { | |
| position: fixed; | |
| top: 16px; | |
| right: 24px; | |
| color: #fff; | |
| font-size: 2.5rem; | |
| text-decoration: none; | |
| line-height: 1; | |
| } | |
| </style> | |
| """ | |
| 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""" | |
| <a class="cr-zoom-thumb" href="#{anchor_id}"> | |
| <img src="{data_uri}"> | |
| </a> | |
| <div class="cr-zoom-overlay" id="{anchor_id}"> | |
| <a class="cr-zoom-close" href="#">×</a> | |
| <a href="#"><img src="{data_uri}"></a> | |
| </div> | |
| """, | |
| 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() | |