From 69cb604fcdf8520620770573d116bd2f6a37e997 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Mon, 27 Apr 2026 22:27:44 +0200 Subject: [PATCH] Retain coach handoff history on dashboard --- .../HyperTwistCoachDashboardWidget.cpp | 374 ++++++++++++++++-- .../HyperTwistCoachDashboardWidget.h | 79 ++++ 2 files changed, 424 insertions(+), 29 deletions(-) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index b2d9135..703b4cc 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -39,6 +39,34 @@ namespace HyperTwistCoachDashboardWidgetInternal return TEXT("Unknown"); } + FString DescribeGuidanceLane(const EHyperTwistTrainingCoachGuidanceLane Lane) + { + switch (Lane) + { + case EHyperTwistTrainingCoachGuidanceLane::FollowUp: + return TEXT("follow-up"); + case EHyperTwistTrainingCoachGuidanceLane::Recommended: + return TEXT("recommended"); + default: + return TEXT("none"); + } + } + + FString DescribeHandoffRecommendationKind(const EHandoffRecommendationKind Kind) + { + switch (Kind) + { + case EHandoffRecommendationKind::Queue: + return TEXT("queue"); + case EHandoffRecommendationKind::FollowUp: + return TEXT("follow-up"); + case EHandoffRecommendationKind::Recommended: + return TEXT("recommended"); + default: + return TEXT("none"); + } + } + bool IsDashboardHandoffGuidanceSource(const FString& SourceLabel) { return SourceLabel.StartsWith(TEXT("coach-memory-dashboard-")); @@ -300,6 +328,7 @@ void UHyperTwistCoachDashboardWidget::RefreshCoachDashboardView() SyncSelectedQueueEntry(); SyncSelectedAttempt(); SyncSelectedGuidanceDecisionHistoryEntry(); + SyncSelectedHandoffHistoryEntry(); SyncSelectedQueueSuppressionHistoryEntry(); SyncSelectedClosureRecoveryHistoryEntry(); SyncPreferredGuidancePreviewLane(false); @@ -1102,6 +1131,46 @@ bool UHyperTwistCoachDashboardWidget::SelectNextGuidanceDecisionHistoryEntry() return true; } +bool UHyperTwistCoachDashboardWidget::SelectPreviousHandoffHistoryEntry() +{ + if (HandoffHistoryEntries.Num() <= 0) + { + SelectedHandoffHistoryIndex = INDEX_NONE; + return false; + } + + if (SelectedHandoffHistoryIndex == INDEX_NONE) + { + SyncSelectedHandoffHistoryEntry(); + return SelectedHandoffHistoryIndex != INDEX_NONE; + } + + SelectedHandoffHistoryIndex = + (SelectedHandoffHistoryIndex - 1 + HandoffHistoryEntries.Num()) % HandoffHistoryEntries.Num(); + UpdateDashboardPresentation(); + return true; +} + +bool UHyperTwistCoachDashboardWidget::SelectNextHandoffHistoryEntry() +{ + if (HandoffHistoryEntries.Num() <= 0) + { + SelectedHandoffHistoryIndex = INDEX_NONE; + return false; + } + + if (SelectedHandoffHistoryIndex == INDEX_NONE) + { + SyncSelectedHandoffHistoryEntry(); + return SelectedHandoffHistoryIndex != INDEX_NONE; + } + + SelectedHandoffHistoryIndex = + (SelectedHandoffHistoryIndex + 1) % HandoffHistoryEntries.Num(); + UpdateDashboardPresentation(); + return true; +} + bool UHyperTwistCoachDashboardWidget::SelectPreviousQueueSuppressionHistoryEntry() { if (QueueSuppressionHistoryEntries.Num() <= 0) @@ -1469,6 +1538,16 @@ void UHyperTwistCoachDashboardWidget::HandleNextGuidanceDecisionClicked() SelectNextGuidanceDecisionHistoryEntry(); } +void UHyperTwistCoachDashboardWidget::HandlePreviousHandoffHistoryClicked() +{ + SelectPreviousHandoffHistoryEntry(); +} + +void UHyperTwistCoachDashboardWidget::HandleNextHandoffHistoryClicked() +{ + SelectNextHandoffHistoryEntry(); +} + void UHyperTwistCoachDashboardWidget::HandlePreviousQueueSuppressionClicked() { SelectPreviousQueueSuppressionHistoryEntry(); @@ -1826,6 +1905,24 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() TEXT("CoachHandoffRecommendationDetail"), 8 ); + HandoffHistoryHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachHandoffHistoryHeader"), + 2 + ); + HandoffHistoryStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachHandoffHistoryStatus"), + 2 + ); + HandoffHistoryDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachHandoffHistoryDetail"), + 8 + ); GuidanceReviewHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( WidgetTree, RootLayout, @@ -1984,6 +2081,39 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() ); StartRecommendedRunButtonLabel = StartRecommendedRunLabelRaw; } + UHorizontalBox* HandoffHistoryNavRow = WidgetTree->ConstructWidget( + UHorizontalBox::StaticClass(), + TEXT("CoachDashboardHandoffHistoryNavRow") + ); + if (HandoffHistoryNavRow != nullptr) + { + if (UVerticalBoxSlot* HandoffHistoryNavRowSlot = RootLayout->AddChildToVerticalBox(HandoffHistoryNavRow)) + { + HandoffHistoryNavRowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 0.0f)); + } + + UTextBlock* PreviousHandoffHistoryLabelRaw = nullptr; + PreviousHandoffHistoryButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + HandoffHistoryNavRow, + TEXT("PreviousHandoffHistoryButton"), + TEXT("PreviousHandoffHistoryButtonLabel"), + TEXT("Prev Handoff"), + PreviousHandoffHistoryLabelRaw + ); + PreviousHandoffHistoryButtonLabel = PreviousHandoffHistoryLabelRaw; + + UTextBlock* NextHandoffHistoryLabelRaw = nullptr; + NextHandoffHistoryButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + HandoffHistoryNavRow, + TEXT("NextHandoffHistoryButton"), + TEXT("NextHandoffHistoryButtonLabel"), + TEXT("Next Handoff"), + NextHandoffHistoryLabelRaw + ); + NextHandoffHistoryButtonLabel = NextHandoffHistoryLabelRaw; + } UHorizontalBox* GuidancePreviewActionRow = WidgetTree->ConstructWidget( UHorizontalBox::StaticClass(), TEXT("CoachDashboardGuidancePreviewActions") @@ -2787,6 +2917,14 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() { NextGuidanceDecisionButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleNextGuidanceDecisionClicked); } + if (PreviousHandoffHistoryButton != nullptr) + { + PreviousHandoffHistoryButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePreviousHandoffHistoryClicked); + } + if (NextHandoffHistoryButton != nullptr) + { + NextHandoffHistoryButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleNextHandoffHistoryClicked); + } if (DismissRecapButton != nullptr) { DismissRecapButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleDismissRecapClicked); @@ -3038,6 +3176,21 @@ void UHyperTwistCoachDashboardWidget::SyncSelectedGuidanceDecisionHistoryEntry() } } +void UHyperTwistCoachDashboardWidget::SyncSelectedHandoffHistoryEntry() +{ + if (HandoffHistoryEntries.Num() <= 0) + { + SelectedHandoffHistoryIndex = INDEX_NONE; + return; + } + + if (SelectedHandoffHistoryIndex < 0 + || SelectedHandoffHistoryIndex >= HandoffHistoryEntries.Num()) + { + SelectedHandoffHistoryIndex = HandoffHistoryEntries.Num() - 1; + } +} + void UHyperTwistCoachDashboardWidget::SyncSelectedQueueSuppressionHistoryEntry() { if (QueueSuppressionHistoryEntries.Num() <= 0) @@ -3914,6 +4067,88 @@ void UHyperTwistCoachDashboardWidget::RecordQueueRecoveryOutcome( } } +void UHyperTwistCoachDashboardWidget::RecordCoachHandoffHistorySnapshot( + const FString& RecommendedHandoffKindLabel, + const FString& RecommendationStatusLine, + const FString& RecommendationDetailLine, + const FString& QueueSourceLabel, + const int32 ClosureContinuationBias, + const int32 ClosureSuppressionBias, + const bool bQueueReady, + const bool bFollowUpReady, + const bool bRecommendedReady) +{ + FHyperTwistCoachDashboardHandoffHistoryEntry Entry; + Entry.RecordedAtUtc = FDateTime::UtcNow().ToIso8601(); + Entry.RecommendedHandoffKindLabel = + RecommendedHandoffKindLabel.IsEmpty() ? TEXT("none") : RecommendedHandoffKindLabel; + Entry.RecommendationStatusLine = RecommendationStatusLine; + Entry.RecommendationDetailLine = RecommendationDetailLine; + Entry.QueueSourceLabel = QueueSourceLabel; + Entry.PreferredGuidanceLane = CachedCoachPanelState.PreferredGuidanceLane; + Entry.PreferredGuidanceSourceLabel = CachedCoachPanelState.PreferredGuidanceSourceLabel; + Entry.PreferredRecoveryAction = CachedCoachPanelState.PreferredQueueRecoveryAction; + Entry.PreferredRecoveryActionSourceLabel = + CachedCoachPanelState.PreferredQueueRecoveryActionSourceLabel; + Entry.AcceptedQueuedHandoffCount = + CachedCoachQueueExecutionSummary.StartedQueuedHandoffEventCount; + Entry.AcceptedFollowUpHandoffCount = + CachedCoachQueueExecutionSummary.StartedFollowUpHandoffEventCount; + Entry.AcceptedRecommendedHandoffCount = + CachedCoachQueueExecutionSummary.StartedRecommendedHandoffEventCount; + Entry.ClosureContinuationBias = ClosureContinuationBias; + Entry.ClosureSuppressionBias = ClosureSuppressionBias; + Entry.bQueueReady = bQueueReady; + Entry.bFollowUpReady = bFollowUpReady; + Entry.bRecommendedReady = bRecommendedReady; + if (!Entry.IsStructurallyValid()) + { + return; + } + + const bool bMatchesLastEntry = HandoffHistoryEntries.Num() > 0 + && HandoffHistoryEntries.Last().RecommendedHandoffKindLabel + == Entry.RecommendedHandoffKindLabel + && HandoffHistoryEntries.Last().RecommendationStatusLine + == Entry.RecommendationStatusLine + && HandoffHistoryEntries.Last().RecommendationDetailLine + == Entry.RecommendationDetailLine + && HandoffHistoryEntries.Last().QueueSourceLabel == Entry.QueueSourceLabel + && HandoffHistoryEntries.Last().PreferredGuidanceLane == Entry.PreferredGuidanceLane + && HandoffHistoryEntries.Last().PreferredGuidanceSourceLabel + == Entry.PreferredGuidanceSourceLabel + && HandoffHistoryEntries.Last().PreferredRecoveryAction + == Entry.PreferredRecoveryAction + && HandoffHistoryEntries.Last().PreferredRecoveryActionSourceLabel + == Entry.PreferredRecoveryActionSourceLabel + && HandoffHistoryEntries.Last().AcceptedQueuedHandoffCount + == Entry.AcceptedQueuedHandoffCount + && HandoffHistoryEntries.Last().AcceptedFollowUpHandoffCount + == Entry.AcceptedFollowUpHandoffCount + && HandoffHistoryEntries.Last().AcceptedRecommendedHandoffCount + == Entry.AcceptedRecommendedHandoffCount + && HandoffHistoryEntries.Last().ClosureContinuationBias + == Entry.ClosureContinuationBias + && HandoffHistoryEntries.Last().ClosureSuppressionBias + == Entry.ClosureSuppressionBias + && HandoffHistoryEntries.Last().bQueueReady == Entry.bQueueReady + && HandoffHistoryEntries.Last().bFollowUpReady == Entry.bFollowUpReady + && HandoffHistoryEntries.Last().bRecommendedReady == Entry.bRecommendedReady; + if (bMatchesLastEntry) + { + return; + } + + HandoffHistoryEntries.Add(Entry); + constexpr int32 MaxRetainedHandoffHistoryEntries = 16; + if (HandoffHistoryEntries.Num() > MaxRetainedHandoffHistoryEntries) + { + const int32 ExcessCount = HandoffHistoryEntries.Num() - MaxRetainedHandoffHistoryEntries; + HandoffHistoryEntries.RemoveAt(0, ExcessCount, EAllowShrinking::No); + } + SelectedHandoffHistoryIndex = HandoffHistoryEntries.Num() - 1; +} + void UHyperTwistCoachDashboardWidget::RecordQueueSuppressionSnapshot() { const bool bHasPrimarySuppression = @@ -4369,6 +4604,12 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::None; FString CoachHandoffRecommendationStatusLine; FString CoachHandoffRecommendationDetailLine; + const bool bQueueReadyForHandoff = CachedCoachPanelState.bCanStartQueuedRun; + const bool bFollowUpReadyForHandoff = + CachedCoachPanelState.bCanStartCoachFollowUpRun + && CachedCoachPanelState.bHasFollowUpGuidance; + const bool bRecommendedReadyForHandoff = + CachedCoachPanelState.bCanStartCoachRecommendedRun; if (!bCanUseCoachHandoff) { CoachHandoffRecommendationStatusLine = @@ -4383,13 +4624,12 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() if (HandoffClosureBias.bHasSignal && HandoffClosureBias.ContinuationBias >= HandoffClosureBias.SuppressionBias + 2) { - if (CachedCoachPanelState.bCanStartQueuedRun && bQueueLooksFollowUpOrRecovery) + if (bQueueReadyForHandoff && bQueueLooksFollowUpOrRecovery) { HandoffRecommendationKind = HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::Queue; } - else if (CachedCoachPanelState.bCanStartCoachFollowUpRun - && CachedCoachPanelState.bHasFollowUpGuidance) + else if (bFollowUpReadyForHandoff) { HandoffRecommendationKind = HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::FollowUp; @@ -4398,12 +4638,12 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() else if (HandoffClosureBias.bHasSignal && HandoffClosureBias.SuppressionBias >= HandoffClosureBias.ContinuationBias + 2) { - if (CachedCoachPanelState.bCanStartQueuedRun && bQueueLooksRecommendedOrPrimary) + if (bQueueReadyForHandoff && bQueueLooksRecommendedOrPrimary) { HandoffRecommendationKind = HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::Queue; } - else if (CachedCoachPanelState.bCanStartCoachRecommendedRun) + else if (bRecommendedReadyForHandoff) { HandoffRecommendationKind = HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::Recommended; @@ -4413,18 +4653,17 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() if (HandoffRecommendationKind == HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::None) { - if (CachedCoachPanelState.bCanStartQueuedRun) + if (bQueueReadyForHandoff) { HandoffRecommendationKind = HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::Queue; } - else if (CachedCoachPanelState.bCanStartCoachFollowUpRun - && CachedCoachPanelState.bHasFollowUpGuidance) + else if (bFollowUpReadyForHandoff) { HandoffRecommendationKind = HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::FollowUp; } - else if (CachedCoachPanelState.bCanStartCoachRecommendedRun) + else if (bRecommendedReadyForHandoff) { HandoffRecommendationKind = HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::Recommended; @@ -4461,23 +4700,29 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() TEXT("Closure continuation bias %d | Closure suppression bias %d | Queue ready: %s | Follow-up ready: %s | Recommended ready: %s | Preferred lane: %s | Memory source: %s%s"), HandoffClosureBias.ContinuationBias, HandoffClosureBias.SuppressionBias, - CachedCoachPanelState.bCanStartQueuedRun ? TEXT("yes") : TEXT("no"), - (CachedCoachPanelState.bCanStartCoachFollowUpRun && CachedCoachPanelState.bHasFollowUpGuidance) - ? TEXT("yes") - : TEXT("no"), - CachedCoachPanelState.bCanStartCoachRecommendedRun ? TEXT("yes") : TEXT("no"), - CachedCoachPanelState.PreferredGuidanceLane == EHyperTwistTrainingCoachGuidanceLane::FollowUp - ? TEXT("follow-up") - : (CachedCoachPanelState.PreferredGuidanceLane - == EHyperTwistTrainingCoachGuidanceLane::Recommended - ? TEXT("recommended") - : TEXT("none")), + bQueueReadyForHandoff ? TEXT("yes") : TEXT("no"), + bFollowUpReadyForHandoff ? TEXT("yes") : TEXT("no"), + bRecommendedReadyForHandoff ? TEXT("yes") : TEXT("no"), + *HyperTwistCoachDashboardWidgetInternal::DescribeGuidanceLane( + CachedCoachPanelState.PreferredGuidanceLane), *HandoffMemorySourceLabel, bDashboardHandoffMemorySource ? TEXT(" | Accepted dashboard handoffs are actively reinforcing this recommendation.") : TEXT("") ); } + RecordCoachHandoffHistorySnapshot( + HyperTwistCoachDashboardWidgetInternal::DescribeHandoffRecommendationKind( + HandoffRecommendationKind), + CoachHandoffRecommendationStatusLine, + CoachHandoffRecommendationDetailLine, + CachedCoachPanelState.NextQueueSourceLabel, + HandoffClosureBias.ContinuationBias, + HandoffClosureBias.SuppressionBias, + bQueueReadyForHandoff, + bFollowUpReadyForHandoff, + bRecommendedReadyForHandoff + ); FString QueuedRunLabel = CachedCoachPanelState.NextQueueSourceLabel.IsEmpty() ? TEXT("Start Queue") : FString::Printf(TEXT("Queue: %s"), *CachedCoachPanelState.NextQueueSourceLabel); @@ -4501,6 +4746,55 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() default: break; } + const FHyperTwistCoachDashboardHandoffHistoryEntry* SelectedHandoffHistoryEntry = + SelectedHandoffHistoryIndex >= 0 + && SelectedHandoffHistoryIndex < HandoffHistoryEntries.Num() + ? &HandoffHistoryEntries[SelectedHandoffHistoryIndex] + : nullptr; + const FString HandoffHistoryStatusLine = + SelectedHandoffHistoryEntry != nullptr + ? FString::Printf( + TEXT("Handoff history: %d entries | selected %d/%d | Recommendation: %s | Recorded: %s"), + HandoffHistoryEntries.Num(), + SelectedHandoffHistoryIndex + 1, + HandoffHistoryEntries.Num(), + SelectedHandoffHistoryEntry->RecommendedHandoffKindLabel.IsEmpty() + ? TEXT("none") + : *SelectedHandoffHistoryEntry->RecommendedHandoffKindLabel, + *SelectedHandoffHistoryEntry->RecordedAtUtc + ) + : TEXT("Handoff history: no retained handoff-pattern shifts recorded yet."); + const FString HandoffHistoryDetailLine = + SelectedHandoffHistoryEntry != nullptr + ? FString::Printf( + TEXT("Accepted queued/follow-up/recommended: %d/%d/%d | queue source: %s | preferred lane: %s | memory source: %s | recovery action: %s | recovery source: %s | closure continuation bias %d | closure suppression bias %d | readiness queue/follow-up/recommended: %s/%s/%s || %s"), + SelectedHandoffHistoryEntry->AcceptedQueuedHandoffCount, + SelectedHandoffHistoryEntry->AcceptedFollowUpHandoffCount, + SelectedHandoffHistoryEntry->AcceptedRecommendedHandoffCount, + SelectedHandoffHistoryEntry->QueueSourceLabel.IsEmpty() + ? TEXT("n/a") + : *SelectedHandoffHistoryEntry->QueueSourceLabel, + *HyperTwistCoachDashboardWidgetInternal::DescribeGuidanceLane( + SelectedHandoffHistoryEntry->PreferredGuidanceLane), + *HyperTwistCoachDashboardWidgetInternal::DescribeCoachMemorySourceLabel( + SelectedHandoffHistoryEntry->PreferredGuidanceSourceLabel), + SelectedHandoffHistoryEntry->PreferredRecoveryAction + == EHyperTwistTrainingQueueRecoveryAction::None + ? TEXT("none") + : *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + SelectedHandoffHistoryEntry->PreferredRecoveryAction), + *HyperTwistCoachDashboardWidgetInternal::DescribeCoachMemorySourceLabel( + SelectedHandoffHistoryEntry->PreferredRecoveryActionSourceLabel), + SelectedHandoffHistoryEntry->ClosureContinuationBias, + SelectedHandoffHistoryEntry->ClosureSuppressionBias, + SelectedHandoffHistoryEntry->bQueueReady ? TEXT("yes") : TEXT("no"), + SelectedHandoffHistoryEntry->bFollowUpReady ? TEXT("yes") : TEXT("no"), + SelectedHandoffHistoryEntry->bRecommendedReady ? TEXT("yes") : TEXT("no"), + SelectedHandoffHistoryEntry->RecommendationDetailLine.IsEmpty() + ? *SelectedHandoffHistoryEntry->RecommendationStatusLine + : *SelectedHandoffHistoryEntry->RecommendationDetailLine + ) + : TEXT("Handoff history detail: retained recommendation shifts and accepted queued/follow-up/recommended handoff patterns will accumulate here over time."); const auto BuildDeckPreviewLine = [](const FHyperTwistTrainingDeck& Deck, const FHyperTwistCoachBrief& Brief, const TCHAR* EmptyPrefix) { if (Deck.IsStructurallyValid()) @@ -5422,6 +5716,34 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { CoachHandoffRecommendationDetailTextBlock->SetText(FText::FromString(CoachHandoffRecommendationDetailLine)); } + if (HandoffHistoryHeaderTextBlock != nullptr) + { + HandoffHistoryHeaderTextBlock->SetText(FText::FromString(TEXT("[Handoff History]"))); + } + if (HandoffHistoryStatusTextBlock != nullptr) + { + HandoffHistoryStatusTextBlock->SetText(FText::FromString(HandoffHistoryStatusLine)); + } + if (HandoffHistoryDetailTextBlock != nullptr) + { + HandoffHistoryDetailTextBlock->SetText(FText::FromString(HandoffHistoryDetailLine)); + } + if (PreviousHandoffHistoryButtonLabel != nullptr) + { + PreviousHandoffHistoryButtonLabel->SetText(FText::FromString(TEXT("Prev Handoff"))); + } + if (PreviousHandoffHistoryButton != nullptr) + { + PreviousHandoffHistoryButton->SetIsEnabled(HandoffHistoryEntries.Num() > 1); + } + if (NextHandoffHistoryButtonLabel != nullptr) + { + NextHandoffHistoryButtonLabel->SetText(FText::FromString(TEXT("Next Handoff"))); + } + if (NextHandoffHistoryButton != nullptr) + { + NextHandoffHistoryButton->SetIsEnabled(HandoffHistoryEntries.Num() > 1); + } if (GuidanceReviewHeaderTextBlock != nullptr) { GuidanceReviewHeaderTextBlock->SetText(FText::FromString(TEXT("[Guidance Review]"))); @@ -5460,7 +5782,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() } if (StartQueuedRunButton != nullptr) { - StartQueuedRunButton->SetIsEnabled(bCanUseCoachHandoff && CachedCoachPanelState.bCanStartQueuedRun); + StartQueuedRunButton->SetIsEnabled(bCanUseCoachHandoff && bQueueReadyForHandoff); } if (StartFollowUpRunButtonLabel != nullptr) { @@ -5468,11 +5790,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() } if (StartFollowUpRunButton != nullptr) { - StartFollowUpRunButton->SetIsEnabled( - bCanUseCoachHandoff - && CachedCoachPanelState.bCanStartCoachFollowUpRun - && CachedCoachPanelState.bHasFollowUpGuidance - ); + StartFollowUpRunButton->SetIsEnabled(bCanUseCoachHandoff && bFollowUpReadyForHandoff); } if (StartRecommendedRunButtonLabel != nullptr) { @@ -5480,9 +5798,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() } if (StartRecommendedRunButton != nullptr) { - StartRecommendedRunButton->SetIsEnabled( - bCanUseCoachHandoff && CachedCoachPanelState.bCanStartCoachRecommendedRun - ); + StartRecommendedRunButton->SetIsEnabled(bCanUseCoachHandoff && bRecommendedReadyForHandoff); } if (PreviewRecommendedButtonLabel != nullptr) { diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 721452c..4e62879 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -214,6 +214,36 @@ struct FHyperTwistCoachDashboardClosureRecoveryEntry } }; +struct FHyperTwistCoachDashboardHandoffHistoryEntry +{ + FString RecordedAtUtc; + FString RecommendedHandoffKindLabel; + FString RecommendationStatusLine; + FString RecommendationDetailLine; + FString QueueSourceLabel; + EHyperTwistTrainingCoachGuidanceLane PreferredGuidanceLane = + EHyperTwistTrainingCoachGuidanceLane::None; + FString PreferredGuidanceSourceLabel; + EHyperTwistTrainingQueueRecoveryAction PreferredRecoveryAction = + EHyperTwistTrainingQueueRecoveryAction::None; + FString PreferredRecoveryActionSourceLabel; + int32 AcceptedQueuedHandoffCount = 0; + int32 AcceptedFollowUpHandoffCount = 0; + int32 AcceptedRecommendedHandoffCount = 0; + int32 ClosureContinuationBias = 0; + int32 ClosureSuppressionBias = 0; + bool bQueueReady = false; + bool bFollowUpReady = false; + bool bRecommendedReady = false; + + bool IsStructurallyValid() const + { + return !RecordedAtUtc.IsEmpty() + && !RecommendedHandoffKindLabel.IsEmpty() + && !RecommendationStatusLine.IsEmpty(); + } +}; + UCLASS(BlueprintType, Blueprintable) class UNREALHYPERTWIST_API UHyperTwistCoachDashboardWidget : public UHyperTwistTrainingPanelWidget { @@ -256,6 +286,9 @@ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") int32 SelectedGuidanceDecisionIndex = INDEX_NONE; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") + int32 SelectedHandoffHistoryIndex = INDEX_NONE; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") int32 SelectedQueueSuppressionIndex = INDEX_NONE; @@ -349,6 +382,12 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") bool SelectNextGuidanceDecisionHistoryEntry(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + bool SelectPreviousHandoffHistoryEntry(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + bool SelectNextHandoffHistoryEntry(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") bool SelectPreviousQueueSuppressionHistoryEntry(); @@ -490,6 +529,27 @@ protected: UPROPERTY(Transient) TObjectPtr CoachHandoffRecommendationDetailTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr HandoffHistoryHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr HandoffHistoryStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr HandoffHistoryDetailTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr PreviousHandoffHistoryButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr PreviousHandoffHistoryButtonLabel = nullptr; + + UPROPERTY(Transient) + TObjectPtr NextHandoffHistoryButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr NextHandoffHistoryButtonLabel = nullptr; + UPROPERTY(Transient) TObjectPtr GuidanceReviewHeaderTextBlock = nullptr; @@ -931,6 +991,12 @@ protected: UFUNCTION() void HandleNextGuidanceDecisionClicked(); + UFUNCTION() + void HandlePreviousHandoffHistoryClicked(); + + UFUNCTION() + void HandleNextHandoffHistoryClicked(); + UFUNCTION() void HandlePreviousQueueSuppressionClicked(); @@ -998,6 +1064,7 @@ private: void SyncRetainedRunRecap(); void SyncSelectedGuidancePreviewCase(); void SyncSelectedGuidanceDecisionHistoryEntry(); + void SyncSelectedHandoffHistoryEntry(); void SyncSelectedQueueSuppressionHistoryEntry(); void SyncSelectedClosureRecoveryHistoryEntry(); void SyncPreferredGuidancePreviewLane(bool bForceAdoptPreferredLane); @@ -1039,6 +1106,17 @@ private: bool bBeforeCompletionGap, bool bSucceeded ); + void RecordCoachHandoffHistorySnapshot( + const FString& RecommendedHandoffKindLabel, + const FString& RecommendationStatusLine, + const FString& RecommendationDetailLine, + const FString& QueueSourceLabel, + int32 ClosureContinuationBias, + int32 ClosureSuppressionBias, + bool bQueueReady, + bool bFollowUpReady, + bool bRecommendedReady + ); void RecordQueueSuppressionSnapshot(); void RecordClosureRecoverySnapshot(); void RecordGuidanceOutcomeDecision( @@ -1087,6 +1165,7 @@ private: FHyperTwistTrainingDeck RetainedGuidanceComparisonDeck; FString RetainedGuidanceComparisonSelectedCaseId; TArray GuidanceDecisionHistoryEntries; + TArray HandoffHistoryEntries; bool bHasQueueRecoveryOutcome = false; FHyperTwistCoachDashboardQueueRecoveryOutcome QueueRecoveryOutcome; TArray QueueRecoveryOutcomeHistory;