diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 9996bb7..01b9c87 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -1290,6 +1290,24 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() TEXT("CoachGuidanceOutcomeDetail"), 8 ); + GuidanceDecisionWeightingHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachGuidanceDecisionWeightingHeader"), + 2 + ); + GuidanceDecisionWeightingStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachGuidanceDecisionWeightingStatus"), + 2 + ); + GuidanceDecisionWeightingDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachGuidanceDecisionWeightingDetail"), + 8 + ); GuidanceDecisionHistoryHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( WidgetTree, RootLayout, @@ -2516,6 +2534,143 @@ EHyperTwistCoachDashboardOutcomeAction UHyperTwistCoachDashboardWidget::DeriveGu : EHyperTwistCoachDashboardOutcomeAction::RedirectCoachGuidance; } +FHyperTwistCoachDashboardDecisionWeighting UHyperTwistCoachDashboardWidget::BuildGuidanceDecisionWeighting( + const EHyperTwistCoachDashboardOutcomeAction BaseAction +) const +{ + FHyperTwistCoachDashboardDecisionWeighting Weighting; + Weighting.BaseAction = BaseAction; + Weighting.WeightedAction = BaseAction; + if (BaseAction == EHyperTwistCoachDashboardOutcomeAction::None) + { + return Weighting; + } + + const FHyperTwistTrainingSessionSummary* DisplayedRunRecap = nullptr; + const FHyperTwistCoachBrief* DisplayedGuidanceComparisonBrief = nullptr; + const FHyperTwistTrainingDeck* DisplayedGuidanceComparisonDeck = nullptr; + FString DisplayedGuidanceComparisonSelectedCaseId; + bool bDisplayedGuidanceComparisonSingleCase = false; + if (!ResolveDisplayedGuidanceComparisonContext( + DisplayedRunRecap, + DisplayedGuidanceComparisonBrief, + DisplayedGuidanceComparisonDeck, + DisplayedGuidanceComparisonSelectedCaseId, + bDisplayedGuidanceComparisonSingleCase) + || DisplayedRunRecap == nullptr + || DisplayedGuidanceComparisonDeck == nullptr) + { + return Weighting; + } + + EHyperTwistCoachDashboardGuidancePreviewLane DisplayedGuidanceComparisonLane = + EHyperTwistCoachDashboardGuidancePreviewLane::None; + if (!ActiveGuidanceLaunchSessionId.IsEmpty() + && DisplayedRunRecap->TrainingSessionId == ActiveGuidanceLaunchSessionId) + { + DisplayedGuidanceComparisonLane = ActiveGuidanceLaunchLane; + } + else if (!RetainedGuidanceComparisonSessionId.IsEmpty() + && DisplayedRunRecap->TrainingSessionId == RetainedGuidanceComparisonSessionId) + { + DisplayedGuidanceComparisonLane = RetainedGuidanceComparisonLane; + } + + if (DisplayedGuidanceComparisonLane == EHyperTwistCoachDashboardGuidancePreviewLane::None) + { + return Weighting; + } + + constexpr int32 MaxMatchedDecisionEntries = 6; + for (int32 Index = GuidanceDecisionHistoryEntries.Num() - 1; + Index >= 0 && Weighting.MatchingDecisionCount < MaxMatchedDecisionEntries; + --Index) + { + const FHyperTwistCoachDashboardDecisionHistoryEntry& Entry = GuidanceDecisionHistoryEntries[Index]; + if (Entry.GuidanceLane != DisplayedGuidanceComparisonLane + || Entry.bSingleCaseScope != bDisplayedGuidanceComparisonSingleCase) + { + continue; + } + + if (bDisplayedGuidanceComparisonSingleCase + && !DisplayedGuidanceComparisonSelectedCaseId.IsEmpty() + && Entry.SelectedCaseId != DisplayedGuidanceComparisonSelectedCaseId) + { + continue; + } + + ++Weighting.MatchingDecisionCount; + Weighting.AverageSuccessRate += Entry.SuccessRate; + if (Weighting.LastAppliedAction == EHyperTwistCoachDashboardOutcomeAction::None) + { + Weighting.LastAppliedAction = Entry.AppliedAction; + } + + switch (Entry.AppliedAction) + { + case EHyperTwistCoachDashboardOutcomeAction::RepeatSelectedCase: + ++Weighting.RepeatSelectedCaseCount; + break; + case EHyperTwistCoachDashboardOutcomeAction::RepeatPreviewDeck: + ++Weighting.RepeatPreviewDeckCount; + break; + case EHyperTwistCoachDashboardOutcomeAction::BroadenPreviewDeck: + ++Weighting.BroadenPreviewDeckCount; + break; + case EHyperTwistCoachDashboardOutcomeAction::RedirectCoachGuidance: + ++Weighting.RedirectCoachGuidanceCount; + break; + default: + break; + } + } + + if (Weighting.MatchingDecisionCount <= 0) + { + return Weighting; + } + + Weighting.AverageSuccessRate /= static_cast(Weighting.MatchingDecisionCount); + + if (bDisplayedGuidanceComparisonSingleCase) + { + if (BaseAction == EHyperTwistCoachDashboardOutcomeAction::BroadenPreviewDeck + && Weighting.RepeatSelectedCaseCount >= 2 + && Weighting.AverageSuccessRate < 0.90f) + { + Weighting.WeightedAction = EHyperTwistCoachDashboardOutcomeAction::RepeatSelectedCase; + Weighting.bHasOverride = true; + } + else if (BaseAction == EHyperTwistCoachDashboardOutcomeAction::RepeatSelectedCase + && Weighting.BroadenPreviewDeckCount >= 2 + && Weighting.AverageSuccessRate >= 0.78f) + { + Weighting.WeightedAction = EHyperTwistCoachDashboardOutcomeAction::BroadenPreviewDeck; + Weighting.bHasOverride = true; + } + } + else + { + if (BaseAction == EHyperTwistCoachDashboardOutcomeAction::RedirectCoachGuidance + && Weighting.RepeatPreviewDeckCount >= 2 + && Weighting.AverageSuccessRate < 0.80f) + { + Weighting.WeightedAction = EHyperTwistCoachDashboardOutcomeAction::RepeatPreviewDeck; + Weighting.bHasOverride = true; + } + else if (BaseAction == EHyperTwistCoachDashboardOutcomeAction::RepeatPreviewDeck + && Weighting.RedirectCoachGuidanceCount >= 2 + && Weighting.AverageSuccessRate >= 0.72f) + { + Weighting.WeightedAction = EHyperTwistCoachDashboardOutcomeAction::RedirectCoachGuidance; + Weighting.bHasOverride = true; + } + } + + return Weighting; +} + FString UHyperTwistCoachDashboardWidget::BuildGuidanceOutcomeActionLabel( const EHyperTwistCoachDashboardOutcomeAction Action) const { @@ -3239,7 +3394,13 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() ); } } - const EHyperTwistCoachDashboardOutcomeAction GuidanceOutcomeAction = DeriveGuidanceOutcomeAction(); + const EHyperTwistCoachDashboardOutcomeAction BaseGuidanceOutcomeAction = DeriveGuidanceOutcomeAction(); + const FHyperTwistCoachDashboardDecisionWeighting GuidanceDecisionWeighting = + BuildGuidanceDecisionWeighting(BaseGuidanceOutcomeAction); + const EHyperTwistCoachDashboardOutcomeAction GuidanceOutcomeAction = + GuidanceDecisionWeighting.WeightedAction != EHyperTwistCoachDashboardOutcomeAction::None + ? GuidanceDecisionWeighting.WeightedAction + : BaseGuidanceOutcomeAction; const bool bCanRedirectGuidanceOutcome = CachedCoachPanelState.bCanStartCoachFollowUpRun || CachedCoachPanelState.bCanStartQueuedRun @@ -3253,6 +3414,30 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() && (GuidanceOutcomeAction != EHyperTwistCoachDashboardOutcomeAction::RedirectCoachGuidance || bCanRedirectGuidanceOutcome); const FString GuidanceOutcomeActionLabel = BuildGuidanceOutcomeActionLabel(GuidanceOutcomeAction); + const FString GuidanceDecisionWeightingStatusLine = + GuidanceDecisionWeighting.IsStructurallyValid() + ? FString::Printf( + TEXT("Weighting: %d matching decisions | Base: %s | Weighted: %s | Last applied: %s"), + GuidanceDecisionWeighting.MatchingDecisionCount, + *BuildGuidanceOutcomeActionLabel(GuidanceDecisionWeighting.BaseAction), + *BuildGuidanceOutcomeActionLabel(GuidanceDecisionWeighting.WeightedAction), + *BuildGuidanceOutcomeActionLabel(GuidanceDecisionWeighting.LastAppliedAction) + ) + : TEXT("Weighting: no matching authored decision history yet; the current outcome action is recap-derived only."); + const FString GuidanceDecisionWeightingDetailLine = + GuidanceDecisionWeighting.IsStructurallyValid() + ? FString::Printf( + TEXT("Decision weighting for this lane/scope: repeat case %d | repeat deck %d | broaden %d | redirect %d | Avg success %.2f | %s"), + GuidanceDecisionWeighting.RepeatSelectedCaseCount, + GuidanceDecisionWeighting.RepeatPreviewDeckCount, + GuidanceDecisionWeighting.BroadenPreviewDeckCount, + GuidanceDecisionWeighting.RedirectCoachGuidanceCount, + GuidanceDecisionWeighting.AverageSuccessRate, + GuidanceDecisionWeighting.bHasOverride + ? TEXT("Recent authored choices adjusted the current guidance-outcome action.") + : TEXT("Recent authored choices reinforced the recap-derived guidance-outcome action.") + ) + : TEXT("Weighting detail: apply outcome actions on the same guidance lane and scope to let the dashboard bias future recommendations."); const FHyperTwistCoachDashboardDecisionHistoryEntry* SelectedGuidanceDecisionEntry = SelectedGuidanceDecisionIndex >= 0 && SelectedGuidanceDecisionIndex < GuidanceDecisionHistoryEntries.Num() @@ -3440,6 +3625,18 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { GuidanceOutcomeActionButton->SetIsEnabled(bCanApplyGuidanceOutcomeAction); } + if (GuidanceDecisionWeightingHeaderTextBlock != nullptr) + { + GuidanceDecisionWeightingHeaderTextBlock->SetText(FText::FromString(TEXT("[Decision Weighting]"))); + } + if (GuidanceDecisionWeightingStatusTextBlock != nullptr) + { + GuidanceDecisionWeightingStatusTextBlock->SetText(FText::FromString(GuidanceDecisionWeightingStatusLine)); + } + if (GuidanceDecisionWeightingDetailTextBlock != nullptr) + { + GuidanceDecisionWeightingDetailTextBlock->SetText(FText::FromString(GuidanceDecisionWeightingDetailLine)); + } 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 6890908..e66c1a8 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -60,6 +60,30 @@ struct FHyperTwistCoachDashboardDecisionHistoryEntry } }; +struct FHyperTwistCoachDashboardDecisionWeighting +{ + int32 MatchingDecisionCount = 0; + int32 RepeatSelectedCaseCount = 0; + int32 RepeatPreviewDeckCount = 0; + int32 BroadenPreviewDeckCount = 0; + int32 RedirectCoachGuidanceCount = 0; + EHyperTwistCoachDashboardOutcomeAction BaseAction = + EHyperTwistCoachDashboardOutcomeAction::None; + EHyperTwistCoachDashboardOutcomeAction WeightedAction = + EHyperTwistCoachDashboardOutcomeAction::None; + EHyperTwistCoachDashboardOutcomeAction LastAppliedAction = + EHyperTwistCoachDashboardOutcomeAction::None; + float AverageSuccessRate = 0.0f; + bool bHasOverride = false; + + bool IsStructurallyValid() const + { + return MatchingDecisionCount > 0 + && BaseAction != EHyperTwistCoachDashboardOutcomeAction::None + && WeightedAction != EHyperTwistCoachDashboardOutcomeAction::None; + } +}; + UCLASS(BlueprintType, Blueprintable) class UNREALHYPERTWIST_API UHyperTwistCoachDashboardWidget : public UHyperTwistTrainingPanelWidget { @@ -261,6 +285,15 @@ protected: UPROPERTY(Transient) TObjectPtr GuidanceOutcomeActionButtonLabel = nullptr; + UPROPERTY(Transient) + TObjectPtr GuidanceDecisionWeightingHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr GuidanceDecisionWeightingStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr GuidanceDecisionWeightingDetailTextBlock = nullptr; + UPROPERTY(Transient) TObjectPtr GuidanceDecisionHistoryHeaderTextBlock = nullptr; @@ -726,6 +759,9 @@ private: bool& bOutDisplayedGuidanceComparisonSingleCase ) const; EHyperTwistCoachDashboardOutcomeAction DeriveGuidanceOutcomeAction() const; + FHyperTwistCoachDashboardDecisionWeighting BuildGuidanceDecisionWeighting( + EHyperTwistCoachDashboardOutcomeAction BaseAction + ) const; FString BuildGuidanceOutcomeActionLabel(EHyperTwistCoachDashboardOutcomeAction Action) const; FHyperTwistTrainingDeck BuildGuidanceOutcomeSelectedCaseDeck() const; void RecordGuidanceOutcomeDecision(