Add study session goals and summaries
This commit is contained in:
@@ -11,12 +11,17 @@ var status_label: Label
|
||||
var start_button: Button
|
||||
var requested_count := 10
|
||||
var targeted_setup := false
|
||||
var goal_option: OptionButton
|
||||
var target_label: Label
|
||||
var pending_launch := false
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_theme_constant_override("separation", 16)
|
||||
if not QuestionBankService.questions_changed.is_connected(_refresh_setup):
|
||||
QuestionBankService.questions_changed.connect(_refresh_setup)
|
||||
StudySessionService.session_started.connect(_on_session_started)
|
||||
StudySessionService.session_failed.connect(_on_session_failed)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "Block Setup"
|
||||
@@ -27,9 +32,23 @@ func _ready() -> void:
|
||||
Style.set_label(system_label, 22, Style.CYAN)
|
||||
add_child(system_label)
|
||||
|
||||
var goal_row := HBoxContainer.new()
|
||||
var goal_label := Label.new()
|
||||
goal_label.text = "Finish line"
|
||||
goal_row.add_child(goal_label)
|
||||
goal_option = OptionButton.new()
|
||||
goal_option.add_item("Questions")
|
||||
goal_option.set_item_metadata(0, "question_count")
|
||||
goal_option.add_item("Study duration")
|
||||
goal_option.set_item_metadata(1, "duration")
|
||||
goal_option.item_selected.connect(_on_goal_changed)
|
||||
goal_row.add_child(goal_option)
|
||||
add_child(goal_row)
|
||||
|
||||
var size_row := HBoxContainer.new()
|
||||
size_row.add_child(Label.new())
|
||||
size_row.get_child(0).text = "Question count"
|
||||
target_label = Label.new()
|
||||
target_label.text = "Question count"
|
||||
size_row.add_child(target_label)
|
||||
size_spin = SpinBox.new()
|
||||
size_spin.min_value = 1
|
||||
size_spin.max_value = 40
|
||||
@@ -70,13 +89,18 @@ func _ready() -> void:
|
||||
|
||||
func on_show(payload: Variant = null) -> void:
|
||||
if typeof(payload) == TYPE_DICTIONARY:
|
||||
targeted_setup = true
|
||||
targeted_setup = bool(payload.get("mastery_target", false))
|
||||
selected_system = str(payload.get("step3_system", "All Systems"))
|
||||
requested_count = int(payload.get("question_count", 10))
|
||||
goal_option.set_meta("mastery_target", targeted_setup)
|
||||
var requested_goal := str(payload.get("goal_type", "question_count"))
|
||||
goal_option.select(1 if requested_goal == "duration" else 0)
|
||||
_on_goal_changed(goal_option.selected)
|
||||
size_spin.value = requested_count
|
||||
QuestionBankService.load_targeted_questions(selected_system, requested_count,
|
||||
int(payload.get("difficulty_min", 0)), int(payload.get("difficulty_max", 0)))
|
||||
else:
|
||||
goal_option.set_meta("mastery_target", false)
|
||||
targeted_setup = false
|
||||
selected_system = str(payload) if payload != null else "All Systems"
|
||||
_refresh_setup()
|
||||
@@ -87,8 +111,9 @@ func _refresh_setup() -> void:
|
||||
return
|
||||
system_label.text = "Zone: %s" % selected_system
|
||||
var available := QuestionBankService.get_questions_for_system(selected_system).size()
|
||||
size_spin.max_value = max(1, available)
|
||||
if not QuestionBankService.is_loading and available > 0:
|
||||
if str(goal_option.get_item_metadata(goal_option.selected)) != "duration":
|
||||
size_spin.max_value = max(5, available)
|
||||
if not QuestionBankService.is_loading and available > 0 and str(goal_option.get_item_metadata(goal_option.selected)) != "duration":
|
||||
size_spin.value = min(requested_count, available) if targeted_setup else min(size_spin.value, available)
|
||||
targeted_setup = false
|
||||
start_button.disabled = available == 0 or QuestionBankService.is_loading
|
||||
@@ -105,5 +130,45 @@ func _start_block() -> void:
|
||||
if not QuestionBankService.has_questions():
|
||||
status_label.text = "Questions have not loaded from the API yet."
|
||||
return
|
||||
BlockSessionService.start_block(selected_system, int(size_spin.value), mode_option.get_item_text(mode_option.selected))
|
||||
pending_launch = true
|
||||
start_button.disabled = true
|
||||
status_label.text = "Preparing your study goal..."
|
||||
var mastery_target := bool(goal_option.get_meta("mastery_target", false))
|
||||
var goal_type := "weak_system" if mastery_target else str(goal_option.get_item_metadata(goal_option.selected))
|
||||
var target := 10 if mastery_target else int(size_spin.value)
|
||||
StudySessionService.start(goal_type, target, mode_option.get_item_text(mode_option.selected),
|
||||
selected_system if selected_system != "All Systems" else "")
|
||||
|
||||
|
||||
func _on_goal_changed(_index: int) -> void:
|
||||
if str(goal_option.get_item_metadata(goal_option.selected)) == "duration":
|
||||
target_label.text = "Minutes"
|
||||
size_spin.min_value = 10
|
||||
size_spin.max_value = 60
|
||||
size_spin.step = 5
|
||||
size_spin.value = 20
|
||||
else:
|
||||
target_label.text = "Question count"
|
||||
size_spin.min_value = 5
|
||||
size_spin.step = 1
|
||||
_refresh_setup()
|
||||
|
||||
|
||||
func _on_session_started() -> void:
|
||||
if not pending_launch or not visible:
|
||||
return
|
||||
pending_launch = false
|
||||
var goal_type := str(StudySessionService.progress.get("goalType", "question_count"))
|
||||
var block_size := 40 if goal_type == "duration" else int(StudySessionService.progress.get("targetValue", size_spin.value))
|
||||
BlockSessionService.start_block(selected_system, block_size, mode_option.get_item_text(mode_option.selected))
|
||||
if goal_type == "duration":
|
||||
BlockSessionService.seconds_remaining = float(int(StudySessionService.progress.get("secondsRemaining", 0)))
|
||||
main.show_screen("player")
|
||||
|
||||
|
||||
func _on_session_failed(message: String) -> void:
|
||||
if not pending_launch or not visible:
|
||||
return
|
||||
pending_launch = false
|
||||
start_button.disabled = false
|
||||
status_label.text = message
|
||||
|
||||
@@ -487,6 +487,7 @@ func _launch_mastery_action(item: Dictionary) -> void:
|
||||
_start_review_queue(system_name)
|
||||
return
|
||||
main.show_screen("setup", {
|
||||
"mastery_target": true,
|
||||
"step3_system": system_name,
|
||||
"question_count": int(action.get("questionCount", 10)),
|
||||
"difficulty_min": int(action.get("difficultyMin", 0)) if action.get("difficultyMin") != null else 0,
|
||||
@@ -639,10 +640,28 @@ func _start_review_queue(step3_system: String = "") -> void:
|
||||
return
|
||||
pending_review_launch = true
|
||||
_show_loading_message("Preparing your review queue...")
|
||||
review_queue_service.start_pass(step3_system)
|
||||
set_meta("pending_review_system", step3_system)
|
||||
if not StudySessionService.session_started.is_connected(_on_review_session_started):
|
||||
StudySessionService.session_started.connect(_on_review_session_started)
|
||||
if not StudySessionService.session_failed.is_connected(_on_review_session_failed):
|
||||
StudySessionService.session_failed.connect(_on_review_session_failed)
|
||||
StudySessionService.start("due_reviews", 20, "Tutor", step3_system)
|
||||
_render_review(DashboardService.data)
|
||||
|
||||
|
||||
func _on_review_session_started() -> void:
|
||||
if not pending_review_launch or not visible:
|
||||
return
|
||||
review_queue_service.start_pass(str(get_meta("pending_review_system", "")),
|
||||
int(StudySessionService.progress.get("targetValue", 20)))
|
||||
|
||||
|
||||
func _on_review_session_failed(message: String) -> void:
|
||||
if pending_review_launch and visible:
|
||||
pending_review_launch = false
|
||||
_show_error(message)
|
||||
|
||||
|
||||
func _on_review_question_loaded(question: Dictionary) -> void:
|
||||
if not pending_review_launch or not visible:
|
||||
return
|
||||
|
||||
@@ -39,12 +39,17 @@ func _ready() -> void:
|
||||
Style.apply_app_theme(self)
|
||||
_build_ui()
|
||||
CreditsService.award_received.connect(_show_coin_award)
|
||||
StudySessionService.session_completed.connect(_on_study_session_completed)
|
||||
theme_service.theme_changed.connect(_on_theme_changed)
|
||||
resized.connect(_update_responsive_layout)
|
||||
_update_responsive_layout()
|
||||
show_screen(("legal_consent" if AuthState.requires_legal_acceptance else "dashboard") if AuthState.is_logged_in else "login")
|
||||
|
||||
|
||||
func _on_study_session_completed(summary: Dictionary) -> void:
|
||||
call_deferred("show_screen", "session_summary", summary)
|
||||
|
||||
|
||||
func _show_coin_award(amount: int, reason: String) -> void:
|
||||
var toast := PanelContainer.new()
|
||||
toast.z_index = 200
|
||||
@@ -287,6 +292,12 @@ func _build_ui() -> void:
|
||||
screens_container.add_child(review)
|
||||
screens["review"] = review
|
||||
|
||||
var session_summary := preload("res://scripts/ui/session_summary.gd").new()
|
||||
session_summary.name = "SessionSummary"
|
||||
session_summary.main = self
|
||||
screens_container.add_child(session_summary)
|
||||
screens["session_summary"] = session_summary
|
||||
|
||||
var review_queue_results := preload("res://scripts/ui/review_queue_results.gd").new()
|
||||
review_queue_results.name = "ReviewQueueResults"
|
||||
review_queue_results.main = self
|
||||
@@ -499,6 +510,7 @@ func _update_header_title(screen_name: String) -> void:
|
||||
"dynamic_questions": "Dynamic Questions",
|
||||
"simulation_history": "Exam Simulations",
|
||||
"simulation_results": "Exam Simulations",
|
||||
"session_summary": "Session Summary",
|
||||
"ccs_hub": "CCS Cases",
|
||||
"flashcards_hub": "Flashcards",
|
||||
"flashcards_study": "Flashcards",
|
||||
|
||||
@@ -1224,13 +1224,16 @@ func _process(delta: float) -> void:
|
||||
return
|
||||
if visible and BlockSessionService.block_started and BlockSessionService.session_kind == "core":
|
||||
_update_timer_bar()
|
||||
if visible and BlockSessionService.block_started and BlockSessionService.mode == "Timed" and (report_overlay == null or not report_overlay.visible):
|
||||
var duration_goal := str(StudySessionService.progress.get("goalType", "")) == "duration"
|
||||
if visible and BlockSessionService.block_started and (BlockSessionService.mode == "Timed" or duration_goal) 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))
|
||||
_set_timer_display(shown_seconds)
|
||||
_update_timer_bar()
|
||||
if BlockSessionService.seconds_remaining <= 0:
|
||||
if BlockSessionService.seconds_remaining <= 0 and not duration_goal:
|
||||
_finish()
|
||||
elif BlockSessionService.seconds_remaining <= 0 and duration_goal:
|
||||
timer_label.text = "Finish current question"
|
||||
if visible and not _is_session_timer_visible() and (report_overlay == null or not report_overlay.visible):
|
||||
# No real time limit applies here - show a simple local count-up stopwatch
|
||||
# ("Time elapsed") so the bottom-bar time slot is never blank, without
|
||||
@@ -2081,6 +2084,12 @@ func _open_reference_url(url: String) -> void:
|
||||
|
||||
|
||||
func _advance_or_finish() -> void:
|
||||
if not StudySessionService.summary.is_empty():
|
||||
StudySessionService.emit_deferred_summary()
|
||||
return
|
||||
if str(StudySessionService.progress.get("goalType", "")) == "duration" and BlockSessionService.seconds_remaining <= 0:
|
||||
StudySessionService.check_completion()
|
||||
return
|
||||
if BlockSessionService.move_next():
|
||||
_load_question()
|
||||
else:
|
||||
@@ -2105,6 +2114,8 @@ func _finish() -> void:
|
||||
|
||||
|
||||
func _exit_run() -> void:
|
||||
if StudySessionService.is_active() and BlockSessionService.session_kind != "simulation":
|
||||
StudySessionService.finish_early()
|
||||
if BlockSessionService.session_kind == "bookmark":
|
||||
BlockSessionService.finish_block()
|
||||
main.show_screen("bookmarks")
|
||||
@@ -2157,6 +2168,8 @@ func _set_timer_display(total_seconds: int) -> void:
|
||||
func _is_session_timer_visible() -> bool:
|
||||
if BlockSessionService.session_kind == "core" or BlockSessionService.session_kind == "simulation":
|
||||
return true
|
||||
if str(StudySessionService.progress.get("goalType", "")) == "duration":
|
||||
return true
|
||||
return BlockSessionService.mode == "Timed"
|
||||
|
||||
|
||||
|
||||
@@ -84,7 +84,21 @@ func _start_another() -> void:
|
||||
another_button.disabled = true
|
||||
status_label.text = "Preparing another review pass..."
|
||||
status_label.visible = true
|
||||
ReviewQueueService.start_pass()
|
||||
if not StudySessionService.session_started.is_connected(_on_review_session_started):
|
||||
StudySessionService.session_started.connect(_on_review_session_started)
|
||||
if not StudySessionService.session_failed.is_connected(_on_review_session_failed):
|
||||
StudySessionService.session_failed.connect(_on_review_session_failed)
|
||||
StudySessionService.start("due_reviews", 20, "Tutor")
|
||||
|
||||
|
||||
func _on_review_session_started() -> void:
|
||||
if launch_pending and visible:
|
||||
ReviewQueueService.start_pass("", int(StudySessionService.progress.get("targetValue", 20)))
|
||||
|
||||
|
||||
func _on_review_session_failed(message: String) -> void:
|
||||
if launch_pending and visible:
|
||||
_on_question_failed(message)
|
||||
|
||||
|
||||
func _on_question_loaded(question: Dictionary) -> void:
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
extends VBoxContainer
|
||||
|
||||
const Brand := preload("res://scripts/ui/medmind_style.gd")
|
||||
|
||||
var main: Control
|
||||
var current_summary: Dictionary = {}
|
||||
var content: VBoxContainer
|
||||
var continue_pending := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
content = VBoxContainer.new()
|
||||
content.custom_minimum_size = Vector2(420, 0)
|
||||
content.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
content.add_theme_constant_override("separation", 14)
|
||||
add_child(content)
|
||||
StudySessionService.session_started.connect(_on_continuation_started)
|
||||
UIThemeService.theme_changed.connect(func(_dark: bool): _render())
|
||||
|
||||
|
||||
func on_show(payload: Variant = null) -> void:
|
||||
current_summary = payload if typeof(payload) == TYPE_DICTIONARY else StudySessionService.summary
|
||||
continue_pending = false
|
||||
_render()
|
||||
|
||||
|
||||
func _render() -> void:
|
||||
for child in content.get_children():
|
||||
child.queue_free()
|
||||
var dark := UIThemeService.is_dark_mode()
|
||||
var completed := str(current_summary.get("state", "")) == "completed"
|
||||
content.add_child(_label("Session complete" if completed else "Session ended", 30, Brand.SUCCESS if completed else Brand.AMBER))
|
||||
var questions := int(current_summary.get("questionsCompleted", 0))
|
||||
var accuracy := int(current_summary.get("accuracy", 0))
|
||||
var elapsed := int(current_summary.get("elapsedSeconds", 0))
|
||||
var detail := "Questions completed: %d\nFirst-attempt accuracy: %d%%\nStudy time: %d min %02d sec" % [
|
||||
questions, accuracy, elapsed / 60, elapsed % 60]
|
||||
if int(current_summary.get("reviewsCompleted", 0)) > 0:
|
||||
detail += "\nScheduled reviews: %d · Recovered: %d" % [current_summary.get("reviewsCompleted", 0), current_summary.get("recovered", 0)]
|
||||
var mastery_change: Variant = current_summary.get("masteryScoreChange", null)
|
||||
if mastery_change != null:
|
||||
detail += "\n%s mastery: %+d points (%s → %s)" % [
|
||||
str(current_summary.get("step3System", "System")), int(mastery_change),
|
||||
str(current_summary.get("initialMasteryState", "")), str(current_summary.get("finalMasteryState", ""))]
|
||||
detail += "\nRZ Coins earned: %d" % int(current_summary.get("coinsEarned", 0))
|
||||
var award_lines: Array[String] = []
|
||||
for award in current_summary.get("coinAwards", []):
|
||||
if typeof(award) == TYPE_DICTIONARY:
|
||||
award_lines.append("+%d %s" % [int(award.get("amount", 0)), str(award.get("description", "RZ Coin award"))])
|
||||
if not award_lines.is_empty():
|
||||
detail += "\nAwards: %s" % "; ".join(award_lines)
|
||||
var body := _label(detail, 17, Brand.text_color(dark))
|
||||
body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
content.add_child(body)
|
||||
var readiness := _label(str(current_summary.get("readinessExplanation", "A completed exam simulation is required to update readiness.")), 13, Brand.muted_color(dark))
|
||||
readiness.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
content.add_child(readiness)
|
||||
var recommendation := _label(_recommendation(), 15, Brand.SKY_BLUE)
|
||||
recommendation.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
content.add_child(recommendation)
|
||||
var actions := HBoxContainer.new()
|
||||
actions.add_theme_constant_override("separation", 12)
|
||||
var finish := Button.new()
|
||||
finish.text = "Finish for today"
|
||||
finish.custom_minimum_size = Vector2(220, 52)
|
||||
finish.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
finish.pressed.connect(func(): DashboardService.refresh(); main.show_screen("dashboard"))
|
||||
actions.add_child(finish)
|
||||
var continue_button := Button.new()
|
||||
continue_button.text = "Continue with another block"
|
||||
continue_button.custom_minimum_size = Vector2(220, 52)
|
||||
continue_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
continue_button.pressed.connect(_continue)
|
||||
actions.add_child(continue_button)
|
||||
content.add_child(actions)
|
||||
|
||||
|
||||
func _continue() -> void:
|
||||
var goal := str(current_summary.get("goalType", "question_count"))
|
||||
if goal == "simulation_block":
|
||||
main.show_screen("simulation_break")
|
||||
return
|
||||
if goal == "due_reviews":
|
||||
continue_pending = true
|
||||
StudySessionService.start("due_reviews", 20, "Tutor", str(current_summary.get("step3System", "")))
|
||||
return
|
||||
main.show_screen("setup", {
|
||||
"step3_system": str(current_summary.get("step3System", "All Systems")),
|
||||
"question_count": int(current_summary.get("targetValue", 10)),
|
||||
"goal_type": goal
|
||||
})
|
||||
|
||||
|
||||
func _on_continuation_started() -> void:
|
||||
if continue_pending and visible:
|
||||
continue_pending = false
|
||||
ReviewQueueService.start_pass(str(current_summary.get("step3System", "")),
|
||||
int(StudySessionService.progress.get("targetValue", 20)))
|
||||
|
||||
|
||||
func _recommendation() -> String:
|
||||
if int(current_summary.get("recovered", 0)) > 0:
|
||||
return "Recommended next session: reinforce another weak system or complete the next scheduled reviews."
|
||||
return "Recommended next session: continue the same goal, or finish for today and return refreshed."
|
||||
|
||||
|
||||
func _label(text: String, size: int, color: Color) -> Label:
|
||||
var label := Label.new()
|
||||
label.text = text
|
||||
label.add_theme_font_override("font", Brand.font)
|
||||
label.add_theme_font_size_override("font_size", size)
|
||||
label.add_theme_color_override("font_color", color)
|
||||
return label
|
||||
@@ -0,0 +1 @@
|
||||
uid://gj2gvcawmgng
|
||||
Reference in New Issue
Block a user