From 2b01fdd8c459062be41b747fec4d8cdbd060dd09 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Mon, 27 Apr 2026 15:36:27 +0200 Subject: [PATCH] Compare recap against coach guidance --- .../HyperTwistCoachDashboardWidget.cpp | 175 +++++++++++++++++- .../HyperTwistCoachDashboardWidget.h | 30 +++ 2 files changed, 203 insertions(+), 2 deletions(-) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index b16d566..34b827e 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -382,12 +382,21 @@ FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::StartActiveGuidance : (ActiveGuidancePreviewLane == EHyperTwistCoachDashboardGuidancePreviewLane::FollowUp ? TEXT("_preview_follow_up") : TEXT("_preview")); + const FString SessionId = DefaultSessionId + SessionSuffix; const FHyperTwistTrainingRunState RunState = StartTrainingRunFromDeck( *Deck, DefaultUserId, - DefaultSessionId + SessionSuffix, + SessionId, DefaultMode ); + CaptureActiveGuidanceLaunchContext( + RunState.Session.IsStructurallyValid() ? RunState.Session.TrainingSessionId : SessionId, + *Deck, + false, + SelectedGuidancePreviewCaseIndex >= 0 && SelectedGuidancePreviewCaseIndex < Deck->Cases.Num() + ? Deck->Cases[SelectedGuidancePreviewCaseIndex].CaseId + : FString() + ); SyncRetainedRunRecap(); SyncSelectedQueueEntry(); SyncSelectedAttempt(); @@ -410,12 +419,19 @@ FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::StartSelectedGuidan : (ActiveGuidancePreviewLane == EHyperTwistCoachDashboardGuidancePreviewLane::FollowUp ? TEXT("_preview_case_follow_up") : TEXT("_preview_case")); + const FString SessionId = DefaultSessionId + SessionSuffix; const FHyperTwistTrainingRunState RunState = StartTrainingRunFromDeck( Deck, DefaultUserId, - DefaultSessionId + SessionSuffix, + SessionId, DefaultMode ); + CaptureActiveGuidanceLaunchContext( + RunState.Session.IsStructurallyValid() ? RunState.Session.TrainingSessionId : SessionId, + Deck, + true, + Deck.Cases.Num() > 0 ? Deck.Cases[0].CaseId : FString() + ); SyncRetainedRunRecap(); SyncSelectedQueueEntry(); SyncSelectedAttempt(); @@ -464,6 +480,13 @@ void UHyperTwistCoachDashboardWidget::ClearRetainedRunRecap() RetainedRunRecapUserId.Reset(); RetainedRunRecapSessionId.Reset(); RetainedRunRecapMode = EHyperTwistTrainingDeliveryMode::Timer; + bHasRetainedGuidanceComparison = false; + RetainedGuidanceComparisonSessionId.Reset(); + RetainedGuidanceComparisonLane = EHyperTwistCoachDashboardGuidancePreviewLane::None; + bRetainedGuidanceComparisonSingleCase = false; + RetainedGuidanceComparisonBrief = FHyperTwistCoachBrief(); + RetainedGuidanceComparisonDeck = FHyperTwistTrainingDeck(); + RetainedGuidanceComparisonSelectedCaseId.Reset(); UpdateDashboardPresentation(); } @@ -1011,6 +1034,24 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() TEXT("CoachRunRecapDetail"), 2 ); + GuidanceComparisonHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachGuidanceComparisonHeader"), + 2 + ); + GuidanceComparisonStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachGuidanceComparisonStatus"), + 2 + ); + GuidanceComparisonDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachGuidanceComparisonDetail"), + 8 + ); CoachHandoffHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( WidgetTree, RootLayout, @@ -1943,6 +1984,18 @@ void UHyperTwistCoachDashboardWidget::SyncRetainedRunRecap() RetainedRunRecapUserId = CachedRunState.Session.UserId; RetainedRunRecapSessionId = CachedRunState.Session.TrainingSessionId; RetainedRunRecapMode = CachedRunState.Session.Mode; + if (!ActiveGuidanceLaunchSessionId.IsEmpty() + && ActiveGuidanceLaunchSessionId == RetainedRunRecapSessionId + && ActiveGuidanceLaunchLane != EHyperTwistCoachDashboardGuidancePreviewLane::None) + { + bHasRetainedGuidanceComparison = true; + RetainedGuidanceComparisonSessionId = ActiveGuidanceLaunchSessionId; + RetainedGuidanceComparisonLane = ActiveGuidanceLaunchLane; + bRetainedGuidanceComparisonSingleCase = bActiveGuidanceLaunchSingleCase; + RetainedGuidanceComparisonBrief = ActiveGuidanceLaunchBrief; + RetainedGuidanceComparisonDeck = ActiveGuidanceLaunchDeck; + RetainedGuidanceComparisonSelectedCaseId = ActiveGuidanceLaunchSelectedCaseId; + } } } @@ -1992,6 +2045,25 @@ const FHyperTwistCoachBrief* UHyperTwistCoachDashboardWidget::GetActiveGuidanceP } } +void UHyperTwistCoachDashboardWidget::CaptureActiveGuidanceLaunchContext( + const FString& SessionId, + const FHyperTwistTrainingDeck& Deck, + const bool bSingleCaseLaunch, + const FString& SelectedCaseId +) +{ + ActiveGuidanceLaunchSessionId = SessionId; + ActiveGuidanceLaunchLane = ActiveGuidancePreviewLane; + bActiveGuidanceLaunchSingleCase = bSingleCaseLaunch; + ActiveGuidanceLaunchDeck = Deck; + ActiveGuidanceLaunchSelectedCaseId = SelectedCaseId; + ActiveGuidanceLaunchBrief = FHyperTwistCoachBrief(); + if (const FHyperTwistCoachBrief* ActiveBrief = GetActiveGuidancePreviewBrief()) + { + ActiveGuidanceLaunchBrief = *ActiveBrief; + } +} + FHyperTwistTrainingDeck UHyperTwistCoachDashboardWidget::BuildSelectedGuidancePreviewCaseDeck() const { const FHyperTwistTrainingDeck* Deck = GetActiveGuidancePreviewDeck(); @@ -2225,6 +2297,12 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() const FHyperTwistTrainingSessionSummary* DisplayedRunRecap = bHasCurrentRunRecap ? &CachedRunSummary : (bHasRetainedRunRecap ? &RetainedRunRecapSummary : nullptr); + const FHyperTwistTrainingDeck* DisplayedRunRecapDeck = bHasCurrentRunRecap + ? &CachedRunState.ActiveDeck + : (bHasRetainedRunRecap ? &RetainedRunRecapDeck : nullptr); + const EHyperTwistTrainingDeliveryMode DisplayedRunRecapMode = bHasCurrentRunRecap + ? CachedRunState.Session.Mode + : RetainedRunRecapMode; const bool bHasDisplayedRunRecap = DisplayedRunRecap != nullptr && !DisplayedRunRecap->TrainingSessionId.IsEmpty(); const FString RunRecapSourceLabel = bHasCurrentRunRecap @@ -2236,6 +2314,36 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() || CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Completed || CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Aborted); const bool bCanDismissStoredRecap = !bHasCurrentRunRecap && bHasRetainedRunRecap; + const bool bHasActiveGuidanceComparison = + bHasCurrentRunRecap + && DisplayedRunRecap != nullptr + && !ActiveGuidanceLaunchSessionId.IsEmpty() + && DisplayedRunRecap->TrainingSessionId == ActiveGuidanceLaunchSessionId + && ActiveGuidanceLaunchLane != EHyperTwistCoachDashboardGuidancePreviewLane::None; + const bool bHasDisplayedRetainedGuidanceComparison = + !bHasCurrentRunRecap + && bHasRetainedGuidanceComparison + && DisplayedRunRecap != nullptr + && !RetainedGuidanceComparisonSessionId.IsEmpty() + && DisplayedRunRecap->TrainingSessionId == RetainedGuidanceComparisonSessionId + && RetainedGuidanceComparisonLane != EHyperTwistCoachDashboardGuidancePreviewLane::None; + const FHyperTwistCoachBrief* DisplayedGuidanceComparisonBrief = bHasActiveGuidanceComparison + ? &ActiveGuidanceLaunchBrief + : (bHasDisplayedRetainedGuidanceComparison ? &RetainedGuidanceComparisonBrief : nullptr); + const FHyperTwistTrainingDeck* DisplayedGuidanceComparisonDeck = bHasActiveGuidanceComparison + ? &ActiveGuidanceLaunchDeck + : (bHasDisplayedRetainedGuidanceComparison ? &RetainedGuidanceComparisonDeck : nullptr); + const FString DisplayedGuidanceComparisonSelectedCaseId = bHasActiveGuidanceComparison + ? ActiveGuidanceLaunchSelectedCaseId + : (bHasDisplayedRetainedGuidanceComparison ? RetainedGuidanceComparisonSelectedCaseId : FString()); + const EHyperTwistCoachDashboardGuidancePreviewLane DisplayedGuidanceComparisonLane = bHasActiveGuidanceComparison + ? ActiveGuidanceLaunchLane + : (bHasDisplayedRetainedGuidanceComparison + ? RetainedGuidanceComparisonLane + : EHyperTwistCoachDashboardGuidancePreviewLane::None); + const bool bDisplayedGuidanceComparisonSingleCase = bHasActiveGuidanceComparison + ? bActiveGuidanceLaunchSingleCase + : (bHasDisplayedRetainedGuidanceComparison ? bRetainedGuidanceComparisonSingleCase : false); const bool bCanUseCoachHandoff = !bHasLiveAttemptTimer && !bHasActiveRun; FString CoachHandoffStatusLine; if (bHasLiveAttemptTimer) @@ -2456,6 +2564,57 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() *SignalSummaryLine ) : TEXT("Rationale detail: unavailable until a guidance preview lane is active."); + const FString GuidanceComparisonLaneLabel = + DisplayedGuidanceComparisonLane == EHyperTwistCoachDashboardGuidancePreviewLane::Recommended + ? TEXT("recommended") + : (DisplayedGuidanceComparisonLane == EHyperTwistCoachDashboardGuidancePreviewLane::FollowUp + ? TEXT("follow-up") + : TEXT("none")); + const bool bDisplayedComparisonSelectedCaseMatchesLastCase = + DisplayedRunRecap != nullptr + && !DisplayedGuidanceComparisonSelectedCaseId.IsEmpty() + && DisplayedRunRecap->LastCaseId == DisplayedGuidanceComparisonSelectedCaseId; + const int32 GuidanceComparisonExpectedCaseCount = + DisplayedGuidanceComparisonDeck != nullptr ? DisplayedGuidanceComparisonDeck->Cases.Num() : 0; + const FString GuidanceComparisonStatusLine = + DisplayedRunRecap != nullptr + && DisplayedGuidanceComparisonBrief != nullptr + && DisplayedGuidanceComparisonDeck != nullptr + ? FString::Printf( + TEXT("Comparison: %s recap vs %s preview | Scope: %s | Focus lane: %s | Expected cases: %d | Completed attempts: %d | Success rate: %.2f"), + *RunRecapSourceLabel, + *GuidanceComparisonLaneLabel, + bDisplayedGuidanceComparisonSingleCase ? TEXT("selected case") : TEXT("full preview deck"), + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedGuidanceComparisonBrief->FocusLane), + GuidanceComparisonExpectedCaseCount, + DisplayedRunRecap->CompletedAttemptCount, + DisplayedRunRecap->SuccessRate + ) + : TEXT("Comparison: complete a run launched from previewed guidance to compare the recap against the original coach intent."); + const FString GuidanceComparisonDetailLine = + DisplayedRunRecap != nullptr + && DisplayedGuidanceComparisonBrief != nullptr + && DisplayedGuidanceComparisonDeck != nullptr + ? FString::Printf( + TEXT("Launch brief: %s | Action: %s | Preview deck: %s | Actual deck: %s | Mode expected/actual: %s / %s | Selection expected/actual: %s / %s | Selected case: %s | Last case matched: %s | Focus cases: %d | Last case: %s"), + DisplayedGuidanceComparisonBrief->Headline.IsEmpty() ? TEXT("n/a") : *DisplayedGuidanceComparisonBrief->Headline, + DisplayedGuidanceComparisonBrief->PrimaryActionLabel.IsEmpty() ? TEXT("n/a") : *DisplayedGuidanceComparisonBrief->PrimaryActionLabel, + DisplayedGuidanceComparisonDeck->Title.IsEmpty() ? *DisplayedGuidanceComparisonDeck->DeckId : *DisplayedGuidanceComparisonDeck->Title, + DisplayedRunRecapDeck != nullptr + ? (DisplayedRunRecapDeck->Title.IsEmpty() ? *DisplayedRunRecapDeck->DeckId : *DisplayedRunRecapDeck->Title) + : TEXT("n/a"), + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedGuidanceComparisonBrief->SuggestedMode), + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedRunRecapMode), + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedGuidanceComparisonBrief->SuggestedSelectionPolicy), + DisplayedRunRecapDeck != nullptr + ? *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedRunRecapDeck->SelectionPolicy) + : TEXT("n/a"), + DisplayedGuidanceComparisonSelectedCaseId.IsEmpty() ? TEXT("n/a") : *DisplayedGuidanceComparisonSelectedCaseId, + bDisplayedComparisonSelectedCaseMatchesLastCase ? TEXT("yes") : TEXT("no"), + DisplayedGuidanceComparisonBrief->FocusCaseIds.Num(), + DisplayedRunRecap->LastCaseId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->LastCaseId + ) + : TEXT("Comparison detail: no preview-launched recap is currently selected."); const bool bCanStartPreviewDeck = !bHasLiveAttemptTimer && !bHasActiveRun && ActivePreviewDeck != nullptr && ActivePreviewDeck->IsStructurallyValid(); const bool bCanStartPreviewCase = bCanStartPreviewDeck && SelectedGuidancePreviewCase != nullptr; @@ -2572,6 +2731,18 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() )); } } + if (GuidanceComparisonHeaderTextBlock != nullptr) + { + GuidanceComparisonHeaderTextBlock->SetText(FText::FromString(TEXT("[Guidance Comparison]"))); + } + if (GuidanceComparisonStatusTextBlock != nullptr) + { + GuidanceComparisonStatusTextBlock->SetText(FText::FromString(GuidanceComparisonStatusLine)); + } + if (GuidanceComparisonDetailTextBlock != nullptr) + { + GuidanceComparisonDetailTextBlock->SetText(FText::FromString(GuidanceComparisonDetailLine)); + } if (RepeatRunButtonLabel != nullptr) { RepeatRunButtonLabel->SetText(FText::FromString(TEXT("Repeat Run"))); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 964522e..28a4ab1 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -188,6 +188,15 @@ protected: UPROPERTY(Transient) TObjectPtr RunRecapDetailTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr GuidanceComparisonHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr GuidanceComparisonStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr GuidanceComparisonDetailTextBlock = nullptr; + UPROPERTY(Transient) TObjectPtr CoachHandoffHeaderTextBlock = nullptr; @@ -614,6 +623,12 @@ private: const FHyperTwistTrainingDeck* GetActiveGuidancePreviewDeck() const; const FHyperTwistCoachBrief* GetActiveGuidancePreviewBrief() const; FHyperTwistTrainingDeck BuildSelectedGuidancePreviewCaseDeck() const; + void CaptureActiveGuidanceLaunchContext( + const FString& SessionId, + const FHyperTwistTrainingDeck& Deck, + bool bSingleCaseLaunch, + const FString& SelectedCaseId + ); bool IsLiveAttemptInspectionPhase() const; int32 GetMaxManualSplitCaptureCount() const; const FHyperTwistTrainingSplitPhaseDefinition* FindNextSplitPhaseToCapture() const; @@ -629,6 +644,21 @@ private: FString RetainedRunRecapUserId; FString RetainedRunRecapSessionId; EHyperTwistTrainingDeliveryMode RetainedRunRecapMode = EHyperTwistTrainingDeliveryMode::Timer; + FString ActiveGuidanceLaunchSessionId; + EHyperTwistCoachDashboardGuidancePreviewLane ActiveGuidanceLaunchLane = + EHyperTwistCoachDashboardGuidancePreviewLane::None; + bool bActiveGuidanceLaunchSingleCase = false; + FHyperTwistCoachBrief ActiveGuidanceLaunchBrief; + FHyperTwistTrainingDeck ActiveGuidanceLaunchDeck; + FString ActiveGuidanceLaunchSelectedCaseId; + bool bHasRetainedGuidanceComparison = false; + FString RetainedGuidanceComparisonSessionId; + EHyperTwistCoachDashboardGuidancePreviewLane RetainedGuidanceComparisonLane = + EHyperTwistCoachDashboardGuidancePreviewLane::None; + bool bRetainedGuidanceComparisonSingleCase = false; + FHyperTwistCoachBrief RetainedGuidanceComparisonBrief; + FHyperTwistTrainingDeck RetainedGuidanceComparisonDeck; + FString RetainedGuidanceComparisonSelectedCaseId; EHyperTwistCoachDashboardGuidancePreviewLane ActiveGuidancePreviewLane = EHyperTwistCoachDashboardGuidancePreviewLane::None; int32 SelectedGuidancePreviewCaseIndex = INDEX_NONE;