Merge main into production
This commit is contained in:
@@ -256,18 +256,26 @@ func restore_simulation_responses(saved_responses: Array) -> void:
|
||||
"is_correct": false,
|
||||
"eliminated_choice_ids": response.get("eliminatedChoiceIds", []).duplicate(),
|
||||
"flagged": bool(response.get("flagged", false)),
|
||||
"elapsed_seconds": int(response.get("elapsedSeconds", 0))
|
||||
"elapsed_seconds": int(response.get("elapsedSeconds", 0)),
|
||||
"is_submitted": _simulation_response_is_submitted(response)
|
||||
}
|
||||
break
|
||||
|
||||
|
||||
func _simulation_response_is_submitted(response: Dictionary) -> bool:
|
||||
var submitted: Variant = response.get("isSubmitted", null)
|
||||
if submitted == null:
|
||||
return not str(response.get("choiceId", "")).is_empty()
|
||||
return bool(submitted)
|
||||
|
||||
|
||||
func get_current_question() -> Dictionary:
|
||||
if active_questions.is_empty() or current_index < 0 or current_index >= active_questions.size():
|
||||
return {}
|
||||
return active_questions[current_index]
|
||||
|
||||
|
||||
func answer_current(choice_id: String, eliminated: Array, flagged: bool) -> Dictionary:
|
||||
func answer_current(choice_id: String, eliminated: Array, flagged: bool, is_submitted := false) -> Dictionary:
|
||||
var question := get_current_question()
|
||||
if question.is_empty():
|
||||
return {}
|
||||
@@ -284,6 +292,7 @@ func answer_current(choice_id: String, eliminated: Array, flagged: bool) -> Dict
|
||||
answers[current_index] = answer
|
||||
if session_kind == "simulation":
|
||||
answer["is_correct"] = false
|
||||
answer["is_submitted"] = is_submitted
|
||||
answers[current_index] = answer
|
||||
session_changed.emit()
|
||||
return answer
|
||||
|
||||
@@ -24,6 +24,7 @@ var pending_action := ""
|
||||
var request_queue: Array = []
|
||||
var request_access_token := ""
|
||||
var last_failed_action := ""
|
||||
var submit_recovery_attempts := 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -69,7 +70,7 @@ func start_next_block() -> void:
|
||||
_send("start", HTTPClient.METHOD_POST, "/%s/blocks/start" % str(simulation.get("id", "")))
|
||||
|
||||
|
||||
func save_response(question_id: String, choice_id: String, eliminated: Array, flagged: bool, elapsed_seconds: int) -> void:
|
||||
func save_response(question_id: String, choice_id: String, eliminated: Array, flagged: bool, elapsed_seconds: int, is_submitted := false) -> void:
|
||||
if simulation.is_empty():
|
||||
return
|
||||
_send("save", HTTPClient.METHOD_PUT, "/%s/responses" % str(simulation.get("id", "")), {
|
||||
@@ -77,13 +78,15 @@ func save_response(question_id: String, choice_id: String, eliminated: Array, fl
|
||||
"choiceId": choice_id,
|
||||
"eliminatedChoiceIds": eliminated,
|
||||
"flagged": flagged,
|
||||
"elapsedSeconds": elapsed_seconds
|
||||
"elapsedSeconds": elapsed_seconds,
|
||||
"isSubmitted": is_submitted
|
||||
})
|
||||
|
||||
|
||||
func submit_block() -> void:
|
||||
if simulation.is_empty():
|
||||
return
|
||||
submit_recovery_attempts = 0
|
||||
_send("submit", HTTPClient.METHOD_POST, "/%s/blocks/submit" % str(simulation.get("id", "")))
|
||||
|
||||
|
||||
@@ -154,6 +157,15 @@ func _on_request_completed(result: int, response_code: int, _headers: PackedStri
|
||||
if response_code == 401:
|
||||
AuthState.handle_unauthorized(request_access_token)
|
||||
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||
if _is_stale_submit_failure(action):
|
||||
return
|
||||
if _should_reconcile_submit_failure(action, response_code):
|
||||
submit_recovery_attempts += 1
|
||||
_send(
|
||||
"submit_reconcile",
|
||||
HTTPClient.METHOD_GET,
|
||||
"/%s" % str(simulation.get("id", "")).uri_encode())
|
||||
return
|
||||
var server_message := body.get_string_from_utf8().strip_edges()
|
||||
var error_parsed: Variant = JSON.parse_string(server_message)
|
||||
if typeof(error_parsed) == TYPE_DICTIONARY:
|
||||
@@ -203,6 +215,25 @@ func _on_request_completed(result: int, response_code: int, _headers: PackedStri
|
||||
return
|
||||
simulation = _normalize_simulation(parsed)
|
||||
state_changed.emit()
|
||||
if action == "submit":
|
||||
submit_recovery_attempts = 0
|
||||
elif action == "submit_reconcile":
|
||||
if str(simulation.get("state", "")) == "block_active":
|
||||
_send("submit", HTTPClient.METHOD_POST, "/%s/blocks/submit" % str(simulation.get("id", "")))
|
||||
else:
|
||||
submit_recovery_attempts = 0
|
||||
|
||||
|
||||
func _is_stale_submit_failure(action: String) -> bool:
|
||||
return action == "submit" and str(simulation.get("state", "")) in ["between_blocks", "completed"]
|
||||
|
||||
|
||||
func _should_reconcile_submit_failure(action: String, response_code: int) -> bool:
|
||||
return (
|
||||
action == "submit" and
|
||||
response_code == 409 and
|
||||
submit_recovery_attempts == 0 and
|
||||
str(simulation.get("state", "")) == "block_active")
|
||||
|
||||
|
||||
func _process_request_queue() -> void:
|
||||
|
||||
@@ -297,7 +297,7 @@ func _build_performance_summary(dark: bool) -> void:
|
||||
if combined != null:
|
||||
prediction_value = "%d" % int(combined)
|
||||
prediction_detail = "Range %d-%d" % [int(CCSService.prediction.get("rangeLow", 0)), int(CCSService.prediction.get("rangeHigh", 0))]
|
||||
_add_metric(row, "Combined Prediction", prediction_value, prediction_detail, dark)
|
||||
_add_metric(row, "Provisional Combined Estimate", prediction_value, prediction_detail, dark)
|
||||
|
||||
|
||||
func _add_metric(parent: BoxContainer, caption: String, value: String, detail: String, dark: bool) -> void:
|
||||
|
||||
@@ -94,7 +94,7 @@ func _build() -> void:
|
||||
readiness_segment_bar = DashboardSegmentBar.new()
|
||||
readiness_segment_bar.custom_minimum_size = Vector2(0, 10)
|
||||
_add_stat_card("readiness", "Exam Readiness", "chart-no-axes-column-increasing", Brand.PURPLE, readiness_segment_bar,
|
||||
"Predicted pass probability from your most recent completed exam simulation.")
|
||||
"Heuristic pass probability from your most recent completed exam simulation.")
|
||||
|
||||
accuracy_progress_bar = ProgressBar.new()
|
||||
accuracy_progress_bar.show_percentage = false
|
||||
@@ -254,8 +254,8 @@ func _render_readiness_card(data: Dictionary) -> void:
|
||||
var label := str(label_value)
|
||||
var pass_percent := int(data.get("examReadinessProjectedPassPercent", 0))
|
||||
var status_color := Brand.SUCCESS if label == "High" else (Brand.AMBER if label == "Medium" else Brand.MAGENTA)
|
||||
var caption := "You're on track!" if label != "Low" else "Keep practicing"
|
||||
_set_stat_card("readiness", label, "Projected pass: %d%%" % pass_percent, status_color, caption)
|
||||
var caption := "Provisional estimate" if label != "Low" else "Keep practicing"
|
||||
_set_stat_card("readiness", label, "Heuristic pass: %d%%" % pass_percent, status_color, caption)
|
||||
(metric_values.get("readiness") as Label).add_theme_color_override("font_color", status_color)
|
||||
readiness_segment_bar.set_progress(int(round(pass_percent / 100.0 * 4.0)), 4, status_color, dark)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -562,7 +562,10 @@ func _ready() -> void:
|
||||
|
||||
var button := Button.new()
|
||||
button.custom_minimum_size = Vector2(0, 68)
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
button.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
button.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING
|
||||
Style.style_answer_button(button)
|
||||
UISoundService.set_button_sound(button, "select")
|
||||
_set_scaled_button_font(button, 17)
|
||||
@@ -672,6 +675,7 @@ func _ready() -> void:
|
||||
|
||||
actions = HBoxContainer.new()
|
||||
actions.add_theme_constant_override("separation", 16)
|
||||
actions.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
card_content.add_child(actions)
|
||||
|
||||
skip_button = Button.new()
|
||||
@@ -701,10 +705,6 @@ func _ready() -> void:
|
||||
bottom_progress_bar.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
bottom_progress_group.add_child(bottom_progress_bar)
|
||||
|
||||
# The timer readout still runs (BlockSessionService pacing/scoring depends on
|
||||
# it, and _update_timer_bar keeps its text current), it's just no longer
|
||||
# parented into the bottom bar - the caption + digits and the "Ctrl+Enter"
|
||||
# hint were clutter next to the Submit button.
|
||||
timer_caption_label = Label.new()
|
||||
timer_caption_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
_set_scaled_label(timer_caption_label, 12, Brand.SLATE)
|
||||
@@ -713,6 +713,7 @@ func _ready() -> void:
|
||||
next_button.text = "Submit Block" if BlockSessionService.session_kind == "simulation" and BlockSessionService.current_index + 1 == BlockSessionService.active_questions.size() else "Next" if BlockSessionService.session_kind == "simulation" else "Submit"
|
||||
next_button.tooltip_text = ""
|
||||
next_button.custom_minimum_size = Vector2(140, 48)
|
||||
next_button.size_flags_horizontal = Control.SIZE_SHRINK_END
|
||||
Style.style_primary_button(next_button)
|
||||
next_button.pressed.connect(_submit_or_next)
|
||||
actions.add_child(next_button)
|
||||
@@ -720,6 +721,8 @@ func _ready() -> void:
|
||||
timer_footer = HBoxContainer.new()
|
||||
timer_footer.add_theme_constant_override("separation", 12)
|
||||
card_content.add_child(timer_footer)
|
||||
timer_footer.add_child(timer_caption_label)
|
||||
timer_footer.add_child(timer_label)
|
||||
timer_bar = ProgressBar.new()
|
||||
timer_bar.show_percentage = false
|
||||
timer_bar.min_value = 0.0
|
||||
@@ -1160,19 +1163,22 @@ func on_hide() -> void:
|
||||
slot_machine_panel.close_panel()
|
||||
|
||||
|
||||
func _save_simulation_current_response() -> void:
|
||||
func _save_simulation_current_response(is_submitted := false) -> void:
|
||||
if BlockSessionService.session_kind != "simulation":
|
||||
return
|
||||
if bool(current_answer.get("is_submitted", false)) and not is_submitted:
|
||||
return
|
||||
var question := BlockSessionService.get_current_question()
|
||||
if question.is_empty():
|
||||
return
|
||||
current_answer = BlockSessionService.answer_current(selected_choice, eliminated, flagged)
|
||||
current_answer = BlockSessionService.answer_current(selected_choice, eliminated, flagged, is_submitted)
|
||||
ExamSimulationService.save_response(
|
||||
str(question.get("id", "")),
|
||||
selected_choice,
|
||||
eliminated,
|
||||
flagged,
|
||||
int(BlockSessionService.get_current_elapsed_seconds()))
|
||||
int(BlockSessionService.get_current_elapsed_seconds()),
|
||||
is_submitted)
|
||||
|
||||
|
||||
func _on_simulation_state_changed() -> void:
|
||||
@@ -1192,6 +1198,9 @@ func _on_simulation_state_changed() -> void:
|
||||
func _on_simulation_request_failed(message: String) -> void:
|
||||
if BlockSessionService.session_kind != "simulation":
|
||||
return
|
||||
if str(ExamSimulationService.simulation.get("state", "")) in ["between_blocks", "completed"]:
|
||||
simulation_submit_pending = false
|
||||
return
|
||||
simulation_submit_pending = false
|
||||
if ExamSimulationService.last_failed_action != "submit":
|
||||
# A per-question autosave failing shouldn't interrupt a timed exam with a
|
||||
@@ -1582,7 +1591,7 @@ func _submit_or_next() -> void:
|
||||
if BlockSessionService.session_kind == "simulation":
|
||||
if selected_choice == "":
|
||||
return
|
||||
_save_simulation_current_response()
|
||||
_save_simulation_current_response(true)
|
||||
var pending_index := _next_pending_simulation_index()
|
||||
if pending_index >= 0:
|
||||
BlockSessionService.move_to(pending_index)
|
||||
@@ -3385,7 +3394,9 @@ func _apply_saved_choice_state() -> void:
|
||||
|
||||
|
||||
func _is_correct_answer_locked() -> bool:
|
||||
return bool(current_answer.get("is_correct", false))
|
||||
return bool(current_answer.get("is_correct", false)) or (
|
||||
BlockSessionService.session_kind == "simulation" and
|
||||
bool(current_answer.get("is_submitted", false)))
|
||||
|
||||
|
||||
func _refresh_submit_state() -> void:
|
||||
|
||||
@@ -1,91 +1,283 @@
|
||||
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())
|
||||
|
||||
|
||||
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))
|
||||
score_label.text = "Predicted Step 3 score: %d (%d-%d)" % [predicted, low, high]
|
||||
summary_label.text = "%d/%d correct | Estimated probability of reaching 200: %d%% | Previously seen questions: %d\nThis provisional educational estimate is not an official USMLE or NBME score and excludes CCS performance." % [
|
||||
int(score.get("correct", 0)), int(score.get("total", 0)), 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 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 _return_from_results() -> void:
|
||||
if return_screen == "simulation_history":
|
||||
main.show_screen("simulation_history", "preserve")
|
||||
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
|
||||
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:
|
||||
main.show_screen("selection", "step3")
|
||||
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() -> 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)
|
||||
return row
|
||||
|
||||
|
||||
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 _clear(container: Control) -> void:
|
||||
for child in container.get_children():
|
||||
child.queue_free()
|
||||
container.remove_child(child)
|
||||
child.free()
|
||||
|
||||
Reference in New Issue
Block a user