Persist coach session queue state

This commit is contained in:
axiomlogicnexus 2026-04-26 04:02:46 +02:00
parent 8d740cb997
commit 8df9776fcf
11 changed files with 1368 additions and 1 deletions

View file

@ -628,6 +628,16 @@ FHyperTwistTrainingRepositoryState UHyperTwistContractLibrary::MakeSampleTrainin
);
}
const FHyperTwistTrainingCoachSessionQueueState CoachSessionQueueState =
MakeSampleTrainingCoachSessionQueueState();
if (CoachSessionQueueState.IsStructurallyValid())
{
RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState(
RepositoryState,
CoachSessionQueueState
);
}
return RepositoryState;
}
@ -686,6 +696,15 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistContractLibrary::MakeSam
}
RepositoryState.StoredCoachActionPlans.Add(FHyperTwistTrainingCoachActionPlan());
const FHyperTwistTrainingCoachSessionQueueState CoachSessionQueueState =
MakeSampleTrainingCoachSessionQueueState();
if (CoachSessionQueueState.IsStructurallyValid())
{
RepositoryState.StoredCoachSessionQueues.Add(CoachSessionQueueState);
RepositoryState.StoredCoachSessionQueues.Add(CoachSessionQueueState);
}
RepositoryState.StoredCoachSessionQueues.Add(FHyperTwistTrainingCoachSessionQueueState());
return UHyperTwistTrainingRepositoryLibrary::DeriveRepositoryIntegrityReport(RepositoryState);
}
@ -1292,6 +1311,25 @@ FHyperTwistCoachBrief UHyperTwistContractLibrary::MakeSampleTrainingCoachFollowU
);
}
FHyperTwistCoachSessionQueueSummary UHyperTwistContractLibrary::MakeSampleTrainingCoachSessionQueueSummary()
{
return UHyperTwistTrainingCoachLibrary::DeriveCoachSessionQueueSummary(
MakeSampleTrainingCoachBrief(),
MakeSampleTrainingCoachFollowUpBrief(),
MakeSampleTrainingCoachArchivePolicySummary(),
MakeSampleTrainingReviewProgramSummary()
);
}
FHyperTwistTrainingCoachSessionQueueState UHyperTwistContractLibrary::MakeSampleTrainingCoachSessionQueueState()
{
return UHyperTwistTrainingRepositoryLibrary::BuildCoachSessionQueueState(
MakeSampleTrainingCoachSessionQueueSummary(),
TEXT("coach_queue_active"),
TEXT("2026-04-28T12:12:00Z")
);
}
bool UHyperTwistContractLibrary::IsPuzzleStateStructurallyValid(const FHyperTwistPuzzleState& State)
{
return State.IsStructurallyValid();

View file

@ -569,3 +569,148 @@ FHyperTwistCoachBrief UHyperTwistTrainingCoachLibrary::DeriveCoachFollowUpBrief(
return Brief;
}
FHyperTwistCoachSessionQueueSummary UHyperTwistTrainingCoachLibrary::DeriveCoachSessionQueueSummary(
const FHyperTwistCoachBrief& CurrentBrief,
const FHyperTwistCoachBrief& FollowUpBrief,
const FHyperTwistTrainingCoachArchivePolicySummary& ArchivePolicySummary,
const FHyperTwistTrainingReviewProgramSummary& ReviewProgramSummary)
{
FHyperTwistCoachSessionQueueSummary Summary;
Summary.UserId = !CurrentBrief.UserId.IsEmpty()
? CurrentBrief.UserId
: FollowUpBrief.UserId;
if (Summary.UserId.IsEmpty())
{
return Summary;
}
Summary.ReferenceUtc = !FollowUpBrief.ReferenceUtc.IsEmpty()
? FollowUpBrief.ReferenceUtc
: (!CurrentBrief.ReferenceUtc.IsEmpty() ? CurrentBrief.ReferenceUtc : ArchivePolicySummary.ReferenceUtc);
Summary.FocusDeckId = !FollowUpBrief.FocusDeckId.IsEmpty()
? FollowUpBrief.FocusDeckId
: (!CurrentBrief.FocusDeckId.IsEmpty() ? CurrentBrief.FocusDeckId : ArchivePolicySummary.FocusDeckId);
Summary.FocusMethodSegmentId = !FollowUpBrief.FocusMethodSegmentId.IsEmpty()
? FollowUpBrief.FocusMethodSegmentId
: (!CurrentBrief.FocusMethodSegmentId.IsEmpty()
? CurrentBrief.FocusMethodSegmentId
: ArchivePolicySummary.FocusMethodSegmentId);
auto AddBriefEntry = [&Summary](
const FString& EntryId,
const FString& SourceLabel,
const FHyperTwistCoachBrief& Brief,
const bool bRequiresFollowUp,
const bool bRequiresArchiveRetentionTuning)
{
if (Brief.UserId.IsEmpty())
{
return;
}
FHyperTwistCoachSessionQueueEntry Entry;
Entry.EntryId = EntryId;
Entry.SourceLabel = SourceLabel;
Entry.Priority = Brief.Priority;
Entry.PriorityScore = Brief.PriorityScore;
Entry.Headline = Brief.Headline;
Entry.SuggestedMode = Brief.SuggestedMode;
Entry.SuggestedSelectionPolicy = Brief.SuggestedSelectionPolicy;
Entry.FocusDeckId = Brief.FocusDeckId;
Entry.FocusMethodSegmentId = Brief.FocusMethodSegmentId;
Entry.FocusCaseIds = Brief.FocusCaseIds;
Entry.bRequiresFollowUp = bRequiresFollowUp;
Entry.bRequiresArchiveRetentionTuning = bRequiresArchiveRetentionTuning;
Summary.Entries.Add(Entry);
};
AddBriefEntry(
TEXT("coach_queue_primary"),
TEXT("primary-brief"),
CurrentBrief,
false,
false
);
if (FollowUpBrief.UserId == Summary.UserId && FollowUpBrief.FocusCaseIds.Num() > 0)
{
AddBriefEntry(
TEXT("coach_queue_follow_up"),
TEXT("follow-up-brief"),
FollowUpBrief,
true,
false
);
}
if (ArchivePolicySummary.IsStructurallyValid()
&& (ArchivePolicySummary.bNeedsArchiveRetentionTuning
|| ArchivePolicySummary.RetainedCarryForwardCaseCount > 0))
{
FHyperTwistCoachBrief ArchiveBrief;
ArchiveBrief.UserId = Summary.UserId;
ArchiveBrief.ReferenceUtc = Summary.ReferenceUtc;
ArchiveBrief.PriorityScore = FMath::Clamp(
55.0f + ArchivePolicySummary.ArchiveRetentionTuningScore,
20.0f,
100.0f
);
ArchiveBrief.Priority = HyperTwistTrainingCoachLibraryInternal::PriorityFromScore(ArchiveBrief.PriorityScore);
ArchiveBrief.Headline = ArchivePolicySummary.bNeedsArchiveRetentionTuning
? TEXT("Archive retention queue needs attention")
: TEXT("Carry-forward archive queue is still active");
ArchiveBrief.SuggestedMode = ArchivePolicySummary.DominantSuggestedMode;
ArchiveBrief.SuggestedSelectionPolicy = ArchivePolicySummary.DominantSelectionPolicy;
ArchiveBrief.FocusDeckId = ArchivePolicySummary.FocusDeckId;
ArchiveBrief.FocusMethodSegmentId = ArchivePolicySummary.FocusMethodSegmentId;
ArchiveBrief.FocusCaseIds = ArchivePolicySummary.ReopenRiskArchivedCaseIds.Num() > 0
? ArchivePolicySummary.ReopenRiskArchivedCaseIds
: (ArchivePolicySummary.ProvisionalArchivedCaseIds.Num() > 0
? ArchivePolicySummary.ProvisionalArchivedCaseIds
: ArchivePolicySummary.RetainedCarryForwardCaseIds);
AddBriefEntry(
TEXT("coach_queue_archive"),
TEXT("archive-policy"),
ArchiveBrief,
false,
ArchivePolicySummary.bNeedsArchiveRetentionTuning
);
}
Summary.Entries.Sort([](const FHyperTwistCoachSessionQueueEntry& Left, const FHyperTwistCoachSessionQueueEntry& Right)
{
if (!FMath::IsNearlyEqual(Left.PriorityScore, Right.PriorityScore))
{
return Left.PriorityScore > Right.PriorityScore;
}
return Left.EntryId < Right.EntryId;
});
for (int32 Index = 0; Index < Summary.Entries.Num(); ++Index)
{
Summary.Entries[Index].SlotIndex = Index;
Summary.FollowUpEntryCount += Summary.Entries[Index].bRequiresFollowUp ? 1 : 0;
Summary.ArchiveRetentionEntryCount += Summary.Entries[Index].bRequiresArchiveRetentionTuning ? 1 : 0;
Summary.ReviewBacklogEntryCount += Summary.Entries[Index].SourceLabel == TEXT("primary-brief")
&& CurrentBrief.FocusLane == EHyperTwistCoachFocusLane::ReviewBacklog
? 1
: 0;
}
Summary.TotalEntryCount = Summary.Entries.Num();
Summary.PrimaryQueueLabel = Summary.Entries.Num() > 0
? Summary.Entries[0].SourceLabel
: FString();
Summary.NextQueueLabel = Summary.Entries.Num() > 1
? Summary.Entries[1].SourceLabel
: FString();
Summary.bHasImmediateQueue = Summary.TotalEntryCount > 0;
Summary.bNeedsArchiveAwareSequencing =
ArchivePolicySummary.bNeedsArchiveRetentionTuning
|| Summary.ArchiveRetentionEntryCount > 0
|| ArchivePolicySummary.RetainedCarryForwardCaseCount > 0
|| ReviewProgramSummary.bNeedsCoachIntervention;
return Summary;
}

View file

@ -301,6 +301,11 @@ namespace HyperTwistTrainingRepositoryLibraryInternal
return UserId + CaseKeySeparator + PlanId;
}
FString MakeCoachSessionQueueKey(const FString& UserId, const FString& QueueId)
{
return UserId + CaseKeySeparator + QueueId;
}
TArray<FString> DeduplicateCaseIds(const TArray<FString>& CaseIds)
{
TArray<FString> UniqueCaseIds;
@ -475,6 +480,223 @@ namespace HyperTwistTrainingRepositoryLibraryInternal
return NormalizedPlan;
}
bool IsTerminalCoachQueueEntryState(const EHyperTwistTrainingCoachSessionQueueEntryState EntryState)
{
return EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Completed
|| EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Skipped
|| EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Expired;
}
FString MakeCoachSessionQueueEntryFingerprint(
const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry
)
{
const FString CaseFingerprint = FString::Join(Entry.FocusCaseIds, TEXT("|"));
return FString::Printf(
TEXT("%s|%s|%s|%s|%d|%d|%s"),
*Entry.SourceLabel,
*Entry.FocusDeckId,
*Entry.FocusMethodSegmentId,
*Entry.Headline,
static_cast<int32>(Entry.SuggestedMode),
static_cast<int32>(Entry.SuggestedSelectionPolicy),
*CaseFingerprint
);
}
FHyperTwistTrainingCoachSessionQueueState NormalizeCoachSessionQueueState(
const FHyperTwistTrainingCoachSessionQueueState& QueueState
)
{
FHyperTwistTrainingCoachSessionQueueState NormalizedState = QueueState;
if (NormalizedState.QueueId.IsEmpty())
{
NormalizedState.QueueId = TEXT("coach_queue_active");
}
NormalizedState.ReferenceUtc = ResolveReferenceUtc(NormalizedState.ReferenceUtc);
NormalizedState.LastUpdatedAtUtc = ResolveReferenceUtc(
NormalizedState.LastUpdatedAtUtc.IsEmpty()
? NormalizedState.ReferenceUtc
: NormalizedState.LastUpdatedAtUtc
);
TMap<FString, int32> EntryIndexById;
TArray<FHyperTwistTrainingCoachSessionQueueStateEntry> NormalizedEntries;
for (const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : QueueState.Entries)
{
FHyperTwistTrainingCoachSessionQueueStateEntry NormalizedEntry = Entry;
NormalizedEntry.FocusCaseIds = DeduplicateCaseIds(Entry.FocusCaseIds);
if (NormalizedEntry.WorkFingerprint.IsEmpty())
{
NormalizedEntry.WorkFingerprint = MakeCoachSessionQueueEntryFingerprint(NormalizedEntry);
}
NormalizedEntry.bPinned = NormalizedEntry.bPinned
|| NormalizedEntry.bRequiresFollowUp
|| NormalizedEntry.bRequiresArchiveRetentionTuning;
if (NormalizedEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready
|| NormalizedEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress)
{
NormalizedEntry.ScheduledAtUtc = !NormalizedEntry.ScheduledAtUtc.IsEmpty()
? NormalizedEntry.ScheduledAtUtc
: NormalizedState.ReferenceUtc;
}
if (IsTerminalCoachQueueEntryState(NormalizedEntry.EntryState))
{
NormalizedEntry.bCanStartImmediately = false;
}
if (!NormalizedEntry.IsStructurallyValid())
{
continue;
}
if (const int32* ExistingIndex = EntryIndexById.Find(NormalizedEntry.EntryId))
{
NormalizedEntries[*ExistingIndex] = NormalizedEntry;
}
else
{
EntryIndexById.Add(NormalizedEntry.EntryId, NormalizedEntries.Add(NormalizedEntry));
}
}
NormalizedEntries.Sort([](
const FHyperTwistTrainingCoachSessionQueueStateEntry& Left,
const FHyperTwistTrainingCoachSessionQueueStateEntry& Right
)
{
if (Left.SlotIndex != Right.SlotIndex)
{
return Left.SlotIndex < Right.SlotIndex;
}
if (!FMath::IsNearlyEqual(Left.PriorityScore, Right.PriorityScore))
{
return Left.PriorityScore > Right.PriorityScore;
}
return Left.EntryId < Right.EntryId;
});
NormalizedState.ActiveEntryId.Reset();
NormalizedState.ActiveTrainingSessionId.Reset();
NormalizedState.PendingEntryCount = 0;
NormalizedState.ReadyEntryCount = 0;
NormalizedState.InProgressEntryCount = 0;
NormalizedState.CompletedEntryCount = 0;
NormalizedState.SkippedEntryCount = 0;
NormalizedState.ExpiredEntryCount = 0;
bool bHasInProgressEntry = false;
bool bHasReadyEntry = false;
for (int32 EntryIndex = 0; EntryIndex < NormalizedEntries.Num(); ++EntryIndex)
{
FHyperTwistTrainingCoachSessionQueueStateEntry& Entry = NormalizedEntries[EntryIndex];
Entry.SlotIndex = EntryIndex;
if (Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress)
{
if (bHasInProgressEntry)
{
Entry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending;
Entry.SourceTrainingSessionId.Reset();
Entry.StartedAtUtc.Reset();
}
else
{
bHasInProgressEntry = true;
Entry.ScheduledAtUtc = !Entry.ScheduledAtUtc.IsEmpty() ? Entry.ScheduledAtUtc : NormalizedState.ReferenceUtc;
Entry.bCanStartImmediately = false;
NormalizedState.ActiveEntryId = Entry.EntryId;
NormalizedState.ActiveTrainingSessionId = Entry.SourceTrainingSessionId;
}
}
if (Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready)
{
if (bHasInProgressEntry || bHasReadyEntry)
{
Entry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending;
Entry.bCanStartImmediately = false;
}
else
{
bHasReadyEntry = true;
Entry.ScheduledAtUtc = !Entry.ScheduledAtUtc.IsEmpty() ? Entry.ScheduledAtUtc : NormalizedState.ReferenceUtc;
Entry.bCanStartImmediately = true;
NormalizedState.ActiveEntryId = Entry.EntryId;
}
}
switch (Entry.EntryState)
{
case EHyperTwistTrainingCoachSessionQueueEntryState::Pending:
NormalizedState.PendingEntryCount += 1;
break;
case EHyperTwistTrainingCoachSessionQueueEntryState::Ready:
NormalizedState.ReadyEntryCount += 1;
break;
case EHyperTwistTrainingCoachSessionQueueEntryState::InProgress:
NormalizedState.InProgressEntryCount += 1;
break;
case EHyperTwistTrainingCoachSessionQueueEntryState::Completed:
NormalizedState.CompletedEntryCount += 1;
break;
case EHyperTwistTrainingCoachSessionQueueEntryState::Skipped:
NormalizedState.SkippedEntryCount += 1;
break;
case EHyperTwistTrainingCoachSessionQueueEntryState::Expired:
NormalizedState.ExpiredEntryCount += 1;
break;
default:
break;
}
}
if (!bHasInProgressEntry && !bHasReadyEntry)
{
for (FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : NormalizedEntries)
{
if (!IsTerminalCoachQueueEntryState(Entry.EntryState))
{
if (Entry.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::Pending)
{
switch (Entry.EntryState)
{
case EHyperTwistTrainingCoachSessionQueueEntryState::Completed:
NormalizedState.CompletedEntryCount -= 1;
break;
case EHyperTwistTrainingCoachSessionQueueEntryState::Skipped:
NormalizedState.SkippedEntryCount -= 1;
break;
case EHyperTwistTrainingCoachSessionQueueEntryState::Expired:
NormalizedState.ExpiredEntryCount -= 1;
break;
default:
break;
}
}
else
{
NormalizedState.PendingEntryCount -= 1;
}
Entry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Ready;
Entry.ScheduledAtUtc = !Entry.ScheduledAtUtc.IsEmpty() ? Entry.ScheduledAtUtc : NormalizedState.ReferenceUtc;
Entry.bCanStartImmediately = true;
NormalizedState.ReadyEntryCount += 1;
NormalizedState.ActiveEntryId = Entry.EntryId;
break;
}
}
}
NormalizedState.TotalEntryCount = NormalizedEntries.Num();
NormalizedState.bHasImmediateQueue = NormalizedState.ReadyEntryCount > 0
|| NormalizedState.InProgressEntryCount > 0;
NormalizedState.Entries = NormalizedEntries;
return NormalizedState;
}
FHyperTwistTrainingCaseLearnerState DeriveLearnerStateFromReviewHistory(
const FString& UserId,
const FString& DeckId,
@ -2680,6 +2902,29 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRepositoryLibrar
SeenCoachActionPlanKeys.Add(ActionPlanKey);
}
Report.TotalCoachSessionQueueCount = RepositoryState.StoredCoachSessionQueues.Num();
TSet<FString> SeenCoachSessionQueueKeys;
for (const FHyperTwistTrainingCoachSessionQueueState& QueueState : RepositoryState.StoredCoachSessionQueues)
{
if (!QueueState.IsStructurallyValid())
{
Report.InvalidCoachSessionQueueCount += 1;
continue;
}
const FString QueueKey = HyperTwistTrainingRepositoryLibraryInternal::MakeCoachSessionQueueKey(
QueueState.UserId,
QueueState.QueueId
);
if (SeenCoachSessionQueueKeys.Contains(QueueKey))
{
Report.DuplicateCoachSessionQueueCount += 1;
continue;
}
SeenCoachSessionQueueKeys.Add(QueueKey);
}
if (Report.InvalidRunRecordCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
@ -2781,6 +3026,20 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRepositoryLibrar
TEXT("normalize-and-deduplicate-coach-action-plans")
);
}
if (Report.InvalidCoachSessionQueueCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
Report.RepairActions,
TEXT("drop-invalid-coach-session-queues")
);
}
if (Report.DuplicateCoachSessionQueueCount > 0)
{
HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction(
Report.RepairActions,
TEXT("normalize-and-deduplicate-coach-session-queues")
);
}
return Report;
}
@ -3089,6 +3348,60 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::RepairR
return Left.PlanId < Right.PlanId;
});
TMap<FString, int32> CoachSessionQueueIndexByKey;
for (const FHyperTwistTrainingCoachSessionQueueState& QueueState : RepositoryState.StoredCoachSessionQueues)
{
if (!QueueState.IsStructurallyValid())
{
continue;
}
const FHyperTwistTrainingCoachSessionQueueState NormalizedQueueState =
HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState);
if (!NormalizedQueueState.IsStructurallyValid())
{
continue;
}
const FString QueueKey = HyperTwistTrainingRepositoryLibraryInternal::MakeCoachSessionQueueKey(
NormalizedQueueState.UserId,
NormalizedQueueState.QueueId
);
if (const int32* ExistingIndex = CoachSessionQueueIndexByKey.Find(QueueKey))
{
RepairedState.StoredCoachSessionQueues[*ExistingIndex] = NormalizedQueueState;
}
else
{
CoachSessionQueueIndexByKey.Add(
QueueKey,
RepairedState.StoredCoachSessionQueues.Add(NormalizedQueueState)
);
}
}
RepairedState.StoredCoachSessionQueues.Sort([](
const FHyperTwistTrainingCoachSessionQueueState& Left,
const FHyperTwistTrainingCoachSessionQueueState& Right
)
{
if (Left.UserId != Right.UserId)
{
return Left.UserId < Right.UserId;
}
const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(
Left.LastUpdatedAtUtc,
Right.LastUpdatedAtUtc
);
if (UtcCompare != 0)
{
return UtcCompare > 0;
}
return Left.QueueId < Right.QueueId;
});
return RepairedState;
}
@ -4997,6 +5310,311 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertC
return UpdatedRepositoryState;
}
FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::BuildCoachSessionQueueState(
const FHyperTwistCoachSessionQueueSummary& QueueSummary,
const FString& QueueId,
const FString& ReferenceUtc
)
{
FHyperTwistTrainingCoachSessionQueueState QueueState;
if (!QueueSummary.IsStructurallyValid())
{
return QueueState;
}
QueueState.QueueId = QueueId.IsEmpty() ? TEXT("coach_queue_active") : QueueId;
QueueState.UserId = QueueSummary.UserId;
QueueState.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(
ReferenceUtc.IsEmpty() ? QueueSummary.ReferenceUtc : ReferenceUtc
);
QueueState.LastUpdatedAtUtc = QueueState.ReferenceUtc;
QueueState.FocusDeckId = QueueSummary.FocusDeckId;
QueueState.FocusMethodSegmentId = QueueSummary.FocusMethodSegmentId;
QueueState.PrimaryQueueLabel = QueueSummary.PrimaryQueueLabel;
QueueState.NextQueueLabel = QueueSummary.NextQueueLabel;
QueueState.bHasImmediateQueue = QueueSummary.bHasImmediateQueue;
QueueState.bNeedsArchiveAwareSequencing = QueueSummary.bNeedsArchiveAwareSequencing;
for (const FHyperTwistCoachSessionQueueEntry& SummaryEntry : QueueSummary.Entries)
{
FHyperTwistTrainingCoachSessionQueueStateEntry QueueEntry;
QueueEntry.EntryId = SummaryEntry.EntryId;
QueueEntry.SourceLabel = SummaryEntry.SourceLabel;
QueueEntry.SlotIndex = SummaryEntry.SlotIndex;
QueueEntry.PriorityLabel = StaticEnum<EHyperTwistCoachPriority>()->GetNameStringByValue(
static_cast<int64>(SummaryEntry.Priority)
);
QueueEntry.PriorityScore = SummaryEntry.PriorityScore;
QueueEntry.Headline = SummaryEntry.Headline;
QueueEntry.SuggestedMode = SummaryEntry.SuggestedMode;
QueueEntry.SuggestedSelectionPolicy = SummaryEntry.SuggestedSelectionPolicy;
QueueEntry.FocusDeckId = SummaryEntry.FocusDeckId;
QueueEntry.FocusMethodSegmentId = SummaryEntry.FocusMethodSegmentId;
QueueEntry.FocusCaseIds = SummaryEntry.FocusCaseIds;
QueueEntry.bRequiresFollowUp = SummaryEntry.bRequiresFollowUp;
QueueEntry.bRequiresArchiveRetentionTuning = SummaryEntry.bRequiresArchiveRetentionTuning;
QueueEntry.bPinned = QueueEntry.bRequiresFollowUp || QueueEntry.bRequiresArchiveRetentionTuning;
QueueEntry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending;
QueueEntry.WorkFingerprint =
HyperTwistTrainingRepositoryLibraryInternal::MakeCoachSessionQueueEntryFingerprint(QueueEntry);
QueueState.Entries.Add(QueueEntry);
}
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState);
}
FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::ResolveCoachSessionQueueState(
const FHyperTwistTrainingCoachSessionQueueState& ExistingQueueState,
const FHyperTwistCoachSessionQueueSummary& QueueSummary,
const FString& ReferenceUtc
)
{
FHyperTwistTrainingCoachSessionQueueState ResolvedQueueState = BuildCoachSessionQueueState(
QueueSummary,
ExistingQueueState.QueueId,
ReferenceUtc
);
if (!ResolvedQueueState.IsStructurallyValid() || !ExistingQueueState.IsStructurallyValid())
{
return ResolvedQueueState;
}
for (FHyperTwistTrainingCoachSessionQueueStateEntry& QueueEntry : ResolvedQueueState.Entries)
{
const FHyperTwistTrainingCoachSessionQueueStateEntry* ExistingEntry =
ExistingQueueState.Entries.FindByPredicate(
[&QueueEntry](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate)
{
return Candidate.EntryId == QueueEntry.EntryId
&& Candidate.WorkFingerprint == QueueEntry.WorkFingerprint
&& Candidate.IsStructurallyValid();
}
);
if (ExistingEntry == nullptr)
{
continue;
}
QueueEntry.EntryState = ExistingEntry->EntryState;
QueueEntry.SourceTrainingSessionId = ExistingEntry->SourceTrainingSessionId;
QueueEntry.ScheduledAtUtc = ExistingEntry->ScheduledAtUtc;
QueueEntry.StartedAtUtc = ExistingEntry->StartedAtUtc;
QueueEntry.CompletedAtUtc = ExistingEntry->CompletedAtUtc;
QueueEntry.bPinned = QueueEntry.bPinned || ExistingEntry->bPinned;
QueueEntry.bCanStartImmediately = ExistingEntry->bCanStartImmediately;
}
ResolvedQueueState.LastUpdatedAtUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(
ReferenceUtc.IsEmpty() ? ResolvedQueueState.ReferenceUtc : ReferenceUtc
);
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(ResolvedQueueState);
}
FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::StartCoachSessionQueueEntryBySourceLabel(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FString& SourceLabel,
const FString& TrainingSessionId,
const FString& StartedAtUtc
)
{
FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState =
HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState);
if (!UpdatedQueueState.IsStructurallyValid() || SourceLabel.IsEmpty())
{
return UpdatedQueueState;
}
const FString ResolvedStartedAtUtc =
HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(StartedAtUtc);
int32 ActivatedIndex = INDEX_NONE;
for (int32 EntryIndex = 0; EntryIndex < UpdatedQueueState.Entries.Num(); ++EntryIndex)
{
const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry = UpdatedQueueState.Entries[EntryIndex];
if (Entry.SourceLabel == SourceLabel
&& !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Entry.EntryState))
{
ActivatedIndex = EntryIndex;
break;
}
}
if (ActivatedIndex == INDEX_NONE)
{
return UpdatedQueueState;
}
for (FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : UpdatedQueueState.Entries)
{
if (Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress)
{
Entry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending;
Entry.SourceTrainingSessionId.Reset();
Entry.StartedAtUtc.Reset();
Entry.bCanStartImmediately = false;
}
}
FHyperTwistTrainingCoachSessionQueueStateEntry& ActiveEntry =
UpdatedQueueState.Entries[ActivatedIndex];
ActiveEntry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::InProgress;
ActiveEntry.SourceTrainingSessionId = TrainingSessionId;
ActiveEntry.ScheduledAtUtc = !ActiveEntry.ScheduledAtUtc.IsEmpty()
? ActiveEntry.ScheduledAtUtc
: ResolvedStartedAtUtc;
ActiveEntry.StartedAtUtc = ResolvedStartedAtUtc;
ActiveEntry.bCanStartImmediately = false;
UpdatedQueueState.LastUpdatedAtUtc = ResolvedStartedAtUtc;
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState);
}
FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::FinalizeCoachSessionQueueEntry(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FString& TrainingSessionId,
const FString& CompletedAtUtc,
bool bAbortEntry
)
{
FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState =
HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState);
if (!UpdatedQueueState.IsStructurallyValid())
{
return UpdatedQueueState;
}
const FString ResolvedCompletedAtUtc =
HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(
CompletedAtUtc.IsEmpty() ? UpdatedQueueState.ReferenceUtc : CompletedAtUtc
);
FHyperTwistTrainingCoachSessionQueueStateEntry* ActiveEntry =
UpdatedQueueState.Entries.FindByPredicate(
[&TrainingSessionId, &UpdatedQueueState](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate)
{
if (!TrainingSessionId.IsEmpty() && Candidate.SourceTrainingSessionId == TrainingSessionId)
{
return true;
}
return Candidate.EntryId == UpdatedQueueState.ActiveEntryId
&& Candidate.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress;
}
);
if (ActiveEntry == nullptr)
{
return UpdatedQueueState;
}
ActiveEntry->EntryState = bAbortEntry
? EHyperTwistTrainingCoachSessionQueueEntryState::Skipped
: EHyperTwistTrainingCoachSessionQueueEntryState::Completed;
ActiveEntry->CompletedAtUtc = ResolvedCompletedAtUtc;
ActiveEntry->bCanStartImmediately = false;
UpdatedQueueState.ActiveEntryId.Reset();
UpdatedQueueState.ActiveTrainingSessionId.Reset();
UpdatedQueueState.LastUpdatedAtUtc = ResolvedCompletedAtUtc;
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState);
}
TArray<FHyperTwistTrainingCoachSessionQueueState> UHyperTwistTrainingRepositoryLibrary::ListCoachSessionQueueStatesForUser(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId
)
{
TArray<FHyperTwistTrainingCoachSessionQueueState> QueueStates;
if (UserId.IsEmpty())
{
return QueueStates;
}
for (const FHyperTwistTrainingCoachSessionQueueState& QueueState : RepositoryState.StoredCoachSessionQueues)
{
if (QueueState.UserId == UserId && QueueState.IsStructurallyValid())
{
QueueStates.Add(QueueState);
}
}
QueueStates.Sort([](
const FHyperTwistTrainingCoachSessionQueueState& Left,
const FHyperTwistTrainingCoachSessionQueueState& Right
)
{
const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(
Left.LastUpdatedAtUtc,
Right.LastUpdatedAtUtc
);
if (UtcCompare != 0)
{
return UtcCompare > 0;
}
return Left.QueueId < Right.QueueId;
});
return QueueStates;
}
bool UHyperTwistTrainingRepositoryLibrary::TryGetCoachSessionQueueState(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId,
const FString& QueueId,
FHyperTwistTrainingCoachSessionQueueState& OutQueueState
)
{
OutQueueState = FHyperTwistTrainingCoachSessionQueueState();
if (UserId.IsEmpty() || QueueId.IsEmpty())
{
return false;
}
const FHyperTwistTrainingCoachSessionQueueState* MatchingQueueState =
RepositoryState.StoredCoachSessionQueues.FindByPredicate(
[&UserId, &QueueId](const FHyperTwistTrainingCoachSessionQueueState& Candidate)
{
return Candidate.UserId == UserId
&& Candidate.QueueId == QueueId
&& Candidate.IsStructurallyValid();
}
);
if (MatchingQueueState == nullptr)
{
return false;
}
OutQueueState = *MatchingQueueState;
return true;
}
FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FHyperTwistTrainingCoachSessionQueueState& QueueState
)
{
FHyperTwistTrainingRepositoryState UpdatedRepositoryState = RepositoryState;
const FHyperTwistTrainingCoachSessionQueueState NormalizedQueueState =
HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState);
if (!NormalizedQueueState.IsStructurallyValid())
{
return UpdatedRepositoryState;
}
FHyperTwistTrainingCoachSessionQueueState* ExistingQueueState =
UpdatedRepositoryState.StoredCoachSessionQueues.FindByPredicate(
[&NormalizedQueueState](const FHyperTwistTrainingCoachSessionQueueState& Candidate)
{
return Candidate.UserId == NormalizedQueueState.UserId
&& Candidate.QueueId == NormalizedQueueState.QueueId;
}
);
if (ExistingQueueState != nullptr)
{
*ExistingQueueState = NormalizedQueueState;
}
else
{
UpdatedRepositoryState.StoredCoachSessionQueues.Add(NormalizedQueueState);
}
return UpdatedRepositoryState;
}
FHyperTwistTrainingCoachActionPlanSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanSummary(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId,

View file

@ -358,6 +358,29 @@ FHyperTwistCoachBrief UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachF
return FHyperTwistCoachBrief();
}
FHyperTwistCoachSessionQueueSummary UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachSessionQueueSummary(
UObject* WorldContextObject)
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject))
{
return TrainingSubsystem->GetActiveCoachSessionQueueSummary();
}
return FHyperTwistCoachSessionQueueSummary();
}
FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachSessionQueueState(
UObject* WorldContextObject
)
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject))
{
return TrainingSubsystem->GetActiveCoachSessionQueueState();
}
return FHyperTwistTrainingCoachSessionQueueState();
}
TArray<FHyperTwistTrainingMethodSegment> UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingLearnerMethodSegments(UObject* WorldContextObject)
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject))
@ -547,6 +570,22 @@ bool UHyperTwistTrainingRuntimeLibrary::TryGetStoredTrainingCoachActionPlan(
return false;
}
bool UHyperTwistTrainingRuntimeLibrary::TryGetStoredTrainingCoachSessionQueueState(
UObject* WorldContextObject,
const FString& UserId,
const FString& QueueId,
FHyperTwistTrainingCoachSessionQueueState& OutQueueState
)
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject))
{
return TrainingSubsystem->TryGetStoredCoachSessionQueueState(UserId, QueueId, OutQueueState);
}
OutQueueState = FHyperTwistTrainingCoachSessionQueueState();
return false;
}
bool UHyperTwistTrainingRuntimeLibrary::TryGetStoredTrainingSessionTemplate(
UObject* WorldContextObject,
const FString& UserId,
@ -633,6 +672,19 @@ bool UHyperTwistTrainingRuntimeLibrary::SaveMethodDrillBatch(
return false;
}
bool UHyperTwistTrainingRuntimeLibrary::SaveCoachSessionQueueState(
UObject* WorldContextObject,
const FHyperTwistTrainingCoachSessionQueueState& QueueState
)
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject))
{
return TrainingSubsystem->SaveCoachSessionQueueState(QueueState);
}
return false;
}
bool UHyperTwistTrainingRuntimeLibrary::RemoveMethodDrillBatch(
UObject* WorldContextObject,
const FString& UserId,

View file

@ -8,6 +8,11 @@
#include "HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h"
namespace HyperTwistTrainingSubsystemInternal
{
const TCHAR* ActiveCoachSessionQueueId = TEXT("coach_queue_active");
}
bool UHyperTwistTrainingSubsystem::HasActiveRun() const
{
return bHasActiveRun && ActiveRunState.IsStructurallyValid();
@ -158,6 +163,16 @@ FHyperTwistCoachBrief UHyperTwistTrainingSubsystem::GetActiveCoachFollowUpBrief(
return ActiveCoachFollowUpBrief;
}
FHyperTwistCoachSessionQueueSummary UHyperTwistTrainingSubsystem::GetActiveCoachSessionQueueSummary() const
{
return ActiveCoachSessionQueueSummary;
}
FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingSubsystem::GetActiveCoachSessionQueueState() const
{
return ActiveCoachSessionQueueState;
}
TArray<FHyperTwistTrainingMethodSegment> UHyperTwistTrainingSubsystem::GetActiveLearnerMethodSegments() const
{
return ActiveLearnerMethodSegments;
@ -293,6 +308,20 @@ bool UHyperTwistTrainingSubsystem::TryGetStoredCoachActionPlan(
);
}
bool UHyperTwistTrainingSubsystem::TryGetStoredCoachSessionQueueState(
const FString& UserId,
const FString& QueueId,
FHyperTwistTrainingCoachSessionQueueState& OutQueueState
) const
{
return UHyperTwistTrainingRepositoryLibrary::TryGetCoachSessionQueueState(
TrainingRepositoryState,
UserId,
QueueId,
OutQueueState
);
}
bool UHyperTwistTrainingSubsystem::TryGetStoredTrainingSessionTemplate(
const FString& UserId,
const FString& TemplateId,
@ -394,6 +423,23 @@ bool UHyperTwistTrainingSubsystem::SaveMethodDrillBatch(
return true;
}
bool UHyperTwistTrainingSubsystem::SaveCoachSessionQueueState(
const FHyperTwistTrainingCoachSessionQueueState& QueueState
)
{
if (!QueueState.IsStructurallyValid())
{
return false;
}
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState(
TrainingRepositoryState,
QueueState
);
RefreshRepositoryViews();
return true;
}
bool UHyperTwistTrainingSubsystem::RemoveMethodDrillBatch(
const FString& UserId,
const FString& BatchId
@ -819,6 +865,7 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartCoachRecommendedR
}
const FString UserId = ActiveRunState.Session.UserId.IsEmpty() ? TEXT("local-user") : ActiveRunState.Session.UserId;
const FHyperTwistTrainingCoachSessionQueueState QueueStateBeforeStart = ActiveCoachSessionQueueState;
const FHyperTwistTrainingCoachActionPlan DraftActionPlan =
UHyperTwistTrainingRepositoryLibrary::BuildCoachActionPlan(
ActiveCoachBrief,
@ -845,6 +892,24 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartCoachRecommendedR
TrainingRepositoryState,
ActiveCoachActionPlan
);
if (QueueStateBeforeStart.IsStructurallyValid())
{
const FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState =
UHyperTwistTrainingRepositoryLibrary::StartCoachSessionQueueEntryBySourceLabel(
QueueStateBeforeStart,
TEXT("primary-brief"),
RunState.Session.TrainingSessionId,
RunState.Session.StartedAtUtc
);
if (UpdatedQueueState.IsStructurallyValid())
{
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState(
TrainingRepositoryState,
UpdatedQueueState
);
ActiveCoachSessionQueueState = UpdatedQueueState;
}
}
RefreshRepositoryViews();
return RunState;
}
@ -861,6 +926,7 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartCoachFollowUpRun(
}
const FHyperTwistCoachBrief FollowUpBrief = ActiveCoachFollowUpBrief;
const FHyperTwistTrainingCoachSessionQueueState QueueStateBeforeStart = ActiveCoachSessionQueueState;
const FHyperTwistTrainingCoachActionPlan DraftActionPlan =
UHyperTwistTrainingRepositoryLibrary::BuildCoachActionPlan(
FollowUpBrief,
@ -887,6 +953,24 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartCoachFollowUpRun(
TrainingRepositoryState,
ActiveCoachActionPlan
);
if (QueueStateBeforeStart.IsStructurallyValid())
{
const FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState =
UHyperTwistTrainingRepositoryLibrary::StartCoachSessionQueueEntryBySourceLabel(
QueueStateBeforeStart,
TEXT("follow-up-brief"),
RunState.Session.TrainingSessionId,
RunState.Session.StartedAtUtc
);
if (UpdatedQueueState.IsStructurallyValid())
{
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState(
TrainingRepositoryState,
UpdatedQueueState
);
ActiveCoachSessionQueueState = UpdatedQueueState;
}
}
RefreshRepositoryViews();
return RunState;
}
@ -1134,6 +1218,22 @@ FHyperTwistTrainingSession UHyperTwistTrainingSubsystem::CompleteActiveRun(bool
ActiveCoachActionPlan
);
}
if (ActiveCoachSessionQueueState.IsStructurallyValid())
{
ActiveCoachSessionQueueState = UHyperTwistTrainingRepositoryLibrary::FinalizeCoachSessionQueueEntry(
ActiveCoachSessionQueueState,
ActiveRunState.Session.TrainingSessionId,
ActiveRunState.Session.EndedAtUtc,
bAbortSession
);
if (ActiveCoachSessionQueueState.IsStructurallyValid())
{
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState(
TrainingRepositoryState,
ActiveCoachSessionQueueState
);
}
}
RefreshRepositoryViews();
return ActiveRunState.Session;
}
@ -1213,6 +1313,8 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews()
ActiveCoachCarryForwardPolicySummary = FHyperTwistTrainingCoachCarryForwardPolicySummary();
ActiveCoachArchivePolicySummary = FHyperTwistTrainingCoachArchivePolicySummary();
ActiveCoachFollowUpBrief = FHyperTwistCoachBrief();
ActiveCoachSessionQueueSummary = FHyperTwistCoachSessionQueueSummary();
ActiveCoachSessionQueueState = FHyperTwistTrainingCoachSessionQueueState();
ActiveLearnerMethodSegments.Reset();
ActiveLearnerPresets.Reset();
ActiveTrainingSessionTemplates.Reset();
@ -1382,6 +1484,44 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews()
ActiveCoachActionPlanOutcomeSummary
)
: FHyperTwistCoachBrief();
ActiveCoachSessionQueueSummary = UHyperTwistTrainingCoachLibrary::DeriveCoachSessionQueueSummary(
ActiveCoachBrief,
ActiveCoachFollowUpBrief,
ActiveCoachArchivePolicySummary,
ActiveReviewProgramSummary
);
if (ActiveCoachSessionQueueSummary.IsStructurallyValid())
{
FHyperTwistTrainingCoachSessionQueueState StoredQueueState;
const bool bHasStoredQueueState = UHyperTwistTrainingRepositoryLibrary::TryGetCoachSessionQueueState(
TrainingRepositoryState,
ActiveCoachSessionQueueSummary.UserId,
HyperTwistTrainingSubsystemInternal::ActiveCoachSessionQueueId,
StoredQueueState
);
ActiveCoachSessionQueueState = bHasStoredQueueState
? UHyperTwistTrainingRepositoryLibrary::ResolveCoachSessionQueueState(
StoredQueueState,
ActiveCoachSessionQueueSummary,
ActiveLearnerDeckStateSummary.ReferenceUtc
)
: UHyperTwistTrainingRepositoryLibrary::BuildCoachSessionQueueState(
ActiveCoachSessionQueueSummary,
HyperTwistTrainingSubsystemInternal::ActiveCoachSessionQueueId,
ActiveLearnerDeckStateSummary.ReferenceUtc
);
if (ActiveCoachSessionQueueState.IsStructurallyValid())
{
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState(
TrainingRepositoryState,
ActiveCoachSessionQueueState
);
}
}
else
{
ActiveCoachSessionQueueState = FHyperTwistTrainingCoachSessionQueueState();
}
}
TArray<FHyperTwistTrainingSessionTemplate> UHyperTwistTrainingSubsystem::BuildTrainingSessionTemplatesForUser(

View file

@ -206,6 +206,12 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
static FHyperTwistCoachBrief MakeSampleTrainingCoachFollowUpBrief();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
static FHyperTwistCoachSessionQueueSummary MakeSampleTrainingCoachSessionQueueSummary();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
static FHyperTwistTrainingCoachSessionQueueState MakeSampleTrainingCoachSessionQueueState();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Validation")
static bool IsPuzzleStateStructurallyValid(const FHyperTwistPuzzleState& State);

View file

@ -142,6 +142,101 @@ struct FHyperTwistCoachBrief
bool bPreferRecognitionReplayReview = false;
};
USTRUCT(BlueprintType)
struct FHyperTwistCoachSessionQueueEntry
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString EntryId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString SourceLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 SlotIndex = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
EHyperTwistCoachPriority Priority = EHyperTwistCoachPriority::Informational;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
float PriorityScore = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString Headline;
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")
bool bRequiresFollowUp = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bRequiresArchiveRetentionTuning = false;
};
USTRUCT(BlueprintType)
struct FHyperTwistCoachSessionQueueSummary
{
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 PrimaryQueueLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString NextQueueLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistCoachSessionQueueEntry> Entries;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 FollowUpEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ArchiveRetentionEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ReviewBacklogEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasImmediateQueue = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bNeedsArchiveAwareSequencing = false;
bool IsStructurallyValid() const
{
return !UserId.IsEmpty() && Entries.Num() > 0;
}
};
UCLASS()
class UNREALHYPERTWIST_API UHyperTwistTrainingCoachLibrary : public UBlueprintFunctionLibrary
{
@ -178,4 +273,12 @@ public:
const FHyperTwistTrainingCoachActionPlan& ActionPlan,
const FHyperTwistTrainingCoachActionPlanOutcomeSummary& OutcomeSummary
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
static FHyperTwistCoachSessionQueueSummary DeriveCoachSessionQueueSummary(
const FHyperTwistCoachBrief& CurrentBrief,
const FHyperTwistCoachBrief& FollowUpBrief,
const FHyperTwistTrainingCoachArchivePolicySummary& ArchivePolicySummary,
const FHyperTwistTrainingReviewProgramSummary& ReviewProgramSummary
);
};

