diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 6104f4e..8e9a8f4 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -2806,6 +2806,7 @@ void UHyperTwistCoachDashboardWidget::RefreshCoachDashboardView() SyncSelectedAttempt(); SyncSelectedGuidanceDecisionHistoryEntry(); SyncSelectedHandoffHistoryEntry(); + SyncSelectedTemplateLaunchHistoryEntry(); SyncSelectedQueueSuppressionHistoryEntry(); SyncSelectedClosureRecoveryHistoryEntry(); SyncSelectedMethodDrillFollowUpHistoryEntry(); @@ -3511,21 +3512,26 @@ FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::StartPreferredActiv return FHyperTwistTrainingRunState(); } - const FString ResolvedUserId = PreferredTemplate->UserId.IsEmpty() + const FHyperTwistTrainingSessionTemplate LaunchedTemplate = *PreferredTemplate; + const FString ResolvedUserId = LaunchedTemplate.UserId.IsEmpty() ? DefaultUserId - : PreferredTemplate->UserId; + : LaunchedTemplate.UserId; const FString LaunchSessionId = FString::Printf( TEXT("%s_template_%s_%s"), *DefaultSessionId, - *PreferredTemplate->TemplateId, + *LaunchedTemplate.TemplateId, *FGuid::NewGuid().ToString(EGuidFormats::Digits) ); LastStepResult = FHyperTwistTrainingRunStepResult(); const FHyperTwistTrainingRunState RunState = StartTrainingRunFromTemplate( - PreferredTemplate->TemplateId, + LaunchedTemplate.TemplateId, ResolvedUserId, LaunchSessionId ); + if (RunState.Session.IsStructurallyValid()) + { + CaptureActiveTemplateLaunchContext(RunState.Session.TrainingSessionId, LaunchedTemplate); + } SyncRetainedRunRecap(); SyncSelectedQueueEntry(); SyncSelectedAttempt(); @@ -3689,6 +3695,9 @@ void UHyperTwistCoachDashboardWidget::ClearRetainedRunRecap() RetainedRunRecapUserId.Reset(); RetainedRunRecapSessionId.Reset(); RetainedRunRecapMode = EHyperTwistTrainingDeliveryMode::Timer; + bHasRetainedTemplateLaunch = false; + RetainedTemplateLaunchSessionId.Reset(); + RetainedTemplateLaunchTemplate = FHyperTwistTrainingSessionTemplate(); bHasRetainedGuidanceComparison = false; RetainedGuidanceComparisonSessionId.Reset(); RetainedGuidanceComparisonLane = EHyperTwistCoachDashboardGuidancePreviewLane::None; @@ -4057,6 +4066,47 @@ bool UHyperTwistCoachDashboardWidget::SelectNextHandoffHistoryEntry() return true; } +bool UHyperTwistCoachDashboardWidget::SelectPreviousTemplateLaunchHistoryEntry() +{ + if (TemplateLaunchHistoryEntries.Num() <= 0) + { + SelectedTemplateLaunchHistoryIndex = INDEX_NONE; + return false; + } + + if (SelectedTemplateLaunchHistoryIndex == INDEX_NONE) + { + SyncSelectedTemplateLaunchHistoryEntry(); + return SelectedTemplateLaunchHistoryIndex != INDEX_NONE; + } + + SelectedTemplateLaunchHistoryIndex = + (SelectedTemplateLaunchHistoryIndex - 1 + TemplateLaunchHistoryEntries.Num()) + % TemplateLaunchHistoryEntries.Num(); + UpdateDashboardPresentation(); + return true; +} + +bool UHyperTwistCoachDashboardWidget::SelectNextTemplateLaunchHistoryEntry() +{ + if (TemplateLaunchHistoryEntries.Num() <= 0) + { + SelectedTemplateLaunchHistoryIndex = INDEX_NONE; + return false; + } + + if (SelectedTemplateLaunchHistoryIndex == INDEX_NONE) + { + SyncSelectedTemplateLaunchHistoryEntry(); + return SelectedTemplateLaunchHistoryIndex != INDEX_NONE; + } + + SelectedTemplateLaunchHistoryIndex = + (SelectedTemplateLaunchHistoryIndex + 1) % TemplateLaunchHistoryEntries.Num(); + UpdateDashboardPresentation(); + return true; +} + bool UHyperTwistCoachDashboardWidget::SelectPreviousQueueSuppressionHistoryEntry() { if (QueueSuppressionHistoryEntries.Num() <= 0) @@ -4613,6 +4663,16 @@ void UHyperTwistCoachDashboardWidget::HandleNextHandoffHistoryClicked() SelectNextHandoffHistoryEntry(); } +void UHyperTwistCoachDashboardWidget::HandlePreviousTemplateLaunchHistoryClicked() +{ + SelectPreviousTemplateLaunchHistoryEntry(); +} + +void UHyperTwistCoachDashboardWidget::HandleNextTemplateLaunchHistoryClicked() +{ + SelectNextTemplateLaunchHistoryEntry(); +} + void UHyperTwistCoachDashboardWidget::HandlePreviousQueueSuppressionClicked() { SelectPreviousQueueSuppressionHistoryEntry(); @@ -5018,6 +5078,58 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() TEXT("CoachTemplateLaunchDetail"), 8 ); + TemplateLaunchHistoryHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachTemplateLaunchHistoryHeader"), + 2 + ); + TemplateLaunchHistoryStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachTemplateLaunchHistoryStatus"), + 2 + ); + TemplateLaunchHistoryDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachTemplateLaunchHistoryDetail"), + 8 + ); + UHorizontalBox* TemplateLaunchHistoryNavRow = WidgetTree->ConstructWidget( + UHorizontalBox::StaticClass(), + TEXT("CoachDashboardTemplateLaunchHistoryNavRow") + ); + if (TemplateLaunchHistoryNavRow != nullptr) + { + if (UVerticalBoxSlot* TemplateLaunchHistoryNavRowSlot = + RootLayout->AddChildToVerticalBox(TemplateLaunchHistoryNavRow)) + { + TemplateLaunchHistoryNavRowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 0.0f)); + } + + UTextBlock* PreviousTemplateLaunchHistoryLabelRaw = nullptr; + PreviousTemplateLaunchHistoryButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + TemplateLaunchHistoryNavRow, + TEXT("PreviousTemplateLaunchHistoryButton"), + TEXT("PreviousTemplateLaunchHistoryButtonLabel"), + TEXT("Prev Template Launch"), + PreviousTemplateLaunchHistoryLabelRaw + ); + PreviousTemplateLaunchHistoryButtonLabel = PreviousTemplateLaunchHistoryLabelRaw; + + UTextBlock* NextTemplateLaunchHistoryLabelRaw = nullptr; + NextTemplateLaunchHistoryButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + TemplateLaunchHistoryNavRow, + TEXT("NextTemplateLaunchHistoryButton"), + TEXT("NextTemplateLaunchHistoryButtonLabel"), + TEXT("Next Template Launch"), + NextTemplateLaunchHistoryLabelRaw + ); + NextTemplateLaunchHistoryButtonLabel = NextTemplateLaunchHistoryLabelRaw; + } HandoffHistoryHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( WidgetTree, RootLayout, @@ -6277,6 +6389,20 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() { NextHandoffHistoryButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleNextHandoffHistoryClicked); } + if (PreviousTemplateLaunchHistoryButton != nullptr) + { + PreviousTemplateLaunchHistoryButton->OnClicked.AddDynamic( + this, + &UHyperTwistCoachDashboardWidget::HandlePreviousTemplateLaunchHistoryClicked + ); + } + if (NextTemplateLaunchHistoryButton != nullptr) + { + NextTemplateLaunchHistoryButton->OnClicked.AddDynamic( + this, + &UHyperTwistCoachDashboardWidget::HandleNextTemplateLaunchHistoryClicked + ); + } if (DismissRecapButton != nullptr) { DismissRecapButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleDismissRecapClicked); @@ -6537,6 +6663,15 @@ void UHyperTwistCoachDashboardWidget::SyncRetainedRunRecap() RetainedRunRecapUserId = CachedRunState.Session.UserId; RetainedRunRecapSessionId = CachedRunState.Session.TrainingSessionId; RetainedRunRecapMode = CachedRunState.Session.Mode; + if (!ActiveTemplateLaunchSessionId.IsEmpty() + && ActiveTemplateLaunchSessionId == RetainedRunRecapSessionId + && ActiveTemplateLaunchTemplate.IsStructurallyValid()) + { + bHasRetainedTemplateLaunch = true; + RetainedTemplateLaunchSessionId = ActiveTemplateLaunchSessionId; + RetainedTemplateLaunchTemplate = ActiveTemplateLaunchTemplate; + RecordTemplateLaunchHistoryEntry(RetainedRunRecapSummary); + } if (!ActiveGuidanceLaunchSessionId.IsEmpty() && ActiveGuidanceLaunchSessionId == RetainedRunRecapSessionId && ActiveGuidanceLaunchLane != EHyperTwistCoachDashboardGuidancePreviewLane::None) @@ -6611,6 +6746,21 @@ void UHyperTwistCoachDashboardWidget::SyncSelectedHandoffHistoryEntry() } } +void UHyperTwistCoachDashboardWidget::SyncSelectedTemplateLaunchHistoryEntry() +{ + if (TemplateLaunchHistoryEntries.Num() <= 0) + { + SelectedTemplateLaunchHistoryIndex = INDEX_NONE; + return; + } + + if (SelectedTemplateLaunchHistoryIndex < 0 + || SelectedTemplateLaunchHistoryIndex >= TemplateLaunchHistoryEntries.Num()) + { + SelectedTemplateLaunchHistoryIndex = TemplateLaunchHistoryEntries.Num() - 1; + } +} + void UHyperTwistCoachDashboardWidget::SyncSelectedQueueSuppressionHistoryEntry() { if (QueueSuppressionHistoryEntries.Num() <= 0) @@ -6783,6 +6933,15 @@ void UHyperTwistCoachDashboardWidget::CaptureActiveGuidanceLaunchContext( } } +void UHyperTwistCoachDashboardWidget::CaptureActiveTemplateLaunchContext( + const FString& SessionId, + const FHyperTwistTrainingSessionTemplate& SessionTemplate +) +{ + ActiveTemplateLaunchSessionId = SessionId; + ActiveTemplateLaunchTemplate = SessionTemplate; +} + void UHyperTwistCoachDashboardWidget::CaptureActiveMethodDrillFollowUpLaunchContext( const FString& SessionId ) @@ -9699,6 +9858,65 @@ void UHyperTwistCoachDashboardWidget::RecordClosureRecoverySnapshot() SelectedClosureRecoveryIndex = ClosureRecoveryHistoryEntries.Num() - 1; } +void UHyperTwistCoachDashboardWidget::RecordTemplateLaunchHistoryEntry( + const FHyperTwistTrainingSessionSummary& CompletedRunRecap +) +{ + if (CompletedRunRecap.TrainingSessionId.IsEmpty() + || !ActiveTemplateLaunchTemplate.IsStructurallyValid() + || ActiveTemplateLaunchSessionId != CompletedRunRecap.TrainingSessionId) + { + return; + } + + FHyperTwistCoachDashboardTemplateLaunchHistoryEntry Entry; + Entry.RecordedAtUtc = FDateTime::UtcNow().ToIso8601(); + Entry.SourceSessionId = CompletedRunRecap.TrainingSessionId; + Entry.TemplateId = ActiveTemplateLaunchTemplate.TemplateId; + Entry.TemplateTitle = ActiveTemplateLaunchTemplate.Title; + Entry.UserId = ActiveTemplateLaunchTemplate.UserId; + Entry.FocusDeckId = ActiveTemplateLaunchTemplate.FocusDeckId; + Entry.SourceReviewPlanId = ActiveTemplateLaunchTemplate.SourceReviewPlanId; + Entry.TemplateKind = ActiveTemplateLaunchTemplate.TemplateKind; + Entry.SessionState = CompletedRunRecap.SessionState; + Entry.SuggestedMode = ActiveTemplateLaunchTemplate.SuggestedMode; + Entry.SuggestedSelectionPolicy = ActiveTemplateLaunchTemplate.SuggestedSelectionPolicy; + Entry.RecommendedCaseCount = ActiveTemplateLaunchTemplate.RecommendedCaseCount; + Entry.FocusCaseCount = ActiveTemplateLaunchTemplate.FocusCaseIds.Num(); + Entry.CompletedAttemptCount = CompletedRunRecap.CompletedAttemptCount; + Entry.TotalMistakes = CompletedRunRecap.TotalMistakes; + Entry.SuccessRate = CompletedRunRecap.SuccessRate; + Entry.CompletedAtUtc = CompletedRunRecap.LastCompletedAtUtc; + Entry.bPreferReviewRun = ActiveTemplateLaunchTemplate.bPreferReviewRun; + Entry.bCoachReview = ActiveTemplateLaunchTemplate.bCoachReview; + Entry.bPreferShortReviewSet = ActiveTemplateLaunchTemplate.bPreferShortReviewSet; + if (!Entry.IsStructurallyValid()) + { + return; + } + + const bool bAlreadyRecorded = TemplateLaunchHistoryEntries.ContainsByPredicate( + [&Entry](const FHyperTwistCoachDashboardTemplateLaunchHistoryEntry& Candidate) + { + return Candidate.SourceSessionId == Entry.SourceSessionId; + } + ); + if (bAlreadyRecorded) + { + return; + } + + TemplateLaunchHistoryEntries.Add(Entry); + constexpr int32 MaxRetainedTemplateLaunchHistoryEntries = 16; + if (TemplateLaunchHistoryEntries.Num() > MaxRetainedTemplateLaunchHistoryEntries) + { + const int32 ExcessCount = + TemplateLaunchHistoryEntries.Num() - MaxRetainedTemplateLaunchHistoryEntries; + TemplateLaunchHistoryEntries.RemoveAt(0, ExcessCount, EAllowShrinking::No); + } + SelectedTemplateLaunchHistoryIndex = TemplateLaunchHistoryEntries.Num() - 1; +} + void UHyperTwistCoachDashboardWidget::RecordMethodDrillFollowUpHistoryEntry( const FHyperTwistTrainingSessionSummary& CompletedRunRecap ) @@ -10860,6 +11078,28 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() const FString RunRecapSourceLabel = bHasCurrentRunRecap ? TEXT("current run") : TEXT("last completed run"); + const bool bHasActiveTemplateLaunchForDisplayedRun = + bHasCurrentRunRecap + && DisplayedRunRecap != nullptr + && !ActiveTemplateLaunchSessionId.IsEmpty() + && DisplayedRunRecap->TrainingSessionId == ActiveTemplateLaunchSessionId + && ActiveTemplateLaunchTemplate.IsStructurallyValid(); + const bool bHasDisplayedRetainedTemplateLaunch = + !bHasCurrentRunRecap + && bHasRetainedTemplateLaunch + && DisplayedRunRecap != nullptr + && !RetainedTemplateLaunchSessionId.IsEmpty() + && DisplayedRunRecap->TrainingSessionId == RetainedTemplateLaunchSessionId + && RetainedTemplateLaunchTemplate.IsStructurallyValid(); + const FHyperTwistTrainingSessionTemplate* DisplayedTemplateLaunch = + bHasActiveTemplateLaunchForDisplayedRun + ? &ActiveTemplateLaunchTemplate + : (bHasDisplayedRetainedTemplateLaunch ? &RetainedTemplateLaunchTemplate : nullptr); + const bool bDisplayedTemplateLaunchUsesReviewLane = + DisplayedTemplateLaunch != nullptr + && (DisplayedTemplateLaunch->bPreferReviewRun + || DisplayedTemplateLaunch->TemplateKind == EHyperTwistTrainingSessionTemplateKind::ReviewPlan + || DisplayedTemplateLaunch->TemplateKind == EHyperTwistTrainingSessionTemplateKind::Coach); const bool bCanRepeatDisplayedRun = !bHasLiveAttemptTimer && RetainedRunRecapDeck.IsStructurallyValid() && (!bHasActiveRun @@ -10995,6 +11235,58 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() PreferredTemplateLaunch->bPreferShortReviewSet ? TEXT("yes") : TEXT("no") ); } + const FHyperTwistCoachDashboardTemplateLaunchHistoryEntry* SelectedTemplateLaunchHistoryEntry = + SelectedTemplateLaunchHistoryIndex >= 0 + && SelectedTemplateLaunchHistoryIndex < TemplateLaunchHistoryEntries.Num() + ? &TemplateLaunchHistoryEntries[SelectedTemplateLaunchHistoryIndex] + : nullptr; + const FString TemplateLaunchHistoryStatusLine = + SelectedTemplateLaunchHistoryEntry != nullptr + ? FString::Printf( + TEXT("Template launch history: %d entries | selected %d/%d | Template: %s | State: %s | Completed: %s"), + TemplateLaunchHistoryEntries.Num(), + SelectedTemplateLaunchHistoryIndex + 1, + TemplateLaunchHistoryEntries.Num(), + SelectedTemplateLaunchHistoryEntry->TemplateId.IsEmpty() + ? TEXT("n/a") + : *SelectedTemplateLaunchHistoryEntry->TemplateId, + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + SelectedTemplateLaunchHistoryEntry->SessionState), + SelectedTemplateLaunchHistoryEntry->CompletedAtUtc.IsEmpty() + ? TEXT("n/a") + : *SelectedTemplateLaunchHistoryEntry->CompletedAtUtc) + : TEXT("Template launch history: no completed template-backed runs retained yet."); + const FString TemplateLaunchHistoryDetailLine = + SelectedTemplateLaunchHistoryEntry != nullptr + ? FString::Printf( + TEXT("Title: %s | kind: %s | mode: %s | selection: %s | deck: %s | user: %s | source plan: %s | recommended %d | focus %d | attempts %d | success %.2f | mistakes %d | review-backed: %s | coach-review: %s | short set: %s"), + SelectedTemplateLaunchHistoryEntry->TemplateTitle.IsEmpty() + ? TEXT("n/a") + : *SelectedTemplateLaunchHistoryEntry->TemplateTitle, + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + SelectedTemplateLaunchHistoryEntry->TemplateKind), + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + SelectedTemplateLaunchHistoryEntry->SuggestedMode), + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + SelectedTemplateLaunchHistoryEntry->SuggestedSelectionPolicy), + SelectedTemplateLaunchHistoryEntry->FocusDeckId.IsEmpty() + ? TEXT("n/a") + : *SelectedTemplateLaunchHistoryEntry->FocusDeckId, + SelectedTemplateLaunchHistoryEntry->UserId.IsEmpty() + ? TEXT("n/a") + : *SelectedTemplateLaunchHistoryEntry->UserId, + SelectedTemplateLaunchHistoryEntry->SourceReviewPlanId.IsEmpty() + ? TEXT("none") + : *SelectedTemplateLaunchHistoryEntry->SourceReviewPlanId, + SelectedTemplateLaunchHistoryEntry->RecommendedCaseCount, + SelectedTemplateLaunchHistoryEntry->FocusCaseCount, + SelectedTemplateLaunchHistoryEntry->CompletedAttemptCount, + SelectedTemplateLaunchHistoryEntry->SuccessRate, + SelectedTemplateLaunchHistoryEntry->TotalMistakes, + SelectedTemplateLaunchHistoryEntry->bPreferReviewRun ? TEXT("yes") : TEXT("no"), + SelectedTemplateLaunchHistoryEntry->bCoachReview ? TEXT("yes") : TEXT("no"), + SelectedTemplateLaunchHistoryEntry->bPreferShortReviewSet ? TEXT("yes") : TEXT("no")) + : TEXT("Template launch history detail: completed runs started through the dashboard template lane will accumulate here with template id, template kind, and source review-plan provenance."); const bool bCanUseCoachHandoff = !bHasLiveAttemptTimer && !bHasActiveRun; FString CoachHandoffStatusLine; if (bHasLiveAttemptTimer) @@ -14958,16 +15250,35 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { if (bHasDisplayedRunRecap) { - RunRecapStatusTextBlock->SetText(FText::FromString(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 - ))); + 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) { @@ -14986,23 +15297,51 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { if (bHasDisplayedRunRecap) { - RunRecapDetailTextBlock->SetText(FText::FromString(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 - ))); + RunRecapDetailTextBlock->SetText(FText::FromString( + 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"), + 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")) + : 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) + )); } else { @@ -15143,6 +15482,39 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { TemplateLaunchDetailTextBlock->SetText(FText::FromString(TemplateLaunchDetailLine)); } + if (TemplateLaunchHistoryHeaderTextBlock != nullptr) + { + TemplateLaunchHistoryHeaderTextBlock->SetText(FText::FromString( + TEXT("[Template Launch History]"))); + } + if (TemplateLaunchHistoryStatusTextBlock != nullptr) + { + TemplateLaunchHistoryStatusTextBlock->SetText(FText::FromString( + TemplateLaunchHistoryStatusLine)); + } + if (TemplateLaunchHistoryDetailTextBlock != nullptr) + { + TemplateLaunchHistoryDetailTextBlock->SetText(FText::FromString( + TemplateLaunchHistoryDetailLine)); + } + if (PreviousTemplateLaunchHistoryButtonLabel != nullptr) + { + PreviousTemplateLaunchHistoryButtonLabel->SetText(FText::FromString( + TEXT("Prev Template Launch"))); + } + if (PreviousTemplateLaunchHistoryButton != nullptr) + { + PreviousTemplateLaunchHistoryButton->SetIsEnabled(TemplateLaunchHistoryEntries.Num() > 1); + } + if (NextTemplateLaunchHistoryButtonLabel != nullptr) + { + NextTemplateLaunchHistoryButtonLabel->SetText(FText::FromString( + TEXT("Next Template Launch"))); + } + if (NextTemplateLaunchHistoryButton != nullptr) + { + NextTemplateLaunchHistoryButton->SetIsEnabled(TemplateLaunchHistoryEntries.Num() > 1); + } if (HandoffHistoryHeaderTextBlock != nullptr) { HandoffHistoryHeaderTextBlock->SetText(FText::FromString(TEXT("[Handoff History]"))); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 13b3948..b400af8 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -372,6 +372,40 @@ struct FHyperTwistCoachDashboardHandoffHistoryEntry } }; +struct FHyperTwistCoachDashboardTemplateLaunchHistoryEntry +{ + FString RecordedAtUtc; + FString SourceSessionId; + FString TemplateId; + FString TemplateTitle; + FString UserId; + FString FocusDeckId; + FString SourceReviewPlanId; + EHyperTwistTrainingSessionTemplateKind TemplateKind = + EHyperTwistTrainingSessionTemplateKind::Recovery; + EHyperTwistTrainingSessionState SessionState = + EHyperTwistTrainingSessionState::Created; + EHyperTwistTrainingDeliveryMode SuggestedMode = EHyperTwistTrainingDeliveryMode::Timer; + EHyperTwistTrainingSelectionPolicy SuggestedSelectionPolicy = + EHyperTwistTrainingSelectionPolicy::Weighted; + int32 RecommendedCaseCount = 0; + int32 FocusCaseCount = 0; + int32 CompletedAttemptCount = 0; + int32 TotalMistakes = 0; + float SuccessRate = 0.0f; + FString CompletedAtUtc; + bool bPreferReviewRun = false; + bool bCoachReview = false; + bool bPreferShortReviewSet = false; + + bool IsStructurallyValid() const + { + return !RecordedAtUtc.IsEmpty() + && !SourceSessionId.IsEmpty() + && !TemplateId.IsEmpty(); + } +}; + struct FHyperTwistCoachDashboardMethodDrillFollowUpHistoryEntry { FString RecordedAtUtc; @@ -560,6 +594,9 @@ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") int32 SelectedHandoffHistoryIndex = INDEX_NONE; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") + int32 SelectedTemplateLaunchHistoryIndex = INDEX_NONE; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") int32 SelectedQueueSuppressionIndex = INDEX_NONE; @@ -671,6 +708,12 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") bool SelectNextHandoffHistoryEntry(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + bool SelectPreviousTemplateLaunchHistoryEntry(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + bool SelectNextTemplateLaunchHistoryEntry(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") bool SelectPreviousQueueSuppressionHistoryEntry(); @@ -839,6 +882,15 @@ protected: UPROPERTY(Transient) TObjectPtr TemplateLaunchDetailTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr TemplateLaunchHistoryHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr TemplateLaunchHistoryStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr TemplateLaunchHistoryDetailTextBlock = nullptr; + UPROPERTY(Transient) TObjectPtr HandoffHistoryHeaderTextBlock = nullptr; @@ -860,6 +912,18 @@ protected: UPROPERTY(Transient) TObjectPtr NextHandoffHistoryButtonLabel = nullptr; + UPROPERTY(Transient) + TObjectPtr PreviousTemplateLaunchHistoryButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr PreviousTemplateLaunchHistoryButtonLabel = nullptr; + + UPROPERTY(Transient) + TObjectPtr NextTemplateLaunchHistoryButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr NextTemplateLaunchHistoryButtonLabel = nullptr; + UPROPERTY(Transient) TObjectPtr GuidanceReviewHeaderTextBlock = nullptr; @@ -1409,6 +1473,12 @@ protected: UFUNCTION() void HandleNextHandoffHistoryClicked(); + UFUNCTION() + void HandlePreviousTemplateLaunchHistoryClicked(); + + UFUNCTION() + void HandleNextTemplateLaunchHistoryClicked(); + UFUNCTION() void HandlePreviousQueueSuppressionClicked(); @@ -1495,6 +1565,7 @@ private: void SyncSelectedGuidancePreviewCase(); void SyncSelectedGuidanceDecisionHistoryEntry(); void SyncSelectedHandoffHistoryEntry(); + void SyncSelectedTemplateLaunchHistoryEntry(); void SyncSelectedQueueSuppressionHistoryEntry(); void SyncSelectedClosureRecoveryHistoryEntry(); void SyncSelectedMethodDrillFollowUpHistoryEntry(); @@ -1578,6 +1649,7 @@ private: ); void RecordQueueSuppressionSnapshot(); void RecordClosureRecoverySnapshot(); + void RecordTemplateLaunchHistoryEntry(const FHyperTwistTrainingSessionSummary& CompletedRunRecap); void RecordMethodDrillFollowUpHistoryEntry(const FHyperTwistTrainingSessionSummary& CompletedRunRecap); void RecordMethodDrillRecoveryMemoryHistorySnapshot( const FHyperTwistCoachDashboardQueueRecoveryWeighting& QueueRecoveryWeighting, @@ -1603,6 +1675,10 @@ private: bool bSingleCaseLaunch, const FString& SelectedCaseId ); + void CaptureActiveTemplateLaunchContext( + const FString& SessionId, + const FHyperTwistTrainingSessionTemplate& SessionTemplate + ); void CaptureActiveMethodDrillFollowUpLaunchContext(const FString& SessionId); bool IsLiveAttemptInspectionPhase() const; int32 GetMaxManualSplitCaptureCount() const; @@ -1619,6 +1695,11 @@ private: FString RetainedRunRecapUserId; FString RetainedRunRecapSessionId; EHyperTwistTrainingDeliveryMode RetainedRunRecapMode = EHyperTwistTrainingDeliveryMode::Timer; + FString ActiveTemplateLaunchSessionId; + FHyperTwistTrainingSessionTemplate ActiveTemplateLaunchTemplate; + bool bHasRetainedTemplateLaunch = false; + FString RetainedTemplateLaunchSessionId; + FHyperTwistTrainingSessionTemplate RetainedTemplateLaunchTemplate; FString ActiveGuidanceLaunchSessionId; EHyperTwistCoachDashboardGuidancePreviewLane ActiveGuidanceLaunchLane = EHyperTwistCoachDashboardGuidancePreviewLane::None; @@ -1642,6 +1723,7 @@ private: FString RetainedGuidanceComparisonSelectedCaseId; TArray GuidanceDecisionHistoryEntries; TArray HandoffHistoryEntries; + TArray TemplateLaunchHistoryEntries; TArray MethodDrillFollowUpHistoryEntries; TArray MethodDrillRecoveryMemoryHistoryEntries; diff --git a/docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_PROVENANCE_PACKET_2026-05-06.md b/docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_PROVENANCE_PACKET_2026-05-06.md new file mode 100644 index 0000000..631902f --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE4_TEMPLATE_LAUNCH_PROVENANCE_PACKET_2026-05-06.md @@ -0,0 +1,99 @@ +# HyperTwist Phase 4 template launch provenance packet + +Created on `2026-05-06` + +Status: + +- first-party HyperTwist packet +- bounded Phase `4` dashboard template provenance slice + +## Purpose + +This packet captures and surfaces template-launch provenance for completed runs started through the dashboard template lane. + +The open tasks were: + +- remember which active session template launched a completed dashboard run +- retain the template `TemplateId`, `TemplateKind`, and `SourceReviewPlanId` alongside the run recap +- add a first-party dashboard history surface so repeated template launches can be audited + +It is not: + +- a repository schema migration packet +- a template browser or template editing packet +- a new run-restart or replay semantics packet +- a broader dashboard history redesign + +## Scope + +Bounded lane: + +- capture active template launch context when `StartPreferredActiveSessionTemplateRun()` succeeds +- carry that context through `SyncRetainedRunRecap()` when the run completes or aborts +- record retained template-launch history snapshots with run outcome fields +- append template provenance to the run recap display when the displayed recap came from a template-backed launch +- add a dedicated `[Template Launch History]` section with simple previous/next navigation + +Out of scope: + +- changing repository persistence for run summaries +- altering template selection priority +- changing method-drill follow-up weighting logic +- changing coach handoff recommendation rules + +## Why this was the right next packet + +The previous packet made the template-backed launch lane visible and clickable from the first-party dashboard. + +That still left one practical gap: + +- once a template-backed run finished, the recap no longer explained which template produced it +- repeated template launches had no retained audit trail on the dashboard itself +- review-preferring template outcomes could not be compared from the same first-party surface that launched them + +The next honest move was therefore: + +- capture template provenance at launch time +- retain it only through the dashboard recap/history path +- and keep the change narrowly scoped to first-party dashboard state and presentation + +## What landed + +Primary code changes: + +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h` + - added retained template-launch history entry shape and selected-history index + - added active/retained template launch context members + - added dashboard text/button members and handlers for a new template-launch history section +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp` + - captures template launch context when the preferred dashboard template run starts + - records template launch provenance when the matching run recap completes or aborts + - retains up to sixteen template launch history snapshots + - appends template provenance into the displayed run recap when applicable + - adds a `[Template Launch History]` status/detail section with previous/next navigation + +## Product effect + +The first-party dashboard now keeps template-backed run provenance attached to the run outcome: + +- run recap shows the template id, template kind, and source review-plan id for matching template launches +- the dashboard retains a small history of completed template-backed runs and their outcome summaries +- operators can page through recent template launches without leaving the dashboard +- review-preferring template runs are now auditable from launch through recap on the same first-party surface + +## Acceptance criteria + +- successful dashboard template launches capture template provenance +- completed or aborted template-backed runs retain that provenance in recap state +- dashboard shows template provenance in run recap when the displayed run came from a template launch +- dashboard exposes retained template-launch history with previous/next navigation +- full product build succeeds + +## Validation checklist + +1. build `UnrealHyperTwist.sln` / `UnrealHyperTwistEditor` +2. launch a run through the dashboard template button +3. complete or abort the run and confirm run recap shows template provenance +4. confirm the dashboard `[Template Launch History]` section records the completed run + +That is the packet.