From 840a8e5d0be775c200464cb3519261c4edca4ee0 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Sun, 26 Apr 2026 02:18:35 +0200 Subject: [PATCH] Add replay-driven coaching analytics and follow-ups --- .../HyperTwistContractLibrary.cpp | 104 +++ .../HyperTwistRecognitionReplayLibrary.cpp | 235 ++++++ .../HyperTwistReplayReviewLibrary.cpp | 344 ++++++++ .../HyperTwistTrainingCoachLibrary.cpp | 471 +++++++++++ .../HyperTwistTrainingRepositoryLibrary.cpp | 788 ++++++++++++++++++ .../HyperTwistTrainingRuntimeLibrary.cpp | 161 ++++ .../HyperTwistTrainingSubsystem.cpp | 337 ++++++++ .../HyperTwistContractLibrary.h | 27 + .../HyperTwistRecognitionReplayLibrary.h | 112 +++ .../HyperTwistReplayReviewLibrary.h | 151 ++++ .../HyperTwistTrainingCoachLibrary.h | 172 ++++ .../HyperTwistTrainingRepositoryLibrary.h | 55 ++ .../HyperTwistTrainingRuntimeLibrary.h | 60 ++ .../HyperTwistTrainingSubsystem.h | 75 ++ .../HyperTwistTrainingTypes.h | 338 +++++++- 15 files changed, 3429 insertions(+), 1 deletion(-) create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistReplay/HyperTwistReplayReviewLibrary.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.h create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistReplay/HyperTwistReplayReviewLibrary.h create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp index 612b309..dbd85c2 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp @@ -1,6 +1,9 @@ #include "HyperTwistBootstrap/HyperTwistContractLibrary.h" +#include "HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.h" +#include "HyperTwistReplay/HyperTwistReplayReviewLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingCatalogLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingCoachLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h" #include "JsonObjectConverter.h" @@ -616,6 +619,15 @@ FHyperTwistTrainingRepositoryState UHyperTwistContractLibrary::MakeSampleTrainin ); } + const FHyperTwistTrainingCoachActionPlan CoachActionPlan = MakeSampleTrainingCoachActionPlan(); + if (CoachActionPlan.IsStructurallyValid()) + { + RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachActionPlan( + RepositoryState, + CoachActionPlan + ); + } + return RepositoryState; } @@ -666,6 +678,14 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistContractLibrary::MakeSam } RepositoryState.StoredMethodDrillBatches.Add(FHyperTwistTrainingMethodDrillBatch()); + const FHyperTwistTrainingCoachActionPlan CoachActionPlan = MakeSampleTrainingCoachActionPlan(); + if (CoachActionPlan.IsStructurallyValid()) + { + RepositoryState.StoredCoachActionPlans.Add(CoachActionPlan); + RepositoryState.StoredCoachActionPlans.Add(CoachActionPlan); + } + RepositoryState.StoredCoachActionPlans.Add(FHyperTwistTrainingCoachActionPlan()); + return UHyperTwistTrainingRepositoryLibrary::DeriveRepositoryIntegrityReport(RepositoryState); } @@ -1018,6 +1038,90 @@ FHyperTwistTrainingReviewProgramSummary UHyperTwistContractLibrary::MakeSampleTr ); } +FHyperTwistReplayReviewAnalytics UHyperTwistContractLibrary::MakeSampleReplayReviewAnalytics() +{ + return UHyperTwistReplayReviewLibrary::DeriveReplayReviewAnalytics( + MakeSampleRuntimeReplayPacket() + ); +} + +FHyperTwistRecognitionReplaySummary UHyperTwistContractLibrary::MakeSampleRecognitionReplaySummary() +{ + return UHyperTwistRecognitionReplayLibrary::DeriveRecognitionReplaySummary( + MakeSampleRecognitionReplayPacket() + ); +} + +TArray UHyperTwistContractLibrary::MakeSampleTrainingCoachSignals() +{ + return UHyperTwistTrainingCoachLibrary::DeriveCoachSignals( + MakeSampleTrainingRunSummary(), + MakeSampleTrainingDeckScopedStats(), + MakeSampleTrainingLearnerDeckStateSummary(), + MakeSampleTrainingLearnerProfile(), + MakeSampleTrainingCoachMemorySnapshot(), + MakeSampleTrainingReviewProgramSummary(), + MakeSampleReplayReviewAnalytics(), + MakeSampleRecognitionReplaySummary() + ); +} + +FHyperTwistCoachBrief UHyperTwistContractLibrary::MakeSampleTrainingCoachBrief() +{ + return UHyperTwistTrainingCoachLibrary::DeriveCoachBrief( + MakeSampleTrainingRunSummary(), + MakeSampleTrainingDeckScopedStats(), + MakeSampleTrainingLearnerDeckStateSummary(), + MakeSampleTrainingLearnerProfile(), + MakeSampleTrainingCoachMemorySnapshot(), + MakeSampleTrainingReviewProgramSummary(), + MakeSampleReplayReviewAnalytics(), + MakeSampleRecognitionReplaySummary() + ); +} + +FHyperTwistTrainingCoachActionPlan UHyperTwistContractLibrary::MakeSampleTrainingCoachActionPlan() +{ + return UHyperTwistTrainingRepositoryLibrary::BuildCoachActionPlan( + MakeSampleTrainingCoachBrief(), + TEXT("coach_plan_training_session_01"), + TEXT("2026-04-28T12:05:00Z") + ); +} + +FHyperTwistTrainingCoachActionPlanSummary UHyperTwistContractLibrary::MakeSampleTrainingCoachActionPlanSummary() +{ + const FHyperTwistTrainingRunStepResult StepResult = MakeSampleTrainingRunStepResult(); + if (!StepResult.IsStructurallyValid()) + { + return FHyperTwistTrainingCoachActionPlanSummary(); + } + + return UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanSummary( + MakeSampleTrainingRepositoryState(), + StepResult.UpdatedRunState.Session.UserId, + TEXT("2026-04-28T12:05:00Z") + ); +} + +FHyperTwistTrainingCoachActionPlanOutcomeSummary UHyperTwistContractLibrary::MakeSampleTrainingCoachActionPlanOutcomeSummary() +{ + return UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanOutcomeSummary( + MakeSampleTrainingRepositoryState(), + MakeSampleTrainingCoachActionPlan(), + TEXT("2026-04-28T12:12:00Z") + ); +} + +FHyperTwistCoachBrief UHyperTwistContractLibrary::MakeSampleTrainingCoachFollowUpBrief() +{ + return UHyperTwistTrainingCoachLibrary::DeriveCoachFollowUpBrief( + MakeSampleTrainingCoachBrief(), + MakeSampleTrainingCoachActionPlan(), + MakeSampleTrainingCoachActionPlanOutcomeSummary() + ); +} + bool UHyperTwistContractLibrary::IsPuzzleStateStructurallyValid(const FHyperTwistPuzzleState& State) { return State.IsStructurallyValid(); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.cpp new file mode 100644 index 0000000..91555c3 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.cpp @@ -0,0 +1,235 @@ +#include "HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.h" + +#include "JsonObjectConverter.h" + +namespace HyperTwistRecognitionReplayLibraryInternal +{ + template + bool DeserializePayload(const FString& Json, TStruct& OutValue) + { + if (Json.IsEmpty()) + { + return false; + } + + return FJsonObjectConverter::JsonObjectStringToUStruct(Json, &OutValue, 0, 0); + } + + FString ResolveRecognitionStage(const FHyperTwistReplayRecognitionPayload& Payload) + { + if (!Payload.ScanStage.IsEmpty()) + { + return Payload.ScanStage; + } + + return Payload.CommittedFaceOrStage; + } + + void StartEpisodeFromEvent( + FHyperTwistRecognitionReplayEpisode& Episode, + const FHyperTwistReplayEvent& Event, + const FHyperTwistReplayRecognitionPayload& Payload, + int32 EpisodeOrdinal) + { + Episode = FHyperTwistRecognitionReplayEpisode(); + Episode.EpisodeId = FString::Printf(TEXT("rec_ep_%03d"), EpisodeOrdinal); + Episode.Stage = ResolveRecognitionStage(Payload); + Episode.StartedAtMs = Event.TimeMs; + } + + void FoldConfidence(FHyperTwistRecognitionReplayEpisode& Episode, const float Confidence, int32& InOutConfidenceCount, float& InOutConfidenceSum) + { + InOutConfidenceSum += Confidence; + ++InOutConfidenceCount; + + if (Episode.PeakConfidence < 0.0f) + { + Episode.PeakConfidence = Confidence; + } + else + { + Episode.PeakConfidence = FMath::Max(Episode.PeakConfidence, Confidence); + } + } + + void FinalizeEpisode( + FHyperTwistRecognitionReplayEpisode& Episode, + const int32 ClosedAtMs, + const bool bCompleted, + TArray& OutEpisodes) + { + Episode.CompletedAtMs = ClosedAtMs; + Episode.DurationMs = FMath::Max(0, Episode.CompletedAtMs - Episode.StartedAtMs); + Episode.bCompleted = bCompleted; + Episode.bAmbiguous = Episode.CorrectionCount > 0 || !Episode.bCompleted; + OutEpisodes.Add(Episode); + Episode = FHyperTwistRecognitionReplayEpisode(); + } +} + +TArray UHyperTwistRecognitionReplayLibrary::CollectRecognitionReplayEpisodes(const FHyperTwistReplayPacket& Packet) +{ + TArray Episodes; + FHyperTwistRecognitionReplayEpisode OpenEpisode; + bool bHasOpenEpisode = false; + int32 EpisodeOrdinal = 1; + int32 ConfidenceCount = 0; + float ConfidenceSum = 0.0f; + + for (const FHyperTwistReplayEvent& Event : Packet.Events) + { + switch (Event.EventType) + { + case EHyperTwistReplayEventType::RecognitionPreview: + case EHyperTwistReplayEventType::RecognitionScan: + case EHyperTwistReplayEventType::RecognitionCommit: + case EHyperTwistReplayEventType::RecognitionCorrection: + { + FHyperTwistReplayRecognitionPayload Payload; + if (!HyperTwistRecognitionReplayLibraryInternal::DeserializePayload(Event.PayloadJson, Payload)) + { + break; + } + + const FString EventStage = HyperTwistRecognitionReplayLibraryInternal::ResolveRecognitionStage(Payload); + if (!bHasOpenEpisode) + { + HyperTwistRecognitionReplayLibraryInternal::StartEpisodeFromEvent(OpenEpisode, Event, Payload, EpisodeOrdinal++); + bHasOpenEpisode = true; + ConfidenceCount = 0; + ConfidenceSum = 0.0f; + } + else if (!EventStage.IsEmpty() && !OpenEpisode.Stage.IsEmpty() && OpenEpisode.Stage != EventStage) + { + OpenEpisode.AverageConfidence = ConfidenceCount > 0 ? ConfidenceSum / static_cast(ConfidenceCount) : -1.0f; + HyperTwistRecognitionReplayLibraryInternal::FinalizeEpisode(OpenEpisode, Event.TimeMs, false, Episodes); + HyperTwistRecognitionReplayLibraryInternal::StartEpisodeFromEvent(OpenEpisode, Event, Payload, EpisodeOrdinal++); + ConfidenceCount = 0; + ConfidenceSum = 0.0f; + } + else if (OpenEpisode.Stage.IsEmpty()) + { + OpenEpisode.Stage = EventStage; + } + + HyperTwistRecognitionReplayLibraryInternal::FoldConfidence(OpenEpisode, Payload.Confidence, ConfidenceCount, ConfidenceSum); + + if (Event.EventType == EHyperTwistReplayEventType::RecognitionPreview) + { + ++OpenEpisode.PreviewCount; + } + else if (Event.EventType == EHyperTwistReplayEventType::RecognitionScan) + { + ++OpenEpisode.ScanCount; + } + else if (Event.EventType == EHyperTwistReplayEventType::RecognitionCorrection) + { + ++OpenEpisode.CorrectionCount; + } + else + { + OpenEpisode.FinalCommitConfidence = Payload.Confidence; + OpenEpisode.FinalCommittedUnit = Payload.CommittedFaceOrStage; + OpenEpisode.AverageConfidence = ConfidenceCount > 0 ? ConfidenceSum / static_cast(ConfidenceCount) : -1.0f; + HyperTwistRecognitionReplayLibraryInternal::FinalizeEpisode(OpenEpisode, Event.TimeMs, true, Episodes); + bHasOpenEpisode = false; + ConfidenceCount = 0; + ConfidenceSum = 0.0f; + } + break; + } + + default: + break; + } + } + + if (bHasOpenEpisode) + { + const int32 ClosedAtMs = Packet.Events.Num() > 0 ? Packet.Events.Last().TimeMs : OpenEpisode.StartedAtMs; + OpenEpisode.AverageConfidence = ConfidenceCount > 0 ? ConfidenceSum / static_cast(ConfidenceCount) : -1.0f; + HyperTwistRecognitionReplayLibraryInternal::FinalizeEpisode(OpenEpisode, ClosedAtMs, false, Episodes); + } + + return Episodes; +} + +FHyperTwistRecognitionReplaySummary UHyperTwistRecognitionReplayLibrary::DeriveRecognitionReplaySummary(const FHyperTwistReplayPacket& Packet) +{ + FHyperTwistRecognitionReplaySummary Summary; + Summary.Episodes = CollectRecognitionReplayEpisodes(Packet); + Summary.EpisodeCount = Summary.Episodes.Num(); + + TSet CoveredStages; + int32 DurationSum = 0; + int32 CompletedDurationCount = 0; + float CommitConfidenceSum = 0.0f; + int32 CommitConfidenceCount = 0; + + for (const FHyperTwistReplayEvent& Event : Packet.Events) + { + switch (Event.EventType) + { + case EHyperTwistReplayEventType::RecognitionPreview: + ++Summary.TotalRecognitionEventCount; + ++Summary.TotalPreviewCount; + break; + + case EHyperTwistReplayEventType::RecognitionScan: + ++Summary.TotalRecognitionEventCount; + ++Summary.TotalScanCount; + break; + + case EHyperTwistReplayEventType::RecognitionCommit: + ++Summary.TotalRecognitionEventCount; + ++Summary.TotalCommitCount; + break; + + case EHyperTwistReplayEventType::RecognitionCorrection: + ++Summary.TotalRecognitionEventCount; + ++Summary.TotalCorrectionCount; + break; + + default: + break; + } + } + + for (const FHyperTwistRecognitionReplayEpisode& Episode : Summary.Episodes) + { + if (!Episode.Stage.IsEmpty()) + { + CoveredStages.Add(Episode.Stage); + } + + if (Episode.bCompleted) + { + ++Summary.CompletedEpisodeCount; + DurationSum += Episode.DurationMs; + ++CompletedDurationCount; + + if (Episode.FinalCommitConfidence >= 0.0f) + { + CommitConfidenceSum += Episode.FinalCommitConfidence; + ++CommitConfidenceCount; + } + + Summary.LastCommittedUnit = Episode.FinalCommittedUnit; + } + + if (Episode.bAmbiguous) + { + ++Summary.AmbiguousEpisodeCount; + } + } + + for (const FString& CoveredStage : CoveredStages) + { + Summary.CoveredStages.Add(CoveredStage); + } + Summary.CoveredStages.Sort(); + Summary.AverageEpisodeDurationMs = CompletedDurationCount > 0 ? DurationSum / CompletedDurationCount : 0; + Summary.AverageCommitConfidence = CommitConfidenceCount > 0 ? CommitConfidenceSum / static_cast(CommitConfidenceCount) : -1.0f; + + return Summary; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistReplay/HyperTwistReplayReviewLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistReplay/HyperTwistReplayReviewLibrary.cpp new file mode 100644 index 0000000..232005d --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistReplay/HyperTwistReplayReviewLibrary.cpp @@ -0,0 +1,344 @@ +#include "HyperTwistReplay/HyperTwistReplayReviewLibrary.h" + +#include "HyperTwistReplay/HyperTwistReplayLibrary.h" +#include "JsonObjectConverter.h" + +namespace HyperTwistReplayReviewLibraryInternal +{ + template + bool DeserializePayload(const FString& Json, TStruct& OutValue) + { + if (Json.IsEmpty()) + { + return false; + } + + return FJsonObjectConverter::JsonObjectStringToUStruct(Json, &OutValue, 0, 0); + } + + FHyperTwistReplayReviewAnchor MakeAnchor( + const FHyperTwistReplayEvent& Event, + const EHyperTwistReplayReviewAnchorKind AnchorKind, + const FString& Label) + { + FHyperTwistReplayReviewAnchor Anchor; + Anchor.AnchorKind = AnchorKind; + Anchor.EventId = Event.EventId; + Anchor.Sequence = Event.Sequence; + Anchor.TimeMs = Event.TimeMs; + Anchor.Label = Label; + return Anchor; + } + + void AddPhaseAnchor( + TArray& Anchors, + const FHyperTwistReplayEvent& Event, + const FHyperTwistReplayPhaseMarkPayload& PhasePayload) + { + const FString Action = PhasePayload.PhaseAction.ToLower(); + const bool bMarksCompletion = (Action == TEXT("end") || Action == TEXT("complete")); + const FString EffectiveAction = bMarksCompletion ? TEXT("complete") : TEXT("start"); + const FString EffectiveLabel = PhasePayload.PhaseKind.IsEmpty() + ? FString::Printf(TEXT("phase %s"), *EffectiveAction) + : FString::Printf(TEXT("%s %s"), *PhasePayload.PhaseKind, *EffectiveAction); + + FHyperTwistReplayReviewAnchor Anchor = MakeAnchor( + Event, + EHyperTwistReplayReviewAnchorKind::PhaseBoundary, + EffectiveLabel); + Anchor.PhaseId = PhasePayload.PhaseId; + Anchor.PhaseKind = PhasePayload.PhaseKind; + Anchor.bMarksCompletion = bMarksCompletion; + Anchors.Add(Anchor); + } +} + +TArray UHyperTwistReplayReviewLibrary::CollectReplayReviewAnchors(const FHyperTwistReplayPacket& Packet) +{ + TArray Anchors; + bool bFirstMoveAnchorAdded = false; + + for (const FHyperTwistReplayEvent& Event : Packet.Events) + { + switch (Event.EventType) + { + case EHyperTwistReplayEventType::InspectionStart: + Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor( + Event, + EHyperTwistReplayReviewAnchorKind::InspectionStart, + TEXT("inspection start"))); + break; + + case EHyperTwistReplayEventType::InspectionEnd: + Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor( + Event, + EHyperTwistReplayReviewAnchorKind::InspectionEnd, + TEXT("inspection end"))); + break; + + case EHyperTwistReplayEventType::TimerStart: + Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor( + Event, + EHyperTwistReplayReviewAnchorKind::TimerStart, + TEXT("timer start"))); + break; + + case EHyperTwistReplayEventType::Move: + if (!bFirstMoveAnchorAdded) + { + Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor( + Event, + EHyperTwistReplayReviewAnchorKind::FirstMove, + TEXT("first move"))); + bFirstMoveAnchorAdded = true; + } + break; + + case EHyperTwistReplayEventType::PhaseMark: + { + FHyperTwistReplayPhaseMarkPayload PhasePayload; + if (HyperTwistReplayReviewLibraryInternal::DeserializePayload(Event.PayloadJson, PhasePayload) + && !PhasePayload.PhaseId.IsEmpty()) + { + HyperTwistReplayReviewLibraryInternal::AddPhaseAnchor(Anchors, Event, PhasePayload); + } + break; + } + + case EHyperTwistReplayEventType::RecognitionCommit: + Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor( + Event, + EHyperTwistReplayReviewAnchorKind::RecognitionCommit, + TEXT("recognition commit"))); + break; + + case EHyperTwistReplayEventType::RecognitionCorrection: + Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor( + Event, + EHyperTwistReplayReviewAnchorKind::RecognitionCorrection, + TEXT("recognition correction"))); + break; + + case EHyperTwistReplayEventType::Pause: + Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor( + Event, + EHyperTwistReplayReviewAnchorKind::Pause, + TEXT("pause"))); + break; + + case EHyperTwistReplayEventType::Resume: + Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor( + Event, + EHyperTwistReplayReviewAnchorKind::Resume, + TEXT("resume"))); + break; + + case EHyperTwistReplayEventType::StateSnapshot: + Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor( + Event, + EHyperTwistReplayReviewAnchorKind::StateSnapshot, + TEXT("state snapshot"))); + break; + + case EHyperTwistReplayEventType::DeviceSync: + Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor( + Event, + EHyperTwistReplayReviewAnchorKind::DeviceSync, + TEXT("device sync"))); + break; + + case EHyperTwistReplayEventType::Annotation: + Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor( + Event, + EHyperTwistReplayReviewAnchorKind::Annotation, + TEXT("annotation"))); + break; + + case EHyperTwistReplayEventType::SolveEnd: + Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor( + Event, + EHyperTwistReplayReviewAnchorKind::SolveEnd, + TEXT("solve end"))); + break; + + default: + break; + } + } + + return Anchors; +} + +FHyperTwistReplayReviewAnalytics UHyperTwistReplayReviewLibrary::DeriveReplayReviewAnalytics(const FHyperTwistReplayPacket& Packet) +{ + FHyperTwistReplayReviewAnalytics Analytics; + Analytics.EventCount = Packet.Events.Num(); + Analytics.bHasImportedSource = !Packet.SourceImportFormat.IsEmpty(); + Analytics.ReviewAnchors = CollectReplayReviewAnchors(Packet); + + if (Packet.Events.Num() == 0) + { + return Analytics; + } + + const FHyperTwistDerivedReplaySummary Summary = Packet.DerivedSummary.TotalTimeMs > 0 || Packet.DerivedSummary.MoveCount > 0 + ? Packet.DerivedSummary + : UHyperTwistReplayLibrary::DeriveReplaySummary(Packet); + + int32 LastEventTimeMs = Packet.Events[0].TimeMs; + int32 PreviousMoveAtMs = INDEX_NONE; + int32 MoveGapCount = 0; + int32 MoveGapSumMs = 0; + int32 PauseStartedAtMs = INDEX_NONE; + float ConfidenceSum = 0.0f; + int32 ConfidenceCount = 0; + TSet UniquePhaseIds; + + Analytics.TimerStartAtMs = INDEX_NONE; + Analytics.SolveEndAtMs = INDEX_NONE; + Analytics.FirstMoveAtMs = INDEX_NONE; + Analytics.LastMoveAtMs = INDEX_NONE; + + for (const FHyperTwistReplayEvent& Event : Packet.Events) + { + Analytics.LongestIdleGapMs = FMath::Max(Analytics.LongestIdleGapMs, Event.TimeMs - LastEventTimeMs); + LastEventTimeMs = Event.TimeMs; + + switch (Event.EventType) + { + case EHyperTwistReplayEventType::TimerStart: + if (Analytics.TimerStartAtMs == INDEX_NONE) + { + Analytics.TimerStartAtMs = Event.TimeMs; + } + break; + + case EHyperTwistReplayEventType::Move: + if (Analytics.FirstMoveAtMs == INDEX_NONE) + { + Analytics.FirstMoveAtMs = Event.TimeMs; + } + + if (PreviousMoveAtMs != INDEX_NONE) + { + MoveGapSumMs += Event.TimeMs - PreviousMoveAtMs; + ++MoveGapCount; + } + + PreviousMoveAtMs = Event.TimeMs; + Analytics.LastMoveAtMs = Event.TimeMs; + break; + + case EHyperTwistReplayEventType::RecognitionPreview: + case EHyperTwistReplayEventType::RecognitionScan: + case EHyperTwistReplayEventType::RecognitionCommit: + case EHyperTwistReplayEventType::RecognitionCorrection: + { + FHyperTwistReplayRecognitionPayload RecognitionPayload; + if (HyperTwistReplayReviewLibraryInternal::DeserializePayload(Event.PayloadJson, RecognitionPayload)) + { + Analytics.bHasRecognitionData = true; + ConfidenceSum += RecognitionPayload.Confidence; + ++ConfidenceCount; + + if (Analytics.LowestRecognitionConfidence < 0.0f) + { + Analytics.LowestRecognitionConfidence = RecognitionPayload.Confidence; + Analytics.HighestRecognitionConfidence = RecognitionPayload.Confidence; + } + else + { + Analytics.LowestRecognitionConfidence = FMath::Min(Analytics.LowestRecognitionConfidence, RecognitionPayload.Confidence); + Analytics.HighestRecognitionConfidence = FMath::Max(Analytics.HighestRecognitionConfidence, RecognitionPayload.Confidence); + } + } + + if (Event.EventType == EHyperTwistReplayEventType::RecognitionPreview + || Event.EventType == EHyperTwistReplayEventType::RecognitionScan) + { + ++Analytics.RecognitionPreviewCount; + } + else if (Event.EventType == EHyperTwistReplayEventType::RecognitionCommit) + { + ++Analytics.RecognitionCommitCount; + } + else + { + ++Analytics.RecognitionCorrectionCount; + } + break; + } + + case EHyperTwistReplayEventType::PhaseMark: + { + FHyperTwistReplayPhaseMarkPayload PhasePayload; + if (HyperTwistReplayReviewLibraryInternal::DeserializePayload(Event.PayloadJson, PhasePayload) + && !PhasePayload.PhaseId.IsEmpty()) + { + UniquePhaseIds.Add(PhasePayload.PhaseId); + ++Analytics.PhaseBoundaryCount; + } + break; + } + + case EHyperTwistReplayEventType::Pause: + ++Analytics.PauseCount; + if (PauseStartedAtMs == INDEX_NONE) + { + PauseStartedAtMs = Event.TimeMs; + } + break; + + case EHyperTwistReplayEventType::Resume: + if (PauseStartedAtMs != INDEX_NONE) + { + Analytics.TotalPausedMs += FMath::Max(0, Event.TimeMs - PauseStartedAtMs); + PauseStartedAtMs = INDEX_NONE; + } + break; + + case EHyperTwistReplayEventType::DeviceSync: + ++Analytics.DeviceSyncCount; + break; + + case EHyperTwistReplayEventType::StateSnapshot: + ++Analytics.StateSnapshotCount; + break; + + case EHyperTwistReplayEventType::Annotation: + ++Analytics.AnnotationCount; + break; + + case EHyperTwistReplayEventType::SolveEnd: + Analytics.SolveEndAtMs = Event.TimeMs; + break; + + default: + break; + } + } + + if (PauseStartedAtMs != INDEX_NONE) + { + const int32 PauseClosedAtMs = Analytics.SolveEndAtMs != INDEX_NONE ? Analytics.SolveEndAtMs : LastEventTimeMs; + Analytics.TotalPausedMs += FMath::Max(0, PauseClosedAtMs - PauseStartedAtMs); + } + + Analytics.UniquePhaseCount = UniquePhaseIds.Num(); + Analytics.DurationAfterTimerStartMs = Analytics.TimerStartAtMs != INDEX_NONE + ? FMath::Max(0, (Analytics.SolveEndAtMs != INDEX_NONE ? Analytics.SolveEndAtMs : LastEventTimeMs) - Analytics.TimerStartAtMs) + : 0; + Analytics.AverageMoveGapMs = MoveGapCount > 0 ? MoveGapSumMs / MoveGapCount : 0; + Analytics.AverageRecognitionConfidence = ConfidenceCount > 0 ? ConfidenceSum / static_cast(ConfidenceCount) : -1.0f; + + for (const FHyperTwistPhaseTiming& PhaseTiming : Summary.PhaseTimings) + { + if (PhaseTiming.DurationMs > Analytics.LongestPhaseDurationMs) + { + Analytics.LongestPhaseDurationMs = PhaseTiming.DurationMs; + Analytics.LongestPhaseKind = PhaseTiming.PhaseKind; + } + } + + return Analytics; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp new file mode 100644 index 0000000..e20e1ce --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp @@ -0,0 +1,471 @@ +#include "HyperTwistTraining/HyperTwistTrainingCoachLibrary.h" + +namespace HyperTwistTrainingCoachLibraryInternal +{ + EHyperTwistCoachPriority PriorityFromScore(const float Score) + { + if (Score >= 85.0f) + { + return EHyperTwistCoachPriority::Critical; + } + if (Score >= 65.0f) + { + return EHyperTwistCoachPriority::Elevated; + } + if (Score >= 40.0f) + { + return EHyperTwistCoachPriority::Advisory; + } + + return EHyperTwistCoachPriority::Informational; + } + + FHyperTwistCoachSignal MakeSignal( + const FString& SignalId, + const EHyperTwistCoachFocusLane FocusLane, + const float Score, + const FString& Headline, + const FString& Detail, + const EHyperTwistTrainingDeliveryMode SuggestedMode, + const EHyperTwistTrainingSelectionPolicy SuggestedSelectionPolicy) + { + FHyperTwistCoachSignal Signal; + Signal.SignalId = SignalId; + Signal.FocusLane = FocusLane; + Signal.Score = Score; + Signal.Priority = PriorityFromScore(Score); + Signal.Headline = Headline; + Signal.Detail = Detail; + Signal.SuggestedMode = SuggestedMode; + Signal.SuggestedSelectionPolicy = SuggestedSelectionPolicy; + Signal.bNeedsImmediateAttention = Signal.Priority == EHyperTwistCoachPriority::Critical; + return Signal; + } + + FString BuildCountDetail(const FString& Prefix, const int32 PrimaryCount, const int32 SecondaryCount) + { + return FString::Printf(TEXT("%s: %d / %d"), *Prefix, PrimaryCount, SecondaryCount); + } +} + +TArray UHyperTwistTrainingCoachLibrary::DeriveCoachSignals( + const FHyperTwistTrainingSessionSummary& SessionSummary, + const FHyperTwistTrainingDeckScopedStats& DeckScopedStats, + const FHyperTwistTrainingLearnerDeckStateSummary& LearnerDeckStateSummary, + const FHyperTwistTrainingLearnerProfile& LearnerProfile, + const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot, + const FHyperTwistTrainingReviewProgramSummary& ReviewProgramSummary, + const FHyperTwistReplayReviewAnalytics& ReplayReviewAnalytics, + const FHyperTwistRecognitionReplaySummary& RecognitionReplaySummary) +{ + TArray Signals; + + if (CoachMemorySnapshot.bNeedsCoachIntervention + || ReviewProgramSummary.bNeedsCoachIntervention + || LearnerDeckStateSummary.OverdueCaseCount >= 3) + { + const float Score = 90.0f + + FMath::Min(static_cast(LearnerDeckStateSummary.OverdueCaseCount) * 2.0f, 8.0f) + + FMath::Min(static_cast(ReviewProgramSummary.CarryoverCaseCount), 6.0f); + FHyperTwistCoachSignal Signal = HyperTwistTrainingCoachLibraryInternal::MakeSignal( + TEXT("review_backlog"), + EHyperTwistCoachFocusLane::ReviewBacklog, + Score, + TEXT("Review backlog needs intervention"), + FString::Printf( + TEXT("overdue=%d, carryover=%d, pendingPlans=%d"), + LearnerDeckStateSummary.OverdueCaseCount, + ReviewProgramSummary.CarryoverCaseCount, + ReviewProgramSummary.PendingPlanCount), + EHyperTwistTrainingDeliveryMode::CoachReviewed, + EHyperTwistTrainingSelectionPolicy::Weighted); + Signal.SuggestedDeckId = !CoachMemorySnapshot.FocusDeckId.IsEmpty() + ? CoachMemorySnapshot.FocusDeckId + : ReviewProgramSummary.FocusDeckId; + Signal.SuggestedMethodSegmentId = !CoachMemorySnapshot.FocusMethodSegmentId.IsEmpty() + ? CoachMemorySnapshot.FocusMethodSegmentId + : ReviewProgramSummary.FocusMethodSegmentId; + Signal.SuggestedCaseIds = CoachMemorySnapshot.OverdueCaseIds.Num() > 0 + ? CoachMemorySnapshot.OverdueCaseIds + : CoachMemorySnapshot.DueCaseIds; + Signals.Add(Signal); + } + + if (CoachMemorySnapshot.bNeedsCadenceRecovery || LearnerProfile.RecentSevenDaySessionCount == 0) + { + const float Score = CoachMemorySnapshot.bNeedsCadenceRecovery ? 72.0f : 48.0f; + FHyperTwistCoachSignal Signal = HyperTwistTrainingCoachLibraryInternal::MakeSignal( + TEXT("cadence_recovery"), + EHyperTwistCoachFocusLane::Cadence, + Score, + TEXT("Cadence recovery recommended"), + FString::Printf( + TEXT("streak=%d, recent7d=%d"), + LearnerProfile.ActiveTrainingDayStreak, + LearnerProfile.RecentSevenDaySessionCount), + EHyperTwistTrainingDeliveryMode::Timer, + EHyperTwistTrainingSelectionPolicy::Weighted); + Signal.SuggestedDeckId = CoachMemorySnapshot.FocusDeckId; + Signal.SuggestedMethodSegmentId = CoachMemorySnapshot.FocusMethodSegmentId; + Signal.SuggestedCaseIds = CoachMemorySnapshot.WeakCaseIds; + Signals.Add(Signal); + } + + if (CoachMemorySnapshot.bNeedsVarietyRotation) + { + FHyperTwistCoachSignal Signal = HyperTwistTrainingCoachLibraryInternal::MakeSignal( + TEXT("variety_rotation"), + EHyperTwistCoachFocusLane::Variety, + 46.0f, + TEXT("Variety rotation advised"), + FString::Printf( + TEXT("favoriteDeckShare=%.2f, recoveryDecks=%d"), + LearnerProfile.FavoriteDeckSessionShare, + CoachMemorySnapshot.RecoveryDeckIds.Num()), + EHyperTwistTrainingDeliveryMode::VirtualCube, + EHyperTwistTrainingSelectionPolicy::Uniform); + if (CoachMemorySnapshot.RecoveryDeckIds.Num() > 0) + { + Signal.SuggestedDeckId = CoachMemorySnapshot.RecoveryDeckIds[0]; + } + if (CoachMemorySnapshot.RecoveryMethodSegmentIds.Num() > 0) + { + Signal.SuggestedMethodSegmentId = CoachMemorySnapshot.RecoveryMethodSegmentIds[0]; + } + Signals.Add(Signal); + } + + if (RecognitionReplaySummary.EpisodeCount > 0 + && (RecognitionReplaySummary.AmbiguousEpisodeCount > 0 + || ReplayReviewAnalytics.RecognitionCorrectionCount > 0 + || (RecognitionReplaySummary.AverageCommitConfidence >= 0.0f + && RecognitionReplaySummary.AverageCommitConfidence < 0.7f))) + { + const float ConfidencePenalty = RecognitionReplaySummary.AverageCommitConfidence >= 0.0f + ? (1.0f - RecognitionReplaySummary.AverageCommitConfidence) * 25.0f + : 0.0f; + const float Score = 68.0f + + static_cast(RecognitionReplaySummary.AmbiguousEpisodeCount * 5) + + static_cast(ReplayReviewAnalytics.RecognitionCorrectionCount * 4) + + ConfidencePenalty; + FHyperTwistCoachSignal Signal = HyperTwistTrainingCoachLibraryInternal::MakeSignal( + TEXT("recognition_clarity"), + EHyperTwistCoachFocusLane::Recognition, + Score, + TEXT("Recognition clarity needs work"), + FString::Printf( + TEXT("episodes=%d, ambiguous=%d, avgCommit=%.2f"), + RecognitionReplaySummary.EpisodeCount, + RecognitionReplaySummary.AmbiguousEpisodeCount, + RecognitionReplaySummary.AverageCommitConfidence), + EHyperTwistTrainingDeliveryMode::RecognitionAssisted, + EHyperTwistTrainingSelectionPolicy::Weighted); + Signal.SuggestedDeckId = CoachMemorySnapshot.FocusDeckId; + Signal.SuggestedMethodSegmentId = CoachMemorySnapshot.FocusMethodSegmentId; + Signal.SuggestedCaseIds = CoachMemorySnapshot.WeakCaseIds; + Signals.Add(Signal); + } + + if ((SessionSummary.TotalMistakes > 0 && SessionSummary.CompletedAttemptCount > 0) + || DeckScopedStats.MistakeAttemptCount > 0 + || ReplayReviewAnalytics.LongestIdleGapMs >= 2500) + { + const float MistakePressure = SessionSummary.CompletedAttemptCount > 0 + ? (static_cast(SessionSummary.TotalMistakes) / SessionSummary.CompletedAttemptCount) * 18.0f + : 0.0f; + const float Score = 52.0f + + MistakePressure + + FMath::Min(static_cast(ReplayReviewAnalytics.LongestIdleGapMs) / 150.0f, 18.0f); + FHyperTwistCoachSignal Signal = HyperTwistTrainingCoachLibraryInternal::MakeSignal( + TEXT("execution_flow"), + EHyperTwistCoachFocusLane::Execution, + Score, + TEXT("Execution flow is uneven"), + FString::Printf( + TEXT("mistakes=%d, idleGapMs=%d, longestPhase=%s"), + SessionSummary.TotalMistakes, + ReplayReviewAnalytics.LongestIdleGapMs, + *ReplayReviewAnalytics.LongestPhaseKind), + EHyperTwistTrainingDeliveryMode::VirtualCube, + EHyperTwistTrainingSelectionPolicy::Weighted); + Signal.SuggestedDeckId = SessionSummary.DeckId; + Signal.SuggestedCaseIds = CoachMemorySnapshot.WeakCaseIds; + Signals.Add(Signal); + } + + if ((DeckScopedStats.AttemptCount > 0 && DeckScopedStats.SuccessCount < DeckScopedStats.FailureCount) + || LearnerDeckStateSummary.AverageDifficultyScore >= 0.7f) + { + const float FailureBias = DeckScopedStats.AttemptCount > 0 + ? (static_cast(DeckScopedStats.FailureCount) / DeckScopedStats.AttemptCount) * 25.0f + : 0.0f; + const float Score = 44.0f + + FailureBias + + (LearnerDeckStateSummary.AverageDifficultyScore * 15.0f); + FHyperTwistCoachSignal Signal = HyperTwistTrainingCoachLibraryInternal::MakeSignal( + TEXT("accuracy_rebuild"), + EHyperTwistCoachFocusLane::Accuracy, + Score, + TEXT("Accuracy rebuild recommended"), + FString::Printf( + TEXT("failures=%d, success=%d, avgDifficulty=%.2f"), + DeckScopedStats.FailureCount, + DeckScopedStats.SuccessCount, + LearnerDeckStateSummary.AverageDifficultyScore), + EHyperTwistTrainingDeliveryMode::Timer, + EHyperTwistTrainingSelectionPolicy::Weighted); + Signal.SuggestedDeckId = SessionSummary.DeckId; + Signal.SuggestedCaseIds = CoachMemorySnapshot.WeakCaseIds; + Signals.Add(Signal); + } + + if (LearnerDeckStateSummary.DueCaseCount > 0 + || LearnerDeckStateSummary.OverdueCaseCount > 0 + || ReviewProgramSummary.PendingPlanCount > 0) + { + const float Score = 38.0f + + FMath::Min(static_cast(LearnerDeckStateSummary.DueCaseCount), 8.0f) + + FMath::Min(static_cast(LearnerDeckStateSummary.OverdueCaseCount) * 2.0f, 12.0f); + FHyperTwistCoachSignal Signal = HyperTwistTrainingCoachLibraryInternal::MakeSignal( + TEXT("stability_maintenance"), + EHyperTwistCoachFocusLane::Stability, + Score, + TEXT("Stability maintenance is due"), + FString::Printf( + TEXT("due=%d, overdue=%d, pendingPlans=%d"), + LearnerDeckStateSummary.DueCaseCount, + LearnerDeckStateSummary.OverdueCaseCount, + ReviewProgramSummary.PendingPlanCount), + EHyperTwistTrainingDeliveryMode::Timer, + EHyperTwistTrainingSelectionPolicy::Spaced); + Signal.SuggestedDeckId = SessionSummary.DeckId; + Signal.SuggestedCaseIds = CoachMemorySnapshot.DueCaseIds; + Signals.Add(Signal); + } + + Signals.Sort([](const FHyperTwistCoachSignal& Left, const FHyperTwistCoachSignal& Right) + { + if (!FMath::IsNearlyEqual(Left.Score, Right.Score)) + { + return Left.Score > Right.Score; + } + + return Left.SignalId < Right.SignalId; + }); + + return Signals; +} + +FHyperTwistCoachBrief UHyperTwistTrainingCoachLibrary::DeriveCoachBrief( + const FHyperTwistTrainingSessionSummary& SessionSummary, + const FHyperTwistTrainingDeckScopedStats& DeckScopedStats, + const FHyperTwistTrainingLearnerDeckStateSummary& LearnerDeckStateSummary, + const FHyperTwistTrainingLearnerProfile& LearnerProfile, + const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot, + const FHyperTwistTrainingReviewProgramSummary& ReviewProgramSummary, + const FHyperTwistReplayReviewAnalytics& ReplayReviewAnalytics, + const FHyperTwistRecognitionReplaySummary& RecognitionReplaySummary) +{ + FHyperTwistCoachBrief Brief; + Brief.UserId = !SessionSummary.UserId.IsEmpty() ? SessionSummary.UserId : LearnerProfile.UserId; + Brief.ReferenceUtc = !CoachMemorySnapshot.ReferenceUtc.IsEmpty() + ? CoachMemorySnapshot.ReferenceUtc + : LearnerProfile.ReferenceUtc; + Brief.TrainingSessionId = SessionSummary.TrainingSessionId; + Brief.SourceReviewPlanId = !ReviewProgramSummary.CurrentPlanId.IsEmpty() + ? ReviewProgramSummary.CurrentPlanId + : ReviewProgramSummary.NextPlanId; + Brief.FocusDeckId = !CoachMemorySnapshot.FocusDeckId.IsEmpty() + ? CoachMemorySnapshot.FocusDeckId + : SessionSummary.DeckId; + Brief.FocusMethodSegmentId = !CoachMemorySnapshot.FocusMethodSegmentId.IsEmpty() + ? CoachMemorySnapshot.FocusMethodSegmentId + : LearnerProfile.FavoriteMethodSegmentId; + Brief.bNeedsCoachReview = CoachMemorySnapshot.bNeedsCoachIntervention || ReviewProgramSummary.bNeedsCoachIntervention; + Brief.bNeedsCadenceRecovery = CoachMemorySnapshot.bNeedsCadenceRecovery; + Brief.bNeedsVarietyRotation = CoachMemorySnapshot.bNeedsVarietyRotation; + Brief.bPreferRecognitionReplayReview = RecognitionReplaySummary.EpisodeCount > 0 + && (RecognitionReplaySummary.AmbiguousEpisodeCount > 0 + || ReplayReviewAnalytics.RecognitionCorrectionCount > 0); + + Brief.Signals = DeriveCoachSignals( + SessionSummary, + DeckScopedStats, + LearnerDeckStateSummary, + LearnerProfile, + CoachMemorySnapshot, + ReviewProgramSummary, + ReplayReviewAnalytics, + RecognitionReplaySummary); + + if (Brief.Signals.Num() == 0) + { + Brief.Priority = EHyperTwistCoachPriority::Informational; + Brief.FocusLane = EHyperTwistCoachFocusLane::Stability; + Brief.PriorityScore = 0.0f; + Brief.Headline = TEXT("Training state is stable"); + Brief.PrimaryActionLabel = TEXT("maintain-rhythm"); + Brief.SuggestedMode = EHyperTwistTrainingDeliveryMode::Timer; + Brief.SuggestedSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted; + return Brief; + } + + const FHyperTwistCoachSignal& PrimarySignal = Brief.Signals[0]; + Brief.Priority = PrimarySignal.Priority; + Brief.FocusLane = PrimarySignal.FocusLane; + Brief.PriorityScore = PrimarySignal.Score; + Brief.Headline = PrimarySignal.Headline; + Brief.PrimaryActionLabel = PrimarySignal.SignalId; + Brief.SuggestedMode = PrimarySignal.SuggestedMode; + Brief.SuggestedSelectionPolicy = PrimarySignal.SuggestedSelectionPolicy; + if (!PrimarySignal.SuggestedDeckId.IsEmpty()) + { + Brief.FocusDeckId = PrimarySignal.SuggestedDeckId; + } + if (!PrimarySignal.SuggestedMethodSegmentId.IsEmpty()) + { + Brief.FocusMethodSegmentId = PrimarySignal.SuggestedMethodSegmentId; + } + Brief.FocusCaseIds = PrimarySignal.SuggestedCaseIds; + + if (Brief.FocusCaseIds.Num() == 0) + { + if (Brief.bNeedsCoachReview && CoachMemorySnapshot.OverdueCaseIds.Num() > 0) + { + Brief.FocusCaseIds = CoachMemorySnapshot.OverdueCaseIds; + } + else if (Brief.bPreferRecognitionReplayReview && CoachMemorySnapshot.WeakCaseIds.Num() > 0) + { + Brief.FocusCaseIds = CoachMemorySnapshot.WeakCaseIds; + } + else + { + Brief.FocusCaseIds = CoachMemorySnapshot.DueCaseIds; + } + } + + return Brief; +} + +FHyperTwistCoachBrief UHyperTwistTrainingCoachLibrary::DeriveCoachFollowUpBrief( + const FHyperTwistCoachBrief& CurrentBrief, + const FHyperTwistTrainingCoachActionPlan& ActionPlan, + const FHyperTwistTrainingCoachActionPlanOutcomeSummary& OutcomeSummary) +{ + FHyperTwistCoachBrief Brief; + if (!ActionPlan.IsStructurallyValid() || !OutcomeSummary.IsStructurallyValid()) + { + return Brief; + } + + Brief.UserId = OutcomeSummary.UserId; + Brief.ReferenceUtc = !OutcomeSummary.ReferenceUtc.IsEmpty() + ? OutcomeSummary.ReferenceUtc + : ActionPlan.ReferenceUtc; + Brief.TrainingSessionId = OutcomeSummary.SourceSessionId; + Brief.SourceReviewPlanId = CurrentBrief.SourceReviewPlanId; + Brief.FocusDeckId = !OutcomeSummary.FocusDeckId.IsEmpty() + ? OutcomeSummary.FocusDeckId + : ActionPlan.FocusDeckId; + Brief.FocusMethodSegmentId = !OutcomeSummary.FocusMethodSegmentId.IsEmpty() + ? OutcomeSummary.FocusMethodSegmentId + : ActionPlan.FocusMethodSegmentId; + Brief.bNeedsCoachReview = ActionPlan.bNeedsCoachReview + || OutcomeSummary.AbortedCount > 0 + || (ActionPlan.bPreferRecognitionReplayReview && OutcomeSummary.ReplayBackedAttemptCount == 0); + Brief.bNeedsCadenceRecovery = ActionPlan.bNeedsCadenceRecovery; + Brief.bNeedsVarietyRotation = ActionPlan.bNeedsVarietyRotation; + Brief.bPreferRecognitionReplayReview = ActionPlan.bPreferRecognitionReplayReview + || OutcomeSummary.RecognitionAttemptCount > 0; + Brief.FocusCaseIds = OutcomeSummary.FollowUpCaseIds.Num() > 0 + ? OutcomeSummary.FollowUpCaseIds + : ActionPlan.FocusCaseIds; + + if (!OutcomeSummary.bNeedsFollowUpPlan && Brief.FocusCaseIds.Num() == 0) + { + return FHyperTwistCoachBrief(); + } + + Brief.PriorityScore = FMath::Clamp( + (100.0f - OutcomeSummary.OutcomeScore) + + (static_cast(OutcomeSummary.UntouchedFocusCaseCount) * 4.0f) + + (static_cast(OutcomeSummary.FailureCount) * 3.0f) + + (static_cast(OutcomeSummary.TimeoutCount) * 4.0f) + + (static_cast(OutcomeSummary.DNFCount) * 5.0f) + + (static_cast(OutcomeSummary.AbortedCount) * 6.0f), + 20.0f, + 100.0f + ); + Brief.Priority = HyperTwistTrainingCoachLibraryInternal::PriorityFromScore(Brief.PriorityScore); + + if (OutcomeSummary.AbortedCount > 0) + { + Brief.FocusLane = EHyperTwistCoachFocusLane::Cadence; + Brief.Headline = TEXT("Resume the interrupted coaching sequence"); + Brief.PrimaryActionLabel = TEXT("resume_aborted_plan"); + Brief.SuggestedMode = EHyperTwistTrainingDeliveryMode::VirtualCube; + Brief.SuggestedSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Scripted; + } + else if (OutcomeSummary.TimeoutCount > 0 || OutcomeSummary.DNFCount > 0) + { + Brief.FocusLane = EHyperTwistCoachFocusLane::Execution; + Brief.Headline = TEXT("Stabilize unfinished focus cases"); + Brief.PrimaryActionLabel = TEXT("stabilize_unfinished_cases"); + Brief.SuggestedMode = EHyperTwistTrainingDeliveryMode::VirtualCube; + Brief.SuggestedSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Scripted; + } + else if (OutcomeSummary.FailureCount > 0) + { + Brief.FocusLane = EHyperTwistCoachFocusLane::Accuracy; + Brief.Headline = TEXT("Rebuild accuracy on coach-selected cases"); + Brief.PrimaryActionLabel = TEXT("rebuild_accuracy"); + Brief.SuggestedMode = EHyperTwistTrainingDeliveryMode::Timer; + Brief.SuggestedSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted; + } + else if (OutcomeSummary.UntouchedFocusCaseCount > 0) + { + Brief.FocusLane = EHyperTwistCoachFocusLane::ReviewBacklog; + Brief.Headline = TEXT("Complete the remaining coach-selected cases"); + Brief.PrimaryActionLabel = TEXT("complete_focus_cases"); + Brief.SuggestedMode = ActionPlan.SuggestedMode; + Brief.SuggestedSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Scripted; + } + else if (Brief.bPreferRecognitionReplayReview) + { + Brief.FocusLane = EHyperTwistCoachFocusLane::Recognition; + Brief.Headline = TEXT("Run recognition-focused follow-up review"); + Brief.PrimaryActionLabel = TEXT("recognition_follow_up"); + Brief.SuggestedMode = EHyperTwistTrainingDeliveryMode::RecognitionAssisted; + Brief.SuggestedSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted; + } + else + { + Brief.FocusLane = CurrentBrief.FocusLane; + Brief.Headline = TEXT("Reinforce the prior coach focus set"); + Brief.PrimaryActionLabel = TEXT("reinforce_focus_cases"); + Brief.SuggestedMode = ActionPlan.SuggestedMode; + Brief.SuggestedSelectionPolicy = ActionPlan.SuggestedSelectionPolicy; + } + + FHyperTwistCoachSignal FollowUpSignal = HyperTwistTrainingCoachLibraryInternal::MakeSignal( + Brief.PrimaryActionLabel, + Brief.FocusLane, + Brief.PriorityScore, + Brief.Headline, + FString::Printf( + TEXT("followUpCases=%d, success=%d, failure=%d, timeout=%d, dnf=%d"), + Brief.FocusCaseIds.Num(), + OutcomeSummary.SuccessCount, + OutcomeSummary.FailureCount, + OutcomeSummary.TimeoutCount, + OutcomeSummary.DNFCount), + Brief.SuggestedMode, + Brief.SuggestedSelectionPolicy + ); + FollowUpSignal.SuggestedDeckId = Brief.FocusDeckId; + FollowUpSignal.SuggestedMethodSegmentId = Brief.FocusMethodSegmentId; + FollowUpSignal.SuggestedCaseIds = Brief.FocusCaseIds; + Brief.Signals = { FollowUpSignal }; + + return Brief; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp index 52da1a6..c7ce656 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp @@ -296,6 +296,11 @@ namespace HyperTwistTrainingRepositoryLibraryInternal return UserId + CaseKeySeparator + BatchId; } + FString MakeCoachActionPlanKey(const FString& UserId, const FString& PlanId) + { + return UserId + CaseKeySeparator + PlanId; + } + TArray DeduplicateCaseIds(const TArray& CaseIds) { TArray UniqueCaseIds; @@ -395,6 +400,67 @@ namespace HyperTwistTrainingRepositoryLibraryInternal return NormalizedBatch; } + FHyperTwistTrainingCoachActionPlan NormalizeCoachActionPlan( + const FHyperTwistTrainingCoachActionPlan& ActionPlan + ) + { + FHyperTwistTrainingCoachActionPlan NormalizedPlan = ActionPlan; + NormalizedPlan.ReferenceUtc = ResolveReferenceUtc(NormalizedPlan.ReferenceUtc); + NormalizedPlan.FocusCaseIds = DeduplicateTemplateCaseIds(NormalizedPlan.FocusCaseIds); + if (NormalizedPlan.CreatedAtUtc.IsEmpty()) + { + NormalizedPlan.CreatedAtUtc = NormalizedPlan.ReferenceUtc; + } + + TMap EntryIndexById; + TArray NormalizedEntries; + for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : ActionPlan.Entries) + { + if (!Entry.IsStructurallyValid()) + { + continue; + } + + if (const int32* ExistingIndex = EntryIndexById.Find(Entry.ActionId)) + { + NormalizedEntries[*ExistingIndex] = Entry; + } + else + { + EntryIndexById.Add(Entry.ActionId, NormalizedEntries.Add(Entry)); + } + } + + NormalizedEntries.Sort([]( + const FHyperTwistTrainingCoachActionPlanEntry& Left, + const FHyperTwistTrainingCoachActionPlanEntry& Right + ) + { + if (!FMath::IsNearlyEqual(Left.PriorityScore, Right.PriorityScore)) + { + return Left.PriorityScore > Right.PriorityScore; + } + + return Left.ActionId < Right.ActionId; + }); + NormalizedPlan.Entries = NormalizedEntries; + + if (NormalizedPlan.FocusDeckId.IsEmpty() && NormalizedPlan.Entries.Num() > 0) + { + NormalizedPlan.FocusDeckId = NormalizedPlan.Entries[0].DeckId; + } + if (NormalizedPlan.FocusMethodSegmentId.IsEmpty() && NormalizedPlan.Entries.Num() > 0) + { + NormalizedPlan.FocusMethodSegmentId = NormalizedPlan.Entries[0].MethodSegmentId; + } + if (NormalizedPlan.Headline.IsEmpty()) + { + NormalizedPlan.Headline = NormalizedPlan.PrimaryActionLabel; + } + + return NormalizedPlan; + } + FHyperTwistTrainingCaseLearnerState DeriveLearnerStateFromReviewHistory( const FString& UserId, const FString& DeckId, @@ -2577,6 +2643,29 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRepositoryLibrar SeenMethodDrillBatchKeys.Add(BatchKey); } + Report.TotalCoachActionPlanCount = RepositoryState.StoredCoachActionPlans.Num(); + TSet SeenCoachActionPlanKeys; + for (const FHyperTwistTrainingCoachActionPlan& ActionPlan : RepositoryState.StoredCoachActionPlans) + { + if (!ActionPlan.IsStructurallyValid()) + { + Report.InvalidCoachActionPlanCount += 1; + continue; + } + + const FString ActionPlanKey = HyperTwistTrainingRepositoryLibraryInternal::MakeCoachActionPlanKey( + ActionPlan.UserId, + ActionPlan.PlanId + ); + if (SeenCoachActionPlanKeys.Contains(ActionPlanKey)) + { + Report.DuplicateCoachActionPlanCount += 1; + continue; + } + + SeenCoachActionPlanKeys.Add(ActionPlanKey); + } + if (Report.InvalidRunRecordCount > 0) { HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction( @@ -2664,6 +2753,20 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRepositoryLibrar TEXT("normalize-and-deduplicate-method-drill-batches") ); } + if (Report.InvalidCoachActionPlanCount > 0) + { + HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction( + Report.RepairActions, + TEXT("drop-invalid-coach-action-plans") + ); + } + if (Report.DuplicateCoachActionPlanCount > 0) + { + HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction( + Report.RepairActions, + TEXT("normalize-and-deduplicate-coach-action-plans") + ); + } return Report; } @@ -2918,6 +3021,60 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::RepairR return Left.BatchId < Right.BatchId; }); + TMap CoachActionPlanIndexByKey; + for (const FHyperTwistTrainingCoachActionPlan& ActionPlan : RepositoryState.StoredCoachActionPlans) + { + if (!ActionPlan.IsStructurallyValid()) + { + continue; + } + + const FHyperTwistTrainingCoachActionPlan NormalizedPlan = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan); + if (!NormalizedPlan.IsStructurallyValid()) + { + continue; + } + + const FString ActionPlanKey = HyperTwistTrainingRepositoryLibraryInternal::MakeCoachActionPlanKey( + NormalizedPlan.UserId, + NormalizedPlan.PlanId + ); + if (const int32* ExistingIndex = CoachActionPlanIndexByKey.Find(ActionPlanKey)) + { + RepairedState.StoredCoachActionPlans[*ExistingIndex] = NormalizedPlan; + } + else + { + CoachActionPlanIndexByKey.Add( + ActionPlanKey, + RepairedState.StoredCoachActionPlans.Add(NormalizedPlan) + ); + } + } + + RepairedState.StoredCoachActionPlans.Sort([]( + const FHyperTwistTrainingCoachActionPlan& Left, + const FHyperTwistTrainingCoachActionPlan& Right + ) + { + if (Left.UserId != Right.UserId) + { + return Left.UserId < Right.UserId; + } + + const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + Left.CreatedAtUtc, + Right.CreatedAtUtc + ); + if (UtcCompare != 0) + { + return UtcCompare > 0; + } + + return Left.PlanId < Right.PlanId; + }); + return RepairedState; } @@ -4328,6 +4485,637 @@ TArray UHyperTwistTrainingRepositoryLibrary: return EffectiveTemplates; } +FHyperTwistTrainingCoachActionPlan UHyperTwistTrainingRepositoryLibrary::BuildCoachActionPlan( + const FHyperTwistCoachBrief& CoachBrief, + const FString& PlanId, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachActionPlan ActionPlan; + if (CoachBrief.UserId.IsEmpty() || PlanId.IsEmpty()) + { + return ActionPlan; + } + + ActionPlan.PlanId = PlanId; + ActionPlan.UserId = CoachBrief.UserId; + ActionPlan.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( + !ReferenceUtc.IsEmpty() ? ReferenceUtc : CoachBrief.ReferenceUtc + ); + ActionPlan.SourceSessionId = CoachBrief.TrainingSessionId; + ActionPlan.SourceReviewPlanId = CoachBrief.SourceReviewPlanId; + ActionPlan.FocusDeckId = CoachBrief.FocusDeckId; + ActionPlan.FocusMethodSegmentId = CoachBrief.FocusMethodSegmentId; + ActionPlan.FocusLaneLabel = StaticEnum()->GetNameStringByValue( + static_cast(CoachBrief.FocusLane) + ); + ActionPlan.PriorityLabel = StaticEnum()->GetNameStringByValue( + static_cast(CoachBrief.Priority) + ); + ActionPlan.PriorityScore = CoachBrief.PriorityScore; + ActionPlan.Headline = CoachBrief.Headline; + ActionPlan.PrimaryActionLabel = CoachBrief.PrimaryActionLabel; + ActionPlan.SuggestedMode = CoachBrief.SuggestedMode; + ActionPlan.SuggestedSelectionPolicy = CoachBrief.SuggestedSelectionPolicy; + ActionPlan.FocusCaseIds = CoachBrief.FocusCaseIds; + ActionPlan.PlanState = EHyperTwistTrainingCoachActionPlanState::Draft; + ActionPlan.CreatedAtUtc = ActionPlan.ReferenceUtc; + ActionPlan.bNeedsCoachReview = CoachBrief.bNeedsCoachReview; + ActionPlan.bNeedsCadenceRecovery = CoachBrief.bNeedsCadenceRecovery; + ActionPlan.bNeedsVarietyRotation = CoachBrief.bNeedsVarietyRotation; + ActionPlan.bPreferRecognitionReplayReview = CoachBrief.bPreferRecognitionReplayReview; + + for (int32 Index = 0; Index < CoachBrief.FocusCaseIds.Num(); ++Index) + { + FHyperTwistTrainingCoachActionPlanEntry Entry; + Entry.ActionId = FString::Printf(TEXT("%s_entry_%02d"), *PlanId, Index + 1); + Entry.CaseId = CoachBrief.FocusCaseIds[Index]; + Entry.DeckId = CoachBrief.FocusDeckId; + Entry.MethodSegmentId = CoachBrief.FocusMethodSegmentId; + Entry.ActionLabel = CoachBrief.PrimaryActionLabel; + Entry.Headline = CoachBrief.Headline; + Entry.PriorityScore = CoachBrief.PriorityScore; + Entry.SuggestedMode = CoachBrief.SuggestedMode; + Entry.SuggestedSelectionPolicy = CoachBrief.SuggestedSelectionPolicy; + Entry.bPreferRecognitionReplayReview = CoachBrief.bPreferRecognitionReplayReview; + if (Entry.IsStructurallyValid()) + { + ActionPlan.Entries.Add(Entry); + } + } + + if (ActionPlan.Entries.Num() == 0) + { + FHyperTwistTrainingCoachActionPlanEntry Entry; + Entry.ActionId = FString::Printf(TEXT("%s_entry_01"), *PlanId); + Entry.CaseId = TEXT("coach-generic"); + Entry.DeckId = CoachBrief.FocusDeckId; + Entry.MethodSegmentId = CoachBrief.FocusMethodSegmentId; + Entry.ActionLabel = CoachBrief.PrimaryActionLabel.IsEmpty() + ? TEXT("coach-focus") + : CoachBrief.PrimaryActionLabel; + Entry.Headline = CoachBrief.Headline; + Entry.PriorityScore = CoachBrief.PriorityScore; + Entry.SuggestedMode = CoachBrief.SuggestedMode; + Entry.SuggestedSelectionPolicy = CoachBrief.SuggestedSelectionPolicy; + Entry.bPreferRecognitionReplayReview = CoachBrief.bPreferRecognitionReplayReview; + ActionPlan.Entries.Add(Entry); + } + + return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan); +} + +FHyperTwistTrainingCoachActionPlan UHyperTwistTrainingRepositoryLibrary::ActivateCoachActionPlan( + const FHyperTwistTrainingCoachActionPlan& ActionPlan, + const FString& TrainingSessionId, + const FString& StartedAtUtc +) +{ + FHyperTwistTrainingCoachActionPlan UpdatedPlan = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan); + if (!UpdatedPlan.IsStructurallyValid()) + { + return UpdatedPlan; + } + + UpdatedPlan.PlanState = EHyperTwistTrainingCoachActionPlanState::Active; + UpdatedPlan.SourceSessionId = !TrainingSessionId.IsEmpty() ? TrainingSessionId : UpdatedPlan.SourceSessionId; + UpdatedPlan.StartedAtUtc = !StartedAtUtc.IsEmpty() + ? StartedAtUtc + : HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(UpdatedPlan.ReferenceUtc); + return UpdatedPlan; +} + +FHyperTwistTrainingCoachActionPlan UHyperTwistTrainingRepositoryLibrary::FinalizeCoachActionPlan( + const FHyperTwistTrainingCoachActionPlan& ActionPlan, + const FHyperTwistTrainingSession& Session, + bool bAbortPlan +) +{ + FHyperTwistTrainingCoachActionPlan UpdatedPlan = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan); + if (!UpdatedPlan.IsStructurallyValid()) + { + return UpdatedPlan; + } + + UpdatedPlan.SourceSessionId = !Session.TrainingSessionId.IsEmpty() + ? Session.TrainingSessionId + : UpdatedPlan.SourceSessionId; + UpdatedPlan.CompletedAtUtc = !Session.EndedAtUtc.IsEmpty() + ? Session.EndedAtUtc + : HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(UpdatedPlan.ReferenceUtc); + UpdatedPlan.PlanState = bAbortPlan || Session.SessionState == EHyperTwistTrainingSessionState::Aborted + ? EHyperTwistTrainingCoachActionPlanState::Aborted + : EHyperTwistTrainingCoachActionPlanState::Completed; + if (UpdatedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Completed) + { + for (FHyperTwistTrainingCoachActionPlanEntry& Entry : UpdatedPlan.Entries) + { + Entry.bCompleted = true; + } + } + + return UpdatedPlan; +} + +TArray UHyperTwistTrainingRepositoryLibrary::ListCoachActionPlansForUser( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId +) +{ + TArray ActionPlans; + if (UserId.IsEmpty()) + { + return ActionPlans; + } + + for (const FHyperTwistTrainingCoachActionPlan& ActionPlan : RepositoryState.StoredCoachActionPlans) + { + if (ActionPlan.UserId == UserId && ActionPlan.IsStructurallyValid()) + { + ActionPlans.Add(ActionPlan); + } + } + + ActionPlans.Sort([](const FHyperTwistTrainingCoachActionPlan& Left, const FHyperTwistTrainingCoachActionPlan& Right) + { + const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + Left.CreatedAtUtc, + Right.CreatedAtUtc + ); + if (UtcCompare != 0) + { + return UtcCompare > 0; + } + + return Left.PlanId < Right.PlanId; + }); + return ActionPlans; +} + +bool UHyperTwistTrainingRepositoryLibrary::TryGetCoachActionPlan( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& PlanId, + FHyperTwistTrainingCoachActionPlan& OutActionPlan +) +{ + OutActionPlan = FHyperTwistTrainingCoachActionPlan(); + if (PlanId.IsEmpty()) + { + return false; + } + + const FHyperTwistTrainingCoachActionPlan* MatchingPlan = + RepositoryState.StoredCoachActionPlans.FindByPredicate( + [&PlanId](const FHyperTwistTrainingCoachActionPlan& Candidate) + { + return Candidate.PlanId == PlanId && Candidate.IsStructurallyValid(); + } + ); + if (MatchingPlan == nullptr) + { + return false; + } + + OutActionPlan = *MatchingPlan; + return true; +} + +FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertCoachActionPlan( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingCoachActionPlan& ActionPlan +) +{ + FHyperTwistTrainingRepositoryState UpdatedRepositoryState = RepositoryState; + const FHyperTwistTrainingCoachActionPlan NormalizedPlan = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan); + if (!NormalizedPlan.IsStructurallyValid()) + { + return UpdatedRepositoryState; + } + + FHyperTwistTrainingCoachActionPlan* ExistingPlan = + UpdatedRepositoryState.StoredCoachActionPlans.FindByPredicate( + [&NormalizedPlan](const FHyperTwistTrainingCoachActionPlan& Candidate) + { + return Candidate.UserId == NormalizedPlan.UserId + && Candidate.PlanId == NormalizedPlan.PlanId; + } + ); + if (ExistingPlan != nullptr) + { + *ExistingPlan = NormalizedPlan; + } + else + { + UpdatedRepositoryState.StoredCoachActionPlans.Add(NormalizedPlan); + } + + return UpdatedRepositoryState; +} + +FHyperTwistTrainingCoachActionPlanSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanSummary( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachActionPlanSummary Summary; + if (UserId.IsEmpty()) + { + return Summary; + } + + Summary.UserId = UserId; + Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc); + + const TArray ActionPlans = ListCoachActionPlansForUser( + RepositoryState, + UserId + ); + if (ActionPlans.Num() == 0) + { + return Summary; + } + + Summary.TotalPlanCount = ActionPlans.Num(); + int32 SuggestedModeCounts[5] = {0, 0, 0, 0, 0}; + TMap FocusLaneCounts; + TMap ActionLabelCounts; + int32 CompletedPlanDurationAccumulator = 0; + int32 CompletedPlanDurationCount = 0; + float PriorityScoreAccumulator = 0.0f; + FString LatestOpenPlanUtc; + FString LatestCompletedUtc; + + for (const FHyperTwistTrainingCoachActionPlan& ActionPlan : ActionPlans) + { + PriorityScoreAccumulator += ActionPlan.PriorityScore; + Summary.TotalEntryCount += ActionPlan.Entries.Num(); + FocusLaneCounts.FindOrAdd(ActionPlan.FocusLaneLabel) += 1; + ActionLabelCounts.FindOrAdd(ActionPlan.PrimaryActionLabel) += 1; + + const int32 SuggestedModeIndex = static_cast(ActionPlan.SuggestedMode); + if (SuggestedModeIndex >= 0 && SuggestedModeIndex < UE_ARRAY_COUNT(SuggestedModeCounts)) + { + SuggestedModeCounts[SuggestedModeIndex] += 1; + } + + int32 LocalCompletedEntries = 0; + for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : ActionPlan.Entries) + { + if (Entry.bCompleted) + { + LocalCompletedEntries += 1; + } + if (Entry.bPreferRecognitionReplayReview) + { + Summary.bHasRecognitionBiasedPlans = true; + } + } + + Summary.CompletedEntryCount += LocalCompletedEntries; + Summary.OpenEntryCount += ActionPlan.Entries.Num() - LocalCompletedEntries; + Summary.bHasRecognitionBiasedPlans = Summary.bHasRecognitionBiasedPlans || ActionPlan.bPreferRecognitionReplayReview; + + switch (ActionPlan.PlanState) + { + case EHyperTwistTrainingCoachActionPlanState::Draft: + Summary.DraftPlanCount += 1; + break; + case EHyperTwistTrainingCoachActionPlanState::Active: + Summary.ActivePlanCount += 1; + break; + case EHyperTwistTrainingCoachActionPlanState::Completed: + Summary.CompletedPlanCount += 1; + break; + case EHyperTwistTrainingCoachActionPlanState::Aborted: + Summary.AbortedPlanCount += 1; + break; + default: + break; + } + + if (ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Active + || ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Draft) + { + const FString SortUtc = !ActionPlan.StartedAtUtc.IsEmpty() + ? ActionPlan.StartedAtUtc + : ActionPlan.CreatedAtUtc; + if (Summary.CurrentPlanId.IsEmpty() + || HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(SortUtc, LatestOpenPlanUtc) > 0) + { + Summary.CurrentPlanId = ActionPlan.PlanId; + Summary.FocusDeckId = ActionPlan.FocusDeckId; + Summary.FocusMethodSegmentId = ActionPlan.FocusMethodSegmentId; + LatestOpenPlanUtc = SortUtc; + } + } + + if (ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Completed + && !ActionPlan.CompletedAtUtc.IsEmpty()) + { + const int32 DurationMs = HyperTwistTrainingRepositoryLibraryInternal::ComputeElapsedMilliseconds( + ActionPlan.CreatedAtUtc, + ActionPlan.CompletedAtUtc + ); + if (DurationMs > 0) + { + CompletedPlanDurationAccumulator += DurationMs; + CompletedPlanDurationCount += 1; + if (Summary.BestCompletedPlanDurationMs == 0 || DurationMs < Summary.BestCompletedPlanDurationMs) + { + Summary.BestCompletedPlanDurationMs = DurationMs; + } + } + + if (Summary.LastCompletedPlanId.IsEmpty() + || HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(ActionPlan.CompletedAtUtc, LatestCompletedUtc) > 0) + { + Summary.LastCompletedPlanId = ActionPlan.PlanId; + LatestCompletedUtc = ActionPlan.CompletedAtUtc; + if (Summary.CurrentPlanId.IsEmpty()) + { + Summary.FocusDeckId = ActionPlan.FocusDeckId; + Summary.FocusMethodSegmentId = ActionPlan.FocusMethodSegmentId; + } + } + } + + if (ActionPlan.bNeedsCoachReview + || ActionPlan.bNeedsCadenceRecovery + || ActionPlan.bNeedsVarietyRotation + || ActionPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Aborted) + { + Summary.bNeedsCoachIntervention = true; + } + } + + Summary.AveragePriorityScore = PriorityScoreAccumulator / static_cast(Summary.TotalPlanCount); + if (CompletedPlanDurationCount > 0) + { + Summary.AverageCompletedPlanDurationMs = + CompletedPlanDurationAccumulator / CompletedPlanDurationCount; + } + + int32 BestModeCount = 0; + for (int32 Index = 0; Index < UE_ARRAY_COUNT(SuggestedModeCounts); ++Index) + { + if (SuggestedModeCounts[Index] > BestModeCount) + { + BestModeCount = SuggestedModeCounts[Index]; + Summary.DominantSuggestedMode = static_cast(Index); + } + } + + int32 BestFocusLaneCount = 0; + for (const TPair& Pair : FocusLaneCounts) + { + if (Pair.Value > BestFocusLaneCount) + { + BestFocusLaneCount = Pair.Value; + Summary.DominantFocusLaneLabel = Pair.Key; + } + } + + int32 BestActionLabelCount = 0; + for (const TPair& Pair : ActionLabelCounts) + { + if (Pair.Value > BestActionLabelCount) + { + BestActionLabelCount = Pair.Value; + Summary.DominantActionLabel = Pair.Key; + } + } + + Summary.ActionPressureScore = + static_cast(Summary.ActivePlanCount * 3) + + static_cast(Summary.DraftPlanCount * 2) + + static_cast(Summary.AbortedPlanCount * 2) + + (static_cast(Summary.OpenEntryCount) * 0.5f) + + (Summary.bNeedsCoachIntervention ? 5.0f : 0.0f); + + return Summary; +} + +FHyperTwistTrainingCoachActionPlanOutcomeSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanOutcomeSummary( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingCoachActionPlan& ActionPlan, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachActionPlanOutcomeSummary Summary; + const FHyperTwistTrainingCoachActionPlan NormalizedPlan = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachActionPlan(ActionPlan); + if (!NormalizedPlan.IsStructurallyValid()) + { + return Summary; + } + + Summary.UserId = NormalizedPlan.UserId; + Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( + !ReferenceUtc.IsEmpty() ? ReferenceUtc : NormalizedPlan.ReferenceUtc + ); + Summary.PlanId = NormalizedPlan.PlanId; + Summary.SourceSessionId = NormalizedPlan.SourceSessionId; + Summary.FocusDeckId = NormalizedPlan.FocusDeckId; + Summary.FocusMethodSegmentId = NormalizedPlan.FocusMethodSegmentId; + Summary.PrimaryActionLabel = NormalizedPlan.PrimaryActionLabel; + Summary.PlanState = NormalizedPlan.PlanState; + Summary.SuggestedMode = NormalizedPlan.SuggestedMode; + + TSet FocusCaseIds; + for (const FString& FocusCaseId : NormalizedPlan.FocusCaseIds) + { + if (!FocusCaseId.IsEmpty()) + { + FocusCaseIds.Add(FocusCaseId); + } + } + + TSet CompletedCaseIds; + for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : NormalizedPlan.Entries) + { + if (!Entry.CaseId.IsEmpty() && FocusCaseIds.Num() == 0) + { + FocusCaseIds.Add(Entry.CaseId); + } + + if (Entry.bCompleted && !Entry.CaseId.IsEmpty()) + { + CompletedCaseIds.Add(Entry.CaseId); + } + } + + Summary.FocusCaseCount = FocusCaseIds.Num(); + + FHyperTwistTrainingRunRecord RunRecord; + if (!NormalizedPlan.SourceSessionId.IsEmpty() + && TryGetRunRecord(RepositoryState, NormalizedPlan.SourceSessionId, RunRecord)) + { + TSet TouchedCaseIds; + int32 TotalTimeAccumulator = 0; + int32 TotalTimeSampleCount = 0; + int32 RecognitionTimeAccumulator = 0; + int32 RecognitionTimeSampleCount = 0; + + for (const FHyperTwistTrainingAttemptHistoryEntry& AttemptHistoryEntry : RunRecord.AttemptHistory) + { + if (FocusCaseIds.Num() > 0 && !FocusCaseIds.Contains(AttemptHistoryEntry.CaseId)) + { + continue; + } + + Summary.AttemptCount += 1; + TouchedCaseIds.Add(AttemptHistoryEntry.CaseId); + + if (!AttemptHistoryEntry.ReplayId.IsEmpty()) + { + Summary.ReplayBackedAttemptCount += 1; + } + + if (AttemptHistoryEntry.DeliveryMode == EHyperTwistTrainingDeliveryMode::RecognitionAssisted + || AttemptHistoryEntry.RecognitionTimeMs > 0) + { + Summary.RecognitionAttemptCount += 1; + } + + if (AttemptHistoryEntry.TotalTimeMs > 0) + { + TotalTimeAccumulator += AttemptHistoryEntry.TotalTimeMs; + TotalTimeSampleCount += 1; + if (Summary.BestTotalTimeMs == 0 || AttemptHistoryEntry.TotalTimeMs < Summary.BestTotalTimeMs) + { + Summary.BestTotalTimeMs = AttemptHistoryEntry.TotalTimeMs; + } + } + + if (AttemptHistoryEntry.RecognitionTimeMs > 0) + { + RecognitionTimeAccumulator += AttemptHistoryEntry.RecognitionTimeMs; + RecognitionTimeSampleCount += 1; + } + + switch (AttemptHistoryEntry.Result) + { + case EHyperTwistTrainingAttemptResult::Success: + Summary.SuccessCount += 1; + if (!AttemptHistoryEntry.CaseId.IsEmpty()) + { + CompletedCaseIds.Add(AttemptHistoryEntry.CaseId); + } + break; + + case EHyperTwistTrainingAttemptResult::Failure: + Summary.FailureCount += 1; + break; + + case EHyperTwistTrainingAttemptResult::Timeout: + Summary.TimeoutCount += 1; + break; + + case EHyperTwistTrainingAttemptResult::DNF: + Summary.DNFCount += 1; + break; + + case EHyperTwistTrainingAttemptResult::Aborted: + Summary.AbortedCount += 1; + break; + + default: + break; + } + } + + Summary.TouchedFocusCaseCount = TouchedCaseIds.Num(); + if (TotalTimeSampleCount > 0) + { + Summary.AverageTotalTimeMs = TotalTimeAccumulator / TotalTimeSampleCount; + } + if (RecognitionTimeSampleCount > 0) + { + Summary.AverageRecognitionTimeMs = RecognitionTimeAccumulator / RecognitionTimeSampleCount; + } + } + + Summary.CompletedFocusCaseCount = CompletedCaseIds.Num(); + Summary.UntouchedFocusCaseCount = FMath::Max(0, Summary.FocusCaseCount - Summary.TouchedFocusCaseCount); + for (const FString& FocusCaseId : NormalizedPlan.FocusCaseIds) + { + if (!FocusCaseId.IsEmpty() && !CompletedCaseIds.Contains(FocusCaseId)) + { + Summary.FollowUpCaseIds.Add(FocusCaseId); + } + } + if (Summary.FollowUpCaseIds.Num() == 0) + { + for (const FHyperTwistTrainingCoachActionPlanEntry& Entry : NormalizedPlan.Entries) + { + if (!Entry.CaseId.IsEmpty() + && !CompletedCaseIds.Contains(Entry.CaseId) + && !Summary.FollowUpCaseIds.Contains(Entry.CaseId)) + { + Summary.FollowUpCaseIds.Add(Entry.CaseId); + } + } + } + if (Summary.FocusCaseCount > 0) + { + Summary.CompletionRate = static_cast(Summary.CompletedFocusCaseCount) + / static_cast(Summary.FocusCaseCount); + } + + const FString PlanStartUtc = !NormalizedPlan.StartedAtUtc.IsEmpty() + ? NormalizedPlan.StartedAtUtc + : NormalizedPlan.CreatedAtUtc; + const FString PlanEndUtc = !NormalizedPlan.CompletedAtUtc.IsEmpty() + ? NormalizedPlan.CompletedAtUtc + : Summary.ReferenceUtc; + if (!PlanStartUtc.IsEmpty() && !PlanEndUtc.IsEmpty()) + { + Summary.PlanDurationMs = HyperTwistTrainingRepositoryLibraryInternal::ComputeElapsedMilliseconds( + PlanStartUtc, + PlanEndUtc + ); + } + + const bool bPlanTerminal = + NormalizedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Completed + || NormalizedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Aborted; + Summary.bMetPrimaryGoal = + bPlanTerminal + && NormalizedPlan.PlanState != EHyperTwistTrainingCoachActionPlanState::Aborted + && (Summary.FocusCaseCount == 0 + ? Summary.SuccessCount > 0 + : Summary.CompletedFocusCaseCount >= Summary.FocusCaseCount); + + Summary.OutcomeScore = + (Summary.CompletionRate * 100.0f) + + (static_cast(Summary.SuccessCount) * 4.0f) + - (static_cast(Summary.FailureCount) * 2.0f) + - (static_cast(Summary.TimeoutCount) * 3.0f) + - (static_cast(Summary.DNFCount) * 4.0f) + - (static_cast(Summary.AbortedCount) * 5.0f) + - (static_cast(Summary.UntouchedFocusCaseCount) * 4.0f) + + (static_cast(Summary.ReplayBackedAttemptCount) * 0.5f); + + Summary.bNeedsFollowUpPlan = + Summary.FollowUpCaseIds.Num() > 0 + || ( + bPlanTerminal + && ( + NormalizedPlan.PlanState == EHyperTwistTrainingCoachActionPlanState::Aborted + || !Summary.bMetPrimaryGoal + || Summary.FailureCount > 0 + || Summary.TimeoutCount > 0 + || Summary.DNFCount > 0 + || (NormalizedPlan.bNeedsCoachReview && Summary.ReplayBackedAttemptCount == 0) + )); + + return Summary; +} + FHyperTwistTrainingCaseRecommendation UHyperTwistTrainingRepositoryLibrary::MakeCaseRecommendation( const FHyperTwistTrainingCase& TrainingCase, const FHyperTwistTrainingCaseLearnerState& LearnerState, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp index aab95f5..369decc 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp @@ -194,6 +194,98 @@ FHyperTwistTrainingCoachMemorySnapshot UHyperTwistTrainingRuntimeLibrary::GetAct return FHyperTwistTrainingCoachMemorySnapshot(); } +FHyperTwistReplayReviewAnalytics UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingReplayReviewAnalytics( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveReplayReviewAnalytics(); + } + + return FHyperTwistReplayReviewAnalytics(); +} + +FHyperTwistRecognitionReplaySummary UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingRecognitionReplaySummary( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveRecognitionReplaySummary(); + } + + return FHyperTwistRecognitionReplaySummary(); +} + +TArray UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachSignals( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveCoachSignals(); + } + + return TArray(); +} + +FHyperTwistCoachBrief UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachBrief(UObject* WorldContextObject) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveCoachBrief(); + } + + return FHyperTwistCoachBrief(); +} + +FHyperTwistTrainingCoachActionPlan UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachActionPlan( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveCoachActionPlan(); + } + + return FHyperTwistTrainingCoachActionPlan(); +} + +FHyperTwistTrainingCoachActionPlanSummary UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachActionPlanSummary( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveCoachActionPlanSummary(); + } + + return FHyperTwistTrainingCoachActionPlanSummary(); +} + +FHyperTwistTrainingCoachActionPlanOutcomeSummary UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachActionPlanOutcomeSummary( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveCoachActionPlanOutcomeSummary(); + } + + return FHyperTwistTrainingCoachActionPlanOutcomeSummary(); +} + +FHyperTwistCoachBrief UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachFollowUpBrief(UObject* WorldContextObject) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveCoachFollowUpBrief(); + } + + return FHyperTwistCoachBrief(); +} + TArray UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingLearnerMethodSegments(UObject* WorldContextObject) { if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) @@ -368,6 +460,21 @@ bool UHyperTwistTrainingRuntimeLibrary::TryGetStoredTrainingReviewPlan( return false; } +bool UHyperTwistTrainingRuntimeLibrary::TryGetStoredTrainingCoachActionPlan( + UObject* WorldContextObject, + const FString& PlanId, + FHyperTwistTrainingCoachActionPlan& OutActionPlan +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->TryGetStoredCoachActionPlan(PlanId, OutActionPlan); + } + + OutActionPlan = FHyperTwistTrainingCoachActionPlan(); + return false; +} + bool UHyperTwistTrainingRuntimeLibrary::TryGetStoredTrainingSessionTemplate( UObject* WorldContextObject, const FString& UserId, @@ -606,6 +713,32 @@ FHyperTwistTrainingDeck UHyperTwistTrainingRuntimeLibrary::BuildActiveReviewDeck return FHyperTwistTrainingDeck(); } +FHyperTwistTrainingDeck UHyperTwistTrainingRuntimeLibrary::BuildActiveCoachRecommendedDeck( + UObject* WorldContextObject, + int32 MaxCases +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->BuildActiveCoachRecommendedDeck(MaxCases); + } + + return FHyperTwistTrainingDeck(); +} + +FHyperTwistTrainingDeck UHyperTwistTrainingRuntimeLibrary::BuildActiveCoachFollowUpDeck( + UObject* WorldContextObject, + int32 MaxCases +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->BuildActiveCoachFollowUpDeck(MaxCases); + } + + return FHyperTwistTrainingDeck(); +} + FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::StartTrainingRunFromTemplate( UObject* WorldContextObject, const FString& TemplateId, @@ -636,6 +769,34 @@ FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::StartRecommendedR return FHyperTwistTrainingRunState(); } +FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::StartCoachRecommendedRun( + UObject* WorldContextObject, + const FString& SessionId, + int32 MaxCases +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->StartCoachRecommendedRun(SessionId, MaxCases); + } + + return FHyperTwistTrainingRunState(); +} + +FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::StartCoachFollowUpRun( + UObject* WorldContextObject, + const FString& SessionId, + int32 MaxCases +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->StartCoachFollowUpRun(SessionId, MaxCases); + } + + return FHyperTwistTrainingRunState(); +} + FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::StartSampleClassicTrainingRun( UObject* WorldContextObject, const FString& UserId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index 946f5e6..9bb0ba3 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -1,6 +1,9 @@ #include "HyperTwistTraining/HyperTwistTrainingSubsystem.h" +#include "HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.h" +#include "HyperTwistReplay/HyperTwistReplayReviewLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingCatalogLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingCoachLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h" @@ -85,6 +88,46 @@ FHyperTwistTrainingCoachMemorySnapshot UHyperTwistTrainingSubsystem::GetActiveCo return ActiveCoachMemorySnapshot; } +FHyperTwistReplayReviewAnalytics UHyperTwistTrainingSubsystem::GetActiveReplayReviewAnalytics() const +{ + return ActiveReplayReviewAnalytics; +} + +FHyperTwistRecognitionReplaySummary UHyperTwistTrainingSubsystem::GetActiveRecognitionReplaySummary() const +{ + return ActiveRecognitionReplaySummary; +} + +TArray UHyperTwistTrainingSubsystem::GetActiveCoachSignals() const +{ + return ActiveCoachSignals; +} + +FHyperTwistCoachBrief UHyperTwistTrainingSubsystem::GetActiveCoachBrief() const +{ + return ActiveCoachBrief; +} + +FHyperTwistTrainingCoachActionPlan UHyperTwistTrainingSubsystem::GetActiveCoachActionPlan() const +{ + return ActiveCoachActionPlan; +} + +FHyperTwistTrainingCoachActionPlanSummary UHyperTwistTrainingSubsystem::GetActiveCoachActionPlanSummary() const +{ + return ActiveCoachActionPlanSummary; +} + +FHyperTwistTrainingCoachActionPlanOutcomeSummary UHyperTwistTrainingSubsystem::GetActiveCoachActionPlanOutcomeSummary() const +{ + return ActiveCoachActionPlanOutcomeSummary; +} + +FHyperTwistCoachBrief UHyperTwistTrainingSubsystem::GetActiveCoachFollowUpBrief() const +{ + return ActiveCoachFollowUpBrief; +} + TArray UHyperTwistTrainingSubsystem::GetActiveLearnerMethodSegments() const { return ActiveLearnerMethodSegments; @@ -208,6 +251,18 @@ bool UHyperTwistTrainingSubsystem::TryGetStoredReviewPlan( ); } +bool UHyperTwistTrainingSubsystem::TryGetStoredCoachActionPlan( + const FString& PlanId, + FHyperTwistTrainingCoachActionPlan& OutActionPlan +) const +{ + return UHyperTwistTrainingRepositoryLibrary::TryGetCoachActionPlan( + TrainingRepositoryState, + PlanId, + OutActionPlan + ); +} + bool UHyperTwistTrainingSubsystem::TryGetStoredTrainingSessionTemplate( const FString& UserId, const FString& TemplateId, @@ -496,6 +551,136 @@ FHyperTwistTrainingDeck UHyperTwistTrainingSubsystem::BuildActiveReviewDeck(int3 ); } +FHyperTwistTrainingDeck UHyperTwistTrainingSubsystem::BuildActiveCoachRecommendedDeck(int32 MaxCases) const +{ + if (!ActiveRunState.ActiveDeck.IsStructurallyValid()) + { + return FHyperTwistTrainingDeck(); + } + + const int32 DesiredCaseCount = MaxCases > 0 ? MaxCases : 3; + TArray FocusCases; + for (const FString& FocusCaseId : ActiveCoachBrief.FocusCaseIds) + { + const FHyperTwistTrainingCase* MatchingCase = ActiveRunState.ActiveDeck.Cases.FindByPredicate( + [&FocusCaseId](const FHyperTwistTrainingCase& Candidate) + { + return Candidate.CaseId == FocusCaseId; + } + ); + if (MatchingCase != nullptr) + { + FocusCases.Add(*MatchingCase); + if (FocusCases.Num() >= DesiredCaseCount) + { + break; + } + } + } + + if (FocusCases.Num() == 0) + { + const FHyperTwistTrainingDeck ReviewDeck = BuildActiveReviewDeck(DesiredCaseCount); + if (ReviewDeck.IsStructurallyValid()) + { + return ReviewDeck; + } + + for (const FHyperTwistTrainingCase& Candidate : ActiveRunState.ActiveDeck.Cases) + { + FocusCases.Add(Candidate); + if (FocusCases.Num() >= DesiredCaseCount) + { + break; + } + } + } + + if (FocusCases.Num() == 0) + { + return FHyperTwistTrainingDeck(); + } + + FHyperTwistTrainingDeck FocusDeck = ActiveRunState.ActiveDeck; + FocusDeck.Cases = FocusCases; + FocusDeck.SelectionPolicy = ActiveCoachBrief.SuggestedSelectionPolicy; + if (ActiveCoachBrief.SuggestedMode != EHyperTwistTrainingDeliveryMode::Timer + && !FocusDeck.DeliveryModes.Contains(ActiveCoachBrief.SuggestedMode)) + { + FocusDeck.DeliveryModes.Insert(ActiveCoachBrief.SuggestedMode, 0); + } + if (!ActiveCoachBrief.Headline.IsEmpty()) + { + FocusDeck.Title = FString::Printf(TEXT("%s Focus"), *ActiveCoachBrief.Headline); + } + if (!FocusDeck.Tags.Contains(TEXT("coach-focus"))) + { + FocusDeck.Tags.Add(TEXT("coach-focus")); + } + if (!ActiveCoachBrief.FocusMethodSegmentId.IsEmpty() && FocusDeck.SubsetId.IsEmpty()) + { + FocusDeck.SubsetId = ActiveCoachBrief.FocusMethodSegmentId; + } + + return FocusDeck; +} + +FHyperTwistTrainingDeck UHyperTwistTrainingSubsystem::BuildActiveCoachFollowUpDeck(int32 MaxCases) const +{ + if (!ActiveRunState.ActiveDeck.IsStructurallyValid() || ActiveCoachFollowUpBrief.UserId.IsEmpty()) + { + return FHyperTwistTrainingDeck(); + } + + const int32 DesiredCaseCount = MaxCases > 0 ? MaxCases : 3; + TArray FocusCases; + for (const FString& FocusCaseId : ActiveCoachFollowUpBrief.FocusCaseIds) + { + const FHyperTwistTrainingCase* MatchingCase = ActiveRunState.ActiveDeck.Cases.FindByPredicate( + [&FocusCaseId](const FHyperTwistTrainingCase& Candidate) + { + return Candidate.CaseId == FocusCaseId; + } + ); + if (MatchingCase != nullptr) + { + FocusCases.Add(*MatchingCase); + if (FocusCases.Num() >= DesiredCaseCount) + { + break; + } + } + } + + if (FocusCases.Num() == 0) + { + return BuildActiveCoachRecommendedDeck(DesiredCaseCount); + } + + FHyperTwistTrainingDeck FocusDeck = ActiveRunState.ActiveDeck; + FocusDeck.Cases = FocusCases; + FocusDeck.SelectionPolicy = ActiveCoachFollowUpBrief.SuggestedSelectionPolicy; + if (ActiveCoachFollowUpBrief.SuggestedMode != EHyperTwistTrainingDeliveryMode::Timer + && !FocusDeck.DeliveryModes.Contains(ActiveCoachFollowUpBrief.SuggestedMode)) + { + FocusDeck.DeliveryModes.Insert(ActiveCoachFollowUpBrief.SuggestedMode, 0); + } + if (!ActiveCoachFollowUpBrief.Headline.IsEmpty()) + { + FocusDeck.Title = FString::Printf(TEXT("%s Follow-Up"), *ActiveCoachFollowUpBrief.Headline); + } + if (!FocusDeck.Tags.Contains(TEXT("coach-follow-up"))) + { + FocusDeck.Tags.Add(TEXT("coach-follow-up")); + } + if (!ActiveCoachFollowUpBrief.FocusMethodSegmentId.IsEmpty()) + { + FocusDeck.SubsetId = ActiveCoachFollowUpBrief.FocusMethodSegmentId; + } + + return FocusDeck; +} + FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartTrainingRunFromTemplate( const FString& TemplateId, const FString& UserId, @@ -592,6 +777,90 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartRecommendedReview return RunState; } +FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartCoachRecommendedRun( + const FString& SessionId, + int32 MaxCases +) +{ + const FHyperTwistTrainingDeck CoachDeck = BuildActiveCoachRecommendedDeck(MaxCases); + if (!CoachDeck.IsStructurallyValid()) + { + return FHyperTwistTrainingRunState(); + } + + const FString UserId = ActiveRunState.Session.UserId.IsEmpty() ? TEXT("local-user") : ActiveRunState.Session.UserId; + const FHyperTwistTrainingCoachActionPlan DraftActionPlan = + UHyperTwistTrainingRepositoryLibrary::BuildCoachActionPlan( + ActiveCoachBrief, + FString::Printf(TEXT("coach_plan_%s"), *SessionId), + FString() + ); + FHyperTwistTrainingRunState RunState = StartTrainingRunFromDeck( + CoachDeck, + UserId, + SessionId, + ActiveCoachBrief.SuggestedMode + ); + if (!RunState.IsStructurallyValid() || !DraftActionPlan.IsStructurallyValid()) + { + return RunState; + } + + ActiveCoachActionPlan = UHyperTwistTrainingRepositoryLibrary::ActivateCoachActionPlan( + DraftActionPlan, + RunState.Session.TrainingSessionId, + RunState.Session.StartedAtUtc + ); + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachActionPlan( + TrainingRepositoryState, + ActiveCoachActionPlan + ); + RefreshRepositoryViews(); + return RunState; +} + +FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartCoachFollowUpRun( + const FString& SessionId, + int32 MaxCases +) +{ + const FHyperTwistTrainingDeck FollowUpDeck = BuildActiveCoachFollowUpDeck(MaxCases); + if (!FollowUpDeck.IsStructurallyValid() || ActiveCoachFollowUpBrief.UserId.IsEmpty()) + { + return FHyperTwistTrainingRunState(); + } + + const FHyperTwistCoachBrief FollowUpBrief = ActiveCoachFollowUpBrief; + const FHyperTwistTrainingCoachActionPlan DraftActionPlan = + UHyperTwistTrainingRepositoryLibrary::BuildCoachActionPlan( + FollowUpBrief, + FString::Printf(TEXT("coach_follow_up_%s"), *SessionId), + FString() + ); + FHyperTwistTrainingRunState RunState = StartTrainingRunFromDeck( + FollowUpDeck, + FollowUpBrief.UserId, + SessionId, + FollowUpBrief.SuggestedMode + ); + if (!RunState.IsStructurallyValid() || !DraftActionPlan.IsStructurallyValid()) + { + return RunState; + } + + ActiveCoachActionPlan = UHyperTwistTrainingRepositoryLibrary::ActivateCoachActionPlan( + DraftActionPlan, + RunState.Session.TrainingSessionId, + RunState.Session.StartedAtUtc + ); + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachActionPlan( + TrainingRepositoryState, + ActiveCoachActionPlan + ); + RefreshRepositoryViews(); + return RunState; +} + FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartTrainingRunFromDeck( const FHyperTwistTrainingDeck& Deck, const FString& UserId, @@ -601,6 +870,8 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartTrainingRunFromDe { ActiveReviewPlanState = FHyperTwistTrainingReviewPlanState(); ActiveReviewFlowStatus = FHyperTwistTrainingReviewFlowStatus(); + ActiveCoachActionPlan = FHyperTwistTrainingCoachActionPlan(); + ActiveCoachFollowUpBrief = FHyperTwistCoachBrief(); ActiveRunState = UHyperTwistTrainingLibrary::StartTrainingRun(Deck, UserId, SessionId, Mode); bHasActiveRun = ActiveRunState.IsStructurallyValid(); RefreshSummary(); @@ -821,6 +1092,18 @@ FHyperTwistTrainingSession UHyperTwistTrainingSubsystem::CompleteActiveRun(bool ); ActiveReviewFlowStatus = UHyperTwistTrainingRepositoryLibrary::DeriveReviewFlowStatus(ActiveReviewPlanState); } + if (ActiveCoachActionPlan.IsStructurallyValid()) + { + ActiveCoachActionPlan = UHyperTwistTrainingRepositoryLibrary::FinalizeCoachActionPlan( + ActiveCoachActionPlan, + ActiveRunState.Session, + bAbortSession + ); + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachActionPlan( + TrainingRepositoryState, + ActiveCoachActionPlan + ); + } RefreshRepositoryViews(); return ActiveRunState.Session; } @@ -830,6 +1113,7 @@ void UHyperTwistTrainingSubsystem::ClearActiveRun() ActiveRunState = FHyperTwistTrainingRunState(); ActiveRunSummary = FHyperTwistTrainingSessionSummary(); ActiveReviewPlanState = FHyperTwistTrainingReviewPlanState(); + ActiveCoachActionPlan = FHyperTwistTrainingCoachActionPlan(); bHasActiveRun = false; RefreshRepositoryViews(); } @@ -885,6 +1169,14 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews() ActiveLearnerDeckStateSummary = FHyperTwistTrainingLearnerDeckStateSummary(); ActiveLearnerProfile = FHyperTwistTrainingLearnerProfile(); ActiveCoachMemorySnapshot = FHyperTwistTrainingCoachMemorySnapshot(); + ActiveReplayReviewAnalytics = FHyperTwistReplayReviewAnalytics(); + ActiveRecognitionReplaySummary = FHyperTwistRecognitionReplaySummary(); + ActiveCoachSignals.Reset(); + ActiveCoachBrief = FHyperTwistCoachBrief(); + ActiveCoachActionPlan = FHyperTwistTrainingCoachActionPlan(); + ActiveCoachActionPlanSummary = FHyperTwistTrainingCoachActionPlanSummary(); + ActiveCoachActionPlanOutcomeSummary = FHyperTwistTrainingCoachActionPlanOutcomeSummary(); + ActiveCoachFollowUpBrief = FHyperTwistCoachBrief(); ActiveLearnerMethodSegments.Reset(); ActiveLearnerPresets.Reset(); ActiveTrainingSessionTemplates.Reset(); @@ -945,6 +1237,12 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews() ActiveRunState.Session.UserId, ActiveLearnerDeckStateSummary.ReferenceUtc ); + ActiveReplayReviewAnalytics = UHyperTwistReplayReviewLibrary::DeriveReplayReviewAnalytics( + ActiveRunState.ReplayPacket + ); + ActiveRecognitionReplaySummary = UHyperTwistRecognitionReplayLibrary::DeriveRecognitionReplaySummary( + ActiveRunState.ReplayPacket + ); ActiveLearnerMethodSegments = UHyperTwistTrainingRepositoryLibrary::DeriveLearnerMethodSegments( TrainingRepositoryState, ActiveRunState.Session.UserId, @@ -974,6 +1272,45 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews() ActiveRunState.Session.UserId, ActiveLearnerDeckStateSummary.ReferenceUtc ); + ActiveCoachActionPlanSummary = UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanSummary( + TrainingRepositoryState, + ActiveRunState.Session.UserId, + ActiveLearnerDeckStateSummary.ReferenceUtc + ); + ActiveCoachActionPlanOutcomeSummary = ActiveCoachActionPlan.IsStructurallyValid() + ? UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanOutcomeSummary( + TrainingRepositoryState, + ActiveCoachActionPlan, + ActiveLearnerDeckStateSummary.ReferenceUtc + ) + : FHyperTwistTrainingCoachActionPlanOutcomeSummary(); + ActiveCoachSignals = UHyperTwistTrainingCoachLibrary::DeriveCoachSignals( + ActiveRunSummary, + ActiveDeckScopedStats, + ActiveLearnerDeckStateSummary, + ActiveLearnerProfile, + ActiveCoachMemorySnapshot, + ActiveReviewProgramSummary, + ActiveReplayReviewAnalytics, + ActiveRecognitionReplaySummary + ); + ActiveCoachBrief = UHyperTwistTrainingCoachLibrary::DeriveCoachBrief( + ActiveRunSummary, + ActiveDeckScopedStats, + ActiveLearnerDeckStateSummary, + ActiveLearnerProfile, + ActiveCoachMemorySnapshot, + ActiveReviewProgramSummary, + ActiveReplayReviewAnalytics, + ActiveRecognitionReplaySummary + ); + ActiveCoachFollowUpBrief = ActiveCoachActionPlanOutcomeSummary.IsStructurallyValid() + ? UHyperTwistTrainingCoachLibrary::DeriveCoachFollowUpBrief( + ActiveCoachBrief, + ActiveCoachActionPlan, + ActiveCoachActionPlanOutcomeSummary + ) + : FHyperTwistCoachBrief(); } TArray UHyperTwistTrainingSubsystem::BuildTrainingSessionTemplatesForUser( diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h index 46ac3ee..6bd7680 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h @@ -3,8 +3,11 @@ #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "HyperTwistCore/HyperTwistCoreTypes.h" +#include "HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.h" #include "HyperTwistReplay/HyperTwistReplayTypes.h" +#include "HyperTwistReplay/HyperTwistReplayReviewLibrary.h" #include "HyperTwistRecognition/HyperTwistRecognitionTypes.h" +#include "HyperTwistTraining/HyperTwistTrainingCoachLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingTypes.h" #include "HyperTwistContractLibrary.generated.h" @@ -161,6 +164,30 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") static FHyperTwistTrainingReviewProgramSummary MakeSampleTrainingReviewProgramSummary(); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Replay") + static FHyperTwistReplayReviewAnalytics MakeSampleReplayReviewAnalytics(); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Recognition") + static FHyperTwistRecognitionReplaySummary MakeSampleRecognitionReplaySummary(); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static TArray MakeSampleTrainingCoachSignals(); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistCoachBrief MakeSampleTrainingCoachBrief(); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistTrainingCoachActionPlan MakeSampleTrainingCoachActionPlan(); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistTrainingCoachActionPlanSummary MakeSampleTrainingCoachActionPlanSummary(); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistTrainingCoachActionPlanOutcomeSummary MakeSampleTrainingCoachActionPlanOutcomeSummary(); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistCoachBrief MakeSampleTrainingCoachFollowUpBrief(); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Validation") static bool IsPuzzleStateStructurallyValid(const FHyperTwistPuzzleState& State); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.h new file mode 100644 index 0000000..14958ed --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.h @@ -0,0 +1,112 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "HyperTwistReplay/HyperTwistReplayTypes.h" +#include "HyperTwistRecognitionReplayLibrary.generated.h" + +USTRUCT(BlueprintType) +struct FHyperTwistRecognitionReplayEpisode +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString EpisodeId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Stage; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 PreviewCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ScanCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 CorrectionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 StartedAtMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 CompletedAtMs = INDEX_NONE; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DurationMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float AverageConfidence = -1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float PeakConfidence = -1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float FinalCommitConfidence = -1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FinalCommittedUnit; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCompleted = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bAmbiguous = false; +}; + +USTRUCT(BlueprintType) +struct FHyperTwistRecognitionReplaySummary +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalRecognitionEventCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalPreviewCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalScanCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalCommitCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalCorrectionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 EpisodeCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 CompletedEpisodeCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AmbiguousEpisodeCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AverageEpisodeDurationMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float AverageCommitConfidence = -1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LastCommittedUnit; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray CoveredStages; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Episodes; +}; + +UCLASS() +class UNREALHYPERTWIST_API UHyperTwistRecognitionReplayLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintPure, Category = "HyperTwist|Recognition") + static TArray CollectRecognitionReplayEpisodes(const FHyperTwistReplayPacket& Packet); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Recognition") + static FHyperTwistRecognitionReplaySummary DeriveRecognitionReplaySummary(const FHyperTwistReplayPacket& Packet); +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistReplay/HyperTwistReplayReviewLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistReplay/HyperTwistReplayReviewLibrary.h new file mode 100644 index 0000000..f0e55ff --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistReplay/HyperTwistReplayReviewLibrary.h @@ -0,0 +1,151 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "HyperTwistReplay/HyperTwistReplayTypes.h" +#include "HyperTwistReplayReviewLibrary.generated.h" + +UENUM(BlueprintType) +enum class EHyperTwistReplayReviewAnchorKind : uint8 +{ + InspectionStart UMETA(DisplayName = "Inspection Start"), + InspectionEnd UMETA(DisplayName = "Inspection End"), + TimerStart UMETA(DisplayName = "Timer Start"), + FirstMove UMETA(DisplayName = "First Move"), + PhaseBoundary UMETA(DisplayName = "Phase Boundary"), + RecognitionCommit UMETA(DisplayName = "Recognition Commit"), + RecognitionCorrection UMETA(DisplayName = "Recognition Correction"), + Pause UMETA(DisplayName = "Pause"), + Resume UMETA(DisplayName = "Resume"), + DeviceSync UMETA(DisplayName = "Device Sync"), + StateSnapshot UMETA(DisplayName = "State Snapshot"), + Annotation UMETA(DisplayName = "Annotation"), + SolveEnd UMETA(DisplayName = "Solve End") +}; + +USTRUCT(BlueprintType) +struct FHyperTwistReplayReviewAnchor +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistReplayReviewAnchorKind AnchorKind = EHyperTwistReplayReviewAnchorKind::Annotation; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString EventId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 Sequence = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Label; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PhaseId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PhaseKind; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bMarksCompletion = false; +}; + +USTRUCT(BlueprintType) +struct FHyperTwistReplayReviewAnalytics +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 EventCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TimerStartAtMs = INDEX_NONE; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SolveEndAtMs = INDEX_NONE; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DurationAfterTimerStartMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 FirstMoveAtMs = INDEX_NONE; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 LastMoveAtMs = INDEX_NONE; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 PauseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalPausedMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 LongestIdleGapMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AverageMoveGapMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 UniquePhaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 PhaseBoundaryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LongestPhaseKind; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 LongestPhaseDurationMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RecognitionPreviewCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RecognitionCommitCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RecognitionCorrectionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float AverageRecognitionConfidence = -1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float LowestRecognitionConfidence = -1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float HighestRecognitionConfidence = -1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DeviceSyncCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 StateSnapshotCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AnnotationCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasRecognitionData = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasImportedSource = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray ReviewAnchors; +}; + +UCLASS() +class UNREALHYPERTWIST_API UHyperTwistReplayReviewLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintPure, Category = "HyperTwist|Replay") + static TArray CollectReplayReviewAnchors(const FHyperTwistReplayPacket& Packet); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Replay") + static FHyperTwistReplayReviewAnalytics DeriveReplayReviewAnalytics(const FHyperTwistReplayPacket& Packet); +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h new file mode 100644 index 0000000..7e0870a --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h @@ -0,0 +1,172 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingTypes.h" +#include "HyperTwistReplay/HyperTwistReplayReviewLibrary.h" +#include "HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.h" +#include "HyperTwistTrainingCoachLibrary.generated.h" + +UENUM(BlueprintType) +enum class EHyperTwistCoachPriority : uint8 +{ + Informational UMETA(DisplayName = "Informational"), + Advisory UMETA(DisplayName = "Advisory"), + Elevated UMETA(DisplayName = "Elevated"), + Critical UMETA(DisplayName = "Critical") +}; + +UENUM(BlueprintType) +enum class EHyperTwistCoachFocusLane : uint8 +{ + ReviewBacklog UMETA(DisplayName = "Review Backlog"), + Cadence UMETA(DisplayName = "Cadence"), + Recognition UMETA(DisplayName = "Recognition"), + Execution UMETA(DisplayName = "Execution"), + Accuracy UMETA(DisplayName = "Accuracy"), + Variety UMETA(DisplayName = "Variety"), + Stability UMETA(DisplayName = "Stability") +}; + +USTRUCT(BlueprintType) +struct FHyperTwistCoachSignal +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SignalId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistCoachFocusLane FocusLane = EHyperTwistCoachFocusLane::Stability; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistCoachPriority Priority = EHyperTwistCoachPriority::Informational; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float Score = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Detail; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDeliveryMode SuggestedMode = EHyperTwistTrainingDeliveryMode::Timer; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingSelectionPolicy SuggestedSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SuggestedDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SuggestedMethodSegmentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray SuggestedCaseIds; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bNeedsImmediateAttention = false; +}; + +USTRUCT(BlueprintType) +struct FHyperTwistCoachBrief +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ReferenceUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TrainingSessionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceReviewPlanId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistCoachPriority Priority = EHyperTwistCoachPriority::Informational; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistCoachFocusLane FocusLane = EHyperTwistCoachFocusLane::Stability; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float PriorityScore = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PrimaryActionLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDeliveryMode SuggestedMode = EHyperTwistTrainingDeliveryMode::Timer; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingSelectionPolicy SuggestedSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusMethodSegmentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray FocusCaseIds; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Signals; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bNeedsCoachReview = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bNeedsCadenceRecovery = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bNeedsVarietyRotation = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bPreferRecognitionReplayReview = false; +}; + +UCLASS() +class UNREALHYPERTWIST_API UHyperTwistTrainingCoachLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static TArray DeriveCoachSignals( + const FHyperTwistTrainingSessionSummary& SessionSummary, + const FHyperTwistTrainingDeckScopedStats& DeckScopedStats, + const FHyperTwistTrainingLearnerDeckStateSummary& LearnerDeckStateSummary, + const FHyperTwistTrainingLearnerProfile& LearnerProfile, + const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot, + const FHyperTwistTrainingReviewProgramSummary& ReviewProgramSummary, + const FHyperTwistReplayReviewAnalytics& ReplayReviewAnalytics, + const FHyperTwistRecognitionReplaySummary& RecognitionReplaySummary + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistCoachBrief DeriveCoachBrief( + const FHyperTwistTrainingSessionSummary& SessionSummary, + const FHyperTwistTrainingDeckScopedStats& DeckScopedStats, + const FHyperTwistTrainingLearnerDeckStateSummary& LearnerDeckStateSummary, + const FHyperTwistTrainingLearnerProfile& LearnerProfile, + const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot, + const FHyperTwistTrainingReviewProgramSummary& ReviewProgramSummary, + const FHyperTwistReplayReviewAnalytics& ReplayReviewAnalytics, + const FHyperTwistRecognitionReplaySummary& RecognitionReplaySummary + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistCoachBrief DeriveCoachFollowUpBrief( + const FHyperTwistCoachBrief& CurrentBrief, + const FHyperTwistTrainingCoachActionPlan& ActionPlan, + const FHyperTwistTrainingCoachActionPlanOutcomeSummary& OutcomeSummary + ); +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h index 6b754ee..123e9f9 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h @@ -2,6 +2,7 @@ #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingCoachLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingTypes.h" #include "HyperTwistTrainingRepositoryLibrary.generated.h" @@ -306,6 +307,60 @@ public: const FString& UserId ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static FHyperTwistTrainingCoachActionPlan BuildCoachActionPlan( + const FHyperTwistCoachBrief& CoachBrief, + const FString& PlanId, + const FString& ReferenceUtc + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static FHyperTwistTrainingCoachActionPlan ActivateCoachActionPlan( + const FHyperTwistTrainingCoachActionPlan& ActionPlan, + const FString& TrainingSessionId, + const FString& StartedAtUtc + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static FHyperTwistTrainingCoachActionPlan FinalizeCoachActionPlan( + const FHyperTwistTrainingCoachActionPlan& ActionPlan, + const FHyperTwistTrainingSession& Session, + bool bAbortPlan + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static TArray ListCoachActionPlansForUser( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Repository|Coach") + static bool TryGetCoachActionPlan( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& PlanId, + FHyperTwistTrainingCoachActionPlan& OutActionPlan + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static FHyperTwistTrainingRepositoryState UpsertCoachActionPlan( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingCoachActionPlan& ActionPlan + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static FHyperTwistTrainingCoachActionPlanSummary DeriveCoachActionPlanSummary( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& ReferenceUtc + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static FHyperTwistTrainingCoachActionPlanOutcomeSummary DeriveCoachActionPlanOutcomeSummary( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingCoachActionPlan& ActionPlan, + const FString& ReferenceUtc + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository") static FHyperTwistTrainingCaseRecommendation MakeCaseRecommendation( const FHyperTwistTrainingCase& TrainingCase, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h index a36f362..500bf4f 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h @@ -2,6 +2,9 @@ #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" +#include "HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.h" +#include "HyperTwistReplay/HyperTwistReplayReviewLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingCoachLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingTypes.h" #include "HyperTwistTrainingRuntimeLibrary.generated.h" @@ -66,6 +69,30 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static FHyperTwistTrainingCoachMemorySnapshot GetActiveTrainingCoachMemorySnapshot(UObject* WorldContextObject); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistReplayReviewAnalytics GetActiveTrainingReplayReviewAnalytics(UObject* WorldContextObject); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistRecognitionReplaySummary GetActiveTrainingRecognitionReplaySummary(UObject* WorldContextObject); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static TArray GetActiveTrainingCoachSignals(UObject* WorldContextObject); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistCoachBrief GetActiveTrainingCoachBrief(UObject* WorldContextObject); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingCoachActionPlan GetActiveTrainingCoachActionPlan(UObject* WorldContextObject); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingCoachActionPlanSummary GetActiveTrainingCoachActionPlanSummary(UObject* WorldContextObject); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingCoachActionPlanOutcomeSummary GetActiveTrainingCoachActionPlanOutcomeSummary(UObject* WorldContextObject); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistCoachBrief GetActiveTrainingCoachFollowUpBrief(UObject* WorldContextObject); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static TArray GetActiveTrainingLearnerMethodSegments(UObject* WorldContextObject); @@ -139,6 +166,13 @@ public: FHyperTwistTrainingReviewPlanState& OutReviewPlan ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static bool TryGetStoredTrainingCoachActionPlan( + UObject* WorldContextObject, + const FString& PlanId, + FHyperTwistTrainingCoachActionPlan& OutActionPlan + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static bool TryGetStoredTrainingSessionTemplate( UObject* WorldContextObject, @@ -252,6 +286,18 @@ public: int32 MaxCases ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingDeck BuildActiveCoachRecommendedDeck( + UObject* WorldContextObject, + int32 MaxCases + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingDeck BuildActiveCoachFollowUpDeck( + UObject* WorldContextObject, + int32 MaxCases + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static FHyperTwistTrainingRunState StartTrainingRunFromTemplate( UObject* WorldContextObject, @@ -268,6 +314,20 @@ public: EHyperTwistTrainingDeliveryMode Mode ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingRunState StartCoachRecommendedRun( + UObject* WorldContextObject, + const FString& SessionId, + int32 MaxCases + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingRunState StartCoachFollowUpRun( + UObject* WorldContextObject, + const FString& SessionId, + int32 MaxCases + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static FHyperTwistTrainingRunState StartSampleClassicTrainingRun( UObject* WorldContextObject, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h index 9bbd2e9..746e759 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h @@ -2,6 +2,9 @@ #include "CoreMinimal.h" #include "Subsystems/GameInstanceSubsystem.h" +#include "HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.h" +#include "HyperTwistReplay/HyperTwistReplayReviewLibrary.h" +#include "HyperTwistTraining/HyperTwistTrainingCoachLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingTypes.h" #include "HyperTwistTrainingSubsystem.generated.h" @@ -59,6 +62,30 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") FHyperTwistTrainingCoachMemorySnapshot GetActiveCoachMemorySnapshot() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") + FHyperTwistReplayReviewAnalytics GetActiveReplayReviewAnalytics() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") + FHyperTwistRecognitionReplaySummary GetActiveRecognitionReplaySummary() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + TArray GetActiveCoachSignals() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistCoachBrief GetActiveCoachBrief() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachActionPlan GetActiveCoachActionPlan() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachActionPlanSummary GetActiveCoachActionPlanSummary() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachActionPlanOutcomeSummary GetActiveCoachActionPlanOutcomeSummary() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistCoachBrief GetActiveCoachFollowUpBrief() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") TArray GetActiveLearnerMethodSegments() const; @@ -125,6 +152,12 @@ public: FHyperTwistTrainingReviewPlanState& OutReviewPlan ) const; + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") + bool TryGetStoredCoachActionPlan( + const FString& PlanId, + FHyperTwistTrainingCoachActionPlan& OutActionPlan + ) const; + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") bool TryGetStoredTrainingSessionTemplate( const FString& UserId, @@ -208,6 +241,12 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") FHyperTwistTrainingDeck BuildActiveReviewDeck(int32 MaxCases) const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingDeck BuildActiveCoachRecommendedDeck(int32 MaxCases) const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingDeck BuildActiveCoachFollowUpDeck(int32 MaxCases) const; + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") FHyperTwistTrainingRunState StartTrainingRunFromTemplate( const FString& TemplateId, @@ -222,6 +261,18 @@ public: EHyperTwistTrainingDeliveryMode Mode ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingRunState StartCoachRecommendedRun( + const FString& SessionId, + int32 MaxCases + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingRunState StartCoachFollowUpRun( + const FString& SessionId, + int32 MaxCases + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") FHyperTwistTrainingRunState StartTrainingRunFromDeck( const FHyperTwistTrainingDeck& Deck, @@ -342,6 +393,30 @@ private: UPROPERTY() FHyperTwistTrainingCoachMemorySnapshot ActiveCoachMemorySnapshot; + UPROPERTY() + FHyperTwistReplayReviewAnalytics ActiveReplayReviewAnalytics; + + UPROPERTY() + FHyperTwistRecognitionReplaySummary ActiveRecognitionReplaySummary; + + UPROPERTY() + TArray ActiveCoachSignals; + + UPROPERTY() + FHyperTwistCoachBrief ActiveCoachBrief; + + UPROPERTY() + FHyperTwistTrainingCoachActionPlan ActiveCoachActionPlan; + + UPROPERTY() + FHyperTwistTrainingCoachActionPlanSummary ActiveCoachActionPlanSummary; + + UPROPERTY() + FHyperTwistTrainingCoachActionPlanOutcomeSummary ActiveCoachActionPlanOutcomeSummary; + + UPROPERTY() + FHyperTwistCoachBrief ActiveCoachFollowUpBrief; + UPROPERTY() TArray ActiveLearnerMethodSegments; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 413fc2e..43da0c1 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -2639,6 +2639,328 @@ struct FHyperTwistTrainingReviewProgramSummary } }; +UENUM(BlueprintType) +enum class EHyperTwistTrainingCoachActionPlanState : uint8 +{ + Draft UMETA(DisplayName = "Draft"), + Active UMETA(DisplayName = "Active"), + Completed UMETA(DisplayName = "Completed"), + Aborted UMETA(DisplayName = "Aborted") +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachActionPlanEntry +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ActionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CaseId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString MethodSegmentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ActionLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float PriorityScore = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDeliveryMode SuggestedMode = EHyperTwistTrainingDeliveryMode::Timer; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingSelectionPolicy SuggestedSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bPreferRecognitionReplayReview = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCompleted = false; + + bool IsStructurallyValid() const + { + return !ActionId.IsEmpty() + && !ActionLabel.IsEmpty() + && (!CaseId.IsEmpty() || !DeckId.IsEmpty() || !MethodSegmentId.IsEmpty()); + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachActionPlan +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PlanId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ReferenceUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceSessionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceReviewPlanId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusMethodSegmentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusLaneLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PriorityLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float PriorityScore = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PrimaryActionLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDeliveryMode SuggestedMode = EHyperTwistTrainingDeliveryMode::Timer; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingSelectionPolicy SuggestedSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray FocusCaseIds; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Entries; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingCoachActionPlanState PlanState = EHyperTwistTrainingCoachActionPlanState::Draft; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CreatedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StartedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CompletedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bNeedsCoachReview = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bNeedsCadenceRecovery = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bNeedsVarietyRotation = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bPreferRecognitionReplayReview = false; + + bool IsStructurallyValid() const + { + return !PlanId.IsEmpty() + && !UserId.IsEmpty() + && !Headline.IsEmpty() + && Entries.Num() > 0; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachActionPlanSummary +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ReferenceUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusMethodSegmentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CurrentPlanId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LastCompletedPlanId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalPlanCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DraftPlanCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ActivePlanCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 CompletedPlanCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AbortedPlanCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 CompletedEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 OpenEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float AveragePriorityScore = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AverageCompletedPlanDurationMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 BestCompletedPlanDurationMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDeliveryMode DominantSuggestedMode = EHyperTwistTrainingDeliveryMode::Timer; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DominantFocusLaneLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DominantActionLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float ActionPressureScore = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasRecognitionBiasedPlans = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bNeedsCoachIntervention = false; + +bool IsStructurallyValid() const + { + return !UserId.IsEmpty() && TotalPlanCount > 0; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachActionPlanOutcomeSummary +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ReferenceUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PlanId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceSessionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusMethodSegmentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PrimaryActionLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingCoachActionPlanState PlanState = EHyperTwistTrainingCoachActionPlanState::Draft; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDeliveryMode SuggestedMode = EHyperTwistTrainingDeliveryMode::Timer; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 FocusCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TouchedFocusCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 CompletedFocusCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 UntouchedFocusCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AttemptCount = 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 ReplayBackedAttemptCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RecognitionAttemptCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AverageTotalTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 BestTotalTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AverageRecognitionTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 PlanDurationMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float CompletionRate = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float OutcomeScore = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray FollowUpCaseIds; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bMetPrimaryGoal = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bNeedsFollowUpPlan = false; + + bool IsStructurallyValid() const + { + return !UserId.IsEmpty() && !PlanId.IsEmpty(); + } +}; + USTRUCT(BlueprintType) struct FHyperTwistTrainingRepositoryState { @@ -2661,6 +2983,9 @@ struct FHyperTwistTrainingRepositoryState UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") TArray StoredMethodDrillBatches; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray StoredCoachActionPlans; }; USTRUCT(BlueprintType) @@ -2722,6 +3047,15 @@ struct FHyperTwistTrainingRepositoryIntegrityReport UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") int32 DuplicateMethodDrillBatchCount = 0; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalCoachActionPlanCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 InvalidCoachActionPlanCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DuplicateCoachActionPlanCount = 0; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") TArray RepairActions; @@ -2738,6 +3072,8 @@ struct FHyperTwistTrainingRepositoryIntegrityReport || InvalidMethodDrillFavoriteCount > 0 || DuplicateMethodDrillFavoriteCount > 0 || InvalidMethodDrillBatchCount > 0 - || DuplicateMethodDrillBatchCount > 0; + || DuplicateMethodDrillBatchCount > 0 + || InvalidCoachActionPlanCount > 0 + || DuplicateCoachActionPlanCount > 0; } };