Add interactive CCS case practice
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
extends Node
|
||||
|
||||
const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd")
|
||||
const DEFAULT_API_URL := "https://s3q.ranaze.com/api/ccs"
|
||||
|
||||
signal cases_changed
|
||||
signal attempt_changed
|
||||
signal orders_changed
|
||||
signal history_changed
|
||||
signal prediction_changed
|
||||
signal request_failed(message: String)
|
||||
|
||||
var api_url := DEFAULT_API_URL
|
||||
var request: HTTPRequest
|
||||
var cases: Array = []
|
||||
var attempt := {}
|
||||
var orders: Array = []
|
||||
var history: Array = []
|
||||
var prediction := {}
|
||||
var is_loading := false
|
||||
var pending_action := ""
|
||||
var queue: Array = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
api_url = EndpointConfig.resolve(str(ProjectSettings.get_setting("step3/ccs_api_url", DEFAULT_API_URL)), "/api/ccs")
|
||||
request = HTTPRequest.new()
|
||||
request.request_completed.connect(_on_request_completed)
|
||||
add_child(request)
|
||||
|
||||
|
||||
func load_cases() -> void:
|
||||
_send("cases", HTTPClient.METHOD_GET, "/cases")
|
||||
|
||||
|
||||
func create_attempt(case_external_id: String, mode: String) -> void:
|
||||
_send("attempt", HTTPClient.METHOD_POST, "/attempts", {"caseExternalId": case_external_id, "mode": mode})
|
||||
|
||||
|
||||
func load_attempt(attempt_id: String) -> void:
|
||||
_send("attempt", HTTPClient.METHOD_GET, "/attempts/%s" % attempt_id.uri_encode())
|
||||
|
||||
|
||||
func send_command(type: String, code: String = "", minutes: int = 0) -> void:
|
||||
if attempt.is_empty():
|
||||
return
|
||||
_send("attempt", HTTPClient.METHOD_POST, "/attempts/%s/commands" % str(attempt.get("id", "")).uri_encode(), {
|
||||
"commandId": "%d-%d" % [Time.get_ticks_usec(), randi()],
|
||||
"expectedRevision": int(attempt.get("revision", 0)),
|
||||
"type": type,
|
||||
"code": code,
|
||||
"minutes": minutes
|
||||
})
|
||||
|
||||
|
||||
func search_orders(query: String) -> void:
|
||||
if query.strip_edges().length() < 2:
|
||||
orders = []
|
||||
orders_changed.emit()
|
||||
return
|
||||
_send("orders", HTTPClient.METHOD_GET, "/orders/search?q=%s&limit=20" % query.strip_edges().uri_encode())
|
||||
|
||||
|
||||
func load_history() -> void:
|
||||
_send("history", HTTPClient.METHOD_GET, "/history?limit=50")
|
||||
|
||||
|
||||
func load_prediction() -> void:
|
||||
_send("prediction", HTTPClient.METHOD_GET, "/prediction")
|
||||
|
||||
|
||||
func abandon() -> void:
|
||||
if attempt.is_empty():
|
||||
return
|
||||
_send("abandon", HTTPClient.METHOD_POST, "/attempts/%s/abandon" % str(attempt.get("id", "")).uri_encode())
|
||||
|
||||
|
||||
func deadline_unix() -> float:
|
||||
var value := str(attempt.get("finalOrdersDeadlineUtc", attempt.get("deadlineUtc", "")))
|
||||
return 0.0 if value.is_empty() or value == "<null>" else Time.get_unix_time_from_datetime_string(value)
|
||||
|
||||
|
||||
func _send(action: String, method: int, suffix: String, payload: Dictionary = {}) -> void:
|
||||
var url := "%s%s" % [api_url.rstrip("/"), suffix]
|
||||
if is_loading:
|
||||
queue.append({"action": action, "method": method, "url": url, "payload": payload.duplicate(true)})
|
||||
return
|
||||
if AuthState.access_token.is_empty():
|
||||
return
|
||||
is_loading = true
|
||||
pending_action = action
|
||||
var headers := PackedStringArray(["Authorization: Bearer %s" % AuthState.access_token])
|
||||
var body := ""
|
||||
if not payload.is_empty():
|
||||
headers.append("Content-Type: application/json")
|
||||
body = JSON.stringify(payload)
|
||||
var error := request.request(url, headers, method, body)
|
||||
if error != OK:
|
||||
is_loading = false
|
||||
request_failed.emit("The CCS request could not be started.")
|
||||
|
||||
|
||||
func _on_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
var action := pending_action
|
||||
pending_action = ""
|
||||
is_loading = false
|
||||
call_deferred("_process_queue")
|
||||
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||
var detail := body.get_string_from_utf8().strip_edges().left(240)
|
||||
request_failed.emit("CCS request failed (%d). %s" % [response_code, detail])
|
||||
return
|
||||
if response_code == 204:
|
||||
attempt = {}
|
||||
attempt_changed.emit()
|
||||
return
|
||||
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
|
||||
if action in ["cases", "orders", "history"] and typeof(parsed) != TYPE_ARRAY:
|
||||
request_failed.emit("The CCS API returned an invalid list.")
|
||||
return
|
||||
if action not in ["cases", "orders", "history"] and typeof(parsed) != TYPE_DICTIONARY:
|
||||
request_failed.emit("The CCS API returned an invalid response.")
|
||||
return
|
||||
match action:
|
||||
"cases": cases = parsed; cases_changed.emit()
|
||||
"orders": orders = parsed; orders_changed.emit()
|
||||
"history": history = parsed; history_changed.emit()
|
||||
"prediction": prediction = parsed; prediction_changed.emit()
|
||||
_:
|
||||
attempt = parsed
|
||||
attempt_changed.emit()
|
||||
|
||||
|
||||
func _process_queue() -> void:
|
||||
if is_loading or queue.is_empty():
|
||||
return
|
||||
var queued: Dictionary = queue.pop_front()
|
||||
var suffix := str(queued.get("url", "")).trim_prefix(api_url.rstrip("/"))
|
||||
_send(str(queued.get("action", "")), int(queued.get("method", HTTPClient.METHOD_GET)), suffix, queued.get("payload", {}))
|
||||
@@ -0,0 +1 @@
|
||||
uid://d3gcghtwmgxtt
|
||||
@@ -0,0 +1,41 @@
|
||||
extends VBoxContainer
|
||||
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
var main: Control
|
||||
var list: VBoxContainer
|
||||
var prediction: Label
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT); add_theme_constant_override("separation", 12)
|
||||
var back := Button.new(); back.text = "< Profile"; back.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN; back.pressed.connect(func(): main.show_screen("profile")); add_child(back)
|
||||
var title := Label.new(); title.text = "CCS Case History"; Style.set_label(title, 32, Style.TEXT); add_child(title)
|
||||
prediction = Label.new(); prediction.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART; Style.set_label(prediction, 16, Style.CYAN); add_child(prediction)
|
||||
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", 8); scroll.add_child(list)
|
||||
CCSService.history_changed.connect(_render)
|
||||
CCSService.prediction_changed.connect(_render_prediction)
|
||||
|
||||
func on_show(_payload: Variant = null) -> void:
|
||||
CCSService.load_history(); CCSService.load_prediction()
|
||||
|
||||
func _render() -> void:
|
||||
for child in list.get_children(): child.queue_free()
|
||||
if CCSService.history.is_empty():
|
||||
var empty := Label.new(); empty.text = "No case attempts yet."; Style.set_label(empty, 16, Style.MUTED); list.add_child(empty); return
|
||||
for item in CCSService.history:
|
||||
var button := Button.new(); button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
var score: Variant = item.get("score", null)
|
||||
button.text = "%s\n%s | %s%s" % [item.get("caseTitle", "Case"), str(item.get("mode", "")).capitalize(), str(item.get("state", "")).capitalize(), " | %d%%" % int(round(float(score))) if score != null else ""]
|
||||
button.disabled = str(item.get("state", "")) == "abandoned"
|
||||
button.pressed.connect(_open.bind(str(item.get("id", "")), str(item.get("state", "")))); list.add_child(button)
|
||||
|
||||
func _open(id: String, state: String) -> void:
|
||||
main.show_screen("ccs_review" if state == "completed" else "ccs_player", id)
|
||||
|
||||
func _render_prediction() -> void:
|
||||
var data: Dictionary = CCSService.prediction
|
||||
if data.get("combinedPredictedScore", null) != null:
|
||||
prediction.text = "Provisional combined prediction: %d (range %d-%d) | CCS %.0f%% from %d first timed cases" % [data.get("combinedPredictedScore", 0), data.get("rangeLow", 0), data.get("rangeHigh", 0), data.get("ccsScore", 0), data.get("contributingCases", 0)]
|
||||
elif data.get("ccsScore", null) != null:
|
||||
prediction.text = "CCS performance: %.0f%% from %d first timed cases. Complete a Day 1 simulation for a combined prediction." % [data.get("ccsScore", 0), data.get("contributingCases", 0)]
|
||||
else: prediction.text = "Complete timed CCS cases to begin estimating CCS performance."
|
||||
@@ -0,0 +1 @@
|
||||
uid://cs2vdscnt4h4j
|
||||
@@ -0,0 +1,44 @@
|
||||
extends VBoxContainer
|
||||
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
var main: Control
|
||||
var list: VBoxContainer
|
||||
var status: Label
|
||||
|
||||
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 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)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
func _start(case_id: String, mode: String) -> void:
|
||||
status.text = "Starting case..."
|
||||
CCSService.create_attempt(case_id, mode)
|
||||
|
||||
func _attempt_ready() -> void:
|
||||
if visible and not CCSService.attempt.is_empty(): main.show_screen("ccs_player")
|
||||
@@ -0,0 +1 @@
|
||||
uid://d2d66msil5rl5
|
||||
@@ -0,0 +1,111 @@
|
||||
extends VBoxContainer
|
||||
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
var main: Control
|
||||
var heading: Label
|
||||
var clock: Label
|
||||
var summary: RichTextLabel
|
||||
var chart: RichTextLabel
|
||||
var orders: VBoxContainer
|
||||
var search: LineEdit
|
||||
var search_results: VBoxContainer
|
||||
var actions: VBoxContainer
|
||||
var status: Label
|
||||
var timer: Timer
|
||||
var end_button: Button
|
||||
var time_row: HBoxContainer
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT); add_theme_constant_override("separation", 8)
|
||||
var top := HBoxContainer.new(); 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 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)
|
||||
var find := Button.new(); find.text = "Search"; find.pressed.connect(func(): CCSService.search_orders(search.text)); clerk.add_child(find)
|
||||
search_results = VBoxContainer.new(); clerk.add_child(search_results)
|
||||
var active_title := Label.new(); active_title.text = "Active orders"; Style.set_label(active_title, 18, Style.TEXT); clerk.add_child(active_title)
|
||||
orders = VBoxContainer.new(); clerk.add_child(orders)
|
||||
var exam_title := Label.new(); exam_title.text = "Physical examination"; Style.set_label(exam_title, 18, Style.TEXT); clerk.add_child(exam_title)
|
||||
actions = VBoxContainer.new(); clerk.add_child(actions)
|
||||
time_row = HBoxContainer.new(); clerk.add_child(time_row)
|
||||
for minutes in [1, 10, 60, 1440]:
|
||||
var button := Button.new(); button.text = "+%s" % ("1 day" if minutes == 1440 else "%d min" % minutes); button.pressed.connect(CCSService.send_command.bind("advance", "", minutes)); time_row.add_child(button)
|
||||
var reassess := Button.new(); reassess.text = "Interval history"; reassess.pressed.connect(CCSService.send_command.bind("interval_history", "", 0)); clerk.add_child(reassess)
|
||||
end_button = Button.new(); end_button.text = "End case"; Style.style_chip(end_button, Style.AMBER); end_button.pressed.connect(_end_case); clerk.add_child(end_button)
|
||||
var leave := Button.new(); leave.text = "Leave case"; Style.style_chip(leave, Style.RED); leave.pressed.connect(_leave); clerk.add_child(leave)
|
||||
status = Label.new(); Style.set_label(status, 14, Style.RED); add_child(status)
|
||||
timer = Timer.new(); timer.wait_time = 1; timer.timeout.connect(_tick); add_child(timer)
|
||||
CCSService.attempt_changed.connect(_render)
|
||||
CCSService.orders_changed.connect(_render_search)
|
||||
CCSService.request_failed.connect(func(message: String): status.text = message)
|
||||
|
||||
func on_show(payload: Variant = null) -> void:
|
||||
status.text = ""
|
||||
if typeof(payload) == TYPE_STRING and not str(payload).is_empty(): CCSService.load_attempt(str(payload))
|
||||
_render(); timer.start()
|
||||
|
||||
func on_hide() -> void: timer.stop()
|
||||
|
||||
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", []))]
|
||||
_clear(orders)
|
||||
for code in attempt.get("activeOrderCodes", []):
|
||||
var row := HBoxContainer.new(); orders.add_child(row)
|
||||
var label := Label.new(); label.text = str(code); label.size_flags_horizontal = Control.SIZE_EXPAND_FILL; row.add_child(label)
|
||||
var cancel := Button.new(); cancel.text = "Cancel"; cancel.pressed.connect(CCSService.send_command.bind("cancel_order", str(code), 0)); row.add_child(cancel)
|
||||
_clear(actions)
|
||||
for exam in attempt.get("availableExams", []):
|
||||
var button := Button.new(); button.text = str(exam.get("name", exam.get("code", "Exam"))); button.pressed.connect(CCSService.send_command.bind("exam", str(exam.get("code", "")), 0)); actions.add_child(button)
|
||||
for location in attempt.get("allowedLocations", []):
|
||||
var button := Button.new(); button.text = "Move to %s" % str(location).capitalize(); button.pressed.connect(CCSService.send_command.bind("location", str(location), 0)); actions.add_child(button)
|
||||
_tick()
|
||||
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."
|
||||
|
||||
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)
|
||||
|
||||
func _place_order(code: String) -> void:
|
||||
CCSService.send_command("order", code); search.text = ""; CCSService.orders = []; _render_search()
|
||||
|
||||
func _tick() -> void:
|
||||
var deadline := CCSService.deadline_unix()
|
||||
if deadline <= 0: clock.text = "Practice | Case time %d min" % int(CCSService.attempt.get("simulatedMinute", 0)); return
|
||||
var remaining := maxi(0, int(ceil(deadline - Time.get_unix_time_from_system())))
|
||||
clock.text = "%02d:%02d | Case time %d min" % [floori(float(remaining) / 60.0), remaining % 60, int(CCSService.attempt.get("simulatedMinute", 0))]
|
||||
if remaining == 0 and not CCSService.is_loading: CCSService.load_attempt(str(CCSService.attempt.get("id", "")))
|
||||
|
||||
func _leave() -> void:
|
||||
CCSService.abandon(); main.show_screen("ccs_hub")
|
||||
|
||||
func _end_case() -> void:
|
||||
CCSService.send_command("exit_case" if str(CCSService.attempt.get("state", "")) == "final_orders" else "end_case")
|
||||
|
||||
func _clear(parent: Node) -> void:
|
||||
for child in parent.get_children(): child.queue_free()
|
||||
|
||||
func _vitals_text(vitals: Dictionary) -> String:
|
||||
var parts: Array[String] = []
|
||||
for key in vitals: parts.append("%s: %s" % [str(key), str(vitals[key])])
|
||||
return " | ".join(parts)
|
||||
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://djuxbj74nhiji
|
||||
@@ -0,0 +1,38 @@
|
||||
extends VBoxContainer
|
||||
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
var main: Control
|
||||
var title: Label
|
||||
var body: RichTextLabel
|
||||
|
||||
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)
|
||||
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 _refresh_loaded_attempt() -> void:
|
||||
if visible and str(CCSService.attempt.get("state", "")) == "completed": on_show(CCSService.attempt)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cjpj86t7rnvy6
|
||||
+10
-1
@@ -47,7 +47,7 @@ func show_screen(screen_name: String, payload: Variant = null) -> void:
|
||||
current_screen_name = screen_name
|
||||
screen.visible = true
|
||||
if header_panel != null:
|
||||
header_panel.visible = screen_name != "login" and screen_name != "player"
|
||||
header_panel.visible = screen_name not in ["login", "player", "ccs_player"]
|
||||
if screen.has_method("on_show"):
|
||||
screen.on_show(payload)
|
||||
|
||||
@@ -179,6 +179,15 @@ func _build_ui() -> void:
|
||||
screens_container.add_child(simulation_history)
|
||||
screens["simulation_history"] = simulation_history
|
||||
|
||||
var ccs_hub := preload("res://scripts/ui/ccs_hub.gd").new()
|
||||
ccs_hub.name = "CcsHub"; ccs_hub.main = self; screens_container.add_child(ccs_hub); screens["ccs_hub"] = ccs_hub
|
||||
var ccs_player := preload("res://scripts/ui/ccs_player.gd").new()
|
||||
ccs_player.name = "CcsPlayer"; ccs_player.main = self; screens_container.add_child(ccs_player); screens["ccs_player"] = ccs_player
|
||||
var ccs_review := preload("res://scripts/ui/ccs_review.gd").new()
|
||||
ccs_review.name = "CcsReview"; ccs_review.main = self; screens_container.add_child(ccs_review); screens["ccs_review"] = ccs_review
|
||||
var ccs_history := preload("res://scripts/ui/ccs_history.gd").new()
|
||||
ccs_history.name = "CcsHistory"; ccs_history.main = self; screens_container.add_child(ccs_history); screens["ccs_history"] = ccs_history
|
||||
|
||||
var profile := preload("res://scripts/ui/progress_profile.gd").new()
|
||||
profile.name = "ProgressProfile"
|
||||
profile.main = self
|
||||
|
||||
@@ -63,6 +63,12 @@ func _ready() -> void:
|
||||
simulations.pressed.connect(func(): main.show_screen("simulation_history"))
|
||||
row.add_child(simulations)
|
||||
|
||||
var ccs_history := Button.new()
|
||||
ccs_history.text = "CCS History"
|
||||
Style.style_chip(ccs_history, Style.CYAN)
|
||||
ccs_history.pressed.connect(func(): main.show_screen("ccs_history"))
|
||||
row.add_child(ccs_history)
|
||||
|
||||
reset_qbank_button = Button.new()
|
||||
reset_qbank_button.text = "Reset QBank"
|
||||
Style.style_chip(reset_qbank_button, Style.AMBER)
|
||||
|
||||
@@ -88,7 +88,7 @@ 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", false)
|
||||
_add_exam_option("CCS Cases", "Interactive case simulations", true, "ccs")
|
||||
|
||||
|
||||
func _build_mode_view() -> void:
|
||||
@@ -197,7 +197,7 @@ func _build_mode_view() -> void:
|
||||
mode_view.add_child(spacer)
|
||||
|
||||
|
||||
func _add_exam_option(option_name: String, description: String, available: bool) -> void:
|
||||
func _add_exam_option(option_name: String, description: String, available: bool, destination: String = "step3") -> 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,7 +208,8 @@ func _add_exam_option(option_name: String, description: String, available: bool)
|
||||
button.disabled = not available
|
||||
if available:
|
||||
Style.style_primary_button(button)
|
||||
button.pressed.connect(func(): _show_stage("step3"))
|
||||
if destination == "ccs": button.pressed.connect(func(): main.show_screen("ccs_hub"))
|
||||
else: button.pressed.connect(func(): _show_stage("step3"))
|
||||
exam_grid.add_child(button)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user