diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 7121577..20dfe16 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -140,6 +140,7 @@ void UHyperTwistCoachDashboardWidget::RefreshCoachDashboardView() { RefreshLiveAttemptClock(); } + SyncRetainedRunRecap(); SyncSelectedQueueEntry(); SyncSelectedAttempt(); UpdateDashboardPresentation(); @@ -161,7 +162,9 @@ FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::ExecutePrimaryCoach RunState = StartCoachRecommendedRun(DefaultCoachMaxCases); } + SyncRetainedRunRecap(); SyncSelectedQueueEntry(); + SyncSelectedAttempt(); UpdateDashboardPresentation(); return RunState; } @@ -249,7 +252,9 @@ FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::StartSelectedQueueE { FHyperTwistTrainingRunState RunState = StartCoachQueueEntry(SelectedEntry->EntryId, DefaultCoachMaxCases); + SyncRetainedRunRecap(); SyncSelectedQueueEntry(); + SyncSelectedAttempt(); UpdateDashboardPresentation(); return RunState; } @@ -333,7 +338,10 @@ FHyperTwistTrainingRunStepResult UHyperTwistCoachDashboardWidget::SubmitSyntheti Attempt.ReplayId = CachedRunState.ReplayPacket.ReplayId; Attempt.CompletedAtUtc = FDateTime::UtcNow().ToIso8601(); - return SubmitTrainingAttempt(Attempt, TimeMs); + const FHyperTwistTrainingRunStepResult StepResult = SubmitTrainingAttempt(Attempt, TimeMs); + SyncRetainedRunRecap(); + SyncSelectedAttempt(); + return StepResult; } bool UHyperTwistCoachDashboardWidget::ContinueCurrentRunFlow() @@ -463,6 +471,8 @@ FHyperTwistTrainingRunStepResult UHyperTwistCoachDashboardWidget::SubmitLiveTime Attempt, FMath::Max(LiveAttemptSolveElapsedMs, 0) ); + SyncRetainedRunRecap(); + SyncSelectedAttempt(); CancelLiveAttemptTimer(); return StepResult; } @@ -542,7 +552,9 @@ void UHyperTwistCoachDashboardWidget::HandlePromoteClicked() void UHyperTwistCoachDashboardWidget::HandleCompleteRunClicked() { CompleteActiveTrainingRun(false); + SyncRetainedRunRecap(); SyncSelectedQueueEntry(); + SyncSelectedAttempt(); UpdateDashboardPresentation(); } @@ -554,7 +566,9 @@ void UHyperTwistCoachDashboardWidget::HandleContinueRunClicked() void UHyperTwistCoachDashboardWidget::HandleClearRunClicked() { ClearActiveTrainingRun(); + SyncRetainedRunRecap(); SyncSelectedQueueEntry(); + SyncSelectedAttempt(); UpdateDashboardPresentation(); } @@ -748,6 +762,24 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() TEXT("CoachActiveRunSummary"), 2 ); + RunRecapHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachRunRecapHeader"), + 2 + ); + RunRecapStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachRunRecapStatus"), + 2 + ); + RunRecapDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachRunRecapDetail"), + 2 + ); LastAttemptTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( WidgetTree, RootLayout, @@ -1410,6 +1442,17 @@ void UHyperTwistCoachDashboardWidget::SyncSelectedAttempt() LastObservedAttemptCount = CachedRunState.Attempts.Num(); } +void UHyperTwistCoachDashboardWidget::SyncRetainedRunRecap() +{ + if (CachedRunState.Session.IsStructurallyValid() + && (CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Completed + || CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Aborted)) + { + RetainedRunRecapSummary = CachedRunSummary; + bHasRetainedRunRecap = !RetainedRunRecapSummary.TrainingSessionId.IsEmpty(); + } +} + void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { const bool bHasActiveRun = CachedRunState.Session.IsStructurallyValid() @@ -1611,6 +1654,17 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() : (bCanContinueCurrentRun ? (CachedRunSummary.AttemptCount > 0 ? TEXT("Start Next Attempt") : TEXT("Start First Attempt")) : TransitionLaunchLabel)); + const bool bHasCurrentRunRecap = bHasActiveRun + && (CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Completed + || CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Aborted); + const FHyperTwistTrainingSessionSummary* DisplayedRunRecap = bHasCurrentRunRecap + ? &CachedRunSummary + : (bHasRetainedRunRecap ? &RetainedRunRecapSummary : nullptr); + const bool bHasDisplayedRunRecap = DisplayedRunRecap != nullptr + && !DisplayedRunRecap->TrainingSessionId.IsEmpty(); + const FString RunRecapSourceLabel = bHasCurrentRunRecap + ? TEXT("current run") + : TEXT("last completed run"); if (HeadlineTextBlock != nullptr) { @@ -1663,6 +1717,67 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() CachedRunSummary.LastCaseId.IsEmpty() ? TEXT("n/a") : *CachedRunSummary.LastCaseId ))); } + if (RunRecapHeaderTextBlock != nullptr) + { + RunRecapHeaderTextBlock->SetText(FText::FromString(TEXT("[Run Recap]"))); + } + if (RunRecapStatusTextBlock != nullptr) + { + if (bHasDisplayedRunRecap) + { + RunRecapStatusTextBlock->SetText(FText::FromString(FString::Printf( + TEXT("Source: %s | Session: %s | Deck: %s | State: %s | Completed at: %s"), + *RunRecapSourceLabel, + *DisplayedRunRecap->TrainingSessionId, + DisplayedRunRecap->DeckId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->DeckId, + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedRunRecap->SessionState), + DisplayedRunRecap->LastCompletedAtUtc.IsEmpty() + ? TEXT("n/a") + : *DisplayedRunRecap->LastCompletedAtUtc + ))); + } + else if (bHasActiveRun) + { + RunRecapStatusTextBlock->SetText(FText::FromString( + TEXT("Source: active run | recap will stabilize here when the run completes or aborts.") + )); + } + else + { + RunRecapStatusTextBlock->SetText(FText::FromString( + TEXT("Source: none yet | complete a run to retain a session recap on this dashboard.") + )); + } + } + if (RunRecapDetailTextBlock != nullptr) + { + if (bHasDisplayedRunRecap) + { + RunRecapDetailTextBlock->SetText(FText::FromString(FString::Printf( + TEXT("Attempts %d/%d | Success %d | Failure %d | Timeout %d | DNF %d | Aborted %d | Success rate %.2f | Best %d ms | Avg %d ms | Avg raw %d ms | Avg inspection %d ms | Mistakes %d | Last case %s"), + DisplayedRunRecap->CompletedAttemptCount, + DisplayedRunRecap->AttemptCount, + DisplayedRunRecap->SuccessCount, + DisplayedRunRecap->FailureCount, + DisplayedRunRecap->TimeoutCount, + DisplayedRunRecap->DNFCount, + DisplayedRunRecap->AbortedCount, + DisplayedRunRecap->SuccessRate, + DisplayedRunRecap->BestTotalTimeMs, + DisplayedRunRecap->AverageTotalTimeMs, + DisplayedRunRecap->AverageRawSolveTimeMs, + DisplayedRunRecap->AverageInspectionTimeMs, + DisplayedRunRecap->TotalMistakes, + DisplayedRunRecap->LastCaseId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->LastCaseId + ))); + } + else + { + RunRecapDetailTextBlock->SetText(FText::FromString( + TEXT("Recap detail: unavailable until a run has been completed or explicitly aborted.") + )); + } + } if (LastAttemptTextBlock != nullptr) { if (ReviewedAttempt != nullptr) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 238614d..8e59658 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -148,6 +148,15 @@ protected: UPROPERTY(Transient) TObjectPtr ActiveRunSummaryTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr RunRecapHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr RunRecapStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr RunRecapDetailTextBlock = nullptr; + UPROPERTY(Transient) TObjectPtr LastAttemptTextBlock = nullptr; @@ -431,6 +440,7 @@ private: void EnsureDefaultDashboardBuilt(); void SyncSelectedQueueEntry(); void SyncSelectedAttempt(); + void SyncRetainedRunRecap(); void UpdateDashboardPresentation(); void RefreshLiveAttemptClock(); FString BuildDefaultDeferredUntilUtc() const; @@ -449,4 +459,6 @@ private: double LiveAttemptSolveStartedAtSeconds = 0.0; int32 LiveAttemptStoredInspectionElapsedMs = 0; int32 LastObservedAttemptCount = INDEX_NONE; + bool bHasRetainedRunRecap = false; + FHyperTwistTrainingSessionSummary RetainedRunRecapSummary; };