Apply guidance outcome from dashboard

This commit is contained in:
axiomlogicnexus 2026-04-27 15:56:22 +02:00
parent 5737214c31
commit d0ccdde06a
2 changed files with 373 additions and 1 deletions

View file

@ -407,6 +407,7 @@ FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::StartActiveGuidance
FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::StartSelectedGuidancePreviewCaseRun()
{
const FHyperTwistTrainingDeck Deck = BuildSelectedGuidancePreviewCaseDeck();
const FHyperTwistTrainingDeck* ActivePreviewDeck = GetActiveGuidancePreviewDeck();
if (!Deck.IsStructurallyValid())
{
UpdateDashboardPresentation();
@ -428,7 +429,7 @@ FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::StartSelectedGuidan
);
CaptureActiveGuidanceLaunchContext(
RunState.Session.IsStructurallyValid() ? RunState.Session.TrainingSessionId : SessionId,
Deck,
ActivePreviewDeck != nullptr && ActivePreviewDeck->IsStructurallyValid() ? *ActivePreviewDeck : Deck,
true,
Deck.Cases.Num() > 0 ? Deck.Cases[0].CaseId : FString()
);
@ -552,6 +553,133 @@ bool UHyperTwistCoachDashboardWidget::ContinueCurrentRunFlow()
return bSucceeded;
}
bool UHyperTwistCoachDashboardWidget::ApplyGuidanceOutcomeAction()
{
const EHyperTwistCoachDashboardOutcomeAction Action = DeriveGuidanceOutcomeAction();
const bool bCanRestartAboveCompletedRun =
!CachedRunState.Session.IsStructurallyValid()
|| CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Completed
|| CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Aborted;
if (bHasLiveAttemptTimer || Action == EHyperTwistCoachDashboardOutcomeAction::None || !bCanRestartAboveCompletedRun)
{
UpdateDashboardPresentation();
return false;
}
const FHyperTwistTrainingSessionSummary* DisplayedRunRecap = nullptr;
const FHyperTwistCoachBrief* DisplayedGuidanceComparisonBrief = nullptr;
const FHyperTwistTrainingDeck* DisplayedGuidanceComparisonDeck = nullptr;
FString DisplayedGuidanceComparisonSelectedCaseId;
bool bDisplayedGuidanceComparisonSingleCase = false;
if (!ResolveDisplayedGuidanceComparisonContext(
DisplayedRunRecap,
DisplayedGuidanceComparisonBrief,
DisplayedGuidanceComparisonDeck,
DisplayedGuidanceComparisonSelectedCaseId,
bDisplayedGuidanceComparisonSingleCase))
{
UpdateDashboardPresentation();
return false;
}
const FString ActionSessionIdBase = FString::Printf(
TEXT("%s_outcome_%s"),
*DefaultSessionId,
*FGuid::NewGuid().ToString(EGuidFormats::Digits)
);
switch (Action)
{
case EHyperTwistCoachDashboardOutcomeAction::RepeatSelectedCase:
{
const FHyperTwistTrainingDeck SelectedCaseDeck = BuildGuidanceOutcomeSelectedCaseDeck();
if (!SelectedCaseDeck.IsStructurallyValid() || DisplayedGuidanceComparisonDeck == nullptr)
{
UpdateDashboardPresentation();
return false;
}
const FHyperTwistTrainingRunState RunState = StartTrainingRunFromDeck(
SelectedCaseDeck,
DefaultUserId,
ActionSessionIdBase + TEXT("_selected_case"),
DefaultMode
);
CaptureActiveGuidanceLaunchContext(
RunState.Session.IsStructurallyValid() ? RunState.Session.TrainingSessionId : ActionSessionIdBase,
*DisplayedGuidanceComparisonDeck,
true,
DisplayedGuidanceComparisonSelectedCaseId
);
SyncRetainedRunRecap();
SyncSelectedQueueEntry();
SyncSelectedAttempt();
UpdateDashboardPresentation();
return RunState.Session.IsStructurallyValid() && RunState.ActiveDeck.IsStructurallyValid();
}
case EHyperTwistCoachDashboardOutcomeAction::RepeatPreviewDeck:
case EHyperTwistCoachDashboardOutcomeAction::BroadenPreviewDeck:
{
if (DisplayedGuidanceComparisonDeck == nullptr || !DisplayedGuidanceComparisonDeck->IsStructurallyValid())
{
UpdateDashboardPresentation();
return false;
}
const FString SessionSuffix = Action == EHyperTwistCoachDashboardOutcomeAction::BroadenPreviewDeck
? TEXT("_broaden_preview")
: TEXT("_repeat_preview");
const FHyperTwistTrainingRunState RunState = StartTrainingRunFromDeck(
*DisplayedGuidanceComparisonDeck,
DefaultUserId,
ActionSessionIdBase + SessionSuffix,
DefaultMode
);
CaptureActiveGuidanceLaunchContext(
RunState.Session.IsStructurallyValid() ? RunState.Session.TrainingSessionId : ActionSessionIdBase,
*DisplayedGuidanceComparisonDeck,
false,
DisplayedGuidanceComparisonSelectedCaseId
);
SyncRetainedRunRecap();
SyncSelectedQueueEntry();
SyncSelectedAttempt();
UpdateDashboardPresentation();
return RunState.Session.IsStructurallyValid() && RunState.ActiveDeck.IsStructurallyValid();
}
case EHyperTwistCoachDashboardOutcomeAction::RedirectCoachGuidance:
{
FHyperTwistTrainingRunState RunState;
if (CachedCoachPanelState.bCanStartCoachFollowUpRun && CachedCoachPanelState.bHasFollowUpGuidance)
{
RunState = StartCoachFollowUpRun(DefaultCoachMaxCases);
}
else if (CachedCoachPanelState.bCanStartQueuedRun)
{
RunState = StartNextQueuedCoachRun(DefaultCoachMaxCases);
}
else if (CachedCoachPanelState.bCanStartCoachRecommendedRun)
{
RunState = StartCoachRecommendedRun(DefaultCoachMaxCases);
}
else
{
UpdateDashboardPresentation();
return false;
}
SyncRetainedRunRecap();
SyncSelectedQueueEntry();
SyncSelectedAttempt();
UpdateDashboardPresentation();
return RunState.Session.IsStructurallyValid() && RunState.ActiveDeck.IsStructurallyValid();
}
default:
UpdateDashboardPresentation();
return false;
}
}
bool UHyperTwistCoachDashboardWidget::StartLiveAttemptTimer()
{
if (!CachedRunState.Session.IsStructurallyValid() || !CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid())
@ -817,6 +945,11 @@ void UHyperTwistCoachDashboardWidget::HandleContinueRunClicked()
ContinueCurrentRunFlow();
}
void UHyperTwistCoachDashboardWidget::HandleGuidanceOutcomeActionClicked()
{
ApplyGuidanceOutcomeAction();
}
void UHyperTwistCoachDashboardWidget::HandleClearRunClicked()
{
ClearActiveTrainingRun();
@ -1152,6 +1285,17 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
);
RepeatRunButtonLabel = RepeatRunLabelRaw;
UTextBlock* GuidanceOutcomeActionLabelRaw = nullptr;
GuidanceOutcomeActionButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
RunRecapActionRow,
TEXT("GuidanceOutcomeActionButton"),
TEXT("GuidanceOutcomeActionButtonLabel"),
TEXT("Apply Outcome"),
GuidanceOutcomeActionLabelRaw
);
GuidanceOutcomeActionButtonLabel = GuidanceOutcomeActionLabelRaw;
UTextBlock* DismissRecapLabelRaw = nullptr;
DismissRecapButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
@ -1797,6 +1941,10 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
{
RepeatRunButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleRepeatRunClicked);
}
if (GuidanceOutcomeActionButton != nullptr)
{
GuidanceOutcomeActionButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleGuidanceOutcomeActionClicked);
}
if (DismissRecapButton != nullptr)
{
DismissRecapButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleDismissRecapClicked);
@ -2108,6 +2256,177 @@ FHyperTwistTrainingDeck UHyperTwistCoachDashboardWidget::BuildSelectedGuidancePr
return SingleCaseDeck;
}
bool UHyperTwistCoachDashboardWidget::ResolveDisplayedGuidanceComparisonContext(
const FHyperTwistTrainingSessionSummary*& OutDisplayedRunRecap,
const FHyperTwistCoachBrief*& OutDisplayedGuidanceComparisonBrief,
const FHyperTwistTrainingDeck*& OutDisplayedGuidanceComparisonDeck,
FString& OutDisplayedGuidanceComparisonSelectedCaseId,
bool& bOutDisplayedGuidanceComparisonSingleCase
) const
{
OutDisplayedRunRecap = nullptr;
OutDisplayedGuidanceComparisonBrief = nullptr;
OutDisplayedGuidanceComparisonDeck = nullptr;
OutDisplayedGuidanceComparisonSelectedCaseId.Reset();
bOutDisplayedGuidanceComparisonSingleCase = false;
const bool bHasCurrentRunRecap = CachedRunState.Session.IsStructurallyValid()
&& (CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Completed
|| CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Aborted);
OutDisplayedRunRecap = bHasCurrentRunRecap
? &CachedRunSummary
: (bHasRetainedRunRecap ? &RetainedRunRecapSummary : nullptr);
if (OutDisplayedRunRecap == nullptr || OutDisplayedRunRecap->TrainingSessionId.IsEmpty())
{
return false;
}
const bool bHasActiveGuidanceComparison =
bHasCurrentRunRecap
&& !ActiveGuidanceLaunchSessionId.IsEmpty()
&& OutDisplayedRunRecap->TrainingSessionId == ActiveGuidanceLaunchSessionId
&& ActiveGuidanceLaunchLane != EHyperTwistCoachDashboardGuidancePreviewLane::None;
const bool bHasDisplayedRetainedGuidanceComparison =
!bHasCurrentRunRecap
&& bHasRetainedGuidanceComparison
&& !RetainedGuidanceComparisonSessionId.IsEmpty()
&& OutDisplayedRunRecap->TrainingSessionId == RetainedGuidanceComparisonSessionId
&& RetainedGuidanceComparisonLane != EHyperTwistCoachDashboardGuidancePreviewLane::None;
if (bHasActiveGuidanceComparison)
{
OutDisplayedGuidanceComparisonBrief = &ActiveGuidanceLaunchBrief;
OutDisplayedGuidanceComparisonDeck = &ActiveGuidanceLaunchDeck;
OutDisplayedGuidanceComparisonSelectedCaseId = ActiveGuidanceLaunchSelectedCaseId;
bOutDisplayedGuidanceComparisonSingleCase = bActiveGuidanceLaunchSingleCase;
}
else if (bHasDisplayedRetainedGuidanceComparison)
{
OutDisplayedGuidanceComparisonBrief = &RetainedGuidanceComparisonBrief;
OutDisplayedGuidanceComparisonDeck = &RetainedGuidanceComparisonDeck;
OutDisplayedGuidanceComparisonSelectedCaseId = RetainedGuidanceComparisonSelectedCaseId;
bOutDisplayedGuidanceComparisonSingleCase = bRetainedGuidanceComparisonSingleCase;
}
return OutDisplayedGuidanceComparisonBrief != nullptr
&& OutDisplayedGuidanceComparisonDeck != nullptr
&& OutDisplayedGuidanceComparisonDeck->IsStructurallyValid();
}
EHyperTwistCoachDashboardOutcomeAction UHyperTwistCoachDashboardWidget::DeriveGuidanceOutcomeAction() const
{
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
|| DisplayedRunRecap->CompletedAttemptCount <= 0)
{
return EHyperTwistCoachDashboardOutcomeAction::None;
}
const int32 NonSuccessCount =
DisplayedRunRecap->FailureCount + DisplayedRunRecap->TimeoutCount + DisplayedRunRecap->DNFCount;
if (bDisplayedGuidanceComparisonSingleCase)
{
if (DisplayedRunRecap->SuccessRate >= 0.85f
&& DisplayedRunRecap->TimeoutCount == 0
&& DisplayedRunRecap->DNFCount == 0
&& DisplayedRunRecap->TotalMistakes <= 1)
{
return EHyperTwistCoachDashboardOutcomeAction::BroadenPreviewDeck;
}
return DisplayedGuidanceComparisonSelectedCaseId.IsEmpty()
? EHyperTwistCoachDashboardOutcomeAction::None
: EHyperTwistCoachDashboardOutcomeAction::RepeatSelectedCase;
}
if (DisplayedRunRecap->SuccessRate >= 0.8f
&& NonSuccessCount <= FMath::Max(1, DisplayedRunRecap->CompletedAttemptCount / 4)
&& DisplayedRunRecap->TotalMistakes <= FMath::Max(1, DisplayedRunRecap->CompletedAttemptCount / 2))
{
return EHyperTwistCoachDashboardOutcomeAction::RedirectCoachGuidance;
}
if (DisplayedRunRecap->SuccessRate >= 0.55f)
{
return EHyperTwistCoachDashboardOutcomeAction::RepeatPreviewDeck;
}
return !DisplayedGuidanceComparisonSelectedCaseId.IsEmpty()
? EHyperTwistCoachDashboardOutcomeAction::RepeatSelectedCase
: EHyperTwistCoachDashboardOutcomeAction::RedirectCoachGuidance;
}
FString UHyperTwistCoachDashboardWidget::BuildGuidanceOutcomeActionLabel(
const EHyperTwistCoachDashboardOutcomeAction Action) const
{
switch (Action)
{
case EHyperTwistCoachDashboardOutcomeAction::RepeatSelectedCase:
return TEXT("Apply: Repeat Selected Case");
case EHyperTwistCoachDashboardOutcomeAction::RepeatPreviewDeck:
return TEXT("Apply: Repeat Preview Deck");
case EHyperTwistCoachDashboardOutcomeAction::BroadenPreviewDeck:
return TEXT("Apply: Broaden to Preview Deck");
case EHyperTwistCoachDashboardOutcomeAction::RedirectCoachGuidance:
return TEXT("Apply: Next Coach Guidance");
default:
return TEXT("Apply Outcome");
}
}
FHyperTwistTrainingDeck UHyperTwistCoachDashboardWidget::BuildGuidanceOutcomeSelectedCaseDeck() const
{
const FHyperTwistTrainingSessionSummary* DisplayedRunRecap = nullptr;
const FHyperTwistCoachBrief* DisplayedGuidanceComparisonBrief = nullptr;
const FHyperTwistTrainingDeck* DisplayedGuidanceComparisonDeck = nullptr;
FString DisplayedGuidanceComparisonSelectedCaseId;
bool bDisplayedGuidanceComparisonSingleCase = false;
if (!ResolveDisplayedGuidanceComparisonContext(
DisplayedRunRecap,
DisplayedGuidanceComparisonBrief,
DisplayedGuidanceComparisonDeck,
DisplayedGuidanceComparisonSelectedCaseId,
bDisplayedGuidanceComparisonSingleCase)
|| DisplayedGuidanceComparisonDeck == nullptr
|| DisplayedGuidanceComparisonSelectedCaseId.IsEmpty())
{
return FHyperTwistTrainingDeck();
}
const FHyperTwistTrainingCase* SelectedCase = DisplayedGuidanceComparisonDeck->Cases.FindByPredicate(
[&DisplayedGuidanceComparisonSelectedCaseId](const FHyperTwistTrainingCase& Candidate)
{
return Candidate.CaseId == DisplayedGuidanceComparisonSelectedCaseId;
});
if (SelectedCase == nullptr)
{
return FHyperTwistTrainingDeck();
}
FHyperTwistTrainingDeck SingleCaseDeck = *DisplayedGuidanceComparisonDeck;
SingleCaseDeck.Cases = { *SelectedCase };
SingleCaseDeck.DeckId = FString::Printf(
TEXT("%s_case_%s"),
DisplayedGuidanceComparisonDeck->DeckId.IsEmpty() ? TEXT("guidance") : *DisplayedGuidanceComparisonDeck->DeckId,
SelectedCase->CaseId.IsEmpty() ? TEXT("selected") : *SelectedCase->CaseId
);
SingleCaseDeck.Title = FString::Printf(
TEXT("%s (Selected Case)"),
DisplayedGuidanceComparisonDeck->Title.IsEmpty() ? TEXT("Guidance Comparison") : *DisplayedGuidanceComparisonDeck->Title
);
return SingleCaseDeck;
}
void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
const bool bHasActiveRun = CachedRunState.Session.IsStructurallyValid()
@ -2722,6 +3041,20 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
);
}
}
const EHyperTwistCoachDashboardOutcomeAction GuidanceOutcomeAction = DeriveGuidanceOutcomeAction();
const bool bCanRedirectGuidanceOutcome =
CachedCoachPanelState.bCanStartCoachFollowUpRun
|| CachedCoachPanelState.bCanStartQueuedRun
|| CachedCoachPanelState.bCanStartCoachRecommendedRun;
const bool bCanApplyGuidanceOutcomeAction =
GuidanceOutcomeAction != EHyperTwistCoachDashboardOutcomeAction::None
&& !bHasLiveAttemptTimer
&& (!bHasActiveRun
|| CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Completed
|| CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Aborted)
&& (GuidanceOutcomeAction != EHyperTwistCoachDashboardOutcomeAction::RedirectCoachGuidance
|| bCanRedirectGuidanceOutcome);
const FString GuidanceOutcomeActionLabel = BuildGuidanceOutcomeActionLabel(GuidanceOutcomeAction);
const bool bCanStartPreviewDeck =
!bHasLiveAttemptTimer && !bHasActiveRun && ActivePreviewDeck != nullptr && ActivePreviewDeck->IsStructurallyValid();
const bool bCanStartPreviewCase = bCanStartPreviewDeck && SelectedGuidancePreviewCase != nullptr;
@ -2862,6 +3195,14 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
GuidanceOutcomeDetailTextBlock->SetText(FText::FromString(GuidanceOutcomeDetailLine));
}
if (GuidanceOutcomeActionButtonLabel != nullptr)
{
GuidanceOutcomeActionButtonLabel->SetText(FText::FromString(GuidanceOutcomeActionLabel));
}
if (GuidanceOutcomeActionButton != nullptr)
{
GuidanceOutcomeActionButton->SetIsEnabled(bCanApplyGuidanceOutcomeAction);
}
if (RepeatRunButtonLabel != nullptr)
{
RepeatRunButtonLabel->SetText(FText::FromString(TEXT("Repeat Run")));

View file

@ -23,6 +23,15 @@ enum class EHyperTwistCoachDashboardGuidancePreviewLane : uint8
FollowUp
};
enum class EHyperTwistCoachDashboardOutcomeAction : uint8
{
None,
RepeatSelectedCase,
RepeatPreviewDeck,
BroadenPreviewDeck,
RedirectCoachGuidance
};
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API UHyperTwistCoachDashboardWidget : public UHyperTwistTrainingPanelWidget
{
@ -137,6 +146,9 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool ContinueCurrentRunFlow();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool ApplyGuidanceOutcomeAction();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool StartLiveAttemptTimer();
@ -206,6 +218,12 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> GuidanceOutcomeDetailTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> GuidanceOutcomeActionButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> GuidanceOutcomeActionButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> CoachHandoffHeaderTextBlock = nullptr;
@ -566,6 +584,9 @@ protected:
UFUNCTION()
void HandleContinueRunClicked();
UFUNCTION()
void HandleGuidanceOutcomeActionClicked();
UFUNCTION()
void HandleClearRunClicked();
@ -632,6 +653,16 @@ private:
const FHyperTwistTrainingDeck* GetActiveGuidancePreviewDeck() const;
const FHyperTwistCoachBrief* GetActiveGuidancePreviewBrief() const;
FHyperTwistTrainingDeck BuildSelectedGuidancePreviewCaseDeck() const;
bool ResolveDisplayedGuidanceComparisonContext(
const FHyperTwistTrainingSessionSummary*& OutDisplayedRunRecap,
const FHyperTwistCoachBrief*& OutDisplayedGuidanceComparisonBrief,
const FHyperTwistTrainingDeck*& OutDisplayedGuidanceComparisonDeck,
FString& OutDisplayedGuidanceComparisonSelectedCaseId,
bool& bOutDisplayedGuidanceComparisonSingleCase
) const;
EHyperTwistCoachDashboardOutcomeAction DeriveGuidanceOutcomeAction() const;
FString BuildGuidanceOutcomeActionLabel(EHyperTwistCoachDashboardOutcomeAction Action) const;
FHyperTwistTrainingDeck BuildGuidanceOutcomeSelectedCaseDeck() const;
void CaptureActiveGuidanceLaunchContext(
const FString& SessionId,
const FHyperTwistTrainingDeck& Deck,