Add question reporting and reference reviews

This commit is contained in:
hz
2026-07-13 01:30:01 -05:00
parent 6eae6c9690
commit 700da4e415
32 changed files with 2684 additions and 6 deletions
+318 -6
View File
@@ -6,6 +6,16 @@ const CORRECT_ADVANCE_DELAY_SECONDS := 1.25
const TEXT_SCALE_MIN := 0.85
const TEXT_SCALE_MAX := 1.35
const TEXT_SCALE_STEP := 0.1
const REPORT_DETAILS_MIN := 10
const REPORT_DETAILS_MAX := 2000
const REPORT_CATEGORIES := [
{"id": "clarity", "label": "Clarity"},
{"id": "medical_accuracy", "label": "Medical accuracy"},
{"id": "answer_explanation", "label": "Answer / explanation"},
{"id": "formatting_media", "label": "Formatting / media"},
{"id": "technical_issue", "label": "Technical issue"},
{"id": "other", "label": "Other"}
]
var main: Control
var choice_buttons: Array[Button] = []
@@ -54,6 +64,20 @@ var feedback_fog: ColorRect
var labs_overlay: Control
var labs_panel: PanelContainer
var labs_content: VBoxContainer
var report_overlay: Control
var report_panel: PanelContainer
var report_form_content: VBoxContainer
var report_success_content: VBoxContainer
var report_category_group: ButtonGroup
var report_category_buttons: Array[CheckBox] = []
var report_details: TextEdit
var report_char_count: Label
var report_error_label: Label
var report_submit_button: Button
var report_cancel_button: Button
var report_success_label: Label
var report_is_submitting := false
var report_question_id := ""
var media_requests: Array[HTTPRequest] = []
var red_fog_time := 0.0
var green_fog_time := 0.0
@@ -76,6 +100,10 @@ func _ready() -> void:
QuestionBankService.ratings_changed.connect(_refresh_player_stats)
if not AuthState.auth_changed.is_connected(_refresh_player_stats):
AuthState.auth_changed.connect(_refresh_player_stats)
if not QuestionBankService.question_report_submitted.is_connected(_on_question_report_submitted):
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)
var top := HBoxContainer.new()
top.add_theme_constant_override("separation", 12)
@@ -156,6 +184,8 @@ func _ready() -> void:
report_button.tooltip_text = "Report question"
report_button.icon = _make_report_icon()
report_button.custom_minimum_size = Vector2(44, 44)
Style.style_chip(report_button, Style.CYAN)
UISoundService.set_button_sound(report_button, "open")
report_button.pressed.connect(_toggle_report_current_question)
question_toolbar.add_child(report_button)
@@ -326,17 +356,23 @@ func _ready() -> void:
add_child(feedback_fog)
_build_labs_modal()
_build_report_modal()
func on_show(_payload: Variant = null) -> void:
_load_question()
func on_hide() -> void:
_hide_report_modal(true)
_hide_labs_modal()
func _process(delta: float) -> void:
_update_answer_feedback(delta)
if visible and BlockSessionService.block_started and BlockSessionService.session_kind == "core":
_update_timer_bar()
if visible and BlockSessionService.block_started and BlockSessionService.mode == "Timed":
if visible and BlockSessionService.block_started and BlockSessionService.mode == "Timed" and (report_overlay == null or not report_overlay.visible):
BlockSessionService.seconds_remaining = max(0.0, BlockSessionService.seconds_remaining - delta)
var shown_seconds := int(ceil(BlockSessionService.seconds_remaining))
var minutes := int(floor(float(shown_seconds) / 60.0))
@@ -346,6 +382,10 @@ func _process(delta: float) -> void:
func _input(event: InputEvent) -> void:
if report_overlay != null and report_overlay.visible and event.is_action_pressed("ui_cancel"):
_hide_report_modal()
get_viewport().set_input_as_handled()
return
if labs_overlay != null and labs_overlay.visible and event.is_action_pressed("ui_cancel"):
_hide_labs_modal()
get_viewport().set_input_as_handled()
@@ -367,6 +407,7 @@ func _input(event: InputEvent) -> void:
func _load_question() -> void:
_hide_report_modal(true)
_reset_wrong_feedback()
_hide_labs_modal()
eliminated.clear()
@@ -785,11 +826,7 @@ func _toggle_report_current_question() -> void:
var question_id := str(BlockSessionService.get_current_question().get("id", ""))
if question_id == "":
return
if reported_question_ids.has(question_id):
reported_question_ids.erase(question_id)
else:
reported_question_ids.append(question_id)
_refresh_toolbar_state()
_show_report_modal(question_id)
func _refresh_toolbar_state() -> void:
@@ -799,6 +836,7 @@ func _refresh_toolbar_state() -> void:
if report_button != null:
var question_id := str(BlockSessionService.get_current_question().get("id", ""))
Style.style_chip(report_button, Style.RED if reported_question_ids.has(question_id) else Style.CYAN)
report_button.tooltip_text = "Report submitted; report another issue" if reported_question_ids.has(question_id) else "Report question"
func _refresh_player_stats() -> void:
@@ -976,6 +1014,280 @@ func _on_media_loaded(
status.visible = false
func _build_report_modal() -> void:
report_overlay = Control.new()
report_overlay.visible = false
report_overlay.top_level = true
report_overlay.mouse_filter = Control.MOUSE_FILTER_STOP
report_overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
report_overlay.gui_input.connect(_report_overlay_gui_input)
add_child(report_overlay)
var dim := ColorRect.new()
dim.color = Color(0.02, 0.03, 0.05, 0.78)
dim.mouse_filter = Control.MOUSE_FILTER_IGNORE
dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
report_overlay.add_child(dim)
var center := CenterContainer.new()
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
report_overlay.add_child(center)
report_panel = PanelContainer.new()
report_panel.custom_minimum_size = Vector2(700, 600)
report_panel.mouse_filter = Control.MOUSE_FILTER_STOP
report_panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Style.RED))
center.add_child(report_panel)
var modal_body := VBoxContainer.new()
modal_body.add_theme_constant_override("separation", 14)
report_panel.add_child(modal_body)
var header := HBoxContainer.new()
header.add_theme_constant_override("separation", 10)
modal_body.add_child(header)
var title := Label.new()
title.text = "Report question"
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_set_scaled_label(title, 26, Style.TEXT)
header.add_child(title)
var close_button := Button.new()
close_button.text = "X"
close_button.tooltip_text = "Close report form"
close_button.custom_minimum_size = Vector2(42, 38)
Style.style_chip(close_button, Style.RED)
UISoundService.set_button_sound(close_button, "close")
close_button.pressed.connect(_hide_report_modal)
header.add_child(close_button)
report_form_content = VBoxContainer.new()
report_form_content.add_theme_constant_override("separation", 12)
modal_body.add_child(report_form_content)
var category_label := Label.new()
category_label.text = "What is the problem?"
_set_scaled_label(category_label, 18, Style.CYAN)
report_form_content.add_child(category_label)
var category_grid := GridContainer.new()
category_grid.columns = 2
category_grid.add_theme_constant_override("h_separation", 18)
category_grid.add_theme_constant_override("v_separation", 8)
report_form_content.add_child(category_grid)
report_category_group = ButtonGroup.new()
report_category_group.allow_unpress = false
for category in REPORT_CATEGORIES:
var category_button := CheckBox.new()
category_button.text = str(category["label"])
category_button.set_meta("report_category_id", str(category["id"]))
category_button.button_group = report_category_group
category_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
category_button.custom_minimum_size = Vector2(0, 38)
_set_scaled_button_font(category_button, 16)
category_button.toggled.connect(_on_report_category_toggled)
category_grid.add_child(category_button)
report_category_buttons.append(category_button)
var details_header := HBoxContainer.new()
report_form_content.add_child(details_header)
var details_label := Label.new()
details_label.text = "Details"
details_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_set_scaled_label(details_label, 18, Style.CYAN)
details_header.add_child(details_label)
report_char_count = Label.new()
report_char_count.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
_set_scaled_label(report_char_count, 14, Style.MUTED)
details_header.add_child(report_char_count)
report_details = TextEdit.new()
report_details.placeholder_text = "Describe what is wrong and what should be reviewed."
report_details.custom_minimum_size = Vector2(0, 190)
report_details.size_flags_vertical = Control.SIZE_EXPAND_FILL
report_details.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
report_details.add_theme_font_size_override("font_size", 16)
report_details.add_theme_color_override("font_color", Style.TEXT)
report_details.add_theme_color_override("font_placeholder_color", Style.MUTED)
report_details.add_theme_stylebox_override("normal", Style.panel_box(Style.SURFACE_DARK, Color("3b4964")))
report_details.text_changed.connect(_on_report_details_changed)
report_form_content.add_child(report_details)
report_error_label = Label.new()
report_error_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
report_error_label.custom_minimum_size = Vector2(0, 28)
_set_scaled_label(report_error_label, 15, Style.RED)
report_form_content.add_child(report_error_label)
var actions := HBoxContainer.new()
actions.add_theme_constant_override("separation", 10)
report_form_content.add_child(actions)
report_cancel_button = Button.new()
report_cancel_button.text = "Cancel"
report_cancel_button.custom_minimum_size = Vector2(130, 46)
Style.style_chip(report_cancel_button, Style.MUTED)
UISoundService.set_button_sound(report_cancel_button, "close")
report_cancel_button.pressed.connect(_hide_report_modal)
actions.add_child(report_cancel_button)
report_submit_button = Button.new()
report_submit_button.text = "Submit report"
report_submit_button.custom_minimum_size = Vector2(170, 46)
report_submit_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
Style.style_primary_button(report_submit_button)
UISoundService.set_button_sound(report_submit_button, "confirmation")
report_submit_button.pressed.connect(_submit_question_report)
actions.add_child(report_submit_button)
report_success_content = VBoxContainer.new()
report_success_content.visible = false
report_success_content.size_flags_vertical = Control.SIZE_EXPAND_FILL
report_success_content.alignment = BoxContainer.ALIGNMENT_CENTER
report_success_content.add_theme_constant_override("separation", 18)
modal_body.add_child(report_success_content)
report_success_label = Label.new()
report_success_label.text = "Report submitted"
report_success_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
report_success_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_set_scaled_label(report_success_label, 24, Style.LIME)
report_success_content.add_child(report_success_label)
var success_close_button := Button.new()
success_close_button.text = "Close"
success_close_button.custom_minimum_size = Vector2(180, 48)
Style.style_primary_button(success_close_button)
UISoundService.set_button_sound(success_close_button, "close")
success_close_button.pressed.connect(_hide_report_modal)
report_success_content.add_child(success_close_button)
_update_report_form_state()
func _report_overlay_gui_input(event: InputEvent) -> void:
var mouse_event := event as InputEventMouseButton
if mouse_event == null or not mouse_event.pressed or mouse_event.button_index != MOUSE_BUTTON_LEFT:
return
if report_panel != null and not report_panel.get_global_rect().has_point(mouse_event.global_position):
_hide_report_modal()
get_viewport().set_input_as_handled()
func _show_report_modal(question_id: String) -> void:
if report_overlay == null or report_is_submitting:
return
_hide_labs_modal()
report_question_id = question_id
report_form_content.visible = true
report_success_content.visible = false
report_details.text = ""
report_error_label.text = ""
for category_button in report_category_buttons:
category_button.button_pressed = false
_set_report_submitting(false)
_update_report_form_state()
report_overlay.visible = true
report_overlay.move_to_front()
BlockSessionService.pause_current_question_timer()
func _hide_report_modal(force: bool = false) -> void:
if report_overlay == null or (report_is_submitting and not force):
return
if report_overlay.visible:
report_overlay.visible = false
BlockSessionService.resume_current_question_timer()
if not report_is_submitting:
report_question_id = ""
func _on_report_category_toggled(pressed: bool) -> void:
if pressed:
_update_report_form_state()
func _on_report_details_changed() -> void:
_update_report_form_state()
func _selected_report_category() -> String:
if report_category_group == null:
return ""
var selected := report_category_group.get_pressed_button()
if selected == null:
return ""
return str(selected.get_meta("report_category_id", ""))
func _update_report_form_state() -> void:
if report_details == null or report_submit_button == null:
return
var details_length := report_details.text.strip_edges().length()
report_char_count.text = "%d / %d" % [details_length, REPORT_DETAILS_MAX]
report_char_count.add_theme_color_override("font_color", Style.RED if details_length > REPORT_DETAILS_MAX else Style.MUTED)
if not report_is_submitting:
report_error_label.text = ""
report_submit_button.disabled = (
report_is_submitting
or _selected_report_category() == ""
or details_length < REPORT_DETAILS_MIN
or details_length > REPORT_DETAILS_MAX
)
func _set_report_submitting(value: bool) -> void:
report_is_submitting = value
if report_details != null:
report_details.editable = not value
for category_button in report_category_buttons:
category_button.disabled = value
if report_cancel_button != null:
report_cancel_button.disabled = value
if report_submit_button != null:
report_submit_button.text = "Submitting..." if value else "Submit report"
_update_report_form_state()
func _submit_question_report() -> void:
if report_is_submitting or report_question_id == "":
return
var category := _selected_report_category()
var details := report_details.text.strip_edges()
if category == "" or details.length() < REPORT_DETAILS_MIN or details.length() > REPORT_DETAILS_MAX:
_update_report_form_state()
return
report_error_label.text = ""
_set_report_submitting(true)
QuestionBankService.submit_question_report(report_question_id, category, details)
func _on_question_report_submitted(question_id: String, _report_id: String) -> void:
if question_id != report_question_id:
return
_set_report_submitting(false)
if not reported_question_ids.has(question_id):
reported_question_ids.append(question_id)
_refresh_toolbar_state()
report_form_content.visible = false
report_success_content.visible = true
report_success_label.text = "Report submitted\nThank you for helping improve this question."
UISoundService.play_sound("confirmation")
func _on_question_report_failed(question_id: String, message: String) -> void:
if question_id != report_question_id:
return
_set_report_submitting(false)
report_error_label.text = message
UISoundService.play_sound("error")
func _build_labs_modal() -> void:
labs_overlay = Control.new()
labs_overlay.visible = false