Spaces:
Running
Running
| import math | |
| from typing import Dict, Optional, Any | |
| from .runner_intelligence_snapshot import RunnerIntelligenceSnapshot | |
| from _app.presentation.ui_text import UI_TEXT | |
| from tools.helpers import decode_chart | |
| def _format_pace(seconds_per_km: float) -> str: | |
| if not seconds_per_km or seconds_per_km <= 0: | |
| return "N/A" | |
| minutes = int(seconds_per_km // 60) | |
| seconds = int(seconds_per_km % 60) | |
| return f"{minutes}:{seconds:02d} /km" | |
| def _build_performance_story(snapshot: RunnerIntelligenceSnapshot, trend_dict: Dict, language: str) -> str: | |
| t = UI_TEXT.get(language, UI_TEXT["en"]) | |
| runs_word = t.get("unit_runs") | |
| dist_word = t.get("lbl_total_of") | |
| story_tpl = t.get("home_story_template", "") | |
| story = story_tpl.format(count=snapshot.run_count, unit=runs_word, of=dist_word, dist=snapshot.weekly_distance_km) | |
| comparison = trend_dict.get("comparison_available", False) if trend_dict else False | |
| if comparison: | |
| dist_delta = trend_dict.get("distance_delta_pct", 0) | |
| pace_delta = trend_dict.get("pace_delta_s_per_km", 0) | |
| dist_trend = t.get("lbl_more" if dist_delta > 0 else "lbl_less", "more" if dist_delta > 0 else "less") | |
| pace_trend = t.get("lbl_faster" if pace_delta < 0 else "lbl_slower", "faster" if pace_delta < 0 else "slower") | |
| that_is = t.get("lbl_that_is") | |
| than_avg = t.get("lbl_than_avg") | |
| pace_was = t.get("lbl_pace_was") | |
| if abs(dist_delta) > 5: | |
| story += f" {that_is} **{abs(dist_delta):.1f}% {dist_trend}** {than_avg}" | |
| if abs(pace_delta) > 2: | |
| formatted_delta = _format_pace(abs(pace_delta)).replace(" /km", "") | |
| story += f" {pace_was} **{formatted_delta} {pace_trend}**." | |
| return story | |
| def _build_delta_summary(trend_dict: Dict, language: str) -> Dict[str, str]: | |
| if not trend_dict or not trend_dict.get("comparison_available"): | |
| return {} | |
| t = UI_TEXT.get(language, UI_TEXT["en"]) | |
| def format_val(metric_name, delta_val): | |
| if delta_val is None: | |
| return t.get("na", "N/A") | |
| is_positive_good = metric_name in ["distance", "frequency", "consistency"] | |
| icon = "⚪" | |
| if delta_val > 0: | |
| icon = "🟢" if is_positive_good else "🔴" | |
| elif delta_val < 0: | |
| icon = "🔴" if is_positive_good else "🟢" | |
| formatted = f"{delta_val:+.1f}" | |
| if metric_name == "distance": | |
| formatted = f"{delta_val:+.1f}%" | |
| elif metric_name == "pace": | |
| unit = t.get("unit_spkm", "s/km") | |
| formatted = f"{delta_val:+.1f} {unit}" | |
| elif metric_name == "frequency": | |
| unit = t.get("unit_runs", "runs") | |
| formatted = f"{int(delta_val):+d} {unit}" | |
| elif metric_name == "hr": | |
| unit = t.get("unit_bpm", "bpm") | |
| formatted = f"{delta_val:+.1f} {unit}" | |
| elif metric_name == "consistency": | |
| unit = t.get("unit_pts", "pts") | |
| formatted = f"{int(delta_val):+d} {unit}" | |
| return f"{icon} {formatted}" | |
| return { | |
| "distance": format_val("distance", trend_dict.get("distance_delta_pct")), | |
| "pace": format_val("pace", trend_dict.get("pace_delta_s_per_km")), | |
| "frequency": format_val("frequency", trend_dict.get("frequency_delta")), | |
| "hr": format_val("hr", trend_dict.get("hr_delta")), | |
| "consistency": format_val("consistency", trend_dict.get("consistency_delta")) | |
| } | |
| def _build_evidence_view(positioning_view: Dict, trend: Dict, language: str) -> str: | |
| t = UI_TEXT.get(language, UI_TEXT["en"]) | |
| evidence = positioning_view.get("evidence", "") | |
| if evidence and isinstance(evidence, dict): | |
| pace_delta = trend.get('pace_trend_s_per_km') if trend else 0 | |
| if pace_delta is None: pace_delta = 0 | |
| pace_word = t.get('positioning_evidence_pace_improved', 'Improved') if pace_delta <= 0 else t.get('positioning_evidence_pace_worsened', 'Worsened') | |
| return f""" | |
| 📈 {t.get('positioning_evidence_distance', 'Dist')}: {evidence.get('distance', 0)} <br> | |
| ⚡ {pace_word}: {evidence.get("pace")} <br> | |
| 🫀 {t.get('positioning_evidence_hr', 'HR')}: {evidence.get('hr', 0)} <br> | |
| 🏃 {t.get('lbl_runs_count', 'Runs')}: {evidence.get("frequency")} <br> | |
| 🎯 {t.get('positioning_evidence_consistency', 'Consistency')}: {evidence.get('consistency', 0)} | |
| """.strip() | |
| else: | |
| return f"{positioning_view.get('trajectory', '')}\n".strip() | |
| def _build_structure_view(structure_status: Dict, recommendation: Dict, language: str) -> str: | |
| if not structure_status: | |
| return "" | |
| t = UI_TEXT.get(language, UI_TEXT["en"]) | |
| wd_comp = structure_status.get("weekday_completed", 0) | |
| wd_total = structure_status.get("weekday_total", 0) | |
| lr_comp = "✅" if structure_status.get("long_run_completed") else "⏳" | |
| classif = structure_status.get("classification", "reset_week") | |
| classif_lbl = t.get(classif, classif) | |
| km_rem = structure_status.get("km_remaining", 0.0) | |
| km_rem_subtext = "" | |
| if km_rem > 0: | |
| subtext_tpl = t.get("lbl_km_remaining_subtext", "{val} km") | |
| km_rem_subtext = f'<span class="subtext">{subtext_tpl.format(val=f"{km_rem:.1f}")}</span>' | |
| advice = recommendation.get('description', "") if recommendation else t.get("coaching_advice", "") | |
| advice_html = f'<div class="coaching-tip">{advice}</div>' if advice else "" | |
| return f""" | |
| <div class="metric-row"><span class="metric-label">{t.get('lbl_weekday_runs', 'Weekday')}:</span> <span class="metric-value">{wd_comp} / {wd_total}</span></div> | |
| <div class="metric-row"><span class="metric-label">{t.get('lbl_long_run', 'Long Run')}:</span> <span class="metric-value">{lr_comp}</span></div> | |
| <div class="metric-row"><span class="metric-label">{t.get('lbl_structure_status', 'Status')}:</span> <span class="metric-value">{classif_lbl}</span></div> | |
| <div class="metric-row"><span class="metric-label">{t.get('lbl_km_remaining', 'Remaining') + ': ' if km_rem > 0 else ''}</span> {km_rem_subtext}</div> | |
| {advice_html} | |
| """.strip() | |
| def _build_goal_status_text(active_goal: Dict, language: str) -> str: | |
| if not active_goal: | |
| return "" | |
| t = UI_TEXT.get(language, UI_TEXT["en"]) | |
| status_key = active_goal.get("status", "unknown") | |
| status_lbl = t.get(f"goal_status_{status_key}", status_key) | |
| tpl = t.get("goal_status_template", "Goal status: {val}") | |
| return tpl.format(val=status_lbl) | |
| def build_intelligence_snapshot(context) -> RunnerIntelligenceSnapshot: | |
| """ | |
| Builds a RunnerIntelligenceSnapshot from a PipelineContext. | |
| This is an aggregation layer only. It uses safe accessors (`getattr`) | |
| to extract already-computed values without introducing new business logic. | |
| """ | |
| summary = context.summary | |
| # Helper to safely extract depending on whether summary is a dict, WeeklySnapshot, or WeeklySummary | |
| def _extract_summary_val(dict_key, attr_names, default, transform=None): | |
| if not summary: | |
| return default | |
| val = default | |
| if isinstance(summary, dict): | |
| val = summary.get(dict_key, default) | |
| else: | |
| for attr in attr_names: | |
| if hasattr(summary, attr): | |
| val = getattr(summary, attr, default) | |
| break | |
| return transform(val) if transform and val != default else val | |
| # --- Extract signals from domain objects (projection layer) --- | |
| recommendation_obj = context.recommendation | |
| insights_obj = context.insights or {} | |
| training_state = None | |
| health_signal = None | |
| positioning_status = None | |
| positioning_change = None | |
| next_run = None | |
| training_focus = None | |
| training_type = None | |
| training_why = None | |
| performance_brief = None | |
| performance_focus = None | |
| if recommendation_obj: | |
| training_focus = getattr(recommendation_obj, "focus", None) | |
| training_type = getattr(recommendation_obj, "session_type", None) | |
| training_why = getattr(recommendation_obj, "description", None) | |
| if not next_run: next_run = getattr(context, "next_run", None) | |
| if not training_focus: training_focus = getattr(context, "training_focus", None) | |
| key_insight = None | |
| forward_focus = None | |
| goal_trajectory = None | |
| goal_progress_pct = None | |
| positioning_view = None | |
| active_goal = None | |
| goal_view = None | |
| if getattr(context, "positioning_view", None): | |
| positioning_view = context.positioning_view | |
| training_state = positioning_view.get("training_phase", None) | |
| health_signal = positioning_view.get("health_signal", None) | |
| positioning_status = positioning_view.get("state", None) | |
| positioning_change = positioning_view.get("change", None) | |
| forward_focus = positioning_view.get("forward_focus", None) | |
| key_insight = positioning_view.get("insight", None) | |
| goal_trajectory = positioning_view.get("goal_trajectory", None) | |
| goal_progress_pct = positioning_view.get("goal_progress_pct", None) | |
| if hasattr(context, "weekly_snapshot"): | |
| weekly_snapshot = context.weekly_snapshot | |
| performance_brief = getattr(weekly_snapshot, "performance_brief", None) | |
| performance_focus = getattr(weekly_snapshot, "performance_focus", None) | |
| else: | |
| performance_brief = getattr(context, "weekly_brief", None) | |
| performance_focus = getattr(context, "weekly_focus", None) | |
| # Fallbacks for scalar signals directly on context (useful for tests/minimal contexts) | |
| # This aligns with the "aggregation layer" philosophy of the builder. | |
| if not key_insight: key_insight = getattr(context, "key_insight", None) | |
| if not forward_focus: forward_focus = getattr(context, "forward_focus", None) | |
| if not training_state: training_state = getattr(context, "training_state", None) | |
| if not health_signal: health_signal = getattr(context, "health_signal", None) | |
| if not positioning_status: positioning_status = getattr(context, "positioning_status", None) | |
| if not positioning_change: positioning_change = getattr(context, "positioning_change", None) | |
| pos_view_safe = getattr(context, "positioning_view", None) or {} | |
| if not goal_trajectory: | |
| goal_trajectory = pos_view_safe.get("goal_trajectory") if isinstance(pos_view_safe, dict) else getattr(pos_view_safe, "goal_trajectory", None) | |
| if not goal_trajectory: | |
| goal_trajectory = getattr(context, "goal_trajectory", "NO_GOAL") | |
| goal_prog_safe = getattr(context, "goal_progress", None) or {} | |
| if goal_progress_pct is None: | |
| goal_progress_pct = getattr(context, "goal_progress_pct", None) | |
| if goal_progress_pct is None: | |
| goal_progress_pct = goal_prog_safe.get("progress_percentage", 0) if isinstance(goal_prog_safe, dict) else getattr(goal_prog_safe, "progress_percentage", 0) | |
| week_charts = getattr(context, "charts", {}) or getattr(context.weekly_snapshot, "charts", {}) | |
| snapshot = RunnerIntelligenceSnapshot( | |
| id=getattr(context.weekly_snapshot, "id", None), | |
| week_start=_extract_summary_val("week_start", ["week_start_date", "week_start"], None), | |
| training_state=training_state, | |
| health_signal=health_signal, | |
| positioning_status=positioning_status, | |
| positioning_change=positioning_change, | |
| goal_trajectory=goal_trajectory, | |
| goal_progress_pct=goal_progress_pct, | |
| next_run=next_run, | |
| training_focus=training_focus, | |
| training_type=training_type, | |
| training_why=training_why, | |
| key_insight=key_insight, | |
| forward_focus=forward_focus, | |
| performance_brief=performance_brief, | |
| performance_focus=performance_focus, | |
| weekly_distance_km=_extract_summary_val( | |
| "total_distance_m", | |
| ["total_distance_km", "total_distance_m"], | |
| 0.0, | |
| transform=lambda x: x / 1000.0 if not hasattr(summary, "total_distance_km") else x | |
| ), | |
| num_runs=_extract_summary_val("num_runs", ["run_count", "num_runs"], 0), | |
| run_count=_extract_summary_val("num_runs", ["run_count", "num_runs"], 0), | |
| consistency_score=_extract_summary_val("consistency_score", ["consistency_score"], 0), | |
| avg_pace=_extract_summary_val("avg_pace_s_per_km", ["avg_pace_sec_per_km", "avg_pace_s_per_km"], 0.0), | |
| avg_hr=_extract_summary_val("avg_hr_bpm", ["avg_hr", "avg_hr_bpm"], 0.0), | |
| structure_status=getattr(context.weekly_snapshot, "structure_status", {}) if context.weekly_snapshot else {}, | |
| # Detailed DTO components for UI transparency | |
| # Week specific trend | |
| trend=context.trends.model_dump() if getattr(context, "trends", None) and hasattr(context.trends, "model_dump") else (context.trends if getattr(context, "trends", None) else {}), | |
| # Week over Weeks trend | |
| weekly_trend=context.weekly_trend.model_dump() if getattr(context, "weekly_trend", None) and hasattr(context.weekly_trend, "model_dump") else (context.weekly_trend if getattr(context, "weekly_trend", None) else {}), | |
| positioning=context.positioning.model_dump() if getattr(context, "positioning", None) and hasattr(context.positioning, "model_dump") else (context.positioning if getattr(context, "positioning", None) else {}), | |
| positioning_view=context.positioning_view.model_dump() if getattr(context, "positioning_view", None) and hasattr(context.positioning_view, "model_dump") else (context.positioning_view if getattr(context, "positioning_view", None) else {}), | |
| goal_trajectory_data=context.goal_trajectory.model_dump() if getattr(context, "goal_trajectory", None) and hasattr(context.goal_trajectory, "model_dump") else (context.goal_trajectory if getattr(context, "goal_trajectory", None) else {}), | |
| insights=context.insights.model_dump() if getattr(context, "insights", None) and hasattr(context.insights, "model_dump") else (context.insights if getattr(context, "insights", None) else {}), | |
| plan=getattr(context, "plan", None), | |
| recommendation=context.recommendation.model_dump() if getattr(context, "recommendation", None) and hasattr(context.recommendation, "model_dump") else (context.recommendation if getattr(context, "recommendation", None) else {}), | |
| charts=decode_chart(week_charts), | |
| weekly_brief=performance_brief, | |
| weekly_focus=performance_focus, | |
| weekly_snapshot = context.weekly_snapshot if getattr(context, "weekly_snapshot", None) and hasattr(context.weekly_snapshot, "model_dump") else (context.weekly_snapshot if getattr(context, "weekly_snapshot", None) else {}), | |
| active_goal = context.active_goal.model_dump() if getattr(context, "active_goal", None) and hasattr(context.active_goal, "model_dump") else (context.active_goal if getattr(context, "active_goal", None) else {}), | |
| goal_view=context.goal_progress.model_dump() if getattr(context, "goal_progress", None) and hasattr(context.goal_progress, "model_dump") else (context.goal_progress if getattr(context, "goal_progress", None) else {}), | |
| ) | |
| language = getattr(context, "language", "en") | |
| snapshot.performance_story = _build_performance_story(snapshot, snapshot.weekly_trend, language) | |
| snapshot.delta_summary = _build_delta_summary(snapshot.weekly_trend, language) | |
| snapshot.evidence_view = _build_evidence_view(snapshot.positioning_view, snapshot.trend, language) | |
| snapshot.structure_view = _build_structure_view(snapshot.structure_status, snapshot.recommendation, language) | |
| snapshot.goal_status_text = _build_goal_status_text(snapshot.active_goal, language) | |
| return snapshot | |