extends Control const Brand := preload("res://scripts/ui/medmind_style.gd") const TrendChart := preload("res://scripts/ui/dashboard_chart.gd") const ActivityChart := preload("res://scripts/ui/dashboard_activity_chart.gd") const DashboardSparkline := preload("res://scripts/ui/dashboard_sparkline.gd") const DashboardSegmentBar := preload("res://scripts/ui/dashboard_segment_bar.gd") const DashboardInfoBadge := preload("res://scripts/ui/dashboard_info_badge.gd") const FeatureFlags := preload("res://scripts/config/feature_flags.gd") var main: Control var scroll: ScrollContainer var content: VBoxContainer var status_label: Label var stat_card_grid: GridContainer var analytics_grid: GridContainer var lower_grid: GridContainer var trend_chart: Control var rating_toggle: Button var accuracy_toggle: Button var activity_chart: Control var system_list: VBoxContainer var focus_content: VBoxContainer var review_list: VBoxContainer var recent_list: VBoxContainer var weekly_summary: VBoxContainer var mission_content: VBoxContainer var weekly_goal_content: VBoxContainer var rank_content: VBoxContainer var metric_values: Dictionary = {} var metric_details: Dictionary = {} var metric_trends: Dictionary = {} var qbank_sparkline: Control var ccs_sparkline: Control var readiness_segment_bar: Control var accuracy_progress_bar: ProgressBar var pending_review_launch := false var review_queue_service: Variant var trend_mode := "rating" var mastery_rows: Array[BoxContainer] = [] var mission_rows: Array[BoxContainer] = [] func _ready() -> void: set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) review_queue_service = get_node("/root/ReviewQueueService") DashboardService.dashboard_changed.connect(_render) DashboardService.dashboard_failed.connect(_show_error) DailyMissionService.missions_changed.connect(_render_missions) DailyMissionService.missions_failed.connect(_show_mission_error) WeeklyGoalService.goal_changed.connect(_render_weekly_goal) WeeklyGoalService.goal_failed.connect(_show_weekly_goal_error) review_queue_service.question_loaded.connect(_on_review_question_loaded) review_queue_service.question_failed.connect(_on_review_question_failed) review_queue_service.pass_completed.connect(_on_review_pass_completed) UIThemeService.theme_changed.connect(_rebuild) resized.connect(_update_responsive) _build() func on_show(_payload: Variant = null) -> void: if DashboardService.data.is_empty(): _show_loading() else: _render() DashboardService.refresh() DailyMissionService.refresh() WeeklyGoalService.refresh() func _build() -> void: for child in get_children(): remove_child(child) child.free() metric_values.clear() metric_details.clear() metric_trends.clear() mastery_rows.clear() mission_rows.clear() scroll = ScrollContainer.new() scroll.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED add_child(scroll) content = VBoxContainer.new() content.size_flags_horizontal = Control.SIZE_EXPAND_FILL content.add_theme_constant_override("separation", 14) scroll.add_child(content) status_label = _label("", 14, Brand.muted_color(UIThemeService.is_dark_mode())) status_label.visible = false content.add_child(status_label) stat_card_grid = GridContainer.new() stat_card_grid.columns = 4 stat_card_grid.add_theme_constant_override("h_separation", 12) stat_card_grid.add_theme_constant_override("v_separation", 12) content.add_child(stat_card_grid) mission_content = null if FeatureFlags.progression_enabled(): mission_content = VBoxContainer.new() mission_content.add_theme_constant_override("separation", 10) content.add_child(_card(mission_content, 16)) qbank_sparkline = DashboardSparkline.new() qbank_sparkline.custom_minimum_size = Vector2(0, 34) _add_stat_card("qbank", "Question Bank Rating", "book-open", Brand.ROYAL_BLUE, qbank_sparkline, "Your Elo-style rating from Question Bank answers, weighted by each question's difficulty.") ccs_sparkline = DashboardSparkline.new() ccs_sparkline.custom_minimum_size = Vector2(0, 34) _add_stat_card("ccs", "CCS Rating", "stethoscope", Brand.SUCCESS, ccs_sparkline, "Your Elo-style rating from timed CCS case performance, weighted by each case's difficulty.") readiness_segment_bar = DashboardSegmentBar.new() readiness_segment_bar.custom_minimum_size = Vector2(0, 10) _add_stat_card("readiness", "Exam Readiness", "chart-no-axes-column-increasing", Brand.PURPLE, readiness_segment_bar, "Heuristic pass probability from your most recent completed exam simulation.") accuracy_progress_bar = ProgressBar.new() accuracy_progress_bar.show_percentage = false accuracy_progress_bar.min_value = 0.0 accuracy_progress_bar.max_value = 100.0 accuracy_progress_bar.custom_minimum_size = Vector2(0, 10) _style_progress(accuracy_progress_bar, Brand.AMBER) _add_stat_card("accuracy", "Average Score", "star", Brand.AMBER, accuracy_progress_bar, "Your first-attempt accuracy across your most recent 100 answered questions.") analytics_grid = GridContainer.new() analytics_grid.columns = 2 analytics_grid.add_theme_constant_override("h_separation", 12) analytics_grid.add_theme_constant_override("v_separation", 12) content.add_child(analytics_grid) var performance := VBoxContainer.new() performance.size_flags_horizontal = Control.SIZE_EXPAND_FILL performance.add_theme_constant_override("separation", 10) var trend_header := HBoxContainer.new() trend_header.add_child(_section_title("Performance trend")) var trend_space := Control.new() trend_space.size_flags_horizontal = Control.SIZE_EXPAND_FILL trend_header.add_child(trend_space) var trend_group := ButtonGroup.new() trend_group.allow_unpress = false rating_toggle = _trend_toggle("Rating", "rating", trend_group) accuracy_toggle = _trend_toggle("Correct %", "accuracy", trend_group) trend_header.add_child(rating_toggle) trend_header.add_child(accuracy_toggle) performance.add_child(trend_header) trend_chart = TrendChart.new() trend_chart.custom_minimum_size = Vector2(520, 190) trend_chart.size_flags_horizontal = Control.SIZE_EXPAND_FILL performance.add_child(trend_chart) performance.add_child(HSeparator.new()) var system_header := HBoxContainer.new() system_header.add_child(_section_title("System mastery path" if FeatureFlags.progression_enabled() else "Performance by system")) var header_space := Control.new() header_space.size_flags_horizontal = Control.SIZE_EXPAND_FILL system_header.add_child(header_space) var breakdown := Button.new() breakdown.text = "View full breakdown" breakdown.flat = true breakdown.pressed.connect(func(): main.show_screen("profile")) system_header.add_child(breakdown) performance.add_child(system_header) system_list = VBoxContainer.new() system_list.add_theme_constant_override("separation", 8) performance.add_child(system_list) analytics_grid.add_child(_card(performance, 18)) var right_column := VBoxContainer.new() right_column.add_theme_constant_override("separation", 12) right_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL analytics_grid.add_child(right_column) rank_content = null if FeatureFlags.progression_enabled(): rank_content = VBoxContainer.new() rank_content.add_theme_constant_override("separation", 9) right_column.add_child(_card(rank_content, 16)) focus_content = VBoxContainer.new() focus_content.add_theme_constant_override("separation", 10) right_column.add_child(_card(focus_content, 16)) review_list = VBoxContainer.new() review_list.add_theme_constant_override("separation", 7) right_column.add_child(_card(review_list, 16)) lower_grid = GridContainer.new() lower_grid.columns = 2 lower_grid.add_theme_constant_override("h_separation", 12) lower_grid.add_theme_constant_override("v_separation", 12) content.add_child(lower_grid) var weekly := VBoxContainer.new() weekly.add_theme_constant_override("separation", 8) weekly.add_child(_section_title("Weekly activity")) var weekly_row := HBoxContainer.new() weekly_row.add_theme_constant_override("separation", 18) weekly.add_child(weekly_row) activity_chart = ActivityChart.new() activity_chart.custom_minimum_size = Vector2(360, 150) activity_chart.size_flags_horizontal = Control.SIZE_EXPAND_FILL weekly_row.add_child(activity_chart) weekly_summary = VBoxContainer.new() weekly_summary.custom_minimum_size.x = 180 weekly_summary.add_theme_constant_override("separation", 10) weekly_row.add_child(weekly_summary) weekly.add_child(HSeparator.new()) weekly_goal_content = null if FeatureFlags.progression_enabled(): weekly_goal_content = VBoxContainer.new() weekly_goal_content.add_theme_constant_override("separation", 8) weekly.add_child(weekly_goal_content) lower_grid.add_child(_card(weekly, 16)) recent_list = VBoxContainer.new() recent_list.add_theme_constant_override("separation", 6) lower_grid.add_child(_card(recent_list, 16)) _update_responsive() if not DashboardService.data.is_empty(): _render() func _rebuild(_is_dark: bool = UIThemeService.is_dark_mode()) -> void: _build() func _render() -> void: var data: Dictionary = DashboardService.data status_label.visible = false var trend: Array = data.get("performanceTrend", []) _render_rating_card("qbank", qbank_sparkline, Brand.ROYAL_BLUE, data.get("questionBankRating", 1000), data.get("questionBankRatingDeltaThisWeek"), data.get("questionBankPercentile"), "%d rated questions" % int(data.get("ratedQuestionCount", 0)), _trend_series(trend, "questionBankRating")) _render_rating_card("ccs", ccs_sparkline, Brand.SUCCESS, data.get("ccsRating", 1000), data.get("ccsRatingDeltaThisWeek"), data.get("ccsPercentile"), "%d rated cases" % int(data.get("ratedCcsCaseCount", 0)), _trend_series(trend, "ccsRating")) _render_readiness_card(data) _render_accuracy_card(data) trend_chart.set_series(data.get("performanceTrend", []), trend_mode, UIThemeService.is_dark_mode()) if FeatureFlags.progression_enabled(): _render_systems(data.get("systems", [])) _render_focus(data.get("systems", [])) else: _render_systems_legacy(data.get("systems", [])) _render_focus_legacy(data.get("systems", [])) _render_review(data) if FeatureFlags.progression_enabled(): _render_rank(data.get("progression", {})) var weekly: Array = data.get("weeklyActivity", []) activity_chart.set_days(weekly, UIThemeService.is_dark_mode()) _render_weekly(data, weekly) _render_recent(data.get("recentActivity", [])) if FeatureFlags.progression_enabled(): _render_missions() _render_weekly_goal() func _render_rank(value: Variant) -> void: if rank_content == null: return _clear(rank_content) rank_content.add_child(_section_title("Clinical progression")) var progression: Dictionary = value if typeof(value) == TYPE_DICTIONARY else {} if progression.is_empty(): rank_content.add_child(_label("Loading rank progress...", 13, Brand.muted_color(UIThemeService.is_dark_mode()))) return var title := _label(str(progression.get("displayName", "Case Scout III")), 23, Brand.PURPLE, true) rank_content.add_child(title) var next_name = progression.get("nextRankName") if next_name == null: rank_content.add_child(_label("Highest division earned", 12, Brand.SUCCESS, true)) else: rank_content.add_child(_label("Next: %s %s" % [str(next_name), str(progression.get("nextDivision", ""))], 12, Brand.muted_color(UIThemeService.is_dark_mode()))) var requirements: Array = progression.get("nextRequirements", []) for requirement_value in requirements.slice(0, mini(3, requirements.size())): var requirement: Dictionary = requirement_value var row := HBoxContainer.new() var label := _label(str(requirement.get("label", "Progress")), 11, Brand.text_color(UIThemeService.is_dark_mode())) label.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(label) row.add_child(_label("%d / %d" % [int(requirement.get("current", 0)), int(requirement.get("required", 0))], 11, Brand.SUCCESS if bool(requirement.get("complete", false)) else Brand.ROYAL_BLUE, true)) rank_content.add_child(row) var view := Button.new() view.text = "View ladder and records" view.flat = true view.alignment = HORIZONTAL_ALIGNMENT_LEFT view.pressed.connect(func(): main.show_screen("profile")) rank_content.add_child(view) func _render_missions() -> void: if mission_content == null: return _clear(mission_content) mission_rows.clear() var header := HBoxContainer.new() header.add_child(_section_title("Daily missions")) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(spacer) header.add_child(_label("Resets at 00:00 UTC", 11, Brand.muted_color(UIThemeService.is_dark_mode()))) mission_content.add_child(header) var missions: Array = DailyMissionService.state.get("missions", []) if missions.is_empty(): mission_content.add_child(_label("Loading today's focused study missions...", 14, Brand.muted_color(UIThemeService.is_dark_mode()))) return var replacement_available := bool(DailyMissionService.state.get("replacementAvailable", false)) for mission_value in missions: var mission: Dictionary = mission_value var row := BoxContainer.new() row.vertical = size.x < 700.0 mission_rows.append(row) row.add_theme_constant_override("separation", 10) var details := VBoxContainer.new() details.size_flags_horizontal = Control.SIZE_EXPAND_FILL var completed := bool(mission.get("completed", false)) var title := _label(str(mission.get("title", "Study mission")), 14, Brand.SUCCESS if completed else Brand.text_color(UIThemeService.is_dark_mode()), true) title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART details.add_child(title) var progress_value := mini(int(mission.get("progress", 0)), int(mission.get("target", 1))) var target := maxi(1, int(mission.get("target", 1))) var progress := ProgressBar.new() progress.show_percentage = false progress.max_value = target progress.value = progress_value progress.custom_minimum_size.y = 7 _style_progress(progress, Brand.SUCCESS if completed else Brand.ROYAL_BLUE) details.add_child(progress) details.add_child(_label("Complete - 3 RZ Coins earned" if completed else "%d of %d" % [progress_value, target], 11, Brand.muted_color(UIThemeService.is_dark_mode()))) row.add_child(details) var launch := _button("Review" if str(mission.get("type", "")) == "due_reviews" else "Start") launch.disabled = completed launch.pressed.connect(_launch_mission.bind(mission)) row.add_child(launch) if replacement_available and not completed: var replace := Button.new() replace.text = "Replace" replace.flat = true replace.pressed.connect(_replace_mission.bind(str(mission.get("id", "")))) row.add_child(replace) mission_content.add_child(row) func _launch_mission(mission: Dictionary) -> void: if str(mission.get("type", "")) == "due_reviews": _start_review_queue() return main.show_screen("setup", { "mastery_target": true, "step3_system": str(mission.get("step3System", "")), "question_count": 10, "difficulty_min": int(mission.get("difficultyMin", 0)), "difficulty_max": 5 if int(mission.get("difficultyMin", 0)) > 0 else 0 }) func _replace_mission(mission_id: String) -> void: DailyMissionService.replace(mission_id) _render_missions() func _show_mission_error(message: String) -> void: if mission_content == null: return _clear(mission_content) mission_content.add_child(_section_title("Daily missions")) mission_content.add_child(_label(message, 13, Brand.MAGENTA)) func _render_weekly_goal() -> void: if weekly_goal_content == null: return _clear(weekly_goal_content) var header := HBoxContainer.new() header.add_child(_section_title("Balanced weekly goal")) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(spacer) header.add_child(_label("Monday-Sunday UTC", 11, Brand.muted_color(UIThemeService.is_dark_mode()))) weekly_goal_content.add_child(header) var components: Array = WeeklyGoalService.state.get("components", []) if components.is_empty(): weekly_goal_content.add_child(_label("Loading weekly progress...", 13, Brand.muted_color(UIThemeService.is_dark_mode()))) return for component_value in components: var component: Dictionary = component_value var row := HBoxContainer.new() row.add_theme_constant_override("separation", 8) var completed := bool(component.get("completed", false)) var label := _label(str(component.get("label", "Weekly progress")), 12, Brand.SUCCESS if completed else Brand.text_color(UIThemeService.is_dark_mode())) label.size_flags_horizontal = Control.SIZE_EXPAND_FILL label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART row.add_child(label) row.add_child(_label("%d / %d" % [int(component.get("progress", 0)), int(component.get("target", 1))], 12, Brand.SUCCESS if completed else Brand.ROYAL_BLUE, true)) if not completed: var action := Button.new() action.text = "Start" action.flat = true action.pressed.connect(_launch_weekly_component.bind(component)) row.add_child(action) weekly_goal_content.add_child(row) if bool(WeeklyGoalService.state.get("completed", false)): weekly_goal_content.add_child(_label("Weekly goal complete - %d RZ Coins earned" % int(WeeklyGoalService.state.get("rewardAmount", 0)), 13, Brand.SUCCESS, true)) else: weekly_goal_content.add_child(_label("Complete all three for up to 10 RZ Coins.", 11, Brand.muted_color(UIThemeService.is_dark_mode()))) func _launch_weekly_component(component: Dictionary) -> void: var type := str(component.get("type", "")) if type == "reviews": _start_review_queue() return var system := str(WeeklyGoalService.state.get("suggestedSystem", "")) main.show_screen("setup", { "mastery_target": not system.is_empty(), "step3_system": system if not system.is_empty() else "All Systems", "question_count": 10, "difficulty_min": 4 if type == "hard_correct" else 0, "difficulty_max": 5 if type == "hard_correct" else 0 }) func _show_weekly_goal_error(message: String) -> void: if weekly_goal_content == null: return _clear(weekly_goal_content) weekly_goal_content.add_child(_section_title("Balanced weekly goal")) weekly_goal_content.add_child(_label(message, 12, Brand.MAGENTA)) func _trend_series(trend: Array, field: String) -> Array: var series: Array = [] for point in trend: var value = point.get(field) if value != null: series.append(float(value)) return series func _render_rating_card(key: String, sparkline: Control, accent: Color, rating_value: Variant, delta_value: Variant, percentile_value: Variant, fallback_caption: String, series: Array) -> void: var dark := UIThemeService.is_dark_mode() var rating := int(rating_value) var trend_text := "" var trend_color := Brand.muted_color(dark) var caption := fallback_caption if delta_value != null: var delta := int(delta_value) var arrow := "↗" if delta >= 0 else "↘" var sign_prefix := "+" if delta >= 0 else "" trend_text = "%s %s%d this week" % [arrow, sign_prefix, delta] trend_color = Brand.SUCCESS if delta >= 0 else Brand.MAGENTA if percentile_value != null: caption = "Top %d%% of users" % int(percentile_value) _set_stat_card(key, str(rating), trend_text, trend_color, caption) if series.size() < 2: sparkline.set_values([float(rating)], accent, Vector2(rating - 10.0, rating + 10.0)) else: var low: float = series.min() var high: float = series.max() var spread := maxf(10.0, (high - low) * 0.5) sparkline.set_values(series, accent, Vector2(low - spread, high + spread)) func _render_readiness_card(data: Dictionary) -> void: var dark := UIThemeService.is_dark_mode() var label_value = data.get("examReadinessLabel") if label_value == null: _set_stat_card("readiness", "--", "", Brand.muted_color(dark), "Complete a simulation to see this") readiness_segment_bar.set_progress(0, 4, Brand.muted_color(dark), dark) return var label := str(label_value) var pass_percent := int(data.get("examReadinessProjectedPassPercent", 0)) var status_color := Brand.SUCCESS if label == "High" else (Brand.AMBER if label == "Medium" else Brand.MAGENTA) var caption := "Provisional estimate" if label != "Low" else "Keep practicing" _set_stat_card("readiness", label, "Heuristic pass: %d%%" % pass_percent, status_color, caption) (metric_values.get("readiness") as Label).add_theme_color_override("font_color", status_color) readiness_segment_bar.set_progress(int(round(pass_percent / 100.0 * 4.0)), 4, status_color, dark) func _render_accuracy_card(data: Dictionary) -> void: var dark := UIThemeService.is_dark_mode() var accuracy := int(data.get("firstAttemptAccuracy", 0)) var delta_value = data.get("accuracyDeltaVsPreviousSample") var trend_text := "" var trend_color := Brand.muted_color(dark) if delta_value != null: var delta := int(delta_value) var arrow := "↗" if delta >= 0 else "↘" var sign_prefix := "+" if delta >= 0 else "" trend_text = "%s %s%d%% vs last 100" % [arrow, sign_prefix, delta] trend_color = Brand.SUCCESS if delta >= 0 else Brand.MAGENTA _set_stat_card("accuracy", "%d%%" % accuracy, trend_text, trend_color, "Last %d questions" % int(data.get("accuracySampleSize", 0))) accuracy_progress_bar.value = accuracy func _render_systems_legacy(systems_value: Array) -> void: _clear(system_list) var systems := systems_value.duplicate(true) if systems.is_empty(): system_list.add_child(_label("Answer questions to build your system breakdown.", 14, Brand.muted_color(UIThemeService.is_dark_mode()))) return var attempted: Array = systems.filter(func(item: Dictionary): return int(item.get("total", 0)) > 0) var strongest_system := "" var weakest_system := "" if not attempted.is_empty(): var strongest: Dictionary = attempted[0] var weakest: Dictionary = attempted[0] for item in attempted: if int(item.get("accuracy", 0)) > int(strongest.get("accuracy", 0)) or ( int(item.get("accuracy", 0)) == int(strongest.get("accuracy", 0)) and int(item.get("total", 0)) > int(strongest.get("total", 0))): strongest = item if int(item.get("accuracy", 0)) < int(weakest.get("accuracy", 0)) or ( int(item.get("accuracy", 0)) == int(weakest.get("accuracy", 0)) and int(item.get("total", 0)) > int(weakest.get("total", 0))): weakest = item strongest_system = str(strongest.get("system", "")) weakest_system = str(weakest.get("system", "")) for item in systems: var row := HBoxContainer.new() row.add_theme_constant_override("separation", 10) var system_name_label := _label(str(item.get("system", "General")), 13, Brand.text_color(UIThemeService.is_dark_mode())) system_name_label.custom_minimum_size.x = 170 system_name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL system_name_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART row.add_child(system_name_label) var bar := ProgressBar.new() bar.show_percentage = false bar.value = int(item.get("accuracy", 0)) bar.custom_minimum_size = Vector2(80, 7) bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL bar.size_flags_vertical = Control.SIZE_SHRINK_CENTER _style_progress(bar, Brand.PURPLE if str(item.get("system", "")) == weakest_system else Brand.ROYAL_BLUE) row.add_child(bar) var total := int(item.get("total", 0)) var percentage_text := "%d%%" % int(item.get("accuracy", 0)) if total > 0 else "—" var percentage := _label(percentage_text, 14, Brand.ROYAL_BLUE, true) percentage.custom_minimum_size.x = 48 row.add_child(percentage) var count := _label("%d / %d" % [int(item.get("correct", 0)), total], 12, Brand.muted_color(UIThemeService.is_dark_mode())) count.custom_minimum_size.x = 58 row.add_child(count) var system_name := str(item.get("system", "")) var marker := "Strongest" if system_name == strongest_system else ("Needs work" if system_name == weakest_system else "") var marker_label := _label(marker, 11, Brand.SUCCESS if marker == "Strongest" else Brand.MAGENTA) marker_label.custom_minimum_size.x = 72 row.add_child(marker_label) system_list.add_child(row) func _render_focus_legacy(systems: Array) -> void: _clear(focus_content) focus_content.add_child(_section_title("Study focus")) if systems.is_empty(): focus_content.add_child(_label("Complete a few questions to receive a focused recommendation.", 14, Brand.muted_color(UIThemeService.is_dark_mode()))) return var weakest: Dictionary = systems[0] for item in systems: if int(item.get("accuracy", 0)) < int(weakest.get("accuracy", 0)): weakest = item focus_content.add_child(_label("Recommended focus", 12, Brand.muted_color(UIThemeService.is_dark_mode()))) focus_content.add_child(_label(str(weakest.get("system", "General")), 23, Brand.PURPLE, true)) focus_content.add_child(_label("%d%% accuracy across %d questions" % [int(weakest.get("accuracy", 0)), int(weakest.get("total", 0))], 13, Brand.text_color(UIThemeService.is_dark_mode()))) var progress := ProgressBar.new() progress.show_percentage = false progress.value = int(weakest.get("accuracy", 0)) progress.custom_minimum_size.y = 8 _style_progress(progress, Brand.PURPLE) focus_content.add_child(progress) var practice := _button("Practice this system") practice.pressed.connect(func(): main.show_screen("selection", "question_bank")) focus_content.add_child(practice) func _render_systems(systems: Array) -> void: _clear(system_list) mastery_rows.clear() if systems.is_empty(): system_list.add_child(_label("Answer questions to begin your mastery path.", 14, Brand.muted_color(UIThemeService.is_dark_mode()))) return for item in systems: var container := VBoxContainer.new() container.add_theme_constant_override("separation", 6) var row := BoxContainer.new() row.vertical = false mastery_rows.append(row) row.add_theme_constant_override("separation", 10) var name_label := _label(str(item.get("system", "General")), 13, Brand.text_color(UIThemeService.is_dark_mode()), true) name_label.custom_minimum_size.x = 190 name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL name_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART row.add_child(name_label) var state := str(item.get("masteryState", "unseen")) var state_label := _label(state.capitalize(), 11, _mastery_color(state), true) state_label.custom_minimum_size.x = 74 row.add_child(state_label) var bar := ProgressBar.new() bar.show_percentage = false bar.value = int(item.get("masteryScore", 0)) bar.custom_minimum_size = Vector2(80, 7) bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL bar.size_flags_vertical = Control.SIZE_SHRINK_CENTER _style_progress(bar, _mastery_color(state)) row.add_child(bar) var percentage := _label("%d%%" % int(item.get("masteryScore", 0)) if int(item.get("distinctQuestions", 0)) > 0 else "--", 14, Brand.ROYAL_BLUE, true) percentage.custom_minimum_size.x = 48 row.add_child(percentage) var action: Dictionary = item.get("recommendedAction", {}) var action_button := _button(str(action.get("label", "Practice"))) action_button.custom_minimum_size.x = 155 action_button.pressed.connect(_launch_mastery_action.bind(item)) row.add_child(action_button) container.add_child(row) var evidence := "%d distinct | %d difficulty 4+ successful" % [int(item.get("distinctQuestions", 0)), int(item.get("difficulty4PlusSuccessful", 0))] if int(item.get("pendingReviews", 0)) > 0: evidence += " | %d review(s) pending" % int(item.get("pendingReviews", 0)) elif bool(item.get("refreshDue", false)): evidence += " | Refresh due" container.add_child(_label(evidence, 11, Brand.muted_color(UIThemeService.is_dark_mode()))) var requirements := _label(_mastery_requirement(item), 11, Brand.muted_color(UIThemeService.is_dark_mode())) requirements.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART requirements.visible = false var requirement_toggle := Button.new() requirement_toggle.text = "Show requirements" requirement_toggle.flat = true requirement_toggle.alignment = HORIZONTAL_ALIGNMENT_LEFT requirement_toggle.pressed.connect(func() -> void: requirements.visible = not requirements.visible requirement_toggle.text = "Hide requirements" if requirements.visible else "Show requirements") container.add_child(requirement_toggle) container.add_child(requirements) system_list.add_child(container) func _render_focus(systems: Array) -> void: _clear(focus_content) focus_content.add_child(_section_title("Study focus")) if systems.is_empty(): focus_content.add_child(_label("Complete a few questions to receive a focused recommendation.", 14, Brand.muted_color(UIThemeService.is_dark_mode()))) return var weakest: Dictionary = systems[0] for item in systems: if _is_higher_focus(item, weakest): weakest = item focus_content.add_child(_label("Recommended focus", 12, Brand.muted_color(UIThemeService.is_dark_mode()))) focus_content.add_child(_label(str(weakest.get("system", "General")), 23, Brand.PURPLE, true)) focus_content.add_child(_label("%s | %d%% mastery across %d questions" % [str(weakest.get("masteryState", "unseen")).capitalize(), int(weakest.get("masteryScore", 0)), int(weakest.get("distinctQuestions", 0))], 13, Brand.text_color(UIThemeService.is_dark_mode()))) var progress := ProgressBar.new() progress.show_percentage = false progress.value = int(weakest.get("masteryScore", 0)) progress.custom_minimum_size.y = 8 _style_progress(progress, Brand.PURPLE) focus_content.add_child(progress) var action: Dictionary = weakest.get("recommendedAction", {}) var practice := _button(str(action.get("label", "Start learning"))) practice.pressed.connect(_launch_mastery_action.bind(weakest)) focus_content.add_child(practice) func _mastery_color(state: String) -> Color: match state: "mastered": return Brand.SUCCESS "strong": return Brand.TEAL "practicing": return Brand.ROYAL_BLUE "learning": return Brand.AMBER _: return Brand.muted_color(UIThemeService.is_dark_mode()) func _mastery_requirement(item: Dictionary) -> String: match str(item.get("masteryState", "unseen")): "unseen": return "Answer your first question in this system to begin the path." "learning": return "Reach 5 distinct questions to enter Practicing." "practicing": return "Strong requires 15 distinct questions and at least 75% mastery." "strong": return "Mastered requires 25 distinct questions, 85% mastery, 5 successful difficulty 4+ questions, and no unresolved reviews." _: return "Complete a difficulty 4-5 refresh when due to keep this mastery current." if bool(item.get("refreshDue", false)) else "This system currently meets every mastery requirement." func _mastery_priority(item: Dictionary) -> int: if int(item.get("pendingReviews", 0)) > 0: return -1000 + int(item.get("masteryScore", 0)) var ranks := {"unseen": 0, "learning": 1, "practicing": 2, "strong": 3, "mastered": 4} return int(ranks.get(str(item.get("masteryState", "unseen")), 0)) * 100 + int(item.get("masteryScore", 0)) func _is_higher_focus(candidate: Dictionary, current: Dictionary) -> bool: var candidate_priority := _mastery_priority(candidate) var current_priority := _mastery_priority(current) if candidate_priority != current_priority: return candidate_priority < current_priority var candidate_date := str(candidate.get("lastEvidenceUtc", "")) var current_date := str(current.get("lastEvidenceUtc", "")) if candidate_date.is_empty(): return not current_date.is_empty() return not current_date.is_empty() and candidate_date < current_date func _launch_mastery_action(item: Dictionary) -> void: var action: Dictionary = item.get("recommendedAction", {}) var system_name := str(item.get("system", "")) if str(action.get("type", "practice")) == "review": _start_review_queue(system_name) return main.show_screen("setup", { "mastery_target": true, "step3_system": system_name, "question_count": int(action.get("questionCount", 10)), "difficulty_min": int(action.get("difficultyMin", 0)) if action.get("difficultyMin") != null else 0, "difficulty_max": int(action.get("difficultyMax", 0)) if action.get("difficultyMax") != null else 0 }) func _render_review(data: Dictionary) -> void: _clear(review_list) review_list.add_child(_section_title("Review queue")) var missed_count := int(data.get("missedQuestionCount", 0)) var missed_action := _review_action( "%d missed questions" % missed_count if missed_count > 0 else "Review queue clear", "x", Brand.MAGENTA, "") missed_action.disabled = missed_count <= 0 or pending_review_launch missed_action.pressed.connect(_start_review_queue) review_list.add_child(missed_action) if Brand.is_uat_environment(): var dynamic_due := int(data.get("dynamicReviewsDue", 0)) review_list.add_child(_review_action( "%d dynamic concepts due" % dynamic_due if dynamic_due > 0 else "Dynamic concepts up to date", "activity", Brand.PURPLE if dynamic_due > 0 else Brand.ROYAL_BLUE, "dynamic_questions")) if Brand.is_uat_environment(): var flashcards_due := int(data.get("flashcardsDue", 0)) review_list.add_child(_review_action( "%d flashcards due" % flashcards_due if flashcards_due > 0 else "Flashcards up to date", "clipboard-plus", Brand.TEAL if flashcards_due > 0 else Brand.ROYAL_BLUE, "flashcards")) review_list.add_child(_review_action("%d bookmarks" % int(data.get("bookmarkCount", 0)), "bookmark", Brand.ROYAL_BLUE, "bookmarks")) review_list.add_child(_review_action("%d notes" % int(data.get("noteCount", 0)), "notebook-pen", Brand.AMBER, "notes")) func _render_weekly(data: Dictionary, days: Array) -> void: _clear(weekly_summary) var questions := 0 var cases := 0 for day in days: questions += int(day.get("questions", 0)) cases += int(day.get("ccsCases", 0)) weekly_summary.add_child(_summary_stat(str(questions), "Questions answered", "notebook-pen", Brand.ROYAL_BLUE)) weekly_summary.add_child(_summary_stat(str(cases), "CCS cases completed", "stethoscope", Brand.PURPLE)) weekly_summary.add_child(_summary_stat("%d-day" % int(data.get("currentStreak", 0)), "Current streak", "sun", Brand.AMBER)) func _render_recent(items: Array) -> void: _clear(recent_list) recent_list.add_child(_section_title("Recent activity")) if items.is_empty(): recent_list.add_child(_label("Your completed study activity will appear here.", 14, Brand.muted_color(UIThemeService.is_dark_mode()))) return for item in items: var row := HBoxContainer.new() row.custom_minimum_size.y = 34 row.add_theme_constant_override("separation", 9) var type := str(item.get("type", "question")) var icon_name := "stethoscope" if type == "ccs" else ("clipboard-list" if type == "simulation" else "activity") var icon := TextureRect.new() icon.texture = load("res://assets/icons/lucide/%s.svg" % icon_name) icon.custom_minimum_size = Vector2(20, 20) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED row.add_child(icon) var title := _label(str(item.get("title", "Study activity")), 13, Brand.text_color(UIThemeService.is_dark_mode())) title.size_flags_horizontal = Control.SIZE_EXPAND_FILL title.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS row.add_child(title) row.add_child(_label(str(item.get("detail", "")), 13, Brand.ROYAL_BLUE, true)) row.add_child(_label(_relative_time(str(item.get("occurredUtc", ""))), 11, Brand.muted_color(UIThemeService.is_dark_mode()))) recent_list.add_child(row) func _info_badge(dark: bool, tooltip: String) -> Control: var badge := DashboardInfoBadge.new() badge.custom_minimum_size = Vector2(16, 16) badge.size_flags_vertical = Control.SIZE_SHRINK_BEGIN badge.mouse_filter = Control.MOUSE_FILTER_STOP badge.tooltip_text = tooltip badge.set_ring_color(Brand.muted_color(dark)) var glyph := _label("i", 10, Brand.muted_color(dark), true) glyph.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) glyph.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER glyph.vertical_alignment = VERTICAL_ALIGNMENT_CENTER glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE badge.add_child(glyph) return badge func _add_stat_card(key: String, title: String, icon_name: String, accent: Color, bottom_visual: Control, info_tooltip: String) -> void: var dark := UIThemeService.is_dark_mode() var card_content := VBoxContainer.new() card_content.custom_minimum_size = Vector2(220, 0) card_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL card_content.add_theme_constant_override("separation", 8) var header := HBoxContainer.new() header.add_theme_constant_override("separation", 10) card_content.add_child(header) var icon_panel := PanelContainer.new() icon_panel.custom_minimum_size = Vector2(40, 40) icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, accent, 20, true)) var icon := TextureRect.new() icon.texture = load("res://assets/icons/lucide/%s.svg" % icon_name) icon.custom_minimum_size = Vector2(18, 18) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon_panel.add_child(icon) header.add_child(icon_panel) var title_label := _label(title, 14, Brand.text_color(dark)) title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL title_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART header.add_child(title_label) header.add_child(_info_badge(dark, info_tooltip)) var value := _label("--", 28, Brand.text_color(dark), true) card_content.add_child(value) var trend := _label("", 13, Brand.muted_color(dark)) card_content.add_child(trend) card_content.add_child(bottom_visual) var caption := _label("Loading", 12, Brand.muted_color(dark)) card_content.add_child(caption) metric_values[key] = value metric_trends[key] = trend metric_details[key] = caption stat_card_grid.add_child(_card(card_content, 16)) func _set_stat_card(key: String, value: String, trend_text: String, trend_color: Color, caption: String) -> void: (metric_values.get(key) as Label).text = value var trend_label: Label = metric_trends.get(key) if trend_label != null: trend_label.text = trend_text trend_label.add_theme_color_override("font_color", trend_color) (metric_details.get(key) as Label).text = caption func _review_action(text: String, icon_name: String, accent: Color, route: String) -> Button: var button := _button(text, icon_name, false) button.alignment = HORIZONTAL_ALIGNMENT_LEFT button.add_theme_color_override("icon_normal_color", accent) if not route.is_empty(): button.pressed.connect(func(): main._on_navigation_pressed(route)) return button func _start_review_queue(step3_system: String = "") -> void: if pending_review_launch: return pending_review_launch = true _show_loading_message("Preparing your review queue...") set_meta("pending_review_system", step3_system) if not FeatureFlags.progression_enabled(): review_queue_service.start_pass(step3_system) _render_review(DashboardService.data) return if not StudySessionService.session_started.is_connected(_on_review_session_started): StudySessionService.session_started.connect(_on_review_session_started) if not StudySessionService.session_failed.is_connected(_on_review_session_failed): StudySessionService.session_failed.connect(_on_review_session_failed) StudySessionService.start("due_reviews", 20, "Tutor", step3_system) _render_review(DashboardService.data) func _on_review_session_started() -> void: if not pending_review_launch or not visible: return review_queue_service.start_pass(str(get_meta("pending_review_system", "")), int(StudySessionService.progress.get("targetValue", 20))) func _on_review_session_failed(message: String) -> void: if pending_review_launch and visible: pending_review_launch = false _show_error(message) func _on_review_question_loaded(question: Dictionary) -> void: if not pending_review_launch or not visible: return pending_review_launch = false if BlockSessionService.start_review_question(question): main.show_screen("player") else: _show_error("The review question could not be prepared.") func _on_review_question_failed(message: String) -> void: if not pending_review_launch or not visible: return pending_review_launch = false _show_error(message) _render_review(DashboardService.data) func _on_review_pass_completed(_summary: Dictionary) -> void: if not pending_review_launch or not visible: return pending_review_launch = false DashboardService.refresh() _show_loading_message("Your review queue is clear.") func _show_loading_message(message: String) -> void: status_label.text = message status_label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode())) status_label.visible = true func _summary_stat(value: String, caption: String, icon_name: String, accent: Color) -> HBoxContainer: var row := HBoxContainer.new() row.add_theme_constant_override("separation", 10) var icon := TextureRect.new() icon.texture = load("res://assets/icons/lucide/%s.svg" % icon_name) icon.custom_minimum_size = Vector2(24, 24) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED row.add_child(icon) var copy := VBoxContainer.new() copy.add_child(_label(value, 20, accent, true)) copy.add_child(_label(caption, 11, Brand.muted_color(UIThemeService.is_dark_mode()))) row.add_child(copy) return row func _card(child: Control, margin: int) -> PanelContainer: var panel := PanelContainer.new() panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL panel.add_theme_stylebox_override("panel", _card_box(margin)) panel.add_child(child) return panel func _card_box(margin: int) -> StyleBoxFlat: var box := StyleBoxFlat.new() var dark := UIThemeService.is_dark_mode() box.bg_color = Color("081426") if dark else Color.WHITE box.border_color = Color("26344f") if dark else Brand.LIGHT_BORDER box.set_border_width_all(1) box.set_corner_radius_all(8) box.content_margin_left = margin box.content_margin_right = margin box.content_margin_top = margin box.content_margin_bottom = margin return box func _button(text: String, icon_name: String = "", primary: bool = true) -> Button: var button := Button.new() button.text = text button.custom_minimum_size = Vector2(0, 42) button.add_theme_font_override("font", Brand.font) button.add_theme_font_size_override("font_size", 14) if not icon_name.is_empty(): button.icon = load("res://assets/icons/lucide/%s.svg" % icon_name) button.add_theme_constant_override("icon_max_width", 18) if primary: var normal := StyleBoxFlat.new() normal.bg_color = Brand.ROYAL_BLUE normal.set_corner_radius_all(6) var hover := normal.duplicate() as StyleBoxFlat hover.bg_color = Color("3474ee") button.add_theme_stylebox_override("normal", normal) button.add_theme_stylebox_override("hover", hover) button.add_theme_stylebox_override("pressed", normal) button.add_theme_color_override("font_color", Color.WHITE) button.add_theme_color_override("icon_normal_color", Color.WHITE) else: button.add_theme_stylebox_override("normal", Brand.secondary_button_box(UIThemeService.is_dark_mode())) return button func _trend_toggle(text: String, mode: String, group: ButtonGroup) -> Button: var button := Button.new() button.text = text button.toggle_mode = true button.button_group = group button.button_pressed = trend_mode == mode button.custom_minimum_size = Vector2(86, 32) button.add_theme_font_override("font", Brand.font) button.add_theme_font_size_override("font_size", 12) var normal := Brand.secondary_button_box(UIThemeService.is_dark_mode()) var selected := StyleBoxFlat.new() selected.bg_color = Brand.ROYAL_BLUE selected.set_corner_radius_all(6) button.add_theme_stylebox_override("normal", normal) button.add_theme_stylebox_override("hover", normal) button.add_theme_stylebox_override("pressed", selected) button.add_theme_color_override("font_pressed_color", Color.WHITE) button.pressed.connect(_set_trend_mode.bind(mode)) return button func _set_trend_mode(mode: String) -> void: trend_mode = mode if trend_chart != null and not DashboardService.data.is_empty(): trend_chart.set_series(DashboardService.data.get("performanceTrend", []), trend_mode, UIThemeService.is_dark_mode()) func _label(text: String, font_size: int, color: Color, bold: bool = false) -> Label: var label := Label.new() label.text = text label.add_theme_font_override("font", Brand.font) label.add_theme_font_size_override("font_size", font_size) label.add_theme_color_override("font_color", color) if bold: label.add_theme_constant_override("outline_size", 0) return label func _section_title(text: String) -> Label: return _label(text, 16, Brand.text_color(UIThemeService.is_dark_mode()), true) func _style_progress(bar: ProgressBar, accent: Color) -> void: var background := StyleBoxFlat.new() background.bg_color = Color("26324a") if UIThemeService.is_dark_mode() else Color("e2e8f0") background.set_corner_radius_all(4) var fill := StyleBoxFlat.new() fill.bg_color = accent fill.set_corner_radius_all(4) bar.add_theme_stylebox_override("background", background) bar.add_theme_stylebox_override("fill", fill) func _show_loading() -> void: if status_label != null: status_label.text = "Loading your progress..." status_label.visible = true func _show_error(message: String) -> void: status_label.text = message status_label.add_theme_color_override("font_color", Brand.MAGENTA) status_label.visible = true func _update_responsive() -> void: if stat_card_grid == null: return var compact := size.x < 1050.0 stat_card_grid.columns = 2 if compact else 4 analytics_grid.columns = 1 if compact else 2 lower_grid.columns = 1 if compact else 2 if trend_chart != null: trend_chart.custom_minimum_size.x = 300.0 if compact else 520.0 var narrow := size.x < 700.0 for row in mastery_rows: row.vertical = narrow for child in row.get_children(): if child is Button: child.custom_minimum_size.x = 0.0 if narrow else 155.0 for row in mission_rows: row.vertical = narrow func _clear(container: Container) -> void: for child in container.get_children(): container.remove_child(child) child.free() func _relative_time(iso_value: String) -> String: var cleaned := iso_value.trim_suffix("Z") var occurred := Time.get_unix_time_from_datetime_string(cleaned) if occurred <= 0: return "" var elapsed := maxi(0, int(Time.get_unix_time_from_system() - occurred)) if elapsed < 3600: return "%dm ago" % maxi(1, int(floor(float(elapsed) / 60.0))) if elapsed < 86400: return "%dh ago" % int(floor(float(elapsed) / 3600.0)) return "%dd ago" % int(floor(float(elapsed) / 86400.0))