From 41a828c9098d2f1dbdc912acd152f6559bcf002c Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Mon, 27 Apr 2026 16:37:21 +0200 Subject: [PATCH] Prefer dashboard guidance lane by decision history --- .../HyperTwistCoachDashboardWidget.cpp | 204 ++++++++++++++++++ .../HyperTwistCoachDashboardWidget.h | 29 +++ 2 files changed, 233 insertions(+) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 01b9c87..e981547 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -144,6 +144,7 @@ void UHyperTwistCoachDashboardWidget::RefreshCoachDashboardView() SyncSelectedQueueEntry(); SyncSelectedAttempt(); SyncSelectedGuidanceDecisionHistoryEntry(); + SyncPreferredGuidancePreviewLane(false); UpdateDashboardPresentation(); } @@ -1308,6 +1309,24 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() TEXT("CoachGuidanceDecisionWeightingDetail"), 8 ); + GuidancePreferenceHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachGuidancePreferenceHeader"), + 2 + ); + GuidancePreferenceStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachGuidancePreferenceStatus"), + 2 + ); + GuidancePreferenceDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachGuidancePreferenceDetail"), + 8 + ); GuidanceDecisionHistoryHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( WidgetTree, RootLayout, @@ -2349,6 +2368,30 @@ void UHyperTwistCoachDashboardWidget::SyncSelectedGuidanceDecisionHistoryEntry() } } +void UHyperTwistCoachDashboardWidget::SyncPreferredGuidancePreviewLane(const bool bForceAdoptPreferredLane) +{ + const FHyperTwistCoachDashboardGuidanceLanePreference Preference = BuildGuidanceLanePreference(); + if (!Preference.IsStructurallyValid()) + { + if (ActiveGuidancePreviewLane != EHyperTwistCoachDashboardGuidancePreviewLane::None + && GetActiveGuidancePreviewDeck() == nullptr) + { + ActiveGuidancePreviewLane = EHyperTwistCoachDashboardGuidancePreviewLane::None; + SelectedGuidancePreviewCaseIndex = INDEX_NONE; + } + return; + } + + const bool bHasActivePreviewDeck = GetActiveGuidancePreviewDeck() != nullptr; + if (bForceAdoptPreferredLane + || ActiveGuidancePreviewLane == EHyperTwistCoachDashboardGuidancePreviewLane::None + || !bHasActivePreviewDeck) + { + ActiveGuidancePreviewLane = Preference.PreferredLane; + SyncSelectedGuidancePreviewCase(); + } +} + const FHyperTwistTrainingDeck* UHyperTwistCoachDashboardWidget::GetActiveGuidancePreviewDeck() const { switch (ActiveGuidancePreviewLane) @@ -2671,6 +2714,125 @@ FHyperTwistCoachDashboardDecisionWeighting UHyperTwistCoachDashboardWidget::Buil return Weighting; } +FHyperTwistCoachDashboardGuidanceLanePreference UHyperTwistCoachDashboardWidget::BuildGuidanceLanePreference() const +{ + FHyperTwistCoachDashboardGuidanceLanePreference Preference; + const bool bHasRecommendedDeck = CachedCoachRecommendedDeck.IsStructurallyValid(); + const bool bHasFollowUpDeck = CachedCoachFollowUpDeck.IsStructurallyValid(); + if (!bHasRecommendedDeck && !bHasFollowUpDeck) + { + return Preference; + } + + if (bHasRecommendedDeck && !bHasFollowUpDeck) + { + Preference.PreferredLane = EHyperTwistCoachDashboardGuidancePreviewLane::Recommended; + return Preference; + } + + if (!bHasRecommendedDeck && bHasFollowUpDeck) + { + Preference.PreferredLane = EHyperTwistCoachDashboardGuidancePreviewLane::FollowUp; + return Preference; + } + + float RecommendedSuccessRateSum = 0.0f; + float FollowUpSuccessRateSum = 0.0f; + constexpr int32 MaxDecisionEntriesToInspect = 8; + int32 InspectedDecisionEntries = 0; + for (int32 Index = GuidanceDecisionHistoryEntries.Num() - 1; + Index >= 0 && InspectedDecisionEntries < MaxDecisionEntriesToInspect; + --Index) + { + const FHyperTwistCoachDashboardDecisionHistoryEntry& Entry = GuidanceDecisionHistoryEntries[Index]; + if (Entry.GuidanceLane == EHyperTwistCoachDashboardGuidancePreviewLane::Recommended) + { + ++Preference.RecommendedDecisionCount; + RecommendedSuccessRateSum += Entry.SuccessRate; + switch (Entry.AppliedAction) + { + case EHyperTwistCoachDashboardOutcomeAction::RepeatSelectedCase: + case EHyperTwistCoachDashboardOutcomeAction::RepeatPreviewDeck: + case EHyperTwistCoachDashboardOutcomeAction::BroadenPreviewDeck: + ++Preference.RecommendedContinueBias; + break; + case EHyperTwistCoachDashboardOutcomeAction::RedirectCoachGuidance: + Preference.RecommendedContinueBias -= 2; + break; + default: + break; + } + ++InspectedDecisionEntries; + } + else if (Entry.GuidanceLane == EHyperTwistCoachDashboardGuidancePreviewLane::FollowUp) + { + ++Preference.FollowUpDecisionCount; + FollowUpSuccessRateSum += Entry.SuccessRate; + switch (Entry.AppliedAction) + { + case EHyperTwistCoachDashboardOutcomeAction::RepeatSelectedCase: + case EHyperTwistCoachDashboardOutcomeAction::RepeatPreviewDeck: + case EHyperTwistCoachDashboardOutcomeAction::BroadenPreviewDeck: + ++Preference.FollowUpContinueBias; + break; + case EHyperTwistCoachDashboardOutcomeAction::RedirectCoachGuidance: + Preference.FollowUpContinueBias -= 2; + break; + default: + break; + } + ++InspectedDecisionEntries; + } + } + + if (Preference.RecommendedDecisionCount > 0) + { + Preference.RecommendedAverageSuccessRate = + RecommendedSuccessRateSum / static_cast(Preference.RecommendedDecisionCount); + } + if (Preference.FollowUpDecisionCount > 0) + { + Preference.FollowUpAverageSuccessRate = + FollowUpSuccessRateSum / static_cast(Preference.FollowUpDecisionCount); + } + + Preference.bHasDecisionSignal = + Preference.RecommendedDecisionCount > 0 || Preference.FollowUpDecisionCount > 0; + + float RecommendedLaneScore = 1.0f; + float FollowUpLaneScore = CachedCoachPanelState.bHasFollowUpGuidance ? 1.25f : 1.0f; + if (Preference.RecommendedDecisionCount > 0) + { + RecommendedLaneScore += static_cast(Preference.RecommendedContinueBias); + RecommendedLaneScore += Preference.RecommendedAverageSuccessRate; + } + if (Preference.FollowUpDecisionCount > 0) + { + FollowUpLaneScore += static_cast(Preference.FollowUpContinueBias); + FollowUpLaneScore += Preference.FollowUpAverageSuccessRate; + } + + if (FollowUpLaneScore > RecommendedLaneScore) + { + Preference.PreferredLane = EHyperTwistCoachDashboardGuidancePreviewLane::FollowUp; + } + else if (RecommendedLaneScore > FollowUpLaneScore) + { + Preference.PreferredLane = EHyperTwistCoachDashboardGuidancePreviewLane::Recommended; + } + else + { + Preference.PreferredLane = + ActiveGuidancePreviewLane != EHyperTwistCoachDashboardGuidancePreviewLane::None + ? ActiveGuidancePreviewLane + : (CachedCoachPanelState.bHasFollowUpGuidance + ? EHyperTwistCoachDashboardGuidancePreviewLane::FollowUp + : EHyperTwistCoachDashboardGuidancePreviewLane::Recommended); + } + + return Preference; +} + FString UHyperTwistCoachDashboardWidget::BuildGuidanceOutcomeActionLabel( const EHyperTwistCoachDashboardOutcomeAction Action) const { @@ -2777,6 +2939,7 @@ void UHyperTwistCoachDashboardWidget::RecordGuidanceOutcomeDecision( GuidanceDecisionHistoryEntries.RemoveAt(0, ExcessCount, EAllowShrinking::No); } SelectedGuidanceDecisionIndex = GuidanceDecisionHistoryEntries.Num() - 1; + SyncPreferredGuidancePreviewLane(true); } } @@ -3153,6 +3316,35 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() : (ActiveGuidancePreviewLane == EHyperTwistCoachDashboardGuidancePreviewLane::FollowUp ? TEXT("follow-up") : TEXT("none")); + const FHyperTwistCoachDashboardGuidanceLanePreference GuidanceLanePreference = + BuildGuidanceLanePreference(); + const FString PreferredGuidanceLaneLabel = + GuidanceLanePreference.PreferredLane == EHyperTwistCoachDashboardGuidancePreviewLane::Recommended + ? TEXT("recommended") + : (GuidanceLanePreference.PreferredLane == EHyperTwistCoachDashboardGuidancePreviewLane::FollowUp + ? TEXT("follow-up") + : TEXT("none")); + const FString GuidancePreferenceStatusLine = + GuidanceLanePreference.IsStructurallyValid() + ? FString::Printf( + TEXT("Preference: next surfaced lane %s | Active preview: %s | Source: %s"), + *PreferredGuidanceLaneLabel, + *ActivePreviewLaneLabel, + GuidanceLanePreference.bHasDecisionSignal ? TEXT("decision-informed") : TEXT("availability fallback") + ) + : TEXT("Preference: no coach preview lane is currently available."); + const FString GuidancePreferenceDetailLine = + GuidanceLanePreference.IsStructurallyValid() + ? FString::Printf( + TEXT("Recommended lane: %d decisions | Continue bias %d | Avg success %.2f || Follow-up lane: %d decisions | Continue bias %d | Avg success %.2f | The dashboard adopts this preferred lane whenever it needs to surface the next guidance preview."), + GuidanceLanePreference.RecommendedDecisionCount, + GuidanceLanePreference.RecommendedContinueBias, + GuidanceLanePreference.RecommendedAverageSuccessRate, + GuidanceLanePreference.FollowUpDecisionCount, + GuidanceLanePreference.FollowUpContinueBias, + GuidanceLanePreference.FollowUpAverageSuccessRate + ) + : TEXT("Preference detail: build recommended or follow-up guidance to let the dashboard choose a next surfaced lane."); const FString GuidancePreviewSelectionLine = SelectedGuidancePreviewCase != nullptr && ActivePreviewDeck != nullptr ? FString::Printf( @@ -3637,6 +3829,18 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { GuidanceDecisionWeightingDetailTextBlock->SetText(FText::FromString(GuidanceDecisionWeightingDetailLine)); } + if (GuidancePreferenceHeaderTextBlock != nullptr) + { + GuidancePreferenceHeaderTextBlock->SetText(FText::FromString(TEXT("[Guidance Preference]"))); + } + if (GuidancePreferenceStatusTextBlock != nullptr) + { + GuidancePreferenceStatusTextBlock->SetText(FText::FromString(GuidancePreferenceStatusLine)); + } + if (GuidancePreferenceDetailTextBlock != nullptr) + { + GuidancePreferenceDetailTextBlock->SetText(FText::FromString(GuidancePreferenceDetailLine)); + } if (GuidanceDecisionHistoryHeaderTextBlock != nullptr) { GuidanceDecisionHistoryHeaderTextBlock->SetText(FText::FromString(TEXT("[Decision History]"))); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index e66c1a8..c48140b 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -84,6 +84,24 @@ struct FHyperTwistCoachDashboardDecisionWeighting } }; +struct FHyperTwistCoachDashboardGuidanceLanePreference +{ + EHyperTwistCoachDashboardGuidancePreviewLane PreferredLane = + EHyperTwistCoachDashboardGuidancePreviewLane::None; + int32 RecommendedDecisionCount = 0; + int32 FollowUpDecisionCount = 0; + int32 RecommendedContinueBias = 0; + int32 FollowUpContinueBias = 0; + float RecommendedAverageSuccessRate = 0.0f; + float FollowUpAverageSuccessRate = 0.0f; + bool bHasDecisionSignal = false; + + bool IsStructurallyValid() const + { + return PreferredLane != EHyperTwistCoachDashboardGuidancePreviewLane::None; + } +}; + UCLASS(BlueprintType, Blueprintable) class UNREALHYPERTWIST_API UHyperTwistCoachDashboardWidget : public UHyperTwistTrainingPanelWidget { @@ -294,6 +312,15 @@ protected: UPROPERTY(Transient) TObjectPtr GuidanceDecisionWeightingDetailTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr GuidancePreferenceHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr GuidancePreferenceStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr GuidancePreferenceDetailTextBlock = nullptr; + UPROPERTY(Transient) TObjectPtr GuidanceDecisionHistoryHeaderTextBlock = nullptr; @@ -739,6 +766,7 @@ private: void SyncRetainedRunRecap(); void SyncSelectedGuidancePreviewCase(); void SyncSelectedGuidanceDecisionHistoryEntry(); + void SyncPreferredGuidancePreviewLane(bool bForceAdoptPreferredLane); void UpdateDashboardPresentation(); void RefreshLiveAttemptClock(); FString BuildDefaultDeferredUntilUtc() const; @@ -762,6 +790,7 @@ private: FHyperTwistCoachDashboardDecisionWeighting BuildGuidanceDecisionWeighting( EHyperTwistCoachDashboardOutcomeAction BaseAction ) const; + FHyperTwistCoachDashboardGuidanceLanePreference BuildGuidanceLanePreference() const; FString BuildGuidanceOutcomeActionLabel(EHyperTwistCoachDashboardOutcomeAction Action) const; FHyperTwistTrainingDeck BuildGuidanceOutcomeSelectedCaseDeck() const; void RecordGuidanceOutcomeDecision(