Add replay-driven coaching analytics and follow-ups

This commit is contained in:
axiomlogicnexus 2026-04-26 02:18:35 +02:00
parent 0bc79a8173
commit 840a8e5d0b
15 changed files with 3429 additions and 1 deletions

View file

@ -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<FHyperTwistCoachSignal> 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();

View file

@ -0,0 +1,235 @@
#include "HyperTwistRecognition/HyperTwistRecognitionReplayLibrary.h"
#include "JsonObjectConverter.h"
namespace HyperTwistRecognitionReplayLibraryInternal
{
template <typename TStruct>
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<FHyperTwistRecognitionReplayEpisode>& 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<FHyperTwistRecognitionReplayEpisode> UHyperTwistRecognitionReplayLibrary::CollectRecognitionReplayEpisodes(const FHyperTwistReplayPacket& Packet)
{
TArray<FHyperTwistRecognitionReplayEpisode> 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<float>(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<float>(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<float>(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<FString> 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<float>(CommitConfidenceCount) : -1.0f;
return Summary;
}

View file

@ -0,0 +1,344 @@
#include "HyperTwistReplay/HyperTwistReplayReviewLibrary.h"
#include "HyperTwistReplay/HyperTwistReplayLibrary.h"
#include "JsonObjectConverter.h"
namespace HyperTwistReplayReviewLibraryInternal
{
template <typename TStruct>
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<FHyperTwistReplayReviewAnchor>& 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<FHyperTwistReplayReviewAnchor> UHyperTwistReplayReviewLibrary::CollectReplayReviewAnchors(const FHyperTwistReplayPacket& Packet)
{
TArray<FHyperTwistReplayReviewAnchor> 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<FString> 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<float>(ConfidenceCount) : -1.0f;
for (const FHyperTwistPhaseTiming& PhaseTiming : Summary.PhaseTimings)
{
if (PhaseTiming.DurationMs > Analytics.LongestPhaseDurationMs)
{
Analytics.LongestPhaseDurationMs = PhaseTiming.DurationMs;
Analytics.LongestPhaseKind = PhaseTiming.PhaseKind;
}
}
return Analytics;
}

View file

@ -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<FHyperTwistCoachSignal> 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<FHyperTwistCoachSignal> Signals;
if (CoachMemorySnapshot.bNeedsCoachIntervention
|| ReviewProgramSummary.bNeedsCoachIntervention
|| LearnerDeckStateSummary.OverdueCaseCount >= 3)
{
const float Score = 90.0f
+ FMath::Min(static_cast<float>(LearnerDeckStateSummary.OverdueCaseCount) * 2.0f, 8.0f)
+ FMath::Min(static_cast<float>(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<float>(RecognitionReplaySummary.AmbiguousEpisodeCount * 5)
+ static_cast<float>(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<float>(SessionSummary.TotalMistakes) / SessionSummary.CompletedAttemptCount) * 18.0f
: 0.0f;
const float Score = 52.0f
+ MistakePressure
+ FMath::Min(static_cast<float>(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<float>(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<float>(LearnerDeckStateSummary.DueCaseCount), 8.0f)
+ FMath::Min(static_cast<float>(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<float>(OutcomeSummary.UntouchedFocusCaseCount) * 4.0f)
+ (static_cast<float>(OutcomeSummary.FailureCount) * 3.0f)
+ (static_cast<float>(OutcomeSummary.TimeoutCount) * 4.0f)
+ (static_cast<float>(OutcomeSummary.DNFCount) * 5.0f)
+ (static_cast<float>(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;
}

View file

@ -296,6 +296,11 @@ namespace HyperTwistTrainingRepositoryLibraryInternal
return UserId + CaseKeySeparator + BatchId;
}
FString MakeCoachActionPlanKey(const FString& UserId, const FString& PlanId)
{
return UserId + CaseKeySeparator + PlanId;
}
TArray<FString> DeduplicateCaseIds(const TArray<FString>& CaseIds)
{
TArray<FString> 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<FString, int32> EntryIndexById;
TArray<FHyperTwistTrainingCoachActionPlanEntry> 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<FString> 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<FString, int32> 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<FHyperTwistTrainingSessionTemplate> 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<EHyperTwistCoachFocusLane>()->GetNameStringByValue(
static_cast<int64>(CoachBrief.FocusLane)
);
ActionPlan.PriorityLabel = StaticEnum<EHyperTwistCoachPriority>()->GetNameStringByValue(
static_cast<int64>(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<FHyperTwistTrainingCoachActionPlan> UHyperTwistTrainingRepositoryLibrary::ListCoachActionPlansForUser(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId
)
{
TArray<FHyperTwistTrainingCoachActionPlan> 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<FHyperTwistTrainingCoachActionPlan> ActionPlans = ListCoachActionPlansForUser(
RepositoryState,
UserId
);
if (ActionPlans.Num() == 0)
{
return Summary;
}
Summary.TotalPlanCount = ActionPlans.Num();
int32 SuggestedModeCounts[5] = {0, 0, 0, 0, 0};
TMap<FString, int32> FocusLaneCounts;
TMap<FString, int32> 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<int32>(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<float>(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<EHyperTwistTrainingDeliveryMode>(Index);
}
}
int32 BestFocusLaneCount = 0;
for (const TPair<FString, int32>& Pair : FocusLaneCounts)
{
if (Pair.Value > BestFocusLaneCount)
{
BestFocusLaneCount = Pair.Value;
Summary.DominantFocusLaneLabel = Pair.Key;
}
}
int32 BestActionLabelCount = 0;
for (const TPair<FString, int32>& Pair : ActionLabelCounts)
{
if (Pair.Value > BestActionLabelCount)
{
BestActionLabelCount = Pair.Value;
Summary.DominantActionLabel = Pair.Key;
}
}
Summary.ActionPressureScore =
static_cast<float>(Summary.ActivePlanCount * 3)
+ static_cast<float>(Summary.DraftPlanCount * 2)
+ static_cast<float>(Summary.AbortedPlanCount * 2)
+ (static_cast<float>(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<FString> FocusCaseIds;
for (const FString& FocusCaseId : NormalizedPlan.FocusCaseIds)
{
if (!FocusCaseId.IsEmpty())
{
FocusCaseIds.Add(FocusCaseId);
}
}
TSet<FString> 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<FString> 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<float>(Summary.CompletedFocusCaseCount)
/ static_cast<float>(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<float>(Summary.SuccessCount) * 4.0f)
- (static_cast<float>(Summary.FailureCount) * 2.0f)
- (static_cast<float>(Summary.TimeoutCount) * 3.0f)
- (static_cast<float>(Summary.DNFCount) * 4.0f)
- (static_cast<float>(Summary.AbortedCount) * 5.0f)
- (static_cast<float>(Summary.UntouchedFocusCaseCount) * 4.0f)
+ (static_cast<float>(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,

View file

@ -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<FHyperTwistCoachSignal> UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachSignals(
UObject* WorldContextObject
)
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject))
{
return TrainingSubsystem->GetActiveCoachSignals();
}
return TArray<FHyperTwistCoachSignal>();
}
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<FHyperTwistTrainingMethodSegment> 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,

View file

@ -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<FHyperTwistCoachSignal> 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<FHyperTwistTrainingMethodSegment> 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<FHyperTwistTrainingCase> 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<FHyperTwistTrainingCase> 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<FHyperTwistTrainingSessionTemplate> UHyperTwistTrainingSubsystem::BuildTrainingSessionTemplatesForUser(

View file

@ -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<FHyperTwistCoachSignal> 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);

View file

@ -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<FString> CoveredStages;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistRecognitionReplayEpisode> Episodes;
};
UCLASS()
class UNREALHYPERTWIST_API UHyperTwistRecognitionReplayLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Recognition")
static TArray<FHyperTwistRecognitionReplayEpisode> CollectRecognitionReplayEpisodes(const FHyperTwistReplayPacket& Packet);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Recognition")
static FHyperTwistRecognitionReplaySummary DeriveRecognitionReplaySummary(const FHyperTwistReplayPacket& Packet);
};

View file

@ -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<FHyperTwistReplayReviewAnchor> ReviewAnchors;
};
UCLASS()
class UNREALHYPERTWIST_API UHyperTwistReplayReviewLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Replay")
static TArray<FHyperTwistReplayReviewAnchor> CollectReplayReviewAnchors(const FHyperTwistReplayPacket& Packet);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Replay")
static FHyperTwistReplayReviewAnalytics DeriveReplayReviewAnalytics(const FHyperTwistReplayPacket& Packet);
};

View file

@ -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<FString> 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<FString> FocusCaseIds;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistCoachSignal> 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<FHyperTwistCoachSignal> 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
);
};

View file

@ -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<FHyperTwistTrainingCoachActionPlan> 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,

View file

@ -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<FHyperTwistCoachSignal> 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<FHyperTwistTrainingMethodSegment> 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,

View file

@ -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<FHyperTwistCoachSignal> 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<FHyperTwistTrainingMethodSegment> 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<FHyperTwistCoachSignal> ActiveCoachSignals;
UPROPERTY()
FHyperTwistCoachBrief ActiveCoachBrief;
UPROPERTY()
FHyperTwistTrainingCoachActionPlan ActiveCoachActionPlan;
UPROPERTY()
FHyperTwistTrainingCoachActionPlanSummary ActiveCoachActionPlanSummary;
UPROPERTY()
FHyperTwistTrainingCoachActionPlanOutcomeSummary ActiveCoachActionPlanOutcomeSummary;
UPROPERTY()
FHyperTwistCoachBrief ActiveCoachFollowUpBrief;
UPROPERTY()
TArray<FHyperTwistTrainingMethodSegment> ActiveLearnerMethodSegments;

View file

@ -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<FString> FocusCaseIds;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingCoachActionPlanEntry> 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<FString> 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<FHyperTwistTrainingMethodDrillBatch> StoredMethodDrillBatches;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingCoachActionPlan> 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<FString> RepairActions;
@ -2738,6 +3072,8 @@ struct FHyperTwistTrainingRepositoryIntegrityReport
|| InvalidMethodDrillFavoriteCount > 0
|| DuplicateMethodDrillFavoriteCount > 0
|| InvalidMethodDrillBatchCount > 0
|| DuplicateMethodDrillBatchCount > 0;
|| DuplicateMethodDrillBatchCount > 0
|| InvalidCoachActionPlanCount > 0
|| DuplicateCoachActionPlanCount > 0;
}
};