143 lines
4.4 KiB
GDScript
143 lines
4.4 KiB
GDScript
extends Node
|
|
|
|
const QUESTION_FILE := "res://data/questions/sample_questions.json"
|
|
const DEFAULT_QUESTIONS_API_URL := "http://localhost:5008/api/questions/search"
|
|
|
|
signal questions_changed
|
|
|
|
var questions: Array = []
|
|
var request: HTTPRequest
|
|
var questions_api_url := DEFAULT_QUESTIONS_API_URL
|
|
|
|
func _ready() -> void:
|
|
questions_api_url = str(ProjectSettings.get_setting("step3/questions_api_url", DEFAULT_QUESTIONS_API_URL))
|
|
request = HTTPRequest.new()
|
|
request.request_completed.connect(_on_api_questions_loaded)
|
|
add_child(request)
|
|
load_questions()
|
|
|
|
|
|
func load_questions() -> void:
|
|
questions.clear()
|
|
var file := FileAccess.open(QUESTION_FILE, FileAccess.READ)
|
|
if file == null:
|
|
push_error("Unable to open question file: %s" % QUESTION_FILE)
|
|
return
|
|
|
|
var parsed = JSON.parse_string(file.get_as_text())
|
|
if typeof(parsed) != TYPE_ARRAY:
|
|
push_error("Question file must contain an array.")
|
|
return
|
|
|
|
for item in parsed:
|
|
if _is_valid_question(item):
|
|
questions.append(item)
|
|
else:
|
|
push_warning("Skipping invalid question: %s" % str(item.get("id", "unknown")))
|
|
|
|
questions_changed.emit()
|
|
|
|
|
|
func load_questions_from_api() -> void:
|
|
if AuthState.access_token.is_empty():
|
|
return
|
|
|
|
var headers := PackedStringArray([
|
|
"Content-Type: application/json",
|
|
"Authorization: Bearer %s" % AuthState.access_token
|
|
])
|
|
var payload := {
|
|
"limit": 80
|
|
}
|
|
var err := request.request(questions_api_url, headers, HTTPClient.METHOD_POST, JSON.stringify(payload))
|
|
if err != OK:
|
|
push_warning("Failed to request questions API. Using local question bank.")
|
|
|
|
|
|
func _on_api_questions_loaded(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
|
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
|
push_warning("Questions API request failed. Using local question bank.")
|
|
return
|
|
|
|
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
|
|
if typeof(parsed) != TYPE_ARRAY:
|
|
push_warning("Questions API response was not an array. Using local question bank.")
|
|
return
|
|
|
|
var api_questions: Array = []
|
|
for item in parsed:
|
|
var normalized := _normalize_api_question(item)
|
|
if _is_valid_question(normalized):
|
|
api_questions.append(normalized)
|
|
|
|
if api_questions.is_empty():
|
|
push_warning("Questions API returned no usable questions. Using local question bank.")
|
|
return
|
|
|
|
questions = api_questions
|
|
questions_changed.emit()
|
|
|
|
|
|
func get_all_questions() -> Array:
|
|
return questions.duplicate(true)
|
|
|
|
|
|
func get_systems() -> Array[String]:
|
|
var system_names: Array[String] = []
|
|
for question in questions:
|
|
var system_name := str(question.get("system", "General"))
|
|
if not system_names.has(system_name):
|
|
system_names.append(system_name)
|
|
system_names.sort()
|
|
return system_names
|
|
|
|
|
|
func get_questions_for_system(system_name: String) -> Array:
|
|
if system_name == "All Systems":
|
|
return get_all_questions()
|
|
|
|
var filtered: Array = []
|
|
for question in questions:
|
|
if str(question.get("system", "")) == system_name:
|
|
filtered.append(question.duplicate(true))
|
|
return filtered
|
|
|
|
|
|
func build_block(system_name: String, block_size: int) -> Array:
|
|
var pool := get_questions_for_system(system_name)
|
|
pool.shuffle()
|
|
return pool.slice(0, min(block_size, pool.size()))
|
|
|
|
|
|
func _is_valid_question(item: Variant) -> bool:
|
|
if typeof(item) != TYPE_DICTIONARY:
|
|
return false
|
|
var required := ["id", "stem", "lead_in", "choices", "correct_choice_id", "explanation", "system", "discipline"]
|
|
for key in required:
|
|
if not item.has(key):
|
|
return false
|
|
return typeof(item["choices"]) == TYPE_ARRAY and item["choices"].size() >= 2
|
|
|
|
|
|
func _normalize_api_question(item: Variant) -> Dictionary:
|
|
if typeof(item) != TYPE_DICTIONARY:
|
|
return {}
|
|
|
|
var normalized := {
|
|
"id": str(item.get("externalId", item.get("id", ""))),
|
|
"api_id": str(item.get("id", "")),
|
|
"stem": str(item.get("stem", "")),
|
|
"lead_in": str(item.get("leadIn", item.get("lead_in", ""))),
|
|
"choices": item.get("choices", []),
|
|
"correct_choice_id": str(item.get("correctChoiceId", item.get("correct_choice_id", ""))),
|
|
"explanation": str(item.get("explanation", "")),
|
|
"choice_explanations": item.get("choiceExplanations", item.get("choice_explanations", {})),
|
|
"discipline": str(item.get("discipline", "")),
|
|
"system": str(item.get("system", "")),
|
|
"difficulty": int(item.get("difficulty", 1)),
|
|
"labs": item.get("labs", []),
|
|
"media": item.get("media", []),
|
|
"tags": item.get("tags", [])
|
|
}
|
|
return normalized
|