Merge pull request 'Main' (#16) from main into production
Reviewed-on: admin/step3#16
This commit is contained in:
@@ -4,6 +4,7 @@ const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
const Brand := preload("res://scripts/ui/medmind_style.gd")
|
||||
const FlashcardPrefillDialog := preload("res://scripts/ui/flashcard_prefill_dialog.gd")
|
||||
const CalculatorPanel := preload("res://scripts/ui/calculator_panel.gd")
|
||||
const SlotMachinePanel := preload("res://scripts/ui/rz_slot_machine_screen.gd")
|
||||
const CreditsBadge := preload("res://scripts/ui/credits_badge.gd")
|
||||
const CLEAR_HIGHLIGHT_DARK_ICON := preload("res://assets/icons/medmind/clear-highlight-dark.svg")
|
||||
const CLEAR_HIGHLIGHT_LIGHT_ICON := preload("res://assets/icons/medmind/clear-highlight-light.svg")
|
||||
@@ -60,6 +61,8 @@ var stem_plain_text := ""
|
||||
var explanation_plain_text := ""
|
||||
var highlight_drag_source := ""
|
||||
var timer_color_state := ""
|
||||
var last_timer_bar_value := -1.0
|
||||
var last_timer_display_seconds := -1
|
||||
var pending_correct_advance := false
|
||||
var question_text_scale := 1.0
|
||||
var presented_choices: Array = []
|
||||
@@ -92,6 +95,7 @@ var text_smaller_button: Button
|
||||
var text_larger_button: Button
|
||||
var labs_button: Button
|
||||
var calculator_button: Button
|
||||
var slot_machine_button: Button
|
||||
var exit_button: Button
|
||||
var clear_highlight_button: Button
|
||||
var flag_button: Button
|
||||
@@ -125,6 +129,7 @@ var labs_overlay: Control
|
||||
var labs_panel: PanelContainer
|
||||
var labs_content: VBoxContainer
|
||||
var calculator_panel
|
||||
var slot_machine_panel
|
||||
var report_overlay: Control
|
||||
var report_panel: PanelContainer
|
||||
var report_form_content: VBoxContainer
|
||||
@@ -345,6 +350,18 @@ func _ready() -> void:
|
||||
UISoundService.set_button_sound(calculator_button, "open")
|
||||
calculator_button.pressed.connect(_toggle_calculator)
|
||||
|
||||
slot_machine_button = Button.new()
|
||||
slot_machine_button.name = "SlotMachineButton"
|
||||
slot_machine_button.tooltip_text = "Open RZ Slot Machine"
|
||||
slot_machine_button.icon = preload("res://assets/icons/lucide/gem.svg")
|
||||
slot_machine_button.custom_minimum_size = Vector2(44, 44)
|
||||
slot_machine_button.add_theme_constant_override("icon_max_width", 22)
|
||||
slot_machine_button.alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
slot_machine_button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
slot_machine_button.focus_mode = Control.FOCUS_NONE
|
||||
UISoundService.set_button_sound(slot_machine_button, "open")
|
||||
slot_machine_button.pressed.connect(_toggle_slot_machine)
|
||||
|
||||
labs_button = Button.new()
|
||||
labs_button.tooltip_text = "Review lab results"
|
||||
labs_button.custom_minimum_size = Vector2(44, 44)
|
||||
@@ -430,6 +447,7 @@ func _ready() -> void:
|
||||
question_toolbar.add_child(text_smaller_button)
|
||||
question_toolbar.add_child(text_larger_button)
|
||||
question_toolbar.add_child(calculator_button)
|
||||
question_toolbar.add_child(slot_machine_button)
|
||||
question_toolbar.add_child(labs_button)
|
||||
question_toolbar.add_child(clear_highlight_button)
|
||||
question_toolbar.add_child(flag_button)
|
||||
@@ -723,6 +741,9 @@ func _ready() -> void:
|
||||
_build_labs_modal()
|
||||
calculator_panel = CalculatorPanel.new()
|
||||
add_child(calculator_panel)
|
||||
slot_machine_panel = SlotMachinePanel.new()
|
||||
slot_machine_panel.main = main
|
||||
add_child(slot_machine_panel)
|
||||
_build_report_modal()
|
||||
_build_media_viewer()
|
||||
resized.connect(_fit_modal_panels)
|
||||
@@ -808,10 +829,10 @@ func _refresh_responsive_layout() -> void:
|
||||
header_toolbar_host.reparent(target_parent)
|
||||
header_toolbar_host.size_flags_horizontal = Control.SIZE_EXPAND_FILL if compact else Control.SIZE_SHRINK_END
|
||||
question_toolbar.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
# 12 bare 44px icon buttons + the wider 92px Feedback button + the ~194px
|
||||
# rank/ELO + points badge, with 8px gaps between all 14 - anything less
|
||||
# 13 bare 44px icon buttons + the wider 92px Feedback button + the ~194px
|
||||
# rank/ELO + points badge, with 8px gaps between all 15 - anything less
|
||||
# than this wraps onto a second row.
|
||||
header_toolbar_host.custom_minimum_size.x = 0.0 if compact else 960.0
|
||||
header_toolbar_host.custom_minimum_size.x = 0.0 if compact else 1020.0
|
||||
if size.x < 560.0:
|
||||
nav_palette.columns = 5
|
||||
else:
|
||||
@@ -1067,7 +1088,7 @@ func _apply_question_theme() -> void:
|
||||
var navigator_heading := navigator_popup.find_child("NavigatorHeading", true, false) as Label
|
||||
if navigator_heading != null:
|
||||
navigator_heading.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
for control in [exit_button, text_smaller_button, text_larger_button, calculator_button, bookmark_button, notes_button, flashcard_button, app_feedback_button]:
|
||||
for control in [exit_button, text_smaller_button, text_larger_button, calculator_button, slot_machine_button, bookmark_button, notes_button, flashcard_button, app_feedback_button]:
|
||||
if control == null:
|
||||
continue
|
||||
var active: bool = control == bookmark_button and BookmarkService.is_bookmarked(str(BlockSessionService.get_current_question().get("id", "")))
|
||||
@@ -1083,6 +1104,8 @@ func _apply_question_theme() -> void:
|
||||
_style_theme_toggle_button(dark)
|
||||
if calculator_panel != null:
|
||||
calculator_panel.refresh_theme_controls()
|
||||
if slot_machine_panel != null:
|
||||
slot_machine_panel.refresh_theme_controls()
|
||||
if credits_badge != null:
|
||||
credits_badge.refresh_theme_controls()
|
||||
if skip_button != null:
|
||||
@@ -1133,6 +1156,8 @@ func on_hide() -> void:
|
||||
if calculator_panel != null:
|
||||
calculator_panel.close_panel()
|
||||
calculator_panel.reset_calculation()
|
||||
if slot_machine_panel != null:
|
||||
slot_machine_panel.close_panel()
|
||||
|
||||
|
||||
func _save_simulation_current_response() -> void:
|
||||
@@ -1168,6 +1193,11 @@ func _on_simulation_request_failed(message: String) -> void:
|
||||
if BlockSessionService.session_kind != "simulation":
|
||||
return
|
||||
simulation_submit_pending = false
|
||||
if ExamSimulationService.last_failed_action != "submit":
|
||||
# A per-question autosave failing shouldn't interrupt a timed exam with a
|
||||
# blocking "Retry" - the next answer/navigation naturally retries it, and
|
||||
# the final submit sends the complete, authoritative state regardless.
|
||||
return
|
||||
next_button.disabled = false
|
||||
next_button.text = "Retry"
|
||||
next_button.tooltip_text = message
|
||||
@@ -1268,6 +1298,8 @@ func _load_question() -> void:
|
||||
flagged = false
|
||||
selected_choice = ""
|
||||
current_answer = {}
|
||||
last_timer_bar_value = -1.0
|
||||
last_timer_display_seconds = -1
|
||||
flag_button.button_pressed = false
|
||||
_refresh_toolbar_state()
|
||||
_clear_answer_review()
|
||||
@@ -2094,7 +2126,10 @@ func _update_timer_bar() -> void:
|
||||
return
|
||||
var is_core_timer := BlockSessionService.session_kind == "core"
|
||||
var ratio := BlockSessionService.get_session_timer_ratio()
|
||||
timer_bar.value = ratio * 100.0
|
||||
var next_value := ratio * 100.0
|
||||
if last_timer_bar_value < 0.0 or absf(next_value - last_timer_bar_value) >= 0.1:
|
||||
last_timer_bar_value = next_value
|
||||
timer_bar.value = next_value
|
||||
if is_core_timer and timer_label.visible:
|
||||
_set_timer_display(int(floor(BlockSessionService.get_core_question_total_elapsed_seconds())))
|
||||
var color_state := "blue"
|
||||
@@ -2114,6 +2149,9 @@ func _set_timer_display(total_seconds: int) -> void:
|
||||
if timer_label == null:
|
||||
return
|
||||
var safe_seconds := maxi(0, total_seconds)
|
||||
if safe_seconds == last_timer_display_seconds:
|
||||
return
|
||||
last_timer_display_seconds = safe_seconds
|
||||
timer_label.text = "%02d:%02d" % [int(floor(float(safe_seconds) / 60.0)), safe_seconds % 60]
|
||||
|
||||
|
||||
@@ -2373,6 +2411,10 @@ func _refresh_toolbar_state() -> void:
|
||||
bookmark_button.tooltip_text = ("Saved; waiting to sync" if bookmark_pending else "Remove bookmark") if bookmark_value else ("Removal waiting to sync" if bookmark_pending else "Save question")
|
||||
if calculator_button != null:
|
||||
calculator_button.visible = true
|
||||
if slot_machine_button != null:
|
||||
slot_machine_button.visible = Brand.is_uat_environment()
|
||||
if credits_badge != null:
|
||||
credits_badge.visible = BlockSessionService.session_kind != "simulation"
|
||||
_apply_question_theme()
|
||||
|
||||
|
||||
@@ -2381,6 +2423,11 @@ func _toggle_calculator() -> void:
|
||||
calculator_panel.toggle_panel()
|
||||
|
||||
|
||||
func _toggle_slot_machine() -> void:
|
||||
if slot_machine_panel != null:
|
||||
slot_machine_panel.toggle_panel()
|
||||
|
||||
|
||||
func _toggle_bookmark_current_question() -> void:
|
||||
var question := BlockSessionService.get_current_question()
|
||||
if not question.is_empty():
|
||||
@@ -3383,7 +3430,9 @@ func _style_choice_button_for_state(index: int, choice_id: String) -> void:
|
||||
var dark: bool = true if theme_service == null else theme_service.is_dark_mode()
|
||||
var state := "default"
|
||||
var correct_id := str(BlockSessionService.get_current_question().get("correct_choice_id", ""))
|
||||
var reviewing := pending_correct_advance or not current_answer.is_empty()
|
||||
# Exam simulations don't reveal right/wrong - you move on after submitting,
|
||||
# same as a real exam - so only "selected"/"eliminated" styling applies here.
|
||||
var reviewing := BlockSessionService.session_kind != "simulation" and (pending_correct_advance or not current_answer.is_empty())
|
||||
if reviewing and choice_id == correct_id:
|
||||
state = "correct"
|
||||
elif locked_wrong_choice_ids.has(choice_id) or (reviewing and choice_id != "" and choice_id != correct_id):
|
||||
@@ -3505,6 +3554,7 @@ func _build_followup_panel(_index: int, choice_id: String) -> void:
|
||||
"selected_option_text": "",
|
||||
"was_correct": false,
|
||||
"resolved": false,
|
||||
"collapsed": false,
|
||||
"has_authored_choices": bool(followup.get("has_authored_choices", false)),
|
||||
"is_main_correct": false
|
||||
}
|
||||
@@ -3576,6 +3626,7 @@ func _resolve_followup(_followup: Dictionary, selected_button: Button, _style_se
|
||||
state["selected_option_text"] = selected_option_text
|
||||
state["was_correct"] = is_correct
|
||||
state["resolved"] = true
|
||||
state["collapsed"] = false
|
||||
followup_states[resolved_choice_id] = state
|
||||
_render_followup_state(resolved_choice_id)
|
||||
|
||||
@@ -3602,6 +3653,8 @@ func _render_followup_state(choice_id: String) -> void:
|
||||
_style_followup_card(index, state)
|
||||
if not bool(state.get("resolved", false)):
|
||||
_render_active_followup(index, state)
|
||||
elif bool(state.get("collapsed", false)):
|
||||
_build_resolved_followup_toggle(index, state, false)
|
||||
else:
|
||||
_build_resolved_followup(index, state)
|
||||
choice_followup_panels[index].visible = true
|
||||
@@ -3610,13 +3663,95 @@ func _render_followup_state(choice_id: String) -> void:
|
||||
|
||||
func _build_resolved_followup(index: int, state: Dictionary) -> void:
|
||||
var content := choice_followup_contents[index]
|
||||
_build_resolved_followup_toggle(index, state, true)
|
||||
|
||||
var prompt := Label.new()
|
||||
prompt.name = "FollowupPrompt"
|
||||
prompt.text = str(state.get("prompt", "What finding in the stem makes this answer less appropriate?"))
|
||||
prompt.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_set_scaled_label(prompt, 14, Brand.text_color(_followup_is_dark()))
|
||||
content.add_child(prompt)
|
||||
|
||||
var selected_id := str(state.get("selected_option_id", ""))
|
||||
var correct_id := str(state.get("correct_choice_id", ""))
|
||||
var options: Array = state.get("choices", [])
|
||||
for option_index in range(options.size()):
|
||||
var option = options[option_index]
|
||||
if typeof(option) != TYPE_DICTIONARY:
|
||||
continue
|
||||
var option_id := str(option.get("id", ""))
|
||||
var option_button := Button.new()
|
||||
option_button.name = "FollowupOption_%s" % option_id
|
||||
option_button.text = "%s. %s" % [_display_label_for_index(option_index), str(option.get("text", ""))]
|
||||
option_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
option_button.disabled = true
|
||||
option_button.custom_minimum_size = Vector2(0, 38)
|
||||
_set_scaled_button_font(option_button, 14)
|
||||
_style_review_followup_option(
|
||||
option_button,
|
||||
_followup_is_dark(),
|
||||
option_id,
|
||||
selected_id,
|
||||
correct_id,
|
||||
true
|
||||
)
|
||||
content.add_child(option_button)
|
||||
|
||||
var explanation := Label.new()
|
||||
explanation.name = "FollowupExplanation"
|
||||
explanation.text = _strip_verdict_prefix(_choice_review_explanation(str(state.get("choice_id", "")), state))
|
||||
explanation.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_set_scaled_label(explanation, 14, Brand.text_color(_followup_is_dark()))
|
||||
content.add_child(explanation)
|
||||
|
||||
|
||||
func _build_resolved_followup_toggle(index: int, state: Dictionary, expanded: bool) -> void:
|
||||
var content := choice_followup_contents[index]
|
||||
var dark := _followup_is_dark()
|
||||
var has_authored_choices := bool(state.get("has_authored_choices", not state.get("choices", []).is_empty()))
|
||||
var was_correct := bool(state.get("was_correct", false))
|
||||
var accent := Brand.SUCCESS if was_correct else Color("fb5f6d") if has_authored_choices else Brand.PURPLE
|
||||
var row := HBoxContainer.new()
|
||||
row.name = "FollowupResult"
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
content.add_child(row)
|
||||
|
||||
var result := Label.new()
|
||||
result.name = "FollowupVerdict"
|
||||
result.text = ("Correct" if was_correct else "Incorrect") if has_authored_choices else "Review complete"
|
||||
result.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_set_scaled_label(result, 14, accent)
|
||||
row.add_child(result)
|
||||
|
||||
var toggle := Button.new()
|
||||
toggle.name = "FollowupToggle"
|
||||
toggle.flat = true
|
||||
toggle.text = "Minimize" if expanded else "Expand"
|
||||
toggle.tooltip_text = "Minimize follow-up review" if expanded else "Expand follow-up review"
|
||||
toggle.icon = preload("res://assets/icons/lucide/chevron-down.svg") if expanded else preload("res://assets/icons/lucide/chevron-right.svg")
|
||||
toggle.icon_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
toggle.custom_minimum_size = Vector2(110, 30)
|
||||
toggle.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
toggle.add_theme_color_override("font_hover_color", accent)
|
||||
toggle.add_theme_color_override("icon_normal_color", Brand.text_color(dark))
|
||||
toggle.add_theme_color_override("icon_hover_color", accent)
|
||||
_set_scaled_button_font(toggle, 13)
|
||||
toggle.pressed.connect(_toggle_followup_state.bind(str(state.get("choice_id", ""))))
|
||||
row.add_child(toggle)
|
||||
|
||||
|
||||
func _toggle_followup_state(choice_id: String) -> void:
|
||||
if not followup_states.has(choice_id):
|
||||
return
|
||||
var state: Dictionary = followup_states[choice_id]
|
||||
if not bool(state.get("resolved", false)):
|
||||
return
|
||||
state["collapsed"] = not bool(state.get("collapsed", false))
|
||||
followup_states[choice_id] = state
|
||||
_render_followup_state(choice_id)
|
||||
_apply_saved_choice_state()
|
||||
|
||||
|
||||
func _add_followup_body(content: VBoxContainer, is_correct: bool) -> VBoxContainer:
|
||||
var body := PanelContainer.new()
|
||||
body.add_theme_stylebox_override("panel", _followup_body_box(_followup_is_dark(), is_correct))
|
||||
|
||||
Reference in New Issue
Block a user