diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index cb6657c..94c6607 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -12318,6 +12318,307 @@ UHyperTwistCoachDashboardWidget::GetDisplayedActiveRunStatusInspectSurface() con return Surface; } +FHyperTwistTrainingCoachDashboardRunRecapStatusInspectSurface +UHyperTwistCoachDashboardWidget::GetDisplayedRunRecapStatusInspectSurface() const +{ + FHyperTwistTrainingCoachDashboardRunRecapStatusInspectSurface Surface; + Surface.Headline = TEXT("Run recap status"); + + const bool bHasActiveRun = CachedRunState.Session.IsStructurallyValid() + && CachedRunState.ActiveDeck.IsStructurallyValid(); + const bool bHasCurrentRunRecap = bHasActiveRun + && (CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Completed + || CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Aborted); + 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 = bHasDisplayedRunRecap + ? (bHasCurrentRunRecap ? TEXT("current run") : TEXT("last completed run")) + : (bHasActiveRun ? TEXT("active run") : TEXT("none yet")); + const bool bHasActiveTemplateLaunchForDisplayedRun = + bHasCurrentRunRecap + && DisplayedRunRecap != nullptr + && !ActiveTemplateLaunchSessionId.IsEmpty() + && DisplayedRunRecap->TrainingSessionId == ActiveTemplateLaunchSessionId + && ActiveTemplateLaunchProvenance.IsStructurallyValid(); + const bool bHasDisplayedRetainedTemplateLaunch = + !bHasCurrentRunRecap + && bHasRetainedTemplateLaunch + && DisplayedRunRecap != nullptr + && !RetainedTemplateLaunchSessionId.IsEmpty() + && DisplayedRunRecap->TrainingSessionId == RetainedTemplateLaunchSessionId + && RetainedTemplateLaunchProvenance.IsStructurallyValid(); + const FHyperTwistTrainingTemplateLaunchProvenance* DisplayedTemplateLaunch = + bHasActiveTemplateLaunchForDisplayedRun + ? &ActiveTemplateLaunchProvenance + : (bHasDisplayedRetainedTemplateLaunch ? &RetainedTemplateLaunchProvenance : nullptr); + const FString DisplayedDeckLabel = + DisplayedRunRecapDeck != nullptr + ? (DisplayedRunRecapDeck->Title.IsEmpty() + ? (DisplayedRunRecapDeck->DeckId.IsEmpty() + ? TEXT("n/a") + : DisplayedRunRecapDeck->DeckId) + : DisplayedRunRecapDeck->Title) + : (bHasDisplayedRunRecap && !DisplayedRunRecap->DeckId.IsEmpty() + ? DisplayedRunRecap->DeckId + : TEXT("n/a")); + + Surface.SourceLabel = RunRecapSourceLabel; + Surface.TrainingSessionId = bHasDisplayedRunRecap + ? DisplayedRunRecap->TrainingSessionId + : TEXT("n/a"); + Surface.DeckId = bHasDisplayedRunRecap && !DisplayedRunRecap->DeckId.IsEmpty() + ? DisplayedRunRecap->DeckId + : TEXT("n/a"); + Surface.DeckLabel = DisplayedDeckLabel; + Surface.SessionStateLabel = bHasDisplayedRunRecap + ? HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedRunRecap->SessionState) + : TEXT("n/a"); + Surface.DeliveryModeLabel = bHasDisplayedRunRecap + ? HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedRunRecapMode) + : TEXT("n/a"); + Surface.CompletedAtUtc = bHasDisplayedRunRecap && !DisplayedRunRecap->LastCompletedAtUtc.IsEmpty() + ? DisplayedRunRecap->LastCompletedAtUtc + : TEXT("n/a"); + Surface.TemplateId = DisplayedTemplateLaunch != nullptr && !DisplayedTemplateLaunch->TemplateId.IsEmpty() + ? DisplayedTemplateLaunch->TemplateId + : TEXT("n/a"); + Surface.TemplateKindLabel = DisplayedTemplateLaunch != nullptr + ? HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedTemplateLaunch->TemplateKind) + : TEXT("n/a"); + Surface.TemplateSourcePlanId = + DisplayedTemplateLaunch != nullptr && !DisplayedTemplateLaunch->SourceReviewPlanId.IsEmpty() + ? DisplayedTemplateLaunch->SourceReviewPlanId + : TEXT("none"); + Surface.bHasDisplayedRunRecap = bHasDisplayedRunRecap; + Surface.bHasCurrentRunRecap = bHasCurrentRunRecap; + Surface.bHasRetainedRunRecap = !bHasCurrentRunRecap && bHasRetainedRunRecap; + Surface.bHasTemplateLaunch = DisplayedTemplateLaunch != nullptr; + Surface.bTemplateUsesReviewLane = + DisplayedTemplateLaunch != nullptr + && (DisplayedTemplateLaunch->bPreferReviewRun + || DisplayedTemplateLaunch->TemplateKind == EHyperTwistTrainingSessionTemplateKind::ReviewPlan + || DisplayedTemplateLaunch->TemplateKind == EHyperTwistTrainingSessionTemplateKind::Coach); + Surface.bHasActiveRun = bHasActiveRun; + + Surface.StatusLine = !DisplayedRunRecapStatusLine.IsEmpty() + ? DisplayedRunRecapStatusLine + : (bHasDisplayedRunRecap + ? (DisplayedTemplateLaunch != nullptr + ? FString::Printf( + TEXT("Source: %s | Session: %s | Deck: %s | State: %s | Completed at: %s | Template: %s (%s) | Source plan: %s"), + *Surface.SourceLabel, + *Surface.TrainingSessionId, + *Surface.DeckId, + *Surface.SessionStateLabel, + *Surface.CompletedAtUtc, + *Surface.TemplateId, + *Surface.TemplateKindLabel, + *Surface.TemplateSourcePlanId) + : FString::Printf( + TEXT("Source: %s | Session: %s | Deck: %s | State: %s | Completed at: %s"), + *Surface.SourceLabel, + *Surface.TrainingSessionId, + *Surface.DeckId, + *Surface.SessionStateLabel, + *Surface.CompletedAtUtc)) + : (bHasActiveRun + ? TEXT("Source: active run | recap will stabilize here when the run completes or aborts.") + : TEXT("Source: none yet | complete a run to retain a session recap on this dashboard."))); + return Surface; +} + +FHyperTwistTrainingCoachDashboardRunRecapDetailInspectSurface +UHyperTwistCoachDashboardWidget::GetDisplayedRunRecapDetailInspectSurface() const +{ + FHyperTwistTrainingCoachDashboardRunRecapDetailInspectSurface Surface; + Surface.Headline = TEXT("Run recap detail"); + + const bool bHasActiveRun = CachedRunState.Session.IsStructurallyValid() + && CachedRunState.ActiveDeck.IsStructurallyValid(); + const bool bHasCurrentRunRecap = bHasActiveRun + && (CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Completed + || CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Aborted); + const FHyperTwistTrainingSessionSummary* DisplayedRunRecap = bHasCurrentRunRecap + ? &CachedRunSummary + : (bHasRetainedRunRecap ? &RetainedRunRecapSummary : nullptr); + const bool bHasDisplayedRunRecap = DisplayedRunRecap != nullptr + && !DisplayedRunRecap->TrainingSessionId.IsEmpty(); + const FString RunRecapSourceLabel = bHasDisplayedRunRecap + ? (bHasCurrentRunRecap ? TEXT("current run") : TEXT("last completed run")) + : (bHasActiveRun ? TEXT("active run") : TEXT("none yet")); + const bool bHasActiveTemplateLaunchForDisplayedRun = + bHasCurrentRunRecap + && DisplayedRunRecap != nullptr + && !ActiveTemplateLaunchSessionId.IsEmpty() + && DisplayedRunRecap->TrainingSessionId == ActiveTemplateLaunchSessionId + && ActiveTemplateLaunchProvenance.IsStructurallyValid(); + const bool bHasDisplayedRetainedTemplateLaunch = + !bHasCurrentRunRecap + && bHasRetainedTemplateLaunch + && DisplayedRunRecap != nullptr + && !RetainedTemplateLaunchSessionId.IsEmpty() + && DisplayedRunRecap->TrainingSessionId == RetainedTemplateLaunchSessionId + && RetainedTemplateLaunchProvenance.IsStructurallyValid(); + const FHyperTwistTrainingTemplateLaunchProvenance* DisplayedTemplateLaunch = + bHasActiveTemplateLaunchForDisplayedRun + ? &ActiveTemplateLaunchProvenance + : (bHasDisplayedRetainedTemplateLaunch ? &RetainedTemplateLaunchProvenance : nullptr); + const FString DisplayedTemplateLaunchUserId = DisplayedTemplateLaunch != nullptr + ? (DisplayedTemplateLaunch->UserId.IsEmpty() + ? (bHasCurrentRunRecap ? CachedRunState.Session.UserId : RetainedRunRecapUserId) + : DisplayedTemplateLaunch->UserId) + : FString(); + const FHyperTwistTrainingTemplateLaunchScopedStats* DisplayedTemplateLaunchStats = + DisplayedTemplateLaunch != nullptr + ? FindCachedTemplateLaunchScopedStats( + DisplayedTemplateLaunch->TemplateId, + DisplayedTemplateLaunchUserId) + : nullptr; + const FString DisplayedTemplateLaunchSelectionDetail = + HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchSelectionDetailFragment( + DisplayedTemplateLaunch != nullptr + && DisplayedTemplateLaunch->SelectionProvenance.IsStructurallyValid() + ? &DisplayedTemplateLaunch->SelectionProvenance + : nullptr + ); + const bool bDisplayedTemplateLaunchUsesReviewLane = + DisplayedTemplateLaunch != nullptr + && (DisplayedTemplateLaunch->bPreferReviewRun + || DisplayedTemplateLaunch->TemplateKind == EHyperTwistTrainingSessionTemplateKind::ReviewPlan + || DisplayedTemplateLaunch->TemplateKind == EHyperTwistTrainingSessionTemplateKind::Coach); + const FString TemplateAggregateFragment = DisplayedTemplateLaunch != nullptr + ? HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsOutcomeFragment( + DisplayedTemplateLaunchStats) + : TEXT("n/a"); + const FString TemplateAnalyticsDetailFragment = DisplayedTemplateLaunch != nullptr + ? HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsDetailFragment( + DisplayedTemplateLaunchStats) + : TEXT("n/a"); + const FString TemplateTitle = DisplayedTemplateLaunch != nullptr + ? (DisplayedTemplateLaunch->Title.IsEmpty() + ? (DisplayedTemplateLaunch->TemplateId.IsEmpty() + ? TEXT("n/a") + : DisplayedTemplateLaunch->TemplateId) + : DisplayedTemplateLaunch->Title) + : TEXT("n/a"); + + Surface.SourceLabel = RunRecapSourceLabel; + Surface.LastCaseId = bHasDisplayedRunRecap && !DisplayedRunRecap->LastCaseId.IsEmpty() + ? DisplayedRunRecap->LastCaseId + : TEXT("n/a"); + Surface.TemplateTitle = TemplateTitle; + Surface.TemplateModeLabel = DisplayedTemplateLaunch != nullptr + ? HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedTemplateLaunch->SuggestedMode) + : TEXT("n/a"); + Surface.TemplateSelectionLabel = DisplayedTemplateLaunch != nullptr + ? HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + DisplayedTemplateLaunch->SuggestedSelectionPolicy) + : TEXT("n/a"); + Surface.TemplateAggregateFragment = TemplateAggregateFragment; + Surface.TemplateAnalyticsDetailFragment = TemplateAnalyticsDetailFragment; + Surface.TemplateSelectionDetailFragment = DisplayedTemplateLaunchSelectionDetail; + Surface.AttemptCount = bHasDisplayedRunRecap ? DisplayedRunRecap->AttemptCount : 0; + Surface.CompletedAttemptCount = bHasDisplayedRunRecap ? DisplayedRunRecap->CompletedAttemptCount : 0; + Surface.SuccessCount = bHasDisplayedRunRecap ? DisplayedRunRecap->SuccessCount : 0; + Surface.FailureCount = bHasDisplayedRunRecap ? DisplayedRunRecap->FailureCount : 0; + Surface.TimeoutCount = bHasDisplayedRunRecap ? DisplayedRunRecap->TimeoutCount : 0; + Surface.DNFCount = bHasDisplayedRunRecap ? DisplayedRunRecap->DNFCount : 0; + Surface.AbortedCount = bHasDisplayedRunRecap ? DisplayedRunRecap->AbortedCount : 0; + Surface.BestTotalTimeMs = bHasDisplayedRunRecap ? DisplayedRunRecap->BestTotalTimeMs : 0; + Surface.AverageTotalTimeMs = bHasDisplayedRunRecap ? DisplayedRunRecap->AverageTotalTimeMs : 0; + Surface.AverageRawSolveTimeMs = bHasDisplayedRunRecap ? DisplayedRunRecap->AverageRawSolveTimeMs : 0; + Surface.AverageInspectionTimeMs = bHasDisplayedRunRecap + ? DisplayedRunRecap->AverageInspectionTimeMs + : 0; + Surface.TotalMistakes = bHasDisplayedRunRecap ? DisplayedRunRecap->TotalMistakes : 0; + Surface.RecommendedCaseCount = DisplayedTemplateLaunch != nullptr + ? DisplayedTemplateLaunch->RecommendedCaseCount + : 0; + Surface.FocusCaseCount = DisplayedTemplateLaunch != nullptr + ? DisplayedTemplateLaunch->FocusCaseIds.Num() + : 0; + Surface.SuccessRate = bHasDisplayedRunRecap ? DisplayedRunRecap->SuccessRate : 0.0f; + Surface.bHasDisplayedRunRecap = bHasDisplayedRunRecap; + Surface.bHasCurrentRunRecap = bHasCurrentRunRecap; + Surface.bHasTemplateLaunch = DisplayedTemplateLaunch != nullptr; + Surface.bTemplateUsesReviewLane = bDisplayedTemplateLaunchUsesReviewLane; + Surface.bHasTemplateSelectionDetail = !DisplayedTemplateLaunchSelectionDetail.IsEmpty(); + Surface.bHasTemplateAnalytics = DisplayedTemplateLaunchStats != nullptr + && DisplayedTemplateLaunchStats->IsStructurallyValid(); + + if (!DisplayedRunRecapDetailLine.IsEmpty()) + { + Surface.DetailLine = DisplayedRunRecapDetailLine; + return Surface; + } + + if (bHasDisplayedRunRecap) + { + Surface.DetailLine = + DisplayedTemplateLaunch != nullptr + ? FString::Printf( + TEXT("Attempts %d/%d | Success %d | Failure %d | Timeout %d | DNF %d | Aborted %d | Success rate %.2f | Best %d ms | Avg %d ms | Avg raw %d ms | Avg inspection %d ms | Mistakes %d | Last case %s | Template title %s | Template mode %s | Template selection %s | Recommended %d | Focus %d | Review-backed %s | Template aggregate %s | Template detail %s"), + Surface.CompletedAttemptCount, + Surface.AttemptCount, + Surface.SuccessCount, + Surface.FailureCount, + Surface.TimeoutCount, + Surface.DNFCount, + Surface.AbortedCount, + Surface.SuccessRate, + Surface.BestTotalTimeMs, + Surface.AverageTotalTimeMs, + Surface.AverageRawSolveTimeMs, + Surface.AverageInspectionTimeMs, + Surface.TotalMistakes, + *Surface.LastCaseId, + *Surface.TemplateTitle, + *Surface.TemplateModeLabel, + *Surface.TemplateSelectionLabel, + Surface.RecommendedCaseCount, + Surface.FocusCaseCount, + Surface.bTemplateUsesReviewLane ? TEXT("yes") : TEXT("no"), + *Surface.TemplateAggregateFragment, + *Surface.TemplateAnalyticsDetailFragment) + : FString::Printf( + TEXT("Attempts %d/%d | Success %d | Failure %d | Timeout %d | DNF %d | Aborted %d | Success rate %.2f | Best %d ms | Avg %d ms | Avg raw %d ms | Avg inspection %d ms | Mistakes %d | Last case %s"), + Surface.CompletedAttemptCount, + Surface.AttemptCount, + Surface.SuccessCount, + Surface.FailureCount, + Surface.TimeoutCount, + Surface.DNFCount, + Surface.AbortedCount, + Surface.SuccessRate, + Surface.BestTotalTimeMs, + Surface.AverageTotalTimeMs, + Surface.AverageRawSolveTimeMs, + Surface.AverageInspectionTimeMs, + Surface.TotalMistakes, + *Surface.LastCaseId); + if (Surface.bHasTemplateSelectionDetail) + { + Surface.DetailLine += TEXT(" | "); + Surface.DetailLine += Surface.TemplateSelectionDetailFragment; + } + } + else + { + Surface.DetailLine = + TEXT("Recap detail: unavailable until a run has been completed or explicitly aborted."); + } + return Surface; +} + FHyperTwistTrainingCoachDashboardActiveAttemptTimerStatusInspectSurface UHyperTwistCoachDashboardWidget::GetDisplayedActiveAttemptTimerStatusInspectSurface() const { @@ -25242,118 +25543,115 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { RunRecapHeaderTextBlock->SetText(FText::FromString(TEXT("[Run Recap]"))); } + if (bHasDisplayedRunRecap) + { + DisplayedRunRecapStatusLine = + DisplayedTemplateLaunch != nullptr + ? FString::Printf( + TEXT("Source: %s | Session: %s | Deck: %s | State: %s | Completed at: %s | Template: %s (%s) | Source plan: %s"), + *RunRecapSourceLabel, + *DisplayedRunRecap->TrainingSessionId, + DisplayedRunRecap->DeckId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->DeckId, + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedRunRecap->SessionState), + DisplayedRunRecap->LastCompletedAtUtc.IsEmpty() + ? TEXT("n/a") + : *DisplayedRunRecap->LastCompletedAtUtc, + DisplayedTemplateLaunch->TemplateId.IsEmpty() + ? TEXT("n/a") + : *DisplayedTemplateLaunch->TemplateId, + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + DisplayedTemplateLaunch->TemplateKind), + DisplayedTemplateLaunch->SourceReviewPlanId.IsEmpty() + ? TEXT("none") + : *DisplayedTemplateLaunch->SourceReviewPlanId) + : FString::Printf( + TEXT("Source: %s | Session: %s | Deck: %s | State: %s | Completed at: %s"), + *RunRecapSourceLabel, + *DisplayedRunRecap->TrainingSessionId, + DisplayedRunRecap->DeckId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->DeckId, + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedRunRecap->SessionState), + DisplayedRunRecap->LastCompletedAtUtc.IsEmpty() + ? TEXT("n/a") + : *DisplayedRunRecap->LastCompletedAtUtc); + } + else if (bHasActiveRun) + { + DisplayedRunRecapStatusLine = + TEXT("Source: active run | recap will stabilize here when the run completes or aborts."); + } + else + { + DisplayedRunRecapStatusLine = + TEXT("Source: none yet | complete a run to retain a session recap on this dashboard."); + } if (RunRecapStatusTextBlock != nullptr) { - if (bHasDisplayedRunRecap) + RunRecapStatusTextBlock->SetText(FText::FromString(DisplayedRunRecapStatusLine)); + } + if (bHasDisplayedRunRecap) + { + DisplayedRunRecapDetailLine = + DisplayedTemplateLaunch != nullptr + ? FString::Printf( + TEXT("Attempts %d/%d | Success %d | Failure %d | Timeout %d | DNF %d | Aborted %d | Success rate %.2f | Best %d ms | Avg %d ms | Avg raw %d ms | Avg inspection %d ms | Mistakes %d | Last case %s | Template title %s | Template mode %s | Template selection %s | Recommended %d | Focus %d | Review-backed %s | Template aggregate %s | Template detail %s"), + DisplayedRunRecap->CompletedAttemptCount, + DisplayedRunRecap->AttemptCount, + DisplayedRunRecap->SuccessCount, + DisplayedRunRecap->FailureCount, + DisplayedRunRecap->TimeoutCount, + DisplayedRunRecap->DNFCount, + DisplayedRunRecap->AbortedCount, + DisplayedRunRecap->SuccessRate, + DisplayedRunRecap->BestTotalTimeMs, + DisplayedRunRecap->AverageTotalTimeMs, + DisplayedRunRecap->AverageRawSolveTimeMs, + DisplayedRunRecap->AverageInspectionTimeMs, + DisplayedRunRecap->TotalMistakes, + DisplayedRunRecap->LastCaseId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->LastCaseId, + DisplayedTemplateLaunch->Title.IsEmpty() + ? TEXT("n/a") + : *DisplayedTemplateLaunch->Title, + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + DisplayedTemplateLaunch->SuggestedMode), + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + DisplayedTemplateLaunch->SuggestedSelectionPolicy), + DisplayedTemplateLaunch->RecommendedCaseCount, + DisplayedTemplateLaunch->FocusCaseIds.Num(), + bDisplayedTemplateLaunchUsesReviewLane ? TEXT("yes") : TEXT("no"), + *HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsOutcomeFragment( + DisplayedTemplateLaunchStats), + *HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsDetailFragment( + DisplayedTemplateLaunchStats)) + : FString::Printf( + TEXT("Attempts %d/%d | Success %d | Failure %d | Timeout %d | DNF %d | Aborted %d | Success rate %.2f | Best %d ms | Avg %d ms | Avg raw %d ms | Avg inspection %d ms | Mistakes %d | Last case %s"), + DisplayedRunRecap->CompletedAttemptCount, + DisplayedRunRecap->AttemptCount, + DisplayedRunRecap->SuccessCount, + DisplayedRunRecap->FailureCount, + DisplayedRunRecap->TimeoutCount, + DisplayedRunRecap->DNFCount, + DisplayedRunRecap->AbortedCount, + DisplayedRunRecap->SuccessRate, + DisplayedRunRecap->BestTotalTimeMs, + DisplayedRunRecap->AverageTotalTimeMs, + DisplayedRunRecap->AverageRawSolveTimeMs, + DisplayedRunRecap->AverageInspectionTimeMs, + DisplayedRunRecap->TotalMistakes, + DisplayedRunRecap->LastCaseId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->LastCaseId); + if (!DisplayedTemplateLaunchSelectionDetail.IsEmpty()) { - RunRecapStatusTextBlock->SetText(FText::FromString( - DisplayedTemplateLaunch != nullptr - ? FString::Printf( - TEXT("Source: %s | Session: %s | Deck: %s | State: %s | Completed at: %s | Template: %s (%s) | Source plan: %s"), - *RunRecapSourceLabel, - *DisplayedRunRecap->TrainingSessionId, - DisplayedRunRecap->DeckId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->DeckId, - *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedRunRecap->SessionState), - DisplayedRunRecap->LastCompletedAtUtc.IsEmpty() - ? TEXT("n/a") - : *DisplayedRunRecap->LastCompletedAtUtc, - DisplayedTemplateLaunch->TemplateId.IsEmpty() - ? TEXT("n/a") - : *DisplayedTemplateLaunch->TemplateId, - *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( - DisplayedTemplateLaunch->TemplateKind), - DisplayedTemplateLaunch->SourceReviewPlanId.IsEmpty() - ? TEXT("none") - : *DisplayedTemplateLaunch->SourceReviewPlanId) - : FString::Printf( - TEXT("Source: %s | Session: %s | Deck: %s | State: %s | Completed at: %s"), - *RunRecapSourceLabel, - *DisplayedRunRecap->TrainingSessionId, - DisplayedRunRecap->DeckId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->DeckId, - *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedRunRecap->SessionState), - DisplayedRunRecap->LastCompletedAtUtc.IsEmpty() - ? TEXT("n/a") - : *DisplayedRunRecap->LastCompletedAtUtc) - )); - } - else if (bHasActiveRun) - { - RunRecapStatusTextBlock->SetText(FText::FromString( - TEXT("Source: active run | recap will stabilize here when the run completes or aborts.") - )); - } - else - { - RunRecapStatusTextBlock->SetText(FText::FromString( - TEXT("Source: none yet | complete a run to retain a session recap on this dashboard.") - )); + DisplayedRunRecapDetailLine += TEXT(" | "); + DisplayedRunRecapDetailLine += DisplayedTemplateLaunchSelectionDetail; } } + else + { + DisplayedRunRecapDetailLine = + TEXT("Recap detail: unavailable until a run has been completed or explicitly aborted."); + } if (RunRecapDetailTextBlock != nullptr) { - if (bHasDisplayedRunRecap) - { - FString RunRecapDetailLine = - DisplayedTemplateLaunch != nullptr - ? FString::Printf( - TEXT("Attempts %d/%d | Success %d | Failure %d | Timeout %d | DNF %d | Aborted %d | Success rate %.2f | Best %d ms | Avg %d ms | Avg raw %d ms | Avg inspection %d ms | Mistakes %d | Last case %s | Template title %s | Template mode %s | Template selection %s | Recommended %d | Focus %d | Review-backed %s | Template aggregate %s | Template detail %s"), - DisplayedRunRecap->CompletedAttemptCount, - DisplayedRunRecap->AttemptCount, - DisplayedRunRecap->SuccessCount, - DisplayedRunRecap->FailureCount, - DisplayedRunRecap->TimeoutCount, - DisplayedRunRecap->DNFCount, - DisplayedRunRecap->AbortedCount, - DisplayedRunRecap->SuccessRate, - DisplayedRunRecap->BestTotalTimeMs, - DisplayedRunRecap->AverageTotalTimeMs, - DisplayedRunRecap->AverageRawSolveTimeMs, - DisplayedRunRecap->AverageInspectionTimeMs, - DisplayedRunRecap->TotalMistakes, - DisplayedRunRecap->LastCaseId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->LastCaseId, - DisplayedTemplateLaunch->Title.IsEmpty() - ? TEXT("n/a") - : *DisplayedTemplateLaunch->Title, - *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( - DisplayedTemplateLaunch->SuggestedMode), - *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( - DisplayedTemplateLaunch->SuggestedSelectionPolicy), - DisplayedTemplateLaunch->RecommendedCaseCount, - DisplayedTemplateLaunch->FocusCaseIds.Num(), - bDisplayedTemplateLaunchUsesReviewLane ? TEXT("yes") : TEXT("no"), - *HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsOutcomeFragment( - DisplayedTemplateLaunchStats), - *HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsDetailFragment( - DisplayedTemplateLaunchStats)) - : FString::Printf( - TEXT("Attempts %d/%d | Success %d | Failure %d | Timeout %d | DNF %d | Aborted %d | Success rate %.2f | Best %d ms | Avg %d ms | Avg raw %d ms | Avg inspection %d ms | Mistakes %d | Last case %s"), - DisplayedRunRecap->CompletedAttemptCount, - DisplayedRunRecap->AttemptCount, - DisplayedRunRecap->SuccessCount, - DisplayedRunRecap->FailureCount, - DisplayedRunRecap->TimeoutCount, - DisplayedRunRecap->DNFCount, - DisplayedRunRecap->AbortedCount, - DisplayedRunRecap->SuccessRate, - DisplayedRunRecap->BestTotalTimeMs, - DisplayedRunRecap->AverageTotalTimeMs, - DisplayedRunRecap->AverageRawSolveTimeMs, - DisplayedRunRecap->AverageInspectionTimeMs, - DisplayedRunRecap->TotalMistakes, - DisplayedRunRecap->LastCaseId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->LastCaseId); - if (!DisplayedTemplateLaunchSelectionDetail.IsEmpty()) - { - RunRecapDetailLine += TEXT(" | "); - RunRecapDetailLine += DisplayedTemplateLaunchSelectionDetail; - } - RunRecapDetailTextBlock->SetText(FText::FromString(RunRecapDetailLine)); - } - else - { - RunRecapDetailTextBlock->SetText(FText::FromString( - TEXT("Recap detail: unavailable until a run has been completed or explicitly aborted.") - )); - } + RunRecapDetailTextBlock->SetText(FText::FromString(DisplayedRunRecapDetailLine)); } if (GuidanceComparisonHeaderTextBlock != nullptr) { diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 0069983..1ed03e5 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -875,6 +875,14 @@ public: FHyperTwistTrainingCoachDashboardActiveRunStatusInspectSurface GetDisplayedActiveRunStatusInspectSurface() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach|Dashboard") + FHyperTwistTrainingCoachDashboardRunRecapStatusInspectSurface + GetDisplayedRunRecapStatusInspectSurface() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach|Dashboard") + FHyperTwistTrainingCoachDashboardRunRecapDetailInspectSurface + GetDisplayedRunRecapDetailInspectSurface() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach|Dashboard") FHyperTwistTrainingCoachDashboardActiveAttemptTimerStatusInspectSurface GetDisplayedActiveAttemptTimerStatusInspectSurface() const; @@ -2014,6 +2022,8 @@ private: FString DisplayedTransitionStatusLine; FString DisplayedActiveRunStatusLine; FString DisplayedActiveRunSummaryLine; + FString DisplayedRunRecapStatusLine; + FString DisplayedRunRecapDetailLine; FString DisplayedCurrentCasePromptLine; FString DisplayedCurrentCaseNotationLine; FString DisplayedAttemptTimeHintLabel; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 26ae23b..89ac55a 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -13300,6 +13300,179 @@ struct FHyperTwistTrainingCoachDashboardActiveRunStatusInspectSurface } }; +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachDashboardRunRecapStatusInspectSurface +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StatusLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TrainingSessionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeckLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SessionStateLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeliveryModeLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CompletedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TemplateId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TemplateKindLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TemplateSourcePlanId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasDisplayedRunRecap = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasCurrentRunRecap = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasRetainedRunRecap = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasTemplateLaunch = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bTemplateUsesReviewLane = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasActiveRun = false; + + bool IsStructurallyValid() const + { + return !StatusLine.IsEmpty() + || bHasDisplayedRunRecap + || bHasActiveRun; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachDashboardRunRecapDetailInspectSurface +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DetailLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LastCaseId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TemplateTitle; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TemplateModeLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TemplateSelectionLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TemplateAggregateFragment; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TemplateAnalyticsDetailFragment; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TemplateSelectionDetailFragment; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AttemptCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 CompletedAttemptCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SuccessCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 FailureCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TimeoutCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DNFCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AbortedCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 BestTotalTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AverageTotalTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AverageRawSolveTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AverageInspectionTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalMistakes = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RecommendedCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 FocusCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float SuccessRate = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasDisplayedRunRecap = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasCurrentRunRecap = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasTemplateLaunch = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bTemplateUsesReviewLane = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasTemplateSelectionDetail = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasTemplateAnalytics = false; + + bool IsStructurallyValid() const + { + return !DetailLine.IsEmpty() + || bHasDisplayedRunRecap + || bHasTemplateLaunch; + } +}; + USTRUCT(BlueprintType) struct FHyperTwistTrainingCoachDashboardActiveAttemptTimerStatusInspectSurface { diff --git a/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md b/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md index 9b975e7..8fa63c0 100644 --- a/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md +++ b/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md @@ -69,8 +69,8 @@ Current correction call from the source-exposed audit and current execution refr - the latest landed bounded `Phase 4` packet closes the retained-idle comparison/outcome actionability gap at the end of the recognition-assisted coach-to-review continuity lane, and the final retained-surface confirmation pass now leaves that closure lane functionally complete - the retained idle widget surface now keeps retained comparison and decision history inspectable without continuing to advertise live launch or guidance-outcome actions once the coach-to-review loop has already collapsed to truthful closed-loop idle - the now-complete deliberate `UHyperTwistTrainingRuntimeLibrary` world-context consumer lane reaches through active generated-mode selector option owned-execution split-phase current-selection merged ordered roster phase-id lookup, normalized-order lookup, and evaluation-order lookup inspect surfaces, so gameplay/Blueprint callers can inspect one retained merged-roster row directly by phase id, normalized order, or evaluation order without scanning the merged roster array or re-running inspect derivation details directly - - the latest landed bounded `Phase 4` packet pivots deliberately into the next unsurfaced live dashboard presentation band and adds displayed method-drill diagnostics and displayed drill follow-up action inspect surfaces over already-cached method-drill diagnostic, follow-up template, follow-up deck, and retained follow-up-weighting state, so gameplay/Blueprint callers can inspect the exact displayed diagnostics lines and surfaced drill follow-up label without scraping widget text or reopening that derivation path - - the next best clean move is to stay on that same live dashboard consumer lane and expose the dedicated run-recap status/detail inspect surface over the current-or-retained recap presentation band + - the latest landed bounded `Phase 4` packet stays on that same live dashboard consumer lane and adds dedicated run-recap status and run-recap detail inspect surfaces over the current-or-retained recap presentation band, so gameplay/Blueprint callers can inspect the exact displayed recap lines plus the retained session/template context behind them without scraping widget text or reopening recap derivation + - the next best clean move is to stay adjacent in that same live dashboard consumer lane and expose the run-recap repeat/dismiss action control surface over the already-retained recap action labels and enablement posture - the current execution-discipline rule that broad refactor / monolith-splitting work should not interrupt the active bounded roadmap packet unless structure is actually blocking it ## Donor portfolio phase taxonomy diff --git a/docs/arch/HYPERTWIST_PHASE4_DASHBOARD_LIVE_RUN_RECAP_STATUS_DETAIL_INSPECT_SURFACE_PACKET_2026-05-10.md b/docs/arch/HYPERTWIST_PHASE4_DASHBOARD_LIVE_RUN_RECAP_STATUS_DETAIL_INSPECT_SURFACE_PACKET_2026-05-10.md new file mode 100644 index 0000000..b1ed49f --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE4_DASHBOARD_LIVE_RUN_RECAP_STATUS_DETAIL_INSPECT_SURFACE_PACKET_2026-05-10.md @@ -0,0 +1,55 @@ +# HyperTwist Phase 4 dashboard live run-recap status/detail inspect surface packet + +- bounded Phase `4` dashboard-consumer slice +- date: `2026-05-10` + +## Intent + +Stay adjacent in the live dashboard consumer lane and expose the current-or-retained run-recap presentation directly to gameplay and Blueprint callers. + +## Why this packet exists + +`UHyperTwistCoachDashboardWidget` already displayed: + +- the live run-recap status line +- the live run-recap detail line +- the retained current-run versus last-completed-run recap posture behind that band + +But that recap band still only surfaced through widget text blocks. Callers still had to scrape UI text to understand which recap source is currently being shown, what completed-session payload is being summarized, and whether template-launch provenance is attached to that recap. + +## What changed + +- added `FHyperTwistTrainingCoachDashboardRunRecapStatusInspectSurface` +- added `FHyperTwistTrainingCoachDashboardRunRecapDetailInspectSurface` +- added `GetDisplayedRunRecapStatusInspectSurface()` +- added `GetDisplayedRunRecapDetailInspectSurface()` +- retained the exact displayed run-recap status/detail lines inside the widget so the new surfaces report the same live strings the dashboard is already presenting +- kept the packet bounded to `UHyperTwistCoachDashboardWidget` plus shared surface types, with no recap-persistence refactor and no backend continuity changes + +## Files + +- `C:\HyperTwist\UnrealHyperTwist\Source\UnrealHyperTwist\Public\HyperTwistTraining\HyperTwistTrainingTypes.h` +- `C:\HyperTwist\UnrealHyperTwist\Source\UnrealHyperTwist\Public\HyperTwistTraining\HyperTwistCoachDashboardWidget.h` +- `C:\HyperTwist\UnrealHyperTwist\Source\UnrealHyperTwist\Private\HyperTwistTraining\HyperTwistCoachDashboardWidget.cpp` +- `C:\HyperTwist\docs\HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md` + +## Outcome + +Gameplay and Blueprint callers can now inspect: + +- the exact displayed live run-recap status line +- the exact displayed live run-recap detail line +- whether the recap is coming from the current completed/aborted run or the retained last-completed run +- the retained session summary metrics behind that band +- the attached template-launch provenance and template analytics fragments when that recap came from a template-backed launch + +without scraping widget text or reopening run-recap derivation. + +## Validation + +- full `Development Editor | Win64` MSBuild on `UnrealHyperTwist.sln` + - passed with `0 Warning(s), 0 Error(s)` + +## Next clean slice + +Stay on the same live dashboard consumer lane and expose the run-recap repeat/dismiss action control surface over the already-retained recap action labels and enablement posture.