Prefer dashboard guidance lane by decision history

This commit is contained in:
axiomlogicnexus 2026-04-27 16:37:21 +02:00
parent 059d34d98a
commit 41a828c909
2 changed files with 233 additions and 0 deletions

View file

@ -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<float>(Preference.RecommendedDecisionCount);
}
if (Preference.FollowUpDecisionCount > 0)
{
Preference.FollowUpAverageSuccessRate =
FollowUpSuccessRateSum / static_cast<float>(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<float>(Preference.RecommendedContinueBias);
RecommendedLaneScore += Preference.RecommendedAverageSuccessRate;
}
if (Preference.FollowUpDecisionCount > 0)
{
FollowUpLaneScore += static_cast<float>(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]")));

View file

@ -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<UTextBlock> GuidanceDecisionWeightingDetailTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> GuidancePreferenceHeaderTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> GuidancePreferenceStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> GuidancePreferenceDetailTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> 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(