"""
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()