Files
cowork-local/tools/extract_handwritten.py
1419587401
CI / test (push) Canceled after 0s
Feature/fsg gamma team ui fix (#3)
## Summary

What changed and why?

## Change Type

- [ ] Cowork feature
- [ ] Bug fix
- [x] Core AI contribution
- [ ] Test / hardening
- [ ] Performance
- [ ] Documentation

## Related Work

Cowork Task:

Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets

Core AI Issue:

Core Task:

Related PR:

## Scope

What is intentionally included?

What is intentionally NOT included?

## Validation

- [ ] Unit tests
- [ ] Integration tests
- [ ] Manual verification
- [ ] Regression check

Commands / evidence:

## Security Impact

Permission / credential / network / customer data impact:

## Compatibility

- [ ] No breaking change
- [ ] Breaking change documented

## Reviewer Notes

Anything Cowork reviewers should pay attention to.

---------

Co-authored-by: Hiep Ha Van <hiephv3@fpt.com>
Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com>
Co-authored-by: Lam Hoang Van <lamhv7@fpt.com>
Co-authored-by: NamPDT <minhanhpkpro@gmail.com>
Reviewed-on: #3
2026-08-20 12:12:56 +00:00

158 lines
6.5 KiB
Python

"""Lift the hand-written audit sections out of 10-18.ui-audit.html.
That file was edited by hand: eight sections carry richer wireframes, prose and
interactive tables than the generator produces, plus the CSS and scripts they
need. Keeping two HTML files around means they drift, so this pulls the
hand-written parts into ``tools/audit_handwritten.py`` — a data module the
builder merges back in, making ``docs/ui-audit.html`` the single output again.
Two things are tokenised out before storing, so they stay generated rather than
frozen at extraction time:
{{SHOT}} the screenshot block (keeps ~8 MB of base64 out of the module)
{{CONTROLS}} the AST-derived control inventory (must track controls.json)
Workflow when you hand-edit one of those sections directly in the page:
1. edit docs/ui-audit.html
2. python tools/extract_handwritten.py (reads it back into the module)
3. python tools/build_audit_page.py (regenerates, edits preserved)
Pass another filename to import sections from a different copy.
"""
from __future__ import annotations
import contextlib
import io
import re
import sys
import tempfile
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
DOCS = REPO / "docs"
OUT = REPO / "tools" / "audit_handwritten.py"
# Hand-written sections are DETECTED, not listed: every section whose text
# differs from what the generator alone would emit is stored.
#
# There used to be a fixed list here, and it cost a section — "Monitoring ▸ Công
# cụ" was hand-written but missing from the list, so each rebuild quietly put
# the generated version back. Diffing against the live output cannot work (it
# already contains the merged result and would find nothing), so the reference
# is a generator-only render produced in-process, with the merge disabled.
#
# These are the ones known so far; anything else detected is added on top.
KNOWN = [
"monitoring-sự-kiện-bảo-mật", "monitoring-lịch-sử-gọi-mcp",
"monitoring-nhật-ký-hành-động", "monitoring-trạng-thái-agent",
"monitoring-agents-admin", "monitoring-icon", "monitoring-công-cụ",
"dialog-settings", "dialog-task-editor",
]
SECTION = re.compile(r'<section class="sec" id="([^"]+)">(.*?)</section>', re.S)
BODY = re.compile(r'(<div class="bd">.*)', re.S)
SHOT = re.compile(r'<div class="shot">.*?</div>', re.S)
CONTROLS = re.compile(r'<details class="ctl">.*?</details>', re.S)
STYLE = re.compile(r"<style>(.*?)</style>", re.S)
SCRIPT = re.compile(r"<script>(.*?)</script>", re.S)
def bodies(html: str) -> dict[str, str]:
"""slug -> the section's <div class="bd"> … </div>, header excluded."""
out = {}
for m in SECTION.finditer(html):
b = BODY.search(m.group(2))
if b:
out[m.group(1)] = b.group(1).strip()
return out
def norm(s: str) -> str:
return re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", s)).strip()
def main(argv: list[str]) -> int:
src_path = DOCS / (argv[0] if argv else "ui-audit.html")
if not src_path.exists():
print(f"khong thay {src_path}")
return 1
src = src_path.read_text(encoding="utf-8")
# Screenshots are re-embedded by the builder; keep the base64 out of here.
hand = bodies(re.sub(r"data:image/png;base64,[A-Za-z0-9+/=]+", "<IMG>", src))
sys.path.insert(0, str(REPO / "tools"))
import build_audit_page as B
# Render what the generator ALONE would produce, into a temp file, and treat
# every section that differs from it as hand-written.
with tempfile.TemporaryDirectory() as tmp:
keep_out, keep_hand = B.OUT, B.HAND_SECTIONS
B.OUT, B.HAND_SECTIONS = Path(tmp) / "gen-only.html", {}
try:
with contextlib.redirect_stdout(io.StringIO()):
B.main()
made = bodies(re.sub(r"data:image/png;base64,[A-Za-z0-9+/=]+", "<IMG>",
B.OUT.read_text(encoding="utf-8")))
finally:
B.OUT, B.HAND_SECTIONS = keep_out, keep_hand
detected = sorted(s for s, body in hand.items() if norm(body) != norm(made.get(s, "")))
slugs = [s for s in hand if s in set(detected) | set(KNOWN)]
new = [s for s in detected if s not in KNOWN]
gone = [s for s in KNOWN if s in hand and s not in detected]
if new:
print(f"phat hien them section viet tay: {new}")
if gone:
# Not an error: a hand section can be edited back to match the generator.
print(f"section trong KNOWN nay giong ban sinh: {gone}")
stored = {}
for slug in slugs:
body = hand[slug]
body = SHOT.sub("{{SHOT}}", body, count=1)
body = CONTROLS.sub("{{CONTROLS}}", body, count=1)
stored[slug] = body
# CSS rules and scripts the hand edits added. Compared against the builder's
# OWN constants, not its output — the output already carries the merge.
extra_css = "\n".join(
ln for ln in STYLE.search(src).group(1).splitlines()
if ln.strip() and ln not in B.CSS)
# Scripts already sitting INSIDE a stored section travel with it — collecting
# them again would bind every listener twice (the +/- steppers would then
# count by two). Only page-level scripts belong in EXTRA_JS.
gen_js = {norm(B.JS)}
in_section = "".join(stored.values())
extra_js = [j for j in SCRIPT.findall(src)
if norm(j) not in gen_js and j not in in_section]
parts = [
'"""Hand-written audit sections, extracted from 10-18.ui-audit.html.\n\n'
"GENERATED by tools/extract_handwritten.py — do not edit by hand; edit the\n"
"source HTML and re-run it. build_audit_page.py substitutes {{SHOT}} and\n"
'{{CONTROLS}} so those stay generated.\n"""\n',
"SECTIONS = {",
]
for slug, body in stored.items():
parts.append(f" {slug!r}: {body!r},")
parts.append("}\n")
parts.append(f"EXTRA_CSS = {extra_css!r}\n")
parts.append("EXTRA_JS = [")
for j in extra_js:
parts.append(f" {j!r},")
parts.append("]\n")
OUT.write_text("\n".join(parts), encoding="utf-8")
print(f"section viet tay : {len(stored)}")
for slug, body in stored.items():
print(f" {slug:32} {len(body):>7,} ky tu"
f" shot={'{{SHOT}}' in body} ctl={'{{CONTROLS}}' in body}")
print(f"CSS them : {len(extra_css.splitlines())} dong")
print(f"script them : {len(extra_js)}")
print(f"ghi -> {OUT.relative_to(REPO)} ({OUT.stat().st_size / 1024:.0f} KB)")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))