View file

@ -347,6 +347,56 @@ public:
const FHyperTwistTrainingCoachActionPlan& ActionPlan
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach")
static FHyperTwistTrainingCoachSessionQueueState BuildCoachSessionQueueState(
const FHyperTwistCoachSessionQueueSummary& QueueSummary,
const FString& QueueId,
const FString& ReferenceUtc
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach")
static FHyperTwistTrainingCoachSessionQueueState ResolveCoachSessionQueueState(
const FHyperTwistTrainingCoachSessionQueueState& ExistingQueueState,
const FHyperTwistCoachSessionQueueSummary& QueueSummary,
const FString& ReferenceUtc
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach")
static FHyperTwistTrainingCoachSessionQueueState StartCoachSessionQueueEntryBySourceLabel(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FString& SourceLabel,
const FString& TrainingSessionId,
const FString& StartedAtUtc
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach")
static FHyperTwistTrainingCoachSessionQueueState FinalizeCoachSessionQueueEntry(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FString& TrainingSessionId,
const FString& CompletedAtUtc,
bool bAbortEntry
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach")
static TArray<FHyperTwistTrainingCoachSessionQueueState> ListCoachSessionQueueStatesForUser(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Repository|Coach")
static bool TryGetCoachSessionQueueState(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FString& UserId,
const FString& QueueId,
FHyperTwistTrainingCoachSessionQueueState& OutQueueState
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach")
static FHyperTwistTrainingRepositoryState UpsertCoachSessionQueueState(
const FHyperTwistTrainingRepositoryState& RepositoryState,
const FHyperTwistTrainingCoachSessionQueueState& QueueState
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach")
static FHyperTwistTrainingCoachActionPlanSummary DeriveCoachActionPlanSummary(
const FHyperTwistTrainingRepositoryState& RepositoryState,

View file

@ -117,6 +117,14 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject"))
static FHyperTwistCoachBrief GetActiveTrainingCoachFollowUpBrief(UObject* WorldContextObject);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject"))
static FHyperTwistCoachSessionQueueSummary GetActiveTrainingCoachSessionQueueSummary(UObject* WorldContextObject);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject"))
static FHyperTwistTrainingCoachSessionQueueState GetActiveTrainingCoachSessionQueueState(
UObject* WorldContextObject
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject"))
static TArray<FHyperTwistTrainingMethodSegment> GetActiveTrainingLearnerMethodSegments(UObject* WorldContextObject);
@ -197,6 +205,14 @@ public:
FHyperTwistTrainingCoachActionPlan& OutActionPlan
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject"))
static bool TryGetStoredTrainingCoachSessionQueueState(
UObject* WorldContextObject,
const FString& UserId,
const FString& QueueId,
FHyperTwistTrainingCoachSessionQueueState& OutQueueState
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject"))
static bool TryGetStoredTrainingSessionTemplate(
UObject* WorldContextObject,
@ -239,6 +255,12 @@ public:
const FHyperTwistTrainingMethodDrillBatch& DrillBatch
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject"))
static bool SaveCoachSessionQueueState(
UObject* WorldContextObject,
const FHyperTwistTrainingCoachSessionQueueState& QueueState
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject"))
static bool RemoveMethodDrillBatch(
UObject* WorldContextObject,

View file

@ -104,6 +104,12 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
FHyperTwistCoachBrief GetActiveCoachFollowUpBrief() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
FHyperTwistCoachSessionQueueSummary GetActiveCoachSessionQueueSummary() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingCoachSessionQueueState GetActiveCoachSessionQueueState() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
TArray<FHyperTwistTrainingMethodSegment> GetActiveLearnerMethodSegments() const;
@ -176,6 +182,13 @@ public:
FHyperTwistTrainingCoachActionPlan& OutActionPlan
) const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
bool TryGetStoredCoachSessionQueueState(
const FString& UserId,
const FString& QueueId,
FHyperTwistTrainingCoachSessionQueueState& OutQueueState
) const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training")
bool TryGetStoredTrainingSessionTemplate(
const FString& UserId,
@ -206,6 +219,9 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill")
bool SaveMethodDrillBatch(const FHyperTwistTrainingMethodDrillBatch& DrillBatch);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
bool SaveCoachSessionQueueState(const FHyperTwistTrainingCoachSessionQueueState& QueueState);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill")
bool RemoveMethodDrillBatch(
const FString& UserId,
@ -453,6 +469,12 @@ private:
UPROPERTY()
FHyperTwistCoachBrief ActiveCoachFollowUpBrief;
UPROPERTY()
FHyperTwistCoachSessionQueueSummary ActiveCoachSessionQueueSummary;
UPROPERTY()
FHyperTwistTrainingCoachSessionQueueState ActiveCoachSessionQueueState;
UPROPERTY()
TArray<FHyperTwistTrainingMethodSegment> ActiveLearnerMethodSegments;

View file

@ -2718,6 +2718,17 @@ enum class EHyperTwistTrainingCoachActionPlanState : uint8
Aborted UMETA(DisplayName = "Aborted")
};
UENUM(BlueprintType)
enum class EHyperTwistTrainingCoachSessionQueueEntryState : uint8
{
Pending UMETA(DisplayName = "Pending"),
Ready UMETA(DisplayName = "Ready"),
InProgress UMETA(DisplayName = "In Progress"),
Completed UMETA(DisplayName = "Completed"),
Skipped UMETA(DisplayName = "Skipped"),
Expired UMETA(DisplayName = "Expired")
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCoachActionPlanEntry
{
@ -2859,6 +2870,152 @@ struct FHyperTwistTrainingCoachActionPlan
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCoachSessionQueueStateEntry
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString EntryId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString SourceLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString WorkFingerprint;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString SourceTrainingSessionId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 SlotIndex = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
EHyperTwistTrainingCoachSessionQueueEntryState EntryState =
EHyperTwistTrainingCoachSessionQueueEntryState::Pending;
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")
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")
FString ScheduledAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString StartedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString CompletedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bRequiresFollowUp = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bRequiresArchiveRetentionTuning = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bPinned = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bCanStartImmediately = false;
bool IsStructurallyValid() const
{
return !EntryId.IsEmpty() && !Headline.IsEmpty() && !WorkFingerprint.IsEmpty();
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCoachSessionQueueState
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString QueueId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString UserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ReferenceUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString LastUpdatedAtUtc;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString FocusDeckId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString FocusMethodSegmentId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString PrimaryQueueLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString NextQueueLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ActiveEntryId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ActiveTrainingSessionId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingCoachSessionQueueStateEntry> Entries;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 PendingEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ReadyEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 InProgressEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 CompletedEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 SkippedEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ExpiredEntryCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasImmediateQueue = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bNeedsArchiveAwareSequencing = false;
bool IsStructurallyValid() const
{
return !QueueId.IsEmpty() && !UserId.IsEmpty() && Entries.Num() > 0;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCoachActionPlanSummary
{
@ -3524,6 +3681,9 @@ struct FHyperTwistTrainingRepositoryState
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingCoachActionPlan> StoredCoachActionPlans;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FHyperTwistTrainingCoachSessionQueueState> StoredCoachSessionQueues;
};
USTRUCT(BlueprintType)
@ -3594,6 +3754,15 @@ struct FHyperTwistTrainingRepositoryIntegrityReport
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DuplicateCoachActionPlanCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalCoachSessionQueueCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 InvalidCoachSessionQueueCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DuplicateCoachSessionQueueCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
TArray<FString> RepairActions;
@ -3612,6 +3781,8 @@ struct FHyperTwistTrainingRepositoryIntegrityReport
|| InvalidMethodDrillBatchCount > 0
|| DuplicateMethodDrillBatchCount > 0
|| InvalidCoachActionPlanCount > 0
|| DuplicateCoachActionPlanCount > 0;
|| DuplicateCoachActionPlanCount > 0
|| InvalidCoachSessionQueueCount > 0
|| DuplicateCoachSessionQueueCount > 0;
}
};