Keep answered follow-ups expanded

This commit is contained in:
hz
2026-07-30 05:11:54 -05:00
parent fa5df8795f
commit 98518e8d39
2 changed files with 118 additions and 11 deletions
+86
View File
@@ -3496,6 +3496,7 @@ func _build_followup_panel(_index: int, choice_id: String) -> void:
"selected_option_text": "",
"was_correct": false,
"resolved": false,
"collapsed": false,
"has_authored_choices": bool(followup.get("has_authored_choices", false)),
"is_main_correct": false
}
@@ -3567,6 +3568,7 @@ func _resolve_followup(_followup: Dictionary, selected_button: Button, _style_se
state["selected_option_text"] = selected_option_text
state["was_correct"] = is_correct
state["resolved"] = true
state["collapsed"] = false
followup_states[resolved_choice_id] = state
_render_followup_state(resolved_choice_id)
@@ -3593,6 +3595,8 @@ func _render_followup_state(choice_id: String) -> void:
_style_followup_card(index, state)
if not bool(state.get("resolved", false)):
_render_active_followup(index, state)
elif bool(state.get("collapsed", false)):
_build_resolved_followup_toggle(index, state, false)
else:
_build_resolved_followup(index, state)
choice_followup_panels[index].visible = true
@@ -3601,13 +3605,95 @@ func _render_followup_state(choice_id: String) -> void:
func _build_resolved_followup(index: int, state: Dictionary) -> void:
var content := choice_followup_contents[index]
_build_resolved_followup_toggle(index, state, true)
var prompt := Label.new()
prompt.name = "FollowupPrompt"
prompt.text = str(state.get("prompt", "What finding in the stem makes this answer less appropriate?"))
prompt.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_set_scaled_label(prompt, 14, Brand.text_color(_followup_is_dark()))
content.add_child(prompt)
var selected_id := str(state.get("selected_option_id", ""))
var correct_id := str(state.get("correct_choice_id", ""))
var options: Array = state.get("choices", [])
for option_index in range(options.size()):
var option = options[option_index]
if typeof(option) != TYPE_DICTIONARY:
continue
var option_id := str(option.get("id", ""))
var option_button := Button.new()
option_button.name = "FollowupOption_%s" % option_id
option_button.text = "%s. %s" % [_display_label_for_index(option_index), str(option.get("text", ""))]
option_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
option_button.disabled = true
option_button.custom_minimum_size = Vector2(0, 38)
_set_scaled_button_font(option_button, 14)
_style_review_followup_option(
option_button,
_followup_is_dark(),
option_id,
selected_id,
correct_id,
true
)
content.add_child(option_button)
var explanation := Label.new()
explanation.name = "FollowupExplanation"
explanation.text = _strip_verdict_prefix(_choice_review_explanation(str(state.get("choice_id", "")), state))
explanation.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_set_scaled_label(explanation, 14, Brand.text_color(_followup_is_dark()))
content.add_child(explanation)
func _build_resolved_followup_toggle(index: int, state: Dictionary, expanded: bool) -> void:
var content := choice_followup_contents[index]
var dark := _followup_is_dark()
var has_authored_choices := bool(state.get("has_authored_choices", not state.get("choices", []).is_empty()))
var was_correct := bool(state.get("was_correct", false))
var accent := Brand.SUCCESS if was_correct else Color("fb5f6d") if has_authored_choices else Brand.PURPLE
var row := HBoxContainer.new()
row.name = "FollowupResult"
row.add_theme_constant_override("separation", 8)
content.add_child(row)
var result := Label.new()
result.name = "FollowupVerdict"
result.text = ("Correct" if was_correct else "Incorrect") if has_authored_choices else "Review complete"
result.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_set_scaled_label(result, 14, accent)
row.add_child(result)
var toggle := Button.new()
toggle.name = "FollowupToggle"
toggle.flat = true
toggle.text = "Minimize" if expanded else "Expand"
toggle.tooltip_text = "Minimize follow-up review" if expanded else "Expand follow-up review"
toggle.icon = preload("res://assets/icons/lucide/chevron-down.svg") if expanded else preload("res://assets/icons/lucide/chevron-right.svg")
toggle.icon_alignment = HORIZONTAL_ALIGNMENT_RIGHT
toggle.custom_minimum_size = Vector2(110, 30)
toggle.add_theme_color_override("font_color", Brand.text_color(dark))
toggle.add_theme_color_override("font_hover_color", accent)
toggle.add_theme_color_override("icon_normal_color", Brand.text_color(dark))
toggle.add_theme_color_override("icon_hover_color", accent)
_set_scaled_button_font(toggle, 13)
toggle.pressed.connect(_toggle_followup_state.bind(str(state.get("choice_id", ""))))
row.add_child(toggle)
func _toggle_followup_state(choice_id: String) -> void:
if not followup_states.has(choice_id):
return
var state: Dictionary = followup_states[choice_id]
if not bool(state.get("resolved", false)):
return
state["collapsed"] = not bool(state.get("collapsed", false))
followup_states[choice_id] = state
_render_followup_state(choice_id)
_apply_saved_choice_state()
func _add_followup_body(content: VBoxContainer, is_correct: bool) -> VBoxContainer:
var body := PanelContainer.new()
body.add_theme_stylebox_override("panel", _followup_body_box(_followup_is_dark(), is_correct))
+32 -11
View File
@@ -13,7 +13,7 @@ func _initialize() -> void:
_test_eliminated_choice_uses_rich_text_strikeout()
_test_eliminated_choice_that_is_also_locked_wrong_still_strikes_through()
_test_explanation_highlighting_has_no_status_heading()
_test_resolved_followup_has_no_header_or_verdict_prefix()
_test_resolved_followup_stays_expanded_and_toggles()
_test_review_content_normalization()
_test_followup_attempt_order_is_stable()
_test_review_followup_is_optional_and_single_attempt()
@@ -178,7 +178,7 @@ func _test_explanation_highlighting_has_no_status_heading() -> void:
player.free()
func _test_resolved_followup_has_no_header_or_verdict_prefix() -> void:
func _test_resolved_followup_stays_expanded_and_toggles() -> void:
var player = load("res://scripts/ui/question_player.gd").new()
var panel := PanelContainer.new()
var content := VBoxContainer.new()
@@ -192,17 +192,38 @@ func _test_resolved_followup_has_no_header_or_verdict_prefix() -> void:
player.presented_choices = [{"id": "B", "text": "Wrong answer"}]
player.followup_states["B"] = {
"choice_id": "B", "choice_text": "Wrong answer", "prompt": "Why?",
"selected_option_text": "Finding", "was_correct": false,
"teaching_point": "Incorrect. Review the finding.", "resolved": true
"choices": [{"id": "A", "text": "Selected finding"}, {"id": "B", "text": "Correct finding"}],
"correct_choice_id": "B", "selected_option_id": "A",
"selected_option_text": "A. Selected finding", "was_correct": false,
"teaching_point": "Incorrect. Review the finding.", "resolved": true, "collapsed": false,
"has_authored_choices": true
}
player._render_followup_state("B")
_assert(content.find_child("FollowupToggle", true, false) == null, "a resolved follow-up should not render a collapse toggle")
var explanation_text := ""
for child in content.get_children():
if child is Label:
explanation_text += child.text
_assert(not explanation_text.contains("Why this is"), "a resolved follow-up should not repeat a 'Why this is...' heading")
_assert(not explanation_text.begins_with("Incorrect."), "a resolved follow-up explanation should not repeat the verdict prefix")
var toggle := content.find_child("FollowupToggle", true, false) as Button
_assert(toggle != null, "a resolved follow-up should render a minimize toggle")
_assert(toggle != null and toggle.text == "Minimize", "a newly resolved follow-up should remain expanded")
_assert(content.find_child("FollowupPrompt", true, false) != null, "the resolved follow-up should retain its prompt")
_assert(content.find_child("FollowupOption_A", true, false) != null, "the resolved follow-up should retain its selected option")
_assert(content.find_child("FollowupOption_B", true, false) != null, "the resolved follow-up should retain its correct option")
var verdict := content.find_child("FollowupVerdict", true, false) as Label
_assert(verdict != null and verdict.text == "Incorrect", "the resolved follow-up should show whether the answer was correct")
if toggle != null:
toggle.pressed.emit()
_assert(bool(player.followup_states["B"].get("collapsed", false)), "the minimize toggle should collapse the follow-up")
_assert(content.find_child("FollowupPrompt", true, false) == null, "a collapsed follow-up should hide its prompt")
toggle = content.find_child("FollowupToggle", true, false) as Button
_assert(toggle != null and toggle.text == "Expand", "a collapsed follow-up should render an expand toggle")
if toggle != null:
toggle.pressed.emit()
_assert(not bool(player.followup_states["B"].get("collapsed", true)), "the expand toggle should restore the follow-up")
_assert(content.find_child("FollowupPrompt", true, false) != null, "expanding should restore the follow-up prompt")
var correct_state: Dictionary = player.followup_states["B"]
correct_state["selected_option_id"] = "B"
correct_state["was_correct"] = true
player.followup_states["B"] = correct_state
player._render_followup_state("B")
verdict = content.find_child("FollowupVerdict", true, false) as Label
_assert(verdict != null and verdict.text == "Correct", "a correct follow-up response should show a correct verdict")
player.choice_buttons[0].free()
player.choice_percent_labels[0].free()
player.choice_top_labels[0].free()