Restyle exam simulation results to match the RZ design system

The results screen was still the legacy VBoxContainer-of-plain-Labels
placeholder from before the Brand/medmind_style migration - no cards,
icons, or colored bars. Rebuild it with a score hero card (medallion
badge, predicted-range pill), three stat cards (correct, pass
probability, previously seen), and a two-column color-coded
content-area breakdown with progress bars, mirroring patterns already
used in progress_profile.gd and simulation_history.gd. No backend or
data changes - every field was already being fetched, just rendered
as bare text.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
hz
2026-07-30 16:15:14 -05:00
co-authored by Claude Sonnet 5
parent 778da32e73
commit be144b272a
2 changed files with 268 additions and 55 deletions
+1
View File
@@ -495,6 +495,7 @@ func _update_header_title(screen_name: String) -> void:
"selection": "Question Bank",
"dynamic_questions": "Dynamic Questions",
"simulation_history": "Exam Simulations",
"simulation_results": "Exam Simulations",
"ccs_hub": "CCS Cases",
"flashcards_hub": "Flashcards",
"flashcards_study": "Flashcards",
+267 -55
View File
@@ -1,85 +1,245 @@
extends VBoxContainer
const Style := preload("res://scripts/ui/ui_style.gd")
const Brand := preload("res://scripts/ui/medmind_style.gd")
const CORRECT_ICON := preload("res://assets/icons/lucide/circle-check.svg")
const PROBABILITY_ICON := preload("res://assets/icons/lucide/activity.svg")
const SEEN_ICON := preload("res://assets/icons/lucide/eye.svg")
const CATEGORY_ICON := preload("res://assets/icons/lucide/chart-no-axes-column-increasing.svg")
const REVIEW_ICON := preload("res://assets/icons/lucide/clipboard-list.svg")
const CATEGORY_PALETTE := [Brand.ROYAL_BLUE, Brand.SUCCESS, Brand.AMBER, Brand.MAGENTA, Brand.SKY_BLUE]
var main: Control
var score_label: Label
var summary_label: Label
var category_list: VBoxContainer
var content: VBoxContainer
var current_simulation := {}
var return_screen := "selection"
var done_button: Button
func _ready() -> void:
set_anchors_preset(Control.PRESET_FULL_RECT)
add_theme_constant_override("separation", 12)
var eyebrow := Label.new()
eyebrow.text = "STEP 3 DAY 1 SIMULATION COMPLETE"
Style.set_label(eyebrow, 14, Style.CYAN)
add_child(eyebrow)
score_label = Label.new()
Style.set_label(score_label, 36, Style.LIME)
add_child(score_label)
summary_label = Label.new()
summary_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(summary_label, 17, Style.TEXT)
add_child(summary_label)
add_theme_constant_override("separation", 0)
var scroll := ScrollContainer.new()
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
add_child(scroll)
var content := VBoxContainer.new()
content = VBoxContainer.new()
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
content.add_theme_constant_override("separation", 10)
content.add_theme_constant_override("separation", 20)
scroll.add_child(content)
var category_heading := Label.new()
category_heading.text = "Performance by content area"
Style.set_label(category_heading, 22, Style.TEXT)
content.add_child(category_heading)
category_list = VBoxContainer.new()
content.add_child(category_list)
var review_button := Button.new()
review_button.text = "Review Questions"
review_button.custom_minimum_size = Vector2(240, 50)
review_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
Style.style_primary_button(review_button)
review_button.pressed.connect(func(): main.show_screen("simulation_review", {"simulation": current_simulation, "return_screen": return_screen}))
content.add_child(review_button)
done_button = Button.new()
done_button.text = "Return to study modes"
done_button.custom_minimum_size = Vector2(240, 50)
done_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
Style.style_primary_button(done_button)
done_button.pressed.connect(_return_from_results)
add_child(done_button)
UIThemeService.theme_changed.connect(func(_dark: bool): _render())
func on_show(payload: Variant = null) -> void:
var wrapper: Dictionary = payload if typeof(payload) == TYPE_DICTIONARY else {}
var simulation: Dictionary = wrapper.get("simulation", wrapper) if not wrapper.is_empty() else ExamSimulationService.simulation
return_screen = str(wrapper.get("return_screen", "selection"))
done_button.text = "Return to simulation history" if return_screen == "simulation_history" else "Return to study modes"
current_simulation = simulation
var score: Dictionary = simulation.get("score", {})
_render()
func _render() -> void:
_clear(content)
var dark := UIThemeService.is_dark_mode()
var score: Dictionary = current_simulation.get("score", {})
content.add_child(_build_hero_card(dark, score))
content.add_child(_build_stats_row(dark, score))
content.add_child(_build_disclaimer(dark, score))
content.add_child(_build_category_card(dark, score))
content.add_child(_build_actions(dark))
func _build_hero_card(dark: bool, score: Dictionary) -> PanelContainer:
var predicted := int(score.get("predictedScore", 0))
var low := int(score.get("rangeLow", 0))
var high := int(score.get("rangeHigh", 0))
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 24)
var copy := VBoxContainer.new()
copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
copy.add_theme_constant_override("separation", 10)
row.add_child(copy)
copy.add_child(_label("STEP 3 DAY 1 SIMULATION COMPLETE", 13, Brand.SKY_BLUE, true))
var headline := _label("Provisional Step 3 estimate: %d" % predicted, 26, Brand.text_color(dark), true)
headline.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
copy.add_child(headline)
var badge := _label("%d-%d Predicted score range" % [low, high], 13, Brand.text_color(dark))
badge.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
badge.add_theme_stylebox_override("normal", Brand.badge_box(dark, Brand.ROYAL_BLUE))
copy.add_child(badge)
row.add_child(_score_medallion(dark, predicted, Brand.ROYAL_BLUE))
return _card(dark, row, Brand.ROYAL_BLUE, true)
func _score_medallion(dark: bool, score: int, accent: Color) -> Control:
var wrap := Control.new()
wrap.custom_minimum_size = Vector2(120, 120)
wrap.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
wrap.size_flags_vertical = Control.SIZE_SHRINK_CENTER
var panel := PanelContainer.new()
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, accent, 60, true))
wrap.add_child(panel)
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
panel.add_child(center)
center.add_child(_label(str(score), 34, Brand.DARK_TEXT, true))
return wrap
func _build_stats_row(dark: bool, score: Dictionary) -> HBoxContainer:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 16)
var correct := int(score.get("correct", 0))
var total := int(score.get("total", 0))
var probability := int(round(float(score.get("passProbability", 0.0)) * 100.0))
var seen := int(score.get("seenCount", 0))
row.add_child(_stat_card(dark, CORRECT_ICON, Brand.ROYAL_BLUE, "%d / %d" % [correct, total], "Correct"))
row.add_child(_stat_card(dark, PROBABILITY_ICON, Brand.SUCCESS, "%d%%" % probability, "Heuristic probability of reaching 200+"))
row.add_child(_stat_card(dark, SEEN_ICON, Brand.SKY_BLUE, str(seen), "Previously seen questions"))
return row
func _stat_card(dark: bool, icon: Texture2D, accent: Color, value: String, caption: String) -> PanelContainer:
var inner := HBoxContainer.new()
inner.add_theme_constant_override("separation", 12)
var icon_panel := PanelContainer.new()
icon_panel.custom_minimum_size = Vector2(44, 44)
icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER
icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, accent, 22, true))
var icon_rect := TextureRect.new()
icon_rect.texture = icon
icon_rect.custom_minimum_size = Vector2(20, 20)
icon_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon_rect.modulate = Brand.DARK_TEXT
icon_panel.add_child(icon_rect)
inner.add_child(icon_panel)
var copy := VBoxContainer.new()
copy.add_theme_constant_override("separation", 2)
copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
copy.add_child(_label(value, 20, Brand.text_color(dark), true))
var caption_label := _label(caption, 12, Brand.muted_color(dark))
caption_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
copy.add_child(caption_label)
inner.add_child(copy)
return _card(dark, inner)
func _build_disclaimer(dark: bool, score: Dictionary) -> Label:
var raw_accuracy := float(score.get("rawAccuracy", 0.0)) * 100.0
var adjusted_accuracy := float(score.get("difficultyAdjustedAccuracy", 0.0)) * 100.0
score_label.text = "Provisional Step 3 estimate: %d (%d-%d)" % [predicted, low, high]
summary_label.text = "%d/%d correct (%.1f%%) | Difficulty-adjusted accuracy: %.1f%%\nHeuristic probability of provisional score >=200: %d%% | Previously seen questions: %d\nThis transparent, unvalidated educational heuristic is not an official USMLE or NBME score and excludes CCS performance." % [
int(score.get("correct", 0)), int(score.get("total", 0)), raw_accuracy, adjusted_accuracy,
probability, int(score.get("seenCount", 0))]
_clear(category_list)
for category in score.get("categories", {}):
var value: Dictionary = score["categories"][category]
var label := Label.new()
label.text = "%s: %d/%d" % [category, int(value.get("correct", 0)), int(value.get("total", 0))]
Style.set_label(label, 16, Style.TEXT)
category_list.add_child(label)
var label := _label(
"%.1f%% raw accuracy · %.1f%% difficulty-adjusted accuracy\nThis transparent, unvalidated educational heuristic is not an official USMLE or NBME score and excludes CCS performance." % [raw_accuracy, adjusted_accuracy],
12, Brand.muted_color(dark))
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
return label
func _build_category_card(dark: bool, score: Dictionary) -> PanelContainer:
var column := VBoxContainer.new()
column.add_theme_constant_override("separation", 14)
var heading := HBoxContainer.new()
heading.add_theme_constant_override("separation", 8)
var heading_icon := TextureRect.new()
heading_icon.texture = CATEGORY_ICON
heading_icon.custom_minimum_size = Vector2(20, 20)
heading_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
heading_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
heading_icon.modulate = Brand.ROYAL_BLUE
heading.add_child(heading_icon)
heading.add_child(_label("Performance by content area", 18, Brand.text_color(dark), true))
column.add_child(heading)
var grid := GridContainer.new()
grid.columns = 2
grid.add_theme_constant_override("h_separation", 32)
grid.add_theme_constant_override("v_separation", 14)
var categories: Dictionary = score.get("categories", {})
var index := 0
for category_name in categories:
var value: Dictionary = categories[category_name]
grid.add_child(_category_row(dark, str(category_name), value, index))
index += 1
if categories.is_empty():
column.add_child(_label("No content-area breakdown is available for this simulation.", 13, Brand.muted_color(dark)))
else:
column.add_child(grid)
return _card(dark, column)
func _category_row(dark: bool, category_name: String, value: Dictionary, index: int) -> HBoxContainer:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 10)
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var accent: Color = CATEGORY_PALETTE[index % CATEGORY_PALETTE.size()]
var dot := PanelContainer.new()
dot.custom_minimum_size = Vector2(10, 10)
dot.size_flags_vertical = Control.SIZE_SHRINK_CENTER
dot.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, accent, 5, true))
row.add_child(dot)
var correct := int(value.get("correct", 0))
var total := int(value.get("total", 0))
var percent := int(round(100.0 * float(correct) / float(total))) if total > 0 else 0
var title := _label(category_name, 13, Brand.text_color(dark))
title.custom_minimum_size.x = 200
title.clip_text = true
title.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
row.add_child(title)
var bar := ProgressBar.new()
bar.show_percentage = false
bar.min_value = 0.0
bar.max_value = 100.0
bar.value = percent
bar.custom_minimum_size = Vector2(90, 7)
bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL
bar.size_flags_vertical = Control.SIZE_SHRINK_CENTER
_style_progress(bar, accent, dark)
row.add_child(bar)
var detail := _label("%d%% %d/%d" % [percent, correct, total], 12, accent if dark else accent.darkened(0.15))
detail.custom_minimum_size.x = 76
detail.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
row.add_child(detail)
return row
func _build_actions(dark: bool) -> HBoxContainer:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 12)
var review_button := Button.new()
review_button.text = "Review Questions"
review_button.icon = REVIEW_ICON
review_button.custom_minimum_size = Vector2(220, 52)
review_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
_style_gradient_button(review_button)
review_button.pressed.connect(func(): main.show_screen("simulation_review", {"simulation": current_simulation, "return_screen": return_screen}))
row.add_child(review_button)
var back_button := Button.new()
back_button.text = "Return to simulation history" if return_screen == "simulation_history" else "Return to study modes"
back_button.custom_minimum_size = Vector2(220, 52)
back_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
_style_outline_button(back_button, dark)
back_button.pressed.connect(_return_from_results)
row.add_child(back_button)
return row
func _return_from_results() -> void:
@@ -89,6 +249,58 @@ func _return_from_results() -> void:
main.show_screen("selection", "step3")
func _card(dark: bool, child: Control, accent: Color = Brand.ROYAL_BLUE, active: bool = false) -> PanelContainer:
var panel := PanelContainer.new()
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var box := Brand.content_card_box(dark, active, accent)
box.content_margin_left = 20
box.content_margin_right = 20
box.content_margin_top = 18
box.content_margin_bottom = 18
panel.add_theme_stylebox_override("panel", box)
panel.add_child(child)
return panel
func _label(text: String, font_size: int, color: Color, _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", color)
return label
func _style_progress(bar: ProgressBar, accent: Color, dark: bool) -> void:
var background := StyleBoxFlat.new()
background.bg_color = Color("26324a") if dark else Color("e2e8f0")
background.set_corner_radius_all(4)
var fill := StyleBoxFlat.new()
fill.bg_color = accent
fill.set_corner_radius_all(4)
bar.add_theme_stylebox_override("background", background)
bar.add_theme_stylebox_override("fill", fill)
func _style_gradient_button(button: Button) -> void:
Brand.apply_font(button)
button.add_theme_font_size_override("font_size", 15)
button.add_theme_stylebox_override("normal", Brand.gradient_button_box())
button.add_theme_stylebox_override("hover", Brand.gradient_button_box())
button.add_theme_stylebox_override("pressed", Brand.gradient_button_box())
button.add_theme_color_override("font_color", Color.WHITE)
button.add_theme_color_override("font_hover_color", Color.WHITE)
func _style_outline_button(button: Button, dark: bool) -> void:
Brand.apply_font(button)
button.add_theme_font_size_override("font_size", 14)
for state in ["normal", "hover", "pressed"]:
button.add_theme_stylebox_override(state, Brand.secondary_button_box(dark, state != "normal"))
button.add_theme_color_override("font_color", Brand.text_color(dark))
func _clear(container: Control) -> void:
for child in container.get_children():
child.queue_free()
container.remove_child(child)
child.free()