Refine follow-up review UI
This commit is contained in:
+250
-35
@@ -7,10 +7,12 @@ const CORRECT_ADVANCE_DELAY_SECONDS := 0.6
|
||||
var main: Control
|
||||
var choice_buttons: Array[Button] = []
|
||||
var choice_percent_labels: Array[Label] = []
|
||||
var choice_top_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 followup_states := {}
|
||||
var active_followup_choice_id := ""
|
||||
var followup_required := false
|
||||
var followup_answered := false
|
||||
@@ -119,10 +121,15 @@ func _ready() -> void:
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
question_panel.add_child(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_content = VBoxContainer.new()
|
||||
question_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
question_content.add_theme_constant_override("separation", 12)
|
||||
scroll.add_child(question_content)
|
||||
content_margin.add_child(question_content)
|
||||
|
||||
stem_label = RichTextLabel.new()
|
||||
stem_label.fit_content = true
|
||||
@@ -157,21 +164,41 @@ func _ready() -> void:
|
||||
percent_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
percent_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
percent_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
percent_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
percent_label.offset_left = 0
|
||||
percent_label.anchor_left = 1.0
|
||||
percent_label.anchor_top = 0.0
|
||||
percent_label.anchor_right = 1.0
|
||||
percent_label.anchor_bottom = 0.0
|
||||
percent_label.offset_left = -92
|
||||
percent_label.offset_top = 8
|
||||
percent_label.offset_right = -18
|
||||
percent_label.offset_bottom = 40
|
||||
Style.set_label(percent_label, 18, Style.TEXT)
|
||||
button.add_child(percent_label)
|
||||
choice_percent_labels.append(percent_label)
|
||||
|
||||
var top_label := Label.new()
|
||||
top_label.visible = false
|
||||
top_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
top_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
top_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
||||
top_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
top_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
top_label.offset_left = 14
|
||||
top_label.offset_top = 10
|
||||
top_label.offset_right = -96
|
||||
top_label.offset_bottom = -252
|
||||
Style.set_label(top_label, 16, Style.RED)
|
||||
button.add_child(top_label)
|
||||
choice_top_labels.append(top_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_top = 48
|
||||
followup_panel.offset_right = -16
|
||||
followup_panel.offset_bottom = -10
|
||||
followup_panel.offset_bottom = -16
|
||||
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)
|
||||
@@ -405,6 +432,7 @@ func _submit_or_next() -> void:
|
||||
if selected_choice == "":
|
||||
return
|
||||
if BlockSessionService.session_kind == "core":
|
||||
_collapse_resolved_followups()
|
||||
var attempted_choice := selected_choice
|
||||
var attempted_question_id := str(BlockSessionService.get_current_question().get("id", ""))
|
||||
QuestionBankService.record_first_attempt(attempted_question_id, attempted_choice)
|
||||
@@ -679,8 +707,10 @@ func _apply_saved_choice_state() -> void:
|
||||
var choices: Array = question.get("choices", [])
|
||||
for index in range(choices.size()):
|
||||
var id := str(choices[index].get("id", ""))
|
||||
var has_followup_panel := index < choice_followup_panels.size() and choice_followup_panels[index].visible
|
||||
_set_choice_button_text(index, choices[index])
|
||||
choice_buttons[index].disabled = locked_wrong_choice_ids.has(id)
|
||||
choice_buttons[index].disabled = locked_wrong_choice_ids.has(id) and not has_followup_panel
|
||||
_style_choice_button_for_state(index, id)
|
||||
if locked_wrong_choice_ids.has(id):
|
||||
choice_buttons[index].modulate = Color(1.0, 0.25, 0.25)
|
||||
elif eliminated.has(id):
|
||||
@@ -703,13 +733,20 @@ 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)
|
||||
choice_buttons[index].custom_minimum_size = _choice_row_size(index, choice_id, has_visible_followup)
|
||||
_set_choice_percent_label(index, "")
|
||||
_set_choice_top_label(index, "")
|
||||
if locked_wrong_choice_ids.has(choice_id):
|
||||
var percentage := _get_choice_selection_percentage(choice_id)
|
||||
if percentage >= 0:
|
||||
_set_choice_percent_label(index, "%d%%" % percentage)
|
||||
choice_buttons[index].text = "%s. %s" % [choice_id, _strike_text(choice_text)]
|
||||
if has_visible_followup:
|
||||
var wrong_text := "%s. %s" % [choice_id, choice_text]
|
||||
choice_buttons[index].text = ""
|
||||
_set_choice_top_label(index, wrong_text)
|
||||
else:
|
||||
var wrong_text := "%s. %s" % [choice_id, _strike_text(choice_text)]
|
||||
choice_buttons[index].text = wrong_text
|
||||
elif _should_show_correct_choice_percentage(choice_id):
|
||||
var correct_percentage := _get_choice_selection_percentage(choice_id)
|
||||
if correct_percentage >= 0:
|
||||
@@ -721,6 +758,29 @@ func _set_choice_button_text(index: int, choice: Dictionary) -> void:
|
||||
choice_buttons[index].text = "%s. %s" % [choice_id, choice_text]
|
||||
|
||||
|
||||
func _style_choice_button_for_state(index: int, choice_id: String) -> void:
|
||||
if choice_id == selected_choice:
|
||||
choice_buttons[index].add_theme_stylebox_override("normal", Style.button_box(Color("18465a"), Style.CYAN))
|
||||
choice_buttons[index].add_theme_stylebox_override("hover", Style.button_box(Color("20647e"), Style.CYAN))
|
||||
choice_buttons[index].add_theme_stylebox_override("pressed", Style.button_box(Color("18465a"), Style.CYAN))
|
||||
else:
|
||||
Style.style_answer_button(choice_buttons[index])
|
||||
|
||||
|
||||
func _choice_row_size(_index: int, choice_id: String, has_visible_followup: bool) -> Vector2:
|
||||
if not has_visible_followup and choice_id != active_followup_choice_id:
|
||||
return Vector2(0, 64)
|
||||
if followup_states.has(choice_id):
|
||||
var state: Dictionary = followup_states[choice_id]
|
||||
if bool(state.get("collapsed", false)):
|
||||
return Vector2(0, 110)
|
||||
return Vector2(0, 300)
|
||||
|
||||
|
||||
func _mark_review_input_handled() -> void:
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _set_choice_percent_label(index: int, text: String) -> void:
|
||||
if index < 0 or index >= choice_percent_labels.size():
|
||||
return
|
||||
@@ -729,6 +789,14 @@ func _set_choice_percent_label(index: int, text: String) -> void:
|
||||
label.visible = text != ""
|
||||
|
||||
|
||||
func _set_choice_top_label(index: int, text: String) -> void:
|
||||
if index < 0 or index >= choice_top_labels.size():
|
||||
return
|
||||
var label := choice_top_labels[index]
|
||||
label.text = text
|
||||
label.visible = text != ""
|
||||
|
||||
|
||||
func _get_choice_selection_percentage(choice_id: String) -> int:
|
||||
var question := BlockSessionService.get_current_question()
|
||||
var percentages: Dictionary = question.get("choice_selection_percentages", {})
|
||||
@@ -779,30 +847,54 @@ func _build_followup_panel(index: int, choice_id: String) -> void:
|
||||
var panel := choice_followup_panels[index]
|
||||
var content := choice_followup_contents[index]
|
||||
var followup := _get_followup_for_choice(choice_id)
|
||||
followup_states[choice_id] = {
|
||||
"choice_id": choice_id,
|
||||
"choice_text": _choice_text_for_id(choice_id),
|
||||
"prompt": str(followup.get("prompt", "")),
|
||||
"choices": followup.get("choices", []),
|
||||
"correct_choice_id": str(followup.get("correct_choice_id", "")),
|
||||
"teaching_point": str(followup.get("teaching_point", "")),
|
||||
"selected_option_id": "",
|
||||
"selected_option_text": "",
|
||||
"was_correct": false,
|
||||
"resolved": false,
|
||||
"collapsed": false,
|
||||
"has_authored_choices": bool(followup.get("has_authored_choices", false))
|
||||
}
|
||||
|
||||
var prompt := Label.new()
|
||||
prompt.text = str(followup.get("prompt", "What clue in the stem should make this answer less likely?"))
|
||||
prompt.text = str(followup.get("prompt", "What finding in the stem makes this answer less appropriate?"))
|
||||
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)
|
||||
if bool(followup.get("has_authored_choices", false)):
|
||||
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.set_meta("option_id", str(option.get("id", "")))
|
||||
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)
|
||||
|
||||
if not bool(followup.get("has_authored_choices", false)):
|
||||
var action_row := HBoxContainer.new()
|
||||
action_row.add_theme_constant_override("separation", 8)
|
||||
content.add_child(action_row)
|
||||
|
||||
var continue_button := Button.new()
|
||||
continue_button.text = "Continue"
|
||||
continue_button.custom_minimum_size = Vector2(130, 32)
|
||||
Style.style_chip(continue_button, Style.CYAN)
|
||||
continue_button.pressed.connect(func(followup_data := followup): _resolve_followup(followup_data, null, false, false))
|
||||
action_row.add_child(continue_button)
|
||||
|
||||
panel.visible = true
|
||||
|
||||
@@ -810,27 +902,59 @@ func _build_followup_panel(index: int, choice_id: String) -> void:
|
||||
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)
|
||||
_resolve_followup(followup, selected_button, true, is_correct)
|
||||
|
||||
|
||||
func _resolve_followup(followup: Dictionary, selected_button: Button, style_selected: bool, is_correct: bool) -> void:
|
||||
if not followup_required or followup_answered:
|
||||
return
|
||||
followup_answered = true
|
||||
var resolved_choice_id := active_followup_choice_id
|
||||
var selected_option_text := selected_button.text if selected_button != null else "Continued"
|
||||
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
|
||||
elif child is HBoxContainer:
|
||||
for row_child in child.get_children():
|
||||
if row_child is Button:
|
||||
row_child.disabled = true
|
||||
|
||||
if selected_button != null and style_selected:
|
||||
_style_followup_result_button(selected_button, is_correct)
|
||||
|
||||
var teaching := Label.new()
|
||||
teaching.text = str(followup.get("teaching_point", "Review the stem clue before choosing again."))
|
||||
teaching.text = str(followup.get("teaching_point", "Review the relevant stem finding before choosing again."))
|
||||
teaching.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(teaching, 14, Style.AMBER)
|
||||
choice_followup_contents[index].add_child(teaching)
|
||||
|
||||
if followup_states.has(resolved_choice_id):
|
||||
var state: Dictionary = followup_states[resolved_choice_id]
|
||||
state["selected_option_id"] = str(selected_button.get_meta("option_id", "")) if selected_button != null and selected_button.has_meta("option_id") else ""
|
||||
state["selected_option_text"] = selected_option_text
|
||||
state["was_correct"] = is_correct
|
||||
state["resolved"] = true
|
||||
state["collapsed"] = false
|
||||
followup_states[resolved_choice_id] = state
|
||||
|
||||
_unlock_after_followup()
|
||||
|
||||
|
||||
func _style_followup_result_button(button: Button, is_correct: bool) -> void:
|
||||
var fill := Color("1f7a3a") if is_correct else Color("8f1d2c")
|
||||
var border := Style.LIME if is_correct else Style.RED
|
||||
button.add_theme_stylebox_override("normal", Style.button_box(fill, border))
|
||||
button.add_theme_stylebox_override("hover", Style.button_box(fill, border))
|
||||
button.add_theme_stylebox_override("disabled", Style.button_box(fill, border))
|
||||
button.add_theme_color_override("font_color", Style.TEXT)
|
||||
|
||||
|
||||
func _unlock_after_followup() -> void:
|
||||
active_followup_choice_id = ""
|
||||
followup_required = false
|
||||
next_button.disabled = false
|
||||
if skip_button != null:
|
||||
@@ -838,10 +962,89 @@ func _unlock_after_followup() -> void:
|
||||
_apply_saved_choice_state()
|
||||
|
||||
|
||||
func _collapse_resolved_followups() -> void:
|
||||
for choice_id in followup_states.keys():
|
||||
var state: Dictionary = followup_states[choice_id]
|
||||
if bool(state.get("resolved", false)):
|
||||
state["collapsed"] = true
|
||||
followup_states[choice_id] = state
|
||||
_render_followup_state(choice_id)
|
||||
_apply_saved_choice_state()
|
||||
|
||||
|
||||
func _toggle_followup_state(choice_id: String) -> void:
|
||||
if not followup_states.has(choice_id):
|
||||
return
|
||||
var state: Dictionary = followup_states[choice_id]
|
||||
state["collapsed"] = not bool(state.get("collapsed", false))
|
||||
followup_states[choice_id] = state
|
||||
_render_followup_state(choice_id)
|
||||
_apply_saved_choice_state()
|
||||
|
||||
|
||||
func _render_followup_state(choice_id: String) -> void:
|
||||
if not followup_states.has(choice_id):
|
||||
return
|
||||
var index := _choice_index_for_id(choice_id)
|
||||
if index < 0 or index >= choice_followup_panels.size():
|
||||
return
|
||||
var state: Dictionary = followup_states[choice_id]
|
||||
_clear_followup_content(index)
|
||||
if bool(state.get("collapsed", false)):
|
||||
_build_collapsed_followup(index, state)
|
||||
else:
|
||||
_build_readonly_followup(index, state)
|
||||
choice_followup_panels[index].visible = true
|
||||
|
||||
|
||||
func _build_collapsed_followup(index: int, state: Dictionary) -> void:
|
||||
var content := choice_followup_contents[index]
|
||||
var summary := Button.new()
|
||||
summary.text = "Review: %s. %s" % [str(state.get("choice_id", "")), str(state.get("choice_text", ""))]
|
||||
summary.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
summary.custom_minimum_size = Vector2(0, 40)
|
||||
Style.style_chip(summary, Style.AMBER)
|
||||
summary.gui_input.connect(func(_event: InputEvent): _mark_review_input_handled())
|
||||
summary.pressed.connect(func(choice_id := str(state.get("choice_id", ""))): _toggle_followup_state(choice_id))
|
||||
content.add_child(summary)
|
||||
|
||||
|
||||
func _build_readonly_followup(index: int, state: Dictionary) -> void:
|
||||
var content := choice_followup_contents[index]
|
||||
var summary := Button.new()
|
||||
summary.text = "Hide review: %s. %s" % [str(state.get("choice_id", "")), str(state.get("choice_text", ""))]
|
||||
summary.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
summary.custom_minimum_size = Vector2(0, 36)
|
||||
Style.style_chip(summary, Style.AMBER)
|
||||
summary.gui_input.connect(func(_event: InputEvent): _mark_review_input_handled())
|
||||
summary.pressed.connect(func(choice_id := str(state.get("choice_id", ""))): _toggle_followup_state(choice_id))
|
||||
content.add_child(summary)
|
||||
|
||||
var prompt := Label.new()
|
||||
prompt.text = str(state.get("prompt", ""))
|
||||
prompt.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(prompt, 14, Style.TEXT)
|
||||
content.add_child(prompt)
|
||||
|
||||
var selected := Label.new()
|
||||
var selected_text := str(state.get("selected_option_text", ""))
|
||||
selected.text = "Your follow-up answer: %s" % selected_text if selected_text != "" else "Follow-up reviewed."
|
||||
selected.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(selected, 14, Style.LIME if bool(state.get("was_correct", false)) else Style.RED)
|
||||
content.add_child(selected)
|
||||
|
||||
var teaching := Label.new()
|
||||
teaching.text = str(state.get("teaching_point", ""))
|
||||
teaching.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(teaching, 14, Style.AMBER)
|
||||
content.add_child(teaching)
|
||||
|
||||
|
||||
func _clear_followups() -> void:
|
||||
active_followup_choice_id = ""
|
||||
followup_required = false
|
||||
followup_answered = false
|
||||
followup_states.clear()
|
||||
for index in range(choice_followup_panels.size()):
|
||||
_clear_followup_content(index)
|
||||
choice_followup_panels[index].visible = false
|
||||
@@ -860,18 +1063,21 @@ func _get_followup_for_choice(choice_id: String) -> Dictionary:
|
||||
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]
|
||||
var choices: Array = authored.get("choices", [])
|
||||
return {
|
||||
"prompt": str(authored.get("prompt", "What clue in the stem should make this answer less likely?")),
|
||||
"choices": authored.get("choices", []),
|
||||
"prompt": str(authored.get("prompt", "What finding in the stem makes this answer less appropriate?")),
|
||||
"choices": choices,
|
||||
"has_authored_choices": not choices.is_empty(),
|
||||
"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.")))
|
||||
"teaching_point": str(authored.get("teachingPoint", authored.get("teaching_point", "Review the relevant stem finding 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."
|
||||
"prompt": "No follow-up has been authored for this answer yet. Re-read the stem before retrying.",
|
||||
"choices": [],
|
||||
"has_authored_choices": false,
|
||||
"correct_choice_id": "",
|
||||
"teaching_point": "Review the stem for the finding that makes this answer less appropriate before trying again."
|
||||
}
|
||||
|
||||
|
||||
@@ -884,6 +1090,15 @@ func _choice_index_for_id(choice_id: String) -> int:
|
||||
return -1
|
||||
|
||||
|
||||
func _choice_text_for_id(choice_id: String) -> String:
|
||||
var question := BlockSessionService.get_current_question()
|
||||
var choices: Array = question.get("choices", [])
|
||||
for choice in choices:
|
||||
if typeof(choice) == TYPE_DICTIONARY and str(choice.get("id", "")) == choice_id:
|
||||
return str(choice.get("text", ""))
|
||||
return ""
|
||||
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user