extends VBoxContainer const Style := preload("res://scripts/ui/ui_style.gd") var main: Control var profile_text: RichTextLabel var reset_qbank_button: Button var reset_qbank_pending := false var outcome_score: SpinBox var outcome_date: LineEdit var outcome_consent: CheckBox var outcome_status: Label var outcome_delete_button: Button var outcome_grid: GridContainer func _ready() -> void: set_anchors_preset(Control.PRESET_FULL_RECT) add_theme_constant_override("separation", 14) var title := Label.new() title.text = "Progress Profile" Style.set_label(title, 34, Style.CYAN) add_child(title) var scroll := ScrollContainer.new() scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL add_child(scroll) var content := VBoxContainer.new() content.size_flags_horizontal = Control.SIZE_EXPAND_FILL content.add_theme_constant_override("separation", 14) scroll.add_child(content) var panel := PanelContainer.new() panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Color("405475"))) panel.custom_minimum_size.y = 300 content.add_child(panel) profile_text = RichTextLabel.new() profile_text.bbcode_enabled = true profile_text.size_flags_vertical = Control.SIZE_EXPAND_FILL panel.add_child(profile_text) _build_outcome_panel(content) var row := HBoxContainer.new() var back := Button.new() back.text = "Back" back.pressed.connect(func(): main.show_screen("selection", "step3")) row.add_child(back) var reset := Button.new() reset.text = "Reset Progress" Style.style_chip(reset, Style.RED) reset.pressed.connect(func(): ProfileService.reset_profile() on_show() ) row.add_child(reset) var simulations := Button.new() simulations.text = "Simulation History" Style.style_chip(simulations, Style.CYAN) simulations.pressed.connect(func(): main.show_screen("simulation_history")) row.add_child(simulations) var ccs_history := Button.new() ccs_history.text = "CCS History" Style.style_chip(ccs_history, Style.CYAN) ccs_history.pressed.connect(func(): main.show_screen("ccs_history")) row.add_child(ccs_history) reset_qbank_button = Button.new() reset_qbank_button.text = "Reset QBank" Style.style_chip(reset_qbank_button, Style.AMBER) reset_qbank_button.pressed.connect(_on_reset_qbank_pressed) row.add_child(reset_qbank_button) add_child(row) if not ExamSimulationService.outcome_changed.is_connected(_refresh_outcome): ExamSimulationService.outcome_changed.connect(_refresh_outcome) if not ExamSimulationService.request_failed.is_connected(_on_outcome_request_failed): ExamSimulationService.request_failed.connect(_on_outcome_request_failed) resized.connect(_refresh_outcome_layout) _refresh_outcome_layout() func on_show(_payload: Variant = null) -> void: _reset_qbank_confirmation() var profile := ProfileService.get_profile() var text := "[b]Overall ELO %d[/b]\n%d rated questions\nCurrent streak: %d\nBest streak: %d\nToday: %d/%d questions\n\n[b]Category ELO[/b]\n" % [ QuestionBankService.overall_elo, QuestionBankService.rated_question_count, profile["current_streak"], profile["best_streak"], profile.get("daily_questions_answered", 0), profile.get("daily_goal_questions", 10) ] if QuestionBankService.category_elos.is_empty(): text += "No rated categories yet.\n" else: var categories := QuestionBankService.category_elos.keys() categories.sort() for category in categories: text += "- %s: %d\n" % [category, int(QuestionBankService.category_elos[category])] text += "\n[b]System Accuracy[/b]\n" if profile["system_stats"].is_empty(): text += "No completed questions yet.\n" else: for system_name in profile["system_stats"].keys(): var stats: Dictionary = profile["system_stats"][system_name] var seen := int(stats.get("seen", 0)) var correct := int(stats.get("correct", 0)) var accuracy := int(round(float(correct) / float(max(seen, 1)) * 100.0)) text += "- %s: %d/%d (%d%%)\n" % [system_name, correct, seen, accuracy] text += "\n[b]Recent Blocks[/b]\n" for block in profile["block_history"]: text += "- %s: %s %d/%d\n" % [ block.get("timestamp", ""), block.get("system", ""), block.get("correct", 0), block.get("total", 0) ] profile_text.text = text outcome_status.text = "Loading official outcome..." ExamSimulationService.load_outcome() func _build_outcome_panel(parent: VBoxContainer) -> void: var panel := PanelContainer.new() panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Color("405475"))) parent.add_child(panel) var section := VBoxContainer.new() section.add_theme_constant_override("separation", 10) panel.add_child(section) var heading := Label.new() heading.text = "Official Step 3 Outcome" Style.set_label(heading, 22, Style.TEXT) section.add_child(heading) var note := Label.new() note.text = "After receiving your official result, you may voluntarily submit it. You can edit or delete it at any time; calibration uses de-identified aggregates." note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART Style.set_label(note, 15, Style.MUTED) section.add_child(note) outcome_grid = GridContainer.new() outcome_grid.add_theme_constant_override("h_separation", 10) outcome_grid.add_theme_constant_override("v_separation", 10) section.add_child(outcome_grid) outcome_score = SpinBox.new() outcome_score.min_value = 1 outcome_score.max_value = 300 outcome_score.value = 200 outcome_score.custom_minimum_size = Vector2(120, 44) outcome_score.size_flags_horizontal = Control.SIZE_EXPAND_FILL outcome_grid.add_child(outcome_score) outcome_date = LineEdit.new() outcome_date.placeholder_text = "Exam date: YYYY-MM-DD" outcome_date.custom_minimum_size = Vector2(220, 44) outcome_date.size_flags_horizontal = Control.SIZE_EXPAND_FILL outcome_grid.add_child(outcome_date) outcome_consent = CheckBox.new() outcome_consent.text = "I consent to calibration use" outcome_consent.size_flags_horizontal = Control.SIZE_EXPAND_FILL outcome_grid.add_child(outcome_consent) var actions := HBoxContainer.new() actions.add_theme_constant_override("separation", 8) outcome_grid.add_child(actions) var save_button := Button.new() save_button.text = "Save outcome" Style.style_chip(save_button, Style.CYAN) save_button.pressed.connect(_save_outcome) actions.add_child(save_button) outcome_delete_button = Button.new() outcome_delete_button.text = "Delete outcome" Style.style_chip(outcome_delete_button, Style.RED) outcome_delete_button.pressed.connect(_delete_outcome) actions.add_child(outcome_delete_button) outcome_status = Label.new() outcome_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART Style.set_label(outcome_status, 15, Style.MUTED) section.add_child(outcome_status) func _refresh_outcome_layout() -> void: if outcome_grid != null: outcome_grid.columns = 1 if size.x < 900.0 else 4 static func validate_outcome(score: int, exam_date: String, consent: bool) -> String: if score < 1 or score > 300: return "Enter an official score from 1 through 300." if not consent: return "Consent is required before saving an outcome." var date_pattern := RegEx.new() date_pattern.compile("^\\d{4}-\\d{2}-\\d{2}$") if date_pattern.search(exam_date) == null: return "Enter the exam date as YYYY-MM-DD." return "" func _save_outcome() -> void: var validation_error := validate_outcome(int(outcome_score.value), outcome_date.text.strip_edges(), outcome_consent.button_pressed) if not validation_error.is_empty(): outcome_status.text = validation_error return outcome_status.text = "Saving..." ExamSimulationService.save_outcome(int(outcome_score.value), outcome_date.text.strip_edges()) func _delete_outcome() -> void: outcome_status.text = "Deleting..." ExamSimulationService.delete_outcome() func _refresh_outcome() -> void: if ExamSimulationService.outcome.is_empty(): outcome_score.value = 200 outcome_date.text = "" outcome_consent.button_pressed = false outcome_delete_button.visible = false outcome_status.text = "No official outcome is stored." return outcome_score.value = int(ExamSimulationService.outcome.get("officialScore", 200)) outcome_date.text = str(ExamSimulationService.outcome.get("examDate", "")).left(10) outcome_consent.button_pressed = true outcome_delete_button.visible = true outcome_status.text = "Official outcome saved." func _on_outcome_request_failed(message: String) -> void: if visible: outcome_status.text = message func _on_reset_qbank_pressed() -> void: if not reset_qbank_pending: reset_qbank_pending = true reset_qbank_button.text = "Confirm Reset QBank" Style.style_chip(reset_qbank_button, Style.RED) return ProfileService.reset_qbank_memory() if BlockSessionService.core_status_message == "All available questions cleared.": BlockSessionService.core_status_message = "" _reset_qbank_confirmation() on_show() func _reset_qbank_confirmation() -> void: reset_qbank_pending = false if reset_qbank_button != null: reset_qbank_button.text = "Reset QBank" Style.style_chip(reset_qbank_button, Style.AMBER)