"""Split and filter coaching narrative text for the web UI.""" from __future__ import annotations from chess_tutor.cohort.opening_families import bucket_display_name from chess_tutor.web.sections import ( NarrativeBlockId, narrative_block_skipped, section_enabled, WebSectionId, ) _OPENING_BUCKET_TITLES = frozenset( bucket_display_name(bucket) for bucket in ("white", "black_vs_e4", "black_vs_d4", "black_vs_c4", "other") ) | {"Other"} _WEB_NARRATIVE_SKIP_TITLES = frozenset( {"Opening patterns", "Top opening positions", "Puzzle practice"} ) _TOP_LEVEL_NARRATIVE_HEADERS = frozenset( { "Story for this player", "Your focus this week", "This week's checklist", "Examples from your games", "Opening patterns", "Top opening positions", "Practice plan", "Puzzle practice", } ) def _is_top_level_narrative_header(line: str, stripped: str) -> bool: if not stripped or line.startswith((" ", "\t")) or stripped.startswith("-"): return False return stripped.rstrip(":") in _TOP_LEVEL_NARRATIVE_HEADERS def _strip_web_duplicate_sections(narrative: str) -> str: """Remove opening/puzzle blocks when dedicated UI cards show them.""" skip_titles: set[str] = set() if section_enabled(WebSectionId.OPENINGS) and narrative_block_skipped( NarrativeBlockId.OPENING_DUPLICATE ): skip_titles.add("Opening patterns") skip_titles.add("Top opening positions") if section_enabled(WebSectionId.PUZZLES) and narrative_block_skipped( NarrativeBlockId.PUZZLE_DUPLICATE ): skip_titles.add("Puzzle practice") if not skip_titles: return narrative.strip() lines = narrative.splitlines() result: list[str] = [] skipping = False i = 0 while i < len(lines): line = lines[i] stripped = line.strip() if stripped in skip_titles or stripped.rstrip(":") in skip_titles: skipping = True i += 1 continue if skipping: if _is_top_level_narrative_header(line, stripped): skipping = False else: i += 1 continue result.append(line) i += 1 return "\n".join(result).strip() def _looks_like_opening_section(section: dict[str, str]) -> bool: title = section["title"] body = section["body"] if title in _WEB_NARRATIVE_SKIP_TITLES or title in _OPENING_BUCKET_TITLES: return True if title.startswith("#") and " — " in title: return True return "You play:" in body and "Recommended:" in body def _looks_like_puzzle_section(section: dict[str, str]) -> bool: title = section["title"] body = section["body"] if title in _WEB_NARRATIVE_SKIP_TITLES: return True if "Rated ladder" in title or "Rated ladder" in body: return True if "lichess.org/training/" in body: return True if "Themes:" in body and " Elo (" in body: return True return False def _looks_like_coaching_report_section(section: dict[str, str]) -> bool: title = section["title"].strip() if title in {"Coaching report", "Story for this player"}: return True return section["body"].strip().startswith("Story for this player") def _looks_like_cohort_tendency_section(section: dict[str, str]) -> bool: title = section["title"].strip().rstrip(":") return title.startswith("Compared to others at your rating") _NARRATIVE_BLOCK_MATCHERS: dict[NarrativeBlockId, object] = { NarrativeBlockId.COACHING_REPORT: _looks_like_coaching_report_section, NarrativeBlockId.COHORT_TENDENCY: _looks_like_cohort_tendency_section, NarrativeBlockId.OPENING_DUPLICATE: _looks_like_opening_section, NarrativeBlockId.PUZZLE_DUPLICATE: _looks_like_puzzle_section, } _CLIMBER_SECTION_PREFIX = "Players who climbed from your bracket toward" def _web_friendly_narrative_title(title: str) -> str: stripped = title.strip().rstrip(":") if stripped.startswith(_CLIMBER_SECTION_PREFIX): return "Playstyle changes (from similar climbers)" if stripped == "Practical emphasis (by priority)": return "Recommended playstyle changes" return title def _should_skip_narrative_section( section: dict[str, str], *, filters: tuple | None = None, ) -> bool: for block_id, matcher in _NARRATIVE_BLOCK_MATCHERS.items(): if narrative_block_skipped(block_id, filters=filters) and matcher(section): return True return False def _split_narrative_sections(narrative: str) -> list[dict[str, str]]: """Split coaching narrative into titled sections.""" text = narrative.strip() if not text: return [] sections: list[dict[str, str]] = [] current_title = "Coaching report" current_lines: list[str] = [] for line in text.splitlines(): stripped = line.strip() if not stripped and not current_lines: continue if ( stripped and not stripped.startswith("-") and not line.startswith(" ") and not line.startswith("\t") and stripped.endswith(":") and len(stripped) < 80 ): if current_lines: sections.append( { "title": current_title, "body": "\n".join(current_lines).strip(), } ) current_title = stripped.rstrip(":") current_lines = [] continue current_lines.append(line.rstrip()) if current_lines: sections.append( { "title": current_title, "body": "\n".join(current_lines).strip(), } ) return sections def build_web_narrative_sections( narrative: str, *, narrative_filters: tuple | None = None, ) -> list[dict[str, str]]: """Narrative blocks for the web UI, respecting section registry filters.""" if not section_enabled(WebSectionId.NARRATIVE): return [] cleaned = _strip_web_duplicate_sections(narrative) sections: list[dict[str, str]] = [] for section in _split_narrative_sections(cleaned): if _should_skip_narrative_section(section, filters=narrative_filters): continue sections.append( { "title": _web_friendly_narrative_title(section["title"]), "body": section["body"], } ) return sections