diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 472d9b3..60c45b0 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -20,6 +20,14 @@ namespace HyperTwistCoachDashboardWidgetInternal bool bHasSignal = false; }; + enum class EHandoffRecommendationKind : uint8 + { + None, + Queue, + FollowUp, + Recommended + }; + template FString EnumDisplayName(const TEnum Value) { @@ -1750,6 +1758,24 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() TEXT("CoachHandoffStatus"), 2 ); + CoachHandoffRecommendationHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachHandoffRecommendationHeader"), + 2 + ); + CoachHandoffRecommendationStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachHandoffRecommendationStatus"), + 2 + ); + CoachHandoffRecommendationDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachHandoffRecommendationDetail"), + 8 + ); GuidanceReviewHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( WidgetTree, RootLayout, @@ -4271,15 +4297,150 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() *RecommendedLine ); } - const FString QueuedRunLabel = CachedCoachPanelState.NextQueueSourceLabel.IsEmpty() + const HyperTwistCoachDashboardWidgetInternal::FClosureRecoveryDashboardBias HandoffClosureBias = + HyperTwistCoachDashboardWidgetInternal::DeriveClosureRecoveryDashboardBias( + ClosureRecoveryHistoryEntries + ); + const bool bQueueLooksFollowUpOrRecovery = + CachedCoachPanelState.NextQueueSourceLabel.Contains(TEXT("follow-up")) + || CachedCoachPanelState.NextQueueSourceLabel.Contains(TEXT("queue-recovery")) + || CachedCoachPanelState.NextQueueSourceLabel.Contains(TEXT("closure_history_follow_up")); + const bool bQueueLooksRecommendedOrPrimary = + CachedCoachPanelState.NextQueueSourceLabel.Contains(TEXT("primary")) + || CachedCoachPanelState.NextQueueSourceLabel.Contains(TEXT("recommended")) + || CachedCoachPanelState.NextQueueSourceLabel.Contains(TEXT("closure_history_recommended")); + HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind HandoffRecommendationKind = + HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::None; + FString CoachHandoffRecommendationStatusLine; + FString CoachHandoffRecommendationDetailLine; + if (!bCanUseCoachHandoff) + { + CoachHandoffRecommendationStatusLine = + bHasLiveAttemptTimer + ? TEXT("Recommendation: wait for the live attempt to finish before applying closure-aware handoff.") + : TEXT("Recommendation: finish or clear the current run before applying closure-aware handoff."); + CoachHandoffRecommendationDetailLine = + TEXT("Handoff recommendation detail: closure-history pressure is retained, but handoff choice is paused while the dashboard is busy."); + } + else + { + if (HandoffClosureBias.bHasSignal + && HandoffClosureBias.ContinuationBias >= HandoffClosureBias.SuppressionBias + 2) + { + if (CachedCoachPanelState.bCanStartQueuedRun && bQueueLooksFollowUpOrRecovery) + { + HandoffRecommendationKind = + HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::Queue; + } + else if (CachedCoachPanelState.bCanStartCoachFollowUpRun + && CachedCoachPanelState.bHasFollowUpGuidance) + { + HandoffRecommendationKind = + HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::FollowUp; + } + } + else if (HandoffClosureBias.bHasSignal + && HandoffClosureBias.SuppressionBias >= HandoffClosureBias.ContinuationBias + 2) + { + if (CachedCoachPanelState.bCanStartQueuedRun && bQueueLooksRecommendedOrPrimary) + { + HandoffRecommendationKind = + HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::Queue; + } + else if (CachedCoachPanelState.bCanStartCoachRecommendedRun) + { + HandoffRecommendationKind = + HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::Recommended; + } + } + + if (HandoffRecommendationKind + == HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::None) + { + if (CachedCoachPanelState.bCanStartQueuedRun) + { + HandoffRecommendationKind = + HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::Queue; + } + else if (CachedCoachPanelState.bCanStartCoachFollowUpRun + && CachedCoachPanelState.bHasFollowUpGuidance) + { + HandoffRecommendationKind = + HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::FollowUp; + } + else if (CachedCoachPanelState.bCanStartCoachRecommendedRun) + { + HandoffRecommendationKind = + HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::Recommended; + } + } + + switch (HandoffRecommendationKind) + { + case HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::Queue: + CoachHandoffRecommendationStatusLine = FString::Printf( + TEXT("Recommendation: start the queued coach run next. Source: %s"), + CachedCoachPanelState.NextQueueSourceLabel.IsEmpty() + ? TEXT("n/a") + : *CachedCoachPanelState.NextQueueSourceLabel + ); + break; + case HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::FollowUp: + CoachHandoffRecommendationStatusLine = TEXT( + "Recommendation: continue with a follow-up coach run next." + ); + break; + case HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::Recommended: + CoachHandoffRecommendationStatusLine = TEXT( + "Recommendation: redirect into the recommended coach run next." + ); + break; + default: + CoachHandoffRecommendationStatusLine = + TEXT("Recommendation: no coach-directed handoff is currently ready."); + break; + } + + CoachHandoffRecommendationDetailLine = FString::Printf( + TEXT("Closure continuation bias %d | Closure suppression bias %d | Queue ready: %s | Follow-up ready: %s | Recommended ready: %s | Preferred lane: %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")) + ); + } + FString QueuedRunLabel = CachedCoachPanelState.NextQueueSourceLabel.IsEmpty() ? TEXT("Start Queue") : FString::Printf(TEXT("Queue: %s"), *CachedCoachPanelState.NextQueueSourceLabel); - const FString FollowUpRunLabel = CachedCoachFollowUpBrief.PrimaryActionLabel.IsEmpty() + FString FollowUpRunLabel = CachedCoachFollowUpBrief.PrimaryActionLabel.IsEmpty() ? TEXT("Start Follow-Up") : CachedCoachFollowUpBrief.PrimaryActionLabel; - const FString RecommendedRunLabel = CachedCoachBrief.PrimaryActionLabel.IsEmpty() + FString RecommendedRunLabel = CachedCoachBrief.PrimaryActionLabel.IsEmpty() ? TEXT("Start Recommended") : CachedCoachBrief.PrimaryActionLabel; + switch (HandoffRecommendationKind) + { + case HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::Queue: + QueuedRunLabel = FString::Printf(TEXT("Recommended: %s"), *QueuedRunLabel); + break; + case HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::FollowUp: + FollowUpRunLabel = FString::Printf(TEXT("Recommended: %s"), *FollowUpRunLabel); + break; + case HyperTwistCoachDashboardWidgetInternal::EHandoffRecommendationKind::Recommended: + RecommendedRunLabel = FString::Printf(TEXT("Recommended: %s"), *RecommendedRunLabel); + break; + default: + break; + } const auto BuildDeckPreviewLine = [](const FHyperTwistTrainingDeck& Deck, const FHyperTwistCoachBrief& Brief, const TCHAR* EmptyPrefix) { if (Deck.IsStructurallyValid()) @@ -5184,6 +5345,18 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { CoachHandoffStatusTextBlock->SetText(FText::FromString(CoachHandoffStatusLine)); } + if (CoachHandoffRecommendationHeaderTextBlock != nullptr) + { + CoachHandoffRecommendationHeaderTextBlock->SetText(FText::FromString(TEXT("[Handoff Recommendation]"))); + } + if (CoachHandoffRecommendationStatusTextBlock != nullptr) + { + CoachHandoffRecommendationStatusTextBlock->SetText(FText::FromString(CoachHandoffRecommendationStatusLine)); + } + if (CoachHandoffRecommendationDetailTextBlock != nullptr) + { + CoachHandoffRecommendationDetailTextBlock->SetText(FText::FromString(CoachHandoffRecommendationDetailLine)); + } if (GuidanceReviewHeaderTextBlock != nullptr) { GuidanceReviewHeaderTextBlock->SetText(FText::FromString(TEXT("[Guidance Review]"))); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 8fba164..721452c 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -481,6 +481,15 @@ protected: UPROPERTY(Transient) TObjectPtr CoachHandoffStatusTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr CoachHandoffRecommendationHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr CoachHandoffRecommendationStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr CoachHandoffRecommendationDetailTextBlock = nullptr; + UPROPERTY(Transient) TObjectPtr GuidanceReviewHeaderTextBlock = nullptr;