Redesign exam and CCS experiences

This commit is contained in:
hz
2026-07-16 22:42:03 -05:00
parent c8bfc26645
commit a778a680e0
35 changed files with 2132 additions and 274 deletions
+124
View File
@@ -0,0 +1,124 @@
extends SceneTree
const Style := preload("res://scripts/ui/ui_style.gd")
const Brand := preload("res://scripts/ui/medmind_style.gd")
var failures := 0
var ccs_service
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
ccs_service = root.get_node("CCSService")
var theme_service = root.get_node("UIThemeService")
var prior_dark_mode: bool = theme_service.is_dark_mode()
var prior_attempt: Dictionary = ccs_service.attempt.duplicate(true)
var prior_orders: Array = ccs_service.orders.duplicate(true)
var prior_scale: float = ccs_service.text_scale
ccs_service.text_scale = 1.0
ccs_service.orders = []
ccs_service.attempt = _sample_attempt()
var host := Control.new()
host.size = Vector2(1440, 900)
root.add_child(host)
var player = load("res://scripts/ui/ccs_player.gd").new()
player.main = host
player.size = host.size
host.add_child(player)
await process_frame
player._render()
await process_frame
_assert(player.heading.text == "CCS Case | Emergency Department", "CCS header should show the readable location without a case id")
_assert(player._ordered_vital_keys(ccs_service.attempt.vitals) == ["temperature", "bloodPressure", "pulse", "respirations", "oxygenSaturation"], "vital chips should use the clinical priority order")
_assert(player.vitals_flow.get_child_count() == 5, "every supplied vital should render as a chip")
_assert(player.search_button.icon != null, "Order Clerk search should use a bundled icon rather than a font glyph")
_assert(player.summary.get_meta("ccs_region", "") == "summary", "summary highlighting should use its own region")
_assert(player.history.get_meta("ccs_region", "") == "history", "history highlighting should use its own region")
_assert(player.appearance.get_meta("ccs_region", "") == "appearance", "appearance highlighting should use its own region")
_assert(player.chart.get_meta("ccs_region", "") == "chart", "chart highlighting should use its own region")
_assert(not player.workspace.vertical, "wide CCS screens should use chart and Order Clerk columns")
_assert(player.orders.get_child_count() == 1, "active orders should render in the Order Clerk")
var active_order_row := player.orders.get_child(0) as HBoxContainer
var active_order_label := active_order_row.get_child(0) as Label
_assert(active_order_label.text == "Normal Saline Bolus", "internal order codes should be human readable")
_assert(player.actions.get_child_count() == 2, "available physical examinations should render as actions")
_assert(player.locations.get_child_count() == 2, "allowed locations should render as move actions")
_assert(not player.clock.visible, "the normal active-case countdown should remain hidden")
var clerk_font_size: int = player.search.get_theme_font_size("font_size")
var chart_font_size: int = player.summary.get_theme_font_size("normal_font_size")
player._zoom(ccs_service.TEXT_SCALE_STEP)
_assert(player.summary.get_theme_font_size("normal_font_size") > chart_font_size, "text zoom should enlarge clinical chart text")
_assert(player.search.get_theme_font_size("font_size") == clerk_font_size, "text zoom should not resize Order Clerk controls")
player.size = Vector2(800, 900)
player._update_responsive_layout()
_assert(player.workspace.vertical, "narrow CCS screens should stack the Order Clerk below the chart")
ccs_service.attempt.state = "final_orders"
ccs_service.attempt.finalOrdersDeadlineUtc = Time.get_datetime_string_from_unix_time(Time.get_unix_time_from_system() + 90.0, true)
player._render()
_assert(player.final_orders_notice.visible and player.clock.visible, "final-orders mode should show instructions and its deadline")
_assert(not player.time_row.visible, "time advancement should be unavailable during final orders")
theme_service.set_dark_mode(true)
await process_frame
theme_service.set_dark_mode(false)
Style.refresh_tree(player, true)
await process_frame
var light_panel := player.chart_panel.get_theme_stylebox("panel") as StyleBoxFlat
_assert(light_panel.bg_color.is_equal_approx(Brand.content_card_box(false, false).bg_color), "CCS light styles should win after the shell palette translation")
_assert(player.summary.get_theme_color("default_color").is_equal_approx(Brand.text_color(false)), "CCS light chart text should remain readable")
theme_service.set_dark_mode(true)
Style.refresh_tree(player, false)
await process_frame
var dark_panel := player.chart_panel.get_theme_stylebox("panel") as StyleBoxFlat
_assert(dark_panel.bg_color.is_equal_approx(Brand.content_card_box(true, false).bg_color), "CCS dark styles should win after the shell palette translation")
_assert(player.summary.get_theme_color("default_color").is_equal_approx(Brand.text_color(true)), "CCS dark chart text should remain readable")
theme_service.set_dark_mode(prior_dark_mode)
await process_frame
host.queue_free()
await process_frame
ccs_service.attempt = prior_attempt
ccs_service.orders = prior_orders
ccs_service.text_scale = prior_scale
quit(1 if failures > 0 else 0)
func _sample_attempt() -> Dictionary:
return {
"id": "attempt-1",
"state": "active",
"location": "emergency_department",
"patientSummary": "24-year-old woman with type 1 diabetes mellitus",
"reasonForEncounter": "Two days of vomiting and progressive lethargy.",
"history": "She stopped insulin when oral intake declined.",
"appearance": "She is lethargic but arousable.",
"vitals": {
"oxygenSaturation": "98% on room air",
"pulse": "118/min",
"temperature": "37.3 C (99.1 F)",
"respirations": "28/min",
"bloodPressure": "88/56 mm Hg"
},
"chart": [{"simulatedMinute": 0, "category": "progress", "text": "Initial evaluation is in progress."}],
"activeOrderCodes": ["normal_saline_bolus"],
"availableExams": [
{"code": "general", "name": "General and hydration examination"},
{"code": "neurologic", "name": "Neurologic examination"}
],
"allowedLocations": ["emergency_department", "icu"],
"deadlineUtc": Time.get_datetime_string_from_unix_time(Time.get_unix_time_from_system() + 600.0, true)
}
func _assert(condition: bool, message: String) -> void:
if condition:
return
failures += 1
push_error(message)
+1
View File
@@ -0,0 +1 @@
uid://sc7j07qkb1ci
+6
View File
@@ -13,6 +13,12 @@ func _initialize() -> void:
_assert(history.has_active_simulation({}, {"state": "block_active"}), "a loaded active block should be resumable")
_assert(history.has_active_simulation({}, {"state": "between_blocks"}), "a loaded between-block simulation should be resumable")
_assert(not history.has_active_simulation({}, {"state": "completed"}), "completed simulations should not appear active")
_assert(history.completed_blocks({"state": "block_active", "currentBlock": 3, "totalBlocks": 12}) == 3, "active block progress should count only finished blocks")
_assert(history.completed_blocks({"state": "between_blocks", "currentBlock": 3, "totalBlocks": 12}) == 4, "between-block progress should include the submitted block")
_assert(history.completed_blocks({"state": "completed", "currentBlock": 11, "totalBlocks": 12}) == 12, "completed simulations should show every block")
_assert(history.progress_percent(3, 12) == 25, "progress percentage should use floating-point division")
_assert(history.remaining_time_text({"state": "between_blocks", "currentBlock": 2, "totalBlocks": 12}) == "~4h 30m remaining", "remaining time should reflect unstarted blocks")
_assert(history.format_datetime_parts({"year": 2026, "month": 7, "day": 16, "hour": 22, "minute": 4}) == "Jul 16, 2026\n10:04 PM", "history dates should use the compact mockup format")
quit(1 if failures > 0 else 0)
+57
View File
@@ -0,0 +1,57 @@
extends SceneTree
var failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var service = root.get_node("ExamSimulationService")
var theme_service = root.get_node("UIThemeService")
var prior_dark_mode: bool = theme_service.is_dark_mode()
service.readiness = {"ready": true, "hasActiveSimulation": true, "availableQuestions": 300, "requiredQuestions": 232}
service.simulation = {"id": "active", "state": "between_blocks", "currentBlock": 2, "totalBlocks": 12, "createdUtc": "2026-07-16T22:04:00Z"}
service.history = [{"id": "completed", "state": "completed", "currentBlock": 11, "totalBlocks": 12, "createdUtc": "2026-07-15T20:35:00Z", "predictedScore": 236, "rangeLow": 221, "rangeHigh": 248, "passProbability": 0.82}]
service.history_next_cursor = "next-page"
var host := Control.new()
host.size = Vector2(1400, 800)
root.add_child(host)
var screen = load("res://scripts/ui/simulation_history.gd").new()
screen.size = host.size
host.add_child(screen)
await process_frame
screen._update_responsive_layout()
screen._render_active_section()
screen._render_history()
await process_frame
_assert(screen.history_list.get_child_count() == 1, "history should render completed attempts without duplicating the active simulation")
_assert(screen.load_more_button.visible, "history should expose pagination when the API returns a cursor")
var active_layout: BoxContainer = screen.find_child("ActiveCardLayout", true, false)
_assert(active_layout != null and not active_layout.vertical, "wide exam screens should use the table-oriented layout")
theme_service.set_dark_mode(false)
await process_frame
var light_active_box := screen.active_section.get_theme_stylebox("panel") as StyleBoxFlat
_assert(light_active_box.bg_color.is_equal_approx(screen._section_box(false).bg_color), "the Exams page should apply its light surface after a live theme change")
theme_service.set_dark_mode(true)
await process_frame
var dark_active_box := screen.active_section.get_theme_stylebox("panel") as StyleBoxFlat
_assert(dark_active_box.bg_color.is_equal_approx(screen._section_box(true).bg_color), "the Exams page should restore its dark surface after a live theme change")
screen.size = Vector2(760, 800)
screen._update_responsive_layout()
await process_frame
active_layout = screen.find_child("ActiveCardLayout", true, false)
_assert(active_layout != null and active_layout.vertical, "narrow exam screens should stack the active simulation card")
_assert(not screen.table_header.visible, "narrow exam screens should replace the table header with history cards")
host.queue_free()
await process_frame
theme_service.set_dark_mode(prior_dark_mode)
quit(1 if failures > 0 else 0)
func _assert(condition: bool, message: String) -> void:
if condition:
return
failures += 1
push_error(message)
+18
View File
@@ -28,6 +28,24 @@ func _initialize() -> void:
_assert(light_card.border_color.is_equal_approx(Brand.ROYAL_BLUE), "active light cards should use the primary blue border")
var light_field := Brand.field_box(false)
_assert(light_field.border_color.is_equal_approx(Brand.LIGHT_BORDER), "light fields should use the semantic cool-gray border")
var theme_toggle := Button.new()
Brand.set_theme_toggle_icon(theme_toggle, true)
_assert(theme_toggle.text.is_empty() and theme_toggle.icon == Brand.SUN_ICON, "dark mode should use the bundled sun icon")
Brand.set_theme_toggle_icon(theme_toggle, false)
_assert(theme_toggle.text.is_empty() and theme_toggle.icon == Brand.MOON_ICON, "light mode should use the bundled moon icon")
var login = load("res://scripts/ui/login_screen.gd").new()
var password := LineEdit.new()
var password_reveal := Button.new()
password.secret = true
login._refresh_password_reveal(password, password_reveal)
var show_icon := password_reveal.icon
password.secret = false
login._refresh_password_reveal(password, password_reveal)
_assert(show_icon != null and password_reveal.icon != null and show_icon != password_reveal.icon, "password visibility should switch bundled eye icons")
password_reveal.free()
password.free()
login.free()
theme_toggle.free()
Style.set_dark_mode(true)
Style.refresh_tree(root, false)