Move simulation history into profile
This commit is contained in:
@@ -71,6 +71,28 @@ public sealed class ExamSimulationTests
|
||||
Assert.Equal(expected, ExamSimulationStore.BlueprintCategory(new Question { System = system }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HistorySummarySupportsScoredAndUnscoredStates()
|
||||
{
|
||||
var active = ExamSimulationStore.ToHistorySummary(new ExamSimulation
|
||||
{
|
||||
State = ExamSimulationStates.BlockActive,
|
||||
CurrentBlock = 3,
|
||||
Items = BuildItems(20)
|
||||
});
|
||||
var completed = ExamSimulationStore.ToHistorySummary(new ExamSimulation
|
||||
{
|
||||
State = ExamSimulationStates.Completed,
|
||||
Items = BuildItems(20),
|
||||
Score = new ExamSimulationScore { PredictedScore = 221, RangeLow = 204, RangeHigh = 238, Correct = 15, Total = 20 }
|
||||
});
|
||||
|
||||
Assert.Null(active.PredictedScore);
|
||||
Assert.Equal(3, active.CurrentBlock);
|
||||
Assert.Equal(221, completed.PredictedScore);
|
||||
Assert.Equal(15, completed.Correct);
|
||||
}
|
||||
|
||||
private static List<ExamSimulationItem> BuildItems(int count, bool wasSeen = false) =>
|
||||
Enumerable.Range(0, count).Select(index => new ExamSimulationItem
|
||||
{
|
||||
|
||||
@@ -53,6 +53,19 @@ public class ExamSimulationsController(ExamSimulationStore simulations) : Contro
|
||||
return Ok(await simulations.GetCompletedAsync(UserId(), limit, cursor));
|
||||
}
|
||||
|
||||
[HttpGet("history")]
|
||||
public async Task<IActionResult> History([FromQuery] int limit = 20, [FromQuery] string? before = null)
|
||||
{
|
||||
DateTime? cursor = null;
|
||||
if (!string.IsNullOrWhiteSpace(before))
|
||||
{
|
||||
if (!DateTime.TryParse(before, null, System.Globalization.DateTimeStyles.RoundtripKind, out var parsed))
|
||||
return BadRequest("The simulation-history cursor is invalid.");
|
||||
cursor = parsed;
|
||||
}
|
||||
return Ok(await simulations.GetHistoryAsync(UserId(), limit, cursor));
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> Get(string id)
|
||||
{
|
||||
|
||||
@@ -137,6 +137,28 @@ public class CompletedExamSimulationPage
|
||||
public DateTime? NextBefore { get; set; }
|
||||
}
|
||||
|
||||
public class ExamSimulationHistorySummary
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string State { get; set; } = "";
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
public DateTime UpdatedUtc { get; set; }
|
||||
public DateTime? CompletedUtc { get; set; }
|
||||
public int CurrentBlock { get; set; }
|
||||
public int TotalBlocks { get; set; } = 12;
|
||||
public int? PredictedScore { get; set; }
|
||||
public int? RangeLow { get; set; }
|
||||
public int? RangeHigh { get; set; }
|
||||
public int? Correct { get; set; }
|
||||
public int Total { get; set; }
|
||||
}
|
||||
|
||||
public class ExamSimulationHistoryPage
|
||||
{
|
||||
public List<ExamSimulationHistorySummary> Items { get; set; } = [];
|
||||
public DateTime? NextBefore { get; set; }
|
||||
}
|
||||
|
||||
public class Step3Outcome
|
||||
{
|
||||
[BsonId]
|
||||
|
||||
@@ -35,6 +35,8 @@ public class ExamSimulationStore
|
||||
Builders<ExamSimulation>.IndexKeys.Ascending(item => item.UserId).Ascending(item => item.State)));
|
||||
_simulations.Indexes.CreateOne(new CreateIndexModel<ExamSimulation>(
|
||||
Builders<ExamSimulation>.IndexKeys.Ascending(item => item.UserId).Ascending(item => item.State).Descending(item => item.CompletedUtc)));
|
||||
_simulations.Indexes.CreateOne(new CreateIndexModel<ExamSimulation>(
|
||||
Builders<ExamSimulation>.IndexKeys.Ascending(item => item.UserId).Descending(item => item.CreatedUtc)));
|
||||
_simulations.Indexes.CreateOne(new CreateIndexModel<ExamSimulation>(
|
||||
Builders<ExamSimulation>.IndexKeys.Ascending(item => item.UpdatedUtc)));
|
||||
_outcomes.Indexes.CreateOne(new CreateIndexModel<Step3Outcome>(
|
||||
@@ -183,6 +185,37 @@ public class ExamSimulationStore
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ExamSimulationHistoryPage> GetHistoryAsync(string userId, int limit, DateTime? before)
|
||||
{
|
||||
limit = Math.Clamp(limit, 1, 50);
|
||||
var filter = Builders<ExamSimulation>.Filter.Eq(item => item.UserId, userId);
|
||||
if (before is not null)
|
||||
filter &= Builders<ExamSimulation>.Filter.Lt(item => item.CreatedUtc, before.Value);
|
||||
var simulations = await _simulations.Find(filter).SortByDescending(item => item.CreatedUtc).Limit(limit + 1).ToListAsync();
|
||||
var hasMore = simulations.Count > limit;
|
||||
if (hasMore) simulations.RemoveAt(simulations.Count - 1);
|
||||
return new ExamSimulationHistoryPage
|
||||
{
|
||||
Items = simulations.Select(ToHistorySummary).ToList(),
|
||||
NextBefore = hasMore && simulations.Count > 0 ? simulations[^1].CreatedUtc : null
|
||||
};
|
||||
}
|
||||
|
||||
public static ExamSimulationHistorySummary ToHistorySummary(ExamSimulation item) => new()
|
||||
{
|
||||
Id = item.Id ?? "",
|
||||
State = item.State,
|
||||
CreatedUtc = item.CreatedUtc,
|
||||
UpdatedUtc = item.UpdatedUtc,
|
||||
CompletedUtc = item.CompletedUtc,
|
||||
CurrentBlock = item.CurrentBlock,
|
||||
PredictedScore = item.Score?.PredictedScore,
|
||||
RangeLow = item.Score?.RangeLow,
|
||||
RangeHigh = item.Score?.RangeHigh,
|
||||
Correct = item.Score?.Correct,
|
||||
Total = item.Score?.Total ?? item.Items.Count
|
||||
};
|
||||
|
||||
public async Task<ExamSimulation> StartNextBlockAsync(ExamSimulation simulation)
|
||||
{
|
||||
if (simulation.State != ExamSimulationStates.BetweenBlocks)
|
||||
|
||||
@@ -7,7 +7,7 @@ signal readiness_changed
|
||||
signal state_changed
|
||||
signal request_failed(message: String)
|
||||
signal outcome_changed
|
||||
signal completed_history_changed
|
||||
signal history_changed
|
||||
|
||||
var api_url := DEFAULT_API_URL
|
||||
var outcome_api_url := "https://s3q.ranaze.com/api/step3-outcomes"
|
||||
@@ -15,8 +15,8 @@ var request: HTTPRequest
|
||||
var readiness := {}
|
||||
var simulation := {}
|
||||
var outcome := {}
|
||||
var completed_history: Array = []
|
||||
var completed_history_next_before := ""
|
||||
var history: Array = []
|
||||
var history_next_before := ""
|
||||
var is_loading := false
|
||||
var pending_action := ""
|
||||
var request_queue: Array = []
|
||||
@@ -40,8 +40,8 @@ func load_active() -> void:
|
||||
_send("active", HTTPClient.METHOD_GET, "/active")
|
||||
|
||||
|
||||
func load_completed_history(before: String = "") -> void:
|
||||
var suffix := "/completed?limit=20"
|
||||
func load_history(before: String = "") -> void:
|
||||
var suffix := "/history?limit=20"
|
||||
if not before.is_empty():
|
||||
suffix += "&before=%s" % before.uri_encode()
|
||||
_send("history_append" if not before.is_empty() else "history", HTTPClient.METHOD_GET, suffix)
|
||||
@@ -172,10 +172,10 @@ func _on_request_completed(result: int, response_code: int, _headers: PackedStri
|
||||
return
|
||||
if action == "history" or action == "history_append":
|
||||
var items: Array = parsed.get("items", [])
|
||||
completed_history = completed_history + items if action == "history_append" else items
|
||||
history = history + items if action == "history_append" else items
|
||||
var next_before: Variant = parsed.get("nextBefore", null)
|
||||
completed_history_next_before = "" if next_before == null else str(next_before)
|
||||
completed_history_changed.emit()
|
||||
history_next_before = "" if next_before == null else str(next_before)
|
||||
history_changed.emit()
|
||||
return
|
||||
if action.begins_with("outcome_"):
|
||||
outcome = parsed
|
||||
|
||||
@@ -173,6 +173,12 @@ func _build_ui() -> void:
|
||||
screens_container.add_child(simulation_review)
|
||||
screens["simulation_review"] = simulation_review
|
||||
|
||||
var simulation_history := preload("res://scripts/ui/simulation_history.gd").new()
|
||||
simulation_history.name = "SimulationHistory"
|
||||
simulation_history.main = self
|
||||
screens_container.add_child(simulation_history)
|
||||
screens["simulation_history"] = simulation_history
|
||||
|
||||
var profile := preload("res://scripts/ui/progress_profile.gd").new()
|
||||
profile.name = "ProgressProfile"
|
||||
profile.main = self
|
||||
|
||||
@@ -41,6 +41,12 @@ func _ready() -> void:
|
||||
)
|
||||
row.add_child(reset)
|
||||
|
||||
var simulations := Button.new()
|
||||
simulations.text = "Simulation History"
|
||||
Style.style_chip(simulations, Style.CYAN)
|
||||
simulations.pressed.connect(func(): main.show_screen("simulation_history"))
|
||||
row.add_child(simulations)
|
||||
|
||||
reset_qbank_button = Button.new()
|
||||
reset_qbank_button.text = "Reset QBank"
|
||||
Style.style_chip(reset_qbank_button, Style.AMBER)
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
extends VBoxContainer
|
||||
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
|
||||
var main: Control
|
||||
var list: VBoxContainer
|
||||
var status_label: Label
|
||||
var load_more_button: Button
|
||||
var pending_simulation_id := ""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_theme_constant_override("separation", 12)
|
||||
if not ExamSimulationService.history_changed.is_connected(_render_history):
|
||||
ExamSimulationService.history_changed.connect(_render_history)
|
||||
if not ExamSimulationService.state_changed.is_connected(_on_simulation_loaded):
|
||||
ExamSimulationService.state_changed.connect(_on_simulation_loaded)
|
||||
if not ExamSimulationService.request_failed.is_connected(_on_request_failed):
|
||||
ExamSimulationService.request_failed.connect(_on_request_failed)
|
||||
var top := HBoxContainer.new()
|
||||
add_child(top)
|
||||
var back := Button.new()
|
||||
back.text = "< Profile"
|
||||
Style.style_chip(back, Style.CYAN)
|
||||
back.pressed.connect(func(): main.show_screen("profile"))
|
||||
top.add_child(back)
|
||||
var title := Label.new()
|
||||
title.text = "Simulation History"
|
||||
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
Style.set_label(title, 30, Style.TEXT)
|
||||
top.add_child(title)
|
||||
var balance := Control.new()
|
||||
balance.custom_minimum_size = Vector2(100, 0)
|
||||
top.add_child(balance)
|
||||
status_label = Label.new()
|
||||
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(status_label, 16, Style.MUTED)
|
||||
add_child(status_label)
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
scroll.size_flags_horizontal = 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)
|
||||
load_more_button = Button.new()
|
||||
load_more_button.text = "Load More"
|
||||
load_more_button.visible = false
|
||||
load_more_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
Style.style_chip(load_more_button, Style.CYAN)
|
||||
load_more_button.pressed.connect(_load_more)
|
||||
add_child(load_more_button)
|
||||
|
||||
|
||||
func on_show(payload: Variant = null) -> void:
|
||||
pending_simulation_id = ""
|
||||
if str(payload) == "preserve" and not ExamSimulationService.history.is_empty():
|
||||
return
|
||||
status_label.text = "Loading simulation history..."
|
||||
load_more_button.disabled = true
|
||||
ExamSimulationService.load_history()
|
||||
|
||||
|
||||
func _render_history() -> void:
|
||||
if not visible:
|
||||
return
|
||||
for child in list.get_children():
|
||||
child.queue_free()
|
||||
if ExamSimulationService.history.is_empty():
|
||||
status_label.text = "No simulations yet."
|
||||
else:
|
||||
status_label.text = "Select a completed simulation to review it, or resume an active simulation."
|
||||
for raw_summary in ExamSimulationService.history:
|
||||
var summary: Dictionary = raw_summary
|
||||
var state := str(summary.get("state", ""))
|
||||
var action := _action_for_state(state)
|
||||
var button := Button.new()
|
||||
button.text = _summary_text(summary)
|
||||
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
button.custom_minimum_size = Vector2(0, 70)
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button.disabled = action == "none"
|
||||
Style.style_chip(button, _state_color(state))
|
||||
if action != "none":
|
||||
button.pressed.connect(_open_simulation.bind(str(summary.get("id", ""))))
|
||||
list.add_child(button)
|
||||
load_more_button.visible = not ExamSimulationService.history_next_before.is_empty()
|
||||
load_more_button.disabled = false
|
||||
|
||||
|
||||
func _summary_text(summary: Dictionary) -> String:
|
||||
var state := str(summary.get("state", ""))
|
||||
var date := str(summary.get("createdUtc", "")).left(10)
|
||||
if state == "completed":
|
||||
return "%s | Completed\nPredicted %d (%d-%d) | %d/%d correct" % [date, int(summary.get("predictedScore", 0)), int(summary.get("rangeLow", 0)), int(summary.get("rangeHigh", 0)), int(summary.get("correct", 0)), int(summary.get("total", 0))]
|
||||
if state == "block_active":
|
||||
return "%s | Active\nResume Block %d of %d (timer continues)" % [date, int(summary.get("currentBlock", 0)) + 1, int(summary.get("totalBlocks", 12))]
|
||||
if state == "between_blocks":
|
||||
return "%s | In progress\n%d of %d blocks complete" % [date, int(summary.get("currentBlock", -1)) + 1, int(summary.get("totalBlocks", 12))]
|
||||
return "%s | Abandoned\nThis simulation cannot be resumed." % date
|
||||
|
||||
|
||||
func _open_simulation(simulation_id: String) -> void:
|
||||
pending_simulation_id = simulation_id
|
||||
status_label.text = "Opening simulation..."
|
||||
ExamSimulationService.load_simulation(simulation_id)
|
||||
|
||||
|
||||
func _on_simulation_loaded() -> void:
|
||||
if not visible or pending_simulation_id.is_empty() or ExamSimulationService.simulation.is_empty():
|
||||
return
|
||||
pending_simulation_id = ""
|
||||
var state := str(ExamSimulationService.simulation.get("state", ""))
|
||||
if state == "completed":
|
||||
main.show_screen("simulation_results", {"simulation": ExamSimulationService.simulation, "return_screen": "simulation_history"})
|
||||
elif state == "block_active":
|
||||
BlockSessionService.start_simulation_block(
|
||||
ExamSimulationService.get_current_questions(),
|
||||
int(ExamSimulationService.simulation.get("currentBlock", 0)),
|
||||
int(ExamSimulationService.simulation.get("totalBlocks", 12)),
|
||||
ExamSimulationService.get_block_deadline_unix())
|
||||
BlockSessionService.restore_simulation_responses(ExamSimulationService.simulation.get("responses", []))
|
||||
main.show_screen("player")
|
||||
elif state == "between_blocks":
|
||||
main.show_screen("simulation_break")
|
||||
else:
|
||||
status_label.text = "This simulation can no longer be resumed."
|
||||
|
||||
|
||||
func _load_more() -> void:
|
||||
load_more_button.disabled = true
|
||||
ExamSimulationService.load_history(ExamSimulationService.history_next_before)
|
||||
|
||||
|
||||
func _on_request_failed(message: String) -> void:
|
||||
if not visible:
|
||||
return
|
||||
pending_simulation_id = ""
|
||||
status_label.text = message
|
||||
load_more_button.disabled = false
|
||||
|
||||
|
||||
static func _action_for_state(state: String) -> String:
|
||||
if state == "completed": return "results"
|
||||
if state == "block_active" or state == "between_blocks": return "resume"
|
||||
return "none"
|
||||
|
||||
|
||||
static func _state_color(state: String) -> Color:
|
||||
if state == "completed": return Style.LIME
|
||||
if state == "block_active" or state == "between_blocks": return Style.CYAN
|
||||
return Style.MUTED
|
||||
@@ -0,0 +1 @@
|
||||
uid://dg3a6hdg4w2pd
|
||||
@@ -12,6 +12,8 @@ var outcome_consent: CheckBox
|
||||
var outcome_status: Label
|
||||
var outcome_delete_button: Button
|
||||
var current_simulation := {}
|
||||
var return_screen := "selection"
|
||||
var done_button: Button
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -47,7 +49,7 @@ func _ready() -> void:
|
||||
review_button.custom_minimum_size = Vector2(240, 50)
|
||||
review_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
Style.style_primary_button(review_button)
|
||||
review_button.pressed.connect(func(): main.show_screen("simulation_review", current_simulation))
|
||||
review_button.pressed.connect(func(): main.show_screen("simulation_review", {"simulation": current_simulation, "return_screen": return_screen}))
|
||||
content.add_child(review_button)
|
||||
|
||||
var outcome_heading := Label.new()
|
||||
@@ -91,17 +93,20 @@ func _ready() -> void:
|
||||
if not ExamSimulationService.outcome_changed.is_connected(_refresh_outcome):
|
||||
ExamSimulationService.outcome_changed.connect(_refresh_outcome)
|
||||
|
||||
var done := Button.new()
|
||||
done.text = "Return to study modes"
|
||||
done.custom_minimum_size = Vector2(240, 50)
|
||||
done.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
Style.style_primary_button(done)
|
||||
done.pressed.connect(func(): main.show_screen("selection", "step3"))
|
||||
add_child(done)
|
||||
done_button = Button.new()
|
||||
done_button.text = "Return to study modes"
|
||||
done_button.custom_minimum_size = Vector2(240, 50)
|
||||
done_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
Style.style_primary_button(done_button)
|
||||
done_button.pressed.connect(_return_from_results)
|
||||
add_child(done_button)
|
||||
|
||||
|
||||
func on_show(payload: Variant = null) -> void:
|
||||
var simulation: Dictionary = payload if typeof(payload) == TYPE_DICTIONARY else ExamSimulationService.simulation
|
||||
var wrapper: Dictionary = payload if typeof(payload) == TYPE_DICTIONARY else {}
|
||||
var simulation: Dictionary = wrapper.get("simulation", wrapper) if not wrapper.is_empty() else ExamSimulationService.simulation
|
||||
return_screen = str(wrapper.get("return_screen", "selection"))
|
||||
done_button.text = "Return to simulation history" if return_screen == "simulation_history" else "Return to study modes"
|
||||
current_simulation = simulation
|
||||
var score: Dictionary = simulation.get("score", {})
|
||||
var predicted := int(score.get("predictedScore", 0))
|
||||
@@ -121,6 +126,13 @@ func on_show(payload: Variant = null) -> void:
|
||||
ExamSimulationService.load_outcome()
|
||||
|
||||
|
||||
func _return_from_results() -> void:
|
||||
if return_screen == "simulation_history":
|
||||
main.show_screen("simulation_history", "preserve")
|
||||
else:
|
||||
main.show_screen("selection", "step3")
|
||||
|
||||
|
||||
func _clear(container: Control) -> void:
|
||||
for child in container.get_children():
|
||||
child.queue_free()
|
||||
|
||||
@@ -18,6 +18,7 @@ var empty_label: Label
|
||||
var media_requests: Array[HTTPRequest] = []
|
||||
var image_overlay: Control
|
||||
var image_texture: TextureRect
|
||||
var return_screen := "selection"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -29,7 +30,7 @@ func _ready() -> void:
|
||||
var back := Button.new()
|
||||
back.text = "< Results"
|
||||
Style.style_chip(back, Style.CYAN)
|
||||
back.pressed.connect(func(): main.show_screen("simulation_results", simulation))
|
||||
back.pressed.connect(func(): main.show_screen("simulation_results", {"simulation": simulation, "return_screen": return_screen}))
|
||||
toolbar.add_child(back)
|
||||
progress_label = Label.new()
|
||||
progress_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
@@ -90,7 +91,9 @@ func _ready() -> void:
|
||||
|
||||
func on_show(payload: Variant = null) -> void:
|
||||
if typeof(payload) == TYPE_DICTIONARY:
|
||||
simulation = payload
|
||||
var wrapper: Dictionary = payload
|
||||
simulation = wrapper.get("simulation", wrapper)
|
||||
return_screen = str(wrapper.get("return_screen", "selection"))
|
||||
questions = simulation.get("reviewQuestions", [])
|
||||
responses.clear()
|
||||
for response in simulation.get("responses", []):
|
||||
|
||||
@@ -14,8 +14,6 @@ var retry_button: Button
|
||||
var simulation_button: Button
|
||||
var simulation_status_label: Label
|
||||
var simulation_preview_link: LinkButton
|
||||
var completed_history_list: VBoxContainer
|
||||
var completed_history_more: Button
|
||||
var pending_simulation_launch := false
|
||||
var current_stage := "exam"
|
||||
var pending_question_bank_launch := false
|
||||
@@ -37,8 +35,6 @@ 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 ExamSimulationService.completed_history_changed.is_connected(_refresh_completed_history):
|
||||
ExamSimulationService.completed_history_changed.connect(_refresh_completed_history)
|
||||
|
||||
_build_exam_view()
|
||||
_build_mode_view()
|
||||
@@ -53,7 +49,6 @@ func on_show(payload: Variant = null) -> void:
|
||||
_refresh_bank_state()
|
||||
_refresh_simulation_preview_visibility()
|
||||
ExamSimulationService.load_readiness()
|
||||
ExamSimulationService.load_completed_history()
|
||||
|
||||
|
||||
func on_hide() -> void:
|
||||
@@ -179,26 +174,6 @@ func _build_mode_view() -> void:
|
||||
mode_view.add_child(simulation_preview_link)
|
||||
_refresh_simulation_preview_visibility()
|
||||
|
||||
var history_heading := Label.new()
|
||||
history_heading.text = "Completed simulations"
|
||||
Style.set_label(history_heading, 20, Style.TEXT)
|
||||
mode_view.add_child(history_heading)
|
||||
completed_history_list = VBoxContainer.new()
|
||||
completed_history_list.add_theme_constant_override("separation", 6)
|
||||
completed_history_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
var history_scroll := ScrollContainer.new()
|
||||
history_scroll.custom_minimum_size = Vector2(0, 150)
|
||||
history_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
history_scroll.add_child(completed_history_list)
|
||||
mode_view.add_child(history_scroll)
|
||||
completed_history_more = Button.new()
|
||||
completed_history_more.text = "Load more"
|
||||
completed_history_more.visible = false
|
||||
completed_history_more.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
Style.style_chip(completed_history_more, Style.CYAN)
|
||||
completed_history_more.pressed.connect(func(): ExamSimulationService.load_completed_history(ExamSimulationService.completed_history_next_before))
|
||||
mode_view.add_child(completed_history_more)
|
||||
|
||||
bank_status_label = Label.new()
|
||||
bank_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(bank_status_label, 16, Style.CYAN)
|
||||
@@ -294,30 +269,6 @@ func _refresh_simulation_preview_visibility() -> void:
|
||||
simulation_preview_link.visible = false
|
||||
|
||||
|
||||
func _refresh_completed_history() -> void:
|
||||
if completed_history_list == null:
|
||||
return
|
||||
for child in completed_history_list.get_children():
|
||||
child.queue_free()
|
||||
for raw_summary in ExamSimulationService.completed_history:
|
||||
var summary: Dictionary = raw_summary
|
||||
var button := Button.new()
|
||||
button.text = "%s | Predicted %d (%d-%d) | %d/%d correct" % [
|
||||
str(summary.get("completedUtc", "")).left(10), int(summary.get("predictedScore", 0)),
|
||||
int(summary.get("rangeLow", 0)), int(summary.get("rangeHigh", 0)),
|
||||
int(summary.get("correct", 0)), int(summary.get("total", 0))]
|
||||
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
Style.style_chip(button, Style.CYAN)
|
||||
button.pressed.connect(_open_completed_simulation.bind(str(summary.get("id", ""))))
|
||||
completed_history_list.add_child(button)
|
||||
completed_history_more.visible = not ExamSimulationService.completed_history_next_before.is_empty()
|
||||
|
||||
|
||||
func _open_completed_simulation(simulation_id: String) -> void:
|
||||
pending_simulation_launch = true
|
||||
ExamSimulationService.load_simulation(simulation_id)
|
||||
|
||||
|
||||
func _on_simulation_state_changed() -> void:
|
||||
_refresh_simulation_state()
|
||||
if not pending_simulation_launch or ExamSimulationService.simulation.is_empty():
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
extends SceneTree
|
||||
|
||||
var failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var history = load("res://scripts/ui/simulation_history.gd")
|
||||
_assert(history._action_for_state("completed") == "results", "completed simulations should open results")
|
||||
_assert(history._action_for_state("block_active") == "resume", "active blocks should resume")
|
||||
_assert(history._action_for_state("between_blocks") == "resume", "between-block simulations should resume")
|
||||
_assert(history._action_for_state("abandoned") == "none", "abandoned simulations should be read-only")
|
||||
quit(1 if failures > 0 else 0)
|
||||
|
||||
|
||||
func _assert(condition: bool, message: String) -> void:
|
||||
if condition:
|
||||
return
|
||||
failures += 1
|
||||
push_error(message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://babu2x7xudmxc
|
||||
Reference in New Issue
Block a user