From dfc224660e543aa4875df5ad959de19bbdeff0db Mon Sep 17 00:00:00 2001 From: hz Date: Thu, 6 Aug 2026 05:13:21 -0500 Subject: [PATCH] Constrain CCS patient dialogue --- scenes/ccs_cases_2/doctor_office.tscn | 5 ++- scripts/ui/ccs_cases_2/doctor_office.gd | 2 +- scripts/ui/ccs_cases_2_screen.gd | 49 +++++++++++++++++++++++++ tests/ccs_cases_2_ui_test.gd | 29 ++++++++++++--- 4 files changed, 78 insertions(+), 7 deletions(-) diff --git a/scenes/ccs_cases_2/doctor_office.tscn b/scenes/ccs_cases_2/doctor_office.tscn index 7aea874..b718362 100644 --- a/scenes/ccs_cases_2/doctor_office.tscn +++ b/scenes/ccs_cases_2/doctor_office.tscn @@ -103,6 +103,7 @@ antialiased = true [node name="DialogueBubble" type="Control" parent="."] z_index = 30 +clip_contents = true layout_mode = 1 anchors_preset = 0 anchor_left = 0.53 @@ -127,7 +128,7 @@ texture = ExtResource("6_chat_bubble") expand_mode = 1 stretch_mode = 0 -[node name="Text" type="Label" parent="DialogueBubble"] +[node name="Text" type="RichTextLabel" parent="DialogueBubble"] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -141,6 +142,8 @@ grow_vertical = 2 theme_override_colors/font_color = Color(0.055, 0.105, 0.145, 1) theme_override_font_sizes/font_size = 20 text = "Hello, doctor." +fit_content = false +scroll_active = false autowrap_mode = 2 vertical_alignment = 1 diff --git a/scripts/ui/ccs_cases_2/doctor_office.gd b/scripts/ui/ccs_cases_2/doctor_office.gd index 79bcf3d..c0ed673 100644 --- a/scripts/ui/ccs_cases_2/doctor_office.gd +++ b/scripts/ui/ccs_cases_2/doctor_office.gd @@ -18,7 +18,7 @@ const DIALOGUE_PAGE_MAX_CHARS := 82 @onready var second_hand: Node2D = $ClockMount/SecondHand @onready var patient: TextureRect = $Patient @onready var dialogue_bubble: Control = $DialogueBubble -@onready var dialogue_text: Label = $DialogueBubble/Text +@onready var dialogue_text: RichTextLabel = $DialogueBubble/Text @onready var dialogue_continue_hint: Label = $DialogueBubble/ContinueHint @onready var choice_area: Control = $DialogueChoiceArea @onready var choice_container: HBoxContainer = $DialogueChoiceArea/DialogueChoices diff --git a/scripts/ui/ccs_cases_2_screen.gd b/scripts/ui/ccs_cases_2_screen.gd index 65ec83d..09832aa 100644 --- a/scripts/ui/ccs_cases_2_screen.gd +++ b/scripts/ui/ccs_cases_2_screen.gd @@ -4,6 +4,11 @@ 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(1500.0, 800.0) +const FOCUSED_INTERVIEW_CASE_ID := "ccs-gastro-duodenal-ulcer-office-001" +const RETIRED_INTERVIEW_TOPIC_IDS := [ + "abdominal_symptoms", "bleeding_alarm_features", "medications", + "medical_history", "social_history", "additional_review" +] var main var stage_root: Control @@ -16,6 +21,7 @@ var current_view := "" var cases_loaded := false var active_loaded := false var pending_action := "" +var restart_case_external_id := "" func _ready() -> void: @@ -39,10 +45,13 @@ func _connect_service() -> void: 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.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" @@ -52,6 +61,7 @@ func on_show(_payload: Variant = null) -> void: func on_hide() -> void: + restart_case_external_id = "" _clear_active_scene() @@ -102,6 +112,9 @@ func _render_attempt(attempt: Dictionary) -> void: 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.") @@ -263,6 +276,37 @@ func _repeat_interview() -> void: 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 + if str(node.get("id", "")) in RETIRED_INTERVIEW_TOPIC_IDS: + return true + for raw_choice in node.get("choices", []): + if str(raw_choice.get("id", "")) in RETIRED_INTERVIEW_TOPIC_IDS: + return true + var patient_text := str(node.get("patientText", "")).to_lower() + return patient_text.contains("stomach bothers me") or patient_text.contains("gnawing ache") + + +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 interview to its latest dizziness and fatigue dialogue...") + 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 @@ -272,6 +316,11 @@ func _on_request_failed(message: String) -> void: 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: active_scene.set_choices_pending(false) else: diff --git a/tests/ccs_cases_2_ui_test.gd b/tests/ccs_cases_2_ui_test.gd index fbacea6..1621eb6 100644 --- a/tests/ccs_cases_2_ui_test.gd +++ b/tests/ccs_cases_2_ui_test.gd @@ -54,6 +54,20 @@ func _run() -> void: await process_frame _assert(screen.current_view == "case_selection", "CCS Cases 2 should begin with compatible case selection") _assert(screen.case_list.get_child_count() == 1, "legacy cases should be excluded from graphical selection") + var retired_attempt := { + "caseExternalId": "ccs-gastro-duodenal-ulcer-office-001" + } + _assert(screen._is_retired_interview_revision(retired_attempt, { + "id": "fatigue_course", "patientText": "My stomach bothers me.", "choices": [] + }), "the retired stomach response should trigger a current-revision restart") + _assert(screen._is_retired_interview_revision(retired_attempt, { + "id": "chief_concern", "patientText": "Dizzy.", + "choices": [{"id": "abdominal_symptoms", "text": "Ask about stomach pain"}] + }), "retired stomach-topic choices should trigger a current-revision restart") + _assert(not screen._is_retired_interview_revision(retired_attempt, { + "id": "chief_concern", "patientText": "Dizzy and tired.", + "choices": [{"id": "dizziness_character", "text": "Ask about dizziness"}] + }), "the focused dizziness and fatigue revision should remain active") service.attempt = _sample_attempt(false) screen._render_state() @@ -219,11 +233,16 @@ func _choice_labels_fit(scene: Control) -> bool: return true -func _label_content_fits(label: Label) -> bool: - var font := label.get_theme_font("font") - var font_size := label.get_theme_font_size("font_size") - var content_height := float(label.get_line_count()) * font.get_height(font_size) - return content_height <= label.size.y + 0.5 +func _label_content_fits(label: Control) -> bool: + if label is RichTextLabel: + return label.get_content_height() <= label.size.y + 0.5 + var plain_label := label as Label + if plain_label == null: + return false + var font := plain_label.get_theme_font("font") + var font_size := plain_label.get_theme_font_size("font_size") + var content_height := float(plain_label.get_line_count()) * font.get_height(font_size) + return content_height <= plain_label.size.y + 0.5 func _assert(condition: bool, message: String) -> void: