Add wrong answer follow-up prompts

This commit is contained in:
hz
2026-07-11 02:16:01 -05:00
parent c716b16c53
commit ad6b3863cc
8 changed files with 212 additions and 3 deletions
+15
View File
@@ -71,6 +71,10 @@ func _build_ui() -> void:
user_label = Label.new()
Style.set_label(user_label, 15, Style.CYAN)
user_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
user_label.mouse_filter = Control.MOUSE_FILTER_STOP
user_label.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
user_label.tooltip_text = "Open profile"
user_label.gui_input.connect(_on_user_label_gui_input)
header.add_child(user_label)
var screens_container := MarginContainer.new()
@@ -246,3 +250,14 @@ func _open_profile_from_menu() -> void:
if BlockSessionService.block_started:
BlockSessionService.finish_block()
show_screen("profile")
func _on_user_label_gui_input(event: InputEvent) -> void:
if not AuthState.is_logged_in:
return
var mouse_event := event as InputEventMouseButton
if mouse_event == null or mouse_event.button_index != MOUSE_BUTTON_LEFT or not mouse_event.pressed:
return
_close_app_menu()
if current_screen_name != "profile":
show_screen("profile")
+158 -3
View File
@@ -7,8 +7,13 @@ const CORRECT_ADVANCE_DELAY_SECONDS := 0.6
var main: Control
var choice_buttons: Array[Button] = []
var choice_percent_labels: Array[Label] = []
var choice_followup_panels: Array[PanelContainer] = []
var choice_followup_contents: Array[VBoxContainer] = []
var eliminated: Array = []
var locked_wrong_choice_ids: Array = []
var active_followup_choice_id := ""
var followup_required := false
var followup_answered := false
var flagged := false
var selected_choice := ""
var current_answer := {}
@@ -159,6 +164,23 @@ func _ready() -> void:
button.add_child(percent_label)
choice_percent_labels.append(percent_label)
var followup_panel := PanelContainer.new()
followup_panel.visible = false
followup_panel.mouse_filter = Control.MOUSE_FILTER_STOP
followup_panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
followup_panel.offset_left = 16
followup_panel.offset_top = 42
followup_panel.offset_right = -16
followup_panel.offset_bottom = -10
followup_panel.add_theme_stylebox_override("panel", Style.panel_box(Color("26151a"), Style.RED))
button.add_child(followup_panel)
choice_followup_panels.append(followup_panel)
var followup_content := VBoxContainer.new()
followup_content.add_theme_constant_override("separation", 6)
followup_panel.add_child(followup_content)
choice_followup_contents.append(followup_content)
question_content.add_child(button)
explanation_label = RichTextLabel.new()
@@ -242,6 +264,7 @@ func _load_question() -> void:
_reset_wrong_feedback()
eliminated.clear()
locked_wrong_choice_ids.clear()
_clear_followups()
flagged = false
selected_choice = ""
current_answer = {}
@@ -300,7 +323,7 @@ func _load_question() -> void:
func _select_choice(index: int) -> void:
if pending_correct_advance:
if pending_correct_advance or followup_required:
return
var question := BlockSessionService.get_current_question()
var choices: Array = question.get("choices", [])
@@ -335,7 +358,7 @@ func _skip_current_question() -> void:
func _choice_gui_input(event: InputEvent, index: int) -> void:
if pending_correct_advance:
if pending_correct_advance or followup_required:
return
var mouse_event := event as InputEventMouseButton
if mouse_event == null or mouse_event.button_index != MOUSE_BUTTON_RIGHT or not mouse_event.pressed:
@@ -361,7 +384,7 @@ func _choice_gui_input(event: InputEvent, index: int) -> void:
func _toggle_eliminate_selected() -> void:
if pending_correct_advance or selected_choice == "" or not current_answer.is_empty():
if pending_correct_advance or followup_required or selected_choice == "" or not current_answer.is_empty():
return
var question := BlockSessionService.get_current_question()
var choices: Array = question.get("choices", [])
@@ -395,9 +418,13 @@ func _submit_or_next() -> void:
_trigger_correct_feedback()
return
_lock_wrong_choice(attempted_choice)
_show_wrong_answer_followup(attempted_choice)
selected_choice = ""
current_answer = {}
next_button.text = "Submit"
next_button.disabled = true
if skip_button != null:
skip_button.disabled = true
_trigger_wrong_feedback()
_apply_saved_choice_state()
return
@@ -666,10 +693,17 @@ func _apply_saved_choice_state() -> void:
if pending_correct_advance:
_apply_correct_answer_preview()
if followup_required:
for index in range(choices.size()):
var id := str(choices[index].get("id", ""))
choice_buttons[index].disabled = id != active_followup_choice_id
func _set_choice_button_text(index: int, choice: Dictionary) -> void:
var choice_id := str(choice.get("id", ""))
var choice_text := str(choice.get("text", ""))
var has_visible_followup := index < choice_followup_panels.size() and choice_followup_panels[index].visible
choice_buttons[index].custom_minimum_size = Vector2(0, 190) if has_visible_followup or choice_id == active_followup_choice_id else Vector2(0, 64)
_set_choice_percent_label(index, "")
if locked_wrong_choice_ids.has(choice_id):
var percentage := _get_choice_selection_percentage(choice_id)
@@ -729,6 +763,127 @@ func _on_choice_percentages_updated(question_id: String, percentages: Dictionary
_apply_saved_choice_state()
func _show_wrong_answer_followup(choice_id: String) -> void:
active_followup_choice_id = choice_id
followup_required = true
followup_answered = false
var index := _choice_index_for_id(choice_id)
if index < 0:
_unlock_after_followup()
return
_build_followup_panel(index, choice_id)
func _build_followup_panel(index: int, choice_id: String) -> void:
_clear_followup_content(index)
var panel := choice_followup_panels[index]
var content := choice_followup_contents[index]
var followup := _get_followup_for_choice(choice_id)
var prompt := Label.new()
prompt.text = str(followup.get("prompt", "What clue in the stem should make this answer less likely?"))
prompt.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(prompt, 14, Style.TEXT)
content.add_child(prompt)
var choices: Array = followup.get("choices", [])
if choices.is_empty():
choices = [{"id": "A", "text": "Review the stem clue"}]
followup["correct_choice_id"] = "A"
followup["teaching_point"] = "Review the stem for the clue that makes this answer less appropriate before trying again."
for option in choices:
if typeof(option) != TYPE_DICTIONARY:
continue
var option_button := Button.new()
option_button.text = "%s. %s" % [str(option.get("id", "")), str(option.get("text", ""))]
option_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
option_button.custom_minimum_size = Vector2(0, 32)
Style.style_chip(option_button, Style.CYAN)
option_button.pressed.connect(func(option_id := str(option.get("id", "")), selected_button := option_button, followup_data := followup):
_answer_followup(option_id, selected_button, followup_data)
)
content.add_child(option_button)
panel.visible = true
func _answer_followup(option_id: String, selected_button: Button, followup: Dictionary) -> void:
if not followup_required or followup_answered:
return
followup_answered = true
var correct_id := str(followup.get("correct_choice_id", ""))
var is_correct := option_id == correct_id
Style.style_chip(selected_button, Style.LIME if is_correct else Style.RED)
var index := _choice_index_for_id(active_followup_choice_id)
if index >= 0:
for child in choice_followup_contents[index].get_children():
if child is Button:
child.disabled = true
var teaching := Label.new()
teaching.text = str(followup.get("teaching_point", "Review the stem clue before choosing again."))
teaching.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(teaching, 14, Style.AMBER)
choice_followup_contents[index].add_child(teaching)
_unlock_after_followup()
func _unlock_after_followup() -> void:
followup_required = false
next_button.disabled = false
if skip_button != null:
skip_button.disabled = false
_apply_saved_choice_state()
func _clear_followups() -> void:
active_followup_choice_id = ""
followup_required = false
followup_answered = false
for index in range(choice_followup_panels.size()):
_clear_followup_content(index)
choice_followup_panels[index].visible = false
func _clear_followup_content(index: int) -> void:
if index < 0 or index >= choice_followup_contents.size():
return
for child in choice_followup_contents[index].get_children():
choice_followup_contents[index].remove_child(child)
child.queue_free()
func _get_followup_for_choice(choice_id: String) -> Dictionary:
var question := BlockSessionService.get_current_question()
var followups: Dictionary = question.get("wrong_answer_followups", {})
if followups.has(choice_id) and typeof(followups[choice_id]) == TYPE_DICTIONARY:
var authored: Dictionary = followups[choice_id]
return {
"prompt": str(authored.get("prompt", "What clue in the stem should make this answer less likely?")),
"choices": authored.get("choices", []),
"correct_choice_id": str(authored.get("correctChoiceId", authored.get("correct_choice_id", ""))),
"teaching_point": str(authored.get("teachingPoint", authored.get("teaching_point", "Review the stem clue before choosing again.")))
}
return {
"prompt": "What clue in the stem should make this answer less likely?",
"choices": [{"id": "A", "text": "Review the stem clue"}],
"correct_choice_id": "A",
"teaching_point": "Review the stem for the clue that makes this answer less appropriate before trying again."
}
func _choice_index_for_id(choice_id: String) -> int:
var question := BlockSessionService.get_current_question()
var choices: Array = question.get("choices", [])
for index in range(choices.size()):
if str(choices[index].get("id", "")) == choice_id:
return index
return -1
func _on_question_rating_updated(question_id: String, question_elo: int, _rating: Dictionary) -> void:
if str(BlockSessionService.get_current_question().get("id", "")) != question_id:
return