diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 12846f4..8953a19 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -687,6 +687,7 @@ void UHyperTwistCoachDashboardWidget::RefreshCoachDashboardView() SyncSelectedHandoffHistoryEntry(); SyncSelectedQueueSuppressionHistoryEntry(); SyncSelectedClosureRecoveryHistoryEntry(); + SyncSelectedMethodDrillFollowUpHistoryEntry(); SyncPreferredGuidancePreviewLane(false); UpdateDashboardPresentation(); } @@ -1689,6 +1690,47 @@ bool UHyperTwistCoachDashboardWidget::SelectNextClosureRecoveryHistoryEntry() return true; } +bool UHyperTwistCoachDashboardWidget::SelectPreviousMethodDrillFollowUpHistoryEntry() +{ + if (MethodDrillFollowUpHistoryEntries.Num() <= 0) + { + SelectedMethodDrillFollowUpHistoryIndex = INDEX_NONE; + return false; + } + + if (SelectedMethodDrillFollowUpHistoryIndex == INDEX_NONE) + { + SyncSelectedMethodDrillFollowUpHistoryEntry(); + return SelectedMethodDrillFollowUpHistoryIndex != INDEX_NONE; + } + + SelectedMethodDrillFollowUpHistoryIndex = + (SelectedMethodDrillFollowUpHistoryIndex - 1 + MethodDrillFollowUpHistoryEntries.Num()) + % MethodDrillFollowUpHistoryEntries.Num(); + UpdateDashboardPresentation(); + return true; +} + +bool UHyperTwistCoachDashboardWidget::SelectNextMethodDrillFollowUpHistoryEntry() +{ + if (MethodDrillFollowUpHistoryEntries.Num() <= 0) + { + SelectedMethodDrillFollowUpHistoryIndex = INDEX_NONE; + return false; + } + + if (SelectedMethodDrillFollowUpHistoryIndex == INDEX_NONE) + { + SyncSelectedMethodDrillFollowUpHistoryEntry(); + return SelectedMethodDrillFollowUpHistoryIndex != INDEX_NONE; + } + + SelectedMethodDrillFollowUpHistoryIndex = + (SelectedMethodDrillFollowUpHistoryIndex + 1) % MethodDrillFollowUpHistoryEntries.Num(); + UpdateDashboardPresentation(); + return true; +} + bool UHyperTwistCoachDashboardWidget::StartLiveAttemptTimer() { if (!CachedRunState.Session.IsStructurallyValid() || !CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid()) @@ -1954,7 +1996,12 @@ void UHyperTwistCoachDashboardWidget::HandleStartPreviewCaseClicked() void UHyperTwistCoachDashboardWidget::HandleStartMethodDrillFollowUpClicked() { - StartActiveMethodDrillFollowUpRun(DefaultSessionId + TEXT("_method_drill_follow_up")); + const FHyperTwistTrainingRunState RunState = + StartActiveMethodDrillFollowUpRun(DefaultSessionId + TEXT("_method_drill_follow_up")); + if (RunState.Session.IsStructurallyValid()) + { + CaptureActiveMethodDrillFollowUpLaunchContext(RunState.Session.TrainingSessionId); + } SyncRetainedRunRecap(); SyncSelectedQueueEntry(); SyncSelectedAttempt(); @@ -2020,6 +2067,16 @@ void UHyperTwistCoachDashboardWidget::HandleNextClosureRecoveryClicked() SelectNextClosureRecoveryHistoryEntry(); } +void UHyperTwistCoachDashboardWidget::HandlePreviousMethodDrillFollowUpHistoryClicked() +{ + SelectPreviousMethodDrillFollowUpHistoryEntry(); +} + +void UHyperTwistCoachDashboardWidget::HandleNextMethodDrillFollowUpHistoryClicked() +{ + SelectNextMethodDrillFollowUpHistoryEntry(); +} + void UHyperTwistCoachDashboardWidget::HandleClearRunClicked() { ClearActiveTrainingRun(); @@ -2463,6 +2520,57 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() ); StartMethodDrillFollowUpButtonLabel = StartMethodDrillFollowUpLabelRaw; } + MethodDrillFollowUpHistoryHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachMethodDrillFollowUpHistoryHeader"), + 2 + ); + MethodDrillFollowUpHistoryStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachMethodDrillFollowUpHistoryStatus"), + 2 + ); + MethodDrillFollowUpHistoryDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachMethodDrillFollowUpHistoryDetail"), + 8 + ); + UHorizontalBox* MethodDrillHistoryNavRow = WidgetTree->ConstructWidget( + UHorizontalBox::StaticClass(), + TEXT("CoachDashboardMethodDrillHistoryNavRow") + ); + if (MethodDrillHistoryNavRow != nullptr) + { + if (UVerticalBoxSlot* MethodDrillHistoryNavRowSlot = RootLayout->AddChildToVerticalBox(MethodDrillHistoryNavRow)) + { + MethodDrillHistoryNavRowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 0.0f)); + } + + UTextBlock* PreviousMethodDrillHistoryLabelRaw = nullptr; + PreviousMethodDrillFollowUpHistoryButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + MethodDrillHistoryNavRow, + TEXT("PreviousMethodDrillFollowUpHistoryButton"), + TEXT("PreviousMethodDrillFollowUpHistoryButtonLabel"), + TEXT("Prev Drill Follow-Up"), + PreviousMethodDrillHistoryLabelRaw + ); + PreviousMethodDrillFollowUpHistoryButtonLabel = PreviousMethodDrillHistoryLabelRaw; + + UTextBlock* NextMethodDrillHistoryLabelRaw = nullptr; + NextMethodDrillFollowUpHistoryButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + MethodDrillHistoryNavRow, + TEXT("NextMethodDrillFollowUpHistoryButton"), + TEXT("NextMethodDrillFollowUpHistoryButtonLabel"), + TEXT("Next Drill Follow-Up"), + NextMethodDrillHistoryLabelRaw + ); + NextMethodDrillFollowUpHistoryButtonLabel = NextMethodDrillHistoryLabelRaw; + } UHorizontalBox* RunRecapActionRow = WidgetTree->ConstructWidget( UHorizontalBox::StaticClass(), TEXT("CoachDashboardRunRecapActions") @@ -3464,6 +3572,20 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() &UHyperTwistCoachDashboardWidget::HandleStartMethodDrillFollowUpClicked ); } + if (PreviousMethodDrillFollowUpHistoryButton != nullptr) + { + PreviousMethodDrillFollowUpHistoryButton->OnClicked.AddDynamic( + this, + &UHyperTwistCoachDashboardWidget::HandlePreviousMethodDrillFollowUpHistoryClicked + ); + } + if (NextMethodDrillFollowUpHistoryButton != nullptr) + { + NextMethodDrillFollowUpHistoryButton->OnClicked.AddDynamic( + this, + &UHyperTwistCoachDashboardWidget::HandleNextMethodDrillFollowUpHistoryClicked + ); + } if (CompleteRunButton != nullptr) { CompleteRunButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleCompleteRunClicked); @@ -3641,6 +3763,16 @@ void UHyperTwistCoachDashboardWidget::SyncRetainedRunRecap() RetainedGuidanceComparisonDeck = ActiveGuidanceLaunchDeck; RetainedGuidanceComparisonSelectedCaseId = ActiveGuidanceLaunchSelectedCaseId; } + if (!ActiveMethodDrillFollowUpLaunchSessionId.IsEmpty() + && ActiveMethodDrillFollowUpLaunchSessionId == RetainedRunRecapSessionId + && ActiveMethodDrillFollowUpLaunchDiagnosticPacket.IsStructurallyValid()) + { + RecordMethodDrillFollowUpHistoryEntry(RetainedRunRecapSummary); + ActiveMethodDrillFollowUpLaunchSessionId.Reset(); + ActiveMethodDrillFollowUpLaunchDiagnosticPacket = + FHyperTwistTrainingMethodDrillDiagnosticPacket(); + ActiveMethodDrillFollowUpLaunchTemplate = FHyperTwistTrainingSessionTemplate(); + } } } @@ -3720,6 +3852,21 @@ void UHyperTwistCoachDashboardWidget::SyncSelectedClosureRecoveryHistoryEntry() } } +void UHyperTwistCoachDashboardWidget::SyncSelectedMethodDrillFollowUpHistoryEntry() +{ + if (MethodDrillFollowUpHistoryEntries.Num() <= 0) + { + SelectedMethodDrillFollowUpHistoryIndex = INDEX_NONE; + return; + } + + if (SelectedMethodDrillFollowUpHistoryIndex < 0 + || SelectedMethodDrillFollowUpHistoryIndex >= MethodDrillFollowUpHistoryEntries.Num()) + { + SelectedMethodDrillFollowUpHistoryIndex = MethodDrillFollowUpHistoryEntries.Num() - 1; + } +} + void UHyperTwistCoachDashboardWidget::SyncPreferredGuidancePreviewLane(const bool bForceAdoptPreferredLane) { const FHyperTwistCoachDashboardGuidanceLanePreference Preference = BuildGuidanceLanePreference(); @@ -3793,6 +3940,15 @@ void UHyperTwistCoachDashboardWidget::CaptureActiveGuidanceLaunchContext( } } +void UHyperTwistCoachDashboardWidget::CaptureActiveMethodDrillFollowUpLaunchContext( + const FString& SessionId +) +{ + ActiveMethodDrillFollowUpLaunchSessionId = SessionId; + ActiveMethodDrillFollowUpLaunchDiagnosticPacket = CachedMethodDrillDiagnosticPacket; + ActiveMethodDrillFollowUpLaunchTemplate = CachedMethodDrillFollowUpTemplate; +} + FHyperTwistTrainingDeck UHyperTwistCoachDashboardWidget::BuildSelectedGuidancePreviewCaseDeck() const { const FHyperTwistTrainingDeck* Deck = GetActiveGuidancePreviewDeck(); @@ -4522,6 +4678,74 @@ FHyperTwistCoachDashboardQueueRecoveryWeighting UHyperTwistCoachDashboardWidget: return Weighting; } +FHyperTwistCoachDashboardMethodDrillFollowUpWeighting +UHyperTwistCoachDashboardWidget::BuildMethodDrillFollowUpWeighting() const +{ + FHyperTwistCoachDashboardMethodDrillFollowUpWeighting Weighting; + if (!CachedMethodDrillDiagnosticPacket.IsStructurallyValid()) + { + return Weighting; + } + + constexpr int32 MaxMatchedHistoryEntries = 6; + for (int32 Index = MethodDrillFollowUpHistoryEntries.Num() - 1; + Index >= 0 && Weighting.MatchingHistoryCount < MaxMatchedHistoryEntries; + --Index) + { + const FHyperTwistCoachDashboardMethodDrillFollowUpHistoryEntry& Entry = + MethodDrillFollowUpHistoryEntries[Index]; + if (Entry.DiagnosticState != CachedMethodDrillDiagnosticPacket.DiagnosticState + || Entry.RecommendedReviewGrade + != CachedMethodDrillDiagnosticPacket.RecommendedReviewGrade) + { + continue; + } + + ++Weighting.MatchingHistoryCount; + Weighting.AverageSuccessRate += Entry.SuccessRate; + Weighting.AverageMistakes += static_cast(Entry.TotalMistakes); + + const bool bStrongOutcome = + Entry.SuccessRate >= 0.72f + && Entry.TotalMistakes <= FMath::Max(1, Entry.CompletedAttemptCount); + const bool bWeakOutcome = + Entry.SuccessRate < 0.50f + || Entry.TotalMistakes > FMath::Max(2, Entry.CompletedAttemptCount * 2); + + if (bStrongOutcome) + { + ++Weighting.StrongOutcomeCount; + Weighting.ContinueNarrowBias += 2; + } + if (Entry.bBoundaryPending || Entry.bBoundaryRepeatBurden) + { + ++Weighting.ContinueNarrowBias; + } + if (bWeakOutcome) + { + ++Weighting.WeakOutcomeCount; + Weighting.RedirectCoachBias += 2; + } + if (!Entry.bBoundaryPending && Entry.SuccessRate < 0.65f) + { + ++Weighting.RedirectCoachBias; + } + } + + if (Weighting.MatchingHistoryCount <= 0) + { + return Weighting; + } + + Weighting.AverageSuccessRate /= static_cast(Weighting.MatchingHistoryCount); + Weighting.AverageMistakes /= static_cast(Weighting.MatchingHistoryCount); + Weighting.bPreferDrillFollowUp = + Weighting.ContinueNarrowBias >= Weighting.RedirectCoachBias + 2; + Weighting.bPreferCoachHandoff = + Weighting.RedirectCoachBias >= Weighting.ContinueNarrowBias + 2; + return Weighting; +} + FString UHyperTwistCoachDashboardWidget::BuildGuidanceOutcomeActionLabel( const EHyperTwistCoachDashboardOutcomeAction Action) const { @@ -4941,6 +5165,71 @@ void UHyperTwistCoachDashboardWidget::RecordClosureRecoverySnapshot() SelectedClosureRecoveryIndex = ClosureRecoveryHistoryEntries.Num() - 1; } +void UHyperTwistCoachDashboardWidget::RecordMethodDrillFollowUpHistoryEntry( + const FHyperTwistTrainingSessionSummary& CompletedRunRecap +) +{ + if (CompletedRunRecap.TrainingSessionId.IsEmpty() + || !ActiveMethodDrillFollowUpLaunchDiagnosticPacket.IsStructurallyValid() + || !ActiveMethodDrillFollowUpLaunchTemplate.IsStructurallyValid()) + { + return; + } + + FHyperTwistCoachDashboardMethodDrillFollowUpHistoryEntry Entry; + Entry.RecordedAtUtc = FDateTime::UtcNow().ToIso8601(); + Entry.SourceRunId = ActiveMethodDrillFollowUpLaunchDiagnosticPacket.RunId; + Entry.SourceDrillId = ActiveMethodDrillFollowUpLaunchDiagnosticPacket.DrillId; + Entry.SourceSessionId = CompletedRunRecap.TrainingSessionId; + Entry.StageLabel = ActiveMethodDrillFollowUpLaunchDiagnosticPacket.StageLabel; + Entry.DiagnosticState = ActiveMethodDrillFollowUpLaunchDiagnosticPacket.DiagnosticState; + Entry.RecommendedReviewGrade = + ActiveMethodDrillFollowUpLaunchDiagnosticPacket.RecommendedReviewGrade; + Entry.TemplateKind = ActiveMethodDrillFollowUpLaunchTemplate.TemplateKind; + Entry.SuggestedMode = ActiveMethodDrillFollowUpLaunchTemplate.SuggestedMode; + Entry.SuggestedSelectionPolicy = + ActiveMethodDrillFollowUpLaunchTemplate.SuggestedSelectionPolicy; + Entry.FocusCaseCount = ActiveMethodDrillFollowUpLaunchDiagnosticPacket.FocusCaseIds.Num(); + Entry.LaunchedCaseCount = CompletedRunRecap.CompletedAttemptCount > 0 + ? FMath::Max(CompletedRunRecap.CompletedAttemptCount, 1) + : ActiveMethodDrillFollowUpLaunchTemplate.RecommendedCaseCount; + Entry.CompletedAttemptCount = CompletedRunRecap.CompletedAttemptCount; + Entry.TotalMistakes = CompletedRunRecap.TotalMistakes; + Entry.SuccessRate = CompletedRunRecap.SuccessRate; + Entry.bBoundaryPending = + ActiveMethodDrillFollowUpLaunchDiagnosticPacket.bBoundaryTriggeredByPendingCases; + Entry.bBoundaryRepeatBurden = + ActiveMethodDrillFollowUpLaunchDiagnosticPacket.bBoundaryTriggeredByRepeatBurden; + Entry.bBoundaryAssistance = + ActiveMethodDrillFollowUpLaunchDiagnosticPacket.bBoundaryTriggeredByAssistance; + if (!Entry.IsStructurallyValid()) + { + return; + } + + const bool bAlreadyRecorded = MethodDrillFollowUpHistoryEntries.ContainsByPredicate( + [&Entry](const FHyperTwistCoachDashboardMethodDrillFollowUpHistoryEntry& Candidate) + { + return Candidate.SourceRunId == Entry.SourceRunId + && Candidate.SourceSessionId == Entry.SourceSessionId; + } + ); + if (bAlreadyRecorded) + { + return; + } + + MethodDrillFollowUpHistoryEntries.Add(Entry); + constexpr int32 MaxRetainedMethodDrillFollowUpHistoryEntries = 16; + if (MethodDrillFollowUpHistoryEntries.Num() > MaxRetainedMethodDrillFollowUpHistoryEntries) + { + const int32 ExcessCount = + MethodDrillFollowUpHistoryEntries.Num() - MaxRetainedMethodDrillFollowUpHistoryEntries; + MethodDrillFollowUpHistoryEntries.RemoveAt(0, ExcessCount, EAllowShrinking::No); + } + SelectedMethodDrillFollowUpHistoryIndex = MethodDrillFollowUpHistoryEntries.Num() - 1; +} + void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { const bool bHasActiveRun = CachedRunState.Session.IsStructurallyValid() @@ -5294,6 +5583,80 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { StartMethodDrillFollowUpLabel = TEXT("No Drill Follow-Up"); } + const FHyperTwistCoachDashboardMethodDrillFollowUpWeighting MethodDrillFollowUpWeighting = + BuildMethodDrillFollowUpWeighting(); + const FHyperTwistCoachDashboardMethodDrillFollowUpHistoryEntry* SelectedMethodDrillFollowUpHistoryEntry = + SelectedMethodDrillFollowUpHistoryIndex >= 0 + && SelectedMethodDrillFollowUpHistoryIndex < MethodDrillFollowUpHistoryEntries.Num() + ? &MethodDrillFollowUpHistoryEntries[SelectedMethodDrillFollowUpHistoryIndex] + : nullptr; + const FString MethodDrillOutcomePreferenceLine = !MethodDrillFollowUpWeighting.IsStructurallyValid() + ? TEXT("no retained drill follow-up history yet") + : (MethodDrillFollowUpWeighting.bPreferDrillFollowUp + ? TEXT("retained drill history favors staying narrow with drill follow-up") + : (MethodDrillFollowUpWeighting.bPreferCoachHandoff + ? TEXT("retained drill history favors handing control back to broader coach guidance") + : TEXT("retained drill history is balanced"))); + const FString MethodDrillFollowUpHistoryStatusLine = + SelectedMethodDrillFollowUpHistoryEntry != nullptr + ? FString::Printf( + TEXT("Drill follow-up history: %d entries | selected %d/%d | Stage: %s | review %s | Recorded: %s"), + MethodDrillFollowUpHistoryEntries.Num(), + SelectedMethodDrillFollowUpHistoryIndex + 1, + MethodDrillFollowUpHistoryEntries.Num(), + SelectedMethodDrillFollowUpHistoryEntry->StageLabel.IsEmpty() + ? TEXT("n/a") + : *SelectedMethodDrillFollowUpHistoryEntry->StageLabel, + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + SelectedMethodDrillFollowUpHistoryEntry->RecommendedReviewGrade), + *SelectedMethodDrillFollowUpHistoryEntry->RecordedAtUtc) + : TEXT("Drill follow-up history: no retained drill follow-up outcomes recorded yet."); + const FString MethodDrillFollowUpHistoryDetailLine = + SelectedMethodDrillFollowUpHistoryEntry != nullptr + ? FString::Printf( + TEXT("State: %s | template: %s | mode: %s | selection: %s | focus %d | launched %d | success %.2f | attempts %d | mistakes %d | pending: %s | repeat burden: %s | assistance: %s"), + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + SelectedMethodDrillFollowUpHistoryEntry->DiagnosticState), + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + SelectedMethodDrillFollowUpHistoryEntry->TemplateKind), + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + SelectedMethodDrillFollowUpHistoryEntry->SuggestedMode), + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + SelectedMethodDrillFollowUpHistoryEntry->SuggestedSelectionPolicy), + SelectedMethodDrillFollowUpHistoryEntry->FocusCaseCount, + SelectedMethodDrillFollowUpHistoryEntry->LaunchedCaseCount, + SelectedMethodDrillFollowUpHistoryEntry->SuccessRate, + SelectedMethodDrillFollowUpHistoryEntry->CompletedAttemptCount, + SelectedMethodDrillFollowUpHistoryEntry->TotalMistakes, + SelectedMethodDrillFollowUpHistoryEntry->bBoundaryPending ? TEXT("yes") : TEXT("no"), + SelectedMethodDrillFollowUpHistoryEntry->bBoundaryRepeatBurden ? TEXT("yes") : TEXT("no"), + SelectedMethodDrillFollowUpHistoryEntry->bBoundaryAssistance ? TEXT("yes") : TEXT("no")) + : TEXT("Drill follow-up history detail: once drill-derived follow-up runs complete, their recap outcomes will accumulate here and feed later drill-vs-handoff judgment."); + if (MethodDrillFollowUpWeighting.IsStructurallyValid()) + { + MethodDrillDiagnosticsStatusLine = FString::Printf( + TEXT("%s | history: %s"), + *MethodDrillDiagnosticsStatusLine, + *MethodDrillOutcomePreferenceLine + ); + MethodDrillDiagnosticsDetailLine = FString::Printf( + TEXT("%s || History match %d | strong %d | weak %d | avg success %.2f | avg mistakes %.2f"), + *MethodDrillDiagnosticsDetailLine, + MethodDrillFollowUpWeighting.MatchingHistoryCount, + MethodDrillFollowUpWeighting.StrongOutcomeCount, + MethodDrillFollowUpWeighting.WeakOutcomeCount, + MethodDrillFollowUpWeighting.AverageSuccessRate, + MethodDrillFollowUpWeighting.AverageMistakes + ); + } + if (bCanLaunchMethodDrillFollowUp && MethodDrillFollowUpWeighting.bPreferDrillFollowUp) + { + StartMethodDrillFollowUpLabel = TEXT("Launch Preferred Drill Follow-Up"); + } + else if (bCanLaunchMethodDrillFollowUp && MethodDrillFollowUpWeighting.bPreferCoachHandoff) + { + StartMethodDrillFollowUpLabel = TEXT("Drill Follow-Up Optional"); + } const bool bHasCurrentRunRecap = bHasActiveRun && (CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Completed || CachedRunState.Session.SessionState == EHyperTwistTrainingSessionState::Aborted); @@ -5716,6 +6079,14 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() HandoffHistoryBias.FollowUpRecommendationCount, HandoffHistoryBias.RecommendedRecommendationCount ); + if (bCanLaunchMethodDrillFollowUp && MethodDrillFollowUpWeighting.IsStructurallyValid()) + { + CoachHandoffRecommendationDetailLine += MethodDrillFollowUpWeighting.bPreferDrillFollowUp + ? TEXT(" | Drill history says stay narrow with drill-derived follow-up before broader coach handoff.") + : (MethodDrillFollowUpWeighting.bPreferCoachHandoff + ? TEXT(" | Drill history says broader coach handoff is now stronger than another narrow drill follow-up.") + : TEXT(" | Drill history is balanced against broader coach handoff.")); + } } RecordCoachHandoffHistorySnapshot( HyperTwistCoachDashboardWidgetInternal::DescribeHandoffRecommendationKind( @@ -6897,6 +7268,41 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { StartMethodDrillFollowUpButton->SetIsEnabled(bCanLaunchMethodDrillFollowUp); } + if (MethodDrillFollowUpHistoryHeaderTextBlock != nullptr) + { + MethodDrillFollowUpHistoryHeaderTextBlock->SetText(FText::FromString( + TEXT("[Method Drill Follow-Up History]"))); + } + if (MethodDrillFollowUpHistoryStatusTextBlock != nullptr) + { + MethodDrillFollowUpHistoryStatusTextBlock->SetText(FText::FromString( + MethodDrillFollowUpHistoryStatusLine)); + } + if (MethodDrillFollowUpHistoryDetailTextBlock != nullptr) + { + MethodDrillFollowUpHistoryDetailTextBlock->SetText(FText::FromString( + MethodDrillFollowUpHistoryDetailLine)); + } + if (PreviousMethodDrillFollowUpHistoryButtonLabel != nullptr) + { + PreviousMethodDrillFollowUpHistoryButtonLabel->SetText(FText::FromString( + TEXT("Prev Drill Follow-Up"))); + } + if (PreviousMethodDrillFollowUpHistoryButton != nullptr) + { + PreviousMethodDrillFollowUpHistoryButton->SetIsEnabled( + MethodDrillFollowUpHistoryEntries.Num() > 1); + } + if (NextMethodDrillFollowUpHistoryButtonLabel != nullptr) + { + NextMethodDrillFollowUpHistoryButtonLabel->SetText(FText::FromString( + TEXT("Next Drill Follow-Up"))); + } + if (NextMethodDrillFollowUpHistoryButton != nullptr) + { + NextMethodDrillFollowUpHistoryButton->SetIsEnabled( + MethodDrillFollowUpHistoryEntries.Num() > 1); + } if (StartQueuedRunButtonLabel != nullptr) { StartQueuedRunButtonLabel->SetText(FText::FromString(QueuedRunLabel)); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 963491f..7f6176a 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -267,6 +267,57 @@ struct FHyperTwistCoachDashboardHandoffHistoryEntry } }; +struct FHyperTwistCoachDashboardMethodDrillFollowUpHistoryEntry +{ + FString RecordedAtUtc; + FString SourceRunId; + FString SourceDrillId; + FString SourceSessionId; + FString StageLabel; + EHyperTwistTrainingMethodDrillDiagnosticState DiagnosticState = + EHyperTwistTrainingMethodDrillDiagnosticState::Unknown; + EHyperTwistTrainingReviewGrade RecommendedReviewGrade = EHyperTwistTrainingReviewGrade::Good; + EHyperTwistTrainingSessionTemplateKind TemplateKind = + EHyperTwistTrainingSessionTemplateKind::ReviewPlan; + EHyperTwistTrainingDeliveryMode SuggestedMode = EHyperTwistTrainingDeliveryMode::Timer; + EHyperTwistTrainingSelectionPolicy SuggestedSelectionPolicy = + EHyperTwistTrainingSelectionPolicy::Weighted; + int32 FocusCaseCount = 0; + int32 LaunchedCaseCount = 0; + int32 CompletedAttemptCount = 0; + int32 TotalMistakes = 0; + float SuccessRate = 0.0f; + bool bBoundaryPending = false; + bool bBoundaryRepeatBurden = false; + bool bBoundaryAssistance = false; + + bool IsStructurallyValid() const + { + return !RecordedAtUtc.IsEmpty() + && !SourceRunId.IsEmpty() + && !SourceDrillId.IsEmpty() + && !SourceSessionId.IsEmpty(); + } +}; + +struct FHyperTwistCoachDashboardMethodDrillFollowUpWeighting +{ + int32 MatchingHistoryCount = 0; + int32 StrongOutcomeCount = 0; + int32 WeakOutcomeCount = 0; + int32 ContinueNarrowBias = 0; + int32 RedirectCoachBias = 0; + float AverageSuccessRate = 0.0f; + float AverageMistakes = 0.0f; + bool bPreferDrillFollowUp = false; + bool bPreferCoachHandoff = false; + + bool IsStructurallyValid() const + { + return MatchingHistoryCount > 0; + } +}; + UCLASS(BlueprintType, Blueprintable) class UNREALHYPERTWIST_API UHyperTwistCoachDashboardWidget : public UHyperTwistTrainingPanelWidget { @@ -318,6 +369,9 @@ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") int32 SelectedClosureRecoveryIndex = INDEX_NONE; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") + int32 SelectedMethodDrillFollowUpHistoryIndex = INDEX_NONE; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") bool bHasLiveAttemptTimer = false; @@ -423,6 +477,12 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") bool SelectNextClosureRecoveryHistoryEntry(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + bool SelectPreviousMethodDrillFollowUpHistoryEntry(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + bool SelectNextMethodDrillFollowUpHistoryEntry(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") bool StartLiveAttemptTimer(); @@ -612,6 +672,27 @@ protected: UPROPERTY(Transient) TObjectPtr StartMethodDrillFollowUpButtonLabel = nullptr; + UPROPERTY(Transient) + TObjectPtr MethodDrillFollowUpHistoryHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr MethodDrillFollowUpHistoryStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr MethodDrillFollowUpHistoryDetailTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr PreviousMethodDrillFollowUpHistoryButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr PreviousMethodDrillFollowUpHistoryButtonLabel = nullptr; + + UPROPERTY(Transient) + TObjectPtr NextMethodDrillFollowUpHistoryButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr NextMethodDrillFollowUpHistoryButtonLabel = nullptr; + UPROPERTY(Transient) TObjectPtr RepeatRunButton = nullptr; @@ -1050,6 +1131,12 @@ protected: UFUNCTION() void HandleNextClosureRecoveryClicked(); + UFUNCTION() + void HandlePreviousMethodDrillFollowUpHistoryClicked(); + + UFUNCTION() + void HandleNextMethodDrillFollowUpHistoryClicked(); + UFUNCTION() void HandleClearRunClicked(); @@ -1108,6 +1195,7 @@ private: void SyncSelectedHandoffHistoryEntry(); void SyncSelectedQueueSuppressionHistoryEntry(); void SyncSelectedClosureRecoveryHistoryEntry(); + void SyncSelectedMethodDrillFollowUpHistoryEntry(); void SyncPreferredGuidancePreviewLane(bool bForceAdoptPreferredLane); void UpdateDashboardPresentation(); void RefreshLiveAttemptClock(); @@ -1136,6 +1224,7 @@ private: ) const; FHyperTwistCoachDashboardGuidanceLanePreference BuildGuidanceLanePreference() const; FHyperTwistCoachDashboardQueueRecoveryWeighting BuildQueueRecoveryWeighting() const; + FHyperTwistCoachDashboardMethodDrillFollowUpWeighting BuildMethodDrillFollowUpWeighting() const; FString BuildGuidanceOutcomeActionLabel(EHyperTwistCoachDashboardOutcomeAction Action) const; FHyperTwistTrainingDeck BuildGuidanceOutcomeSelectedCaseDeck() const; void RecordQueueRecoveryOutcome( @@ -1164,6 +1253,7 @@ private: ); void RecordQueueSuppressionSnapshot(); void RecordClosureRecoverySnapshot(); + void RecordMethodDrillFollowUpHistoryEntry(const FHyperTwistTrainingSessionSummary& CompletedRunRecap); void RecordGuidanceOutcomeDecision( EHyperTwistCoachDashboardOutcomeAction RecommendedAction, EHyperTwistCoachDashboardOutcomeAction AppliedAction, @@ -1181,6 +1271,7 @@ private: bool bSingleCaseLaunch, const FString& SelectedCaseId ); + void CaptureActiveMethodDrillFollowUpLaunchContext(const FString& SessionId); bool IsLiveAttemptInspectionPhase() const; int32 GetMaxManualSplitCaptureCount() const; const FHyperTwistTrainingSplitPhaseDefinition* FindNextSplitPhaseToCapture() const; @@ -1203,6 +1294,9 @@ private: FHyperTwistCoachBrief ActiveGuidanceLaunchBrief; FHyperTwistTrainingDeck ActiveGuidanceLaunchDeck; FString ActiveGuidanceLaunchSelectedCaseId; + FString ActiveMethodDrillFollowUpLaunchSessionId; + FHyperTwistTrainingMethodDrillDiagnosticPacket ActiveMethodDrillFollowUpLaunchDiagnosticPacket; + FHyperTwistTrainingSessionTemplate ActiveMethodDrillFollowUpLaunchTemplate; bool bHasRetainedGuidanceComparison = false; FString RetainedGuidanceComparisonSessionId; EHyperTwistCoachDashboardGuidancePreviewLane RetainedGuidanceComparisonLane = @@ -1213,6 +1307,7 @@ private: FString RetainedGuidanceComparisonSelectedCaseId; TArray GuidanceDecisionHistoryEntries; TArray HandoffHistoryEntries; + TArray MethodDrillFollowUpHistoryEntries; bool bHasQueueRecoveryOutcome = false; FHyperTwistCoachDashboardQueueRecoveryOutcome QueueRecoveryOutcome; TArray QueueRecoveryOutcomeHistory;