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