477 lines
16 KiB
GDScript
477 lines
16 KiB
GDScript
extends Control
|
|
|
|
const Brand := preload("res://scripts/ui/medmind_style.gd")
|
|
const Style := preload("res://scripts/ui/ui_style.gd")
|
|
const DOCTOR_SCENE := preload("res://scenes/ccs_cases_2/doctor_office.tscn")
|
|
const DESIGN_SIZE := Vector2(1600.0, 900.0)
|
|
const FOCUSED_INTERVIEW_CASE_ID := "ccs-gastro-duodenal-ulcer-office-001"
|
|
const FOCUSED_INTERVIEW_REVISION := 4
|
|
|
|
var main
|
|
var stage_root: Control
|
|
var active_scene: Control
|
|
var selection_view: VBoxContainer
|
|
var case_list: VBoxContainer
|
|
var status_label: Label
|
|
var retry_button: Button
|
|
var current_view := ""
|
|
var cases_loaded := false
|
|
var active_loaded := false
|
|
var pending_action := ""
|
|
var pending_exam_code := ""
|
|
var restart_case_external_id := ""
|
|
|
|
|
|
func _ready() -> void:
|
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
clip_contents = true
|
|
_build_selection_view()
|
|
stage_root = Control.new()
|
|
stage_root.name = "GraphicalCcsStage"
|
|
stage_root.size = DESIGN_SIZE
|
|
stage_root.visible = false
|
|
add_child(stage_root)
|
|
_connect_service()
|
|
resized.connect(_fit_stage)
|
|
_fit_stage()
|
|
|
|
|
|
func _connect_service() -> void:
|
|
if not CCSService.cases_changed.is_connected(_on_cases_changed):
|
|
CCSService.cases_changed.connect(_on_cases_changed)
|
|
if not CCSService.attempt_changed.is_connected(_on_attempt_changed):
|
|
CCSService.attempt_changed.connect(_on_attempt_changed)
|
|
if not CCSService.request_failed.is_connected(_on_request_failed):
|
|
CCSService.request_failed.connect(_on_request_failed)
|
|
if not CCSService.orders_changed.is_connected(_on_orders_changed):
|
|
CCSService.orders_changed.connect(_on_orders_changed)
|
|
if not CCSService.attempt_abandoned.is_connected(_on_attempt_abandoned):
|
|
CCSService.attempt_abandoned.connect(_on_attempt_abandoned)
|
|
|
|
|
|
func on_show(_payload: Variant = null) -> void:
|
|
CCSService.launch_origin = "ccs_cases_2"
|
|
restart_case_external_id = ""
|
|
cases_loaded = false
|
|
active_loaded = false
|
|
pending_action = "loading"
|
|
_show_selection("Loading interactive cases...")
|
|
CCSService.load_cases()
|
|
CCSService.load_active()
|
|
|
|
|
|
func on_hide() -> void:
|
|
restart_case_external_id = ""
|
|
_clear_active_scene()
|
|
|
|
|
|
func set_pause_menu_open(open: bool) -> void:
|
|
if active_scene != null and active_scene.has_method("set_dialogue_input_enabled"):
|
|
active_scene.set_dialogue_input_enabled(not open)
|
|
|
|
|
|
func close_transient_overlay() -> bool:
|
|
if active_scene != null and active_scene.has_method("close_transient_overlay"):
|
|
return bool(active_scene.close_transient_overlay())
|
|
return false
|
|
|
|
|
|
func compatible_cases(cases: Array) -> Array:
|
|
return cases.filter(func(item: Variant) -> bool:
|
|
return typeof(item) == TYPE_DICTIONARY \
|
|
and bool(item.get("interactiveInterviewAvailable", false)) \
|
|
and bool(item.get("graphicalPresentationAvailable", false)) \
|
|
and str(item.get("graphicalPresentation", {}).get("sceneKey", "")) == "doctor_office")
|
|
|
|
|
|
func _on_cases_changed() -> void:
|
|
cases_loaded = true
|
|
_render_state()
|
|
|
|
|
|
func _on_attempt_changed() -> void:
|
|
active_loaded = true
|
|
var completed_action := pending_action
|
|
var completed_exam_code := pending_exam_code
|
|
pending_action = ""
|
|
pending_exam_code = ""
|
|
if completed_action == "physical_exam" and active_scene != null \
|
|
and current_view == "doctor_office":
|
|
_refresh_exam_result(CCSService.attempt, completed_exam_code)
|
|
return
|
|
if completed_action in ["orders", "cancel_order", "advance", "enter_final_orders"] \
|
|
and active_scene != null and current_view == "doctor_office":
|
|
_refresh_case_workspace(CCSService.attempt)
|
|
if completed_action != "enter_final_orders":
|
|
active_scene.show_order_action_complete(completed_action)
|
|
return
|
|
_render_state()
|
|
|
|
|
|
func _refresh_exam_result(attempt: Dictionary, exam_code: String) -> void:
|
|
_refresh_case_workspace(attempt)
|
|
active_scene.show_exam_result(attempt.get("chart", []), exam_code)
|
|
|
|
|
|
func _refresh_case_workspace(attempt: Dictionary) -> void:
|
|
active_scene.configure_presentation(
|
|
attempt.get("graphicalPresentation", {}),
|
|
str(attempt.get("simulatedStartUtc", "")),
|
|
int(attempt.get("simulatedMinute", 0)),
|
|
attempt.get("vitals", {}),
|
|
str(attempt.get("medicalHistory", "")), attempt.get("chart", []))
|
|
active_scene.configure_examinations(attempt.get("availableExams", []), attempt.get("chart", []))
|
|
active_scene.configure_orders(attempt)
|
|
|
|
|
|
func _render_state() -> void:
|
|
if not visible:
|
|
return
|
|
if not active_loaded:
|
|
return
|
|
var attempt: Dictionary = CCSService.attempt
|
|
if not attempt.is_empty() and str(attempt.get("state", "")) == "completed" \
|
|
and str(attempt.get("experienceMode", "")) == "graphical_interview":
|
|
_render_debrief(attempt)
|
|
return
|
|
if not attempt.is_empty() and str(attempt.get("state", "")) in ["active", "final_orders"]:
|
|
_render_attempt(attempt)
|
|
elif cases_loaded and active_loaded:
|
|
_render_case_selection()
|
|
|
|
|
|
func _render_attempt(attempt: Dictionary) -> void:
|
|
if str(attempt.get("state", "")) not in ["active", "final_orders"]:
|
|
_show_unsupported_attempt("This active case is not available in the graphical interview yet.")
|
|
return
|
|
var interview: Dictionary = attempt.get("interview", {})
|
|
if interview.is_empty():
|
|
_show_unsupported_attempt("This active case does not include a graphical dialogue interview.")
|
|
return
|
|
var node: Dictionary = interview.get("currentNode", {})
|
|
if node.is_empty():
|
|
_show_unsupported_attempt("The graphical interview could not load its current dialogue.")
|
|
return
|
|
if _is_retired_interview_revision(attempt, node):
|
|
_restart_with_current_interview(attempt)
|
|
return
|
|
if str(attempt.get("experienceMode", "management")) != "graphical_interview" \
|
|
and bool(attempt.get("openingCompleted", false)) and not bool(node.get("isTerminal", false)):
|
|
_show_unsupported_attempt("This case has progressed beyond the graphical interview.")
|
|
return
|
|
_show_doctor_office(node, attempt)
|
|
|
|
|
|
func _build_selection_view() -> void:
|
|
selection_view = VBoxContainer.new()
|
|
selection_view.name = "CaseSelection"
|
|
selection_view.add_theme_constant_override("separation", 16)
|
|
add_child(selection_view)
|
|
selection_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
var heading := Label.new()
|
|
heading.text = "Choose an Interactive CCS Case"
|
|
Brand.apply_font(heading)
|
|
heading.add_theme_font_size_override("font_size", 28)
|
|
selection_view.add_child(heading)
|
|
var description := Label.new()
|
|
description.text = "Interview the patient by selecting the questions you want to ask."
|
|
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
Brand.apply_font(description)
|
|
description.add_theme_font_size_override("font_size", 16)
|
|
selection_view.add_child(description)
|
|
status_label = Label.new()
|
|
status_label.name = "CaseSelectionStatus"
|
|
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
Brand.apply_font(status_label)
|
|
selection_view.add_child(status_label)
|
|
retry_button = Button.new()
|
|
retry_button.text = "Retry"
|
|
retry_button.visible = false
|
|
retry_button.custom_minimum_size = Vector2(120, 42)
|
|
retry_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
|
Style.style_primary_button(retry_button)
|
|
retry_button.pressed.connect(_retry_load)
|
|
selection_view.add_child(retry_button)
|
|
var scroll := ScrollContainer.new()
|
|
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
|
selection_view.add_child(scroll)
|
|
case_list = VBoxContainer.new()
|
|
case_list.name = "InteractiveCaseList"
|
|
case_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
case_list.add_theme_constant_override("separation", 10)
|
|
scroll.add_child(case_list)
|
|
|
|
|
|
func _show_selection(message: String = "") -> void:
|
|
_clear_active_scene()
|
|
current_view = "case_selection"
|
|
selection_view.visible = true
|
|
stage_root.visible = false
|
|
status_label.text = message
|
|
retry_button.visible = false
|
|
|
|
|
|
func _show_unsupported_attempt(message: String) -> void:
|
|
_show_selection(message)
|
|
_clear_children(case_list)
|
|
status_label.text += " Finish or abandon it from CCS Cases before starting another graphical interview."
|
|
|
|
|
|
func _render_case_selection() -> void:
|
|
_show_selection()
|
|
_clear_children(case_list)
|
|
var available := compatible_cases(CCSService.cases)
|
|
if available.is_empty():
|
|
status_label.text = "No published CCS cases currently have a supported graphical interview."
|
|
return
|
|
status_label.text = "%d interactive %s available." % [available.size(), "case is" if available.size() == 1 else "cases are"]
|
|
retry_button.visible = false
|
|
for raw_case in available:
|
|
var case: Dictionary = raw_case
|
|
var button := Button.new()
|
|
button.name = "Case%s" % str(case.get("externalId", "")).to_pascal_case()
|
|
button.text = "%s\n%s · Difficulty %d · %d minutes" % [
|
|
str(case.get("title", "Untitled case")), str(case.get("system", "")),
|
|
int(case.get("difficulty", 0)), int(case.get("realTimeMinutes", 0))]
|
|
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
|
button.custom_minimum_size = Vector2(0, 76)
|
|
Style.style_answer_button(button)
|
|
button.pressed.connect(_start_case.bind(str(case.get("externalId", ""))))
|
|
case_list.add_child(button)
|
|
|
|
|
|
func _start_case(case_external_id: String) -> void:
|
|
if not pending_action.is_empty() or case_external_id.is_empty():
|
|
return
|
|
pending_action = "creating"
|
|
status_label.text = "Starting case..."
|
|
_set_case_buttons_disabled(true)
|
|
CCSService.create_attempt(case_external_id, "timed")
|
|
|
|
|
|
func _show_doctor_office(node: Dictionary, attempt: Dictionary = {}) -> void:
|
|
if active_scene == null:
|
|
_set_active_scene(DOCTOR_SCENE.instantiate(), "doctor_office")
|
|
active_scene.choice_selected.connect(_submit_dialogue_choice)
|
|
active_scene.physical_exam_selected.connect(_submit_physical_exam)
|
|
active_scene.physical_exam_finish_requested.connect(_finish_physical_examination)
|
|
active_scene.order_search_requested.connect(_search_orders)
|
|
active_scene.orders_submit_requested.connect(_submit_orders)
|
|
active_scene.order_cancel_requested.connect(_cancel_order)
|
|
active_scene.time_advance_requested.connect(_advance_time)
|
|
active_scene.final_orders_submit_requested.connect(_submit_final_orders)
|
|
active_scene.debrief_return_requested.connect(_return_to_cases)
|
|
active_scene.debrief_repeat_requested.connect(_repeat_interview)
|
|
selection_view.visible = false
|
|
stage_root.visible = true
|
|
active_scene.configure_experience_mode(str(attempt.get("experienceMode", "management")))
|
|
active_scene.configure_presentation(
|
|
attempt.get("graphicalPresentation", {}),
|
|
str(attempt.get("simulatedStartUtc", "")),
|
|
int(attempt.get("simulatedMinute", 0)),
|
|
attempt.get("vitals", {}),
|
|
str(attempt.get("medicalHistory", "")), attempt.get("chart", []))
|
|
active_scene.configure_examinations(attempt.get("availableExams", []), attempt.get("chart", []))
|
|
active_scene.configure_interview(node)
|
|
active_scene.configure_orders(attempt)
|
|
active_scene.set_choices_pending(not pending_action.is_empty())
|
|
|
|
|
|
func _submit_dialogue_choice(choice_id: String) -> void:
|
|
if not pending_action.is_empty() or choice_id.is_empty():
|
|
return
|
|
pending_action = "dialogue"
|
|
active_scene.set_choices_pending(true)
|
|
CCSService.send_dialogue_choice(choice_id)
|
|
|
|
|
|
func _submit_physical_exam(exam_code: String) -> void:
|
|
if not pending_action.is_empty() or exam_code.is_empty():
|
|
return
|
|
pending_action = "physical_exam"
|
|
pending_exam_code = exam_code
|
|
active_scene.set_physical_exam_pending(exam_code)
|
|
CCSService.send_command("exam", exam_code)
|
|
|
|
|
|
func _finish_physical_examination() -> void:
|
|
if not pending_action.is_empty():
|
|
return
|
|
pending_action = "enter_final_orders"
|
|
active_scene.set_physical_exam_pending("finishing_case")
|
|
CCSService.send_command("end_case")
|
|
|
|
|
|
func _search_orders(query: String) -> void:
|
|
CCSService.search_orders(query)
|
|
|
|
|
|
func _on_orders_changed() -> void:
|
|
if active_scene != null and active_scene.has_method("render_order_search"):
|
|
active_scene.render_order_search(CCSService.orders)
|
|
|
|
|
|
func _submit_orders(codes: Array) -> void:
|
|
if not pending_action.is_empty() or codes.is_empty():
|
|
return
|
|
pending_action = "orders"
|
|
active_scene.set_orders_pending(true, "Submitting orders...")
|
|
var commands: Array = []
|
|
for code in codes:
|
|
commands.append({"type": "order", "code": str(code), "minutes": 0})
|
|
CCSService.send_batch(commands)
|
|
|
|
|
|
func _cancel_order(code: String) -> void:
|
|
if not pending_action.is_empty() or code.is_empty():
|
|
return
|
|
pending_action = "cancel_order"
|
|
active_scene.set_orders_pending(true, "Cancelling order...")
|
|
CCSService.send_batch([{"type": "cancel_order", "code": code, "minutes": 0}])
|
|
|
|
|
|
func _advance_time(minutes: int) -> void:
|
|
if not pending_action.is_empty() or minutes <= 0:
|
|
return
|
|
pending_action = "advance"
|
|
active_scene.set_orders_pending(true, "Obtaining results...")
|
|
CCSService.send_command("advance", "", minutes)
|
|
|
|
|
|
func _submit_final_orders() -> void:
|
|
if not pending_action.is_empty():
|
|
return
|
|
pending_action = "submit_final_orders"
|
|
active_scene.set_orders_pending(true, "Submitting final orders...")
|
|
CCSService.send_command("exit_case")
|
|
|
|
|
|
func _render_debrief(attempt: Dictionary) -> void:
|
|
if active_scene == null:
|
|
_set_active_scene(DOCTOR_SCENE.instantiate(), "doctor_office")
|
|
active_scene.debrief_return_requested.connect(_return_to_cases)
|
|
active_scene.debrief_repeat_requested.connect(_repeat_interview)
|
|
selection_view.visible = false
|
|
stage_root.visible = true
|
|
active_scene.configure_presentation(
|
|
attempt.get("graphicalPresentation", {}),
|
|
str(attempt.get("simulatedStartUtc", "")),
|
|
int(attempt.get("simulatedMinute", 0)),
|
|
attempt.get("vitals", {}),
|
|
str(attempt.get("medicalHistory", "")), attempt.get("chart", []))
|
|
active_scene.show_debrief(attempt.get("interviewDebrief", {}), attempt.get("score", {}))
|
|
current_view = "interview_debrief"
|
|
|
|
|
|
func _return_to_cases() -> void:
|
|
CCSService.attempt = {}
|
|
pending_action = ""
|
|
_render_case_selection()
|
|
|
|
|
|
func _repeat_interview() -> void:
|
|
var case_external_id := str(CCSService.attempt.get("caseExternalId", ""))
|
|
if case_external_id.is_empty() or not pending_action.is_empty():
|
|
return
|
|
pending_action = "creating"
|
|
CCSService.create_attempt(case_external_id, "timed")
|
|
|
|
|
|
func _is_retired_interview_revision(attempt: Dictionary, _node: Dictionary) -> bool:
|
|
if str(attempt.get("caseExternalId", "")) != FOCUSED_INTERVIEW_CASE_ID:
|
|
return false
|
|
var presentation: Dictionary = attempt.get("graphicalPresentation", {})
|
|
return int(presentation.get("interviewRevision", 0)) < FOCUSED_INTERVIEW_REVISION
|
|
|
|
|
|
func _restart_with_current_interview(attempt: Dictionary) -> void:
|
|
if pending_action == "refreshing_interview":
|
|
return
|
|
restart_case_external_id = str(attempt.get("caseExternalId", ""))
|
|
pending_action = "refreshing_interview"
|
|
_show_selection("Updating this case to the latest interactive examination...")
|
|
CCSService.abandon()
|
|
|
|
|
|
func _on_attempt_abandoned() -> void:
|
|
if restart_case_external_id.is_empty():
|
|
return
|
|
var case_external_id := restart_case_external_id
|
|
restart_case_external_id = ""
|
|
pending_action = "creating"
|
|
status_label.text = "Starting the updated interview..."
|
|
CCSService.create_attempt(case_external_id, "timed")
|
|
|
|
|
|
func _on_request_failed(message: String) -> void:
|
|
if not visible:
|
|
return
|
|
var failed_action := pending_action
|
|
pending_action = ""
|
|
if failed_action == "creating" and message.contains("409"):
|
|
status_label.text = "An active case was found. Loading it now..."
|
|
CCSService.load_active()
|
|
return
|
|
if failed_action == "refreshing_interview":
|
|
restart_case_external_id = ""
|
|
status_label.text = "The updated interview could not be started. %s" % message
|
|
retry_button.visible = true
|
|
return
|
|
if active_scene != null:
|
|
if failed_action == "physical_exam":
|
|
pending_exam_code = ""
|
|
active_scene.show_physical_exam_error(message)
|
|
else:
|
|
active_scene.set_choices_pending(false)
|
|
if active_scene.has_method("set_orders_pending"):
|
|
active_scene.set_orders_pending(false, message)
|
|
else:
|
|
status_label.text = message
|
|
retry_button.visible = failed_action == "loading"
|
|
_set_case_buttons_disabled(false)
|
|
|
|
|
|
func _retry_load() -> void:
|
|
on_show()
|
|
|
|
|
|
func _set_active_scene(scene: Control, view_name: String) -> void:
|
|
_clear_active_scene()
|
|
active_scene = scene
|
|
current_view = view_name
|
|
stage_root.add_child(active_scene)
|
|
active_scene.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
|
|
|
|
func _clear_active_scene() -> void:
|
|
if active_scene != null and is_instance_valid(active_scene):
|
|
if active_scene.get_parent() != null:
|
|
active_scene.get_parent().remove_child(active_scene)
|
|
active_scene.queue_free()
|
|
active_scene = null
|
|
if current_view == "doctor_office":
|
|
current_view = ""
|
|
|
|
|
|
func _set_case_buttons_disabled(disabled: bool) -> void:
|
|
for child in case_list.get_children():
|
|
if child is BaseButton:
|
|
child.disabled = disabled
|
|
|
|
|
|
func _clear_children(parent: Node) -> void:
|
|
for child in parent.get_children():
|
|
child.queue_free()
|
|
|
|
|
|
func _fit_stage() -> void:
|
|
if stage_root == null:
|
|
return
|
|
var available := size
|
|
if available.x <= 0.0 or available.y <= 0.0:
|
|
return
|
|
stage_root.scale = Vector2.ONE
|
|
stage_root.position = Vector2.ZERO
|
|
stage_root.size = available
|