"""Structured coaching payload for the web UI.""" from __future__ import annotations from typing import Any from chess_tutor.cohort.opening_families import bucket_display_name from chess_tutor.playstyle_coaching.opening_climbers import position_heading from chess_tutor.inference.output import format_elo_outlook_lines from chess_tutor.inference.pipeline import InferenceResult from chess_tutor.web.embedding import build_tsne_web_plots from chess_tutor.web.narrative_sections import build_web_narrative_sections from chess_tutor.web.puzzle_series import build_puzzle_series from chess_tutor.web.sections import ( DEFAULT_WEB_UI_LAYOUT, WebSectionId, WebSectionSpec, ensure_complete_layout, section_enabled, ui_layout_config, ) from chess_tutor.web.style_elo_plot import build_style_scale from chess_tutor.web.style_traits import build_style_traits from chess_tutor.web.version import version_info def _opening_cards(result: InferenceResult) -> list[dict[str, Any]]: plan = result.actionable_plan if plan is None or not plan.opening_insights: return [] cards: list[dict[str, Any]] = [] for rank, ins in enumerate(plan.opening_insights, start=1): title = position_heading( rank, position_label=ins.position_label, bucket=ins.bucket, ) cards.append( { "rank": rank, "bucket": title, "context": bucket_display_name(ins.bucket) if ins.bucket else None, "position_label": ins.position_label, "you_play": ins.user_move_san, "you_play_moves": ins.user_move_uci, "you_play_share": ins.user_move_at_position_share, "sample_share": ins.user_current_share, "bucket_n_games": ins.position_visit_count or ins.bucket_n_games, "recommended": ins.recommended_move_san, "recommended_moves": ins.recommended_move_uci, "recommended_label": ins.recommended_opening or ins.family_label, "climber_switch_rate": ins.climber_switch_rate, } ) return cards def build_web_payload( result: InferenceResult, *, corpus_id: str = "standard_600", layout: tuple[WebSectionSpec, ...] | None = None, ) -> dict[str, Any]: """JSON-serializable report for the local web UI.""" resolved_layout = ensure_complete_layout( layout if layout is not None else DEFAULT_WEB_UI_LAYOUT ) ctx = result.coaching_report.context outlook_lines = format_elo_outlook_lines(result, ctx) narrative = result.coaching_narrative.strip() meta = ( dict(result.actionable_plan.metadata) if result.actionable_plan is not None else {} ) payload: dict[str, Any] = { "ui_layout": ui_layout_config(sections=resolved_layout), "header": { "username": result.username, "site": result.site, "player_elo": result.player_elo, "native_player_elo": result.native_player_elo, "elo_bracket": ctx.elo_bracket, "n_games": result.n_games_downloaded, "policy": result.coaching_policy, }, "rating_outlook": outlook_lines, "narrative_markdown": narrative, "lichess_study_url": meta.get("lichess_study_url"), "lichess_study_error": meta.get("lichess_study_error"), "version": version_info(), } if section_enabled(WebSectionId.NARRATIVE, layout=resolved_layout): payload["narrative_sections"] = build_web_narrative_sections(narrative) else: payload["narrative_sections"] = [] if section_enabled(WebSectionId.OPENINGS, layout=resolved_layout): payload["opening_cards"] = _opening_cards(result) else: payload["opening_cards"] = [] if section_enabled(WebSectionId.PUZZLES, layout=resolved_layout): payload["puzzle_series"] = build_puzzle_series(result.actionable_plan) else: payload["puzzle_series"] = [] if section_enabled(WebSectionId.TSNE_PLOT, layout=resolved_layout) or section_enabled( WebSectionId.ELO_TSNE_PLOT, layout=resolved_layout ): embedding_plots = build_tsne_web_plots(result, corpus_id=corpus_id) payload["tsne_plot"] = ( embedding_plots["tsne_plot"] if section_enabled(WebSectionId.TSNE_PLOT, layout=resolved_layout) else None ) payload["elo_tsne_plot"] = ( embedding_plots["elo_tsne_plot"] if section_enabled(WebSectionId.ELO_TSNE_PLOT, layout=resolved_layout) else None ) else: payload["tsne_plot"] = None payload["elo_tsne_plot"] = None if section_enabled(WebSectionId.STYLE_SCALE, layout=resolved_layout): payload["style_scale"] = build_style_scale(result, corpus_id=corpus_id) else: payload["style_scale"] = None if section_enabled(WebSectionId.STYLE_TRAITS, layout=resolved_layout): payload["style_traits"] = build_style_traits(result) else: payload["style_traits"] = [] return payload