Files
step3/scripts/ui/ccs_review.gd
T

185 lines
12 KiB
GDScript

extends VBoxContainer
const Style := preload("res://scripts/ui/ui_style.gd")
const Brand := preload("res://scripts/ui/medmind_style.gd")
var main: Control
var title: Label
var score_body: RichTextLabel
var criteria_container: VBoxContainer
var timeline_body: RichTextLabel
var references: VBoxContainer
var report_dialog: Control
var report_button: Button
var current_attempt := {}
var status: Label
var starting_case := false
var theme_service
var theme_button: Button
func _ready() -> void:
theme_service = get_node("/root/UIThemeService")
set_anchors_preset(Control.PRESET_FULL_RECT); add_theme_constant_override("separation", 10)
var toolbar := HBoxContainer.new(); toolbar.add_theme_constant_override("separation", 8); add_child(toolbar)
title = Label.new(); title.size_flags_horizontal = Control.SIZE_EXPAND_FILL; Style.set_label(title, 30, Style.TEXT); toolbar.add_child(title)
var smaller := Button.new(); smaller.text = "A-"; smaller.tooltip_text = "Decrease text size"; smaller.pressed.connect(_zoom.bind(-CCSService.TEXT_SCALE_STEP)); toolbar.add_child(smaller)
var larger := Button.new(); larger.text = "A+"; larger.tooltip_text = "Increase text size"; larger.pressed.connect(_zoom.bind(CCSService.TEXT_SCALE_STEP)); toolbar.add_child(larger)
var clear := Button.new(); clear.text = "Clear"; clear.tooltip_text = "Clear highlights"; clear.pressed.connect(_clear_highlights); toolbar.add_child(clear)
report_button = Button.new(); report_button.text = "Report"; report_button.tooltip_text = "Report CCS case"; report_button.pressed.connect(_report); toolbar.add_child(report_button)
theme_button = Button.new(); theme_button.custom_minimum_size = Vector2(44, 44); theme_button.add_theme_font_size_override("font_size", 22); UISoundService.set_button_sound(theme_button, "toggle"); theme_button.pressed.connect(theme_service.toggle_theme); toolbar.add_child(theme_button); _refresh_theme_button()
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)
score_body = _study_label("score"); content.add_child(score_body)
criteria_container = VBoxContainer.new(); criteria_container.add_theme_constant_override("separation", 12); content.add_child(criteria_container)
timeline_body = _study_label("timeline"); timeline_body.custom_minimum_size.y = 260; content.add_child(timeline_body)
references = VBoxContainer.new(); content.add_child(references)
CCSService.attempt_changed.connect(_refresh_loaded_attempt); CCSService.report_submitted.connect(_report_submitted)
report_dialog = preload("res://scripts/ui/ccs_report_dialog.gd").new(); add_child(report_dialog)
var row := HBoxContainer.new(); add_child(row)
var cases := Button.new(); cases.text = "Another Case"; Style.style_primary_button(cases); cases.pressed.connect(_another_case); row.add_child(cases)
var back := Button.new(); back.text = "Back to CCS Cases"; back.pressed.connect(_back_to_cases); row.add_child(back)
status = Label.new(); Style.set_label(status, 14, Style.RED); row.add_child(status)
CCSService.request_failed.connect(_request_failed)
if not theme_service.theme_changed.is_connected(_on_theme_changed): theme_service.theme_changed.connect(_on_theme_changed)
if not CCSService.text_scale_changed.is_connected(_apply_text_scale): CCSService.text_scale_changed.connect(_apply_text_scale)
func _study_label(region: String) -> RichTextLabel:
var label := RichTextLabel.new(); label.bbcode_enabled = true; label.fit_content = true; label.selection_enabled = true; label.set_meta("ccs_region", region); label.gui_input.connect(_text_input.bind(label)); return label
func on_show(payload: Variant = null) -> void:
starting_case = false
if status != null: status.text = ""
if typeof(payload) == TYPE_STRING: CCSService.load_attempt(str(payload)); return
current_attempt = payload if typeof(payload) == TYPE_DICTIONARY else CCSService.attempt
if current_attempt.is_empty(): return
var score: Dictionary = current_attempt.get("score", {})
title.text = "%s: %d%%" % [current_attempt.get("caseTitle", "Case review"), int(round(float(score.get("percent", 0))))]
_render_text(); _render_references(); _apply_text_scale()
func _score_plain() -> String:
var score: Dictionary = current_attempt.get("score", {})
var elapsed_seconds := int(current_attempt.get("elapsedSeconds", 0))
var lines: Array[String] = ["Case result: %d/%d points (%.0f%%)" % [score.get("earnedPoints", 0), score.get("possiblePoints", 0), score.get("percent", 0)], "Time taken: %d min %02d sec" % [floori(float(elapsed_seconds) / 60.0), elapsed_seconds % 60]]
var rating: Dictionary = current_attempt.get("rating", {})
if bool(rating.get("rated", false)):
lines.append("CCS ELO: %d (%+d)" % [rating.get("userElo", 1000), rating.get("userEloDelta", 0)])
else:
lines.append("CCS ELO unchanged: this case was not rating-eligible.")
return "\n".join(lines)
func _timeline_plain() -> String:
var lines: Array[String] = ["Case timeline"]
for entry in current_attempt.get("chart", []): lines.append("T+%d [%s] %s" % [entry.get("simulatedMinute", 0), entry.get("category", "note"), entry.get("text", "")])
return "\n\n".join(lines)
func _render_text() -> void:
var id := str(current_attempt.get("id", ""))
score_body.text = CCSService.highlighted_text(id, "score", _score_plain())
timeline_body.text = CCSService.highlighted_text(id, "timeline", _timeline_plain())
_render_criteria()
func _render_criteria() -> void:
for child in criteria_container.get_children(): child.free()
var index := 0
for criterion in current_attempt.get("score", {}).get("criteria", []):
var criterion_data: Dictionary = criterion
var result_status := str(criterion_data.get("status", "missed"))
var accent := _status_color(result_status)
var panel := PanelContainer.new()
panel.add_theme_stylebox_override("panel", Style.panel_box(Style.SURFACE, accent))
criteria_container.add_child(panel)
var body := VBoxContainer.new(); body.add_theme_constant_override("separation", 7); panel.add_child(body)
var criterion_title := Label.new(); criterion_title.text = str(criterion_data.get("title", criterion_data.get("criterionId", "Criterion"))); criterion_title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART; Style.set_label(criterion_title, 20, Style.TEXT); body.add_child(criterion_title)
var status_label := Label.new(); status_label.text = _status_text(result_status, int(criterion_data.get("points", 0))); Style.set_label(status_label, 16, accent); body.add_child(status_label)
_add_detail(body, "EXPECTED", str(criterion_data.get("expectedAction", "")), "criterion_%d_expected" % index, Style.CYAN)
var observed := str(criterion_data.get("observedAction", "Not performed"))
if criterion_data.get("observedMinute", null) != null: observed += " at minute %d" % criterion_data.get("observedMinute", 0)
_add_detail(body, "OBSERVED", observed, "criterion_%d_observed" % index, accent)
_add_detail(body, "WHY THIS MATTERS", str(criterion_data.get("rationale", "")), "criterion_%d_explanation" % index, Style.AMBER, true)
index += 1
func _add_detail(parent: VBoxContainer, caption: String, value: String, region: String, color: Color, muted_value: bool = false) -> void:
var caption_label := Label.new(); caption_label.text = caption; Style.set_label(caption_label, 13, color); parent.add_child(caption_label)
var value_label := _study_label(region); value_label.text = CCSService.highlighted_text(str(current_attempt.get("id", "")), region, value); value_label.add_theme_color_override("default_color", Style.MUTED if muted_value else Style.TEXT); parent.add_child(value_label)
func _status_color(result_status: String) -> Color:
if result_status == "credited": return Style.LIME
if result_status == "avoided": return Style.AMBER
return Style.RED
func _status_text(result_status: String, points: int) -> String:
if result_status == "credited": return "CREDITED | +%d pts" % points
if result_status == "harmful": return "HARMFUL | %d pts" % points
if result_status == "avoided": return "AVOIDED | 0 pts"
return "MISSED | 0 pts"
func _render_references() -> void:
for child in references.get_children(): child.queue_free()
if current_attempt.get("references", []).is_empty(): return
var heading := Label.new(); heading.text = "References"; Style.set_label(heading, int(round(19.0 * CCSService.text_scale)), Style.TEXT); references.add_child(heading)
for reference in current_attempt.get("references", []):
var link := LinkButton.new(); link.text = str(reference.get("title", reference.get("url", "Reference"))); link.pressed.connect(OS.shell_open.bind(str(reference.get("url", "")))); link.add_theme_font_size_override("font_size", int(round(16.0 * CCSService.text_scale))); references.add_child(link)
func _text_input(event: InputEvent, label: RichTextLabel) -> void:
var mouse := event as InputEventMouseButton
if mouse != null and mouse.button_index == MOUSE_BUTTON_LEFT and not mouse.pressed: call_deferred("_commit_selection", label)
func _commit_selection(label: RichTextLabel) -> void:
if label.get_selected_text().strip_edges().is_empty(): return
var start := label.get_selection_from(); var end := label.get_selection_to()
CCSService.toggle_highlight(str(current_attempt.get("id", "")), str(label.get_meta("ccs_region", "")), mini(start,end), maxi(start,end)); label.deselect(); _render_text()
func _zoom(delta: float) -> void:
CCSService.adjust_text_scale(delta); _apply_text_scale(); _render_references()
func _apply_text_scale() -> void:
var font_size := int(round(18.0 * CCSService.text_scale))
for label in [score_body, timeline_body]: label.add_theme_font_size_override("normal_font_size", font_size); label.add_theme_font_size_override("bold_font_size", font_size)
_scale_criteria(criteria_container)
func _scale_criteria(node: Node) -> void:
if node is RichTextLabel:
node.add_theme_font_size_override("normal_font_size", int(round(17.0 * CCSService.text_scale)))
elif node is Label:
if not node.has_meta("ccs_base_font_size"): node.set_meta("ccs_base_font_size", node.get_theme_font_size("font_size"))
node.add_theme_font_size_override("font_size", int(round(float(node.get_meta("ccs_base_font_size")) * CCSService.text_scale)))
for child in node.get_children(): _scale_criteria(child)
func _clear_highlights() -> void:
CCSService.clear_highlights(str(current_attempt.get("id", ""))); _render_text()
func _report() -> void: report_dialog.show_for(str(current_attempt.get("id", "")))
func _report_submitted(_id: String) -> void: Style.style_chip(report_button, Style.RED); report_button.tooltip_text = "Report submitted; report another issue"
func _refresh_loaded_attempt() -> void:
if not visible: return
if starting_case and str(CCSService.attempt.get("state", "")) in ["active", "final_orders"]:
starting_case = false; main.show_screen("ccs_player"); return
if str(CCSService.attempt.get("state", "")) == "completed": on_show(CCSService.attempt)
func _another_case() -> void:
if CCSService.launch_origin == "ccs_cases_2":
main.show_screen("ccs_cases_2")
return
if starting_case: return
starting_case = true
status.text = "Loading a random timed case..."
CCSService.create_attempt("", "timed")
func _back_to_cases() -> void:
main.show_screen("ccs_cases_2" if CCSService.launch_origin == "ccs_cases_2" else "ccs_hub")
func _request_failed(message: String) -> void:
if not visible or not starting_case: return
starting_case = false
status.text = message
func _on_theme_changed(_is_dark_mode: bool) -> void:
_refresh_theme_button()
if visible: _render_text()
func _refresh_theme_button() -> void:
if theme_button == null: return
Brand.set_theme_toggle_icon(theme_button, theme_service.is_dark_mode())
theme_button.tooltip_text = "Switch to light mode" if theme_service.is_dark_mode() else "Switch to dark mode"
Style.style_chip(theme_button, Style.CYAN)