extends VBoxContainer const Style := preload("res://scripts/ui/ui_style.gd") const Brand := preload("res://scripts/ui/medmind_style.gd") signal stage_changed(stage_name: String) var main: Control var exam_view: VBoxContainer var mode_view: VBoxContainer var question_bank_launch_view: VBoxContainer var stage_scroll: ScrollContainer var stage_content: VBoxContainer var exam_grid: VBoxContainer var exam_top_row: BoxContainer var mode_grid: GridContainer var question_bank_button: Button var bank_status_label: Label var bank_error_label: Label var retry_button: Button var launch_status_label: Label var launch_error_label: Label var launch_retry_button: Button var ccs_button: Button var ccs_status_label: Label var pending_ccs_launch := false var current_stage := "exam" var pending_question_bank_launch := false var themed_controls: Array[Control] = [] var responsive_cards: Array[PanelContainer] = [] var responsive_card_rows: Array[BoxContainer] = [] func _ready() -> void: set_anchors_preset(Control.PRESET_FULL_RECT) add_theme_constant_override("separation", 18) resized.connect(_refresh_grid_columns) if not QuestionBankService.questions_changed.is_connected(_on_questions_changed): QuestionBankService.questions_changed.connect(_on_questions_changed) if not QuestionBankService.questions_load_failed.is_connected(_on_questions_load_failed): QuestionBankService.questions_load_failed.connect(_on_questions_load_failed) if not CCSService.attempt_changed.is_connected(_on_ccs_attempt_changed): CCSService.attempt_changed.connect(_on_ccs_attempt_changed) if not CCSService.request_failed.is_connected(_on_ccs_failed): CCSService.request_failed.connect(_on_ccs_failed) stage_scroll = ScrollContainer.new() stage_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL stage_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL stage_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED add_child(stage_scroll) stage_content = VBoxContainer.new() stage_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL stage_scroll.add_child(stage_content) _build_exam_view() _build_mode_view() _build_question_bank_launch_view() _show_stage("exam") func on_show(payload: Variant = null) -> void: if str(payload) == "step3": _show_stage("step3") elif str(payload) == "question_bank": _show_stage("question_bank") call_deferred("_select_question_bank") elif str(payload) == "exam": _show_stage("exam") _refresh_bank_state() func on_hide() -> void: pending_question_bank_launch = false pending_ccs_launch = false func _build_exam_view() -> void: exam_view = VBoxContainer.new() exam_view.add_theme_constant_override("separation", 18) exam_view.size_flags_vertical = Control.SIZE_EXPAND_FILL stage_content.add_child(exam_view) var eyebrow := Label.new() eyebrow.text = "GET STARTED" eyebrow.add_theme_font_override("font", Brand.font) eyebrow.add_theme_font_size_override("font_size", 14) eyebrow.add_theme_color_override("font_color", Brand.SKY_BLUE) exam_view.add_child(eyebrow) var title := Label.new() title.text = "What are you preparing for?" title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART title.size_flags_horizontal = Control.SIZE_EXPAND_FILL title.add_theme_font_override("font", Brand.font) title.add_theme_font_size_override("font_size", 34) exam_view.add_child(title) var subtitle := Label.new() subtitle.text = "Select a path to enter its study modes. More exams will be added as they are ready." subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART subtitle.add_theme_font_override("font", Brand.font) subtitle.add_theme_font_size_override("font_size", 17) subtitle.add_theme_color_override("font_color", Brand.SLATE) subtitle.set_meta("medmind_role", "muted") themed_controls.append(subtitle) exam_view.add_child(subtitle) exam_grid = VBoxContainer.new() exam_grid.add_theme_constant_override("separation", 16) exam_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL exam_grid.size_flags_vertical = Control.SIZE_EXPAND_FILL exam_view.add_child(exam_grid) exam_top_row = BoxContainer.new() exam_top_row.add_theme_constant_override("separation", 16) exam_top_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL exam_grid.add_child(exam_top_row) _add_exam_option("STEP 1", "Foundational Sciences", "Build a strong understanding of the basic science concepts tested on Step 1.", "microscope", false) _add_exam_option("STEP 2", "Clinical Knowledge", "Master clinical concepts and apply knowledge to patient care scenarios.", "stethoscope", false) _add_exam_option("STEP 3", "Advanced Clinical Management", "Focus on advanced clinical management, CCS cases, and real-world decision making.", "clipboard-plus", true) func _build_mode_view() -> void: mode_view = VBoxContainer.new() mode_view.add_theme_constant_override("separation", 16) mode_view.size_flags_vertical = Control.SIZE_EXPAND_FILL stage_content.add_child(mode_view) var back_button := Button.new() back_button.text = "< All exams" back_button.alignment = HORIZONTAL_ALIGNMENT_LEFT back_button.custom_minimum_size = Vector2(0, 48) back_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL _style_back_bar(back_button) back_button.set_meta("medmind_role", "back_bar") themed_controls.append(back_button) UISoundService.set_button_sound(back_button, "back") back_button.pressed.connect(func(): _show_stage("exam")) mode_view.add_child(back_button) var eyebrow := Label.new() eyebrow.text = "STEP 3" eyebrow.add_theme_font_override("font", Brand.font) eyebrow.add_theme_font_size_override("font_size", 14) eyebrow.add_theme_color_override("font_color", Brand.TEAL) mode_view.add_child(eyebrow) var title := Label.new() title.text = "Choose a study mode" title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART title.size_flags_horizontal = Control.SIZE_EXPAND_FILL title.add_theme_font_override("font", Brand.font) title.add_theme_font_size_override("font_size", 34) mode_view.add_child(title) var subtitle := Label.new() subtitle.text = "Build clinical judgment through focused authored questions." subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART subtitle.add_theme_font_override("font", Brand.font) subtitle.add_theme_font_size_override("font_size", 17) subtitle.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode())) subtitle.set_meta("medmind_role", "muted") themed_controls.append(subtitle) mode_view.add_child(subtitle) mode_grid = GridContainer.new() mode_grid.columns = 2 mode_grid.add_theme_constant_override("h_separation", 16) mode_grid.add_theme_constant_override("v_separation", 16) mode_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL mode_view.add_child(mode_grid) var question_card := _add_mode_card( "Question Bank", "Adaptive core run", "Available", "Recommended", "Targeted practice with adaptive learning\nto strengthen your weaknesses.", "clipboard-list", true, Brand.TEAL) question_bank_button = question_card.action question_bank_button.pressed.connect(_select_question_bank) bank_status_label = question_card.status bank_error_label = question_card.error retry_button = question_card.retry retry_button.pressed.connect(_retry_question_bank) if Brand.is_uat_environment(): var ccs_card := _add_mode_card( "CCS Cases", "Interactive case simulations", "Available", "Available", "Practice real-world clinical scenarios and\nimprove your management skills.", "stethoscope", true, Brand.TEAL) ccs_button = ccs_card.action ccs_status_label = ccs_card.status ccs_button.pressed.connect(_select_ccs) func _build_question_bank_launch_view() -> void: question_bank_launch_view = VBoxContainer.new() question_bank_launch_view.name = "QuestionBankLaunch" question_bank_launch_view.size_flags_horizontal = Control.SIZE_EXPAND_FILL question_bank_launch_view.size_flags_vertical = Control.SIZE_EXPAND_FILL question_bank_launch_view.alignment = BoxContainer.ALIGNMENT_CENTER stage_content.add_child(question_bank_launch_view) var center := CenterContainer.new() center.size_flags_horizontal = Control.SIZE_EXPAND_FILL center.size_flags_vertical = Control.SIZE_EXPAND_FILL question_bank_launch_view.add_child(center) var panel := PanelContainer.new() panel.custom_minimum_size = Vector2(560, 300) panel.set_meta("medmind_wide_minimum", Vector2(560, 300)) panel.add_theme_stylebox_override("panel", Brand.content_card_box(UIThemeService.is_dark_mode(), true, Brand.TEAL)) panel.set_meta("medmind_role", "question_bank_launch") themed_controls.append(panel) responsive_cards.append(panel) center.add_child(panel) var content := VBoxContainer.new() content.alignment = BoxContainer.ALIGNMENT_CENTER content.add_theme_constant_override("separation", 18) panel.add_child(content) var icon := TextureRect.new() icon.texture = preload("res://assets/icons/lucide/clipboard-list.svg") icon.custom_minimum_size = Vector2(54, 54) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.modulate = Brand.TEAL if UIThemeService.is_dark_mode() else Brand.ROYAL_BLUE icon.set_meta("medmind_role", "question_bank_launch_icon") themed_controls.append(icon) content.add_child(icon) var title := Label.new() title.text = "Preparing your question bank" title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER title.add_theme_font_override("font", Brand.font) title.add_theme_font_size_override("font_size", 27) content.add_child(title) launch_status_label = Label.new() launch_status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER launch_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART launch_status_label.add_theme_font_override("font", Brand.font) launch_status_label.add_theme_font_size_override("font_size", 16) launch_status_label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode())) launch_status_label.set_meta("medmind_role", "muted") themed_controls.append(launch_status_label) content.add_child(launch_status_label) launch_error_label = Label.new() launch_error_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER launch_error_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART launch_error_label.add_theme_font_override("font", Brand.font) launch_error_label.add_theme_font_size_override("font_size", 15) launch_error_label.add_theme_color_override("font_color", Style.RED) content.add_child(launch_error_label) var actions := HBoxContainer.new() actions.alignment = BoxContainer.ALIGNMENT_CENTER actions.add_theme_constant_override("separation", 12) content.add_child(actions) launch_retry_button = Button.new() launch_retry_button.text = "Retry" launch_retry_button.custom_minimum_size = Vector2(120, 44) launch_retry_button.pressed.connect(_retry_question_bank) actions.add_child(launch_retry_button) var cancel := Button.new() cancel.text = "Back to Home" cancel.custom_minimum_size = Vector2(150, 44) cancel.pressed.connect(func(): main.show_screen("selection", "exam")) actions.add_child(cancel) func _add_mode_card( title_text: String, subtitle_text: String, availability_text: String, badge_text: String, description: String, icon_name: String, enabled: bool, accent: Color ) -> Dictionary: var dark := UIThemeService.is_dark_mode() var visual_accent := accent if dark or not enabled else Brand.ROYAL_BLUE var badge_accent := Brand.SUCCESS if enabled else accent var card := PanelContainer.new() card.custom_minimum_size = Vector2(430, 210) card.set_meta("medmind_wide_minimum", Vector2(430, 210)) card.size_flags_horizontal = Control.SIZE_EXPAND_FILL card.add_theme_stylebox_override("panel", _mode_card_box(enabled, accent)) card.set_meta("medmind_role", "mode_card") card.set_meta("medmind_enabled", enabled) card.set_meta("medmind_accent", accent) themed_controls.append(card) responsive_cards.append(card) mode_grid.add_child(card) var content := VBoxContainer.new() content.mouse_filter = Control.MOUSE_FILTER_IGNORE content.add_theme_constant_override("separation", 14) card.add_child(content) var header := BoxContainer.new() header.vertical = false header.mouse_filter = Control.MOUSE_FILTER_IGNORE header.add_theme_constant_override("separation", 20) content.add_child(header) responsive_card_rows.append(header) var icon_panel := PanelContainer.new() icon_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE icon_panel.custom_minimum_size = Vector2(72, 72) icon_panel.add_theme_stylebox_override("panel", _circle_box(visual_accent, 44)) icon_panel.set_meta("medmind_role", "icon_circle") icon_panel.set_meta("medmind_accent", accent) icon_panel.set_meta("medmind_enabled", enabled) themed_controls.append(icon_panel) header.add_child(icon_panel) var icon := TextureRect.new() icon.name = "%sIcon" % title_text.replace(" ", "") icon.mouse_filter = Control.MOUSE_FILTER_IGNORE icon.texture = preload("res://assets/branding/medmind-icon.png") if icon_name == "medmind" else load("res://assets/icons/lucide/%s.svg" % icon_name) icon.material = Brand.logo_cutout_material() if icon_name == "medmind" else null icon.modulate = visual_accent icon.set_meta("medmind_role", "accent_icon") icon.set_meta("medmind_accent", accent) icon.set_meta("medmind_enabled", enabled) themed_controls.append(icon) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon_panel.add_child(icon) var details := VBoxContainer.new() details.mouse_filter = Control.MOUSE_FILTER_IGNORE details.size_flags_horizontal = Control.SIZE_EXPAND_FILL details.alignment = BoxContainer.ALIGNMENT_CENTER details.add_theme_constant_override("separation", 5) header.add_child(details) var title := Label.new() title.text = title_text title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART title.add_theme_font_override("font", Brand.font) title.add_theme_font_size_override("font_size", 22) details.add_child(title) var subtitle := Label.new() subtitle.text = "%s | %s" % [subtitle_text, availability_text] subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART subtitle.add_theme_font_override("font", Brand.font) subtitle.add_theme_font_size_override("font_size", 16) subtitle.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode())) subtitle.set_meta("medmind_role", "muted") themed_controls.append(subtitle) details.add_child(subtitle) var badge := Label.new() badge.text = badge_text badge.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN badge.add_theme_font_override("font", Brand.font) badge.add_theme_font_size_override("font_size", 13) badge.add_theme_color_override("font_color", badge_accent) badge.add_theme_stylebox_override("normal", _badge_box(badge_accent)) badge.set_meta("medmind_role", "badge") badge.set_meta("medmind_accent", accent) badge.set_meta("medmind_enabled", enabled) themed_controls.append(badge) details.add_child(badge) content.add_child(HSeparator.new()) var bottom := BoxContainer.new() bottom.vertical = false bottom.mouse_filter = Control.MOUSE_FILTER_IGNORE bottom.add_theme_constant_override("separation", 16) content.add_child(bottom) responsive_card_rows.append(bottom) var description_label := Label.new() description_label.text = description description_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART description_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL description_label.add_theme_font_override("font", Brand.font) description_label.add_theme_font_size_override("font_size", 16) description_label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode())) description_label.set_meta("medmind_role", "muted") themed_controls.append(description_label) bottom.add_child(description_label) var arrow_panel := PanelContainer.new() arrow_panel.custom_minimum_size = Vector2(48, 48) arrow_panel.add_theme_stylebox_override("panel", _circle_box(visual_accent if enabled else Brand.SLATE, 24, enabled)) arrow_panel.set_meta("medmind_role", "arrow_circle") arrow_panel.set_meta("medmind_accent", accent if enabled else Brand.SLATE) arrow_panel.set_meta("medmind_enabled", enabled) themed_controls.append(arrow_panel) bottom.add_child(arrow_panel) var arrow := TextureRect.new() arrow.texture = preload("res://assets/icons/lucide/chevron-right.svg") arrow.modulate = Color.WHITE if UIThemeService.is_dark_mode() else Brand.ROYAL_BLUE arrow.set_meta("medmind_role", "arrow_icon") themed_controls.append(arrow) arrow.expand_mode = TextureRect.EXPAND_IGNORE_SIZE arrow.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED arrow_panel.add_child(arrow) var status := Label.new() status.visible = false status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART status.add_theme_font_override("font", Brand.font) status.add_theme_font_size_override("font_size", 14) status.add_theme_color_override("font_color", accent) content.add_child(status) var error := Label.new() error.visible = false error.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART error.add_theme_font_override("font", Brand.font) error.add_theme_font_size_override("font_size", 14) error.add_theme_color_override("font_color", Style.RED) content.add_child(error) var retry := Button.new() retry.text = "Retry" retry.visible = false retry.custom_minimum_size = Vector2(110, 38) retry.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN Style.style_chip(retry, Style.AMBER) content.add_child(retry) var action := Button.new() action.text = "" action.flat = true action.disabled = not enabled action.tooltip_text = title_text if enabled else "Coming Soon" action.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) card.add_child(action) card.move_child(action, 0) return {"action": action, "status": status, "error": error, "retry": retry} func _mode_card_box(enabled: bool, accent: Color) -> StyleBoxFlat: var dark := UIThemeService.is_dark_mode() var visual_accent := accent if dark or not enabled else Brand.ROYAL_BLUE var box := Brand.content_card_box(dark, enabled, visual_accent) box.content_margin_left = 24 box.content_margin_right = 24 box.content_margin_top = 20 box.content_margin_bottom = 18 return box func _circle_box(accent: Color, radius: int, filled: bool = false) -> StyleBoxFlat: var box := Brand.icon_circle_box(UIThemeService.is_dark_mode(), accent, radius, filled) box.content_margin_left = 22 if radius > 30 else 12 box.content_margin_right = box.content_margin_left box.content_margin_top = box.content_margin_left box.content_margin_bottom = box.content_margin_left return box func _badge_box(accent: Color) -> StyleBoxFlat: return Brand.badge_box(UIThemeService.is_dark_mode(), accent) func _style_back_bar(button: Button) -> void: var dark := UIThemeService.is_dark_mode() var normal := StyleBoxFlat.new() normal.bg_color = Color("0b1427aa") if dark else Color("ffffffdd") normal.border_color = Color("33415d") if dark else Color("d4def0") normal.set_border_width_all(1) normal.set_corner_radius_all(8) normal.content_margin_left = 16 var hover := normal.duplicate() as StyleBoxFlat hover.border_color = Brand.SKY_BLUE button.add_theme_stylebox_override("normal", normal) button.add_theme_stylebox_override("hover", hover) func _add_exam_option(option_name: String, title_text: String, description: String, icon_name: String, available: bool) -> void: var card := PanelContainer.new() card.custom_minimum_size = Vector2(470, 178) card.set_meta("medmind_wide_minimum", Vector2(470, 178)) card.size_flags_horizontal = Control.SIZE_EXPAND_FILL card.add_theme_stylebox_override("panel", _exam_card_box(available)) card.set_meta("medmind_role", "exam_card") card.set_meta("medmind_enabled", available) themed_controls.append(card) responsive_cards.append(card) if available: exam_grid.add_child(card) else: exam_top_row.add_child(card) var content := VBoxContainer.new() content.mouse_filter = Control.MOUSE_FILTER_IGNORE content.add_theme_constant_override("separation", 10) card.add_child(content) var header := BoxContainer.new() header.vertical = false header.mouse_filter = Control.MOUSE_FILTER_IGNORE header.add_theme_constant_override("separation", 20) content.add_child(header) responsive_card_rows.append(header) var icon_panel := PanelContainer.new() icon_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE icon_panel.custom_minimum_size = Vector2(78, 78) var icon_box := Brand.icon_circle_box(UIThemeService.is_dark_mode(), Brand.ROYAL_BLUE if available else Brand.PURPLE, 39) icon_box.content_margin_left = 20 icon_box.content_margin_right = 20 icon_box.content_margin_top = 20 icon_box.content_margin_bottom = 20 icon_panel.add_theme_stylebox_override("panel", icon_box) icon_panel.set_meta("medmind_role", "exam_icon_circle") icon_panel.set_meta("medmind_enabled", available) themed_controls.append(icon_panel) header.add_child(icon_panel) var icon := TextureRect.new() icon.mouse_filter = Control.MOUSE_FILTER_IGNORE icon.texture = load("res://assets/icons/lucide/%s.svg" % icon_name) icon.modulate = Brand.ROYAL_BLUE if available else Brand.PURPLE icon.set_meta("medmind_role", "exam_icon") icon.set_meta("medmind_enabled", available) themed_controls.append(icon) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon_panel.add_child(icon) var details := VBoxContainer.new() details.mouse_filter = Control.MOUSE_FILTER_IGNORE details.size_flags_horizontal = Control.SIZE_EXPAND_FILL details.alignment = BoxContainer.ALIGNMENT_CENTER details.add_theme_constant_override("separation", 8) header.add_child(details) var eyebrow := Label.new() eyebrow.mouse_filter = Control.MOUSE_FILTER_IGNORE eyebrow.text = option_name eyebrow.add_theme_font_override("font", Brand.font) eyebrow.add_theme_font_size_override("font_size", 16) eyebrow.add_theme_color_override("font_color", Brand.ROYAL_BLUE) details.add_child(eyebrow) var title := Label.new() title.mouse_filter = Control.MOUSE_FILTER_IGNORE title.text = title_text title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART title.add_theme_font_override("font", Brand.font) title.add_theme_font_size_override("font_size", 22) details.add_child(title) var status := Label.new() status.mouse_filter = Control.MOUSE_FILTER_IGNORE status.text = "Available" if available else "Coming Soon" status.add_theme_font_override("font", Brand.font) status.add_theme_font_size_override("font_size", 14) status.add_theme_color_override("font_color", Brand.SUCCESS if available else Brand.PURPLE) status.add_theme_stylebox_override("normal", Brand.badge_box(UIThemeService.is_dark_mode(), Brand.SUCCESS if available else Brand.PURPLE)) status.set_meta("medmind_role", "exam_badge") status.set_meta("medmind_enabled", available) themed_controls.append(status) details.add_child(status) var arrow := TextureRect.new() arrow.mouse_filter = Control.MOUSE_FILTER_IGNORE arrow.texture = preload("res://assets/icons/lucide/chevron-right.svg") arrow.modulate = Brand.TEAL if UIThemeService.is_dark_mode() and available else Brand.ROYAL_BLUE arrow.set_meta("medmind_role", "exam_arrow") arrow.set_meta("medmind_enabled", available) themed_controls.append(arrow) arrow.custom_minimum_size = Vector2(32, 32) arrow.expand_mode = TextureRect.EXPAND_IGNORE_SIZE arrow.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED header.add_child(arrow) card.tooltip_text = description var action := Button.new() action.text = "" action.flat = true action.disabled = not available action.tooltip_text = "Open Step 3 study modes" if available else "Coming Soon" action.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) if available: action.pressed.connect(func(): _show_stage("step3")) card.add_child(action) func _exam_card_box(available: bool) -> StyleBoxFlat: var box := Brand.content_card_box(UIThemeService.is_dark_mode(), available, Brand.ROYAL_BLUE) box.content_margin_left = 24 box.content_margin_right = 24 box.content_margin_top = 20 box.content_margin_bottom = 20 return box func refresh_theme_controls() -> void: var dark := UIThemeService.is_dark_mode() for control in themed_controls: if not is_instance_valid(control): continue var role := str(control.get_meta("medmind_role", "")) var accent: Color = control.get_meta("medmind_accent", Brand.ROYAL_BLUE) var enabled := bool(control.get_meta("medmind_enabled", false)) var visual_accent := accent if dark or not enabled else Brand.ROYAL_BLUE match role: "mode_card": control.add_theme_stylebox_override("panel", _mode_card_box(enabled, accent)) "exam_card": control.add_theme_stylebox_override("panel", _exam_card_box(enabled)) "question_bank_launch": control.add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.TEAL)) "question_bank_launch_icon": control.modulate = Brand.TEAL if dark else Brand.ROYAL_BLUE "icon_circle": control.add_theme_stylebox_override("panel", _circle_box(visual_accent, 36)) "arrow_circle": control.add_theme_stylebox_override("panel", _circle_box(visual_accent, 24, enabled)) "exam_icon_circle": control.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE if enabled else Brand.PURPLE, 39)) "badge": var badge_accent := Brand.SUCCESS if enabled else accent control.add_theme_stylebox_override("normal", _badge_box(badge_accent)) control.add_theme_color_override("font_color", badge_accent) "exam_badge": control.add_theme_stylebox_override("normal", Brand.badge_box(dark, Brand.SUCCESS if enabled else Brand.PURPLE)) "muted": control.add_theme_color_override("font_color", Brand.muted_color(dark)) "muted_rich": control.add_theme_color_override("default_color", Brand.muted_color(dark)) "accent_icon": control.modulate = visual_accent "exam_icon": control.modulate = Brand.ROYAL_BLUE if enabled else Brand.PURPLE "arrow_icon": control.modulate = Color.WHITE if dark else Brand.ROYAL_BLUE "exam_arrow": control.modulate = Brand.TEAL if dark and enabled else Brand.ROYAL_BLUE "back_bar": _style_back_bar(control as Button) for node in [exam_view, mode_view, question_bank_launch_view]: if node != null: for child in node.find_children("*", "Label", true, false): var label := child as Label if not label.has_meta("medmind_role"): label.add_theme_color_override("font_color", Brand.text_color(dark)) func _show_stage(stage_name: String) -> void: current_stage = stage_name stage_changed.emit(stage_name) pending_question_bank_launch = false if exam_view != null: exam_view.visible = stage_name == "exam" if mode_view != null: mode_view.visible = stage_name == "step3" if question_bank_launch_view != null: question_bank_launch_view.visible = stage_name == "question_bank" _refresh_grid_columns() _refresh_bank_state() func _select_question_bank() -> void: if pending_question_bank_launch: return pending_question_bank_launch = true var excluded_ids: Array = ProfileService.get_profile().get("correct_question_ids", []).duplicate() QuestionBankService.load_next_random_question(excluded_ids) _refresh_bank_state() func _select_ccs() -> void: CCSService.launch_origin = "ccs" pending_ccs_launch = true ccs_button.disabled = true ccs_status_label.text = "Selecting a timed case..." ccs_status_label.visible = true CCSService.create_attempt("", "timed") func _on_ccs_attempt_changed() -> void: if not pending_ccs_launch or CCSService.attempt.is_empty(): return pending_ccs_launch = false ccs_button.disabled = false ccs_status_label.text = "" ccs_status_label.visible = false main.show_screen("ccs_player") func _on_ccs_failed(message: String) -> void: if not pending_ccs_launch: return pending_ccs_launch = false ccs_button.disabled = false ccs_status_label.text = message ccs_status_label.visible = true func _retry_question_bank() -> void: pending_question_bank_launch = true QuestionBankService.retry_now() _refresh_bank_state() func _on_questions_changed() -> void: _refresh_bank_state() if pending_question_bank_launch and visible and current_stage in ["step3", "question_bank"] and QuestionBankService.load_state == "ready": pending_question_bank_launch = false call_deferred("_start_question_bank") func _on_questions_load_failed(_message: String) -> void: # Keep the launch intent alive while the service performs its automatic # backoff retry. A terminal failure still waits for an explicit retry. if QuestionBankService.load_state != "retrying": pending_question_bank_launch = false _refresh_bank_state() func _refresh_bank_state() -> void: if question_bank_button == null: return var state := QuestionBankService.load_state question_bank_button.disabled = pending_question_bank_launch retry_button.visible = state == "failed" or state == "retrying" retry_button.disabled = state == "retrying" launch_retry_button.visible = state == "failed" or state == "retrying" launch_retry_button.disabled = state == "retrying" bank_error_label.text = QuestionBankService.last_error if state == "failed" or state == "retrying" else "" bank_error_label.visible = not bank_error_label.text.is_empty() launch_error_label.text = bank_error_label.text launch_error_label.visible = not launch_error_label.text.is_empty() if pending_question_bank_launch or state == "loading": bank_status_label.text = "Preparing your question bank..." elif state == "retrying": bank_status_label.text = "Question bank unavailable. Retrying in %d seconds." % QuestionBankService.next_retry_seconds elif state == "ready": bank_status_label.text = "Question bank ready" elif state == "failed": bank_status_label.text = "The question bank could not be loaded." else: bank_status_label.text = "" bank_status_label.visible = not bank_status_label.text.is_empty() launch_status_label.text = bank_status_label.text launch_status_label.visible = not launch_status_label.text.is_empty() func _start_question_bank() -> void: if not visible or current_stage not in ["step3", "question_bank"] or not QuestionBankService.has_questions(): return if BlockSessionService.start_core_run(): UISoundService.play_sound("question") main.show_screen("player") else: bank_error_label.text = BlockSessionService.core_status_message bank_error_label.visible = true launch_error_label.text = BlockSessionService.core_status_message launch_error_label.visible = true func _refresh_grid_columns() -> void: var column_count := 1 if size.x < 820.0 else 2 var compact_cards := size.x < 520.0 if exam_top_row != null: exam_top_row.vertical = column_count == 1 if mode_grid != null: mode_grid.columns = column_count for card in responsive_cards: if is_instance_valid(card): card.custom_minimum_size = Vector2(0, 0) if compact_cards else card.get_meta("medmind_wide_minimum", Vector2(430, 178)) for row in responsive_card_rows: if is_instance_valid(row): row.vertical = compact_cards row.alignment = BoxContainer.ALIGNMENT_BEGIN if compact_cards else BoxContainer.ALIGNMENT_CENTER