Launch question bank directly from navigation
This commit is contained in:
+4
-3
@@ -335,7 +335,7 @@ func _populate_navigation(container: VBoxContainer, include_brand: bool) -> void
|
||||
container.add_child(HSeparator.new())
|
||||
|
||||
_add_nav_button(container, "home", "Home", "house", true)
|
||||
_add_nav_button(container, "practice", "Practice", "pencil", true)
|
||||
_add_nav_button(container, "question_bank", "Question Bank", "clipboard-list", true)
|
||||
_add_nav_button(container, "exams", "Exams", "clipboard-list", true)
|
||||
_add_nav_button(container, "ccs", "CCS Cases", "stethoscope", true)
|
||||
_add_nav_button(container, "performance", "Performance", "chart-no-axes-column-increasing", true)
|
||||
@@ -379,7 +379,7 @@ func _add_nav_button(container: VBoxContainer, key: String, label_text: String,
|
||||
func _on_navigation_pressed(key: String) -> void:
|
||||
match key:
|
||||
"home": show_screen("selection", "exam")
|
||||
"practice": show_screen("selection", "step3")
|
||||
"question_bank": show_screen("selection", "question_bank")
|
||||
"exams": show_screen("simulation_history")
|
||||
"ccs": show_screen("ccs_hub")
|
||||
"performance": show_screen("profile")
|
||||
@@ -780,11 +780,12 @@ func _exit_run() -> void:
|
||||
var ccs_player = screens.get("ccs_player")
|
||||
if ccs_player != null and ccs_player.has_method("leave_case"): ccs_player.leave_case()
|
||||
return
|
||||
var was_core_run := BlockSessionService.session_kind == "core"
|
||||
if BlockSessionService.block_started and BlockSessionService.session_kind != "simulation":
|
||||
BlockSessionService.finish_block()
|
||||
elif BlockSessionService.session_kind == "simulation":
|
||||
BlockSessionService.block_started = false
|
||||
show_screen("selection", "step3")
|
||||
show_screen("selection", "exam" if was_core_run else "step3")
|
||||
|
||||
|
||||
func _open_profile_from_menu() -> void:
|
||||
|
||||
@@ -4,9 +4,11 @@ extends RefCounted
|
||||
static func key_for_screen(screen_name: String, payload: Variant = null) -> String:
|
||||
var key := "home"
|
||||
if screen_name == "selection":
|
||||
key = "practice" if str(payload) == "step3" else "home"
|
||||
key = "question_bank" if str(payload) == "question_bank" else "home"
|
||||
elif screen_name in ["map", "setup", "review"]:
|
||||
key = "study_plan" if screen_name == "map" else "practice"
|
||||
key = "study_plan" if screen_name == "map" else "question_bank"
|
||||
elif screen_name == "player":
|
||||
key = "question_bank"
|
||||
elif screen_name.begins_with("simulation_"):
|
||||
key = "exams"
|
||||
elif screen_name.begins_with("ccs_"):
|
||||
|
||||
@@ -975,7 +975,7 @@ func _load_question() -> void:
|
||||
|
||||
var question := BlockSessionService.get_current_question()
|
||||
if question.is_empty():
|
||||
main.show_screen("selection", "step3")
|
||||
main.show_screen("selection", "exam" if BlockSessionService.session_kind == "core" else "step3")
|
||||
return
|
||||
progress_button.text = _question_progress_text()
|
||||
progress_button.disabled = BlockSessionService.session_kind == "core"
|
||||
@@ -1603,16 +1603,17 @@ func _advance_or_finish() -> void:
|
||||
func _finish() -> void:
|
||||
if BlockSessionService.session_kind == "core":
|
||||
BlockSessionService.finish_block()
|
||||
main.show_screen("selection", "step3")
|
||||
main.show_screen("selection", "exam")
|
||||
return
|
||||
var results := BlockSessionService.finish_block()
|
||||
main.show_screen("review", results)
|
||||
|
||||
|
||||
func _exit_run() -> void:
|
||||
var was_core_run := BlockSessionService.session_kind == "core"
|
||||
BlockSessionService.resume_current_question_timer()
|
||||
BlockSessionService.finish_block()
|
||||
main.show_screen("selection", "step3")
|
||||
main.show_screen("selection", "exam" if was_core_run else "step3")
|
||||
|
||||
|
||||
func _update_timer_bar() -> void:
|
||||
|
||||
@@ -8,6 +8,7 @@ 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
|
||||
@@ -17,6 +18,9 @@ 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
|
||||
@@ -51,12 +55,16 @@ func _ready() -> void:
|
||||
|
||||
_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()
|
||||
@@ -202,6 +210,84 @@ func _build_mode_view() -> void:
|
||||
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,
|
||||
@@ -527,6 +613,8 @@ func refresh_theme_controls() -> void:
|
||||
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))
|
||||
@@ -542,7 +630,7 @@ func refresh_theme_controls() -> void:
|
||||
"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]:
|
||||
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
|
||||
@@ -558,11 +646,15 @@ func _show_stage(stage_name: String) -> void:
|
||||
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)
|
||||
@@ -604,13 +696,16 @@ func _retry_question_bank() -> void:
|
||||
|
||||
func _on_questions_changed() -> void:
|
||||
_refresh_bank_state()
|
||||
if pending_question_bank_launch and visible and current_stage == "step3" and QuestionBankService.load_state == "ready":
|
||||
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:
|
||||
pending_question_bank_launch = false
|
||||
# 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()
|
||||
|
||||
|
||||
@@ -621,8 +716,12 @@ func _refresh_bank_state() -> void:
|
||||
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":
|
||||
@@ -634,16 +733,21 @@ func _refresh_bank_state() -> void:
|
||||
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 != "step3" or not QuestionBankService.has_questions():
|
||||
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:
|
||||
|
||||
@@ -7,8 +7,10 @@ var failures := 0
|
||||
|
||||
func _initialize() -> void:
|
||||
_assert(NavigationRoutes.key_for_screen("selection", "exam") == "home", "exam selection should activate Home")
|
||||
_assert(NavigationRoutes.key_for_screen("selection", "step3") == "practice", "Step 3 modes should activate Practice")
|
||||
_assert(NavigationRoutes.key_for_screen("setup") == "practice", "block setup should activate Practice")
|
||||
_assert(NavigationRoutes.key_for_screen("selection", "step3") == "home", "Step 3 mode selection should remain part of Home")
|
||||
_assert(NavigationRoutes.key_for_screen("selection", "question_bank") == "question_bank", "direct question bank launch should activate Question Bank")
|
||||
_assert(NavigationRoutes.key_for_screen("setup") == "question_bank", "block setup should activate Question Bank")
|
||||
_assert(NavigationRoutes.key_for_screen("player") == "question_bank", "question player should map to Question Bank")
|
||||
_assert(NavigationRoutes.key_for_screen("simulation_results") == "exams", "simulation results should activate Exams")
|
||||
_assert(NavigationRoutes.key_for_screen("ccs_review") == "ccs", "CCS review should activate CCS Cases")
|
||||
_assert(NavigationRoutes.key_for_screen("profile") == "performance", "profile should activate Performance")
|
||||
|
||||
@@ -32,6 +32,10 @@ func _run() -> void:
|
||||
_assert(dynamic_icon != null, "dynamic questions should expose its mode icon")
|
||||
if dynamic_icon != null:
|
||||
_assert(dynamic_icon.texture.resource_path == "res://assets/icons/lucide/activity.svg", "dynamic questions should use a web-safe vector icon")
|
||||
selector._show_stage("question_bank")
|
||||
await process_frame
|
||||
_assert(selector.question_bank_launch_view.visible and not selector.mode_view.visible, "direct question bank navigation should show only the focused launch state")
|
||||
_assert(selector.launch_retry_button != null and selector.launch_error_label != null, "question bank launch should retain retry and error controls")
|
||||
|
||||
var main_screen = load("res://scripts/ui/main.gd").new()
|
||||
main_screen.size = host.size
|
||||
@@ -43,6 +47,9 @@ func _run() -> void:
|
||||
_assert(main_screen.theme_button.icon_alignment == HORIZONTAL_ALIGNMENT_CENTER, "the theme toggle icon should remain centered")
|
||||
_assert(main_screen.header_panel.find_child("HeaderStats", true, false) == null, "the application header should not display rating or role labels")
|
||||
_assert(not main_screen.account_chevron_button.visible, "nonessential account controls should collapse on phone-width headers")
|
||||
var question_bank_nav := main_screen.sidebar.find_child("NavQuestionBank", true, false) as Button
|
||||
_assert(question_bank_nav != null and question_bank_nav.text == "Question Bank", "the Practice navigation item should be replaced by Question Bank")
|
||||
_assert(main_screen.sidebar.find_child("NavPractice", true, false) == null, "the sidebar should not retain a Practice navigation item")
|
||||
|
||||
host.queue_free()
|
||||
await process_frame
|
||||
|
||||
Reference in New Issue
Block a user