Add study session goals and summaries
This commit is contained in:
@@ -197,6 +197,7 @@ func _on_request_completed(result: int, response_code: int, _headers: PackedStri
|
||||
request_failed.emit("The simulation API returned an invalid response.")
|
||||
return
|
||||
CreditsService.apply_award_response(parsed)
|
||||
StudySessionService.apply_response(parsed)
|
||||
if action == "readiness":
|
||||
readiness = parsed
|
||||
readiness_changed.emit()
|
||||
|
||||
@@ -437,6 +437,7 @@ func _enqueue_attempt(kind: String, question_id: String, choice_id: String, pres
|
||||
"presented_choice_ids": presented_choice_ids.duplicate(),
|
||||
"duration_seconds": maxi(0, duration_seconds),
|
||||
"met_time_limit": met_time_limit,
|
||||
"study_session_id": StudySessionService.active_session_id,
|
||||
"retry_count": 0
|
||||
})
|
||||
_save_attempt_queue()
|
||||
@@ -472,6 +473,11 @@ func _process_attempt_queue() -> void:
|
||||
"durationSeconds": int(queued.get("duration_seconds", 0)),
|
||||
"metTimeLimit": bool(queued.get("met_time_limit", true))
|
||||
})
|
||||
var study_session_id := str(queued.get("study_session_id", ""))
|
||||
if not study_session_id.is_empty():
|
||||
var decoded: Dictionary = JSON.parse_string(body)
|
||||
decoded["studySessionId"] = study_session_id
|
||||
body = JSON.stringify(decoded)
|
||||
if str(queued.get("kind", "answer")) == "skip":
|
||||
url = "%s/%s/skip" % [question_api_base_url.rstrip("/"), question_id.uri_encode()]
|
||||
body = ""
|
||||
@@ -546,6 +552,8 @@ func _apply_first_attempt_response(parsed: Dictionary) -> void:
|
||||
_update_question_elo(question_id, question_elo)
|
||||
_apply_rating_change(rating)
|
||||
CreditsService.apply_award_response(parsed)
|
||||
# Let Tutor users inspect the final answer before the summary replaces the player.
|
||||
StudySessionService.apply_response(parsed, true)
|
||||
choice_percentages_updated.emit(question_id, percentages)
|
||||
question_rating_updated.emit(question_id, question_elo, rating)
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ var retained_count := 0
|
||||
var outbox: Array = []
|
||||
var active_account_key := ""
|
||||
var step3_system := ""
|
||||
var target_count := 20
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -49,8 +50,9 @@ func _ready() -> void:
|
||||
_on_auth_changed()
|
||||
|
||||
|
||||
func start_pass(requested_step3_system: String = "") -> void:
|
||||
func start_pass(requested_step3_system: String = "", requested_count: int = 20) -> void:
|
||||
step3_system = requested_step3_system
|
||||
target_count = clampi(requested_count, 1, 20)
|
||||
excluded_question_ids.clear()
|
||||
reviewed_count = 0
|
||||
cleared_count = 0
|
||||
@@ -59,6 +61,10 @@ func start_pass(requested_step3_system: String = "") -> void:
|
||||
|
||||
|
||||
func load_next() -> void:
|
||||
if reviewed_count >= target_count:
|
||||
StudySessionService.emit_deferred_summary()
|
||||
pass_completed.emit(get_summary())
|
||||
return
|
||||
if loading:
|
||||
return
|
||||
if not AuthState.can_send_authenticated_request():
|
||||
@@ -99,7 +105,8 @@ func record_first_submission(question: Dictionary, choice_id: String, presented_
|
||||
"cycle": current_cycle,
|
||||
"choiceId": choice_id,
|
||||
"presentedChoiceIds": presented_choice_ids.duplicate(),
|
||||
"durationSeconds": clampi(duration_seconds, 0, 86400)
|
||||
"durationSeconds": clampi(duration_seconds, 0, 86400),
|
||||
"studySessionId": StudySessionService.active_session_id
|
||||
}
|
||||
outbox.append({"account": _account_key(), "payload": payload, "retry_count": 0})
|
||||
_save_outbox()
|
||||
@@ -191,6 +198,7 @@ func _on_attempt_completed(result: int, response_code: int, _headers_value: Pack
|
||||
var synced_is_correct := false
|
||||
if typeof(parsed) == TYPE_DICTIONARY:
|
||||
CreditsService.apply_award_response(parsed)
|
||||
StudySessionService.apply_response(parsed, true)
|
||||
pending_count = int(parsed.get("pendingCount", parsed.get("pending_count", pending_count)))
|
||||
synced_question_id = str(parsed.get("questionExternalId", parsed.get("question_external_id", "")))
|
||||
synced_is_correct = bool(parsed.get("isCorrect", parsed.get("is_correct", false)))
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
extends Node
|
||||
|
||||
signal session_started
|
||||
signal progress_changed
|
||||
signal session_completed(summary: Dictionary)
|
||||
signal session_failed(message: String)
|
||||
|
||||
const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd")
|
||||
const DEFAULT_URL := "https://us3q.ranaze.com/api/study-sessions"
|
||||
|
||||
var api_url := DEFAULT_URL
|
||||
var request: HTTPRequest
|
||||
var active_session_id := ""
|
||||
var progress: Dictionary = {}
|
||||
var summary: Dictionary = {}
|
||||
var pending_action := ""
|
||||
var pending_launch: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
api_url = EndpointConfig.resolve(DEFAULT_URL, "/api/study-sessions")
|
||||
request = HTTPRequest.new()
|
||||
request.timeout = 25.0
|
||||
request.request_completed.connect(_on_completed)
|
||||
add_child(request)
|
||||
AuthState.auth_changed.connect(_on_auth_changed)
|
||||
|
||||
|
||||
func start(goal_type: String, target_value: int, mode: String, step3_system: String = "", launch: Dictionary = {}) -> void:
|
||||
if not AuthState.can_send_authenticated_request():
|
||||
session_failed.emit("Login is required to start a study session.")
|
||||
return
|
||||
pending_launch = launch.duplicate(true)
|
||||
summary = {}
|
||||
_send("create", HTTPClient.METHOD_POST, "", {
|
||||
"clientSessionId": _new_uuid(), "goalType": goal_type, "targetValue": target_value,
|
||||
"mode": mode, "step3System": step3_system
|
||||
})
|
||||
|
||||
|
||||
func finish_early() -> void:
|
||||
if active_session_id.is_empty():
|
||||
return
|
||||
_send("finish", HTTPClient.METHOD_POST, "/%s/finish" % active_session_id.uri_encode())
|
||||
|
||||
|
||||
func check_completion() -> void:
|
||||
if active_session_id.is_empty():
|
||||
return
|
||||
_send("check", HTTPClient.METHOD_POST, "/%s/check" % active_session_id.uri_encode())
|
||||
|
||||
|
||||
func apply_response(parsed: Dictionary, defer_completion: bool = false) -> void:
|
||||
var next_progress: Variant = parsed.get("studySessionProgress", null)
|
||||
if typeof(next_progress) == TYPE_DICTIONARY:
|
||||
progress = next_progress
|
||||
active_session_id = str(progress.get("id", active_session_id))
|
||||
progress_changed.emit()
|
||||
var next_summary: Variant = parsed.get("sessionSummary", null)
|
||||
if typeof(next_summary) == TYPE_DICTIONARY and not next_summary.is_empty():
|
||||
summary = next_summary
|
||||
active_session_id = ""
|
||||
if not defer_completion:
|
||||
session_completed.emit(summary)
|
||||
|
||||
|
||||
func emit_deferred_summary() -> void:
|
||||
if not summary.is_empty():
|
||||
session_completed.emit(summary)
|
||||
|
||||
|
||||
func is_active() -> bool:
|
||||
return not active_session_id.is_empty()
|
||||
|
||||
|
||||
func seconds_remaining() -> int:
|
||||
return int(progress.get("secondsRemaining", 0))
|
||||
|
||||
|
||||
func _send(action: String, method: int, suffix: String, payload: Dictionary = {}) -> void:
|
||||
if request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
||||
return
|
||||
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(api_url + suffix, headers, method, body)
|
||||
if error != OK:
|
||||
pending_action = ""
|
||||
session_failed.emit("The study session could not be started.")
|
||||
|
||||
|
||||
func _on_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
var action := pending_action
|
||||
pending_action = ""
|
||||
if response_code == 401:
|
||||
AuthState.handle_unauthorized(AuthState.access_token)
|
||||
return
|
||||
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||
session_failed.emit("The study session request failed.")
|
||||
return
|
||||
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
|
||||
if typeof(parsed) != TYPE_DICTIONARY:
|
||||
session_failed.emit("The study session response was invalid.")
|
||||
return
|
||||
var next_progress: Dictionary = parsed.get("progress", {})
|
||||
progress = next_progress
|
||||
active_session_id = str(progress.get("id", "")) if str(progress.get("state", "")) == "active" else ""
|
||||
var next_summary: Variant = parsed.get("summary", null)
|
||||
if typeof(next_summary) == TYPE_DICTIONARY:
|
||||
summary = next_summary
|
||||
progress_changed.emit()
|
||||
if action == "create":
|
||||
session_started.emit()
|
||||
elif not summary.is_empty():
|
||||
session_completed.emit(summary)
|
||||
|
||||
|
||||
func _on_auth_changed() -> void:
|
||||
if not AuthState.is_logged_in:
|
||||
active_session_id = ""
|
||||
progress = {}
|
||||
summary = {}
|
||||
|
||||
|
||||
func _new_uuid() -> String:
|
||||
var bytes := Crypto.new().generate_random_bytes(16)
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80
|
||||
var value := bytes.hex_encode()
|
||||
return "%s-%s-%s-%s-%s" % [value.substr(0, 8), value.substr(8, 4), value.substr(12, 4), value.substr(16, 4), value.substr(20, 12)]
|
||||
@@ -0,0 +1 @@
|
||||
uid://da63hc4qgjlso
|
||||
Reference in New Issue
Block a user