Implement persistent review queue

This commit is contained in:
hz
2026-07-19 12:51:24 -05:00
parent 1bfb3f5347
commit 7508d4fe1d
20 changed files with 1155 additions and 20 deletions
+65 -5
View File
@@ -27,6 +27,7 @@ var core_status_message := ""
var simulation_block_index := 0
var simulation_total_blocks := 12
var simulation_deadline_unix := 0.0
var review_first_submission_recorded := false
const CORE_MIN_TARGET_SECONDS := 45.0
const CORE_MAX_TARGET_SECONDS := 150.0
@@ -107,6 +108,38 @@ func replace_bookmark_question(question: Dictionary) -> bool:
return true
func start_review_question(question: Dictionary) -> bool:
if question.is_empty():
return false
system_name = str(question.get("system", "All Systems"))
mode = "Tutor"
session_kind = "review"
active_questions = [_prepare_question_choices(question)]
answers = [null]
current_index = 0
seconds_remaining = 0.0
session_initial_seconds = 90.0
current_question_started_at = Time.get_ticks_msec() / 1000.0
current_question_pause_started_at = 0.0
review_first_submission_recorded = false
block_started = true
session_changed.emit()
return true
func replace_review_question(question: Dictionary) -> bool:
if session_kind != "review" or question.is_empty():
return false
active_questions = [_prepare_question_choices(question)]
answers = [null]
current_index = 0
current_question_started_at = Time.get_ticks_msec() / 1000.0
current_question_pause_started_at = 0.0
review_first_submission_recorded = false
session_changed.emit()
return true
func start_simulation_block(questions: Array, block_index: int, total_blocks: int, deadline_unix: float) -> void:
system_name = "All Systems"
mode = "Simulation"
@@ -239,6 +272,32 @@ func submit_bookmark_attempt(choice_id: String, eliminated: Array, flagged: bool
return {"answer": answer, "is_correct": bool(answer.is_correct), "final": bool(answer.is_correct)}
func submit_review_attempt(choice_id: String, eliminated: Array, flagged: bool) -> Dictionary:
var question := get_current_question()
if session_kind != "review" or question.is_empty():
return {}
var first_submission := not review_first_submission_recorded
var answer := {
"question": question.duplicate(true),
"selected_choice_id": choice_id,
"is_correct": choice_id == str(question.get("correct_choice_id", "")),
"eliminated_choice_ids": eliminated.duplicate(),
"flagged": flagged,
"elapsed_seconds": get_current_elapsed_seconds(),
"target_seconds": 0.0
}
review_first_submission_recorded = true
if bool(answer.is_correct):
answers[current_index] = answer
session_changed.emit()
return {
"answer": answer,
"is_correct": bool(answer.is_correct),
"final": bool(answer.is_correct),
"first_submission": first_submission
}
func skip_bookmark_question(eliminated: Array, flagged: bool) -> Dictionary:
var question := get_current_question()
if session_kind != "bookmark" or question.is_empty():
@@ -316,7 +375,7 @@ func get_progress_text() -> String:
func finish_block() -> Dictionary:
if session_kind in ["core", "bookmark"]:
if session_kind in ["core", "bookmark", "review"]:
block_started = false
session_changed.emit()
return {}
@@ -435,19 +494,20 @@ func get_session_timer_ratio() -> float:
func pause_current_question_timer() -> void:
if session_kind == "core" and current_question_pause_started_at <= 0.0:
if session_kind in ["core", "review"] and current_question_pause_started_at <= 0.0:
var now := Time.get_ticks_msec() / 1000.0
current_question_pause_started_at = now
_pause_core_question_total_timer(now)
if session_kind == "core":
_pause_core_question_total_timer(now)
func resume_current_question_timer() -> void:
if session_kind == "core" and current_question_pause_started_at > 0.0:
if session_kind in ["core", "review"] and current_question_pause_started_at > 0.0:
var now := Time.get_ticks_msec() / 1000.0
var paused_duration := now - current_question_pause_started_at
current_question_started_at += paused_duration
current_question_pause_started_at = 0.0
if not core_question_total_frozen:
if session_kind == "core" and not core_question_total_frozen:
core_question_total_active_since = now
+268
View File
@@ -0,0 +1,268 @@
extends Node
signal question_loaded(question: Dictionary)
signal question_failed(message: String)
signal pass_completed(summary: Dictionary)
signal attempt_synced(question_id: String, is_correct: bool, pending_count: int)
const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd")
const DEFAULT_API_URL := "https://s3q.ranaze.com/api/review-queue"
const OUTBOX_PATH := "user://pending_review_attempts.json"
var api_url := DEFAULT_API_URL
var next_request: HTTPRequest
var attempt_request: HTTPRequest
var retry_timer: Timer
var loading := false
var submitting := false
var request_token := ""
var excluded_question_ids: Array = []
var current_cycle := 0
var pending_count := 0
var reviewed_count := 0
var cleared_count := 0
var retained_count := 0
var outbox: Array = []
var active_account_key := ""
func _ready() -> void:
api_url = EndpointConfig.resolve(
str(ProjectSettings.get_setting("step3/review_queue_api_url", DEFAULT_API_URL)),
"/api/review-queue")
next_request = HTTPRequest.new()
next_request.timeout = 25.0
next_request.request_completed.connect(_on_next_completed)
add_child(next_request)
attempt_request = HTTPRequest.new()
attempt_request.timeout = 25.0
attempt_request.request_completed.connect(_on_attempt_completed)
add_child(attempt_request)
retry_timer = Timer.new()
retry_timer.one_shot = true
retry_timer.wait_time = 5.0
retry_timer.timeout.connect(_process_outbox)
add_child(retry_timer)
_load_outbox()
AuthState.auth_changed.connect(_on_auth_changed)
_on_auth_changed()
func start_pass() -> void:
excluded_question_ids.clear()
reviewed_count = 0
cleared_count = 0
retained_count = 0
load_next()
func load_next() -> void:
if loading:
return
if not AuthState.can_send_authenticated_request():
question_failed.emit("Login is required to load the review queue.")
return
loading = true
request_token = AuthState.access_token
var payload := {
"excludeExternalIds": excluded_question_ids,
"supportsChoicePools": true
}
var error := next_request.request(
"%s/next" % api_url,
_headers(),
HTTPClient.METHOD_POST,
JSON.stringify(payload))
if error != OK:
loading = false
question_failed.emit("The review queue could not be loaded. Check your connection and try again.")
func record_first_submission(question: Dictionary, choice_id: String, presented_choice_ids: Array, duration_seconds: int) -> void:
var question_id := str(question.get("id", ""))
if question_id == "" or choice_id == "" or current_cycle < 1:
return
var is_correct := choice_id == str(question.get("correct_choice_id", ""))
reviewed_count += 1
if is_correct:
cleared_count += 1
pending_count = maxi(0, pending_count - 1)
else:
retained_count += 1
var payload := {
"clientAttemptId": _new_uuid(),
"questionExternalId": question_id,
"cycle": current_cycle,
"choiceId": choice_id,
"presentedChoiceIds": presented_choice_ids.duplicate(),
"durationSeconds": clampi(duration_seconds, 0, 86400)
}
outbox.append({"account": _account_key(), "payload": payload, "retry_count": 0})
_save_outbox()
_process_outbox()
func get_summary() -> Dictionary:
return {
"reviewed": reviewed_count,
"cleared": cleared_count,
"remaining": maxi(0, pending_count),
"needs_review": retained_count
}
func has_pending_submissions() -> bool:
return _next_account_item_index() >= 0
func _on_next_completed(result: int, response_code: int, _headers_value: PackedStringArray, body: PackedByteArray) -> void:
loading = false
if response_code == 401:
AuthState.handle_unauthorized(request_token)
return
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
question_failed.emit("The review queue could not be loaded. Check your connection and try again.")
return
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
if typeof(parsed) != TYPE_DICTIONARY:
question_failed.emit("The review service returned an invalid response.")
return
pending_count = int(parsed.get("pendingCount", parsed.get("pending_count", pending_count)))
var raw_question: Variant = parsed.get("question", {})
if typeof(raw_question) != TYPE_DICTIONARY or raw_question.is_empty():
pass_completed.emit(get_summary())
return
var question := QuestionBankService.normalize_api_question(raw_question)
if question.is_empty() or str(question.get("id", "")) == "":
question_failed.emit("The review service returned an invalid question.")
return
current_cycle = int(parsed.get("cycle", 0))
question["review_cycle"] = current_cycle
var question_id := str(question.get("id", ""))
if not excluded_question_ids.has(question_id):
excluded_question_ids.append(question_id)
question_loaded.emit(question)
func _process_outbox() -> void:
if submitting or outbox.is_empty() or not AuthState.can_send_authenticated_request():
return
var index := _next_account_item_index()
if index < 0:
return
var item: Dictionary = outbox[index]
item["active"] = true
outbox[index] = item
submitting = true
request_token = AuthState.access_token
var error := attempt_request.request(
"%s/attempts" % api_url,
_headers(),
HTTPClient.METHOD_POST,
JSON.stringify(item.get("payload", {})))
if error != OK:
submitting = false
item.erase("active")
item["retry_count"] = int(item.get("retry_count", 0)) + 1
outbox[index] = item
_save_outbox()
retry_timer.start()
func _on_attempt_completed(result: int, response_code: int, _headers_value: PackedStringArray, body: PackedByteArray) -> void:
submitting = false
var index := _active_item_index()
if index < 0:
return
var item: Dictionary = outbox[index]
item.erase("active")
if response_code == 401:
outbox[index] = item
_save_outbox()
AuthState.handle_unauthorized(request_token)
return
if result == HTTPRequest.RESULT_SUCCESS and response_code >= 200 and response_code < 300:
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
var synced_question_id := ""
var synced_is_correct := false
if typeof(parsed) == TYPE_DICTIONARY:
pending_count = int(parsed.get("pendingCount", parsed.get("pending_count", pending_count)))
synced_question_id = str(parsed.get("questionExternalId", parsed.get("question_external_id", "")))
synced_is_correct = bool(parsed.get("isCorrect", parsed.get("is_correct", false)))
outbox.remove_at(index)
_save_outbox()
attempt_synced.emit(synced_question_id, synced_is_correct, pending_count)
_process_outbox()
return
if response_code >= 400 and response_code < 500 and response_code not in [408, 429]:
outbox.remove_at(index)
_save_outbox()
_process_outbox()
return
item["retry_count"] = int(item.get("retry_count", 0)) + 1
outbox[index] = item
_save_outbox()
retry_timer.start()
func _headers() -> PackedStringArray:
return PackedStringArray([
"Content-Type: application/json",
"Authorization: Bearer %s" % AuthState.access_token
])
func _on_auth_changed() -> void:
active_account_key = _account_key()
if AuthState.is_logged_in:
_process_outbox()
elif retry_timer != null:
retry_timer.stop()
func _account_key() -> String:
return AuthState.username.strip_edges().to_lower().sha256_text() if not AuthState.username.strip_edges().is_empty() else ""
func _next_account_item_index() -> int:
for index in range(outbox.size()):
if str(outbox[index].get("account", "")) == active_account_key:
return index
return -1
func _active_item_index() -> int:
for index in range(outbox.size()):
if bool(outbox[index].get("active", false)):
return index
return -1
func _load_outbox() -> void:
outbox = []
if not FileAccess.file_exists(OUTBOX_PATH):
return
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(OUTBOX_PATH))
if typeof(parsed) != TYPE_ARRAY:
return
for item in parsed:
if typeof(item) == TYPE_DICTIONARY and typeof(item.get("payload", {})) == TYPE_DICTIONARY:
item.erase("active")
outbox.append(item)
func _save_outbox() -> void:
var stored := outbox.duplicate(true)
for item in stored:
item.erase("active")
var file := FileAccess.open(OUTBOX_PATH, FileAccess.WRITE)
if file != null:
file.store_string(JSON.stringify(stored))
func _new_uuid() -> String:
var bytes := Crypto.new().generate_random_bytes(16)
bytes[6] = (bytes[6] & 0x0f) | 0x40
bytes[8] = (bytes[8] & 0x3f) | 0x80
var value := bytes.hex_encode()
return "%s-%s-%s-%s-%s" % [value.substr(0, 8), value.substr(8, 4), value.substr(12, 4), value.substr(16, 4), value.substr(20, 12)]
@@ -0,0 +1 @@
uid://coquauokw5nup
+56 -2
View File
@@ -20,12 +20,16 @@ var recent_list: VBoxContainer
var weekly_summary: VBoxContainer
var metric_values: Dictionary = {}
var metric_details: Dictionary = {}
var pending_review_launch := false
func _ready() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
DashboardService.dashboard_changed.connect(_render)
DashboardService.dashboard_failed.connect(_show_error)
ReviewQueueService.question_loaded.connect(_on_review_question_loaded)
ReviewQueueService.question_failed.connect(_on_review_question_failed)
ReviewQueueService.pass_completed.connect(_on_review_pass_completed)
UIThemeService.theme_changed.connect(_rebuild)
resized.connect(_update_responsive)
_build()
@@ -229,7 +233,15 @@ func _render_focus(systems: Array) -> void:
func _render_review(data: Dictionary) -> void:
_clear(review_list)
review_list.add_child(_section_title("Review queue"))
review_list.add_child(_review_action("%d missed questions" % int(data.get("missedQuestionCount", 0)), "x", Brand.MAGENTA, "profile"))
var missed_count := int(data.get("missedQuestionCount", 0))
var missed_action := _review_action(
"%d missed questions" % missed_count if missed_count > 0 else "Review queue clear",
"x",
Brand.MAGENTA,
"")
missed_action.disabled = missed_count <= 0 or pending_review_launch
missed_action.pressed.connect(_start_review_queue)
review_list.add_child(missed_action)
review_list.add_child(_review_action("%d bookmarks" % int(data.get("bookmarkCount", 0)), "bookmark", Brand.ROYAL_BLUE, "bookmarks"))
review_list.add_child(_review_action("%d notes" % int(data.get("noteCount", 0)), "notebook-pen", Brand.AMBER, "notes"))
@@ -311,10 +323,52 @@ func _review_action(text: String, icon_name: String, accent: Color, route: Strin
var button := _button(text, icon_name, false)
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.add_theme_color_override("icon_normal_color", accent)
button.pressed.connect(func(): main._on_navigation_pressed(route))
if not route.is_empty():
button.pressed.connect(func(): main._on_navigation_pressed(route))
return button
func _start_review_queue() -> void:
if pending_review_launch:
return
pending_review_launch = true
_show_loading_message("Preparing your review queue...")
ReviewQueueService.start_pass()
_render_review(DashboardService.data)
func _on_review_question_loaded(question: Dictionary) -> void:
if not pending_review_launch or not visible:
return
pending_review_launch = false
if BlockSessionService.start_review_question(question):
main.show_screen("player")
else:
_show_error("The review question could not be prepared.")
func _on_review_question_failed(message: String) -> void:
if not pending_review_launch or not visible:
return
pending_review_launch = false
_show_error(message)
_render_review(DashboardService.data)
func _on_review_pass_completed(_summary: Dictionary) -> void:
if not pending_review_launch or not visible:
return
pending_review_launch = false
DashboardService.refresh()
_show_loading_message("Your review queue is clear.")
func _show_loading_message(message: String) -> void:
status_label.text = message
status_label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
status_label.visible = true
func _summary_stat(value: String, caption: String, icon_name: String, accent: Color) -> HBoxContainer:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 10)
+11 -1
View File
@@ -253,6 +253,12 @@ func _build_ui() -> void:
screens_container.add_child(review)
screens["review"] = review
var review_queue_results := preload("res://scripts/ui/review_queue_results.gd").new()
review_queue_results.name = "ReviewQueueResults"
review_queue_results.main = self
screens_container.add_child(review_queue_results)
screens["review_queue_results"] = review_queue_results
var simulation_break := preload("res://scripts/ui/simulation_break.gd").new()
simulation_break.name = "SimulationBreak"
simulation_break.main = self
@@ -846,8 +852,12 @@ func _exit_run() -> void:
if ccs_player != null and ccs_player.has_method("leave_case"): ccs_player.leave_case()
return
var was_core_run := BlockSessionService.session_kind == "core"
var was_review_run := BlockSessionService.session_kind == "review"
if BlockSessionService.block_started and BlockSessionService.session_kind != "simulation":
BlockSessionService.finish_block()
elif BlockSessionService.session_kind == "simulation":
BlockSessionService.block_started = false
show_screen("selection", "exam" if was_core_run else "step3")
if was_review_run:
show_screen("dashboard")
else:
show_screen("selection", "exam" if was_core_run else "step3")
+68 -7
View File
@@ -168,6 +168,12 @@ func _ready() -> void:
BookmarkService.question_loaded.connect(_on_bookmark_question_loaded)
if not BookmarkService.question_failed.is_connected(_on_bookmark_question_failed):
BookmarkService.question_failed.connect(_on_bookmark_question_failed)
if not ReviewQueueService.question_loaded.is_connected(_on_review_question_loaded):
ReviewQueueService.question_loaded.connect(_on_review_question_loaded)
if not ReviewQueueService.question_failed.is_connected(_on_review_question_failed):
ReviewQueueService.question_failed.connect(_on_review_question_failed)
if not ReviewQueueService.pass_completed.is_connected(_on_review_pass_completed):
ReviewQueueService.pass_completed.connect(_on_review_pass_completed)
question_card = PanelContainer.new()
question_card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
@@ -1036,18 +1042,21 @@ func _load_question() -> void:
next_button.text = "Submit"
next_button.disabled = true
skip_button.disabled = false
skip_button.visible = true
skip_button.visible = BlockSessionService.session_kind != "review"
skip_button.text = "Skip"
report_button.visible = BlockSessionService.session_kind != "simulation"
var question := BlockSessionService.get_current_question()
if question.is_empty():
main.show_screen("bookmarks" if BlockSessionService.session_kind == "bookmark" else "selection", "exam" if BlockSessionService.session_kind == "core" else "step3")
if BlockSessionService.session_kind == "review":
main.show_screen("dashboard")
else:
main.show_screen("bookmarks" if BlockSessionService.session_kind == "bookmark" else "selection", "exam" if BlockSessionService.session_kind == "core" else "step3")
return
BookmarkService.observe_question(question)
progress_button.text = _question_progress_text()
progress_button.disabled = BlockSessionService.session_kind in ["core", "bookmark"]
progress_button.tooltip_text = "" if BlockSessionService.session_kind in ["core", "bookmark"] else "Open question navigator"
progress_button.disabled = BlockSessionService.session_kind in ["core", "bookmark", "review"]
progress_button.tooltip_text = "" if BlockSessionService.session_kind in ["core", "bookmark", "review"] else "Open question navigator"
var show_timer := _is_session_timer_visible()
timer_label.visible = show_timer
timer_bar.visible = show_timer
@@ -1291,7 +1300,7 @@ func _submit_or_next() -> void:
if selected_choice == "":
return
var question_id_for_attempt := str(BlockSessionService.get_current_question().get("id", ""))
if BlockSessionService.session_kind in ["core", "bookmark"]:
if BlockSessionService.session_kind in ["core", "bookmark", "review"]:
_collapse_resolved_followups()
var attempted_choice := selected_choice
if BlockSessionService.session_kind == "core":
@@ -1301,10 +1310,22 @@ func _submit_or_next() -> void:
_presented_choice_ids(),
int(floor(BlockSessionService.get_core_question_total_elapsed_seconds()))
)
var result := BlockSessionService.submit_core_attempt(selected_choice, eliminated, flagged) if BlockSessionService.session_kind == "core" else BlockSessionService.submit_bookmark_attempt(selected_choice, eliminated, flagged)
var result: Dictionary
if BlockSessionService.session_kind == "core":
result = BlockSessionService.submit_core_attempt(selected_choice, eliminated, flagged)
elif BlockSessionService.session_kind == "bookmark":
result = BlockSessionService.submit_bookmark_attempt(selected_choice, eliminated, flagged)
else:
result = BlockSessionService.submit_review_attempt(selected_choice, eliminated, flagged)
if result.is_empty():
return
var answer: Dictionary = result.get("answer", {})
if BlockSessionService.session_kind == "review" and bool(result.get("first_submission", false)):
ReviewQueueService.record_first_submission(
BlockSessionService.get_current_question(),
attempted_choice,
_presented_choice_ids(),
int(floor(float(answer.get("elapsed_seconds", 0.0)))))
if bool(result.get("is_correct", false)):
current_answer = answer
_update_question_elo_label(true)
@@ -1339,6 +1360,11 @@ func _submit_or_next() -> void:
BookmarkService.next_practice()
else:
_finish()
elif BlockSessionService.session_kind == "review":
awaiting_next_question = true
next_button.text = "Loading..."
next_button.disabled = true
ReviewQueueService.load_next()
else:
_advance_or_finish()
@@ -1708,6 +1734,10 @@ func _finish() -> void:
BlockSessionService.finish_block()
main.show_screen("selection", "exam")
return
if BlockSessionService.session_kind == "review":
BlockSessionService.finish_block()
main.show_screen("review_queue_results", ReviewQueueService.get_summary())
return
var results := BlockSessionService.finish_block()
main.show_screen("review", results)
@@ -1717,6 +1747,11 @@ func _exit_run() -> void:
BlockSessionService.finish_block()
main.show_screen("bookmarks")
return
if BlockSessionService.session_kind == "review":
BlockSessionService.finish_block()
DashboardService.refresh()
main.show_screen("dashboard")
return
var was_core_run := BlockSessionService.session_kind == "core"
BlockSessionService.resume_current_question_timer()
BlockSessionService.finish_block()
@@ -1973,6 +2008,32 @@ func _on_bookmark_question_failed(message: String) -> void:
next_button.tooltip_text = message
func _on_review_question_loaded(question: Dictionary) -> void:
if not visible or BlockSessionService.session_kind != "review" or not awaiting_next_question:
return
awaiting_next_question = false
if BlockSessionService.replace_review_question(question):
_load_question()
UISoundService.play_sound("question")
func _on_review_question_failed(message: String) -> void:
if not visible or BlockSessionService.session_kind != "review":
return
awaiting_next_question = false
next_button.text = "Retry"
next_button.disabled = false
next_button.tooltip_text = message
func _on_review_pass_completed(summary: Dictionary) -> void:
if not visible or BlockSessionService.session_kind != "review":
return
awaiting_next_question = false
BlockSessionService.finish_block()
main.show_screen("review_queue_results", summary)
func refresh_theme_controls() -> void:
if media_viewer_background != null:
media_viewer_background.color = Style.BACKGROUND
@@ -3458,7 +3519,7 @@ func _refresh_nav_palette() -> void:
for child in nav_palette.get_children():
child.queue_free()
if BlockSessionService.session_kind == "core":
if BlockSessionService.session_kind in ["core", "review"]:
return
for index in range(BlockSessionService.active_questions.size()):
+151
View File
@@ -0,0 +1,151 @@
extends VBoxContainer
const Brand := preload("res://scripts/ui/medmind_style.gd")
var main: Control
var title_label: Label
var summary_label: Label
var status_label: Label
var another_button: Button
var results_panel: PanelContainer
var dashboard_button: Button
var launch_pending := false
var latest_remaining := 0
func _ready() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
alignment = BoxContainer.ALIGNMENT_CENTER
add_theme_constant_override("separation", 18)
UIThemeService.theme_changed.connect(_apply_theme)
ReviewQueueService.question_loaded.connect(_on_question_loaded)
ReviewQueueService.question_failed.connect(_on_question_failed)
ReviewQueueService.pass_completed.connect(_on_pass_completed)
ReviewQueueService.attempt_synced.connect(_on_attempt_synced)
var spacer := Control.new()
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
add_child(spacer)
results_panel = PanelContainer.new()
results_panel.custom_minimum_size = Vector2(520, 0)
results_panel.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
add_child(results_panel)
var content := VBoxContainer.new()
content.add_theme_constant_override("separation", 16)
results_panel.add_child(content)
title_label = _label("Review pass complete", 28, true)
content.add_child(title_label)
summary_label = _label("", 17)
summary_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
content.add_child(summary_label)
status_label = _label("", 14)
status_label.visible = false
content.add_child(status_label)
another_button = Button.new()
another_button.text = "Start Another Review"
another_button.custom_minimum_size.y = 46
another_button.pressed.connect(_start_another)
content.add_child(another_button)
dashboard_button = Button.new()
dashboard_button.text = "Back to Dashboard"
dashboard_button.custom_minimum_size.y = 46
dashboard_button.pressed.connect(_back_to_dashboard)
content.add_child(dashboard_button)
var bottom_spacer := Control.new()
bottom_spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
add_child(bottom_spacer)
_apply_theme()
func on_show(payload: Variant = null) -> void:
var summary: Dictionary = payload if typeof(payload) == TYPE_DICTIONARY else ReviewQueueService.get_summary()
var reviewed := int(summary.get("reviewed", 0))
var cleared := int(summary.get("cleared", 0))
var remaining := int(summary.get("remaining", 0))
latest_remaining = remaining
summary_label.text = "Reviewed %d question%s\nCleared %d from the queue\n%d still need%s review" % [
reviewed,
"" if reviewed == 1 else "s",
cleared,
remaining,
"s" if remaining == 1 else ""
]
another_button.disabled = remaining <= 0 or ReviewQueueService.has_pending_submissions()
status_label.text = "Saving your review result..." if ReviewQueueService.has_pending_submissions() else ""
status_label.visible = not status_label.text.is_empty()
launch_pending = false
DashboardService.refresh()
func _start_another() -> void:
if launch_pending:
return
launch_pending = true
another_button.disabled = true
status_label.text = "Preparing another review pass..."
status_label.visible = true
ReviewQueueService.start_pass()
func _on_question_loaded(question: Dictionary) -> void:
if not launch_pending or not visible:
return
launch_pending = false
if BlockSessionService.start_review_question(question):
main.show_screen("player")
else:
_on_question_failed("The review question could not be prepared.")
func _on_question_failed(message: String) -> void:
if not launch_pending or not visible:
return
launch_pending = false
another_button.disabled = false
status_label.text = message
status_label.visible = true
func _on_pass_completed(summary: Dictionary) -> void:
if not launch_pending or not visible:
return
launch_pending = false
on_show(summary)
func _on_attempt_synced(_question_id: String, _is_correct: bool, server_pending_count: int) -> void:
if not visible:
return
var summary := ReviewQueueService.get_summary()
summary["remaining"] = server_pending_count
on_show(summary)
func _back_to_dashboard() -> void:
main.show_screen("dashboard")
func _apply_theme(_dark: bool = UIThemeService.is_dark_mode()) -> void:
var dark := UIThemeService.is_dark_mode()
if results_panel != null:
results_panel.add_theme_stylebox_override("panel", Brand.panel_box(dark))
for label in [title_label, summary_label]:
if label != null:
label.add_theme_color_override("font_color", Brand.text_color(dark))
if status_label != null:
status_label.add_theme_color_override("font_color", Brand.muted_color(dark))
if another_button != null:
another_button.add_theme_stylebox_override("normal", Brand.secondary_button_box(dark))
if dashboard_button != null:
dashboard_button.add_theme_stylebox_override("normal", Brand.secondary_button_box(dark))
func _label(text: String, font_size: int, bold: bool = false) -> Label:
var label := Label.new()
label.text = text
label.add_theme_font_override("font", Brand.font)
label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", Brand.text_color(UIThemeService.is_dark_mode()))
if bold:
label.add_theme_constant_override("outline_size", 0)
return label
+1
View File
@@ -0,0 +1 @@
uid://c5ac2bohtsllu