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") var main: Control var scroll: ScrollContainer var content: VBoxContainer var status_label: Label var metric_grid: GridContainer var analytics_grid: GridContainer var lower_grid: GridContainer var trend_chart: Control 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 metric_values: Dictionary = {} var metric_details: Dictionary = {} var pending_review_launch := false func _ready() -> void: set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) DashboardService.dashboard_changed.connect(_render) DashboardService.dashboard_failed.connect(_show_error) ReviewQueueService.question_loaded.connect(_on_review_question_loaded) ReviewQueueService.question_failed.connect(_on_review_question_failed) ReviewQueueService.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() func _build() -> void: for child in get_children(): remove_child(child) child.free() metric_values.clear() metric_details.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) metric_grid = GridContainer.new() metric_grid.columns = 4 metric_grid.add_theme_constant_override("h_separation", 0) metric_grid.add_theme_constant_override("v_separation", 8) content.add_child(_card(metric_grid, 12)) _add_metric("qbank", "Question Bank Rating", "chart-no-axes-column-increasing", Brand.ROYAL_BLUE) _add_metric("ccs", "CCS Rating", "stethoscope", Brand.PURPLE) _add_metric("accuracy", "First-Attempt Accuracy", "activity", Brand.ROYAL_BLUE) _add_metric("time", "Study Time", "clock-3", Brand.PURPLE) 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) performance.add_child(_section_title("Performance trend")) 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("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) 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) 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 _set_metric("qbank", str(int(data.get("questionBankRating", 1000))), "%d rated questions" % int(data.get("ratedQuestionCount", 0))) _set_metric("ccs", str(int(data.get("ccsRating", 1000))), "%d rated cases" % int(data.get("ratedCcsCaseCount", 0))) _set_metric("accuracy", "%d%%" % int(data.get("firstAttemptAccuracy", 0)), "Last %d questions" % int(data.get("accuracySampleSize", 0))) _set_metric("time", _format_duration(int(data.get("studyTimeSecondsThisWeek", 0))), "This week") var trend_values: Array = [] for point in data.get("performanceTrend", []): if int(point.get("total", 0)) > 0: trend_values.append(float(point.get("accuracy", 0))) trend_chart.set_values(trend_values, UIThemeService.is_dark_mode()) _render_systems(data.get("systems", [])) _render_focus(data.get("systems", [])) _render_review(data) var weekly: Array = data.get("weeklyActivity", []) activity_chart.set_days(weekly, UIThemeService.is_dark_mode()) _render_weekly(data, weekly) _render_recent(data.get("recentActivity", [])) func _render_systems(systems_value: Array) -> void: _clear(system_list) var systems := systems_value.duplicate(true) systems.sort_custom(func(a: Dictionary, b: Dictionary): return int(a.get("total", 0)) > int(b.get("total", 0))) 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 displayed: Array = systems.slice(0, mini(5, systems.size())) var best_accuracy := -1 var worst_accuracy := 101 for item in displayed: best_accuracy = maxi(best_accuracy, int(item.get("accuracy", 0))) worst_accuracy = mini(worst_accuracy, int(item.get("accuracy", 0))) for item in displayed: 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 = 190 system_name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS 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(150, 7) bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL bar.size_flags_vertical = Control.SIZE_SHRINK_CENTER _style_progress(bar, Brand.PURPLE if int(item.get("accuracy", 0)) == worst_accuracy else Brand.ROYAL_BLUE) row.add_child(bar) var percentage := _label("%d%%" % int(item.get("accuracy", 0)), 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)), int(item.get("total", 0))], 12, Brand.muted_color(UIThemeService.is_dark_mode())) count.custom_minimum_size.x = 58 row.add_child(count) var marker := "Strongest" if int(item.get("accuracy", 0)) == best_accuracy else ("Needs work" if int(item.get("accuracy", 0)) == worst_accuracy 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(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", "chevron-right") practice.pressed.connect(func(): main.show_screen("selection", "question_bank")) focus_content.add_child(practice) 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) 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 _add_metric(key: String, title: String, icon_name: String, accent: Color) -> void: var cell := HBoxContainer.new() cell.size_flags_horizontal = Control.SIZE_EXPAND_FILL cell.custom_minimum_size = Vector2(230, 96) cell.add_theme_constant_override("separation", 12) var icon_panel := PanelContainer.new() icon_panel.custom_minimum_size = Vector2(48, 48) icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(UIThemeService.is_dark_mode(), accent, 24)) var icon := TextureRect.new() icon.texture = load("res://assets/icons/lucide/%s.svg" % icon_name) icon.custom_minimum_size = Vector2(22, 22) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon_panel.add_child(icon) cell.add_child(icon_panel) var copy := VBoxContainer.new() copy.size_flags_vertical = Control.SIZE_SHRINK_CENTER copy.add_child(_label(title, 15, Brand.text_color(UIThemeService.is_dark_mode()))) var value := _label("--", 29, Brand.text_color(UIThemeService.is_dark_mode()), true) copy.add_child(value) var detail := _label("Loading", 13, Brand.muted_color(UIThemeService.is_dark_mode())) copy.add_child(detail) metric_values[key] = value metric_details[key] = detail cell.add_child(copy) metric_grid.add_child(cell) func _set_metric(key: String, value: String, detail: String) -> void: (metric_values.get(key) as Label).text = value (metric_details.get(key) as Label).text = detail 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() -> void: if pending_review_launch: return pending_review_launch = true _show_loading_message("Preparing your review queue...") ReviewQueueService.start_pass() _render_review(DashboardService.data) 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 _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 metric_grid == null: return var compact := size.x < 1050.0 metric_grid.columns = 2 if compact else 4 analytics_grid.columns = 1 if compact else 2 lower_grid.columns = 1 if compact else 2 func _clear(container: Container) -> void: for child in container.get_children(): container.remove_child(child) child.free() func _format_duration(total_seconds: int) -> String: var hours := int(floor(float(total_seconds) / 3600.0)) var minutes := int(floor(float(total_seconds % 3600) / 60.0)) return "%dh %02dm" % [hours, minutes] 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))