Files
step3/scripts/ui/progress_profile.gd
T

267 lines
9.3 KiB
GDScript

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
var ccs_rating_label: Label
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)
ccs_rating_label = Label.new()
Style.set_label(ccs_rating_label, 20, Style.CYAN)
content.add_child(ccs_rating_label)
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)
if not CCSService.rating_changed.is_connected(_refresh_ccs_rating):
CCSService.rating_changed.connect(_refresh_ccs_rating)
resized.connect(_refresh_outcome_layout)
_refresh_outcome_layout()
func on_show(_payload: Variant = null) -> void:
_reset_qbank_confirmation()
_refresh_ccs_rating()
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
CCSService.load_rating()
outcome_status.text = "Loading official outcome..."
ExamSimulationService.load_outcome()
func _refresh_ccs_rating() -> void:
ccs_rating_label.text = "CCS ELO %d | %d rated cases" % [CCSService.rating.get("elo", 1000), CCSService.rating.get("ratedCaseCount", 0)]
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)