Add dashboard guidance decision history

This commit is contained in:
axiomlogicnexus 2026-04-27 16:14:12 +02:00
parent d0ccdde06a
commit 214aeb42b7
2 changed files with 340 additions and 0 deletions

View file

@ -143,6 +143,7 @@ void UHyperTwistCoachDashboardWidget::RefreshCoachDashboardView()
SyncRetainedRunRecap();
SyncSelectedQueueEntry();
SyncSelectedAttempt();
SyncSelectedGuidanceDecisionHistoryEntry();
UpdateDashboardPresentation();
}
@ -611,6 +612,18 @@ bool UHyperTwistCoachDashboardWidget::ApplyGuidanceOutcomeAction()
true,
DisplayedGuidanceComparisonSelectedCaseId
);
if (RunState.Session.IsStructurallyValid() && RunState.ActiveDeck.IsStructurallyValid())
{
RecordGuidanceOutcomeDecision(
Action,
EHyperTwistCoachDashboardOutcomeAction::RepeatSelectedCase,
*DisplayedRunRecap,
*DisplayedGuidanceComparisonBrief,
*DisplayedGuidanceComparisonDeck,
DisplayedGuidanceComparisonSelectedCaseId,
bDisplayedGuidanceComparisonSingleCase
);
}
SyncRetainedRunRecap();
SyncSelectedQueueEntry();
SyncSelectedAttempt();
@ -641,6 +654,18 @@ bool UHyperTwistCoachDashboardWidget::ApplyGuidanceOutcomeAction()
false,
DisplayedGuidanceComparisonSelectedCaseId
);
if (RunState.Session.IsStructurallyValid() && RunState.ActiveDeck.IsStructurallyValid())
{
RecordGuidanceOutcomeDecision(
Action,
Action,
*DisplayedRunRecap,
*DisplayedGuidanceComparisonBrief,
*DisplayedGuidanceComparisonDeck,
DisplayedGuidanceComparisonSelectedCaseId,
bDisplayedGuidanceComparisonSingleCase
);
}
SyncRetainedRunRecap();
SyncSelectedQueueEntry();
SyncSelectedAttempt();
@ -668,6 +693,18 @@ bool UHyperTwistCoachDashboardWidget::ApplyGuidanceOutcomeAction()
return false;
}
if (RunState.Session.IsStructurallyValid() && RunState.ActiveDeck.IsStructurallyValid())
{
RecordGuidanceOutcomeDecision(
Action,
EHyperTwistCoachDashboardOutcomeAction::RedirectCoachGuidance,
*DisplayedRunRecap,
*DisplayedGuidanceComparisonBrief,
*DisplayedGuidanceComparisonDeck,
DisplayedGuidanceComparisonSelectedCaseId,
bDisplayedGuidanceComparisonSingleCase
);
}
SyncRetainedRunRecap();
SyncSelectedQueueEntry();
SyncSelectedAttempt();
@ -680,6 +717,46 @@ bool UHyperTwistCoachDashboardWidget::ApplyGuidanceOutcomeAction()
}
}
bool UHyperTwistCoachDashboardWidget::SelectPreviousGuidanceDecisionHistoryEntry()
{
if (GuidanceDecisionHistoryEntries.Num() <= 0)
{
SelectedGuidanceDecisionIndex = INDEX_NONE;
return false;
}
if (SelectedGuidanceDecisionIndex == INDEX_NONE)
{
SyncSelectedGuidanceDecisionHistoryEntry();
return SelectedGuidanceDecisionIndex != INDEX_NONE;
}
SelectedGuidanceDecisionIndex =
(SelectedGuidanceDecisionIndex - 1 + GuidanceDecisionHistoryEntries.Num()) % GuidanceDecisionHistoryEntries.Num();
UpdateDashboardPresentation();
return true;
}
bool UHyperTwistCoachDashboardWidget::SelectNextGuidanceDecisionHistoryEntry()
{
if (GuidanceDecisionHistoryEntries.Num() <= 0)
{
SelectedGuidanceDecisionIndex = INDEX_NONE;
return false;
}
if (SelectedGuidanceDecisionIndex == INDEX_NONE)
{
SyncSelectedGuidanceDecisionHistoryEntry();
return SelectedGuidanceDecisionIndex != INDEX_NONE;
}
SelectedGuidanceDecisionIndex =
(SelectedGuidanceDecisionIndex + 1) % GuidanceDecisionHistoryEntries.Num();
UpdateDashboardPresentation();
return true;
}
bool UHyperTwistCoachDashboardWidget::StartLiveAttemptTimer()
{
if (!CachedRunState.Session.IsStructurallyValid() || !CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid())
@ -950,6 +1027,16 @@ void UHyperTwistCoachDashboardWidget::HandleGuidanceOutcomeActionClicked()
ApplyGuidanceOutcomeAction();
}
void UHyperTwistCoachDashboardWidget::HandlePreviousGuidanceDecisionClicked()
{
SelectPreviousGuidanceDecisionHistoryEntry();
}
void UHyperTwistCoachDashboardWidget::HandleNextGuidanceDecisionClicked()
{
SelectNextGuidanceDecisionHistoryEntry();
}
void UHyperTwistCoachDashboardWidget::HandleClearRunClicked()
{
ClearActiveTrainingRun();
@ -1203,6 +1290,24 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
TEXT("CoachGuidanceOutcomeDetail"),
8
);
GuidanceDecisionHistoryHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachGuidanceDecisionHistoryHeader"),
2
);
GuidanceDecisionHistoryStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachGuidanceDecisionHistoryStatus"),
2
);
GuidanceDecisionHistoryDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachGuidanceDecisionHistoryDetail"),
8
);
CoachHandoffHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
@ -1296,6 +1401,28 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
);
GuidanceOutcomeActionButtonLabel = GuidanceOutcomeActionLabelRaw;
UTextBlock* PreviousGuidanceDecisionLabelRaw = nullptr;
PreviousGuidanceDecisionButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
RunRecapActionRow,
TEXT("PreviousGuidanceDecisionButton"),
TEXT("PreviousGuidanceDecisionButtonLabel"),
TEXT("Prev Decision"),
PreviousGuidanceDecisionLabelRaw
);
PreviousGuidanceDecisionButtonLabel = PreviousGuidanceDecisionLabelRaw;
UTextBlock* NextGuidanceDecisionLabelRaw = nullptr;
NextGuidanceDecisionButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
RunRecapActionRow,
TEXT("NextGuidanceDecisionButton"),
TEXT("NextGuidanceDecisionButtonLabel"),
TEXT("Next Decision"),
NextGuidanceDecisionLabelRaw
);
NextGuidanceDecisionButtonLabel = NextGuidanceDecisionLabelRaw;
UTextBlock* DismissRecapLabelRaw = nullptr;
DismissRecapButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
@ -1945,6 +2072,14 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
{
GuidanceOutcomeActionButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleGuidanceOutcomeActionClicked);
}
if (PreviousGuidanceDecisionButton != nullptr)
{
PreviousGuidanceDecisionButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePreviousGuidanceDecisionClicked);
}
if (NextGuidanceDecisionButton != nullptr)
{
NextGuidanceDecisionButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleNextGuidanceDecisionClicked);
}
if (DismissRecapButton != nullptr)
{
DismissRecapButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleDismissRecapClicked);
@ -2181,6 +2316,21 @@ void UHyperTwistCoachDashboardWidget::SyncSelectedGuidancePreviewCase()
}
}
void UHyperTwistCoachDashboardWidget::SyncSelectedGuidanceDecisionHistoryEntry()
{
if (GuidanceDecisionHistoryEntries.Num() <= 0)
{
SelectedGuidanceDecisionIndex = INDEX_NONE;
return;
}
if (SelectedGuidanceDecisionIndex < 0
|| SelectedGuidanceDecisionIndex >= GuidanceDecisionHistoryEntries.Num())
{
SelectedGuidanceDecisionIndex = GuidanceDecisionHistoryEntries.Num() - 1;
}
}
const FHyperTwistTrainingDeck* UHyperTwistCoachDashboardWidget::GetActiveGuidancePreviewDeck() const
{
switch (ActiveGuidancePreviewLane)
@ -2427,6 +2577,54 @@ FHyperTwistTrainingDeck UHyperTwistCoachDashboardWidget::BuildGuidanceOutcomeSel
return SingleCaseDeck;
}
void UHyperTwistCoachDashboardWidget::RecordGuidanceOutcomeDecision(
const EHyperTwistCoachDashboardOutcomeAction RecommendedAction,
const EHyperTwistCoachDashboardOutcomeAction AppliedAction,
const FHyperTwistTrainingSessionSummary& DisplayedRunRecap,
const FHyperTwistCoachBrief& DisplayedGuidanceComparisonBrief,
const FHyperTwistTrainingDeck& DisplayedGuidanceComparisonDeck,
const FString& DisplayedGuidanceComparisonSelectedCaseId,
const bool bDisplayedGuidanceComparisonSingleCase
)
{
FHyperTwistCoachDashboardDecisionHistoryEntry Entry;
Entry.RecordedAtUtc = FDateTime::UtcNow().ToIso8601();
Entry.SourceSessionId = DisplayedRunRecap.TrainingSessionId;
Entry.GuidanceLane = !ActiveGuidanceLaunchSessionId.IsEmpty()
&& DisplayedRunRecap.TrainingSessionId == ActiveGuidanceLaunchSessionId
? ActiveGuidanceLaunchLane
: (!RetainedGuidanceComparisonSessionId.IsEmpty()
&& DisplayedRunRecap.TrainingSessionId == RetainedGuidanceComparisonSessionId
? RetainedGuidanceComparisonLane
: EHyperTwistCoachDashboardGuidancePreviewLane::None);
Entry.bSingleCaseScope = bDisplayedGuidanceComparisonSingleCase;
Entry.SelectedCaseId = DisplayedGuidanceComparisonSelectedCaseId;
Entry.BriefHeadline = DisplayedGuidanceComparisonBrief.Headline.IsEmpty()
? (DisplayedGuidanceComparisonDeck.Title.IsEmpty()
? DisplayedGuidanceComparisonDeck.DeckId
: DisplayedGuidanceComparisonDeck.Title)
: DisplayedGuidanceComparisonBrief.Headline;
Entry.RecommendedAction = RecommendedAction;
Entry.RecommendedActionLabel = BuildGuidanceOutcomeActionLabel(RecommendedAction);
Entry.AppliedAction = AppliedAction;
Entry.AppliedActionLabel = BuildGuidanceOutcomeActionLabel(AppliedAction);
Entry.SuccessRate = DisplayedRunRecap.SuccessRate;
Entry.CompletedAttemptCount = DisplayedRunRecap.CompletedAttemptCount;
Entry.TotalMistakes = DisplayedRunRecap.TotalMistakes;
if (Entry.IsStructurallyValid())
{
GuidanceDecisionHistoryEntries.Add(Entry);
constexpr int32 MaxRetainedDecisionHistoryEntries = 16;
if (GuidanceDecisionHistoryEntries.Num() > MaxRetainedDecisionHistoryEntries)
{
const int32 ExcessCount = GuidanceDecisionHistoryEntries.Num() - MaxRetainedDecisionHistoryEntries;
GuidanceDecisionHistoryEntries.RemoveAt(0, ExcessCount, EAllowShrinking::No);
}
SelectedGuidanceDecisionIndex = GuidanceDecisionHistoryEntries.Num() - 1;
}
}
void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
const bool bHasActiveRun = CachedRunState.Session.IsStructurallyValid()
@ -3055,6 +3253,45 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
&& (GuidanceOutcomeAction != EHyperTwistCoachDashboardOutcomeAction::RedirectCoachGuidance
|| bCanRedirectGuidanceOutcome);
const FString GuidanceOutcomeActionLabel = BuildGuidanceOutcomeActionLabel(GuidanceOutcomeAction);
const FHyperTwistCoachDashboardDecisionHistoryEntry* SelectedGuidanceDecisionEntry =
SelectedGuidanceDecisionIndex >= 0
&& SelectedGuidanceDecisionIndex < GuidanceDecisionHistoryEntries.Num()
? &GuidanceDecisionHistoryEntries[SelectedGuidanceDecisionIndex]
: nullptr;
const FString GuidanceDecisionLaneLabel =
SelectedGuidanceDecisionEntry != nullptr
? (SelectedGuidanceDecisionEntry->GuidanceLane == EHyperTwistCoachDashboardGuidancePreviewLane::Recommended
? TEXT("recommended")
: (SelectedGuidanceDecisionEntry->GuidanceLane == EHyperTwistCoachDashboardGuidancePreviewLane::FollowUp
? TEXT("follow-up")
: TEXT("none")))
: TEXT("none");
const FString GuidanceDecisionHistoryStatusLine =
SelectedGuidanceDecisionEntry != nullptr
? FString::Printf(
TEXT("Decision history: %d entries | selected %d/%d | Lane: %s | Scope: %s | Recorded: %s"),
GuidanceDecisionHistoryEntries.Num(),
SelectedGuidanceDecisionIndex + 1,
GuidanceDecisionHistoryEntries.Num(),
*GuidanceDecisionLaneLabel,
SelectedGuidanceDecisionEntry->bSingleCaseScope ? TEXT("selected case") : TEXT("preview deck"),
*SelectedGuidanceDecisionEntry->RecordedAtUtc
)
: TEXT("Decision history: no applied guidance-outcome decisions recorded yet.");
const FString GuidanceDecisionHistoryDetailLine =
SelectedGuidanceDecisionEntry != nullptr
? FString::Printf(
TEXT("Session: %s | Brief: %s | Recommended: %s | Applied: %s | Selected case: %s | Success rate: %.2f | Completed attempts: %d | Mistakes: %d"),
*SelectedGuidanceDecisionEntry->SourceSessionId,
SelectedGuidanceDecisionEntry->BriefHeadline.IsEmpty() ? TEXT("n/a") : *SelectedGuidanceDecisionEntry->BriefHeadline,
*SelectedGuidanceDecisionEntry->RecommendedActionLabel,
*SelectedGuidanceDecisionEntry->AppliedActionLabel,
SelectedGuidanceDecisionEntry->SelectedCaseId.IsEmpty() ? TEXT("n/a") : *SelectedGuidanceDecisionEntry->SelectedCaseId,
SelectedGuidanceDecisionEntry->SuccessRate,
SelectedGuidanceDecisionEntry->CompletedAttemptCount,
SelectedGuidanceDecisionEntry->TotalMistakes
)
: TEXT("Decision detail: apply a guidance outcome from the dashboard to retain the authored choice history.");
const bool bCanStartPreviewDeck =
!bHasLiveAttemptTimer && !bHasActiveRun && ActivePreviewDeck != nullptr && ActivePreviewDeck->IsStructurallyValid();
const bool bCanStartPreviewCase = bCanStartPreviewDeck && SelectedGuidancePreviewCase != nullptr;
@ -3203,6 +3440,34 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
GuidanceOutcomeActionButton->SetIsEnabled(bCanApplyGuidanceOutcomeAction);
}
if (GuidanceDecisionHistoryHeaderTextBlock != nullptr)
{
GuidanceDecisionHistoryHeaderTextBlock->SetText(FText::FromString(TEXT("[Decision History]")));
}
if (GuidanceDecisionHistoryStatusTextBlock != nullptr)
{
GuidanceDecisionHistoryStatusTextBlock->SetText(FText::FromString(GuidanceDecisionHistoryStatusLine));
}
if (GuidanceDecisionHistoryDetailTextBlock != nullptr)
{
GuidanceDecisionHistoryDetailTextBlock->SetText(FText::FromString(GuidanceDecisionHistoryDetailLine));
}
if (PreviousGuidanceDecisionButtonLabel != nullptr)
{
PreviousGuidanceDecisionButtonLabel->SetText(FText::FromString(TEXT("Prev Decision")));
}
if (PreviousGuidanceDecisionButton != nullptr)
{
PreviousGuidanceDecisionButton->SetIsEnabled(GuidanceDecisionHistoryEntries.Num() > 1);
}
if (NextGuidanceDecisionButtonLabel != nullptr)
{
NextGuidanceDecisionButtonLabel->SetText(FText::FromString(TEXT("Next Decision")));
}
if (NextGuidanceDecisionButton != nullptr)
{
NextGuidanceDecisionButton->SetIsEnabled(GuidanceDecisionHistoryEntries.Num() > 1);
}
if (RepeatRunButtonLabel != nullptr)
{
RepeatRunButtonLabel->SetText(FText::FromString(TEXT("Repeat Run")));

View file

@ -32,6 +32,34 @@ enum class EHyperTwistCoachDashboardOutcomeAction : uint8
RedirectCoachGuidance
};
struct FHyperTwistCoachDashboardDecisionHistoryEntry
{
FString RecordedAtUtc;
FString SourceSessionId;
EHyperTwistCoachDashboardGuidancePreviewLane GuidanceLane =
EHyperTwistCoachDashboardGuidancePreviewLane::None;
bool bSingleCaseScope = false;
FString SelectedCaseId;
FString BriefHeadline;
FString RecommendedActionLabel;
EHyperTwistCoachDashboardOutcomeAction RecommendedAction =
EHyperTwistCoachDashboardOutcomeAction::None;
FString AppliedActionLabel;
EHyperTwistCoachDashboardOutcomeAction AppliedAction =
EHyperTwistCoachDashboardOutcomeAction::None;
float SuccessRate = 0.0f;
int32 CompletedAttemptCount = 0;
int32 TotalMistakes = 0;
bool IsStructurallyValid() const
{
return !RecordedAtUtc.IsEmpty()
&& !SourceSessionId.IsEmpty()
&& RecommendedAction != EHyperTwistCoachDashboardOutcomeAction::None
&& AppliedAction != EHyperTwistCoachDashboardOutcomeAction::None;
}
};
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API UHyperTwistCoachDashboardWidget : public UHyperTwistTrainingPanelWidget
{
@ -71,6 +99,9 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard")
int32 SelectedAttemptIndex = INDEX_NONE;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard")
int32 SelectedGuidanceDecisionIndex = INDEX_NONE;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard")
bool bHasLiveAttemptTimer = false;
@ -149,6 +180,12 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool ApplyGuidanceOutcomeAction();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool SelectPreviousGuidanceDecisionHistoryEntry();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool SelectNextGuidanceDecisionHistoryEntry();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool StartLiveAttemptTimer();
@ -224,6 +261,27 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> GuidanceOutcomeActionButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> GuidanceDecisionHistoryHeaderTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> GuidanceDecisionHistoryStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> GuidanceDecisionHistoryDetailTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> PreviousGuidanceDecisionButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> PreviousGuidanceDecisionButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> NextGuidanceDecisionButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> NextGuidanceDecisionButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> CoachHandoffHeaderTextBlock = nullptr;
@ -587,6 +645,12 @@ protected:
UFUNCTION()
void HandleGuidanceOutcomeActionClicked();
UFUNCTION()
void HandlePreviousGuidanceDecisionClicked();
UFUNCTION()
void HandleNextGuidanceDecisionClicked();
UFUNCTION()
void HandleClearRunClicked();
@ -641,6 +705,7 @@ private:
void SyncSelectedAttempt();
void SyncRetainedRunRecap();
void SyncSelectedGuidancePreviewCase();
void SyncSelectedGuidanceDecisionHistoryEntry();
void UpdateDashboardPresentation();
void RefreshLiveAttemptClock();
FString BuildDefaultDeferredUntilUtc() const;
@ -663,6 +728,15 @@ private:
EHyperTwistCoachDashboardOutcomeAction DeriveGuidanceOutcomeAction() const;
FString BuildGuidanceOutcomeActionLabel(EHyperTwistCoachDashboardOutcomeAction Action) const;
FHyperTwistTrainingDeck BuildGuidanceOutcomeSelectedCaseDeck() const;
void RecordGuidanceOutcomeDecision(
EHyperTwistCoachDashboardOutcomeAction RecommendedAction,
EHyperTwistCoachDashboardOutcomeAction AppliedAction,
const FHyperTwistTrainingSessionSummary& DisplayedRunRecap,
const FHyperTwistCoachBrief& DisplayedGuidanceComparisonBrief,
const FHyperTwistTrainingDeck& DisplayedGuidanceComparisonDeck,
const FString& DisplayedGuidanceComparisonSelectedCaseId,
bool bDisplayedGuidanceComparisonSingleCase
);
void CaptureActiveGuidanceLaunchContext(
const FString& SessionId,
const FHyperTwistTrainingDeck& Deck,
@ -699,6 +773,7 @@ private:
FHyperTwistCoachBrief RetainedGuidanceComparisonBrief;
FHyperTwistTrainingDeck RetainedGuidanceComparisonDeck;
FString RetainedGuidanceComparisonSelectedCaseId;
TArray<FHyperTwistCoachDashboardDecisionHistoryEntry> GuidanceDecisionHistoryEntries;
EHyperTwistCoachDashboardGuidancePreviewLane ActiveGuidancePreviewLane =
EHyperTwistCoachDashboardGuidancePreviewLane::None;
int32 SelectedGuidancePreviewCaseIndex = INDEX_NONE;