diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 07c161d..48c9420 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -186,11 +186,13 @@ void UHyperTwistCoachDashboardWidget::RefreshCoachDashboardView() RefreshLiveAttemptClock(); } RecordQueueSuppressionSnapshot(); + RecordClosureRecoverySnapshot(); SyncRetainedRunRecap(); SyncSelectedQueueEntry(); SyncSelectedAttempt(); SyncSelectedGuidanceDecisionHistoryEntry(); SyncSelectedQueueSuppressionHistoryEntry(); + SyncSelectedClosureRecoveryHistoryEntry(); SyncPreferredGuidancePreviewLane(false); UpdateDashboardPresentation(); } @@ -1031,6 +1033,47 @@ bool UHyperTwistCoachDashboardWidget::SelectNextQueueSuppressionHistoryEntry() return true; } +bool UHyperTwistCoachDashboardWidget::SelectPreviousClosureRecoveryHistoryEntry() +{ + if (ClosureRecoveryHistoryEntries.Num() <= 0) + { + SelectedClosureRecoveryIndex = INDEX_NONE; + return false; + } + + if (SelectedClosureRecoveryIndex == INDEX_NONE) + { + SyncSelectedClosureRecoveryHistoryEntry(); + return SelectedClosureRecoveryIndex != INDEX_NONE; + } + + SelectedClosureRecoveryIndex = + (SelectedClosureRecoveryIndex - 1 + ClosureRecoveryHistoryEntries.Num()) + % ClosureRecoveryHistoryEntries.Num(); + UpdateDashboardPresentation(); + return true; +} + +bool UHyperTwistCoachDashboardWidget::SelectNextClosureRecoveryHistoryEntry() +{ + if (ClosureRecoveryHistoryEntries.Num() <= 0) + { + SelectedClosureRecoveryIndex = INDEX_NONE; + return false; + } + + if (SelectedClosureRecoveryIndex == INDEX_NONE) + { + SyncSelectedClosureRecoveryHistoryEntry(); + return SelectedClosureRecoveryIndex != INDEX_NONE; + } + + SelectedClosureRecoveryIndex = + (SelectedClosureRecoveryIndex + 1) % ClosureRecoveryHistoryEntries.Num(); + UpdateDashboardPresentation(); + return true; +} + bool UHyperTwistCoachDashboardWidget::StartLiveAttemptTimer() { if (!CachedRunState.Session.IsStructurallyValid() || !CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid()) @@ -1326,6 +1369,16 @@ void UHyperTwistCoachDashboardWidget::HandleNextQueueSuppressionClicked() SelectNextQueueSuppressionHistoryEntry(); } +void UHyperTwistCoachDashboardWidget::HandlePreviousClosureRecoveryClicked() +{ + SelectPreviousClosureRecoveryHistoryEntry(); +} + +void UHyperTwistCoachDashboardWidget::HandleNextClosureRecoveryClicked() +{ + SelectNextClosureRecoveryHistoryEntry(); +} + void UHyperTwistCoachDashboardWidget::HandleClearRunClicked() { ClearActiveTrainingRun(); @@ -2036,6 +2089,24 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() TEXT("CoachClosureRecoveryDetail"), 8 ); + ClosureRecoveryHistoryHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachClosureRecoveryHistoryHeader"), + 2 + ); + ClosureRecoveryHistoryStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachClosureRecoveryHistoryStatus"), + 2 + ); + ClosureRecoveryHistoryDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachClosureRecoveryHistoryDetail"), + 8 + ); QueueRecoveryHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( WidgetTree, RootLayout, @@ -2210,6 +2281,40 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() NextQueueSuppressionButtonLabel = NextQueueSuppressionLabelRaw; } + UHorizontalBox* ClosureRecoveryNavRow = WidgetTree->ConstructWidget( + UHorizontalBox::StaticClass(), + TEXT("CoachDashboardClosureRecoveryNavRow") + ); + if (ClosureRecoveryNavRow != nullptr) + { + if (UVerticalBoxSlot* ClosureRecoveryNavRowSlot = RootLayout->AddChildToVerticalBox(ClosureRecoveryNavRow)) + { + ClosureRecoveryNavRowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 0.0f)); + } + + UTextBlock* PreviousClosureRecoveryLabelRaw = nullptr; + PreviousClosureRecoveryButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + ClosureRecoveryNavRow, + TEXT("PreviousClosureRecoveryButton"), + TEXT("PreviousClosureRecoveryButtonLabel"), + TEXT("Prev Closure"), + PreviousClosureRecoveryLabelRaw + ); + PreviousClosureRecoveryButtonLabel = PreviousClosureRecoveryLabelRaw; + + UTextBlock* NextClosureRecoveryLabelRaw = nullptr; + NextClosureRecoveryButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + ClosureRecoveryNavRow, + TEXT("NextClosureRecoveryButton"), + TEXT("NextClosureRecoveryButtonLabel"), + TEXT("Next Closure"), + NextClosureRecoveryLabelRaw + ); + NextClosureRecoveryButtonLabel = NextClosureRecoveryLabelRaw; + } + UHorizontalBox* AttemptInputRow = WidgetTree->ConstructWidget( UHorizontalBox::StaticClass(), TEXT("CoachDashboardAttemptInputRow") @@ -2524,6 +2629,20 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() &UHyperTwistCoachDashboardWidget::HandleNextQueueSuppressionClicked ); } + if (PreviousClosureRecoveryButton != nullptr) + { + PreviousClosureRecoveryButton->OnClicked.AddDynamic( + this, + &UHyperTwistCoachDashboardWidget::HandlePreviousClosureRecoveryClicked + ); + } + if (NextClosureRecoveryButton != nullptr) + { + NextClosureRecoveryButton->OnClicked.AddDynamic( + this, + &UHyperTwistCoachDashboardWidget::HandleNextClosureRecoveryClicked + ); + } if (RepeatRunButton != nullptr) { RepeatRunButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleRepeatRunClicked); @@ -2806,6 +2925,21 @@ void UHyperTwistCoachDashboardWidget::SyncSelectedQueueSuppressionHistoryEntry() } } +void UHyperTwistCoachDashboardWidget::SyncSelectedClosureRecoveryHistoryEntry() +{ + if (ClosureRecoveryHistoryEntries.Num() <= 0) + { + SelectedClosureRecoveryIndex = INDEX_NONE; + return; + } + + if (SelectedClosureRecoveryIndex < 0 + || SelectedClosureRecoveryIndex >= ClosureRecoveryHistoryEntries.Num()) + { + SelectedClosureRecoveryIndex = ClosureRecoveryHistoryEntries.Num() - 1; + } +} + void UHyperTwistCoachDashboardWidget::SyncPreferredGuidancePreviewLane(const bool bForceAdoptPreferredLane) { const FHyperTwistCoachDashboardGuidanceLanePreference Preference = BuildGuidanceLanePreference(); @@ -3645,6 +3779,89 @@ void UHyperTwistCoachDashboardWidget::RecordQueueSuppressionSnapshot() SelectedQueueSuppressionIndex = QueueSuppressionHistoryEntries.Num() - 1; } +void UHyperTwistCoachDashboardWidget::RecordClosureRecoverySnapshot() +{ + const int32 HistoricalKeepCount = + CachedCoachSchedulePolicySummary.HistoricalFollowUpContinuationCount; + const int32 HistoricalSuppressCount = + CachedCoachSchedulePolicySummary.HistoricalBorderlineFollowUpSuppressionCount; + const FHyperTwistCoachSignal* ClosureRecoverySignal = CachedCoachSignals.FindByPredicate( + [](const FHyperTwistCoachSignal& Signal) + { + return Signal.SignalId == TEXT("closure_history_follow_up_continuation") + || Signal.SignalId == TEXT("closure_history_recommended_suppression"); + } + ); + const bool bHasClosureSignal = ClosureRecoverySignal != nullptr; + + FHyperTwistCoachDashboardClosureRecoveryEntry Entry; + Entry.RecordedAtUtc = FDateTime::UtcNow().ToIso8601(); + Entry.HistoricalFollowUpContinuationCount = HistoricalKeepCount; + Entry.HistoricalBorderlineFollowUpSuppressionCount = HistoricalSuppressCount; + Entry.bFollowUpContinuationFromRecoveryHistory = + CachedCoachSchedulePolicySummary.bFollowUpContinuationFromRecoveryHistory; + Entry.bBorderlineFollowUpSuppressedByRecoveryHistory = + CachedCoachSchedulePolicySummary.bBorderlineFollowUpSuppressedByRecoveryHistory; + Entry.PreferredGuidanceLane = CachedCoachPanelState.PreferredGuidanceLane; + Entry.PreferredGuidanceSourceLabel = CachedCoachPanelState.PreferredGuidanceSourceLabel; + Entry.ClosurePolicyLine = CachedCoachSchedulePolicySummary.ClosurePolicyLine; + if (bHasClosureSignal) + { + Entry.ActiveSignalId = ClosureRecoverySignal->SignalId; + Entry.ActiveSignalHeadline = ClosureRecoverySignal->Headline; + Entry.ActiveSignalDetail = ClosureRecoverySignal->Detail; + Entry.ActiveSignalScore = ClosureRecoverySignal->Score; + Entry.bActiveSignalPromoted = + CachedCoachBrief.Signals.Num() > 0 + && CachedCoachBrief.Signals[0].SignalId == ClosureRecoverySignal->SignalId; + } + + const bool bHasClosureRecoveryHistory = + HistoricalKeepCount > 0 + || HistoricalSuppressCount > 0 + || Entry.bFollowUpContinuationFromRecoveryHistory + || Entry.bBorderlineFollowUpSuppressedByRecoveryHistory + || bHasClosureSignal; + if (!bHasClosureRecoveryHistory || !Entry.IsStructurallyValid()) + { + return; + } + + const bool bMatchesLastEntry = ClosureRecoveryHistoryEntries.Num() > 0 + && ClosureRecoveryHistoryEntries.Last().HistoricalFollowUpContinuationCount + == Entry.HistoricalFollowUpContinuationCount + && ClosureRecoveryHistoryEntries.Last().HistoricalBorderlineFollowUpSuppressionCount + == Entry.HistoricalBorderlineFollowUpSuppressionCount + && ClosureRecoveryHistoryEntries.Last().bFollowUpContinuationFromRecoveryHistory + == Entry.bFollowUpContinuationFromRecoveryHistory + && ClosureRecoveryHistoryEntries.Last().bBorderlineFollowUpSuppressedByRecoveryHistory + == Entry.bBorderlineFollowUpSuppressedByRecoveryHistory + && ClosureRecoveryHistoryEntries.Last().PreferredGuidanceLane + == Entry.PreferredGuidanceLane + && ClosureRecoveryHistoryEntries.Last().PreferredGuidanceSourceLabel + == Entry.PreferredGuidanceSourceLabel + && ClosureRecoveryHistoryEntries.Last().ClosurePolicyLine + == Entry.ClosurePolicyLine + && ClosureRecoveryHistoryEntries.Last().ActiveSignalId + == Entry.ActiveSignalId + && ClosureRecoveryHistoryEntries.Last().bActiveSignalPromoted + == Entry.bActiveSignalPromoted; + if (bMatchesLastEntry) + { + return; + } + + ClosureRecoveryHistoryEntries.Add(Entry); + constexpr int32 MaxRetainedClosureRecoveryEntries = 16; + if (ClosureRecoveryHistoryEntries.Num() > MaxRetainedClosureRecoveryEntries) + { + const int32 ExcessCount = + ClosureRecoveryHistoryEntries.Num() - MaxRetainedClosureRecoveryEntries; + ClosureRecoveryHistoryEntries.RemoveAt(0, ExcessCount, EAllowShrinking::No); + } + SelectedClosureRecoveryIndex = ClosureRecoveryHistoryEntries.Num() - 1; +} + void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { const bool bHasActiveRun = CachedRunState.Session.IsStructurallyValid() @@ -5393,6 +5610,60 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() ? *ClosureRecoverySignal->Detail : TEXT("") ); + const FHyperTwistCoachDashboardClosureRecoveryEntry* SelectedClosureRecoveryEntry = + SelectedClosureRecoveryIndex >= 0 + && SelectedClosureRecoveryIndex < ClosureRecoveryHistoryEntries.Num() + ? &ClosureRecoveryHistoryEntries[SelectedClosureRecoveryIndex] + : nullptr; + const FString SelectedClosureHistoryLaneLabel = + SelectedClosureRecoveryEntry != nullptr + ? (SelectedClosureRecoveryEntry->PreferredGuidanceLane + == EHyperTwistTrainingCoachGuidanceLane::FollowUp + ? TEXT("follow-up") + : (SelectedClosureRecoveryEntry->PreferredGuidanceLane + == EHyperTwistTrainingCoachGuidanceLane::Recommended + ? TEXT("recommended") + : TEXT("none"))) + : TEXT("none"); + const FString ClosureRecoveryHistoryStatusLine = + SelectedClosureRecoveryEntry != nullptr + ? FString::Printf( + TEXT("Closure history: %d entries | selected %d/%d | Lane: %s | Recorded: %s"), + ClosureRecoveryHistoryEntries.Num(), + SelectedClosureRecoveryIndex + 1, + ClosureRecoveryHistoryEntries.Num(), + *SelectedClosureHistoryLaneLabel, + *SelectedClosureRecoveryEntry->RecordedAtUtc + ) + : TEXT("Closure history: no retained closure-recovery shifts recorded yet."); + const FString ClosureRecoveryHistoryDetailLine = + SelectedClosureRecoveryEntry != nullptr + ? FString::Printf( + TEXT("Policy: %s | keep %d | suppress %d | continuation active: %s | borderline suppression active: %s | source: %s | promoted: %s | signal: %s%s%s"), + SelectedClosureRecoveryEntry->ClosurePolicyLine.IsEmpty() + ? TEXT("n/a") + : *SelectedClosureRecoveryEntry->ClosurePolicyLine, + SelectedClosureRecoveryEntry->HistoricalFollowUpContinuationCount, + SelectedClosureRecoveryEntry->HistoricalBorderlineFollowUpSuppressionCount, + SelectedClosureRecoveryEntry->bFollowUpContinuationFromRecoveryHistory + ? TEXT("yes") + : TEXT("no"), + SelectedClosureRecoveryEntry->bBorderlineFollowUpSuppressedByRecoveryHistory + ? TEXT("yes") + : TEXT("no"), + SelectedClosureRecoveryEntry->PreferredGuidanceSourceLabel.IsEmpty() + ? TEXT("n/a") + : *SelectedClosureRecoveryEntry->PreferredGuidanceSourceLabel, + SelectedClosureRecoveryEntry->bActiveSignalPromoted ? TEXT("yes") : TEXT("no"), + SelectedClosureRecoveryEntry->ActiveSignalHeadline.IsEmpty() + ? TEXT("n/a") + : *SelectedClosureRecoveryEntry->ActiveSignalHeadline, + SelectedClosureRecoveryEntry->ActiveSignalDetail.IsEmpty() ? TEXT("") : TEXT(" || detail: "), + SelectedClosureRecoveryEntry->ActiveSignalDetail.IsEmpty() + ? TEXT("") + : *SelectedClosureRecoveryEntry->ActiveSignalDetail + ) + : TEXT("Closure history detail: retained keep-vs-suppress shifts will accumulate here as recovery history changes the closure posture over time."); if (ClosureRecoveryHeaderTextBlock != nullptr) { ClosureRecoveryHeaderTextBlock->SetText(FText::FromString(TEXT("[Closure Recovery]"))); @@ -5405,6 +5676,34 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { ClosureRecoveryDetailTextBlock->SetText(FText::FromString(ClosureRecoveryDetailLine)); } + if (ClosureRecoveryHistoryHeaderTextBlock != nullptr) + { + ClosureRecoveryHistoryHeaderTextBlock->SetText(FText::FromString(TEXT("[Closure Recovery History]"))); + } + if (ClosureRecoveryHistoryStatusTextBlock != nullptr) + { + ClosureRecoveryHistoryStatusTextBlock->SetText(FText::FromString(ClosureRecoveryHistoryStatusLine)); + } + if (ClosureRecoveryHistoryDetailTextBlock != nullptr) + { + ClosureRecoveryHistoryDetailTextBlock->SetText(FText::FromString(ClosureRecoveryHistoryDetailLine)); + } + if (PreviousClosureRecoveryButtonLabel != nullptr) + { + PreviousClosureRecoveryButtonLabel->SetText(FText::FromString(TEXT("Prev Closure"))); + } + if (PreviousClosureRecoveryButton != nullptr) + { + PreviousClosureRecoveryButton->SetIsEnabled(ClosureRecoveryHistoryEntries.Num() > 1); + } + if (NextClosureRecoveryButtonLabel != nullptr) + { + NextClosureRecoveryButtonLabel->SetText(FText::FromString(TEXT("Next Closure"))); + } + if (NextClosureRecoveryButton != nullptr) + { + NextClosureRecoveryButton->SetIsEnabled(ClosureRecoveryHistoryEntries.Num() > 1); + } if (QueueRecoveryHeaderTextBlock != nullptr) { QueueRecoveryHeaderTextBlock->SetText(FText::FromString(TEXT("[Queue Recovery]"))); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 23b9432..7a352d4 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -179,6 +179,35 @@ struct FHyperTwistCoachDashboardQueueSuppressionEntry } }; +struct FHyperTwistCoachDashboardClosureRecoveryEntry +{ + FString RecordedAtUtc; + int32 HistoricalFollowUpContinuationCount = 0; + int32 HistoricalBorderlineFollowUpSuppressionCount = 0; + bool bFollowUpContinuationFromRecoveryHistory = false; + bool bBorderlineFollowUpSuppressedByRecoveryHistory = false; + EHyperTwistTrainingCoachGuidanceLane PreferredGuidanceLane = + EHyperTwistTrainingCoachGuidanceLane::None; + FString PreferredGuidanceSourceLabel; + FString ClosurePolicyLine; + FString ActiveSignalId; + FString ActiveSignalHeadline; + FString ActiveSignalDetail; + float ActiveSignalScore = 0.0f; + bool bActiveSignalPromoted = false; + + bool IsStructurallyValid() const + { + return !RecordedAtUtc.IsEmpty() + && (HistoricalFollowUpContinuationCount > 0 + || HistoricalBorderlineFollowUpSuppressionCount > 0 + || bFollowUpContinuationFromRecoveryHistory + || bBorderlineFollowUpSuppressedByRecoveryHistory + || !ActiveSignalId.IsEmpty() + || !ClosurePolicyLine.IsEmpty()); + } +}; + UCLASS(BlueprintType, Blueprintable) class UNREALHYPERTWIST_API UHyperTwistCoachDashboardWidget : public UHyperTwistTrainingPanelWidget { @@ -224,6 +253,9 @@ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") int32 SelectedQueueSuppressionIndex = INDEX_NONE; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") + int32 SelectedClosureRecoveryIndex = INDEX_NONE; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") bool bHasLiveAttemptTimer = false; @@ -317,6 +349,12 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") bool SelectNextQueueSuppressionHistoryEntry(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + bool SelectPreviousClosureRecoveryHistoryEntry(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + bool SelectNextClosureRecoveryHistoryEntry(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") bool StartLiveAttemptTimer(); @@ -593,6 +631,27 @@ protected: UPROPERTY(Transient) TObjectPtr ClosureRecoveryDetailTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr ClosureRecoveryHistoryHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr ClosureRecoveryHistoryStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr ClosureRecoveryHistoryDetailTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr PreviousClosureRecoveryButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr PreviousClosureRecoveryButtonLabel = nullptr; + + UPROPERTY(Transient) + TObjectPtr NextClosureRecoveryButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr NextClosureRecoveryButtonLabel = nullptr; + UPROPERTY(Transient) TObjectPtr QueueRecoveryHeaderTextBlock = nullptr; @@ -863,6 +922,12 @@ protected: UFUNCTION() void HandleNextQueueSuppressionClicked(); + UFUNCTION() + void HandlePreviousClosureRecoveryClicked(); + + UFUNCTION() + void HandleNextClosureRecoveryClicked(); + UFUNCTION() void HandleClearRunClicked(); @@ -919,6 +984,7 @@ private: void SyncSelectedGuidancePreviewCase(); void SyncSelectedGuidanceDecisionHistoryEntry(); void SyncSelectedQueueSuppressionHistoryEntry(); + void SyncSelectedClosureRecoveryHistoryEntry(); void SyncPreferredGuidancePreviewLane(bool bForceAdoptPreferredLane); void UpdateDashboardPresentation(); void RefreshLiveAttemptClock(); @@ -959,6 +1025,7 @@ private: bool bSucceeded ); void RecordQueueSuppressionSnapshot(); + void RecordClosureRecoverySnapshot(); void RecordGuidanceOutcomeDecision( EHyperTwistCoachDashboardOutcomeAction RecommendedAction, EHyperTwistCoachDashboardOutcomeAction AppliedAction, @@ -1009,6 +1076,7 @@ private: FHyperTwistCoachDashboardQueueRecoveryOutcome QueueRecoveryOutcome; TArray QueueRecoveryOutcomeHistory; TArray QueueSuppressionHistoryEntries; + TArray ClosureRecoveryHistoryEntries; EHyperTwistCoachDashboardGuidancePreviewLane ActiveGuidancePreviewLane = EHyperTwistCoachDashboardGuidancePreviewLane::None; int32 SelectedGuidancePreviewCaseIndex = INDEX_NONE;