"""Web UI section registry. Every known section/block is registered here. To disable something, set ``enabled=False`` (sections) or ``skip=True`` (narrative blocks) — do not delete entries. Removed ids are re-added automatically as disabled so they can be turned back on later via ``with_section_enabled`` / ``with_narrative_block_skip``. """ from __future__ import annotations from dataclasses import dataclass, replace from enum import StrEnum from typing import Any class WebSectionId(StrEnum): """Top-level blocks in the results panel.""" PLAYER_BANNER = "player_banner" STYLE_SCALE = "style_scale" STYLE_TRAITS = "style_traits" TSNE_PLOT = "tsne_plot" ELO_TSNE_PLOT = "elo_tsne_plot" NARRATIVE = "narrative" OPENINGS = "openings" PUZZLES = "puzzles" class NarrativeBlockId(StrEnum): """Narrative subsections filtered out of ``narrative_sections``.""" COACHING_REPORT = "coaching_report" COHORT_TENDENCY = "cohort_tendency" OPENING_DUPLICATE = "opening_duplicate" PUZZLE_DUPLICATE = "puzzle_duplicate" @dataclass(frozen=True) class WebSectionSpec: id: WebSectionId enabled: bool order: int label: str = "" @dataclass(frozen=True) class NarrativeBlockSpec: id: NarrativeBlockId """When ``True``, matching narrative blocks are omitted from the web payload.""" skip: bool # Canonical registry — keep every section here; toggle ``enabled`` instead of deleting. _SECTION_REGISTRY: dict[WebSectionId, WebSectionSpec] = { WebSectionId.PLAYER_BANNER: WebSectionSpec( WebSectionId.PLAYER_BANNER, True, 10, "Player summary" ), WebSectionId.STYLE_SCALE: WebSectionSpec( WebSectionId.STYLE_SCALE, True, 20, "Playing style" ), WebSectionId.TSNE_PLOT: WebSectionSpec( WebSectionId.TSNE_PLOT, True, 30, "Playstyle map (t-SNE)" ), WebSectionId.ELO_TSNE_PLOT: WebSectionSpec( WebSectionId.ELO_TSNE_PLOT, True, 40, "Rating vs t-SNE" ), WebSectionId.STYLE_TRAITS: WebSectionSpec( WebSectionId.STYLE_TRAITS, True, 50, "Playstyle changes" ), WebSectionId.NARRATIVE: WebSectionSpec( WebSectionId.NARRATIVE, True, 60, "Coaching narrative" ), WebSectionId.OPENINGS: WebSectionSpec( WebSectionId.OPENINGS, True, 70, "Top opening positions" ), WebSectionId.PUZZLES: WebSectionSpec( WebSectionId.PUZZLES, True, 80, "Puzzle practice" ), } _NARRATIVE_BLOCK_REGISTRY: dict[NarrativeBlockId, NarrativeBlockSpec] = { NarrativeBlockId.COACHING_REPORT: NarrativeBlockSpec( NarrativeBlockId.COACHING_REPORT, skip=True ), NarrativeBlockId.COHORT_TENDENCY: NarrativeBlockSpec( NarrativeBlockId.COHORT_TENDENCY, skip=True ), NarrativeBlockId.OPENING_DUPLICATE: NarrativeBlockSpec( NarrativeBlockId.OPENING_DUPLICATE, skip=True ), NarrativeBlockId.PUZZLE_DUPLICATE: NarrativeBlockSpec( NarrativeBlockId.PUZZLE_DUPLICATE, skip=True ), } # Active layout — edit ``enabled`` / ``order`` on registry entries, or pass overrides # to ``resolve_web_layout``. Entries must not be removed from the registries above. DEFAULT_WEB_UI_LAYOUT: tuple[WebSectionSpec, ...] = tuple( sorted(_SECTION_REGISTRY.values(), key=lambda s: s.order) ) DEFAULT_NARRATIVE_BLOCK_FILTERS: tuple[NarrativeBlockSpec, ...] = tuple( _NARRATIVE_BLOCK_REGISTRY.values() ) def resolve_web_layout( overrides: tuple[WebSectionSpec, ...] | None = None, ) -> tuple[WebSectionSpec, ...]: """Merge optional overrides onto the canonical section registry.""" merged = dict(_SECTION_REGISTRY) if overrides is not None: for spec in overrides: merged[spec.id] = spec return tuple(sorted(merged.values(), key=lambda s: s.order)) def resolve_narrative_block_filters( overrides: tuple[NarrativeBlockSpec, ...] | None = None, ) -> tuple[NarrativeBlockSpec, ...]: """Merge optional overrides onto the canonical narrative block registry.""" merged = dict(_NARRATIVE_BLOCK_REGISTRY) if overrides is not None: for spec in overrides: merged[spec.id] = spec return tuple(merged.values()) def ensure_complete_layout( layout: tuple[WebSectionSpec, ...] | None = None, ) -> tuple[WebSectionSpec, ...]: """Return a full layout with every ``WebSectionId`` (disabled if omitted).""" partial = {spec.id: spec for spec in (layout or ())} complete: list[WebSectionSpec] = [] for sid, default in _SECTION_REGISTRY.items(): if sid in partial: complete.append(partial[sid]) else: complete.append(replace(default, enabled=False)) return tuple(sorted(complete, key=lambda s: s.order)) def ensure_complete_narrative_filters( filters: tuple[NarrativeBlockSpec, ...] | None = None, ) -> tuple[NarrativeBlockSpec, ...]: """Return all narrative block filters (not skipped if omitted).""" partial = {spec.id: spec for spec in (filters or ())} complete: list[NarrativeBlockSpec] = [] for bid, default in _NARRATIVE_BLOCK_REGISTRY.items(): if bid in partial: complete.append(partial[bid]) else: complete.append(replace(default, skip=False)) return tuple(complete) def with_section_enabled( layout: tuple[WebSectionSpec, ...], section_id: WebSectionId | str, *, enabled: bool, ) -> tuple[WebSectionSpec, ...]: """Return a copy of ``layout`` with one section enabled or disabled.""" sid = WebSectionId(section_id) base = ensure_complete_layout(layout) return tuple( replace(spec, enabled=enabled) if spec.id == sid else spec for spec in base ) def with_narrative_block_skip( filters: tuple[NarrativeBlockSpec, ...], block_id: NarrativeBlockId | str, *, skip: bool, ) -> tuple[NarrativeBlockSpec, ...]: """Return a copy of ``filters`` with one narrative block shown or hidden.""" bid = NarrativeBlockId(block_id) base = ensure_complete_narrative_filters(filters) return tuple( replace(spec, skip=skip) if spec.id == bid else spec for spec in base ) def ui_layout_config( *, sections: tuple[WebSectionSpec, ...] | None = None, narrative_blocks: tuple[NarrativeBlockSpec, ...] | None = None, ) -> dict[str, Any]: """Serializable layout for the frontend and API consumers.""" resolved_sections = ensure_complete_layout( resolve_web_layout(sections) if sections is not None else DEFAULT_WEB_UI_LAYOUT ) resolved_blocks = ensure_complete_narrative_filters( resolve_narrative_block_filters(narrative_blocks) if narrative_blocks is not None else DEFAULT_NARRATIVE_BLOCK_FILTERS ) return { "sections": [ { "id": spec.id.value, "enabled": spec.enabled, "order": spec.order, "label": spec.label, } for spec in resolved_sections ], "narrative_blocks": [ {"id": block.id.value, "skip": block.skip} for block in resolved_blocks ], } def section_enabled( section_id: WebSectionId | str, *, layout: tuple[WebSectionSpec, ...] = DEFAULT_WEB_UI_LAYOUT, ) -> bool: sid = WebSectionId(section_id) for spec in ensure_complete_layout(layout): if spec.id == sid: return spec.enabled return False def narrative_block_skipped( block_id: NarrativeBlockId | str, *, filters: tuple[NarrativeBlockSpec, ...] | None = None, ) -> bool: bid = NarrativeBlockId(block_id) resolved = ensure_complete_narrative_filters( filters if filters is not None else DEFAULT_NARRATIVE_BLOCK_FILTERS ) for spec in resolved: if spec.id == bid: return spec.skip return False def enabled_section_ids_ordered( layout: tuple[WebSectionSpec, ...] = DEFAULT_WEB_UI_LAYOUT, ) -> list[str]: return [ spec.id.value for spec in ensure_complete_layout(layout) if spec.enabled ]