Add CCS study tools and case reporting

This commit is contained in:
hz
2026-07-15 10:43:03 -05:00
parent 7256f93002
commit 9f379cda5c
13 changed files with 542 additions and 56 deletions
+21 -17
View File
@@ -2,43 +2,47 @@ extends VBoxContainer
const Style := preload("res://scripts/ui/ui_style.gd")
var main: Control
var list: VBoxContainer
var status: Label
var start_button: Button
func _ready() -> void:
set_anchors_preset(Control.PRESET_FULL_RECT)
add_theme_constant_override("separation", 14)
var back := Button.new(); back.text = "< All exams"; back.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
Style.style_chip(back, Style.CYAN); back.pressed.connect(func(): main.show_screen("selection", "exam")); add_child(back)
var title := Label.new(); title.text = "CCS Case Practice"; Style.set_label(title, 34, Style.TEXT); add_child(title)
var back := Button.new(); back.text = "< Step 3 modes"; back.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
Style.style_chip(back, Style.CYAN); back.pressed.connect(func(): main.show_screen("selection", "step3")); add_child(back)
var title := Label.new(); title.text = "CCS Cases"; Style.set_label(title, 34, Style.TEXT); add_child(title)
var note := Label.new(); note.text = "Manage a simulated patient through examination, orders, treatment, location, and time."; Style.set_label(note, 16, Style.MUTED); add_child(note)
status = Label.new(); Style.set_label(status, 15, Style.CYAN); add_child(status)
var scroll := ScrollContainer.new(); scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL; add_child(scroll)
list = VBoxContainer.new(); list.size_flags_horizontal = Control.SIZE_EXPAND_FILL; list.add_theme_constant_override("separation", 10); scroll.add_child(list)
start_button = Button.new()
start_button.text = "Start Timed Case"
start_button.custom_minimum_size = Vector2(280, 64)
start_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
Style.style_primary_button(start_button)
start_button.pressed.connect(_start)
add_child(start_button)
var spacer := Control.new(); spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL; add_child(spacer)
var history := Button.new(); history.text = "Case History"; history.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
Style.style_chip(history, Style.CYAN); history.pressed.connect(func(): main.show_screen("ccs_history")); add_child(history)
CCSService.cases_changed.connect(_render)
CCSService.attempt_changed.connect(_attempt_ready)
CCSService.request_failed.connect(func(message: String): status.text = message)
CCSService.request_failed.connect(_on_request_failed)
func on_show(_payload: Variant = null) -> void:
status.text = "Loading cases..."
CCSService.load_cases()
func _render() -> void:
for child in list.get_children(): child.queue_free()
status.text = "No published cases are available yet." if CCSService.cases.is_empty() else "%d cases available" % CCSService.cases.size()
for case in CCSService.cases:
var panel := PanelContainer.new(); panel.add_theme_stylebox_override("panel", Style.panel_box(Style.SURFACE_DARK, Color("405475"))); list.add_child(panel)
var row := HBoxContainer.new(); row.add_theme_constant_override("separation", 12); panel.add_child(row)
var details := Label.new(); details.text = "%s\n%s | %d-minute case" % [case.get("title", "Untitled case"), case.get("system", ""), case.get("realTimeMinutes", 20)]
details.size_flags_horizontal = Control.SIZE_EXPAND_FILL; Style.set_label(details, 18, Style.TEXT); row.add_child(details)
var practice := Button.new(); practice.text = "Practice"; Style.style_chip(practice, Style.CYAN); practice.pressed.connect(_start.bind(str(case.get("externalId", "")), "practice")); row.add_child(practice)
var timed := Button.new(); timed.text = "Timed"; Style.style_primary_button(timed); timed.pressed.connect(_start.bind(str(case.get("externalId", "")), "timed")); row.add_child(timed)
start_button.disabled = CCSService.cases.is_empty()
func _start(case_id: String, mode: String) -> void:
func _start() -> void:
status.text = "Starting case..."
CCSService.create_attempt(case_id, mode)
start_button.disabled = true
CCSService.create_attempt("", "timed")
func _attempt_ready() -> void:
if visible and not CCSService.attempt.is_empty(): main.show_screen("ccs_player")
func _on_request_failed(message: String) -> void:
status.text = message
start_button.disabled = CCSService.cases.is_empty()
+68 -9
View File
@@ -14,17 +14,24 @@ var status: Label
var timer: Timer
var end_button: Button
var time_row: HBoxContainer
var report_dialog: Control
var report_button: Button
var workspace: HSplitContainer
func _ready() -> void:
set_anchors_preset(Control.PRESET_FULL_RECT); add_theme_constant_override("separation", 8)
var top := HBoxContainer.new(); add_child(top)
var top := HBoxContainer.new(); top.add_theme_constant_override("separation", 8); add_child(top)
heading = Label.new(); heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL; Style.set_label(heading, 25, Style.TEXT); top.add_child(heading)
clock = Label.new(); Style.set_label(clock, 18, Style.AMBER); top.add_child(clock)
var columns := HSplitContainer.new(); columns.size_flags_vertical = Control.SIZE_EXPAND_FILL; columns.split_offset = 850; add_child(columns)
var chart_panel := VBoxContainer.new(); chart_panel.custom_minimum_size.x = 500; columns.add_child(chart_panel)
summary = RichTextLabel.new(); summary.bbcode_enabled = true; summary.custom_minimum_size.y = 160; chart_panel.add_child(summary)
chart = RichTextLabel.new(); chart.bbcode_enabled = true; chart.size_flags_vertical = Control.SIZE_EXPAND_FILL; chart_panel.add_child(chart)
var clerk_scroll := ScrollContainer.new(); clerk_scroll.custom_minimum_size.x = 430; columns.add_child(clerk_scroll)
var smaller := Button.new(); smaller.text = "A-"; smaller.tooltip_text = "Decrease text size"; smaller.pressed.connect(_zoom.bind(-CCSService.TEXT_SCALE_STEP)); top.add_child(smaller)
var larger := Button.new(); larger.text = "A+"; larger.tooltip_text = "Increase text size"; larger.pressed.connect(_zoom.bind(CCSService.TEXT_SCALE_STEP)); top.add_child(larger)
var clear_highlights := Button.new(); clear_highlights.text = "Clear"; clear_highlights.tooltip_text = "Clear highlights"; clear_highlights.pressed.connect(_clear_highlights); top.add_child(clear_highlights)
report_button = Button.new(); report_button.text = "Report"; report_button.tooltip_text = "Report CCS case"; report_button.pressed.connect(_report); top.add_child(report_button)
workspace = HSplitContainer.new(); workspace.size_flags_vertical = Control.SIZE_EXPAND_FILL; workspace.split_offset = 850; add_child(workspace)
var chart_panel := VBoxContainer.new(); chart_panel.custom_minimum_size.x = 500; workspace.add_child(chart_panel)
summary = RichTextLabel.new(); summary.bbcode_enabled = true; summary.selection_enabled = true; summary.custom_minimum_size.y = 160; summary.set_meta("ccs_region", "summary"); summary.gui_input.connect(_text_input.bind(summary)); chart_panel.add_child(summary)
chart = RichTextLabel.new(); chart.bbcode_enabled = true; chart.selection_enabled = true; chart.size_flags_vertical = Control.SIZE_EXPAND_FILL; chart.set_meta("ccs_region", "chart"); chart.gui_input.connect(_text_input.bind(chart)); chart_panel.add_child(chart)
var clerk_scroll := ScrollContainer.new(); clerk_scroll.custom_minimum_size.x = 430; workspace.add_child(clerk_scroll)
var clerk := VBoxContainer.new(); clerk.size_flags_horizontal = Control.SIZE_EXPAND_FILL; clerk.add_theme_constant_override("separation", 8); clerk_scroll.add_child(clerk)
var order_title := Label.new(); order_title.text = "Order Clerk"; Style.set_label(order_title, 21, Style.CYAN); clerk.add_child(order_title)
search = LineEdit.new(); search.placeholder_text = "Search tests, treatments, and procedures"; search.text_submitted.connect(func(_value): CCSService.search_orders(search.text)); clerk.add_child(search)
@@ -45,6 +52,8 @@ func _ready() -> void:
CCSService.attempt_changed.connect(_render)
CCSService.orders_changed.connect(_render_search)
CCSService.request_failed.connect(func(message: String): status.text = message)
CCSService.report_submitted.connect(_report_submitted)
report_dialog = preload("res://scripts/ui/ccs_report_dialog.gd").new(); add_child(report_dialog)
func on_show(payload: Variant = null) -> void:
status.text = ""
@@ -57,9 +66,8 @@ func _render() -> void:
var attempt: Dictionary = CCSService.attempt
if attempt.is_empty(): return
if str(attempt.get("state", "")) == "completed": main.show_screen("ccs_review", attempt); return
heading.text = "%s | %s" % [attempt.get("caseTitle", "CCS case"), str(attempt.get("location", "")).capitalize()]
summary.text = "[b]%s[/b]\n%s\n\n[b]Vitals[/b] %s" % [attempt.get("patientSummary", ""), attempt.get("reasonForEncounter", ""), _vitals_text(attempt.get("vitals", {}))]
chart.text = "[b]History[/b]\n%s\n\n[b]Appearance[/b]\n%s\n\n[b]Chart[/b]\n%s" % [attempt.get("history", ""), attempt.get("appearance", ""), _chart_text(attempt.get("chart", []))]
heading.text = "CCS Case | %s" % str(attempt.get("location", "")).capitalize()
_render_study_text()
_clear(orders)
for code in attempt.get("activeOrderCodes", []):
var row := HBoxContainer.new(); orders.add_child(row)
@@ -74,12 +82,14 @@ func _render() -> void:
end_button.text = "Submit final orders" if str(attempt.get("state", "")) == "final_orders" else "End case"
time_row.visible = str(attempt.get("state", "")) == "active"
if str(attempt.get("state", "")) == "final_orders": status.text = "Case-end instructions: finalize or cancel orders, then submit."
_apply_text_scale()
func _render_search() -> void:
_clear(search_results)
for order in CCSService.orders:
var button := Button.new(); button.text = "%s (%s)" % [order.get("canonicalName", ""), order.get("category", "")]; button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.pressed.connect(_place_order.bind(str(order.get("code", "")))); search_results.add_child(button)
_scale_controls(search_results)
func _place_order(code: String) -> void:
CCSService.send_command("order", code); search.text = ""; CCSService.orders = []; _render_search()
@@ -109,3 +119,52 @@ func _chart_text(entries: Array) -> String:
var lines: Array[String] = []
for entry in entries: lines.append("T+%d [%s] %s" % [entry.get("simulatedMinute", 0), entry.get("category", "note"), entry.get("text", "")])
return "\n\n".join(lines)
func _summary_plain() -> String:
var attempt := CCSService.attempt
return "%s\n%s\n\nVitals %s" % [attempt.get("patientSummary", ""), attempt.get("reasonForEncounter", ""), _vitals_text(attempt.get("vitals", {}))]
func _chart_plain() -> String:
var attempt := CCSService.attempt
return "History\n%s\n\nAppearance\n%s\n\nChart\n%s" % [attempt.get("history", ""), attempt.get("appearance", ""), _chart_text(attempt.get("chart", []))]
func _render_study_text() -> void:
var id := str(CCSService.attempt.get("id", ""))
summary.text = CCSService.highlighted_text(id, "summary", _summary_plain())
chart.text = CCSService.highlighted_text(id, "chart", _chart_plain())
func _text_input(event: InputEvent, label: RichTextLabel) -> void:
var mouse := event as InputEventMouseButton
if mouse != null and mouse.button_index == MOUSE_BUTTON_LEFT and not mouse.pressed:
call_deferred("_commit_selection", label)
func _commit_selection(label: RichTextLabel) -> void:
var selected := label.get_selected_text().strip_edges()
if selected.is_empty(): return
var start := label.get_selection_from(); var end := label.get_selection_to()
CCSService.toggle_highlight(str(CCSService.attempt.get("id", "")), str(label.get_meta("ccs_region", "")), mini(start, end), maxi(start, end))
label.deselect(); _render_study_text()
func _zoom(delta: float) -> void:
CCSService.adjust_text_scale(delta); _apply_text_scale()
func _apply_text_scale() -> void:
var size := int(round(18.0 * CCSService.text_scale))
for label in [summary, chart]:
label.add_theme_font_size_override("normal_font_size", size); label.add_theme_font_size_override("bold_font_size", size)
_scale_controls(workspace)
func _scale_controls(node: Node) -> void:
if node is Label or node is Button or node is LineEdit:
if not node.has_meta("ccs_base_font_size"): node.set_meta("ccs_base_font_size", node.get_theme_font_size("font_size"))
node.add_theme_font_size_override("font_size", int(round(float(node.get_meta("ccs_base_font_size")) * CCSService.text_scale)))
for child in node.get_children(): _scale_controls(child)
func _clear_highlights() -> void:
CCSService.clear_highlights(str(CCSService.attempt.get("id", ""))); _render_study_text()
func _report() -> void:
report_dialog.show_for(str(CCSService.attempt.get("id", "")))
func _report_submitted(_id: String) -> void:
Style.style_chip(report_button, Style.RED); report_button.tooltip_text = "Report submitted; report another issue"
+76
View File
@@ -0,0 +1,76 @@
extends Control
const Style := preload("res://scripts/ui/ui_style.gd")
const CATEGORIES := [
{"id":"clarity", "label":"Clarity"}, {"id":"medical_accuracy", "label":"Medical accuracy"},
{"id":"case_progression_scoring", "label":"Case progression / scoring"}, {"id":"formatting_media", "label":"Formatting / media"},
{"id":"technical_issue", "label":"Technical issue"}, {"id":"other", "label":"Other"}
]
var group: ButtonGroup
var details: TextEdit
var count: Label
var error: Label
var submit: Button
var form: VBoxContainer
var success: VBoxContainer
var attempt_id := ""
var panel: PanelContainer
func _ready() -> void:
visible = false; top_level = true; mouse_filter = Control.MOUSE_FILTER_STOP; set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
var dim := ColorRect.new(); dim.color = Color(0,0,0,0.7); dim.set_anchors_preset(Control.PRESET_FULL_RECT); add_child(dim)
var center := CenterContainer.new(); center.set_anchors_preset(Control.PRESET_FULL_RECT); add_child(center)
panel = PanelContainer.new(); panel.custom_minimum_size = Vector2(680, 560); panel.add_theme_stylebox_override("panel", Style.panel_box(Style.SURFACE_DARK, Style.RED)); center.add_child(panel)
var root := VBoxContainer.new(); root.add_theme_constant_override("separation", 12); panel.add_child(root)
var title := Label.new(); title.text = "Report CCS case"; Style.set_label(title, 25, Style.TEXT); root.add_child(title)
form = VBoxContainer.new(); form.add_theme_constant_override("separation", 10); root.add_child(form)
group = ButtonGroup.new(); group.allow_unpress = false
var grid := GridContainer.new(); grid.columns = 2; form.add_child(grid)
for category in CATEGORIES:
var option := CheckBox.new(); option.text = category.label; option.set_meta("category", category.id); option.button_group = group; option.toggled.connect(func(_value): _refresh()); grid.add_child(option)
var header := HBoxContainer.new(); form.add_child(header)
var prompt := Label.new(); prompt.text = "Details"; prompt.size_flags_horizontal = Control.SIZE_EXPAND_FILL; header.add_child(prompt)
count = Label.new(); header.add_child(count)
details = TextEdit.new(); details.placeholder_text = "Describe what should be reviewed."; details.custom_minimum_size = Vector2(0, 190); details.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY; details.text_changed.connect(_refresh); form.add_child(details)
error = Label.new(); Style.set_label(error, 14, Style.RED); form.add_child(error)
var actions := HBoxContainer.new(); form.add_child(actions)
var cancel := Button.new(); cancel.text = "Cancel"; cancel.pressed.connect(hide_dialog); actions.add_child(cancel)
submit = Button.new(); submit.text = "Submit report"; submit.size_flags_horizontal = Control.SIZE_EXPAND_FILL; Style.style_primary_button(submit); submit.pressed.connect(_submit); actions.add_child(submit)
success = VBoxContainer.new(); success.visible = false; root.add_child(success)
var success_label := Label.new(); success_label.text = "Report submitted\nThank you for helping improve this case."; success_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER; Style.set_label(success_label, 22, Style.LIME); success.add_child(success_label)
var close := Button.new(); close.text = "Close"; close.pressed.connect(hide_dialog); success.add_child(close)
CCSService.report_submitted.connect(_submitted); CCSService.report_failed.connect(_failed)
resized.connect(_refresh_size)
_refresh()
_refresh_size()
func show_for(value: String) -> void:
attempt_id = value; details.text = ""; error.text = ""; form.visible = true; success.visible = false
for button in group.get_buttons(): button.button_pressed = false
visible = true; move_to_front(); _refresh()
func hide_dialog() -> void:
visible = false
func _unhandled_input(event: InputEvent) -> void:
if visible and event.is_action_pressed("ui_cancel"): hide_dialog(); get_viewport().set_input_as_handled()
func _refresh_size() -> void:
if panel != null: panel.custom_minimum_size = Vector2(minf(680.0, maxf(300.0, size.x - 32.0)), minf(560.0, maxf(420.0, size.y - 32.0)))
func _refresh() -> void:
var length := details.text.strip_edges().length() if details != null else 0
if count != null: count.text = "%d / 2000" % length
if submit != null: submit.disabled = group.get_pressed_button() == null or length < 10 or length > 2000
func _submit() -> void:
var selected := group.get_pressed_button();
if selected == null: return
submit.disabled = true; submit.text = "Submitting..."
CCSService.submit_report(attempt_id, str(selected.get_meta("category", "")), details.text.strip_edges())
func _submitted(_id: String) -> void:
submit.text = "Submit report"; form.visible = false; success.visible = true
func _failed(message: String) -> void:
submit.text = "Submit report"; error.text = message; _refresh()
+1
View File
@@ -0,0 +1 @@
uid://bvabwfcabwpkh
+74 -24
View File
@@ -3,36 +3,86 @@ extends VBoxContainer
const Style := preload("res://scripts/ui/ui_style.gd")
var main: Control
var title: Label
var body: RichTextLabel
var score_body: RichTextLabel
var timeline_body: RichTextLabel
var references: VBoxContainer
var report_dialog: Control
var report_button: Button
var current_attempt := {}
func _ready() -> void:
set_anchors_preset(Control.PRESET_FULL_RECT); add_theme_constant_override("separation", 12)
title = Label.new(); Style.set_label(title, 32, Style.TEXT); add_child(title)
body = RichTextLabel.new(); body.bbcode_enabled = true; body.fit_content = false; body.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.meta_clicked.connect(func(meta): OS.shell_open(str(meta))); add_child(body)
CCSService.attempt_changed.connect(_refresh_loaded_attempt)
set_anchors_preset(Control.PRESET_FULL_RECT); add_theme_constant_override("separation", 10)
var toolbar := HBoxContainer.new(); toolbar.add_theme_constant_override("separation", 8); add_child(toolbar)
title = Label.new(); title.size_flags_horizontal = Control.SIZE_EXPAND_FILL; Style.set_label(title, 30, Style.TEXT); toolbar.add_child(title)
var smaller := Button.new(); smaller.text = "A-"; smaller.tooltip_text = "Decrease text size"; smaller.pressed.connect(_zoom.bind(-CCSService.TEXT_SCALE_STEP)); toolbar.add_child(smaller)
var larger := Button.new(); larger.text = "A+"; larger.tooltip_text = "Increase text size"; larger.pressed.connect(_zoom.bind(CCSService.TEXT_SCALE_STEP)); toolbar.add_child(larger)
var clear := Button.new(); clear.text = "Clear"; clear.tooltip_text = "Clear highlights"; clear.pressed.connect(_clear_highlights); toolbar.add_child(clear)
report_button = Button.new(); report_button.text = "Report"; report_button.tooltip_text = "Report CCS case"; report_button.pressed.connect(_report); toolbar.add_child(report_button)
var scroll := ScrollContainer.new(); scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL; add_child(scroll)
var content := VBoxContainer.new(); content.size_flags_horizontal = Control.SIZE_EXPAND_FILL; content.add_theme_constant_override("separation", 14); scroll.add_child(content)
score_body = _study_label("score"); score_body.custom_minimum_size.y = 200; content.add_child(score_body)
timeline_body = _study_label("timeline"); timeline_body.custom_minimum_size.y = 260; content.add_child(timeline_body)
references = VBoxContainer.new(); content.add_child(references)
CCSService.attempt_changed.connect(_refresh_loaded_attempt); CCSService.report_submitted.connect(_report_submitted)
report_dialog = preload("res://scripts/ui/ccs_report_dialog.gd").new(); add_child(report_dialog)
var row := HBoxContainer.new(); add_child(row)
var cases := Button.new(); cases.text = "More cases"; Style.style_primary_button(cases); cases.pressed.connect(func(): main.show_screen("ccs_hub")); row.add_child(cases)
var history := Button.new(); history.text = "Case history"; history.pressed.connect(func(): main.show_screen("ccs_history")); row.add_child(history)
func on_show(payload: Variant = null) -> void:
if typeof(payload) == TYPE_STRING:
CCSService.load_attempt(str(payload))
return
var attempt: Dictionary = payload if typeof(payload) == TYPE_DICTIONARY else CCSService.attempt
if attempt.is_empty(): return
var score: Dictionary = attempt.get("score", {})
title.text = "%s: %d%%" % [attempt.get("caseTitle", "Case review"), int(round(float(score.get("percent", 0))))]
var text := "[b]Scoring review[/b]\n"
for criterion in score.get("criteria", []):
var status := str(criterion.get("status", "missed"))
text += "\n[color=%s][b]%s %s%d[/b][/color]\n%s\n" % [Style.LIME.to_html() if status in ["credited", "avoided"] else Style.RED.to_html(), status.capitalize(), "+" if int(criterion.get("points", 0)) >= 0 else "", criterion.get("points", 0), criterion.get("rationale", "")]
text += "\n[b]Case timeline[/b]\n"
for entry in attempt.get("chart", []): text += "\nT+%d [%s] %s" % [entry.get("simulatedMinute", 0), entry.get("category", "note"), entry.get("text", "")]
if not attempt.get("references", []).is_empty():
text += "\n\n[b]References[/b]\n"
for reference in attempt.get("references", []): text += "\n[url=%s]%s[/url]" % [reference.get("url", ""), reference.get("title", reference.get("url", "Reference"))]
body.text = text
func _study_label(region: String) -> RichTextLabel:
var label := RichTextLabel.new(); label.bbcode_enabled = true; label.fit_content = true; label.selection_enabled = true; label.set_meta("ccs_region", region); label.gui_input.connect(_text_input.bind(label)); return label
func on_show(payload: Variant = null) -> void:
if typeof(payload) == TYPE_STRING: CCSService.load_attempt(str(payload)); return
current_attempt = payload if typeof(payload) == TYPE_DICTIONARY else CCSService.attempt
if current_attempt.is_empty(): return
var score: Dictionary = current_attempt.get("score", {})
title.text = "%s: %d%%" % [current_attempt.get("caseTitle", "Case review"), int(round(float(score.get("percent", 0))))]
_render_text(); _render_references(); _apply_text_scale()
func _score_plain() -> String:
var lines: Array[String] = ["Scoring review"]
for criterion in current_attempt.get("score", {}).get("criteria", []):
lines.append("%s %s%d\n%s" % [str(criterion.get("status", "missed")).capitalize(), "+" if int(criterion.get("points", 0)) >= 0 else "", criterion.get("points", 0), criterion.get("rationale", "")])
return "\n\n".join(lines)
func _timeline_plain() -> String:
var lines: Array[String] = ["Case timeline"]
for entry in current_attempt.get("chart", []): lines.append("T+%d [%s] %s" % [entry.get("simulatedMinute", 0), entry.get("category", "note"), entry.get("text", "")])
return "\n\n".join(lines)
func _render_text() -> void:
var id := str(current_attempt.get("id", ""))
score_body.text = CCSService.highlighted_text(id, "score", _score_plain())
timeline_body.text = CCSService.highlighted_text(id, "timeline", _timeline_plain())
func _render_references() -> void:
for child in references.get_children(): child.queue_free()
if current_attempt.get("references", []).is_empty(): return
var heading := Label.new(); heading.text = "References"; Style.set_label(heading, int(round(19.0 * CCSService.text_scale)), Style.TEXT); references.add_child(heading)
for reference in current_attempt.get("references", []):
var link := LinkButton.new(); link.text = str(reference.get("title", reference.get("url", "Reference"))); link.pressed.connect(OS.shell_open.bind(str(reference.get("url", "")))); link.add_theme_font_size_override("font_size", int(round(16.0 * CCSService.text_scale))); references.add_child(link)
func _text_input(event: InputEvent, label: RichTextLabel) -> void:
var mouse := event as InputEventMouseButton
if mouse != null and mouse.button_index == MOUSE_BUTTON_LEFT and not mouse.pressed: call_deferred("_commit_selection", label)
func _commit_selection(label: RichTextLabel) -> void:
if label.get_selected_text().strip_edges().is_empty(): return
var start := label.get_selection_from(); var end := label.get_selection_to()
CCSService.toggle_highlight(str(current_attempt.get("id", "")), str(label.get_meta("ccs_region", "")), mini(start,end), maxi(start,end)); label.deselect(); _render_text()
func _zoom(delta: float) -> void:
CCSService.adjust_text_scale(delta); _apply_text_scale(); _render_references()
func _apply_text_scale() -> void:
var size := int(round(18.0 * CCSService.text_scale))
for label in [score_body, timeline_body]: label.add_theme_font_size_override("normal_font_size", size); label.add_theme_font_size_override("bold_font_size", size)
func _clear_highlights() -> void:
CCSService.clear_highlights(str(current_attempt.get("id", ""))); _render_text()
func _report() -> void: report_dialog.show_for(str(current_attempt.get("id", "")))
func _report_submitted(_id: String) -> void: Style.style_chip(report_button, Style.RED); report_button.tooltip_text = "Report submitted; report another issue"
func _refresh_loaded_attempt() -> void:
if visible and str(CCSService.attempt.get("state", "")) == "completed": on_show(CCSService.attempt)
+49 -4
View File
@@ -14,7 +14,10 @@ var retry_button: Button
var simulation_button: Button
var simulation_status_label: Label
var simulation_preview_link: LinkButton
var ccs_button: Button
var ccs_status_label: Label
var pending_simulation_launch := false
var pending_ccs_launch := false
var current_stage := "exam"
var pending_question_bank_launch := false
@@ -35,6 +38,10 @@ func _ready() -> void:
ExamSimulationService.request_failed.connect(_on_simulation_failed)
if not AuthState.auth_changed.is_connected(_refresh_simulation_preview_visibility):
AuthState.auth_changed.connect(_refresh_simulation_preview_visibility)
if not CCSService.attempt_changed.is_connected(_on_ccs_attempt_changed):
CCSService.attempt_changed.connect(_on_ccs_attempt_changed)
if not CCSService.request_failed.is_connected(_on_ccs_failed):
CCSService.request_failed.connect(_on_ccs_failed)
_build_exam_view()
_build_mode_view()
@@ -53,6 +60,7 @@ func on_show(payload: Variant = null) -> void:
func on_hide() -> void:
pending_question_bank_launch = false
pending_ccs_launch = false
func _build_exam_view() -> void:
@@ -88,7 +96,6 @@ func _build_exam_view() -> void:
_add_exam_option("STEP 1", "Foundational sciences", false)
_add_exam_option("STEP 2", "Clinical knowledge", false)
_add_exam_option("STEP 3", "Advanced clinical management", true)
_add_exam_option("CCS Cases", "Interactive case simulations", true, "ccs")
func _build_mode_view() -> void:
@@ -159,6 +166,21 @@ func _build_mode_view() -> void:
simulation_button.pressed.connect(_select_simulation)
mode_grid.add_child(simulation_button)
ccs_button = Button.new()
ccs_button.text = "CCS Cases\nInteractive case simulations | Available"
ccs_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
ccs_button.custom_minimum_size = Vector2(360, 150)
ccs_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
ccs_button.add_theme_font_size_override("font_size", 20)
Style.style_primary_button(ccs_button)
ccs_button.pressed.connect(_select_ccs)
mode_grid.add_child(ccs_button)
ccs_status_label = Label.new()
ccs_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(ccs_status_label, 15, Style.MUTED)
mode_view.add_child(ccs_status_label)
simulation_status_label = Label.new()
simulation_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(simulation_status_label, 15, Style.MUTED)
@@ -197,7 +219,7 @@ func _build_mode_view() -> void:
mode_view.add_child(spacer)
func _add_exam_option(option_name: String, description: String, available: bool, destination: String = "step3") -> void:
func _add_exam_option(option_name: String, description: String, available: bool) -> void:
var button := Button.new()
button.text = "%s\n%s | %s" % [option_name, description, "Available" if available else "Coming Soon"]
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
@@ -208,8 +230,7 @@ func _add_exam_option(option_name: String, description: String, available: bool,
button.disabled = not available
if available:
Style.style_primary_button(button)
if destination == "ccs": button.pressed.connect(func(): main.show_screen("ccs_hub"))
else: button.pressed.connect(func(): _show_stage("step3"))
button.pressed.connect(func(): _show_stage("step3"))
exam_grid.add_child(button)
@@ -231,6 +252,30 @@ func _select_question_bank() -> void:
_refresh_bank_state()
func _select_ccs() -> void:
pending_ccs_launch = true
ccs_button.disabled = true
ccs_status_label.text = "Selecting a timed case..."
CCSService.create_attempt("", "timed")
func _on_ccs_attempt_changed() -> void:
if not pending_ccs_launch or CCSService.attempt.is_empty():
return
pending_ccs_launch = false
ccs_button.disabled = false
ccs_status_label.text = ""
main.show_screen("ccs_player")
func _on_ccs_failed(message: String) -> void:
if not pending_ccs_launch:
return
pending_ccs_launch = false
ccs_button.disabled = false
ccs_status_label.text = message
func _retry_question_bank() -> void:
pending_question_bank_launch = true
QuestionBankService.retry_now()