Add post-answer review and per-question loading

This commit is contained in:
hz
2026-07-14 15:34:38 -05:00
parent abbe6cf9f8
commit 7529ed5863
9 changed files with 265 additions and 42 deletions
-1
View File
@@ -235,7 +235,6 @@ func _on_login_request_completed(result: int, response_code: int, _headers: Pack
String(response.get("role", "")),
String(response.get("exp", ""))
)
QuestionBankService.load_questions_from_api()
UISoundService.play_sound("correct")
password_input.text = ""
registration_password_input.text = ""
+134 -23
View File
@@ -41,6 +41,7 @@ var timer_color_state := ""
var pending_correct_advance := false
var question_text_scale := 1.0
var presented_choices: Array = []
var awaiting_next_question := false
var progress_label: Label
var timer_label: Label
@@ -48,6 +49,8 @@ var player_stats_label: Label
var stem_label: RichTextLabel
var lead_label: Label
var explanation_label: RichTextLabel
var references_container: VBoxContainer
var question_scroll: ScrollContainer
var question_elo_label: Label
var next_button: Button
var skip_button: Button
@@ -107,6 +110,10 @@ func _ready() -> void:
QuestionBankService.question_report_submitted.connect(_on_question_report_submitted)
if not QuestionBankService.question_report_failed.is_connected(_on_question_report_failed):
QuestionBankService.question_report_failed.connect(_on_question_report_failed)
if not QuestionBankService.questions_changed.is_connected(_on_random_question_changed):
QuestionBankService.questions_changed.connect(_on_random_question_changed)
if not QuestionBankService.questions_load_failed.is_connected(_on_random_question_failed):
QuestionBankService.questions_load_failed.connect(_on_random_question_failed)
var top := HBoxContainer.new()
top.add_theme_constant_override("separation", 12)
@@ -212,15 +219,15 @@ func _ready() -> void:
question_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
add_child(question_panel)
var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
question_panel.add_child(scroll)
question_scroll = ScrollContainer.new()
question_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
question_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
question_panel.add_child(question_scroll)
var content_margin := MarginContainer.new()
content_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
content_margin.add_theme_constant_override("margin_right", 16)
scroll.add_child(content_margin)
question_scroll.add_child(content_margin)
question_content = VBoxContainer.new()
question_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
@@ -329,6 +336,11 @@ func _ready() -> void:
_set_scaled_rich_text(explanation_label, 17)
question_content.add_child(explanation_label)
references_container = VBoxContainer.new()
references_container.visible = false
references_container.add_theme_constant_override("separation", 6)
question_content.add_child(references_container)
var actions := HBoxContainer.new()
actions.add_theme_constant_override("separation", 10)
skip_button = Button.new()
@@ -341,6 +353,7 @@ func _ready() -> void:
next_button = Button.new()
next_button.text = "Submit"
next_button.tooltip_text = ""
next_button.custom_minimum_size = Vector2(140, 48)
next_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
Style.style_primary_button(next_button)
@@ -389,6 +402,7 @@ func on_show(_payload: Variant = null) -> void:
func on_hide() -> void:
awaiting_next_question = false
_hide_report_modal(true)
_hide_labs_modal()
@@ -444,6 +458,7 @@ func _load_question() -> void:
flag_button.button_pressed = false
_refresh_toolbar_state()
explanation_label.visible = false
_clear_references()
question_elo_label.visible = false
question_elo_label.text = ""
next_button.text = "Submit"
@@ -489,13 +504,7 @@ func _load_question() -> void:
flagged = bool(saved_answer.get("flagged", false))
flag_button.button_pressed = flagged
_refresh_toolbar_state()
if BlockSessionService.mode == "Tutor":
_show_explanation()
next_button.text = "Next"
else:
_apply_saved_choice_state()
next_button.text = "Next"
next_button.disabled = false
_show_answer_review()
_update_question_elo_label(true)
_refresh_nav_palette()
@@ -628,6 +637,8 @@ func _toggle_eliminate_selected() -> void:
func _submit_or_next() -> void:
if awaiting_next_question:
return
if pending_correct_advance:
return
if current_answer.is_empty():
@@ -649,6 +660,7 @@ func _submit_or_next() -> void:
if bool(result.get("is_correct", false)):
current_answer = answer
_update_question_elo_label(true)
_show_answer_review()
_trigger_correct_feedback()
return
_lock_wrong_choice(attempted_choice)
@@ -666,14 +678,58 @@ func _submit_or_next() -> void:
_presented_choice_ids()
)
current_answer = BlockSessionService.answer_current(selected_choice, eliminated, flagged)
if BlockSessionService.mode == "Tutor":
_show_explanation()
next_button.text = "Next"
else:
next_button.text = "Next"
_refresh_nav_palette()
_show_answer_review()
else:
_advance_or_finish()
if BlockSessionService.session_kind == "core":
_request_next_core_question()
else:
_advance_or_finish()
func _request_next_core_question() -> void:
awaiting_next_question = true
next_button.text = "Loading..."
next_button.disabled = true
QuestionBankService.load_next_random_question(BlockSessionService.get_core_excluded_question_ids())
func _on_random_question_changed() -> void:
if not awaiting_next_question or QuestionBankService.load_state != "ready":
return
var loaded_questions := QuestionBankService.get_all_questions()
if loaded_questions.is_empty():
_on_random_question_failed("No additional questions are available.")
return
if not BlockSessionService.append_core_question(loaded_questions[0]):
_on_random_question_failed("The next question could not be prepared.")
return
awaiting_next_question = false
_load_question()
UISoundService.play_sound("question")
func _on_random_question_failed(message: String) -> void:
if not awaiting_next_question:
return
awaiting_next_question = false
next_button.text = "Retry"
next_button.disabled = false
next_button.tooltip_text = message
func _show_answer_review() -> void:
_show_explanation()
_show_references(BlockSessionService.get_current_question().get("references", []))
next_button.text = "Next"
next_button.disabled = false
if skip_button != null:
skip_button.disabled = true
skip_button.visible = false
if question_scroll != null:
var review_target: Control = explanation_label
if references_container.visible:
review_target = references_container
question_scroll.call_deferred("ensure_control_visible", review_target)
func _show_explanation() -> void:
@@ -698,6 +754,62 @@ func _show_explanation() -> void:
_refresh_nav_palette()
func _show_references(raw_references: Variant) -> void:
_clear_references()
if typeof(raw_references) != TYPE_ARRAY:
return
var valid_references: Array = []
for reference in raw_references:
if typeof(reference) != TYPE_DICTIONARY:
continue
var url := str(reference.get("url", "")).strip_edges()
if not _is_safe_reference_url(url):
continue
valid_references.append(reference)
if valid_references.is_empty():
return
var heading := Label.new()
heading.text = "References"
_set_scaled_label(heading, 18, Style.TEXT)
references_container.add_child(heading)
for reference in valid_references:
var title := str(reference.get("title", "Reference")).strip_edges()
var publisher := str(reference.get("publisher", "")).strip_edges()
var url := str(reference.get("url", "")).strip_edges()
var link := LinkButton.new()
link.text = title if publisher == "" else "%s - %s" % [title, publisher]
link.tooltip_text = url
link.size_flags_horizontal = Control.SIZE_EXPAND_FILL
link.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
link.add_theme_color_override("font_color", Style.CYAN)
link.add_theme_color_override("font_hover_color", Style.MAGENTA)
link.set_meta("question_base_font_size", 16)
link.add_theme_font_size_override("font_size", _scaled_font_size(16))
link.pressed.connect(_open_reference_url.bind(url))
references_container.add_child(link)
references_container.visible = true
func _clear_references() -> void:
if references_container == null:
return
for child in references_container.get_children():
references_container.remove_child(child)
child.queue_free()
references_container.visible = false
func _is_safe_reference_url(url: String) -> bool:
return url.begins_with("https://") and url.length() > 8
func _open_reference_url(url: String) -> void:
if not _is_safe_reference_url(url):
return
OS.shell_open(url)
func _advance_or_finish() -> void:
if BlockSessionService.move_next():
_load_question()
@@ -757,11 +869,8 @@ func _trigger_wrong_feedback() -> void:
func _trigger_correct_feedback() -> void:
UISoundService.play_sound("correct")
pending_correct_advance = true
correct_advance_delay = CORRECT_ADVANCE_DELAY_SECONDS
red_fog_time = 0.0
green_fog_time = FEEDBACK_FADE_SECONDS
next_button.disabled = true
if skip_button != null:
skip_button.disabled = true
for button in choice_buttons:
@@ -948,6 +1057,8 @@ func _refresh_scaled_fonts(node: Node) -> void:
node.add_theme_font_size_override("font_size", _scaled_font_size(base_size))
elif node is Button:
node.add_theme_font_size_override("font_size", _scaled_font_size(base_size))
elif node is LinkButton:
node.add_theme_font_size_override("font_size", _scaled_font_size(base_size))
elif node is RichTextLabel:
node.add_theme_font_size_override("normal_font_size", _scaled_font_size(base_size))
node.add_theme_font_size_override("bold_font_size", _scaled_font_size(base_size))
@@ -1543,7 +1654,7 @@ func _apply_saved_choice_state() -> void:
else:
choice_buttons[index].modulate = Color.WHITE
if pending_correct_advance:
if pending_correct_advance or bool(current_answer.get("is_correct", false)):
_apply_correct_answer_preview()
if followup_required:
+3 -9
View File
@@ -184,15 +184,9 @@ func _show_stage(stage_name: String) -> void:
func _select_question_bank() -> void:
if QuestionBankService.has_questions() and QuestionBankService.load_state == "ready":
_start_question_bank()
return
pending_question_bank_launch = true
if QuestionBankService.load_state == "idle":
QuestionBankService.load_questions_from_api()
elif QuestionBankService.load_state == "failed":
QuestionBankService.retry_now()
var excluded_ids: Array = ProfileService.get_profile().get("correct_question_ids", []).duplicate()
QuestionBankService.load_next_random_question(excluded_ids)
_refresh_bank_state()
@@ -227,7 +221,7 @@ func _refresh_bank_state() -> void:
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 = "%d questions ready" % QuestionBankService.get_all_questions().size()
bank_status_label.text = "Question bank ready"
elif state == "failed":
bank_status_label.text = "The question bank could not be loaded."
else: