Implement persistent review queue
This commit is contained in:
@@ -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
@@ -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")
|
||||
|
||||
@@ -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()):
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://c5ac2bohtsllu
|
||||
Reference in New Issue
Block a user