Spaces:
Running
Running
Food Desert commited on
Commit ·
0cd97fa
1
Parent(s): 56966b7
Fix tag source color mapping and add UI color smoke checks
Browse files- app.py +34 -6
- scripts/smoke_ui_state.py +81 -0
app.py
CHANGED
|
@@ -749,13 +749,25 @@ def _build_tooltip_payload(row_defs: List[Dict[str, Any]], max_rows: int) -> str
|
|
| 749 |
row_defs_ui = (row_defs or [])[: max(0, int(max_rows))]
|
| 750 |
tips: Dict[str, str] = {}
|
| 751 |
rows: List[List[str]] = []
|
|
|
|
| 752 |
for row in row_defs_ui:
|
| 753 |
tags = _dedupe_norm_tags(row.get("tags", []) if isinstance(row, dict) else [])
|
| 754 |
rows.append(tags)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 755 |
for t in tags:
|
| 756 |
if t not in tips:
|
| 757 |
tips[t] = _tooltip_text_for_tag(t)
|
| 758 |
-
return json.dumps({"rows": rows, "tips": tips}, ensure_ascii=True)
|
| 759 |
|
| 760 |
|
| 761 |
def _build_row_component_updates(
|
|
@@ -1621,32 +1633,48 @@ client_js = """
|
|
| 1621 |
|
| 1622 |
const readTooltipMap = () => {
|
| 1623 |
const el = document.querySelector("#psq-tooltip-map textarea, #psq-tooltip-map input");
|
| 1624 |
-
if (!el) return { rows: [], tips: {} };
|
| 1625 |
const raw = (el.value || "").trim();
|
| 1626 |
-
if (!raw) return { rows: [], tips: {} };
|
| 1627 |
try {
|
| 1628 |
const obj = JSON.parse(raw);
|
| 1629 |
-
if (!obj || typeof obj !== "object") return { rows: [], tips: {} };
|
| 1630 |
const rows = Array.isArray(obj.rows) ? obj.rows : [];
|
|
|
|
| 1631 |
const tips = (obj.tips && typeof obj.tips === "object") ? obj.tips : {};
|
| 1632 |
-
return { rows, tips };
|
| 1633 |
} catch (_) {
|
| 1634 |
-
return { rows: [], tips: {} };
|
| 1635 |
}
|
| 1636 |
};
|
| 1637 |
|
| 1638 |
const applyTooltips = () => {
|
| 1639 |
const payload = readTooltipMap();
|
| 1640 |
const rowTags = Array.isArray(payload.rows) ? payload.rows : [];
|
|
|
|
| 1641 |
const tipMap = (payload.tips && typeof payload.tips === "object") ? payload.tips : {};
|
|
|
|
| 1642 |
const rowEls = document.querySelectorAll(".lego-tags");
|
| 1643 |
rowEls.forEach((rowEl, rowIdx) => {
|
| 1644 |
const tags = Array.isArray(rowTags[rowIdx]) ? rowTags[rowIdx] : [];
|
|
|
|
| 1645 |
const labels = rowEl.querySelectorAll("label");
|
| 1646 |
labels.forEach((label, tagIdx) => {
|
| 1647 |
const span = label.querySelector("span");
|
| 1648 |
const tag = (tagIdx < tags.length) ? tags[tagIdx] : "";
|
| 1649 |
const tip = tag && Object.prototype.hasOwnProperty.call(tipMap, tag) ? (tipMap[tag] || "") : "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1650 |
if (tip) {
|
| 1651 |
label.title = tip;
|
| 1652 |
if (span) span.title = tip;
|
|
|
|
| 749 |
row_defs_ui = (row_defs or [])[: max(0, int(max_rows))]
|
| 750 |
tips: Dict[str, str] = {}
|
| 751 |
rows: List[List[str]] = []
|
| 752 |
+
meta_rows: List[List[Dict[str, Any]]] = []
|
| 753 |
for row in row_defs_ui:
|
| 754 |
tags = _dedupe_norm_tags(row.get("tags", []) if isinstance(row, dict) else [])
|
| 755 |
rows.append(tags)
|
| 756 |
+
row_meta = row.get("tag_meta", {}) if isinstance(row, dict) and isinstance(row.get("tag_meta", {}), dict) else {}
|
| 757 |
+
meta_row: List[Dict[str, Any]] = []
|
| 758 |
+
for t in tags:
|
| 759 |
+
raw_meta = row_meta.get(t, {}) if isinstance(row_meta.get(t, {}), dict) else {}
|
| 760 |
+
meta_row.append(
|
| 761 |
+
{
|
| 762 |
+
"origin": _normalize_selection_origin(str(raw_meta.get("origin", "selection"))),
|
| 763 |
+
"preselected": bool(raw_meta.get("preselected", False)),
|
| 764 |
+
}
|
| 765 |
+
)
|
| 766 |
+
meta_rows.append(meta_row)
|
| 767 |
for t in tags:
|
| 768 |
if t not in tips:
|
| 769 |
tips[t] = _tooltip_text_for_tag(t)
|
| 770 |
+
return json.dumps({"rows": rows, "meta_rows": meta_rows, "tips": tips}, ensure_ascii=True)
|
| 771 |
|
| 772 |
|
| 773 |
def _build_row_component_updates(
|
|
|
|
| 1633 |
|
| 1634 |
const readTooltipMap = () => {
|
| 1635 |
const el = document.querySelector("#psq-tooltip-map textarea, #psq-tooltip-map input");
|
| 1636 |
+
if (!el) return { rows: [], meta_rows: [], tips: {} };
|
| 1637 |
const raw = (el.value || "").trim();
|
| 1638 |
+
if (!raw) return { rows: [], meta_rows: [], tips: {} };
|
| 1639 |
try {
|
| 1640 |
const obj = JSON.parse(raw);
|
| 1641 |
+
if (!obj || typeof obj !== "object") return { rows: [], meta_rows: [], tips: {} };
|
| 1642 |
const rows = Array.isArray(obj.rows) ? obj.rows : [];
|
| 1643 |
+
const meta_rows = Array.isArray(obj.meta_rows) ? obj.meta_rows : [];
|
| 1644 |
const tips = (obj.tips && typeof obj.tips === "object") ? obj.tips : {};
|
| 1645 |
+
return { rows, meta_rows, tips };
|
| 1646 |
} catch (_) {
|
| 1647 |
+
return { rows: [], meta_rows: [], tips: {} };
|
| 1648 |
}
|
| 1649 |
};
|
| 1650 |
|
| 1651 |
const applyTooltips = () => {
|
| 1652 |
const payload = readTooltipMap();
|
| 1653 |
const rowTags = Array.isArray(payload.rows) ? payload.rows : [];
|
| 1654 |
+
const rowMeta = Array.isArray(payload.meta_rows) ? payload.meta_rows : [];
|
| 1655 |
const tipMap = (payload.tips && typeof payload.tips === "object") ? payload.tips : {};
|
| 1656 |
+
const validOrigins = new Set(["rewrite", "selection", "probe", "structural", "implied", "user"]);
|
| 1657 |
const rowEls = document.querySelectorAll(".lego-tags");
|
| 1658 |
rowEls.forEach((rowEl, rowIdx) => {
|
| 1659 |
const tags = Array.isArray(rowTags[rowIdx]) ? rowTags[rowIdx] : [];
|
| 1660 |
+
const metas = Array.isArray(rowMeta[rowIdx]) ? rowMeta[rowIdx] : [];
|
| 1661 |
const labels = rowEl.querySelectorAll("label");
|
| 1662 |
labels.forEach((label, tagIdx) => {
|
| 1663 |
const span = label.querySelector("span");
|
| 1664 |
const tag = (tagIdx < tags.length) ? tags[tagIdx] : "";
|
| 1665 |
const tip = tag && Object.prototype.hasOwnProperty.call(tipMap, tag) ? (tipMap[tag] || "") : "";
|
| 1666 |
+
const meta = (tagIdx < metas.length && metas[tagIdx] && typeof metas[tagIdx] === "object")
|
| 1667 |
+
? metas[tagIdx]
|
| 1668 |
+
: {};
|
| 1669 |
+
const originRaw = String(meta.origin || "selection").trim().toLowerCase();
|
| 1670 |
+
const origin = validOrigins.has(originRaw) ? originRaw : "selection";
|
| 1671 |
+
const preselected = !!meta.preselected;
|
| 1672 |
+
label.setAttribute("data-psq-origin", origin);
|
| 1673 |
+
label.setAttribute("data-psq-preselected", preselected ? "1" : "0");
|
| 1674 |
+
if (span) {
|
| 1675 |
+
span.setAttribute("data-psq-origin", origin);
|
| 1676 |
+
span.setAttribute("data-psq-preselected", preselected ? "1" : "0");
|
| 1677 |
+
}
|
| 1678 |
if (tip) {
|
| 1679 |
label.title = tip;
|
| 1680 |
if (span) span.title = tip;
|
scripts/smoke_ui_state.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
| 1 |
import sys
|
|
|
|
|
|
|
| 2 |
from pathlib import Path
|
| 3 |
|
| 4 |
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
@@ -10,6 +12,82 @@ def _assert(cond: bool, msg: str) -> None:
|
|
| 10 |
raise AssertionError(msg)
|
| 11 |
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
def test_prompt_uses_visible_rows_only() -> None:
|
| 14 |
# If selected state contains stale hidden tags, prompt should still reflect visible-row selections only.
|
| 15 |
row_defs = [
|
|
@@ -183,6 +261,9 @@ def test_shared_tag_mirrors_without_unrelated_cross_toggle() -> None:
|
|
| 183 |
|
| 184 |
|
| 185 |
def main() -> None:
|
|
|
|
|
|
|
|
|
|
| 186 |
test_prompt_uses_visible_rows_only()
|
| 187 |
test_row_deduping()
|
| 188 |
test_rebuild_ignores_stale_selected_state()
|
|
|
|
| 1 |
import sys
|
| 2 |
+
import re
|
| 3 |
+
import json
|
| 4 |
from pathlib import Path
|
| 5 |
|
| 6 |
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
| 12 |
raise AssertionError(msg)
|
| 13 |
|
| 14 |
|
| 15 |
+
def _css_block(selector: str) -> str:
|
| 16 |
+
pat = re.compile(rf"{re.escape(selector)}\s*\{{([^}}]+)\}}", re.DOTALL)
|
| 17 |
+
m = pat.search(app.css)
|
| 18 |
+
_assert(m is not None, f"missing CSS selector block: {selector}")
|
| 19 |
+
return m.group(1)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _css_prop(block: str, prop: str) -> str:
|
| 23 |
+
pat = re.compile(rf"{re.escape(prop)}\s*:\s*([^;]+);", re.IGNORECASE)
|
| 24 |
+
m = pat.search(block)
|
| 25 |
+
_assert(m is not None, f"missing CSS property '{prop}' in block: {block}")
|
| 26 |
+
return m.group(1).strip().lower()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_toggle_color_contract_matches_legend() -> None:
|
| 30 |
+
legend_bg_by_origin = {
|
| 31 |
+
"rewrite": _css_prop(_css_block(".source-legend .chip.rewrite"), "background"),
|
| 32 |
+
"selection": _css_prop(_css_block(".source-legend .chip.selection"), "background"),
|
| 33 |
+
"probe": _css_prop(_css_block(".source-legend .chip.probe"), "background"),
|
| 34 |
+
"structural": _css_prop(_css_block(".source-legend .chip.structural"), "background"),
|
| 35 |
+
"implied": _css_prop(_css_block(".source-legend .chip.implied"), "background"),
|
| 36 |
+
"user": _css_prop(_css_block(".source-legend .chip.user"), "background"),
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
on_bg2_by_origin = {
|
| 40 |
+
"rewrite": _css_prop(_css_block('.lego-tags label[data-psq-preselected="1"][data-psq-origin="rewrite"] span'), "--on-bg2"),
|
| 41 |
+
"selection": _css_prop(_css_block('.lego-tags label[data-psq-preselected="1"][data-psq-origin="selection"] span'), "--on-bg2"),
|
| 42 |
+
"probe": _css_prop(_css_block('.lego-tags label[data-psq-preselected="1"][data-psq-origin="probe"] span'), "--on-bg2"),
|
| 43 |
+
"structural": _css_prop(_css_block('.lego-tags label[data-psq-preselected="1"][data-psq-origin="structural"] span'), "--on-bg2"),
|
| 44 |
+
"implied": _css_prop(_css_block('.lego-tags label[data-psq-preselected="1"][data-psq-origin="implied"] span'), "--on-bg2"),
|
| 45 |
+
"user": _css_prop(_css_block('.lego-tags label[data-psq-preselected="0"] span'), "--on-bg2"),
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
for origin, legend_bg in legend_bg_by_origin.items():
|
| 49 |
+
_assert(
|
| 50 |
+
on_bg2_by_origin[origin] == legend_bg,
|
| 51 |
+
f"legend/toggle color mismatch for '{origin}': toggle={on_bg2_by_origin[origin]} legend={legend_bg}",
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_client_js_stamps_source_color_attributes() -> None:
|
| 56 |
+
js = app.client_js
|
| 57 |
+
has_origin_attr = ("data-psq-origin" in js) or ("dataset.psqOrigin" in js)
|
| 58 |
+
has_preselected_attr = ("data-psq-preselected" in js) or ("dataset.psqPreselected" in js)
|
| 59 |
+
_assert(has_origin_attr, "client_js does not stamp per-tag origin attributes on checkbox labels")
|
| 60 |
+
_assert(has_preselected_attr, "client_js does not stamp per-tag preselected attributes on checkbox labels")
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def test_tooltip_payload_includes_row_meta_for_color_mapping() -> None:
|
| 64 |
+
row_defs = [
|
| 65 |
+
{
|
| 66 |
+
"name": "r1",
|
| 67 |
+
"label": "R1",
|
| 68 |
+
"tags": ["solo", "female"],
|
| 69 |
+
"tag_meta": {
|
| 70 |
+
"solo": {"origin": "rewrite", "preselected": True},
|
| 71 |
+
"female": {"origin": "selection", "preselected": False},
|
| 72 |
+
},
|
| 73 |
+
}
|
| 74 |
+
]
|
| 75 |
+
raw = app._build_tooltip_payload(row_defs, max_rows=app.display_max_rows_default)
|
| 76 |
+
payload = json.loads(raw)
|
| 77 |
+
_assert("meta_rows" in payload, "tooltip payload missing meta_rows")
|
| 78 |
+
_assert(isinstance(payload["meta_rows"], list), "tooltip payload meta_rows must be a list")
|
| 79 |
+
_assert(len(payload["meta_rows"]) == 1, "meta_rows should align with visible row count")
|
| 80 |
+
_assert(isinstance(payload["meta_rows"][0], list), "meta_rows row must be a list")
|
| 81 |
+
_assert(len(payload["meta_rows"][0]) == 2, "meta_rows tag metadata count must align with tags")
|
| 82 |
+
|
| 83 |
+
first = payload["meta_rows"][0][0]
|
| 84 |
+
second = payload["meta_rows"][0][1]
|
| 85 |
+
_assert(first.get("origin") == "rewrite", "first tag origin metadata mismatch")
|
| 86 |
+
_assert(first.get("preselected") is True, "first tag preselected metadata mismatch")
|
| 87 |
+
_assert(second.get("origin") == "selection", "second tag origin metadata mismatch")
|
| 88 |
+
_assert(second.get("preselected") is False, "second tag preselected metadata mismatch")
|
| 89 |
+
|
| 90 |
+
|
| 91 |
def test_prompt_uses_visible_rows_only() -> None:
|
| 92 |
# If selected state contains stale hidden tags, prompt should still reflect visible-row selections only.
|
| 93 |
row_defs = [
|
|
|
|
| 261 |
|
| 262 |
|
| 263 |
def main() -> None:
|
| 264 |
+
test_toggle_color_contract_matches_legend()
|
| 265 |
+
test_client_js_stamps_source_color_attributes()
|
| 266 |
+
test_tooltip_payload_includes_row_meta_for_color_mapping()
|
| 267 |
test_prompt_uses_visible_rows_only()
|
| 268 |
test_row_deduping()
|
| 269 |
test_rebuild_ignores_stale_selected_state()
|