Redesign study navigation and light theme
This commit is contained in:
+440
-187
@@ -1,25 +1,26 @@
|
||||
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 exam_grid: GridContainer
|
||||
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 simulation_button: Button
|
||||
var simulation_status_label: Label
|
||||
var simulation_preview_link: LinkButton
|
||||
var ccs_button: Button
|
||||
var ccs_status_label: Label
|
||||
var pending_simulation_launch := false
|
||||
var pending_ccs_launch := false
|
||||
var current_stage := "exam"
|
||||
var pending_question_bank_launch := false
|
||||
var themed_controls: Array[Control] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -30,14 +31,6 @@ func _ready() -> void:
|
||||
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 ExamSimulationService.readiness_changed.is_connected(_refresh_simulation_state):
|
||||
ExamSimulationService.readiness_changed.connect(_refresh_simulation_state)
|
||||
if not ExamSimulationService.state_changed.is_connected(_on_simulation_state_changed):
|
||||
ExamSimulationService.state_changed.connect(_on_simulation_state_changed)
|
||||
if not ExamSimulationService.request_failed.is_connected(_on_simulation_failed):
|
||||
ExamSimulationService.request_failed.connect(_on_simulation_failed)
|
||||
if not AuthState.auth_changed.is_connected(_refresh_simulation_preview_visibility):
|
||||
AuthState.auth_changed.connect(_refresh_simulation_preview_visibility)
|
||||
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):
|
||||
@@ -54,8 +47,6 @@ func on_show(payload: Variant = null) -> void:
|
||||
elif str(payload) == "exam":
|
||||
_show_stage("exam")
|
||||
_refresh_bank_state()
|
||||
_refresh_simulation_preview_visibility()
|
||||
ExamSimulationService.load_readiness()
|
||||
|
||||
|
||||
func on_hide() -> void:
|
||||
@@ -70,63 +61,82 @@ func _build_exam_view() -> void:
|
||||
add_child(exam_view)
|
||||
|
||||
var eyebrow := Label.new()
|
||||
eyebrow.text = "CHOOSE YOUR EXAM"
|
||||
Style.set_label(eyebrow, 14, Style.CYAN)
|
||||
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?"
|
||||
Style.set_label(title, 34, Style.TEXT)
|
||||
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
|
||||
Style.set_label(subtitle, 17, Style.MUTED)
|
||||
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 = GridContainer.new()
|
||||
exam_grid.columns = 2
|
||||
exam_grid.add_theme_constant_override("h_separation", 16)
|
||||
exam_grid.add_theme_constant_override("v_separation", 16)
|
||||
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", false)
|
||||
_add_exam_option("STEP 2", "Clinical knowledge", false)
|
||||
_add_exam_option("STEP 3", "Advanced clinical management", true)
|
||||
_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", 18)
|
||||
mode_view.add_theme_constant_override("separation", 16)
|
||||
mode_view.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
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(150, 42)
|
||||
Style.style_chip(back_button, Style.CYAN)
|
||||
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"
|
||||
Style.set_label(eyebrow, 14, Style.LIME)
|
||||
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"
|
||||
Style.set_label(title, 34, Style.TEXT)
|
||||
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 questions. Additional modes are in development."
|
||||
subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(subtitle, 17, Style.MUTED)
|
||||
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()
|
||||
@@ -136,106 +146,410 @@ func _build_mode_view() -> void:
|
||||
mode_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
mode_view.add_child(mode_grid)
|
||||
|
||||
question_bank_button = Button.new()
|
||||
question_bank_button.text = "Question Bank\nAdaptive core run | Available"
|
||||
question_bank_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
question_bank_button.custom_minimum_size = Vector2(360, 150)
|
||||
question_bank_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
question_bank_button.add_theme_font_size_override("font_size", 20)
|
||||
Style.style_primary_button(question_bank_button)
|
||||
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)
|
||||
mode_grid.add_child(question_bank_button)
|
||||
|
||||
var dynamic_button := Button.new()
|
||||
dynamic_button.text = "Dynamic Questions\nAI-generated practice | Coming Soon"
|
||||
dynamic_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
dynamic_button.custom_minimum_size = Vector2(360, 150)
|
||||
dynamic_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
dynamic_button.add_theme_font_size_override("font_size", 20)
|
||||
dynamic_button.disabled = true
|
||||
mode_grid.add_child(dynamic_button)
|
||||
|
||||
simulation_button = Button.new()
|
||||
simulation_button.text = "Step 3 Day 1 Simulation\n232 questions | Checking readiness"
|
||||
simulation_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
simulation_button.custom_minimum_size = Vector2(360, 150)
|
||||
simulation_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
simulation_button.add_theme_font_size_override("font_size", 20)
|
||||
Style.style_primary_button(simulation_button)
|
||||
simulation_button.disabled = true
|
||||
simulation_button.pressed.connect(_select_simulation)
|
||||
mode_grid.add_child(simulation_button)
|
||||
|
||||
ccs_button = Button.new()
|
||||
ccs_button.text = "CCS Cases\nInteractive case simulations | Available"
|
||||
ccs_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
ccs_button.custom_minimum_size = Vector2(360, 150)
|
||||
ccs_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
ccs_button.add_theme_font_size_override("font_size", 20)
|
||||
Style.style_primary_button(ccs_button)
|
||||
ccs_button.pressed.connect(_select_ccs)
|
||||
mode_grid.add_child(ccs_button)
|
||||
|
||||
ccs_status_label = Label.new()
|
||||
ccs_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(ccs_status_label, 15, Style.MUTED)
|
||||
mode_view.add_child(ccs_status_label)
|
||||
|
||||
simulation_status_label = Label.new()
|
||||
simulation_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(simulation_status_label, 15, Style.MUTED)
|
||||
mode_view.add_child(simulation_status_label)
|
||||
|
||||
simulation_preview_link = LinkButton.new()
|
||||
simulation_preview_link.text = "Temporary: preview completed exam results"
|
||||
simulation_preview_link.tooltip_text = "SUPER only; generates a nonpersistent simulated result"
|
||||
simulation_preview_link.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
simulation_preview_link.add_theme_color_override("font_color", Style.CYAN)
|
||||
simulation_preview_link.add_theme_color_override("font_hover_color", Style.MAGENTA)
|
||||
simulation_preview_link.pressed.connect(_preview_simulation_results)
|
||||
mode_view.add_child(simulation_preview_link)
|
||||
_refresh_simulation_preview_visibility()
|
||||
|
||||
bank_status_label = Label.new()
|
||||
bank_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(bank_status_label, 16, Style.CYAN)
|
||||
mode_view.add_child(bank_status_label)
|
||||
|
||||
bank_error_label = Label.new()
|
||||
bank_error_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(bank_error_label, 15, Style.RED)
|
||||
mode_view.add_child(bank_error_label)
|
||||
|
||||
retry_button = Button.new()
|
||||
retry_button.text = "Retry Question Bank"
|
||||
retry_button.custom_minimum_size = Vector2(220, 46)
|
||||
retry_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
Style.style_chip(retry_button, Style.AMBER)
|
||||
bank_status_label = question_card.status
|
||||
bank_error_label = question_card.error
|
||||
retry_button = question_card.retry
|
||||
retry_button.pressed.connect(_retry_question_bank)
|
||||
mode_view.add_child(retry_button)
|
||||
|
||||
_add_mode_card(
|
||||
"Dynamic Questions",
|
||||
"AI-generated practice",
|
||||
"Coming Soon",
|
||||
"Coming Soon",
|
||||
"Endless practice with AI-generated questions\ntailored to your performance.",
|
||||
"medmind",
|
||||
false,
|
||||
Brand.PURPLE)
|
||||
|
||||
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)
|
||||
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
mode_view.add_child(spacer)
|
||||
|
||||
var simulation_note := HBoxContainer.new()
|
||||
simulation_note.add_theme_constant_override("separation", 8)
|
||||
mode_view.add_child(simulation_note)
|
||||
var info := Label.new()
|
||||
info.text = "i"
|
||||
info.add_theme_font_size_override("font_size", 17)
|
||||
info.add_theme_color_override("font_color", Brand.SLATE)
|
||||
simulation_note.add_child(info)
|
||||
var note := RichTextLabel.new()
|
||||
note.bbcode_enabled = true
|
||||
note.fit_content = true
|
||||
note.scroll_active = false
|
||||
note.text = "Full-length, 12-block simulation is available in the [url=exams][color=#7c3aed]Exams[/color][/url] section."
|
||||
note.add_theme_font_override("normal_font", Brand.font)
|
||||
note.add_theme_font_size_override("normal_font_size", 15)
|
||||
note.add_theme_color_override("default_color", Brand.SLATE)
|
||||
note.set_meta("medmind_role", "muted_rich")
|
||||
themed_controls.append(note)
|
||||
note.meta_clicked.connect(func(_meta: Variant): main.show_screen("simulation_history"))
|
||||
simulation_note.add_child(note)
|
||||
|
||||
func _add_exam_option(option_name: String, description: String, available: bool) -> void:
|
||||
var button := Button.new()
|
||||
button.text = "%s\n%s | %s" % [option_name, description, "Available" if available else "Coming Soon"]
|
||||
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
button.custom_minimum_size = Vector2(360, 150)
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
button.add_theme_font_size_override("font_size", 20)
|
||||
button.disabled = not available
|
||||
|
||||
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.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)
|
||||
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 := HBoxContainer.new()
|
||||
header.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
header.add_theme_constant_override("separation", 20)
|
||||
content.add_child(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.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.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 = "● %s" % 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 := HBoxContainer.new()
|
||||
bottom.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
bottom.add_theme_constant_override("separation", 16)
|
||||
content.add_child(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.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)
|
||||
if available:
|
||||
Style.style_primary_button(button)
|
||||
button.pressed.connect(func(): _show_stage("step3"))
|
||||
exam_grid.add_child(button)
|
||||
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 := HBoxContainer.new()
|
||||
header.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
header.add_theme_constant_override("separation", 20)
|
||||
content.add_child(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))
|
||||
"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]:
|
||||
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"
|
||||
@@ -256,6 +570,7 @@ func _select_ccs() -> void:
|
||||
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")
|
||||
|
||||
|
||||
@@ -265,6 +580,7 @@ func _on_ccs_attempt_changed() -> void:
|
||||
pending_ccs_launch = false
|
||||
ccs_button.disabled = false
|
||||
ccs_status_label.text = ""
|
||||
ccs_status_label.visible = false
|
||||
main.show_screen("ccs_player")
|
||||
|
||||
|
||||
@@ -274,6 +590,7 @@ func _on_ccs_failed(message: String) -> void:
|
||||
pending_ccs_launch = false
|
||||
ccs_button.disabled = false
|
||||
ccs_status_label.text = message
|
||||
ccs_status_label.visible = true
|
||||
|
||||
|
||||
func _retry_question_bank() -> void:
|
||||
@@ -294,72 +611,6 @@ func _on_questions_load_failed(_message: String) -> void:
|
||||
_refresh_bank_state()
|
||||
|
||||
|
||||
func _select_simulation() -> void:
|
||||
pending_simulation_launch = true
|
||||
simulation_button.disabled = true
|
||||
if bool(ExamSimulationService.readiness.get("hasActiveSimulation", false)):
|
||||
ExamSimulationService.load_active()
|
||||
else:
|
||||
ExamSimulationService.create_simulation()
|
||||
|
||||
|
||||
func _preview_simulation_results() -> void:
|
||||
pending_simulation_launch = true
|
||||
simulation_preview_link.disabled = true
|
||||
simulation_status_label.text = "Generating simulated exam results..."
|
||||
ExamSimulationService.load_results_preview()
|
||||
|
||||
|
||||
func _refresh_simulation_preview_visibility() -> void:
|
||||
if simulation_preview_link != null:
|
||||
simulation_preview_link.visible = false
|
||||
|
||||
|
||||
func _on_simulation_state_changed() -> void:
|
||||
_refresh_simulation_state()
|
||||
if not pending_simulation_launch or ExamSimulationService.simulation.is_empty():
|
||||
return
|
||||
pending_simulation_launch = false
|
||||
var state := str(ExamSimulationService.simulation.get("state", ""))
|
||||
if state == "block_active":
|
||||
_launch_simulation_block()
|
||||
elif state == "completed":
|
||||
main.show_screen("simulation_results", ExamSimulationService.simulation)
|
||||
else:
|
||||
main.show_screen("simulation_break")
|
||||
|
||||
|
||||
func _launch_simulation_block() -> void:
|
||||
BlockSessionService.start_simulation_block(
|
||||
ExamSimulationService.get_current_questions(),
|
||||
int(ExamSimulationService.simulation.get("currentBlock", 0)),
|
||||
int(ExamSimulationService.simulation.get("totalBlocks", 12)),
|
||||
ExamSimulationService.get_block_deadline_unix())
|
||||
BlockSessionService.restore_simulation_responses(ExamSimulationService.simulation.get("responses", []))
|
||||
main.show_screen("player")
|
||||
|
||||
|
||||
func _on_simulation_failed(message: String) -> void:
|
||||
pending_simulation_launch = false
|
||||
_refresh_simulation_state()
|
||||
if simulation_preview_link != null:
|
||||
simulation_preview_link.disabled = false
|
||||
if simulation_status_label != null:
|
||||
simulation_status_label.text = message
|
||||
|
||||
|
||||
func _refresh_simulation_state() -> void:
|
||||
if simulation_button == null:
|
||||
return
|
||||
var simulation_ready := bool(ExamSimulationService.readiness.get("ready", false))
|
||||
var active := bool(ExamSimulationService.readiness.get("hasActiveSimulation", false)) or not ExamSimulationService.simulation.is_empty() and str(ExamSimulationService.simulation.get("state", "")) in ["between_blocks", "block_active"]
|
||||
var available := int(ExamSimulationService.readiness.get("availableQuestions", 0))
|
||||
simulation_button.disabled = pending_simulation_launch or (not simulation_ready and not active)
|
||||
simulation_button.text = "Resume Step 3 Day 1 Simulation\nProgress is saved between blocks" if active else "Step 3 Day 1 Simulation\n232 questions | %s" % ("Available" if simulation_ready else "%d/232 ready" % available)
|
||||
if simulation_status_label != null and not pending_simulation_launch:
|
||||
simulation_status_label.text = "Full-length, 12-block simulation. Predicted scores are provisional and exclude CCS." if simulation_ready or active else "The simulation unlocks when at least 232 published questions are available."
|
||||
|
||||
|
||||
func _refresh_bank_state() -> void:
|
||||
if question_bank_button == null:
|
||||
return
|
||||
@@ -368,6 +619,7 @@ func _refresh_bank_state() -> void:
|
||||
retry_button.visible = state == "failed" or state == "retrying"
|
||||
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()
|
||||
if pending_question_bank_launch or state == "loading":
|
||||
bank_status_label.text = "Preparing your question bank..."
|
||||
elif state == "retrying":
|
||||
@@ -378,6 +630,7 @@ func _refresh_bank_state() -> void:
|
||||
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()
|
||||
|
||||
|
||||
func _start_question_bank() -> void:
|
||||
@@ -392,7 +645,7 @@ func _start_question_bank() -> void:
|
||||
|
||||
func _refresh_grid_columns() -> void:
|
||||
var column_count := 1 if size.x < 820.0 else 2
|
||||
if exam_grid != null:
|
||||
exam_grid.columns = column_count
|
||||
if exam_top_row != null:
|
||||
exam_top_row.vertical = column_count == 1
|
||||
if mode_grid != null:
|
||||
mode_grid.columns = column_count
|
||||
|
||||
Reference in New Issue
Block a user